Files
Epic-The-Musical-Timeline/docs/ARCHITECTURE.md
T

71 lines
7.1 KiB
Markdown

# Architecture
How `epic-the-musical-timeline` is built today. Update in the same commit as any structural change.
## Overview
A Bun server (`Bun.serve`) with HTML-import routes serves the voyage app at `/`, the mockup gallery at
`/mockups/` and the design-round mockups at `/mockups/NN-slug/`, plus `/content/*` from an optional mounted
content folder. Bun's bundler turns each HTML page and its imported TS/CSS into hashed assets. `bun run
build` bundles the server and every page into `dist/`; the Docker image runs that bundle. The app reads the
album data (`src/data/album/`); the mockups read the frozen sample dataset. Every page mounts one shared
credits snippet.
## Map
| Path | Responsibility |
|---|---|
| `src/server.ts` | `startServer(port, { contentDir? })`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), `/content/*` via `src/content/routes.ts`, 404 otherwise. Runs when `import.meta.main`; `PORT` env, default 3000. |
| `src/mockups/index.html`, `index.ts`, `index.css` | Mockup gallery at `/mockups/`: static cards linking each mockup, then `mountCredits()`. |
| `src/mockups/registry.ts` | `mockups: MockupInfo[]` — slug, number, name, pitch, what to try. |
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`). |
| `src/data/types.ts` | `Saga`, `Song`, `Part`, `Motif`, `StoryTime`, `OdysseyRef`, `Video`. |
| `src/data/sample.ts` | `sagas`, `motifs`, `songs`. Placeholder lyrics; unverified fields `// TODO(verify)`. |
| `src/data/query.ts` | `songsWithMotif`, `motifsInSong`, `gapYears`, `sagaOf`. |
| `src/shared/credits.ts` | `FAN_NOTICE`, `mountCredits(target?)`. |
| `src/shared/audio-slot.ts` | `createAudioSlot(song)`: `<audio>` for `song.audio`, else an empty-state slot. |
| `src/shared/video-cards.ts` | `createVideoCards(song)`: YouTube link cards, no embeds. |
| `src/shared/credits.css` | Styles for credits, audio slot, video cards. |
| `src/content/routes.ts` | `contentResponse(req, dir)`: `/content/library.json`, `/content/files/<path>` (traversal-guarded, HTTP ranges), `/content/homer/<odyssey\|iliad>/<book>.json`. All 404 when no folder is mounted. |
| `src/content/library.ts` | `scanLibrary(dir)`: sagas (story order) and songs from `Songs and Lyrics/EPIC - <Saga> (<year>)/`; `.lrc` preferred over `.txt`. |
| `src/content/homer.ts` | `splitBooks(text)`: Gutenberg Butler text → books (heading, summary, paragraphs). |
| `src/shared/lrc.ts` | `parseLrc(text)`: timed lyric lines, sorted. |
| `src/app/index.html`, `main.ts`, `style.css` | The app page: stage (canvas, frieze layer, caption, timeline) and panel. `main.ts` wires store → controller → world/curtain, panel, timeline, play/prev/next, keyboard (←/→, Esc), the no-WebGL frieze stage. `style.css` holds the design tokens (see `DESIGN.md`). |
| `src/app/state.ts` | `AppState` (`songId`, `tab`, `motif`, `timeline`, `ribbons`, `read`), `parseState`/`stateToSearch` (URL), `createStore` (push on song change, replace otherwise, `popstate` restores). |
| `src/app/voyage.ts` | `transitionFor(from, to)`: same saga → sail; adjacent saga → that saga's curtain; further → fade; plus the target's landing. |
| `src/app/controller.ts` | Runs steps one at a time, last request wins, a cancelled step's curtain is lifted by the next. Order under a curtain: cover → sail → landing → horizon on the frieze ground line → lift. Play voyage (3.4s per song, silent), Back to sea. |
| `src/app/world/` | `createWorld(canvas)` (null without WebGL 2): `route.ts` (9 places, 40 stops, pure), stylised islands, ship, ribbons ("threads on the sea"), `landings/` cave, arena, hall. |
| `src/app/frieze/` | `friezeSvg(sagaId)` for the 9 sagas from a figure kit; `createCurtain(layer)`: drop, lift (part along the ground line, or zoom through a doorway), fade, `groundY()`. |
| `src/app/panel/` | `mountPanel`: About (facts, Odyssey reading view), Listen & lyrics (real player, line-synced lyrics, Watch), Paths (strip when the timeline is hidden, motif rows, nine-place chart), Fan works. |
| `src/app/timeline/` | `layoutTimeline` (pure: current saga expanded, others compact) and `mountTimeline`: stops, you-are-here, threads, hide/show, "Threads on the sea". |
| `src/app/content.ts` | Client for `/content/*`: library match by normalised title, lyrics (`.lrc` timed / `.txt`), Homer books, passage start by keyword. |
| `src/data/album/` | `sagas`, `songs` (40, album order, `landing`), `motifs` (character themes derived from singers, except Odysseus), `fanWorks` (empty in v1), query helpers. |
| `tests/` | `data.test.ts` (dataset invariants), `shared.test.ts` (DOM modules under happy-dom), `server.test.ts` (every route 200s, index links every registry slug, each mockup has the credits hook), `content.test.ts` (LRC, library scan, Homer split, `/content/*` routes on a temp fixture), `album.test.ts`, `state.test.ts`, `voyage.test.ts` (transitions, controller with fakes), `route.test.ts`, `frieze.test.ts`, `timeline.test.ts`, `panel.test.ts`, `app-content.test.ts`. |
| `Dockerfile` | Two-stage `oven/bun:1.3`: install + build, then copy `dist/` and run from `WORKDIR /app/dist`. |
## Data flow
1. Request hits `Bun.serve`; an HTML-import route returns the bundled page (dev: bundled on demand with HMR
via `bun --hot`; production: prebuilt assets from `dist/`).
2. The page's `main.ts` imports `src/data/*` and `src/shared/*` (bundled in) and calls `mountCredits()`.
3. App: the URL becomes `AppState`; every change goes through the store. The controller moves the stage
(curtain + world), and the panel and timeline re-render from the store. The panel fetches
`/content/library.json`, the song's audio and lyrics, and Homer books on demand; all of it is optional.
## External dependencies
Optional at runtime: the content folder named by `CONTENT_DIR` (real audio, lyrics, cover art, the Odyssey and
Iliad: Butcher & Lang / Lang, Leaf & Myers preferred, Butler as fallback). It is mounted read-only, never committed or baked into the image (spec 003). Without it
the site works and `/content/*` answers 404. Otherwise none. Fonts (`@fontsource/*`) and three.js are bundled from npm. Outbound links only: YouTube
(video cards) and Project Gutenberg (Odyssey references) — the site works without them.
## Invariants
- Every page renders `FAN_NOTICE` via `mountCredits()`.
- `src/server.ts` routes, `src/mockups/registry.ts` and the cards in `src/mockups/index.html` list the same slugs
(`tests/server.test.ts`).
- Every motif id used by a part exists; every motif appears in at least one song (`tests/data.test.ts`).
- No two mockups ship byte-identical `style.css` (the build fails otherwise).
- The album has 40 songs whose titles and order match the content folder (`tests/album.test.ts`).
- No lyric text in the repo; lyrics are fetched from `/content/` at runtime only.
## Known rough edges
- The production bundle only runs with `dist/` as the cwd (`bun run start`, Docker `WORKDIR /app/dist`);
`bun dist/server.js` from the repo root crashes. See `docs/TODO.md`.
- Routes, registry and index cards are three hand-maintained lists. See `docs/TODO.md`.