Express-App mit JSON-Speicher unter VCH_DATA_DIR, Monatskalender mit Fotos, Personen mit Rollen und Teilzeit-Freitagen, Abwesenheiten, Feiertage je Bundesland, faire Zufallsauslosung mit Fixieren und Neu-Auslosen. Nextcloud: CalDAV-Discovery, Zielkalender wählbar, Termine je Werktag schreiben und aktualisieren, Abwesenheiten aus Kalender importieren, Personen samt Fotos aus dem Systemadressbuch übernehmen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
64 lines
2.1 KiB
JavaScript
64 lines
2.1 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const DEFAULT_STATE = () => ({
|
|
persons: [],
|
|
absences: [],
|
|
holidays: [],
|
|
assignments: {},
|
|
exported: {},
|
|
settings: {
|
|
meetingTime: '09:30',
|
|
durationMinutes: 15,
|
|
bundesland: 'BY',
|
|
calendarDisplayName: 'Morning Meeting',
|
|
inviteAttendees: false,
|
|
historyWindowDays: 60,
|
|
holidaysSeeded: false,
|
|
targetCalendarHref: '',
|
|
targetCalendarName: '',
|
|
absenceCalendarHref: '',
|
|
absenceCalendarName: '',
|
|
},
|
|
});
|
|
|
|
/** Bildtyp anhand der ersten Bytes erkennen. */
|
|
export function imageMime(buffer) {
|
|
if (!buffer || buffer.length < 12) return null;
|
|
if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg';
|
|
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return 'image/png';
|
|
if (buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') return 'image/webp';
|
|
if (buffer.subarray(0, 4).toString('ascii') === 'GIF8') return 'image/gif';
|
|
return null;
|
|
}
|
|
|
|
/** Einfacher JSON-Speicher mit atomarem Schreiben. Fotos liegen daneben im Ordner photos/. */
|
|
export function createStore(dataDir) {
|
|
const file = path.join(dataDir, 'rotatory.json');
|
|
const photoDir = path.join(dataDir, 'photos');
|
|
fs.mkdirSync(photoDir, { recursive: true });
|
|
|
|
let state = DEFAULT_STATE();
|
|
if (fs.existsSync(file)) {
|
|
const loaded = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
state = { ...state, ...loaded, settings: { ...state.settings, ...(loaded.settings || {}) } };
|
|
}
|
|
|
|
function save() {
|
|
const tmp = file + '.tmp';
|
|
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
fs.renameSync(tmp, file);
|
|
}
|
|
|
|
const photoPath = (id) => path.join(photoDir, `${id}.jpg`);
|
|
return {
|
|
get state() { return state; },
|
|
save,
|
|
photoPath,
|
|
hasPhoto: (id) => fs.existsSync(photoPath(id)),
|
|
readPhoto: (id) => (fs.existsSync(photoPath(id)) ? fs.readFileSync(photoPath(id)) : null),
|
|
savePhoto: (id, buffer) => fs.writeFileSync(photoPath(id), buffer),
|
|
deletePhoto: (id) => { try { fs.unlinkSync(photoPath(id)); } catch {} },
|
|
};
|
|
}
|