4.3 KiB
4.3 KiB
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.envif present via Node's native--env-file-if-existsflag (no dotenv dependency).npm test(ornode test_days.js) — runs the one test file, a plainassert-based script (no test framework) covering the recurrence/segment logic indays.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 vianode-ical, expands every VEVENT (recurring or not) withical.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]isnull(nothing),'muted'(untagged event present but not called out), a color name string (one leading emoji matched, fromtagsConfig), 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 intagsConfigstill gets a color (theUNKNOWN_EMOJI_COLORfallback), 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-icalitself encodes date-only DTSTART values, so the two stay consistent. Don't default these to UTC; that was a real bug once.
- Each
server.js— a barenode:httpserver, no framework (routes are simple enough not to need one).GET /api/overview?url=<ics>fetches the URL server-side and callsbuildOverview— this exists only because Google's ICS endpoint can't be fetched from the browser (CORS). Validates the URL ishttp/httpsbefore fetching (the one trust-boundary check, since the server will fetch whatever URL it's given).tags.jsonis read fresh on every request (no caching), so editing it takes effect on the next reload with no restart needed — only changes to.jsfiles need a server restart.public/index.html— the entire frontend: HTML, CSS, and JS inline in one file, no build step. Fetches/api/overviewand/tags.json, renders a 12-month grid per year. Colors are CSS custom properties (TAG_COLOR_VARmaps a color name fromtags.jsonto a--var); a segment holding a 2-color array renders as alinear-gradientsplit top/bottom instead of a flat fill. Tooltips use the nativetitleattribute (no custom tooltip JS) — hover text is built server-side indays.js.tags.json— user-editable, not code. Maps an emoji to{ label, color }.colormust be one of the names inTAG_COLOR_VARinindex.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) — optionalCALURL=<ics url>, used only to prefill/auto-load the URL input on page load viaGET /api/default-url. Never log or print its value; it's a private calendar URL.