Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 5 additions & 22 deletions .claude/skills/scene-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ user-invocable: true

# scene-design

The one design skill for the **scene** monorepo. Both apps — [tracker](../../../apps/tracker)
(a FastTracker 2-style module player) and [party](../../../apps/party) (a
demoparty archive player) — share a **demoscene** identity layered on the shared
halo-design family tokens. The product _is_ retro, so the surface leans into a
FT2 / Amiga / DOS-demoscene look rather than the family's neutral chrome.

This skill covers the **shared scene identity** once, then the two apps' **deltas**.
Both scene apps share a **demoscene** identity layered on the halo-design family
tokens — the product _is_ retro, so the surface leans into a FT2 / Amiga /
DOS-demoscene look rather than the family's neutral chrome. Shared identity
first, then each app's deltas.

## Tokens

Expand Down Expand Up @@ -60,10 +57,7 @@ a demoscene **title-screen** treatment instead:
just an accent dot — it reads as a lit demoscene logo.
- **No dot** → no trailing accent period.

Lowercase and the terse voice still hold. This is exactly the kind of deviation
the family sanctions when it's documented with a reason (see the `halo-design`
"Wordmark" section) — this section is that documentation. Per-app text differs
(below).
Lowercase and the terse voice still hold. Per-app text differs (below).

## Icons & voice

Expand Down Expand Up @@ -111,14 +105,3 @@ the family sanctions when it's documented with a reason (see the `halo-design`
- **Layout** — landing is a grid of party **cards** (the glyph is the artwork
fallback when a party has no logo); the player stage is shared via
`@scene/player` (`PlayerStage`, with the `SampleBrowser`).

## Production sources of truth

- `packages/design/src/halo.css` — tokens (adopted halo-design), the theme store,
fonts. Consumed as `@scene/design`.
- `packages/player` — `@scene/player`: the shared libopenmpt engine + transport /
`PlayerStage` UI both apps embed.
- tracker: `apps/tracker/frontend/src/routes/{+page,+layout}.svelte`,
`apps/tracker/frontend/static/favicon.svg`.
- party: `apps/party/frontend/src/routes/{+page,+layout}.svelte`,
`apps/party/frontend/static/favicon.svg`.
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Siblings: deploy via `../raspi`; media sidecar pattern from `../scribe`.
packages/ shared FRONTEND libs (yarn workspace members, source-only)
player/ @scene/player — libopenmpt (chiptune3) engine + store + transport UI
design/ @scene/design — halo tokens, fonts, theme store
crates/ shared BACKEND libs (cargo workspace members)
scene-backend/ error/auth/CSP/SPA/scan plumbing shared by the app backends
apps/
tracker/ MOD/tracker-music player
backend/ tracker-backend (cargo member)
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"apps/tracker/backend",
"apps/tracker/integration",
"apps/party/backend",
"crates/scene-backend",
"services/transcoder",
]

Expand All @@ -14,6 +15,8 @@ edition = "2021"
version = "0.1.0"

