Skip to main content

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

LayerChoice
FrameworkReact 19
BuildVite 8 with @vitejs/plugin-react-swc
Routingreact-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
Chartsreact-apexcharts
Graphs@xyflow/react (topology, dependency graphs)
Statezustand stores in src/stores/
Code splittingPer-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.

PathPageScopeBacking RPC
/auth/sign-inSignInPageguestOmnitronAuth.signIn
/DashboardPageglobalaggregate from many services
/nodesNodesPageglobalOmnitronNodes
/projectsProjectsPageglobalOmnitronProject
/systemSystemInfoPageglobalOmnitronSystemInfo
/settingsSettingsPageglobalOmnitronAuth + config
/logsLogsPageglobalOmnitronLogs
/appsAppsListPageprojectOmnitronDaemon.list
/apps/:nameAppDetailPageprojectOmnitronDaemon.getApp + many
/stacksStacksPageprojectOmnitronProject.listStacks
/stacks/:nameStackDetailPageprojectOmnitronProject.getStackStatus
/metricsMetricsPageprojectOmnitronMetrics (getSnapshot / querySeries)
/topologyTopologyPageprojectderived from OmnitronDiscovery + services
/containersContainersPageprojectOmnitronInfra
/deploymentsDeploymentsPageprojectOmnitronDeploy
/alertsAlertsPageprojectOmnitronAlerts
/pipelinesPipelinesPageprojectOmnitronPipelines
/tracesTracesPageprojectOmnitronTraces
/dashboard-builderDashboardBuilderPageprojectcustom 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 elementVisible toDisabled for
Read-only pages (Dashboard, Logs, Metrics, Traces)everyone
Start / Stop / Restart buttonsviewer (visible)viewer (disabled with tooltip)
Scale, Reload, Execoperator + adminviewer (hidden)
Secrets editoradminnon-admin (hidden)
Shutdown daemon, Reload configadminnon-admin (hidden)
Settings → usersadminnon-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 98009803. 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

CommandEffect
omnitron webapp buildCompile the bundle (vite builddist/)
omnitron webapp start [-f]Start the omnitron-nginx container serving static + gateway (-f/--force recreates it)
omnitron webapp stopStop and remove the nginx container
omnitron webapp statusReport whether the console container is running / healthy
omnitron webapp openOpen 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 zustand realtime store applies pushed events; no TanStack/React Query cache layer.
  • WebSocket for OmnitronEvents subscribers — one DaemonWsClient socket per tab.

Adding a new page

The pattern is mechanical:

  1. Add apps/omnitron/webapp/src/pages/my-page.tsx exporting a default React component.
  2. Add a lazy import + <Route path="my-page" ...> in the router (src/routes/index.tsx), wrapping in <ProjectRoute> if the page is project-scoped.
  3. Add a nav entry in src/layouts/console-layout.tsx.
  4. Import the typed service proxy from src/netron/client.ts (e.g. import { daemon, metrics } from 'src/netron/client') and call it from useEffect; for live updates, read from the src/stores/realtime.store.ts zustand 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's OmnitronSecrets is the right place.
  • Polling instead of subscribing. When OmnitronEvents already 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 from src/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/netron RPC 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