# Beacon - repository guide A self-hosted music client: an Electron desktop app (and a Docker/web build of the same frontend) for Navidrome/Subsonic, Jellyfin and Plex, with casting to Sonos, AirPlay, Chromecast and DLNA. The user-facing description lives in `README.md`; this file is the working context - how the pieces fit, what the conventions are, and which mistakes have already been made once. ## The two halves **`src/` - the frontend** (Vue 3 **Options API**, Vuetify 4, Pinia, TypeScript) | Path | What it is | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `src/main/index.ts` | Electron main process (~700 lines): the window (one instance, size and position remembered), the Windows taskbar buttons, auto-update, and spawning the bundled connect binary. Media keys are the renderer's (`services/mediaSession.ts`) | | `src/preload/index.ts` | The context bridge. Thin, and it has to stay that way | | `src/renderer/src/` | The whole app. `views/` (routed pages, `views/mobile/` for the phone shell), `components/`, `stores/` (Pinia), `services/` (everything that is not a component: `subsonic/` the API client, `connect/` the backend client, `playback/`, `lyrics/`, `library/`), `layouts/`, `i18n/locales/` (en, de, es, fr, it) | **`connect/` - the backend** (Python 3.13, FastAPI, `uv`) | Path | What it is | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `connect/main.py` | App bootstrap. `load_dotenv()` runs _before_ the `core.*`/`routes.*` imports on purpose - several of them read their config at module import time | | `connect/routes/` | HTTP surface. `proxy.py` is the important one: `/rest/*` is a transparent passthrough to the media server, or to `media/jellyfin_bridge.py` / `media/plex_bridge.py`, which speak Subsonic on their behalf | | `connect/core/` | Sessions, auth, the playback clock, radio (relay, ICY metadata, history), waveform, recommendations | | `connect/delivery/` | One module per cast target: `sonos.py`, `airplay.py`, `chromecast.py`, `dlna.py`, coordinated by `manager.py` | | `connect/static/remote/` | The phone Remote Control UI - plain JS, no build step, served by connect itself | | `connect/static/party/` | Build output (gitignored) of the party-mode guest page, `src/renderer/src/party/` via `pnpm build:party`. Reachable without a login, so `routes/party.py` serves only it and checks everything (see `docs/help/guides/party-over-the-internet.md`) | The frontend never talks to the media server directly: every call goes through connect's proxy, which is also what makes Jellyfin and Plex look like Subsonic to everything above it. Casting is entirely connect's job - the speaker fetches from connect, not from the app. ## Commands ```bash pnpm dev # frontend + backend together (this is the one you want) pnpm test:unit # frontend unit suite (jsdom) pnpm test:layout # the *.browser.test.ts suite, real Chromium via Playwright pnpm type-check # vue-tsc pnpm lint # oxlint + eslint, both with --fix pnpm format:check # prettier pnpm screenshots # README screenshots as WebP, set up in scripts/screenshots/shots.mjs cd connect && uv run pytest # backend suite cd connect && uv run --group dev ruff check . # backend lint cd connect && uv run --group dev ruff format . # backend format (line length 100) ``` CI runs exactly these (`.github/workflows/test-frontend.yml`, `test-python.yml`), so a green local run means a green CI run. ## Testing Three layers, and which one a test belongs in is a real decision: - **jsdom** (`**/__tests__/*.test.ts`) - the default. Logic, stores, services, component behaviour. - **Real browser** (`**/__tests__/*.browser.test.ts`, `pnpm test:layout`) - only for what jsdom cannot answer: computed layout (container queries, `clamp()`, flex-wrap resolution, 3D transforms) and IndexedDB. jsdom would "pass" these against a fake layout without checking anything. - **pytest** (`connect/tests/`) - the backend, including the Jellyfin/Plex bridges and every delivery target. ### How much to test where The two halves are deliberately not held to the same standard. **The backend gets thorough coverage.** A fault there surfaces as silence in another room, across three device classes, often on hardware that isn't on the development machine - the suite is the only place it can be caught early. **The frontend is tested on function, not on appearance.** Its backend-shaped half - `stores/`, `services/`, everything that computes, reconciles or holds state - counts as logic and gets the same thorough coverage as `connect/`; that is where the real bugs have been. Components are tested for behaviour: a click does what it should, an empty result shows the empty state, an error path is reachable. Layout is the exception, not the routine - and only what jsdom genuinely cannot answer (see the browser layer above). **Don't pin down what is meant to change.** Counts of rendered elements, their order, class names, copy, sizes - a test over those never finds a bug and turns every deliberate design change red. Before writing one, ask what a failure would mean: a real defect, or someone having redesigned something on purpose? Only the first is worth a test. This is the question the mutation check below does _not_ answer - a skeleton-loader count passes it happily and is still worth nothing. When a test like that goes red on an intentional change, delete it rather than adjusting its numbers. Coverage is reported over _every_ renderer source file, not only the imported ones - a number that rises as coverage gets narrower is worse than none. But a renderer coverage percentage is not a goal in itself, and neither half's number is the measure: **check a test by breaking the code it covers.** If the suite stays green, the test does not test. ## Conventions ### Style and UI `docs/styleguide.md` is the design system - **read it before building or reshaping any UI**, and update it when a decision in it changes. Its enforceable half lives as shared classes in `src/renderer/src/assets/base.css` (`.beacon-panel`, `.panel-title`, `.section-title`, `.eyebrow-label`, `.beacon-dialog`, `.beacon-markdown`, `.mobile-row`); `docs/styleguide.html` is the same thing rendered, for looking at rather than reading. Two rules that come up on every frontend change: - **No Vuetify utility classes in templates.** No `d-flex`, `align-center`, `w-100`, `mb-4`. Layout goes in a scoped `