[workspace.dependencies]
# Shared backend plumbing (error/auth/CSP/SPA/scan utilities) for the app crates.
scene-backend = { path = "crates/scene-backend" }
axum = { version = "0.8", features = ["macros", "tokio", "tracing"] }
rusqlite = { version = "0.40", features = ["bundled"] }
walkdir = "2"
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,17 @@ packages/ shared FRONTEND libs (yarn workspace, source-only)
player/ @scene/player — libopenmpt engine + transport UI
design/ @scene/design — halo design tokens, fonts, theme
apps/
tracker/{backend,frontend,e2e}
tracker/{backend,frontend,integration}
party/{backend,frontend,parties}
services/
transcoder/ ffmpeg sidecar (own image)
Cargo.toml one Rust workspace (all backends + e2e + transcoder)
Cargo.toml one Rust workspace (all backends + integration + transcoder)
package.json one yarn workspace (packages/* + apps/*/frontend)
justfile task runner
```

**Two workspaces, one repo:** frontends use yarn (Berry, vendored — no global
yarn needed); backends are one cargo workspace sharing a single `Cargo.lock` and
`target/`. Shared packages export raw `.svelte`/`.ts`; the consuming app's Vite
transpiles them (no build step in `packages/*`).
Two workspaces, one repo — yarn for frontends (Berry, vendored), one cargo
workspace for backends; conventions in [CLAUDE.md](CLAUDE.md).

## Develop

Expand Down
76 changes: 33 additions & 43 deletions apps/party/AMIGA-ROMS.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
# Amiga Kickstart ROMs (party app)

Amiga demos need a **Kickstart ROM** (the Amiga's boot firmware). ROMs are
copyrighted, so they are **not** bundled — you supply them. There are two ways to
provide one:

1. **Server-side** — drop the ROM in the party's support dir; it's served to all
visitors. Do this for ROMs you're licensed to redistribute in your deployment.
2. **Client-side upload** — a visitor supplies their own ROM in the browser; it's
injected into the emulator and never sent to the server (see *Client-side ROMs*).

Without a ROM, PUAE falls back to the built-in **AROS** replacement (lower
compatibility — many AGA demos misbehave or drop to a CLI).
Amiga demos need a **Kickstart ROM** (boot firmware). ROMs are copyrighted and
**not** bundled — supply them either **server-side** (drop in the support dir,
served to all visitors — for ROMs you're licensed to redistribute) or
**client-side** (a visitor uploads their own in the browser, never sent to the
server — see *Client-side ROMs*). Without one, PUAE falls back to the built-in
**AROS** replacement (lower compatibility — many AGA demos misbehave or drop to
a CLI).

## Which ROMs, and where

Place ROMs in **`<PARTY_ROOT>/.support/`** (the shared, unscanned support dir that
spans all parties — the same place as `results.txt`-adjacent assets). **Filenames
matter**: PUAE (libretro-uae) selects the ROM by the machine model's expected
filename, so the name must be exact.
Place ROMs in **`<PARTY_ROOT>/.support/`** (the shared, unscanned support dir
spanning all parties). **Filenames matter**: PUAE (libretro-uae) selects the ROM
by the machine model's expected filename — exact names only.

| filename | Kickstart | size | CRC32 | MD5 | used by (demo filename tag) |
| --- | --- | --- | --- | --- | --- |
Expand All @@ -29,7 +24,9 @@ filename, so the name must be exact.
> different ROMs** (different CRC). You can't rename one to the other.

ROMs must be **raw/decrypted** dumps (no `rom.key`, no `AMIROMTYPE1` header). The
CRC32/MD5 above are for the raw dumps — verify yours match.
CRC32/MD5 above are for the raw dumps — verify yours match. `.support/` (ROMs
included) is baked into the data image by `just package-party-data` — see
`parties/README.md`, Step 8.

## How the app picks the machine + ROM

Expand All @@ -52,21 +49,21 @@ just amiga "Desert Dream" # ambiguous → it lists the matches and stops
just amiga "/path/to/Demo (AGA).hdf"
```

**Why bother when the app runs them in a browser:** the in-browser core
(libretro-uae under EmulatorJS) has **no JIT**. A 68020+ demo pushing a full-rate
AGA display is exactly the workload that needs one, so AGA entries can crawl in the
SPA while being perfectly fine on the hardware they targeted. fs-uae JITs. That
makes this the way to tell **"this demo is broken"** from **"the WASM core is too
slow"** — a distinction you can't make from the browser alone, and the one that
matters when deciding whether an `.hdf` needs rebuilding.
The in-browser core (libretro-uae under EmulatorJS) has **no JIT**; fs-uae JITs.
A 68020+ AGA demo can crawl in the SPA and still be fine on its target hardware —
if it runs at speed in fs-uae, the image is good and the browser is the limit
(don't rebuild the `.hdf`).

The recipe derives the machine and Kickstart from the same filename tags as the
table above, so a local run reproduces what the app *aims* for. It mirrors the two
app defaults that bite if you hand-roll the command:

- **8 MB fast RAM on A1200.** The model preset implies it, but a bare
`--amiga_model=A1200` leaves any sizable demo aborting with *"not enough memory
available"* — the single most common reason a freshly-imaged demo "doesn't start".
table above, and mirrors the two app defaults that bite if you hand-roll the
command:

- **8 MB fast RAM on A1200.** The model preset implies it, but the individual
memory options default to fast = 0 and **override the preset** — any sizable
demo then aborts with *"not enough memory available"* / returncode 10 and drops
to the CLI. The single most common reason a freshly-imaged demo "doesn't
start". The app forces `puae_fastmem_size = "8"`
(`frontend/src/lib/EjsEmulator.svelte`); the recipe passes `--fast_memory=8192`.
- **`(030)` → `A4000` + `--cpu=68030`**, since fs-uae has no `A4000/030` model.

Equivalent by hand:
Expand All @@ -76,9 +73,9 @@ fs-uae --amiga_model=A1200 --kickstart_file="$PARTY_ROOT/.support/kick40068.A120
```

Floppy images (`.adf`/`.dms`/`.adz`/`.ipf`) go to DF0 instead of a hard drive; the
recipe picks by extension. Walking the tree over SMB takes minutes, so the image
list is cached at `~/.cache/scene-amiga-images.txt` and rebuilt only when a search
misseswhich also picks up newly-added demos on its own.
recipe picks by extension. The image list is cached at
`~/.cache/scene-amiga-images.txt` (SMB walks are slow) and rebuilt when a search
misses, which also picks up newly-added demos.

## Verifying a ROM

Expand All @@ -95,16 +92,9 @@ dump of your own hardware. Amiga Forever ROMs may be encrypted (`rom.key`); decr
to raw before placing (or keep `rom.key` alongside — libretro-uae can read encrypted
ROMs if the key is present, but raw is simplest).

## Deploy

`.support/` is packaged with the party data. `just package-party-data` strips macOS
junk (`._*`, `.DS_Store`) — those sidecar files next to the ROMs are harmless. Just
ensure the real ROM files are present with the exact names above.

## Client-side ROMs

When the server doesn't have a ROM a demo needs, the Amiga player shows an **upload**
control. The visitor picks their own ROM file; the SPA injects it into the emulator
(client-side only — the ROM is never uploaded to the server) and can remember it
(IndexedDB) so it isn't re-picked every launch. This lets a deployment ship *no*
copyrighted ROMs while still letting users who own them run the demos.
When the server lacks a ROM a demo needs, the Amiga player shows an **upload**
control: the visitor picks their own ROM, the SPA injects it into the emulator
(never uploaded to the server) and remembers it (IndexedDB). A deployment can ship
*no* copyrighted ROMs while users who own them still run the demos.
14 changes: 0 additions & 14 deletions apps/party/CREDITS.md

This file was deleted.

18 changes: 5 additions & 13 deletions apps/party/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
# loads `.env` from the working dir on startup. Copy to `.env` and adjust.
# Relative paths below resolve against `backend/`. Only PARTY_ROOT is required.

# Root of the Parties/ tree — each immediate subdirectory is one party.
# Quote the value if the path contains spaces.
# Root of the Parties/ tree — each immediate subdirectory is one party
# (config read from each party's `.party.json`). Quote paths with spaces.
PARTY_ROOT="/path/to/Scene/Parties"

# Per-party config now lives inside each party folder as `.party.json` (compos +
# scraped results), so the tree is self-contained and there's nothing to configure
# here — the backend reads <PARTY_ROOT>/<party>/.party.json for each party.

# SQLite cache (path index + parsed metadata). Rebuilt on demand.
PARTY_DB_PATH=party.db

Expand All @@ -32,13 +28,9 @@ PARTY_BIND=127.0.0.1:3020
# Bypass forward-auth for local dev / LAN-only deploys (do NOT set in prod).
PARTY_OPEN=1

# Kiosk = read-only public mode: refuses operator mutations (POST /api/rescan
# → 403) and redacts the filesystem root from /status; the SPA hides the rescan
# button. Orthogonal to auth (PARTY_OPEN = who can reach it; KIOSK = what actions
# exist). Turn ON for a public instance — especially the immutable data-image
# deploy, where the tree never changes so rescan is meaningless. /api/meta
# enrichment stays on (idempotent, content-hash-keyed). Off (unset) for the
# NAS-mounted admin/dev instance that needs rescan.
# Kiosk = read-only public mode: rescan → 403, /status root redacted, SPA hides
# the rescan button. Orthogonal to PARTY_OPEN (auth vs allowed actions); /api/meta
# enrichment stays on. Set for public / data-image deploys.
# PARTY_KIOSK=1

# Transcoder sidecar. Leave unset until the service exists; image/video assets
Expand Down
1 change: 1 addition & 0 deletions apps/party/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ name = "party_backend"
path = "src/lib.rs"

[dependencies]
scene-backend = { workspace = true }
axum = { workspace = true }
rusqlite = { workspace = true }
walkdir = { workspace = true }
Expand Down
Loading
Loading