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
+74
View File
@@ -0,0 +1,74 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A single-user local web app: paste a Google Calendar ICS URL, see a two-year
(current + next) calendar overview. Each day is split into three segments —
morning (00:0012:00), afternoon (12:0018:00), evening (18:0024:00) — that
light up based on events, colored by emoji found at the start of the event
title (configured in `tags.json`). No build step, no frontend framework,
one runtime dependency (`node-ical`).
## Commands
- `npm start` (or `./start.sh`, which runs on port 4000 instead of the
default 3000) — starts the server. Loads `.env` if present via Node's
native `--env-file-if-exists` flag (no dotenv dependency).
- `npm test` (or `node test_days.js`) — runs the one test file, a plain
`assert`-based script (no test framework) covering the recurrence/segment
logic in `days.js`.
- `PORT=<n> npm start` — override the port (default 3000).
## Architecture
- **`days.js`** — the only file with real logic, and the only one covered
by tests. `buildOverview(icsText, tagsConfig, rangeStartStr, rangeEndStr)`
is a pure function: parses ICS text via `node-ical`, expands every VEVENT
(recurring or not) with `ical.expandRecurringEvent(..., { expandOngoing: true })`
— this one call correctly handles EXDATE and RECURRENCE-ID overrides, so
there's no manual recurrence-expansion or override-merging code here.
Returns `{ "YYYY-MM-DD": { segments: [...], tooltip: "..." } }`.
- Each `segments[i]` is `null` (nothing), `'muted'` (untagged event
present but not called out), a color name string (one leading emoji
matched, from `tagsConfig`), or a 2-element array of color names (two
leading emoji — see below).
- **Emoji detection**: `leadingEmojis()` reads up to 2 emoji from the
start of the event title, tolerating a single space between them (so
both `"✈️💜 Title"` and `"✈️ 💜 Title"` count as a pair). A title with
one leading emoji not present in `tagsConfig` still gets a color (the
`UNKNOWN_EMOJI_COLOR` fallback), so it stands out from plain
untagged/muted events.
- **All-day events**: DTEND is exclusive per RFC 5545, handled by
subtracting 1ms before computing the local day.
- **No-TZID timestamps**: treated as floating local time per RFC 5545
(formatted in the system's own timezone, not UTC) — this matches how
`node-ical` itself encodes date-only DTSTART values, so the two stay
consistent. Don't default these to UTC; that was a real bug once.
- **`server.js`** — a bare `node:http` server, no framework (routes are
simple enough not to need one). `GET /api/overview?url=<ics>` fetches the
URL server-side and calls `buildOverview` — this exists only because
Google's ICS endpoint can't be fetched from the browser (CORS). Validates
the URL is `http`/`https` before fetching (the one trust-boundary check,
since the server will fetch whatever URL it's given). `tags.json` is read
fresh on every request (no caching), so editing it takes effect on the
next reload with no restart needed — only changes to `.js` files need a
server restart.
- **`public/index.html`** — the entire frontend: HTML, CSS, and JS inline in
one file, no build step. Fetches `/api/overview` and `/tags.json`, renders
a 12-month grid per year. Colors are CSS custom properties
(`TAG_COLOR_VAR` maps a color name from `tags.json` to a `--var`); a
segment holding a 2-color array renders as a `linear-gradient` split
top/bottom instead of a flat fill. Tooltips use the native `title`
attribute (no custom tooltip JS) — hover text is built server-side in
`days.js`.
- **`tags.json`** — user-editable, not code. Maps an emoji to
`{ label, color }`. `color` must be one of the names in `TAG_COLOR_VAR` in
`index.html` (`blue`, `aqua`, `yellow`, `green`, `violet`, `red`,
`magenta`, `orange`, `gray`, `purple`, `lightblue`) — an unrecognized
color name silently falls back to blue rather than erroring, so a typo
here won't crash anything but will look wrong.
- **`.env`** (gitignored) — optional `CALURL=<ics url>`, used only to
prefill/auto-load the URL input on page load via `GET /api/default-url`.
Never log or print its value; it's a private calendar URL.