Files
KH_Clock/CLAUDE.md

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. 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.