Rotatory anlegen: Morning-Meeting-Moderation auslosen und nach Nextcloud schreiben

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>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-09-16 06:39:37 +00:00
commit 95756778d8
26 files changed
+4697

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
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 {} },
};
}