# Architecture ## Layer overview ``` src/ ├── routes/ # SvelteKit pages (thin; they compose features) ├── app.html # plain SPA shell ├── app.css # global styles (imports Bootstrap + bootstrap-icons) └── lib/ ├── domain/ # pure types + pure functions (no Svelte, no fetch) ├── state/ # store factories (persisted, broadcast across tabs) ├── api/ # HTTP client + resource modules (points, scenarios…) ├── auth/ # auth store, login/logout, requireAuthenticated() ├── i18n/ # t() store, locale loading, message dictionaries ├── map/ # IMap interface + MapLibre impl + plot helpers + tools ├── ui/ # library-agnostic primitives (panel, tab, editor…) └── features/ # vertical slices — one folder per feature ├── auth/ # Navbar, LoginForm ├── footer/ ├── prediction/ # ControlPanel, ScenarioPanel, Point/Scenario/Curve editors ├── tracking/ # TelemetryPanel ├── workspaces/ # multi-layer workspace system ├── timeline/ # global playback clock └── settings/ # schema-driven settings panel mocks/ # dev-server API mock (enabled via VITE_USE_MOCK_API) ``` **Rules of thumb** - `domain/` depends on nothing. - `state/`, `i18n/`, `api/` depend only on `domain/`. - `map/` depends on `domain/` (and Maplibre, internally). - `ui/` depends on `domain/`, `state/`, `i18n/` at most. - `features/*` depend on everything in `lib/` but never on each other directly — cross-feature coordination happens through stores (e.g. `workspacesStore` is shared by prediction + timeline). - `routes/` compose features. Pages stay thin. ## Data flow ### Stores `$state` → persisted via `persisted()` (localStorage + BroadcastChannel for cross-tab sync). `workspaces`, `settings`, and `auth` state live here. ### API calls All go through `$api` (`src/lib/api/client.ts`) which: - prepends `VITE_API_BASE_URL` - reads the `csrftoken` cookie (priming it via `/api/csrf/` if missing) - serializes JSON bodies and parses structured DRF errors into `ApiError` - delegates 401s to the auth layer (`setUnauthorizedHandler`) ### Auth `authStore` is the single source of truth for who is logged in. Pages that require auth call `requireAuthenticated()` from `$auth` in `onMount` — this refreshes state and redirects to `/login` if anonymous. The API client triggers the same redirect on 401s. ### Map The map library is behind the `IMap` interface. Every plot operation (line, marker, circle) runs through a **Scene** — a named collection of layers owned by a feature. When a feature is done with its layers, `scene.clear()` or `map.disposeScene(name)` removes them all at once. This is how each workspace paints an independent trajectory without stepping on its neighbors. Swapping MapLibre for another library means implementing `IMap` once (see `src/lib/map/maplibre.ts`) and updating `createMapLibreMap()` at one call-site inside `Map.svelte`. ### Workspaces A workspace is an independently-configured prediction layer. The user creates several, edits their flight parameters separately, runs each one, and sees all their trajectories overlaid on the same map with their own colors and opacities. `workspacesStore.run(id)` calls the prediction API and stores the result in-memory on that workspace. `WorkspaceRenderer.svelte` listens to the workspaces store and repaints layers when workspaces change. It also drives the **global timeline**: whenever workspaces change it recomputes the global `[min, max]` time range (the union of every visible workspace’s trajectory span) and updates the timeline store. ### Timeline `timelineStore` is a plain writable backed by a `requestAnimationFrame` loop. It exposes `play`/`pause`/`seek`/`setSpeed`. `WorkspaceRenderer` subscribes to `time` and draws a cursor marker on each visible workspace at that timestamp. Workspaces stay in sync because they all sample the same clock. ### i18n `initI18n()` (called from the root layout) loads the saved locale from localStorage, imports the JSON dictionary, and stores it. Components use the `$t` store from `$i18n`: ```svelte