4.8 KiB
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 ondomain/.map/depends ondomain/(and Maplibre, internally).ui/depends ondomain/,state/,i18n/at most.features/*depend on everything inlib/but never on each other directly — cross-feature coordination happens through stores (e.g.workspacesStoreis 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
csrftokencookie (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:
<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-staticwithfallback: '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 loadsapp.css). - No server-side routes. All data comes from the Django REST backend.