Skip to content

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