This page is safe to hand to an AI assistant — there are no secrets on it. It describes how dontpanic.fm is hosted and what you need to know to rewrite it without breaking anything.
What is actually serving this
Two copies of Caddy and a plain static file server. There is no PHP, no Node, no database, no framework and no build step. The HTML you see is the HTML on disk.
-
The Mac Mini runs Caddy v2.11.4 as the user
david. It serves the folder/Users/Shared/dontpanic.fm/directly, and proxies/listen/*to Icecast on port 4250 on the same machine. -
A Raspberry Pi 5 is the public door. It holds the
dontpanic.fmcertificate, terminates HTTPS, and forwards requests to the Mac Mini over the private Tailscale network. The Mini has no direct route from the internet, which is why the Pi exists at all.
Practically: you edit files on the Mac Mini, and the Pi publishes them. You never touch the Pi.
Where the files are
/Users/Shared/dontpanic.fm/
├── index.html the front page — players are built from streams.json
├── style.css placeholder styling, replace freely
├── streams.json the station list
├── on-air.html how to broadcast
├── authoring.html this page
└── CLAUDE.md working notes
Saving a file publishes it. There is no deploy step and no
cache to clear — the moment you write index.html, that is
what the public sees. A broken page is broken in public. Keep a copy of
anything you are about to replace: cp index.html index.bak.html
Getting in
ssh matt@macmini.tail32e87c.ts.net
cd /Users/Shared/dontpanic.fm
Or work locally on your own Mac and push the whole folder up:
rsync -av ./ matt@macmini.tail32e87c.ts.net:/Users/Shared/dontpanic.fm/
Both need Tailscale running. The folder belongs to you — no
sudo is required for anything on this page.
Four rules that will bite you
These are not style preferences. Each one produces a failure that looks like a server fault but is not.
1. Links need the .html extension
The file server does no extension guessing.
/on-air.html works; /on-air returns 404. Write
<a href="/on-air.html">, never href="/on-air".
2. Files must stay world-readable
Caddy runs as david, but this folder belongs to matt.
It can only serve what it can read. Files need 644 and
directories 755. An editor or script that writes
600 produces a 403 Forbidden that looks like the
server is broken. If a new file will not load, check this first:
chmod 644 newfile.html # or, for everything:
chmod -R u=rwX,go=rX /Users/Shared/dontpanic.fm
3. /listen/ belongs to the radio
Anything under /listen/ is proxied to Icecast, not read from
disk. Do not create a file or folder called listen — it
would be unreachable. Use those URLs, do not shadow them:
/listen/matt,/listen/stream,/listen/marvin— the audio/listen/status-json.xsl— which mounts are live, as JSON
4. streams.json drives the players
index.html fetches streams.json at page load and
builds one player per entry, then polls the status URL every fifteen seconds
to mark each station on air or offline. Adding a station is
a data edit:
{ "name": "Late Show", "mount": "late",
"url": "/listen/late", "description": "What it is." }
If you rewrite index.html from scratch you may drop this
mechanism entirely — nothing else depends on it. But if you keep the
players, keep the offline check: an <audio> element pointed
at a mount with no source fails silently, and a silent player reads as a
broken website.
What you are free to change
index.html and style.css are placeholders written to
be thrown away. Replace the markup, the layout, the typography, the colours,
the whole approach. Add as many pages, images, fonts and scripts as you like.
Nothing in the hosting setup cares what the HTML looks like.
The only fixed points are the four rules above and the fact that
/ serves index.html.
Putting Aether on the page
Aether Desktop is Tauri v2 — a Rust shell around a webview, with the
audio running as a Web Audio graph and a dsp-wasm/ crate already
in the workspace. Web Audio is a browser API, so the audio engine should run
in an ordinary browser tab with the Rust shell's jobs stubbed out. That makes
the shortest path to showing it off hosting the front end you already
have, rather than porting anything.
The wasm target is a user-level install, no sudo:
rustup target add wasm32-unknown-unknown
Three things worth considering, roughly in order of how native they are here:
-
Let it listen to the station. The mounts are proxied under
this same origin, so a page here can pull
/listen/streamthrough Web Audio and run an analyser over it. The toy becomes a visualiser for whatever is on air — different every time someone opens it. -
Let it broadcast. Anything that reaches the
/mattmount appears on the front page as a station. See how to get on air. -
Mind the downmix. Icecast carries stereo MP3, so a
four-track spatial field cannot survive the stream intact. Render binaurally
first — Web Audio's
PannerNodedoes HRTF panning — so the spatialisation is encoded into the stereo image rather than lost from it.
Leave these alone
-
The Caddy config (
/opt/homebrew/etc/Caddyfile) is David's. You do not need it: files here are already served, and anything you run on a port above 1024 is already reachable across Tailscale atmacmini.tail32e87c.ts.net:PORT. Ask him if you want a server-side process reachable at a public URL — that needs a config change. -
~/hitchhiker-fm/radio.liqis the running station. You can read it; do not edit it without telling David. - Docker containers on the Mini — chat bridges and a live evaluation. The machine is tight on memory; do not start more.
-
Big media goes on
/Volumes/Mega, never the internal disk, which has very little space left.
Checking your work
curl -sS -o /dev/null -w '%{http_code}\n' https://dontpanic.fm/
curl -sS -o /dev/null -w '%{http_code}\n' https://dontpanic.fm/yourpage.html
200 is good. 403 means permissions — rule 2.
404 means the filename or the extension is wrong — rule 1.