Initial commit: year overview app for Google Calendar ICS feeds

Local Node app that fetches a Google Calendar ICS URL server-side (to avoid
browser CORS restrictions) and renders a two-year overview with each day
split into morning/afternoon/evening segments, colored by emoji found at
the start of event titles (configured in tags.json).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 11:26:39 +02:00
co-authored by Claude Sonnet 5
commit 74284ada97
10 changed files with 800 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
'use strict';
const assert = require('node:assert/strict');
const { buildOverview } = require('./days.js');
const ICS = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//test//test//EN
BEGIN:VEVENT
UID:standup-1@test
DTSTAMP:20260101T000000Z
DTSTART;TZID=UTC:20260105T090000
DTEND;TZID=UTC:20260105T093000
SUMMARY:Standup
RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=5
EXDATE;TZID=UTC:20260119T090000
END:VEVENT
BEGIN:VEVENT
UID:standup-1@test
RECURRENCE-ID;TZID=UTC:20260126T090000
DTSTAMP:20260101T000000Z
DTSTART;TZID=UTC:20260127T140000
DTEND;TZID=UTC:20260127T143000
SUMMARY:Standup (moved)
END:VEVENT
BEGIN:VEVENT
UID:trip-1@test
DTSTAMP:20260101T000000Z
DTSTART;VALUE=DATE:20260110
DTEND;VALUE=DATE:20260112
SUMMARY:💜 Weekend trip
END:VEVENT
BEGIN:VEVENT
UID:flight-1@test
DTSTAMP:20260101T000000Z
DTSTART;TZID=UTC:20260106T100000
DTEND;TZID=UTC:20260106T103000
SUMMARY:✈️ Flight to Spain
END:VEVENT
BEGIN:VEVENT
UID:party-1@test
DTSTAMP:20260101T000000Z
DTSTART;TZID=UTC:20260108T200000
DTEND;TZID=UTC:20260108T220000
SUMMARY:🎉 Party
END:VEVENT
BEGIN:VEVENT
UID:combo-1@test
DTSTAMP:20260101T000000Z
DTSTART;TZID=UTC:20260115T090000
DTEND;TZID=UTC:20260115T093000
SUMMARY:✈️💜 Trip with Tessa
END:VEVENT
BEGIN:VEVENT
UID:combo-2@test
DTSTAMP:20260101T000000Z
DTSTART;VALUE=DATE:20260122
DTEND;VALUE=DATE:20260123
SUMMARY:✈️ 💜 Madeira
END:VEVENT
END:VCALENDAR
`;
const tagsConfig = {
'💜': { label: 'Partner Tessa', color: 'magenta' },
'✈️': { label: 'Holidays', color: 'aqua' },
};
const overview = buildOverview(ICS, tagsConfig, '2026-01-01', '2026-01-31');
// plain weekly recurrence: morning segment, untagged -> muted
assert.deepEqual(overview['2026-01-05'].segments, ['muted', null, null]);
assert.match(overview['2026-01-05'].tooltip, /09:00 Standup/);
// EXDATE removes this occurrence entirely (nothing else touches this day)
assert.equal(overview['2026-01-19'], undefined);
// RECURRENCE-ID moved the Jan 26 occurrence away
assert.equal(overview['2026-01-26'], undefined);
// ...to Jan 27, 14:00 -> afternoon segment
assert.deepEqual(overview['2026-01-27'].segments, [null, 'muted', null]);
assert.match(overview['2026-01-27'].tooltip, /14:00 Standup \(moved\)/);
// all-day multi-day event with a known leading emoji lights every segment, both spanned days
assert.deepEqual(overview['2026-01-10'].segments, ['magenta', 'magenta', 'magenta']);
assert.deepEqual(overview['2026-01-11'].segments, ['magenta', 'magenta', 'magenta']);
assert.match(overview['2026-01-10'].tooltip, /All day 💜 Weekend trip/);
// DTEND is exclusive: Jan 12 is NOT part of the trip, but it IS a Monday standup
assert.deepEqual(overview['2026-01-12'].segments, ['muted', null, null]);
// leading emoji that IS in tagsConfig takes that entry's color, not re-prefixed in tooltip
assert.deepEqual(overview['2026-01-06'].segments, ['aqua', null, null]);
assert.equal(overview['2026-01-06'].tooltip, '10:00 ✈️ Flight to Spain');
// leading emoji NOT in tagsConfig still stands out (not muted) via the fallback color
assert.deepEqual(overview['2026-01-08'].segments, [null, null, 'blue']);
assert.equal(overview['2026-01-08'].tooltip, '20:00 🎉 Party');
// two consecutive leading emoji (no space between) split the segment into both colors
assert.deepEqual(overview['2026-01-15'].segments, [['aqua', 'magenta'], null, null]);
assert.equal(overview['2026-01-15'].tooltip, '09:00 ✈️💜 Trip with Tessa');
// single-emoji case (Jan 6, asserted above) still resolves to a plain string, not an array —
// the split path only triggers for a genuine two-emoji run.
assert.equal(Array.isArray(overview['2026-01-06'].segments[0]), false);
// two leading emoji WITH a space between them (how this calendar actually types combos) still split
assert.deepEqual(overview['2026-01-22'].segments, [['aqua', 'magenta'], ['aqua', 'magenta'], ['aqua', 'magenta']]);
assert.equal(overview['2026-01-22'].tooltip, 'All day ✈️ 💜 Madeira');
console.log('all assertions passed');