# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is A Pygame framebuffer clock for a Le Potato (ARM Debian) connected to an auditorium TV. Renders directly to `/dev/fb0` — no X11/Wayland. Staff push announcements via a browser dashboard or API webhook. ## Running locally **Server + dashboard only (no pygame needed — works on WSL/Windows):** ```bash python3 main.py --no-display ``` **Windowed display mode (requires a graphical environment):** ```bash python3 main.py --dev ``` Press **Escape** to close. On WSL2 this needs WSLg or VcXsrv. **First run:** if `config.toml` has no `token`, one is auto-generated and saved. If `password_hash` is empty, a setup wizard appears at `http://localhost:8080/setup`. **Copy config from example before first run:** ```bash cp config.toml.example config.toml ``` ## Architecture The app has two concurrent execution contexts that communicate through `MessageState`: - **`DisplayThread`** (`display.py`) — a daemon thread running pygame. Polls `state.get()` on every frame tick. When a message is active it replaces the clock entirely; font size is binary-searched to fill 88% of the screen. - **`ClockServer`** (`server.py`) — aiohttp running in the asyncio main thread. Updates `state` in response to dashboard/API requests. `MessageState` (`state.py`) is the only shared object between the two contexts. It uses a `threading.Lock` and stores expiry as a `time.monotonic()` timestamp. Expiry is checked lazily on `get()` — there is no background timer. `AppConfig` (`config.py`) is loaded once at startup from `config.toml`. The only post-startup writes are `save_password_hash()` (on first-run setup) and auto-generated token (on first run with empty token). Both use regex replacement on the raw TOML file to avoid reformatting. ## Server routes | Route | Auth | Purpose | |---|---|---| | `GET /` | Session cookie | Serve dashboard `index.html` | | `GET /setup`, `POST /setup` | None | First-run password wizard | | `GET /login`, `POST /login` | None | Dashboard login | | `POST /logout` | Session cookie | Dashboard logout | | `POST /dashboard/message` | Session cookie | Send message (from dashboard JS) | | `DELETE /dashboard/message` | Session cookie | Clear message (from dashboard JS) | | `GET /dashboard/status` | Session cookie | Polled every 5 s by dashboard JS | | `POST /api/message` | Bearer token + rate limit | Webhook / external integrations | | `DELETE /api/message` | Bearer token + rate limit | Clear via API | | `GET /api/status` | Bearer token | Check state via API | The dashboard (`dashboard/index.html`, `style.css`, `app.js`) calls the `/dashboard/*` routes, not `/api/*`. Static assets are served from `dashboard/` at `/static/`. ## Key behaviours to preserve - `display.py` is **not imported** when `--no-display` is set — this allows the server to run without pygame installed. - Message font layout is **cached** in `DisplayThread.run()` and only recomputed when the message text changes. - `_update_config_field` in `config.py` uses regex on the raw TOML — it only rewrites the matched field, leaving all comments and formatting intact. - `secrets.compare_digest` is used for bearer token comparison to prevent timing attacks.