13 KiB
RadioStore — development guide
This document describes the implemented Apple TV client, Swift packages, and React admin web app, how they fit together, and how to run them quickly. Roadmap and product intent remain in plan.md; compact editor handoff notes live in CURSOR_SESSION_CONTEXT.md.
1. Repository layout (monorepo)
| Path | Role |
|---|---|
Apps/RadioStoreTV/ |
Xcode tvOS 17 application. Depends on the local SPM package Packages/App (RadioStoreApp). |
Packages/Core/ |
Shared domain types (RadioStation, StreamKind, mock sample station). |
Packages/API/ |
Swift package RadioStoreAPI: REST client, DTOs, file-based mock client, RadioStoreAPIServing protocol. |
Packages/Player/ |
Playback: RadioPlayerController + RadioPlayerView (Siri Remote Play/Pause and surface up/down for mix volume). |
Packages/App/ |
Swift package RadioStoreApp: splash, tab shell, browse UI, profile placeholder, environment-driven API wiring, bundled JSON fixtures. |
admin-web/ |
React admin (Vite): login, stations, users — section 8 below and admin-web/README.md. |
backend/ |
Minimal Vapor 4 scaffold + Package.resolved — backend/README.md. Full /api/v1 + /admin/v1 still per plan.md. |
docker-compose.yml |
Profile compile: compile backend (swift:5.10-jammy) and admin (node:20-bookworm-slim); scripts/docker-compile-all.sh. |
2. Apple TV application (what ships today)
2.1 Entry point
- Target:
RadioStoreTVinApps/RadioStoreTV/RadioStoreTV.xcodeproj. - SwiftUI entry:
RadioStoreTVApp.swiftpresentsRadioStoreRootView()fromRadioStoreApp.
2.2 User-facing flow
- Splash —
RadioStoreSplashView: branding; Continue or short auto-advance. - Main shell —
RadioStoreMainView:TabViewwith two tabs:- Browse —
CatalogBrowseView: Geo → Country → genres (sections) → station → player. - Profile —
ProfileView: placeholder for Sign in with Apple and synced data (not wired yet).
- Browse —
2.3 Catalog data sources (Browse)
Browse resolves geos in this order:
- If an
RadioStoreAPIServingimplementation is active,fetchCatalog(bearerToken: nil)runs; success mapsCatalogPayloadinto UI models viaCatalogBrowseMapping. - On failure or when no API service is configured, the UI falls back to in-memory
CatalogMockData.
Which service is created is determined by RadioStoreProcessEnvironment (see §5) unless you pass an explicit live configuration into RadioStoreRootView.
2.4 Playback
RadioPlayerView+RadioPlayerControllerloadRadioStationfromCoreusingAVPlayer.
3. Swift packages and responsibilities
3.1 Core (Packages/Core)
| Piece | Description |
|---|---|
StreamKind |
mp3 | m3u8 (Codable). |
RadioStation |
id, title, imageURL, streamURL, streamKind; Hashable for navigation. |
MockRadioStation.eftelingLive |
Sample MP3 URL for simulator-friendly checks. |
Platforms: tvOS 17, macOS 14 (macOS supports running RadioStoreAPI unit tests that depend on Core).
3.2 RadioStoreAPI (Packages/API, Xcode SPM folder name API)
Product: library RadioStoreAPI.
| Piece | Description |
|---|---|
RadioStoreAPIServing |
Protocol implemented by both the HTTP client and the file mock; Browse depends on any RadioStoreAPIServing. Includes catalogTaskIdentity for SwiftUI .task(id:). |
RadioStoreAPIConfiguration |
Live API base URL only (no trailing slash); paths such as api/v1/catalog are appended by the client. |
RadioStoreAPIClient |
URLSession-based REST implementation aligned with plan.md §5. |
FileMockRadioStoreAPIClient |
Reads canned JSON from a directory; no network (§6). |
| DTOs | CatalogPayload / regions / countries / genres / stations (CatalogStationDTO uses Core.StreamKind). Session: DeviceRegisterRequest, DeviceRegisterResponse, AppleAuthRequest, AppleAuthResponse. User: FavoritesPayload, ListenEventsBatchRequest, ListenEventDTO. JSON uses snake_case keys via explicit CodingKeys. |
RadioStoreAPIError |
invalidURL, invalidResponse, httpStatus, encoding, decoding, fixtureMissing. |
Tests: run from repo root:
cd Packages/API && swift test
3.3 Player (Packages/Player)
Depends on Core. Exposes RadioPlayerController and RadioPlayerView only (UI + playback).
3.4 RadioStoreApp (Packages/App)
Depends on Core, Player, RadioStoreAPI.
| Piece | Description |
|---|---|
RadioStoreRootView |
Public entry: init(apiConfiguration:bundle:). Explicit RadioStoreAPIConfiguration forces live RadioStoreAPIClient and skips process-environment routing. Otherwise RadioStoreProcessEnvironment.resolveAPIBackend(bundle:) decides file mocks vs live vs none. |
RadioStoreProcessEnvironment |
Maps ProcessInfo.processInfo.environment to none | live | fileMocks. |
| Bundled fixtures | Packages/App/Sources/RadioStoreApp/Resources/MockAPI/ included via SPM resources: [.process("Resources")]. |
4. REST surface (client expectations)
The HTTP client calls paths relative to the configured base URL:
| Method | Path | Response / notes |
|---|---|---|
| GET | api/v1/catalog |
CatalogPayload |
| GET | api/v1/stations/{uuid} |
CatalogStationDTO |
| POST | api/v1/device/register |
DeviceRegisterResponse |
| POST | api/v1/auth/apple |
AppleAuthResponse |
| GET | api/v1/me/favorites |
FavoritesPayload |
| PUT | api/v1/me/favorites |
body FavoritesPayload, empty success |
| POST | api/v1/me/listen-events |
body ListenEventsBatchRequest, empty success |
Optional Authorization: Bearer … is supported where the protocol allows a token.
5. Debug mode: process environment (no server)
When RadioStoreRootView() is constructed without an explicit apiConfiguration, RadioStoreProcessEnvironment reads:
| Variable | Meaning |
|---|---|
RADIOSTORE_USE_FILE_MOCKS |
Truthy values: 1, true, YES, yes, TRUE. Enables FileMockRadioStoreAPIClient. |
RADIOSTORE_MOCK_FIXTURES_DIR |
Absolute path to a folder of JSON files. If unset while file mocks are on, the app uses the bundled MockAPI directory resolved via the RadioStoreApp resource bundle. |
RADIOSTORE_API_BASE_URL |
Live REST base URL when file mocks are off. |
Precedence: file mocks → live URL → no API client (Browse uses CatalogMockData only).
Xcode: Product → Scheme → Edit Scheme → Run → Environment Variables. The shared scheme RadioStoreTV.xcscheme ships disabled templates for these variables; enable RADIOSTORE_USE_FILE_MOCKS (and optionally RADIOSTORE_MOCK_FIXTURES_DIR) when debugging without a backend.
If RADIOSTORE_MOCK_FIXTURES_DIR points into the repo (for example the MockAPI source folder), you can edit JSON without rebuilding resource copies inside the app bundle. Verify the path matches your machine layout (especially $(SRCROOT)).
6. File mock fixture names
Under the chosen fixtures directory (bundled or RADIOSTORE_MOCK_FIXTURES_DIR):
| File | Used for |
|---|---|
catalog.json |
CatalogPayload |
station_{lowercase-uuid}.json |
Per-id station; if absent, station.json is used |
device_register.json |
DeviceRegisterResponse |
auth_apple.json |
AppleAuthResponse |
favorites.json |
FavoritesPayload |
replaceFavorites and appendListenEvents succeed without extra fixture files.
Shape reference: bundled catalog.json and Packages/API/Tests/RadioStoreAPITests/.
7. Quick start (development)
7.1 Prerequisites
- Xcode with tvOS 17 SDK.
- Apple TV Simulator (e.g. Apple TV) installed.
7.2 Run the app in Xcode
- Open
Apps/RadioStoreTV/RadioStoreTV.xcodeproj. - Select the
RadioStoreTVscheme and an Apple TV simulator. - Run (⌘R).
Default behavior (no scheme env vars): no HTTP client → Browse uses CatalogMockData; playback still works with the bundled demo stream URLs.
7.3 Browse using bundled JSON “API” (no server)
- Edit Scheme → Run → Environment Variables.
- Enable
RADIOSTORE_USE_FILE_MOCKS(1). - Optional: enable
RADIOSTORE_MOCK_FIXTURES_DIRand point it at
Packages/App/Sources/RadioStoreApp/Resources/MockAPI
(adjust path / use$(SRCROOT)as in the shared scheme). - Run again: Browse loads
catalog.jsonthroughFileMockRadioStoreAPIClient.
7.4 Browse against a real API
- Ensure file mocks are off.
- Set
RADIOSTORE_API_BASE_URLto your server origin (no trailing slash), for examplehttp://localhost:8080. - Mind ATS and simulator networking if you use plain HTTP.
Alternatively, pass RadioStoreRootView(apiConfiguration: RadioStoreAPIConfiguration(baseURL:)) from the app entry (forces live client; ignores env routing).
7.5 Command-line build (CI or sanity check)
From the repo root:
cd Apps/RadioStoreTV && xcodebuild \
-scheme RadioStoreTV \
-destination 'platform=tvOS Simulator,name=Apple TV' \
build
7.6 API package tests (macOS host)
cd Packages/API && swift test
7.7 Admin panel (React)
See admin-web/README.md. Short version:
cd admin-web && cp .env.example .env && npm install && npm run dev
Configure VITE_ADMIN_API_BASE_URL and CORS on the server. Screens: Login (email/password or pasted bearer token), Dashboard, Stations (list + edit/create), Users (read-only).
7.8 Docker: compile backend + admin
docker-compose.yml defines profile compile (nothing starts by default). Bind-mounts ./backend and ./admin-web, writes backend/.build (Swift PM layout inside that folder) and admin-web/dist.
# Optional: repo-root `.env` for `VITE_ADMIN_API_BASE_URL` during admin production build (see `.env.example`).
docker compose --profile compile run --rm backend-compile
docker compose --profile compile run --rm admin-compile
Or: scripts/docker-compile-all.sh.
The swift:5.10-jammy image used for the backend is large on first pull.
8. Admin web (admin-web/)
Stack matches plan.md §6: Vite, React 18, React Router 6, Bootstrap 5 + react-bootstrap.
| Area | Location |
|---|---|
| Routes / shell | admin-web/src/App.tsx, AdminLayout.tsx |
| Auth (stored bearer token) | AuthContext.tsx, ProtectedRoute.tsx |
| HTTP client | api/client.ts (Authorization: Bearer …) |
| Typed admin calls | api/adminApi.ts |
| Shared TS shapes | types/admin.ts |
JSON field names use snake_case (image_url, stream_url, …) so payloads align naturally with a future Vapor API.
9. Further reading
- Implementation plan — phases, MVP definition, backend/admin direction.
- Cursor session context — short-lived snapshot of last focus and constraints for tooling.