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

+51
View File
@@ -0,0 +1,51 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildEventLines, buildCalendar, escapeText, foldLine, eventUid } from '../server/ics.js';
test('escapeText maskiert Sonderzeichen', () => {
assert.equal(escapeText('a,b;c\\d\ne'), 'a\\,b\\;c\\\\d\\ne');
});
test('foldLine faltet lange Zeilen nach 75 Oktetten', () => {
const long = 'X'.repeat(200);
const folded = foldLine(long);
const lines = folded.split('\r\n');
assert.ok(lines.length > 1);
assert.ok(lines.every((l) => Buffer.byteLength(l) <= 75));
assert.ok(lines.slice(1).every((l) => l.startsWith(' ')));
assert.equal(lines.map((l, i) => (i ? l.slice(1) : l)).join(''), long);
// Umlaute werden nicht zerschnitten
const umlaut = foldLine('Ä'.repeat(100));
assert.equal(umlaut.split('\r\n').map((l, i) => (i ? l.slice(1) : l)).join(''), 'Ä'.repeat(100));
});
test('Termin mit Uhrzeit und Zeitzone', () => {
const lines = buildEventLines({ uid: 'u1', date: '2026-10-05', summary: 'Morning Meeting: A / B', time: '09:00', durationMinutes: 15, sequence: 2, now: new Date('2026-09-15T10:00:00Z') });
assert.ok(lines.includes('DTSTART;TZID=Europe/Berlin:20261005T090000'));
assert.ok(lines.includes('DTEND;TZID=Europe/Berlin:20261005T091500'));
assert.ok(lines.includes('SEQUENCE:2'));
assert.ok(lines.includes('DTSTAMP:20260915T100000Z'));
});
test('ganztägiger Termin ohne Uhrzeit', () => {
const lines = buildEventLines({ uid: 'u1', date: '2026-10-30', summary: 'x', time: '' });
assert.ok(lines.includes('DTSTART;VALUE=DATE:20261030'));
assert.ok(lines.includes('DTEND;VALUE=DATE:20261031'));
});
test('Kalender ist wohlgeformt, ohne METHOD für CalDAV, mit ORGANIZER vor Teilnehmern', () => {
const ev = buildEventLines({ uid: eventUid('2026-10-05'), date: '2026-10-05', summary: 'S', time: '09:00', attendees: [{ name: 'Max Mustermann', email: 'max@example.de' }], organizer: { name: 'Rotatory', email: 'team@example.de' } });
const ics = buildCalendar([ev]);
assert.ok(!ics.includes('METHOD:'), 'CalDAV-Objekte dürfen keine METHOD tragen');
assert.ok(buildCalendar([ev], { method: 'PUBLISH' }).includes('METHOD:PUBLISH'));
assert.ok(ics.includes('ORGANIZER;CN="Rotatory":mailto:team@example.de'));
assert.ok(ics.indexOf('ORGANIZER') < ics.indexOf('ATTENDEE'));
const noOrganizer = buildCalendar([buildEventLines({ uid: 'x', date: '2026-10-05', summary: 'S', attendees: [{ name: 'A', email: 'a@example.de' }] })]);
assert.ok(!noOrganizer.includes('ATTENDEE'), 'ohne ORGANIZER keine Teilnehmer');
assert.ok(ics.startsWith('BEGIN:VCALENDAR\r\n'));
assert.ok(ics.endsWith('END:VCALENDAR\r\n'));
assert.ok(ics.includes('BEGIN:VTIMEZONE'));
assert.ok(ics.includes('UID:rotatory-2026-10-05@rotatory.local'));
assert.ok(ics.includes('ATTENDEE;CN="Max Mustermann"'));
assert.ok(ics.split('\r\n').every((l) => Buffer.byteLength(l) <= 75));
});