Repository navigation
Architecture
How the dashboard is put together and how a page request turns into fleet data.
No browser code calls reportmate-api. Every data path goes:
- A client component calls a same-origin path under
/api/.... - That route handler in
app/api/**/route.tsbuilds upstream headers withgetInternalApiHeaders()fromlib/api-auth.tsand fetches${API_BASE_URL}/api/v1/.... - The JSON is returned to the browser, in most routes with
Cache-Control: no-store.
Two things follow from this. The API base URL and the shared secret stay
server-side — they are read from process.env inside route handlers, never from
a NEXT_PUBLIC_ variable. And the user's own session is not what authenticates
the upstream hop: X-Internal-Secret authenticates the proxy to the API, so
the middleware has to be the thing that stops an anonymous browser from reaching
a data route. It answers /api/* with a JSON 401 rather than an HTML redirect
for exactly that reason.
lib/api-auth.ts picks the upstream credential in a fixed order:
API_INTERNAL_SECRET as X-Internal-Secret, else REPORTMATE_PASSPHRASE as
X-API-PASSPHRASE, else no auth header at all (which the API rejects).
middleware.ts runs on every path except static assets, api/auth,
api/healthz, api/health, api/transmission and api/version. In order it:
-
Resolves device identifiers. On
/device/<identifier>it classifies the identifier as a UUID, asset tag, device name, hostname or serial number. Asset tags, device names and hostnames are looked up against${API_BASE_URL}/api/v1/devices?limit=1000and redirected (HTTP 307) to the canonical/device/<serial>. Serial numbers and UUIDs pass through untouched. -
Bypasses auth for genuine localhost in development, and for the whole app
when
NEXT_PUBLIC_DEMO_MODE=true. -
Checks the session with
getToken. A kiosk viewer token is additionally restricted to GET/HEAD on an allow-list of read-only paths; anything else is 403 (API) or redirected to/events(page). -
Rejects or redirects — JSON 401 for
/api/*, a redirect to/auth/signin?callbackUrl=...for pages.
| Path | Contents |
|---|---|
app/ |
App Router pages and route handlers |
app/api/ |
The BFF proxy layer — every server route |
lib/ |
auth.ts (NextAuth options), auth-roles.ts, api-auth.ts, device-names.ts
|
src/components/ |
UI: tabs/, widgets/, tables/, cards/, navigation/, settings/, ui/
|
src/lib/data-processing/ |
Per-module normalizers that turn raw module JSON into view models |
src/lib/kiosk/ |
Kiosk token parsing, session minting, viewer allow-list |
src/lib/settings/ |
Types, defaults and migrations for the server-side settings document |
src/lib/installs/ |
Managed-install status and system-problem helpers |
src/lib/system/ |
Installed-update search and KB coverage for the System report |
src/hooks/ |
Data-loading hooks (useDeviceData, useInstallsData, useProgressiveData, SWR helpers) |
src/providers/ |
React context: SWR, theme-adjacent settings, platform filter, debug mode, demo mode |
e2e/ |
Playwright specs |
middleware.ts |
Auth, kiosk gating, device-identifier resolution |
The agents report per-module payloads (applications, hardware, identity, installs, inventory, management, network, peripherals, security, system). Two shapes of page consume them:
-
Fleet reports (
/hardware,/system, …) useuseDeviceDatafromsrc/hooks/useDeviceData.ts, which fetches/api/v1/devicesfor the inventory context and/api/v1/<module>for the module rows, then joins them client-side. -
Device detail (
/device/<serial>) fetches/api/device/<id>/infofor the header and/api/device/<id>/modules/<module>per tab, so a tab's payload is only pulled when that tab is opened.
Normalization lives in src/lib/data-processing/modules/*.ts, one file per
module, orchestrated by device-mapper-modular.ts. Some of the file names MODULAR_MIGRATION.md
in the repo lists do not exist; the modules/ directory is the source of truth.
calculateDeviceStatus in src/lib/data-processing/device-status.ts decides
the status every page shows, in this order:
- Archived — the device is archived.
- Storage — the API carries an inventory state from an asset inventory system saying the device is checked in to storage. A stored device is expected to be silent, so it never counts as stale or missing.
- Active — last seen under 24 hours ago.
- Stale — last seen under 7 days ago.
- Missing — anything older, or no last-seen time at all.
The dashboard, the devices list, the device page and every report page use the
same rule, and report pages share reportDeviceStatus for their Active, Stale,
Missing and Storage pills. Stored devices are left out of the installs error,
warning, pending and success drill-downs, and the fleet status donut gives them
their own slice.
The dashboard opens a WebSocket to an Azure Web PubSub hub using the
json.webpubsub.azure.v1 subprotocol. The token comes from
/api/v1/negotiate?device=dashboard — the same-origin proxy, because minting a
stream token needs the internal secret. If the negotiate call fails or returns no
URL (or takes more than 10 seconds), the connection pill reads polling and the
feed updates from the regular refresh alone. That refresh calls /api/dashboard
every 30 seconds whether or not the socket is connected: the socket carries
events only, and /api/dashboard is the sole source of the device list and of
the install statistics behind the Errors and Warnings tiles.
There is no shared cache tier. Route handlers set dynamic = 'force-dynamic' and
revalidate = 0, and pass cache: 'no-store' upstream. /api/v1/installs and
/api/v1/installs/data share an in-process cache in
app/api/v1/installs/shared.ts, and lib/device-names.ts keeps an in-process
serial-to-name map. /api/cache/status and /api/cache/invalidate exist but are
descriptive stubs — they report and acknowledge, they do not clear anything.
AGPL-3.0 · reportmate.app