3.2 KiB
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):
python3 main.py --no-display
Windowed display mode (requires a graphical environment):
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:
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. Pollsstate.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. Updatesstatein 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.pyis not imported when--no-displayis 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_fieldinconfig.pyuses regex on the raw TOML — it only rewrites the matched field, leaving all comments and formatting intact.secrets.compare_digestis used for bearer token comparison to prevent timing attacks.