# Personal Dashboard — Plan Mode: **greenfield** _Generated from BRIEF.md + _shared-app-kit/APP_KIT.md._ ## Snapshot - Tier: **personal**, **local-only**, **no auth** (bound to localhost). - Slug: `personal-dashboard` - Domain: none — runs at `http://localhost:4317` (stable port for PWA + launcher). - Surfaces: web PWA, e-ink, PDF edition. (CLI / Scriptable deferred.) - Default theme: system → Flexoki / Flexoki-dark. ## Kit deviations (local-only) — build must honor Keep kit **look + structure** (tokens, fonts, layout, themes, eink, API-first Zod, export/import, PWA shell, design primitives). **Swap infra:** - ❌ Drop deps: `@clerk/nextjs`, `@neondatabase/serverless`, `stripe`, `svix`. - ❌ Delete: `middleware.ts`/`proxy.ts` Clerk guard, `app/sign-in`, `app/sign-up`, `app/api/me`, `app/api/tokens`, Clerk `` in `AppNav`, ClerkProvider. - ✅ DB: **SQLite via `better-sqlite3` + `drizzle-orm/better-sqlite3`** (file at `data/dashboard.db`). `drizzle.config.ts` dialect `sqlite`. - ✅ Auth shim: `lib/auth.ts` returns a fixed single-user id (`"local"`). All user-scoped rows use it; export/import keep the kit's `version/app/exportedAt` shape. - ✅ No Vercel: remove `vercel.json`. Add deps `better-sqlite3`, `rss-parser`, `node-ical`, `puppeteer` (PDF), `date-fns`. ## Data model (`lib/schemas/`, Drizzle in `db/schema.ts`) ```ts Source { id, kind, label, enabled, config: json, refreshSeconds, position } kind ∈ weather|news|onthisday|sports|calendar|mastodon|bluesky|github|vercel|obsidian|todos Snapshot { id, sourceId→Source, payload: json, fetchedAt, ok, error|null } // cache Todo { id, title, done, due|null, createdAt } Edition { id, date, pdfPath, generatedAt } Setting { key (pk), value: json } // location, units, layout ``` Per-source payloads get their own Zod schema in `lib/schemas/sources/.ts`. ## Source module pattern (the core abstraction) Each source = `lib/sources/.ts` exporting: `{ kind, label, keyless, configSchema, payloadSchema, fetch(config) }`, registered in `lib/sources/registry.ts`. A source has: a fetcher, a typed payload, a **dashboard card** (`components/cards/Card.tsx`), and an **edition section** (`components/edition/Section.tsx`). Snapshots cached in SQLite; refreshed on load when older than `refreshSeconds`, or via manual refresh. Missing creds → card shows "needs config", never crashes. ## API routes | Method | Path | Notes | |---|---|---| | GET | `/api/health` | liveness | | GET/POST | `/api/sources` | list configured + latest snapshot / create | | GET/PATCH/DELETE | `/api/sources/[id]` | read (cached or fresh) / update config / remove | | POST | `/api/sources/[id]/refresh` | force fetch, store Snapshot | | GET/POST | `/api/todos` · PATCH/DELETE `/api/todos/[id]` | local todos | | GET | `/api/editions` · POST `/api/edition` | list / generate dated PDF | | GET | `/api/export` · POST `/api/import` | kit JSON contract (single-user id) | | GET | `/api/settings` · PUT `/api/settings` | location, units, card layout | ## Build tasks (ordered) 1. Copy scaffold → repo (preserve `.git`/BRIEF/PLAN). Rename `package.json` → `personal-dashboard`. 2. **Strip cloud** per "Kit deviations": remove Clerk/Stripe/Neon deps + files, swap to SQLite, add new deps, write single-user `lib/auth.ts`, delete `vercel.json`. `pnpm install`. 3. Zod schemas: core (`source`, `snapshot`, `todo`, `edition`, `setting`) + `lib/sources/registry.ts` skeleton. 4. `db/schema.ts` (sqlite tables) + `db/index.ts` (better-sqlite3). `pnpm db:push`. Seed default Sources (the keyless four + todos enabled). 5. Stub all `/api/*` routes → 501, so contract is visible. Verify with `curl`. 6. Implement core handlers: settings, todos, sources CRUD, snapshot refresh + cache TTL, export/import. 7. **Keyless sources end-to-end:** `weather` (Open-Meteo + geocode), `onthisday` (Wikipedia), `news` (rss-parser), `obsidian` (scan `/Users/kortum/Humdrum` mtimes, read-only). Each: fetch + payload schema + card. 8. Dashboard UI: responsive card grid reading `/api/sources`; per-card loading/error/needs-config states; manual refresh. Todos card (add/toggle). Mount kit `ShortcutsRoot` + `SettingsDrawer`. 9. Settings page: location/units, RSS feed list, per-source enable + ordering, vault path. 10. **PDF edition:** `/print` newspaper route (columns, Awke display, forced mono/light, uses same source data); `POST /api/edition` drives puppeteer → save to `editions/` → insert row; "Generate edition" button + editions list. 11. **Credentialed sources** (same pattern, stub-friendly): `calendar` (ICS via node-ical), `github` (PAT), `vercel` (token), `mastodon` (instance+token), `bluesky` (handle+app-pw), `sports` (ESPN public JSON). Wire as creds land; each independent. 12. PWA: `public/manifest.json` (name/icons/theme), icons via `scripts/generate-icons.mjs`, service worker shell + network-first API. 13. E-ink pass: dashboard + `/print` legible mono, no motion, run §Eink rubric. 14. **Launcher:** `scripts/launch.command` (build-if-stale → `next start` on :4317 → open browser) + `scripts/make-app.sh` (osacompile a double-clickable `Personal Dashboard.app` that runs it without a Terminal window). 15. README: setup, `.env.local` keys per source, launcher install, edition output path. ## Open questions (not blockers) - App display name + icon (placeholder "Personal Dashboard"). - Edition cadence: manual button only in v0; scheduled (launchd) later. - Editions save dir — default `/editions/`. - Daily AI "editor's note" (Anthropic) for the edition — v1. ## Out of scope (v0) - Multi-user, sign-up, remote deploy, Stripe. - Native app (PWA only). CLI + Scriptable surfaces. - Writing to the Obsidian vault (read-only source; `Humdrum/Claude/` hook stands).