# 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*