Stores¶
Shared state lives in src/lib/stores/*.svelte.ts: modules exporting plain
functions over runes ($state, $derived). Data fetched per page goes through
TanStack Query composables in src/lib/api/queries/ instead.
Gateway store¶
src/lib/stores/gateway.svelte.ts holds the device list and per-device detail:
gateways[], config drafts and pending changes, confirmation countdowns,
rollback notices, and the live-subscription map.
| Function | Purpose |
|---|---|
detailToGateway() |
Map a GetGateway response to the portal's Gateway type |
gatewayToConfig() |
Serialise portal state into the config_data JSON for SaveGatewayConfig |
upsertGateway() |
Insert or update a row |
applyChanges() |
SaveGatewayConfig → ApplyGatewayConfig |
subscribeToGatewayEvents() |
Open (or reuse) the viewer WebSocket for a device and feed frames into the store |
applyTemplate() |
Materialise a service profile into the objects it creates |
detachTemplateInstance() |
Stop managing a profile's objects as a unit, keeping them |
removeTemplateInstance() |
Delete a profile and the objects it created |
The editing pages call per-object mutators (addFirewallRule,
reorderFirewallRules, addInterface, renamePort, addNatRule,
addStaticRoute, addAlias, addService, addSchedule,
setIpForwarding, setSyslogForwarding, setInterfaceDhcp, the DNS policy
mutators, …). They change the draft only; nothing reaches the device until
applyChanges().
gatewayToConfig() is where fields get lost
It names fields explicitly, so a field added to Gateway and not to the
mapper is dropped on the next save and the setting silently does not stick.
src/lib/mapper-completeness.test.ts guards both mappers; the round-trip
tests in src/lib/stores/ (interface-roundtrip, rule-serialization,
syslog-roundtrip, state-mapping, pending-edits, …) guard specific
shapes. Add a config field, add it to both mappers.
Realtime (viewer WebSocket)¶
src/lib/realtime.ts exchanges the Zitadel token for a viewer ticket
(IssueViewerTicket) and opens
{VITE_REALTIME_WS_URL}/gateway/{deviceId}?role=viewer&ticket=…. The portal
acts on four frame types and ignores the rest:
| Frame | Handling |
|---|---|
status |
{online} from the DO when the gateway socket closes; updates status and freezes last_seen_at |
state_report |
Live interface state merged into the store without touching config |
heartbeat |
Keeps last-seen fresh; appends a CPU/memory/temperature sample to metrics-history |
command_result |
Routed to the waiting caller by commandId (diagnostics) |
Reconnects use exponential backoff from 1 s to 30 s, minting a new ticket each
time. Close code 4402 (session ceiling) is expected and reconnects normally.
subscribeToGatewayEvents() keeps one subscription per device.
Agent-update progress does not come from the socket: AgentUpdateCard polls
GetAgentUpdateStatus while a phase is in flight.
Other stores¶
| Store | State |
|---|---|
org.svelte.ts |
orgs[] from ListOrganizations, currentOrgId (persisted in localStorage as digilan.currentOrgId; empty = all orgs) |
devices.svelte.ts |
Device list for the admin pages |
fleet-summary.svelte.ts |
The fleet firewall summary as nexus computed it, with a local fallback |
metrics-history.svelte.ts |
In-memory CPU/memory/temperature series, seeded from GetGatewayMetrics and extended by heartbeat frames |
connection.svelte.ts |
/healthz polling, isConnected() |
mobile.svelte.ts |
Breakpoint and drawer state |
theme.svelte.ts |
Light/dark, in localStorage |
panel.svelte.ts |
Secondary panel expanded state |
toast.svelte.ts |
Transient notifications |
Per-user preferences¶
src/lib/preferences.ts stores a person's choices about the portal so they
follow them between browsers: dashboard pins, the firewall simple/advanced
view, dismissed equipment suggestions, folded panel sections. Values go to
nexus (GetUserPreferences / SetUserPreference, table user_preferences)
and to localStorage; reads come from the local copy first so the page draws
immediately, and the server's answer corrects it. Against a nexus without the
endpoint, the local copy is the whole behaviour.
Theme and the selected organization are not preferences; they stay in
this browser's localStorage.
Where state goes¶
| Kind of state | Home |
|---|---|
| Shared across components or surviving navigation | a store |
| Fetched from the API for a page | TanStack Query in src/lib/api/queries/ |
| A person's choice that should follow them | preferences.ts |
| Specific to this browser or component | localStorage or component state |