CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A local-only, single-user personal dashboard (weather, news, history, sports, calendar,
todos, Mastodon/Bluesky, GitHub/Vercel, Obsidian) plus a printable daily newspaper PDF.
Runs only at http://localhost:4317. See README.md for usage, BRIEF.md/PLAN.md for intent.
It descends from the 37 Apps kit (../_shared-app-kit/) but deliberately drops the
cloud stack: no Clerk, Stripe, Vercel, or Neon. Don't "restore kit parity" by adding them
back — local-only is the point.
Commands
pnpm dev # dev server on :4317 (Turbopack)
pnpm build # MUST be --webpack (set in script) — see gotchas
pnpm start # run the production build on :4317
pnpm typecheck # tsc --noEmit — run this to verify changes
pnpm db:push # apply db/schema.ts to data/dashboard.db
pnpm db:seed # idempotently add missing default sources
pnpm icons # regenerate PWA icons from public/icons/source.svg
No test suite. Verify with pnpm typecheck + a manual run (or puppeteer screenshot against
the dev server, which is how the UI was validated during the build).
Architecture (the parts that span files)
API-first, single data path. Every surface reads /api/*. Only lib/store.ts touches
the DB (Drizzle over local SQLite, better-sqlite3). Route handlers wrap logic in
withErrors() from lib/api.ts and call requireUser() from lib/auth.ts — a no-op
single-user shim (returns "local"). If real auth is ever needed, that one file is the seam.
The source module pattern is the core abstraction. A "source" (weather, news, github…)
is defined once in lib/sources/modules/<kind>.ts as a SourceModule: fetch(), a Zod
configSchema + payloadSchema, and isConfigured(). Modules register in
lib/sources/modules/index.ts. The flow:
db.sourcesrows hold per-source config/enabled/refreshSeconds/position.lib/refresh.tsruns a module'sfetch, validates the payload, and stores asnapshotrow = the cache.ensureFresh()refreshes when forced or stale;currentState()reads cache only (instant paint).- The dashboard (
components/Dashboard.tsx) renders one card per source;components/cards/CardBodies.tsxswitches onkindto render each payload. - Credentialed sources with missing keys return
configured:false→ card shows "needs config" instead of erroring.
To add a source: schema in lib/schemas/sources/<kind>.ts → module in
lib/sources/modules/<kind>.ts → register in the barrel → add a branch in CardBodies.tsx
→ add to scripts/seed.ts and run pnpm db:seed.
macOS-native sources (Calendar.app, Things 3) run AppleScript through lib/mac.ts
(runOsa + FS/RS-delimited output parsed by parseRows). The host process needs Automation
permission (macOS prompts once). Calendar lists names via /api/integrations/calendars;
Things completes a task via /api/integrations/things/complete. The "todos" kind is the
Things Today source, rendered by components/cards/ThingsCard.tsx (special-cased in the
Dashboard for its complete-checkbox). A vestigial local /api/todos + todos table remain
for export compatibility but aren't shown.
Bento layout: each source stores explicit cols/rows grid spans (lib/layout.ts
spanStyle); the dashboard is a dense grid-auto-flow with fixed gridAutoRows, and cards
fill their tile and scroll internally. The dashboard's "Edit layout" mode
(components/Dashboard.tsx) makes cards dnd-kit drag sources for reordering (renumbers
position) and adds a bottom-right corner handle that resizes by whole grid cells (writes
cols/rows); both persist via PATCH /api/sources/[id]. The old size enum column +
TILE_SPANS are vestigial (kept for export/back-compat, no longer drive layout).
The newspaper. /print is a server component rendering current snapshots
(lib/edition-data.ts) with print CSS in app/globals.css (.paper, @media print hides
the nav). POST /api/edition (lib/pdf.ts) refreshes sources, then drives puppeteer to
render /print to a PDF saved under editions/, recorded in the editions table and served
by /api/editions/[id]/file.
Gotchas
pnpm buildisnext build --webpack.@serwist/next(the PWA service worker) is a webpack plugin and breaks under Next 16's default Turbopack build. Don't change it back to plainnext build. Dev stays on Turbopack (SW is disabled in dev).- Native deps must stay in
serverExternalPackages(next.config.ts):better-sqlite3,puppeteer. And in pnpm'sonlyBuiltDependenciesallowlist (package.json) so their postinstall build scripts run. typedRoutesis intentionally off — nav is config-driven (lib/nav.ts) with string hrefs, which fights route-literal typing.- Obsidian is read-only. The obsidian source only stats/lists vault files. Never write to
the vault from here (a global hook also blocks writes outside
Humdrum/Claude/). data/(SQLite),editions/(PDFs), andPersonal Dashboard.appare gitignored local artifacts.- Theme tokens come from
app/tokens.css(synced from the kit). Components usevar(--token)inline styles, never hard-coded colors. The e-ink theme exists but is unused (desktop-only).