Standalone, read-only runtime-trace observability console for the Protean dynamic-module platform.
A single-page app that consumes the control-plane REST of any Protean-enabled
Spring application and renders its request traces and per-module metrics. It is
not coupled to any one backend and ships nothing server-side — it talks to the
platform over an SSE stream (with a fetch fallback) through the Vite dev proxy.
- Context
- Features
- Screens
- Quick Start
- Installation
- Configuration
- Architecture
- Platform API surface
- Scripts
- Dependencies
- Authentication
- Roadmap
- Reference
- License
Protean is a dynamic-module platform for Spring apps: consumers deploy, hot-swap,
and isolate modules at runtime. Every request that flows through a module is
captured as a RequestTrace, and per-module counters roll up into a
ModuleMetricsSnapshot. Protean exposes these over a read-only control-plane
REST surface (/platform/traces, /platform/traces/metrics, /platform/modules).
This console is the operator-facing view of that surface. It is intentionally thin: one data layer that mirrors the platform's Java records, and a dashboard that maps 1:1 to them. Because it is decoupled, the same build points at any Protean-enabled app just by changing the dev proxy target.
traceId is the correlation id shared across the trace table, application logs,
and RFC 9457 error bodies — so a trace in this console is the same id you grep for
in logs.
- Live SSE stream — one connection to
/platform/traces/streammultiplexestrace/metrics/modules/summaryevents pushed ~1×/s, with manual Connect / Disconnect control. Replaces the previous 5s REST polling. - Connection ack — the platform's
readyframe names itself and its version, states its trace-ring capacity, whether traces and per-module metrics are recording, and its push period. The console reports those instead of assuming them: the silence watchdog is sized from the stated period, the header prints the stated ring size, and the module table stops guessing why it is empty. A platform that sends no ack keeps working — the console then says it does not know rather than falling back to a number nobody gave it. - Live / sample auto-fallback — when a platform is reachable, data is
live; when none is running, the console falls back to grounded mock data so the UI is always explorable. The top bar shows a LIVE vs SAMPLE DATA badge. - KPI row — cumulative headline counters (request volume, error rate,
latency, active modules), with a recent-window trend sub-line and isolation-mode
split from the
summaryevent. Trends are hidden (not faked) with no baseline. - p95 latency chart — single-hue telemetry series, bucketed per minute, with data-driven axis ticks.
- Status mix — distribution of request outcomes at a glance.
- Module metrics table — per-module counters joined with live module status
(isolation mode, trust tier) from
/platform/modules; a detail drawer per row. - Recent traces table — columns map 1:1 to
RequestTrace; click atraceIdto copy it. - IndexedDB trace history — traces are retained across the platform ring-buffer eviction and reloads, with infinite-scroll "Load older" and a two-step Clear.
- Virtualized tables — the metrics and trace tables render only visible rows, with persisted header-click sort, search, and errors-only filters.
- Login shell (stub) and persisted UI state (theme / sort), with a first-paint theme script that avoids a dark-mode flash.
- Accessible status color — ok/warn/crit is always paired with an icon/label, never color alone; charts stay single-hue (no red/green categorical).
The dashboard is a single scrollable view: top bar → KPI row → latency chart + status mix → module metrics table → recent traces table. On first load (or while a fetch is in flight) it renders skeletons rather than an empty screen.
Shown with SAMPLE DATA (no live platform); the top-bar badge flips to LIVE when a Protean app is reachable.
# 1. install deps
npm install
# 2. point the dev proxy at your Protean app (optional — defaults to :8080)
cp .env.example .env # then edit VITE_PROTEAN_TARGET
# 3. run the dev server
npm run devOpen the printed URL (default http://localhost:5173). With no platform running
you'll see SAMPLE DATA; start a Protean app on the target host and the SSE
stream switches it to LIVE on the next event.
- Node.js ≥ 20.19 (developed on v26). Vite 8 requires a modern Node LTS.
- npm (lockfile committed). Any npm-compatible client works, but
npm cireproduces the committedpackage-lock.jsonexactly.
git clone git@github.com:htcom-code/protean-console.git
cd protean-console
npm ci # or: npm install
npm run devConfiguration is via Vite env vars (.env, gitignored — see .env.example):
| Variable | Default | Purpose |
|---|---|---|
VITE_PROTEAN_TARGET |
http://localhost:8080 |
Target Protean-enabled Spring app. The dev proxy forwards /platform/* to this host. |
The proxy (in vite.config.ts) means the browser only ever talks to the Vite dev
server on the same origin — no CORS setup needed against the platform.
src/
├── main.tsx # entry
├── App.tsx # dashboard layout + selection/auth state
├── components/ # presentational; take plain props
│ ├── top-bar.tsx # LIVE/SAMPLE badge, connect/disconnect, theme toggle
│ ├── connection-banner.tsx
│ ├── kpi-row.tsx # cumulative values + windowed summary trends
│ ├── latency-chart.tsx # single-hue p95 series
│ ├── status-mix.tsx
│ ├── module-table.tsx # metrics ⋈ module status (virtualized)
│ ├── module-detail-panel.tsx
│ ├── trace-table.tsx # RequestTrace rows (virtualized), click-to-copy traceId
│ └── login-screen.tsx
├── hooks/
│ ├── use-console-data.ts # SSE stream (EventSource) + connection state
│ ├── use-trace-store.ts # IndexedDB trace history + load-older
│ ├── use-persistent-state.ts
│ ├── use-auth.ts
│ └── use-theme.ts
└── lib/
├── api.ts # HTTP boundary: REST fallback, module routes, p95 derive
├── types.ts # mirror of the Java records — keep in sync
├── mock.ts # grounded fallback data (live: false)
├── trace-db.ts # IndexedDB access + trace key
├── format.ts # number/latency formatting (tabular-nums)
└── utils.ts
Key conventions
- The data layer is the only place that knows about HTTP (
src/lib/api.ts). Components take plain props; state lives inApp.tsxand the hooks. src/lib/types.tsmirrors the Java recordsRequestTrace/ModuleMetricsSnapshot— keep them in sync with the backend.- Styling: Tailwind v4 with shadcn (
base-nova) on@base-ui/react(not radix). The neutral ramp is the shared slate scale aligned with the protean-web homepage, plus a teal--brandtoken. Status and telemetry hues are extra tokens insrc/index.css(--ok/--warn/--crit/--telemetry), all exposed via@theme inline. - Numeric/telemetry text is monospace +
tabular-nums.
The live path is a single SSE connection to GET /platform/traces/stream, which
opens with an acknowledgement and then multiplexes four named events:
| Event | Maps to | Notes |
|---|---|---|
ready |
StreamReady |
Connection ack, first frame, repeated on every reconnect. Optional: platforms that predate it are supported. |
trace |
RequestTrace[] |
Incremental delta since the last seq (initial snapshot on connect). |
metrics |
ModuleMetricsSnapshot[] |
Full per-module snapshot each tick. Opt-in via protean.trace.metrics.enabled. |
modules |
ModuleStatus[] |
Joined into the module table for isolation mode / trust tier. |
summary |
TraceSummary |
Windowed KPI aggregate + trend vs the previous window + active-module split by mode. |
The ready ack is not a data frame. It carries what a client must learn once, at
connect time, and cannot derive from the stream afterwards — on a quiet platform
nothing else arrives at all, so it is also the only thing separating the stream
is open from the platform is running:
event: ready
data: {"platform":"protean","platformVersion":"0.1.0","tracesEnabled":true,
"metricsEnabled":false,"buffered":5,"tickMs":1000,"capacity":200}
| Field | What the console does with it |
|---|---|
platform / platformVersion |
Shown beside the origin in the header. Display only — no behaviour branches on which implementation answered. null version is rendered as unknown, never replaced with a guess. |
tracesEnabled / metricsEnabled |
Separates "recording is off" from "recording is on and idle" — the two an empty panel cannot tell apart. tracesEnabled is read first: per-module metrics are aggregated from recorded requests, so with recording off no rows can appear whatever the metrics flag says. |
buffered |
Read, but not acted on: it counts the rows of the trace frame that follows (not the ring's size), and those rows arrive regardless. |
tickMs |
Sizes the silence watchdog (6 periods, clamped to 2–60s) instead of assuming 1Hz. |
capacity |
The ring size as of this connection, printed in the LIVE badge. |
Fields may be added later; the console ignores the ones it does not recognize. Every value is connection-scoped — the console drops them when the stream is rebuilt and re-learns them from the next ack, because they are live settings on the platform.
The read-only REST surface (GET /platform/traces, /platform/traces/metrics,
/platform/modules, /platform/modules/{id}/routes) is still mirrored in
api.ts as a fallback and for the module-routes drawer.
| Script | Command | Description |
|---|---|---|
npm run dev |
vite |
Dev server with the /platform proxy. |
npm run build |
tsc -b && vite build |
Type-check then production build. |
npm run preview |
vite preview |
Serve the production build locally. |
npm run lint |
oxlint |
Lint with oxlint. |
Runtime
react/react-dom19@base-ui/react— headless primitives (base, not radix)shadcn/@shadcn/react— component style layer (base-novastyle)tailwindcss4 +@tailwindcss/vite,tw-animate-cssclass-variance-authority,clsx,tailwind-merge— class compositionlucide-react— icons@fontsource-variable/geist(sans) +@fontsource-variable/raleway(heading)
Dev / build
vite8 +@vitejs/plugin-reacttypescript7oxlint@types/*
Protean does not impose auth on /platform/** by default — it delegates to the
consuming app (typically Spring Security). Because that policy is yours, the
console ships auth-agnostic: the operator attaches whatever credential the
deployment expects. REST requests share a single choke point (getJson in
src/lib/api.ts), while the live SSE stream — opened with EventSource, which
cannot send headers — is best secured with a session cookie. This is not yet
implemented; the scheme is your choice. See the
authentication guide.
- Auth — pick a scheme and implement the header provider + 401/403 surface (see the authentication guide).
- Server-side filtering —
errorsOnly/status/minLatencyMsare already sent as query params, but the recent-traces table still filters client-side too. - i18n / analytics — not set up (internal MVP).
- Protean platform — backend dynamic-module platform (
org.htcom:protean) this console observes. - Base UI — headless component primitives.
- shadcn — component style layer.
- Tailwind CSS v4 · Vite · React · oxlint.
- RFC 9457 — Problem Details error bodies
(the
traceIdcorrelation shared with the trace table).
MIT © 2026 htjulia.
