Files
2026-07-17 20:23:52 +02:00

4.3 KiB
Raw Permalink Blame History

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, 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=<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.