# Don't Panic FM — working notes for Matt (and Matt's AI) This folder **is** the website. There is no build step, no framework, no deploy command. Caddy serves these files straight off disk, so saving a file publishes it. Reload the browser and you are looking at your change. ## Where things are | What | Where | |---|---| | This folder | `/Users/Shared/dontpanic.fm/` on the MacMini | | **Served publicly at** | **https://dontpanic.fm** | | Also served at | `https://fm.mini.private.fish` (the same files, tailnet only) | | Machine | `macmini` on the tailnet — `macmini.tail32e87c.ts.net` | | Project wiki | `https://dontpanic.mini.private.fish` | ⚠️ **This folder is PUBLIC.** Since 2026-08-29 the Pi5 fronts it at `dontpanic.fm` with a real certificate, so anything you save here is on the open internet immediately — no deploy step, no delay, no login. Do not put passwords, tokens, private recordings or anything unreleased in here. The tailnet address still works and serves the identical files; it is not a private staging area. ## The files - `index.html` — the page. Replace it entirely if you want; nothing else depends on its markup. - `style.css` — placeholder styling. None of it is load-bearing. - `streams.json` — **the station registry.** `index.html` builds one player per entry, so adding a station is a data edit, not a code edit. - `CLAUDE.md` — this file. Worth updating as you learn things. ## Editing Edit in place over the tailnet: ```bash ssh macmini cd /Users/Shared/dontpanic.fm ``` Check your change actually renders — this should print `200`: ```bash curl -s -o /dev/null -w '%{http_code}\n' https://fm.mini.private.fish/ ``` If you prefer to work locally and push, `rsync` over the tailnet: ```bash rsync -av ./ macmini:/Users/Shared/dontpanic.fm/ ``` ## The streams Audio comes from **Icecast** on the MacMini, port `4250`. The page reaches it through Caddy at `/listen/…` rather than hitting the port directly — a page served over `https` cannot play audio over plain `http`, so the proxy is what makes the players work at all. Keep that shape. Live mounts today: | Mount | Page URL | What it is | |---|---|---| | `/stream` | `/listen/stream` | Hitchhiker FM — the house station | | `/marvin` | `/listen/marvin` | Marvin Station, relayed from the Pi4 | Listener counts and current track come from Icecast's status JSON at `/listen/status-json.xsl`. `index.html` already polls it every 30 seconds. ### Adding a station to the *page* Edit `streams.json`. No other file needs to change: ```json { "name": "Late Show", "mount": "late", "url": "/listen/late", "description": "What it is." } ``` That only adds a player. The mount has to exist on the server too. ### Adding a station to the *server* The station itself is one readable Liquidsoap script: ``` /Users/david/hitchhiker-fm/radio.liq ``` Its own README puts it well: *"One machine, one stream, one readable script. The station is this file. Change the station by changing this file."* A new mount is an `output.icecast(...)` block. Reload with `/Users/david/hitchhiker-fm/bin/reload-stream`. ⚠️ **`radio.liq` is David's running station.** Hitchhiker FM is live on it and `finsburypark.tv` is served from it. Read it before you touch it, add rather than rewrite, and tell David when you have. If you break it, the `mksafe` wrapper keeps the mount alive with silence rather than 404ing — which means a mistake is quiet, not obvious. Check your work. ### Broadcasting live There is already a live DJ door — Liquidsoap's `input.harbor`: - host `macmini.tail32e87c.ts.net`, port `4261`, mount `/live` - password is in `/Users/david/hitchhiker-fm/secrets/harbor_password` - works with BUTT, Mixxx, or anything that speaks Icecast source protocol Connect and the station switches to you automatically; disconnect and it falls back to the playlist. Nothing to schedule, nothing to press. ## Your Rust soundtoy There is a Rust toolchain on the machine already — `rustc` 1.98, `cargo` on `PATH`, with `aarch64-apple-darwin` and `aarch64-unknown-linux-gnu` installed. Three ways to bring a soundtoy in, none of which needs root: **1. Compile it to WASM and drop it in.** The best fit, because it becomes part of this site with nothing to run or supervise. The wasm target isn't installed yet, but that is a user-level install — no `sudo`: ```bash rustup target add wasm32-unknown-unknown cargo install wasm-pack # if you want the JS glue generated ``` Build, then copy the `.wasm` and its loader into this folder. It is live the moment the file lands. **2. Run it as a native binary on a port.** It already builds for `aarch64-apple-darwin`. Anything above port 1024 runs as a normal user, and on the tailnet you can reach it directly — `http://macmini.tail32e87c.ts.net:8080` — with Caddy not involved at all. Good for websockets or anything that needs server-side state. Ask David if you want a proper hostname in front of it. **3. Make it a *source* and put it on air.** The most interesting option. If your toy generates audio rather than just playing it, it can stream into the station through the same harbor door a DJ would use — port `4261`, mount `/live` (see *Broadcasting live* above). Liquidsoap treats it exactly like a human DJ: connect and you are on air, disconnect and the playlist resumes. No new infrastructure, no scheduling, nothing to install. ## Showing the soundtoy off A soundtoy on a radio station's website can be more than an embedded demo. Some shapes worth considering, roughly in order of how native they are to this site: **Make it a channel.** Point the toy at the harbor door and it stops being a demo and becomes a station you can tune to — it appears in `streams.json` beside Hitchhiker FM and Marvin, and anyone with the URL can listen to it running. Generative work suits this unusually well: it never repeats, so a stream is a truer representation of it than a recording. **Make it listen to the station.** The mounts are already proxied under this origin, so a WASM build can pull `/listen/stream` through Web Audio, run an analyser over it, and react to whatever is on air right now. The toy becomes a visualiser or an effects layer for live radio rather than a separate thing on the same page — and it is different every time someone opens it, because the station is. **Give it a slot.** Not everything has to run continuously. A generative piece can hold a scheduled hour — an overnight show that only exists while it is being transmitted. There is no scheduler on this machine and there is not going to be one soon: AzuraCast, the obvious candidate, carries a vendor warning that Apple Silicon is not supported, so it will not run on this Mac Mini at all. Until something else fills that gap, a slot means the harbor door and a `cron`/`launchd` entry of your own. **Make the page a control surface.** Visitors change parameters in the browser; the toy's output goes out on air. The website stops being a page about the station and becomes an instrument the audience is playing. Worth thinking about what happens with two people at once — that constraint is usually where the interesting design decision is. The framing on the project wiki is that the radio is not coverage of the work but one of the places the work exists. A soundtoy that only sits in a page is coverage; one that is audible on the station is the work. ## The authoring guide There is a public page covering all of this for whoever rewrites the site next: It carries no secrets, so it can be handed to an AI assistant as a URL. It explains what serves the pages (two Caddys and a file server — no PHP, no Node, no build step), and the four rules that produce failures which look like server faults but are not: links need the `.html` extension, files must stay world-readable at 644 or Caddy 403s, `/listen/` belongs to the radio proxy, and `streams.json` drives the players. ## What not to touch You have a normal user account, **not** an admin one, and deliberately so — nothing above needs root. If you hit something that genuinely does, ask David rather than looking for a way around it. The MacMini also runs a lot besides the radio, and it is **tight on memory** — 8 GB with ~25 containers, swap almost full. So: - Don't start new Docker containers without asking. There isn't room. - Leave the `matrix-*` and `supabase_*` containers alone — chat bridges and a live evaluation. - `/opt/homebrew/etc/Caddyfile` is David's. You don't need it: static files here are already served, and a toy on a high port is already reachable over the tailnet. Ask him if you want a hostname in front of something. - Media files belong on `/Volumes/Mega` (288 GiB free), never the internal SSD (~11 GiB free). ## Going live — done `dontpanic.fm` went public on 2026-08-29. The shape, for reference: the MacMini has had no public ingress since 2026-08-11, so the **Pi5** terminates TLS and reverse-proxies here over the tailnet, rewriting the Host header to `fm.mini.private.fish`. Same pattern as `finsburypark.tv`. Files, media and Icecast all stay on this machine; the Pi is only the door. Still unbuilt: `studio.dontpanic.fm` (a station manager) and `listen.dontpanic.fm` (the streams on their own name). Both need a Caddy block from David. ## What's coming AzuraCast — a full station manager with playlists, scheduling, DJ handover and listener stats — is planned but **not installed**; the machine needs headroom first. The plan, with its checkpoints, lives on the wiki: For what the four `.fm` domains are and how they relate: