63 lines
3.2 KiB
Markdown
63 lines
3.2 KiB
Markdown
# 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.
|