# Fresco — handoff to Codex This is the front-end for a horizontal-strip dashboard the user looks at all day. It is a *display*, not an app: it reads `dashboard-data.json` and renders. There is one button (Update) and one toggle area (Tweaks). That's it. ## Files - `Fresco.html` — entry point. Loads React + Babel, the data, and the components. - `direction-fresco.jsx` — all the React components (the masthead, calendar strip, tasks, vault, weather, trains, agents, readwise, currently, ticker). - `tokens.css` — Flexoki dark palette + spacing/type tokens. Don't add colors here without asking. - `data.js` — **mock data**, loaded as `window.DASH = {...}`. Replace with a fetch of `dashboard-data.json` (see below). - `tweaks-panel.jsx` — the in-page Tweaks panel. Keep it; the user wants to keep tuning the masthead. - `fonts/` — iA Writer Quattro (SIL Open Font License). Body face. Don't swap. ## What to change for production ### 1. Replace `data.js` with a JSON fetch Currently `Fresco.html` loads `data.js` via a `<script>` tag, which sets `window.DASH = {...}`. For the real app, fetch JSON instead: ```js // Replace handleUpdate() in Fresco.html with: const handleUpdate = React.useCallback(async () => { const r = await fetch("./dashboard-data.json?t=" + Date.now(), { cache: "no-store" }); const next = await r.json(); window.DASH = next; setData(next); setBumpKey(k => k + 1); }, []); ``` And on first load, fetch before the first render (or render against a "loading" placeholder). ### 2. Keep the data shape *exactly* The JSON your writer produces must have the same top-level keys and field names as `data.js`: - `meta` — `{ now, updated, source, location, ... }` - `weather` — array of `{ city, temp, note, ... }` - `calendar` — array of `{ date, events: [{ time, title, location?, kind? }] }` - `tasks` — array of `{ title, due, status, ... }` - `vault` — array of `{ title, kind, last, words?, target? }` - `readwise` — array of `{ text, source }` - `agents` — array of `{ agent, topic, status }` - `trains` — array of `{ depart, route, status }` - `currently` — `{ … whatever the "Currently" column shows … }` Read `data.js` carefully before you start — every field name there is referenced in `direction-fresco.jsx`. Don't rename, don't flatten, don't widen. ### 3. `meta.now` is the source of truth for time Several columns compute "today / this week / this month" buckets against `meta.now`, not against `Date.now()`. Set `meta.now` to `new Date().toISOString()` at the moment your writer runs. This means: - The whole frame is deterministic — regenerate against any moment. - If the writer last ran at 23:00 and someone opens the page at 00:30, the dashboard still makes sense (it's showing 23:00's view, not a half-broken now). All times are local (`Europe/Rome`). Don't suffix `Z`. ### 4. Two-layer refresh Keep these decoupled: - **Writer** — your script that pulls calendar / weather / trains / vault / readwise / agents and writes `dashboard-data.json`. Run on whatever cadence each source needs (cron, launchd, file-watch). - **Reader** — `Fresco.html`, which polls the JSON. Already wired: `setInterval(handleUpdate, 60 * 60 * 1000)`. Drop to 5 min once it's real; the JSON is tiny. If the writer breaks, the reader keeps showing the last good frame. Don't have the reader call the writer. ### 5. Serve over HTTP, not `file://` `fetch("./dashboard-data.json")` is finicky on `file://` in some browsers. Run a tiny static server in the folder (`python -m http.server`, or a one-liner Node script) and point the Obsidian launcher note at `http://localhost:PORT/Fresco.html`. ### 6. Fallback markdown The user wants a plaintext fallback. Have your writer also emit a small markdown file — `## Today` / `## Soon` / weather / next train — that Obsidian can show natively if the dashboard's down or they're on mobile. Keep it dumb. ## What *not* to change - **Fonts.** iA Writer Quattro for body, JetBrains Mono for tabular data. Lora / Source Serif / Literata are alternates the user can pick from in Tweaks; don't remove them. Don't add Inter or Roboto. - **Colors.** Flexoki only. If you need a new semantic (e.g. "task overdue >a week"), pick from the existing palette and ask the user before adding state. - **The masthead tracking animation.** It's the only motion on the page and it's deliberate. Half-cosine sweep, 30s default, pauses on tab hidden. Don't replace with a metronomic CSS keyframe without confirming it still feels like a breath. - **The horizontal-scroll metaphor.** The fresco is wider than the viewport on purpose. Vertical-wheel-to-horizontal-scroll is wired in `Fresco.html`. Don't reflow into vertical columns without asking the user. ## Open question to confirm with the user The dashboard is currently read-only. Some columns *could* become interactive (click a task to mark done, click a calendar event to open the source). Going interactive would change the typography density. Confirm before adding click handlers everywhere. ## Adapters needed (Codex's todo list) Each one writes into the corresponding key of `dashboard-data.json`: - [ ] Calendar (Google / Apple via CalDAV) → `calendar` - [ ] Tasks (Things 3 / Reminders / Linear / wherever) → `tasks` - [ ] Vault (Obsidian file watcher) → `vault` - [ ] Weather (two cities, current + one-line note) → `weather` - [ ] Trains (Trenitalia or local feed) → `trains` - [ ] Readwise (real API) → `readwise` - [ ] Agents — **including Codex's own commit / session activity** — → `agents` - [ ] Currently / now-playing (Last.fm or similar) → `currently` Ship the spine first. A dashboard with five real adapters at 60fps beats a dashboard with all twelve and 200ms jank. — *the designer*