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:
commit
95756778d8
26 files changed
+4697
No files matched your search
@@ -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 {} },
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user