Web console
The Omnitron webapp at apps/omnitron/webapp/ (package
@omnitron/console) is the visual counterpart of the CLI. It's a
React 19 + Vite + MUI SPA that talks to the daemon via the
@omnitron-dev/prism/netron client — the same Netron RPC surface
the CLI uses.
Verified against apps/omnitron/webapp/src/.
Tech stack
| Layer | Choice |
|---|---|
| Framework | React 19 |
| Build | Vite 8 with @vitejs/plugin-react-swc |
| Routing | react-router-dom 7 |
| UI components | @mui/material (Emotion), with select components from @omnitron-dev/prism |
| RPC client | @omnitron-dev/prism/netron (createMultiBackendClient) over HTTP + WebSocket |
| Charts | react-apexcharts |
| Graphs | @xyflow/react (topology, dependency graphs) |
| State | zustand stores in src/stores/ |
| Code splitting | Per-page React.lazy() |
Filesystem layout
webapp/
├── index.html
├── vite.config.ts
├── package.json
├── public/ # static assets shipped as-is
└── src/
├── main.tsx # entrypoint
├── app.tsx # <App/> root
├── routes/ # router definition
├── layouts/ # ConsoleLayout, AuthLayout
├── pages/ # 20+ pages, lazy-loaded
├── components/ # shared UI building blocks
├── hooks/ # custom React hooks
├── netron/ # RPC client + DaemonWsClient wiring
├── auth/ # AuthGuard, GuestGuard, ProjectGuard, auth store
├── stores/ # zustand state stores
├── assets/ # icons
├── utils/
└── stubs/ # fs/path stubs for Node builtins pulled in by Prism
Route map
All routes other than /auth/sign-in are gated by AuthGuard.
Routes marked project-scoped require an active project (via
ProjectGuard) — selected from the project switcher in the
sidebar.
| Path | Page | Scope | Backing RPC |
|---|---|---|---|
/auth/sign-in | SignInPage | guest | OmnitronAuth.signIn |
/ | DashboardPage | global | aggregate from many services |
/nodes | NodesPage | global | OmnitronNodes |
/projects | ProjectsPage | global | OmnitronProject |
/system | SystemInfoPage | global | OmnitronSystemInfo |
/settings | SettingsPage | global | OmnitronAuth + config |
/logs | LogsPage | global | OmnitronLogs |
/apps | AppsListPage | project | OmnitronDaemon.list |
/apps/:name | AppDetailPage | project | OmnitronDaemon.getApp + many |
/stacks | StacksPage | project | OmnitronProject.listStacks |
/stacks/:name | StackDetailPage | project | OmnitronProject.getStackStatus |
/metrics | MetricsPage | project | OmnitronMetrics (getSnapshot / querySeries) |
/topology | TopologyPage | project | derived from OmnitronDiscovery + services |
/containers | ContainersPage | project | OmnitronInfra |
/deployments | DeploymentsPage | project | OmnitronDeploy |
/alerts | AlertsPage | project | OmnitronAlerts |
/pipelines | PipelinesPage | project | OmnitronPipelines |
/traces | TracesPage | project | OmnitronTraces |
/dashboard-builder | DashboardBuilderPage | project | custom dashboard composer |
* | redirect to / | — | — |
Page-by-page
Dashboard
The landing page after sign-in. Shows:
- Daemon status card (version, uptime, PID).
- Total apps / online / errored counts.
- Aggregate CPU / memory.
- Recent alerts.
- Recent deployments.
- Cluster status when cluster mode is enabled.
Pages fetch through the typed Netron client directly (the
daemon / metrics proxies from src/netron/client.ts) inside
useEffect + polling intervals, and subscribe to a shared
zustand realtime store (src/stores/realtime.store.ts) for
live pushes. There is no TanStack/React Query layer.
Apps list & detail
/apps — table view of every managed app: status, instances,
CPU, memory, restart count, mode (classic vs bootstrap),
critical flag.
/apps/:name — single-app deep dive:
- Live status with restart history timeline.
- Per-process breakdown (sub-processes for bootstrap mode).
- Resource graphs (CPU / RSS / event-loop lag).
- Health probes summary.
- DI dependency graph (renders Mermaid via daemon's
getDependencyGraph). - Log stream with filter (level, grep).
- Resolved environment variables.
- Action buttons: Start / Stop / Restart / Reload / Scale.
- Inspect button → drills into
OmnitronDaemon.inspect.
Stacks
/stacks — projects' stacks side-by-side with status, app count,
runtime (Docker container count).
/stacks/:name — per-stack view: which apps run here, which
infrastructure, runtime status. Start / stop the whole stack.
Logs
Cross-app log viewer. Three modes:
- Tail — most recent N lines per app.
- Stream — live append; auto-scroll; pause on hover.
- Search — server-side regex over the log files.
Filters:
- App selector (multi).
- Level (≥).
- Free-text regex.
- Time range.
Uses OmnitronLogs.streamLogs for live mode; queryLogs for
history.
Metrics
Time-series view backed by the daemon's metrics aggregator. Per app, per metric name, per label combination. Built on ApexCharts.
Predefined panels:
- CPU per app, stacked.
- RSS per app.
- Event-loop lag p95.
- RPC latency p95 per service.
- Error rate per app.
The /dashboard-builder page lets operators compose custom
panels and save them.
Topology
/topology — XYFlow service graph derived from
OmnitronDiscovery plus the live Netron service mesh. Nodes are
services; edges are observed calls. Useful to see "who actually
talks to whom" vs the static declaration.
Containers
/containers — Docker container inventory from OmnitronInfra.
Shows status, image, ports, resource use. Per-container actions:
restart, logs, exec (when daemon has Docker socket access).
Deployments
/deployments — deployment history from OmnitronDeploy. List
of past deploys, strategy, version, deployer, duration. Trigger
new deploys or rollbacks here too.
Alerts
/alerts — alert rule manager + event log. Rules tab: list,
edit, create. Events tab: fired alerts, ack / resolve actions.
Pipelines
/pipelines — CI/CD pipeline list, definition editor, run
history, live run view.
Traces
/traces — distributed trace viewer. Trace list with filters,
single-trace waterfall, service-map view. Backed by
OmnitronTraces.
Nodes
/nodes — fleet node inventory. Per-node:
- Status, hostname, last-seen timestamp.
- Uptime bar (90 days of green/yellow/red segments — backed by
getUptimeBar). - SSH connectivity test.
- Per-node actions: drain, remove, edit.
Projects
/projects — registered project list. Add / scan / remove
projects. Project switcher in sidebar mirrors this.
System info
/system — host inventory: OS, CPU, RAM, disk, network
interfaces. Refreshes on demand.
Settings
/settings — auth user management, change-password, system
preferences.
Auth flow
The access token is held in sessionStorage under the key
omnitron_token (via Prism's SessionTokenStorage). Logout
clears it and calls OmnitronAuth.signOut({ sessionId }). The
signIn / validateToken / refreshSession methods are the
only ones that don't require a bearer token.
RBAC in the UI
The webapp respects the same three roles as the daemon
(viewer / operator / admin). Behaviour:
| Surface element | Visible to | Disabled for |
|---|---|---|
| Read-only pages (Dashboard, Logs, Metrics, Traces) | everyone | — |
| Start / Stop / Restart buttons | viewer (visible) | viewer (disabled with tooltip) |
| Scale, Reload, Exec | operator + admin | viewer (hidden) |
| Secrets editor | admin | non-admin (hidden) |
| Shutdown daemon, Reload config | admin | non-admin (hidden) |
| Settings → users | admin | non-admin (hidden) |
Surface elements consult the useAuthStore (zustand) auth state
and disable / hide accordingly. The daemon still enforces
server-side via @Public({ auth: { roles } }) on each RPC method
— UI gating is convenience, not security.
Real-time event flow
The webapp uses OmnitronEvents for live updates:
The webapp subscribes through a single DaemonWsClient
(src/netron/ws-client.ts) that feeds the shared realtime store,
so the dashboard reflects state changes essentially instantly —
without per-component polling for the events that the daemon
already pushes.
Build / run modes
Dev mode
cd apps/omnitron/webapp
pnpm dev # Vite dev server with HMR (port 9810)
The RPC client uses a relative baseUrl, so the Vite dev server
proxies the daemon for it (see vite.config.ts): /netron/* →
http://localhost:9801 (daemon HTTP) and /ws → the daemon's
Netron WebSocket transport on :9802. The daemon must already be
running (omnitron up).
The dev server deliberately sits at 9810, outside 9800–9803.
It used to ask for 9802 — the daemon's WebSocket port — so it
could not start while the daemon it exists to talk to was running,
and its own /ws proxy pointed back at itself. Sign in with the same account you use against the
production build — the 001_initial_schema migration seeds a single
admin user with password admin on first daemon start, and the
console's Change-password screen is the only way to change it.
Production build
cd apps/omnitron/webapp
pnpm build # tsc -b && vite build → dist/
The compiled bundle lands in webapp/dist/. It is not served
by the daemon directly — omnitron webapp start launches an
omnitron-nginx container that mounts dist/, serves it on
:9800, and reverse-proxies /netron/* to the daemon's HTTP
listener (:9801) and /ws to its WebSocket transport (:9802).
Open the webapp
| Command | Effect |
|---|---|
omnitron webapp build | Compile the bundle (vite build → dist/) |
omnitron webapp start [-f] | Start the omnitron-nginx container serving static + gateway (-f/--force recreates it) |
omnitron webapp stop | Stop and remove the nginx container |
omnitron webapp status | Report whether the console container is running / healthy |
omnitron webapp open | Open the running webapp in the system browser (starts it first if stopped) |
Project switcher
The sidebar carries a project dropdown — switching projects re-scopes every project-scoped page to that project's apps / stacks / etc. Stored in the auth context; persists across sessions.
Empty / error states
Every page is built around three states:
- Loading — skeleton with shimmer (
<LoadingScreen />from Prism). - Empty — friendly empty state with the primary action ("Add your first project", "Connect a node", …).
- Error — error card with retry button + JSON of the underlying error (operators love seeing the actual error).
Performance characteristics
- Code-splitting — each page is
React.lazy'd. - HMR — Vite + SWC; instant in dev.
- Polling + push — pages poll the daemon on an interval and
the shared
zustandrealtime store applies pushed events; no TanStack/React Query cache layer. - WebSocket for
OmnitronEventssubscribers — oneDaemonWsClientsocket per tab.
Adding a new page
The pattern is mechanical:
- Add
apps/omnitron/webapp/src/pages/my-page.tsxexporting a default React component. - Add a lazy import +
<Route path="my-page" ...>in the router (src/routes/index.tsx), wrapping in<ProjectRoute>if the page is project-scoped. - Add a nav entry in
src/layouts/console-layout.tsx. - Import the typed service proxy from
src/netron/client.ts(e.g.import { daemon, metrics } from 'src/netron/client') and call it fromuseEffect; for live updates, read from thesrc/stores/realtime.store.tszustand store.
No separate API layer to wire — the daemon already exposes everything as typed Netron services.
Anti-patterns
- Storing secrets in
localStorage. Use the auth token only; for actual secrets, the daemon'sOmnitronSecretsis the right place. - Polling instead of subscribing. When
OmnitronEventsalready emits the change, use it — saves the daemon a lot of RPC roundtrips. - Bypassing
AuthGuard. Every protected route should sit under the<AuthGuard>wrapper; ad-hoc auth checks per page drift. - Calling the daemon directly with
fetch. Use the typed service proxies fromsrc/netron/client.ts— they carry the bearer token and give you end-to-end types from the daemon DTOs.
See also
- CLI — the same operator surface from the terminal
- Services reference — every RPC the webapp consumes
- Architecture — where the webapp sits
- Prism — provides the
@omnitron-dev/prism/netronRPC client the webapp uses, plus select UI components (the bulk of the UI is MUI) - netron-browser — the underlying browser RPC transport that Prism's netron client builds on