diff --git a/.gitignore b/.gitignore index 2ba379a..8038817 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,6 @@ docs/superpowers/ # claude code per-project state .claude/ -# design handoff bundle from claude design — local reference only -.design-prototype/ -.design-prototype-stream/ - # generated configs and secrets config/*.env !config/*.env.example @@ -16,10 +12,6 @@ config/icecast.xml # runtime data data/ -# streamer build artifacts -streamer/node_modules/ -streamer/dist/ - # OS junk .DS_Store Thumbs.db diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4770aa3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,206 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What this is - -Self-hosted multi-station internet radio at `https://denpa.femboy.page`. Three docker services (Icecast, Liquidsoap, Astro frontend) plus a Caddy host service handle streaming, now-playing metadata, history, and the SPA. Each station is a folder on a Hetzner Storage Box with a `_meta.yml` and audio tracks; adding a station does not require a frontend rebuild. - -## Architecture - -``` - Caddy (host service, /etc/caddy/Caddyfile) - │ denpa.femboy.page - ┌──────────────────┬──────────┴──────────┬─────────────────┐ - │ │ │ │ - /.{mp3,opus} /api/* /now-playing/* / - │ │ │ │ - icecast:12000 frontend:12001 caddy file_server frontend:12001 - (libretime/ (astro hybrid SSR on /srv/denpa- (astro static) - icecast:2.4.4) via node adapter) radio/data - ▲ - │ source protocol on the compose network - liquidsoap (savonet/liquidsoap:v2.3.3) - │ reads /library//tracks - │ writes /now-playing/.json + .history.json - ▼ - /mnt/trashbox/denpa-radio/library/ ← Hetzner Storage Box (CIFS, RO bind) -``` - -Why `/now-playing/*.json` is served by Caddy directly (not the frontend): liquidsoap writes the files atomically, they are tiny static JSON, and serving them through Caddy means a frontend outage does not break now-playing readers (frontend itself fetches them client-side). - -## Library layout (the "by station" promise) - -On the Storage Box, mounted at `/mnt/trashbox/denpa-radio/library/` on summer: - -``` -/ # id must match [a-z0-9-]+ -├── _meta.yml # name, description, color, tags -├── cover.{jpg,png,webp} # optional; served by /api/stations//cover -├── tracks/ # audio files; subdirs supported (e.g. tracks/Volume Alpha/, tracks/Singles/) -└── jingles/ # optional, currently unused in v1 -``` - -Liquidsoap's `playlist()` recurses into subdirectories of `tracks/` by default — organize by album/source however you like. Any decoder-supported audio works (flac, mp3, opus, m4a, wav, ogg). Non-audio files in `tracks/` will be picked up and fail to decode (FFmpeg returns no audio stream); keep cover art and other non-audio at the station root, NOT inside `tracks/`. - -Adding a station: mkdir + `_meta.yml` + tracks → edit `config/liquidsoap.liq` to add a new station block following the `# === station: ===` pattern → tar-pipe the script and `docker compose restart liquidsoap` on summer. The frontend's `/api/stations.json` picks up the new entry within ~30s (Cache-Control max-age) without a rebuild. No Caddy reload, no Icecast restart. - -## Deployment target ("summer") - -- Host: `summer.node.femboy.page` (port 32020 for SSH; Forgejo git on 22). -- Stack lives at `/srv/denpa-radio/`. -- Caddy v2.10 is a system service, NOT containerized; config at `/etc/caddy/Caddyfile`. -- Disk constraint: ~33 GB free root; the library never lives on summer (it is on the Storage Box). - -### SSH - -The `summer` host alias is in `~/.ssh/config` (port 32020, user root) but does NOT specify an `IdentityFile`. Always pass the key explicitly: - -```bash -ssh -o IdentityAgent=none -o IdentitiesOnly=yes -i ~/.ssh/keys/devilreef summer '' -``` - -For git pushes (Forgejo on summer, port 22, user `git`): - -```bash -GIT_SSH_COMMAND="ssh -i ~/.ssh/keys/devilreef -o IdentityAgent=none -o IdentitiesOnly=yes" git push ... -``` - -### Transfer (no rsync on Windows Git Bash) - -Use a tar-pipe instead: - -```bash -cd /c/Users/user/Documents/Projects/personal/denpa-radio -tar -cf - | ssh -o IdentityAgent=none -o IdentitiesOnly=yes -i ~/.ssh/keys/devilreef summer 'cd /srv/denpa-radio && tar -xf -' -``` - -When piping large binaries through Git Bash, `cat | ssh ...` may corrupt UTF-8-interpreted bytes; use `dd if= bs=1M | ssh ...` if you see that. - -### Compose conventions (summer-server style guide) - -- All services have `restart: always`. (The original style guide says `unless-stopped`, but real crashes recover the same way and `docker kill` does NOT trigger a restart with either policy — only manual `docker start` or a real crash does.) -- Public ports bind to `172.17.0.1:` (docker bridge IP). Caddy proxies. Never bind to `0.0.0.0`. -- Allocated ports: `12000` icecast, `12001` frontend. -- Env files in `config/*.env` are gitignored; `config/*.env.example` is committed and copied + edited on summer at deploy time. -- Image pinning: databases/streaming infra to patch + distro (e.g. `libretime/icecast:2.4.4`, `savonet/liquidsoap:v2.3.3`). -- Healthchecks inside containers must use `127.0.0.1`, not `localhost` (alpine wget/curl resolves `localhost` to `::1` while Node binds IPv4 only). - -## Frontend (`frontend/`) - -Astro 5 with `output: 'static'` + `@astrojs/node` standalone adapter — Astro 5 merged "hybrid" into static, with per-route SSR enabled by `export const prerender = false`. Two React islands; everything else is `.astro` (zero JS). - -Dependencies are NOT installed on the dev machine via Astro CLI — install with `npm install` in `frontend/`. Most CLI invocations in this shell go through the direct binary because `npx ` may fail in the host's Bash: - -```bash -cd frontend -./node_modules/.bin/astro check -./node_modules/.bin/astro build -./node_modules/.bin/astro dev --host 127.0.0.1 --port 4321 -./node_modules/.bin/vitest run # all tests -./node_modules/.bin/vitest run src/tests/format.test.ts -./node_modules/.bin/eslint src -``` - -Path aliases `@lib/*`, `@components/*`, `@styles/*` are resolved by both `tsconfig.json` (for editor + Astro) and `vite.config.ts` (for Vitest). - -### Component layout - -- `src/pages/index.astro` is the only page. It calls `listStations(LIBRARY_ROOT)` server-side and passes the result to ``. `prerender = false` so this happens on each request — adding a new station folder is reflected within the cache window. -- `src/components/HeroPlayer.tsx` is the only large interactive island. Owns the `