feat: polish

This commit is contained in:
Anatoly Antonov 2026-04-22 01:27:38 +09:00
parent 2e6177fe74
commit 4bd927bb4e
137 changed files with 6357 additions and 137560 deletions

105
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,105 @@
# 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 workspaces 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
<h5>{$t('panel.workspaces')}</h5>
```
Adding a locale: drop `xx.json` under `src/lib/i18n/locales/`, register it in
`loaders` and in the `Locale` type.
## Build & deploy
- `@sveltejs/adapter-static` with `fallback: 'index.html'` produces a pure SPA.
- SSR is disabled in the root `+layout.ts` (`ssr = false`, `prerender = false`).
- CSS is imported once in `+layout.svelte` (which loads `app.css`).
- No server-side routes. All data comes from the Django REST backend.