# 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:00–12:00), afternoon (12:00–18:00), evening (18:00–24: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= 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=` 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`, `white`) — 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=`, 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.