web-console
Responsibility
Section titled “Responsibility”apps/web-console is the permanent React administration UI. It owns
presentation, form state, bounded list pagination, and a typed HTTP client. It
does not decide whether an operation is authorized; platform-api repeats all
authentication, tenant, organization, and role checks.
Operational tables for tenants, users, memberships, devices, device types, and Flows
use forward cursor responses with a browser-held cursor
history for previous-page navigation. They offer page sizes of 25, 50, and 100,
and reset to the first page when scope or filters change. Tenant, Device Type,
Device, User, and Membership pages display the API’s exact filtered
totalCount; other tables do not infer totals that their APIs do not provide.
Tenant and Device Type search, status, and page size, Device filters and page
size, and User and Membership search, status, role, and page size are URL state,
while the current page’s cursor
chain is session-bound browser-history state. A detail return restores it, a
reload starts on page 1, and a successful mutation reloads the current page or
its predecessor when the page became empty.
The Flow selector searches 50 results at a time and exposes an explicit continuation action. The selected Flow remains URL state and is restored through the singleton API when it lies beyond the first selector page. Flow-version history appends 25 older entries only after an explicit action, preserves loaded entries on failure, and never displays an invented exact total.
Device-Type selectors in Device forms, inventory filters, Flow creation, manual Flow tests, and template instantiation search the server and expose an explicit continuation action. Organization selectors use the scoped hierarchy search and show visible ancestor paths; inaccessible branches are never offered.
The counted administration UIs and their Platform API responses are one compatibility unit. Roll back both images to their previous compatible pair, or forward-fix the unmatched image when a paired rollback is unavailable.
React Router runs in declarative browser mode. Canonical routes are ordinary
paths such as /tenants/17/devices/{deviceId}; the central route module owns
path generation, canonical positive-number validation, aggregate UUID
validation, route metadata, and known legacy-hash
mapping. Components do not assemble routes independently. Known old hash links
are replaced once in browser history. Unknown, syntactically invalid, invisible,
or unauthorized resources render the same neutral not-available state.
Tenant, Device, Device-Type, and Raw-message details are dedicated routes.
Overview, Dashboards, Organizations, Flows, Users, Memberships, Audit, and Admin
remain tenant-owned routes. /tenants and /raw-ingestion are global routes
without an active tenant. A separate Administration group exposes the temporary
IOTA import only to platform administrators. Navigation hides administration
and raw-data views when the current identity lacks the corresponding role. That
is a usability feature, not a security boundary.
Startup and session flow
Section titled “Startup and session flow”- The browser loads
/runtime-config.jsonwithno-store. - The client validates
service, environment, version, and API base URL. - It requests API diagnostics and attempts a refresh of an existing session.
- With no valid session it displays the login screen.
- Login returns an access token to the client and sets the rotating refresh token as a host-only HttpOnly cookie on the API origin.
- The access token remains in memory. It is not written to local storage.
- A temporary-password account is restricted to the password-change screen.
The tenant selector in the sidebar header loads only tenants visible to the
current identity. It requests 50 rows per page, searches on the server, and
loads each continuation only after the user selects Load more tenants.
Failed continuations retain the current options and remain retryable; repeated
or concurrent requests for one cursor never append an option twice. A search
change invalidates earlier search and continuation responses so that late
responses cannot replace or extend the current result. The selector exposes a
keyboard-operable combobox and listbox; users can move from the search field to
an option with the arrow keys and select it with Enter. The selected tenant
comes from the canonical URL and is
loaded through GET /v1/tenants/{tenantNumber}. The number is passed to Tenant
collections and creates; detail and subresource calls use flat aggregate API
routes. The internal Tenant UUID remains available only for local permission
evaluation. The first visible Tenant is only the root-route default,
not the authority for an already addressed route. Switching tenants always
opens /tenants/{tenantNumber}/overview. The breadcrumb uses the same route
metadata and links every completed hierarchy level. The
sidebar footer renders profile details from GET /v1/auth/me and provides the
session logout action; it does not infer or cache additional identity data.
Membership User and Organization selectors request 50 rows per server-search page and load continuations only after an explicit action. Current options survive a failed continuation and the action remains retryable. Duplicate or concurrent cursor requests cannot append duplicates, and a query change invalidates late responses. Organization targets come from the hierarchy search contract and are restricted to active, actor-manageable units. Membership table labels are resolved only for the current page, never from a silently truncated collection response. The labels are embedded in the authoritative Membership page contract, so a 100-row table still performs one list request rather than User and Organization detail requests for every row.
The fixed dashboards view selects exactly one Device through a paginated,
server-searched picker or the
/tenants/{tenantNumber}/devices/{deviceId}/dashboard deep link. It polls
only the current snapshot every 60 seconds while the tab is visible. Timeseries
history loads on Device or range changes and explicit refresh, remains bounded
to 30 days and 5,000 rows, and is never persisted as dashboard configuration.
The collection navigation inventory maps every table, selector, timeline and bounded detail collection to its API contract and verification evidence.
Raw lists retrieve metadata only. A Raw detail route requires both the Raw ID
and its receivedAt TimescaleDB key, retrieves payload bytes on entry, and
checks tenant ownership again for tenant-owned routes. Authorized platform
administrators can queue a confirmed replay with a fresh browser-generated
Idempotency-Key and then refresh its execution status explicitly.
Raw, Audit, and Device assignment-history investigations append 25 records only after Load older entries. A fixed upper anchor survives every continuation; Refresh invalidates pending responses and starts with a new anchor. A cursor is reserved synchronously, so repeated or concurrent activation appends one logical page at most once. Failures preserve loaded evidence and expose the same retry action. Loaded counts are deliberately not exact timeline totals.
The typed client validates administrative and telemetry responses with the
authoritative runtime schemas from @sens-platform/contracts. Response objects permit
additive fields, while known fields, identifiers, statuses, timestamps, and
bounded metadata must still satisfy the shared contract. An invalid response is
treated as an API contract failure rather than trusted by the UI.
IOTA source URL, username, and password remain local state of the Admin view. They never enter routing, the authentication context, Local Storage, Session Storage, or IndexedDB. The password is cleared after every import attempt.
Static runtime server
Section titled “Static runtime server”The Vite build is served by apps/web-console/server.mjs as a non-root Node.js
process. It provides:
/healthz,/readyz,/version, and/metrics;/runtime-config.jsongenerated from validated environment values;- immutable caching for hashed assets and no-cache behavior for the app shell;
- SPA fallback for client routes;
- Content Security Policy,
frame-ancestors 'none', no-referrer, permissions, and content-type protections.
The CSP permits inline style attributes through
style-src-attr 'unsafe-inline' because the locally bundled Radix primitives
calculate overlay positions and responsive layout variables in the browser.
Stylesheets remain restricted to style-src 'self'. Each HTML response also
receives a cryptographically random CSP nonce, which the bundled Radix
scroll-lock helper applies to its one dynamically generated style element.
Scripts remain restricted to script-src 'self'. These exceptions do not
permit inline script execution or third-party style origins.
The server has no database or Secret dependency. Readiness proves that the
static service is ready, not that platform-api is ready; the browser displays
the API diagnostic state separately.
Configuration
Section titled “Configuration”SENS_WEB_CONSOLE_API_BASE_URL is the only application connection and contains
no credentials. The API must allow the exact web-console origin for
credentialed login and refresh requests. See
Runtime Configuration.
Failure diagnosis
Section titled “Failure diagnosis”| Symptom | First check |
|---|---|
| Startup screen never completes | Browser network result for /runtime-config.json, then API /readyz. |
Login works with curl but not in the browser | Exact API CORS allowlist, browser Origin, and cookie response. |
| A navigation item is missing | Current role and selected tenant from GET /v1/auth/me. |
| UI and API report different versions | Argo CD image selections and both /version endpoints. |
| Deep link returns 404 | Static server SPA fallback and Ingress rewrite behavior. |
Tests live beside the client, runtime configuration, auth screens, routes, permissions, views, and static server. Customer behavior is documented under Platform.
Optional browser telemetry
Section titled “Optional browser telemetry”Browser Sentry loads from runtime configuration and sends sanitized exceptions
without PII, replay, profiles, attachments, console breadcrumbs, or public
source maps. PostHog loads only after explicit consent and authenticated
pseudonymous identity. Autocapture, pageviews, page-leave capture, replay, and
person profiles are disabled. The browser SDK also disables PostHog feature
flags and remote configuration because the adapter uses no remotely managed
PostHog features. The browser therefore does not request PostHog’s asset/config
host, which keeps third-party origins out of script-src. Logout or consent
revocation resets the local analytics identity; low-level product correctness
never depends on analytics.