Dashboard and Workspaces¶
ONIX ships two distinct workspace shells at this pin, plus three older internal consoles (/workspace, /lab, /admin). This page covers how each shell is composed, how its layout is persisted, and how widgets communicate with each other.
Two shells, one widget-registration idea¶
/dashboard (DashboardShell, src/components/dashboard/dashboard-shell.tsx) is a CSS-grid frame with four zones — a persistent left sidebar, a full-bleed center area, a persistent right sidebar, and a bottom status bar — each collapsible and resizable by plain pointer-event drag handles (no drag library is used; the component's own comment describes useResizeDrag as "the whole implementation"). Handles are also keyboard-operable: arrow keys nudge the same clamp function a pointer drag uses, so a splitter is never a dead, unreachable Tab stop. The shell itself is deliberately inert: it "fetches nothing," and an empty widget registry renders a clean shell rather than an error, because "no widgets installed yet" is explicitly not treated as an engine fact worth reporting on.
Widgets are contributed through src/components/dashboard/registry.ts, a plain in-memory Map keyed by widget id: a widget module calls registerWidget({id, title, zone, Component}) once, typically as an import side effect, and the shell discovers everything through listWidgets(zone). src/app/dashboard/widgets.tsx is the single place that composes the actual roster — chart, screener, the three-section Strategy Workbench (Strategies / Studio / Backtests), an operator-authority banner, instrument watchlist, venue connections, and the seven runtime widgets (orders, fills, positions, account, portfolio, a performance-report widget, and a private-operator-gated order ticket — see capability-status.md for what each of those actually publishes at this pin) — and its own comment states the ordering rule explicitly: the chart registers first because "the center zone's default tab is the FIRST registered center widget."
/workspace-v2 (src/workspace-v2/registry.ts) takes a stricter approach. Its widget catalog, BUILTIN_WIDGETS, is a compile-time, frozen constant — the module's own security note is explicit that "there is no registerWidget() API" and nothing about a widget's import path, component, or lifecycle is ever constructed from a network response or capability payload. Backend-derived capability data may only toggle an already-declared widget's availability (rendering it versus its stated unavailableReason); it can never add, replace, or repoint an entry. At this pin the frozen catalog holds nine widget types: the chart and the Strategy Studio (the module's comments describe these as this surface's original "proof widgets," requiring backend capabilities engine.v1.documents and engine.v2.spine respectively), the Strategy Manager and Backtests widgets shared with /dashboard, four runtime execution-table widgets (open positions, closed positions, open orders, order history), and a development-only status widget. Every entry declares the backend capability it requires and a plain-English reason to render in its place when that capability is absent.
The two older consoles, /workspace (a resizable multi-pane console over versioned Nautilus DTOs, src/workspace/layout.ts) and /lab//admin, exist and are reachable but are treated by the app's own navigation as secondary tools rather than the trading product — see index.md.
Layout persistence: two policies, one reason¶
Both shells persist layout to window.localStorage, but they diverge on how they treat a corrupted or oversized stored document, and the divergence is deliberate rather than inconsistent:
/dashboard'slayout-store.ts(keyonix.dashboard.layout) treats layout as pure UI preference — sidebar widths, collapse flags, hidden-widget ids, and the last-active center tab. An unknown or malformed stored value falls back toDEFAULT_LAYOUTsilently. The module's own comment ties this directly to the project's broader "fail visibly" rule: that rule "governs required trading-domain data," not a forgotten pixel width from a previous build./workspace-v2'slayout-storage.ts(keyonix.workspace-v2.layout.v1) is deliberately stricter: a stored document that fails its full structural parse, or exceeds a declared byte ceiling, is surfaced to the shell as a visiblefailurestring rather than silently discarded — the module's comment states this route "deliberately does not adopt/dashboard's silent fall-back-to-default," because malformed, oversized, or unknown input must "fail safely AND visibly" on this newer surface. A previously-saved, still-valid document that predates a newly added singleton widget is upgraded in place (graftMissingSingletons) rather than rejected outright.
In both cases, layout state is explicitly not an engine fact: nothing about the backend's market data, positions, or run history is ever persisted in localStorage by these modules — only the browser's arrangement of panes and widget visibility.
How widgets talk to each other¶
Neither shell uses a global state-management library for cross-widget communication (see client-state-and-cache.md). Instead, small, purpose-built React contexts carry exactly the shared facts widgets need:
DashboardSelectionContext(src/components/dashboard/dashboard-context.tsx) carries the one thing most dashboard widgets need to agree on: which instrument, bar type, and gateway resolution key (e.g.1m,4h— the venue-neutral vocabulary/v2/resolutionspublishes, not a TradingView resolution string) are "selected" right now. The module is deliberately dependency-free — "no gateway import, no engine type, just nullable strings and their setters" — so any widget can read or write selection without coupling to the datafeed.ConnectionSelectionContext(src/components/dashboard/connection-context.tsx) carries which venue connection and account the trading surfaces are currently attributed to, including the backend's own already-masked account label and its own environment string (paper/live/sandbox) — carried verbatim rather than re-derived, because the module's comment states plainly that "mode is a backend fact the UI must display conspicuously and must never compute."
Selection flows in both directions between widgets. The chart widget's own documentation states it explicitly: "Watchlist click → setSymbol on the live widget; TV's own header search → onSymbolChanged → back into the shared selection," and the same bidirectional rule applies to the selected resolution. The screener contributes a third link: it does not draw on the chart directly, but publishes a minimal ChartOverlayTarget (src/screener/overlay-slot.ts), which a separate adapter (src/screener/overlay-adapter.tsx) consumes to draw pattern-observation overlays on the mounted chart widget — a one-way publish/subscribe seam kept deliberately narrow so ownership of "what draws on the chart" stays with one module.
Order ticket, portfolio, and performance widgets¶
Three /dashboard runtime widgets are easy to miss because they render mostly-empty panels at
this pin, but the widget code itself is real and registered, not a stub:
- Order ticket (
src/components/runtime/ticket.tsx) is a compact, venue-neutral form that submits and cancels orders against the backend's operator command queue (POST /v2/orders,POST /v2/orders/cancel), then pollsGET /v2/commands/{command_id}for the resulting lifecycle rather than assuming a direct fill. Every value the operator types crosses to the gateway exactly as typed, with no float parsing or reformatting. The whole form is disabled unless the deployment resolves toprivate-operator(see gateway-realtime.md); apublic-readonlyhost shows the form but refuses submission. This is implemented, private-operator UI: it genuinely reaches the backend's order-command surface, but whether a specific deployment's backend actually executes a submitted command against a live venue was not independently re-verified from the frontend alone — see capability-status.md. - Portfolio (
src/components/runtime/portfolio.tsx) renders the official Nautilus portfolio document (GET /v2/portfolio) generically, because the backend has not published a fixed field shape for it as of this pin. Its own comment records that this route is "unpublished on every gateway reachable today," so the widget currently renders the neutral not-published panel rather than live figures. - Performance report (
src/components/runtime/performance.tsx) is a placeholder for a standalone performance/statistics report reachable without first knowing a backtest job id. Its own comment is explicit that no such route exists at this pin — a finished backtest job's ownreports/statsfields (rendered on the Backtests widget) are the closest currently-published equivalent — so this widget always renders its not-published state.
Automation bridges¶
While /dashboard is mounted it also installs several small, enumerated browser-automation hooks
— one for the chart, and one each for the screener workspace, venue connections/order ticket, and
the strategy workbench — meant for locally attached development/support tooling, not as a general
product feature. See chart-mcp-bridge.md for what each hook can and cannot
reach, and why that page's own status reflects an outstanding publication sign-off.
What is and is not verified here¶
The zone layout, drag-resize mechanics, widget registration pattern, both persistence policies, and the bidirectional selection contexts described above are verified against source and accompanying unit tests at this pin (dashboard-shell.test.tsx, layout-store.test.ts, registry.test.ts, dashboard-context.test.tsx, layout-storage.test.ts). The /workspace-v2 widget roster (9 types) is narrower than /dashboard's composed roster at this pin and omits several /dashboard widgets outright (the watchlist, venue connections, screener) — it is best read as a stricter, still-growing foundation rather than a full replacement for the dashboard shell. The order ticket, portfolio, and performance widgets are verified as registered, real components with their own unit tests (ticket.test.tsx, portfolio.test.tsx, performance.test.tsx); whether a given deployment's backend actually executes a submitted order is a separate, backend-owned fact not re-verified here. See capability-status.md for the consolidated table.
Evidence and source pins for this page
Verified. Current behaviour, confirmed in source at the pinned commit.
Verified on against the following immutable sources:
frontend@99f49dae:src/components/dashboard/dashboard-shell.tsxfrontend@99f49dae:src/components/dashboard/registry.tsfrontend@99f49dae:src/components/dashboard/layout-store.tsfrontend@99f49dae:src/workspace-v2/registry.tsfrontend@99f49dae:src/workspace-v2/layout-storage.tsfrontend@99f49dae:src/components/dashboard/dashboard-context.tsxfrontend@99f49dae:src/components/runtime/ticket.tsxfrontend@99f49dae:src/components/runtime/portfolio.tsxfrontend@99f49dae:src/components/runtime/performance.tsx
Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.