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.tsClerk guard,app/sign-in,app/sign-up,app/api/me,app/api/tokens, Clerk<UserButton>inAppNav, ClerkProvider. - ✅ DB: SQLite via
better-sqlite3+drizzle-orm/better-sqlite3(file atdata/dashboard.db).drizzle.config.tsdialectsqlite. - ✅ Auth shim:
lib/auth.tsreturns a fixed single-user id ("local"). All user-scoped rows use it; export/import keep the kit'sversion/app/exportedAtshape. - ✅ No Vercel: remove
vercel.json. Add depsbetter-sqlite3,rss-parser,node-ical,puppeteer(PDF),date-fns.
Data model (lib/schemas/, Drizzle in db/schema.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/<kind>.ts.
Source module pattern (the core abstraction)
Each source = lib/sources/<kind>.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/<Kind>Card.tsx), and an edition section
(components/edition/<Kind>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)
- Copy scaffold → repo (preserve
.git/BRIEF/PLAN). Renamepackage.json→personal-dashboard. - Strip cloud per "Kit deviations": remove Clerk/Stripe/Neon deps + files, swap to SQLite, add new deps, write single-user
lib/auth.ts, deletevercel.json.pnpm install. - Zod schemas: core (
source,snapshot,todo,edition,setting) +lib/sources/registry.tsskeleton. db/schema.ts(sqlite tables) +db/index.ts(better-sqlite3).pnpm db:push. Seed default Sources (the keyless four + todos enabled).- Stub all
/api/*routes → 501, so contract is visible. Verify withcurl. - Implement core handlers: settings, todos, sources CRUD, snapshot refresh + cache TTL, export/import.
- Keyless sources end-to-end:
weather(Open-Meteo + geocode),onthisday(Wikipedia),news(rss-parser),obsidian(scan/Users/kortum/Humdrummtimes, read-only). Each: fetch + payload schema + card. - Dashboard UI: responsive card grid reading
/api/sources; per-card loading/error/needs-config states; manual refresh. Todos card (add/toggle). Mount kitShortcutsRoot+SettingsDrawer. - Settings page: location/units, RSS feed list, per-source enable + ordering, vault path.
- PDF edition:
/printnewspaper route (columns, Awke display, forced mono/light, uses same source data);POST /api/editiondrives puppeteer → save toeditions/→ insert row; "Generate edition" button + editions list. - 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. - PWA:
public/manifest.json(name/icons/theme), icons viascripts/generate-icons.mjs, service worker shell + network-first API. - E-ink pass: dashboard +
/printlegible mono, no motion, run §Eink rubric. - Launcher:
scripts/launch.command(build-if-stale →next starton :4317 → open browser) +scripts/make-app.sh(osacompile a double-clickablePersonal Dashboard.appthat runs it without a Terminal window). - README: setup,
.env.localkeys 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
<repo>/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).