Update CLAUDE.md for database, admin backend and track history

Document the expanded scope, admin pages, broadcast and track history
rules, upload handling, coding conventions, server-only deployment
steps and planned work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel MesserandClaude Opus 5.5 committed 2026-10-29 14:03:09 -05:00
1 parent 1057c71561
commit 83e8482812
1 file changed
+78 -15
+78 -15
View File
@@ -13,48 +13,97 @@ The shows have distinct themes and identities:
## Scope
This repo covers the **frontend only** — the public-facing website. Icecast configuration, Yunohost server administration, and streaming software are out of scope. Do not look for or suggest changes to server-side streaming config.
This repo covers the **website** — the public pages, the admin backend, and the site's MariaDB database. Icecast configuration, Yunohost server administration, and streaming software are out of scope. Do not look for or suggest changes to server-side streaming config.
The one approved exception is [cli/poll_icecast.php](cli/poll_icecast.php), a cron job that reads the Icecast status API to record track history. It only reads from Icecast; it never configures it.
## Key Files
Public pages:
- [index.php](index.php) — Main page. Contains the audio player, now-playing widget, upcoming schedule section, and about section. Show information is updated here manually before each broadcast.
- [now_playing.php](now_playing.php) — Fetches the currently playing track from the Icecast JSON status API and returns it as HTML. Called by the frontend every 15 seconds via `setInterval`.
- [live.php](live.php) — Page for active broadcasts. Player on the left, recently played list on the right; the header background and heading change with the current show.
- [live.php](live.php) — Page to send listeners to while streaming. Player on the left, recently played list on the right; the sidebar background and the heading change with the current show.
- [now_playing.php](now_playing.php) — Returns the currently playing track as HTML. Called by `index.php` and `live.php` every 15 seconds.
- [recently_played.php](recently_played.php) — Returns the current broadcast's track history as table rows for `live.php`.
- [includes/icecast.php](includes/icecast.php) — Shared Icecast fetching/parsing, album art lookup and Last.fm link helpers.
Admin backend ([admin/](admin/)):
- [admin/login.php](admin/login.php), [admin/logout.php](admin/logout.php) — Username/password login.
- [admin/index.php](admin/index.php) — Dashboard: broadcast status, queue the next show, switch the live show, clear the queue.
- [admin/shows.php](admin/shows.php), [admin/show_edit.php](admin/show_edit.php) — List, add, edit and delete shows, including background image uploads.
Shared includes:
- [includes/db.php](includes/db.php) — `getDB()` returns a shared PDO connection (session time zone UTC).
- [includes/broadcasts.php](includes/broadcasts.php) — Broadcast lifecycle and track history logic.
- [includes/icecast.php](includes/icecast.php) — Icecast fetching/parsing, album art lookup (iTunes Search API) and Last.fm link helpers.
- [includes/broadcasts.php](includes/broadcasts.php) — Broadcast lifecycle, show queueing and track history logic.
- [includes/auth.php](includes/auth.php) — Admin sessions, login throttling and CSRF helpers.
- [includes/admin_layout.php](includes/admin_layout.php) — `adminHeader()`/`adminFooter()` page chrome and `flash()` messages for admin pages.
Command-line tools (run over SSH from the site root):
- [cli/poll_icecast.php](cli/poll_icecast.php) — Cron-run poller that records broadcasts and tracks.
- [cli/set_show.php](cli/set_show.php) — Sets which show is airing next (or now).
- [cli/set_show.php](cli/set_show.php) — Lists shows, or queues/sets the show by slug. Same behaviour as the dashboard control.
- [cli/create_admin.php](cli/create_admin.php) — Creates an admin account or resets its password.
- [admin/](admin/) — Admin backend. Every page calls `requireAdmin()` from [includes/auth.php](includes/auth.php) and uses `adminHeader()`/`adminFooter()` from [includes/admin_layout.php](includes/admin_layout.php).
Other:
- [sql/schema.sql](sql/schema.sql) — Full database schema plus seed data for the five shows. Safe to re-run.
- [config/db_credentials.example.php](config/db_credentials.example.php) — Template for the gitignored `config/db_credentials.php`.
## Architecture
- **Backend:** PHP only. No Composer, no package manager, no build step.
- **Frontend:** HTML5 UP Strata template with custom CSS additions. No npm, no webpack, no preprocessor — edit CSS directly in `assets/css/main.css` or inline `<style>` blocks.
- **Now-playing:** `index.php` and `live.php` fetch `now_playing.php` via JavaScript every 15 seconds and inject the result into `#now-playing`. The fetch uses `arrayBuffer` + `TextDecoder` to handle the UTF-8 encoding correctly on the client side.
- **Now-playing:** `index.php` and `live.php` fetch `now_playing.php` via JavaScript every 15 seconds and inject the result into `#now-playing`. The fetch uses `arrayBuffer` + `TextDecoder` to handle the UTF-8 encoding correctly on the client side. `live.php` fetches `recently_played.php` the same way.
- **Duplicated sidebar:** `live.php` was copied from `index.php`, so the sidebar markup, now-playing styles and fetch script exist in both. Changes to one usually need making in the other.
- **Sidebar background:** `#header` in `main.css` uses `images/bg.jpg` with `background-size: cover`. Don't switch it back to `bg.png` — that file has an alpha fade at the bottom that lets the brown background colour show through.
## Database
MariaDB on localhost. Credentials live in `config/db_credentials.php`, which is gitignored and uploaded to the server separately; `config/db_credentials.example.php` is the template. Schema is in [sql/schema.sql](sql/schema.sql):
MariaDB on localhost. Credentials live in `config/db_credentials.php`, which is gitignored and uploaded to the server separately. Tables (see [sql/schema.sql](sql/schema.sql)):
- `shows` — the shows, with an optional `background_image` (filename in `images/backgrounds/`).
- `broadcasts` — one row per streaming session: pending (queued by `cli/set_show.php`), live, or ended.
- `shows` — the shows, with a unique `slug` and an optional `background_image` (filename in `images/backgrounds/`).
- `broadcasts` — one row per streaming session: pending (`started_at` NULL, queued from the dashboard or `cli/set_show.php`), live (`ended_at` NULL), or ended.
- `played_tracks` — tracks per broadcast. Cleared ten minutes after the broadcast ends; no long-term history is kept.
- `admins` — admin accounts (username + password hash).
- `login_attempts` — failed logins by IP, used for throttling.
`cli/poll_icecast.php` runs from cron every minute and polls Icecast four times, 15 seconds apart. A broadcast starts when the stream comes online and ends after five minutes unseen (so brief drop-outs don't split a show). Before going live, choose the show on the admin dashboard (or run `php cli/set_show.php <slug>`). Shows and their background images are managed at `admin/shows.php`; uploads are saved to `images/backgrounds/` under generated names, so the web server needs write access to that directory.
All DATETIME values are UTC. `recently_played.php` outputs `<time datetime="...Z">` and `live.php` reformats them in the listener's local time zone.
## Broadcasts and Track History
`cli/poll_icecast.php` runs from cron every minute and polls Icecast four times, 15 seconds apart (with a lock file so runs never overlap). Rules, implemented in `recordPoll()` in [includes/broadcasts.php](includes/broadcasts.php):
- When the stream comes online, the queued broadcast starts. If nothing was queued, a broadcast with no show is created so tracks are still recorded.
- A new track is recorded whenever title/artist/album differ from the last one, with its album art URL looked up once.
- A broadcast ends once it has been unseen for five minutes, so brief encoder drop-outs don't split a show.
- Tracks are deleted ten minutes after their broadcast ends.
- If the status API can't be reached, the poll is skipped entirely so an outage isn't mistaken for the stream ending.
In the recently played list, album art and album title link to the album's Last.fm page; the now-playing widget links to the track's Last.fm page.
## Admin Authentication
Username and password only; the site sends no email. Accounts are created and passwords reset over SSH with `php cli/create_admin.php <username>`. Passwords are hashed with `password_hash()` (PASSWORD_DEFAULT). Sessions use a `rfe_admin` cookie scoped to the admin directory (HttpOnly, SameSite=Strict, Secure over HTTPS) and expire after four hours idle. Five failed logins from one IP within 15 minutes locks that IP out for the rest of the window. Every POST form must include `csrfField()` and its handler must call `verifyCsrf()` first.
Username and password only; the site sends no email. Accounts are created and passwords reset over SSH with `php cli/create_admin.php <username>`. Passwords are hashed with `password_hash()` (PASSWORD_DEFAULT — bcrypt, chosen for portability over Argon2). Sessions use a `rfe_admin` cookie scoped to the admin directory (HttpOnly, SameSite=Strict, Secure over HTTPS) and expire after four hours idle. Five failed logins from one IP within 15 minutes locks that IP out for the rest of the window.
When adding admin pages:
- Call `requireAdmin()` first and render with `adminHeader($title, $admin)` / `adminFooter()`.
- Every POST form must include `csrfField()`, and its handler must call `verifyCsrf()` before doing anything else.
- Use post-redirect-get: handle the POST, call `flash()`, then redirect.
## Background Image Uploads
Uploaded through `admin/show_edit.php`: JPEG, PNG or WebP up to 5 MB (PHP's `post_max_size` is 8 MB). Files are validated by content (finfo + `getimagesize`), not extension, and saved to `images/backgrounds/` as `<slug>-<random>.<ext>`. Replaced or removed images are deleted from disk. The web server user must be able to write to that directory. Uploads are gitignored — they belong to each server's database, so they're uploaded per server through the admin page.
## Conventions
- Files in `includes/` and `config/` return 403 if requested directly; files in `cli/` refuse to run outside the command line (`PHP_SAPI !== 'cli'`). Follow the same pattern for new files.
- Use prepared statements for all queries with user input, and escape all output with `htmlspecialchars` (the `e()` helper in admin pages).
- `index.php` and `live.php` use CRLF line endings; the PHP includes and scripts use LF. Preserve whichever a file already has.
- PHP files use 4-space indentation; the HTML templates use tabs, matching Strata.
## Icecast Integration
- **Stream URL:** `https://radio.cyberpunklibrarian.nohost.me/rfe`
- **Status API:** `https://radio.cyberpunklibrarian.nohost.me/status-json.xsl`
The status API returns JSON with track metadata under `icestats.source`. The `source` key may be a single object or an array depending on how many mountpoints are active; `now_playing.php` handles both cases.
The status API returns JSON with track metadata under `icestats.source`; when the stream is offline there is no `source` key. The `source` key may be a single object or an array depending on how many mountpoints are active; `fetchCurrentTrack()` in [includes/icecast.php](includes/icecast.php) handles both cases. Track titles arrive as `Title - Artist - Album`.
## Known Encoding Quirk
@@ -65,10 +114,24 @@ Icecast double-encodes track metadata as Latin-1 interpreted as UTF-8. The fix i
There is no CI/CD pipeline. Deployment is manual:
1. Commit and push changes to the self-hosted Gitea repository.
2. Upload changed files to the Yunohost web server via SFTP.
2. Upload changed files to the Yunohost web server via SFTP. Never overwrite `config/db_credentials.php` or the contents of `images/backgrounds/` on the server.
There is no staging environment — changes go straight to production.
Server-only setup (one-time, or when the schema changes):
- Apply the schema: `mysql -u <db_user> -p <db_name> < sql/schema.sql` (use `sudo mysql <db_name> < sql/schema.sql` if the database user lacks CREATE privileges).
- Upload `config/db_credentials.php` with the server's credentials.
- Add the cron entry: `* * * * * php /path/to/rfe/cli/poll_icecast.php`, run as a user that can read the credentials file.
- Make `images/backgrounds/` writable by the user PHP runs as.
- Create an admin account with `php cli/create_admin.php <username>`.
## Updating Show Information
Show info lives in the `#one` section of `index.php`. Previous shows are commented out rather than deleted, so there's a history of past broadcasts in the markup. When updating for a new show, comment out the current show block and add the new one above it.
Before going live, queue the show on the admin dashboard (or `php cli/set_show.php <slug>`) and point listeners to `live.php`.
## Planned Work
- `index.php` will be reworked into more evergreen content (About, schedules, etc.) and will link to `live.php`. Until then, nothing on the site links to `live.php`.
- The admin backend will grow to manage more of the site.