# Personal Dashboard A local, single-user dashboard inspired by [firstlight](https://github.com/cruftbox/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 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) ```bash 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 1. `lib/schemas/sources/.ts` — Zod `Config` + `Payload`. 2. `lib/sources/modules/.ts` — implement `SourceModule` (`fetch`, `isConfigured`). 3. Register it in `lib/sources/modules/index.ts`. 4. Add a render branch in `components/cards/CardBodies.tsx`. 5. Add it to `scripts/seed.ts` and run `pnpm 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 — only `lib/store.ts` does. - **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.