Personal Dashboard
A local, single-user dashboard inspired by firstlight: weather, news, on-this-day history, sports, calendar, todos, recent Mastodon/Bluesky replies, GitHub & Vercel activity, and recently edited Obsidian notes — on one screen, plus a printable daily newspaper PDF.
Runs only on your Mac at http://localhost:4317. No accounts, no cloud, no deploy.
Built on the 37 Apps kit (themes, fonts, layout, PWA), with the cloud bits swapped out: no Clerk/Stripe/Vercel/Neon — local SQLite + a double-click launcher instead.
Run it (no terminal)
bash scripts/make-app.sh
This creates Personal Dashboard.app in the project folder. Double-click it (or drag
it to your Dock). First launch installs deps + builds (a minute or two), then opens your
browser. Subsequent launches are instant. The server keeps running until you quit it.
You can also double-click scripts/launch.command directly — same thing, but it shows
a Terminal window with logs.
Run it (terminal, for development)
pnpm install
cp .env.example .env.local # fill in optional source keys
pnpm db:push && pnpm db:seed # create + seed the local SQLite db
pnpm dev # http://localhost:4317
| Command | What |
|---|---|
pnpm dev |
Dev server (Turbopack, hot reload) on :4317 |
pnpm build |
Production build (webpack — required by the PWA service worker) |
pnpm start |
Run the production build on :4317 |
pnpm typecheck |
tsc --noEmit |
pnpm db:push |
Apply the schema to data/dashboard.db |
pnpm db:seed |
Add any missing default sources (idempotent) |
pnpm icons |
Regenerate PWA icons from public/icons/source.svg |
Sources
Each source is a self-contained module in lib/sources/modules/. Keyless ones work out of
the box; credentialed ones show "needs config" until you add their keys to .env.local
and enable them in Settings.
| Source | Needs | Env keys |
|---|---|---|
| Weather | nothing (Open-Meteo) | set a location in Settings (or WEATHER_LAT/WEATHER_LON) |
| On This Day | nothing (Wikipedia) | — |
| News | nothing (RSS) | edit feed list in Settings |
| Obsidian | nothing (reads vault, read-only) | OBSIDIAN_VAULT (default /Users/kortum/Humdrum) |
| Scores | nothing (ESPN) | pick leagues in Settings config (e.g. basketball/nba) |
| GitHub | token | GITHUB_TOKEN, optional GITHUB_USER |
| Vercel | token | VERCEL_TOKEN, optional VERCEL_TEAM_ID |
| Mastodon | instance + token | MASTODON_INSTANCE, MASTODON_TOKEN |
| Bluesky | handle + app password | BLUESKY_HANDLE, BLUESKY_APP_PASSWORD |
| Calendar | local Calendar.app (pick which calendars in Settings) or ICS URLs | — / CALENDAR_ICS_URLS |
| Today | local Things 3 (pulls the Today list) | — |
| Links | nothing — a list of label + URL (your other web apps, bookmarks) | — |
Toggle, reorder, resize (bento tiles: small → large), and configure sources in Settings. Each card refreshes on its own interval (cached in SQLite); hit ↻ on a card or Refresh all to force it. Weather takes a place name (geocoded) and defaults to Napa, CA. News merges your own RSS with a curated set.
Add more cards: Settings → Add source lets you add another card of any kind — e.g. a second Links card for bookmarks, or a second News card for personal feeds. Delete any card with its ✕.
How Scores works
The Scores card has two independent settings:
- Leagues — show every game in these leagues. One ESPN path per line.
basketball/nba→ all NBA games (including the whole playoffs). - Favorite teams — add only these teams' games, on top of the leagues above, without
pulling the rest of their league. One per line,
league | team:baseball/mlb | Cubs football/nfl | Bears hockey/nhl | Blackhawks
So leagues basketball/nba + those teams = all NBA scores plus just the Cubs, Bears, and
Blackhawks — and you never see other MLB/NFL/NHL games. Common ESPN paths: basketball/nba,
baseball/mlb, football/nfl, hockey/nhl, soccer/eng.1. Games are grouped by league on
the card. (ESPN's public scoreboard is keyless.)
macOS permission: Calendar and Things use AppleScript. The first time the app reads them, macOS asks to allow automation — click OK. (Grant it to the launcher app / your terminal.)
Add a new source
lib/schemas/sources/<kind>.ts— ZodConfig+Payload.lib/sources/modules/<kind>.ts— implementSourceModule(fetch,isConfigured).- Register it in
lib/sources/modules/index.ts. - Add a render branch in
components/cards/CardBodies.tsx. - Add it to
scripts/seed.tsand runpnpm db:seed.
The Paper
The Paper tab generates a dated newspaper PDF from your current dashboard data
(/print is the live HTML version). PDFs are saved to editions/ in the project
folder and listed in the app. Generation refreshes stale sources first, then renders
/print to PDF via headless Chrome.
Architecture
- Next.js 16 (App Router) + TypeScript + Tailwind v4 + kit theme tokens.
- API-first: every surface reads
/api/*(Zod-validated). The web client never touches the DB directly — onlylib/store.tsdoes. - Local SQLite via
better-sqlite3+ Drizzle (data/dashboard.db). - Single-user shim (
lib/auth.ts) — no auth; swap in real auth there if ever needed. - Source module pattern (
lib/sources/) — fetch + Zod payload + card + cache snapshot. - PWA via
@serwist/next(installable, offline shell). Themes: Flexoki / Uchu / Humdrum (light + dark). E-ink theme exists but is unused on desktop.
Data is portable: Settings → Export/Import JSON (/api/export, /api/import).
See BRIEF.md and PLAN.md for the original intent and build plan.