All checks were successful
deploy / deploy (push) Successful in 48s
Instead of fetching up to 50k inventory snapshots (~442MB), scope the query to only the time range of the current submission batch with a 15-minute buffer. For a typical 1-hour batch this drops from 50k rows to ~300 rows, well within the 1280Mi pod memory limit. The coalesced_at_idx on intent_inventory_snapshots covers the BETWEEN clause so this remains efficient. Proof: history-writer OOM kills from refreshQuoteOutcomes inventory fetch Assumptions: 15min buffer covers the attribution window for all submissions Still fake: heuristic gap outcomes may attribute trades imprecisely
401 lines
18 KiB
Markdown
401 lines
18 KiB
Markdown
# Implementation Turn: operator dashboard foundation, funds desk, and operator controls
|
|
|
|
Status: open
|
|
Opened: 2026-04-04
|
|
|
|
## Goal
|
|
Build a real operator dashboard service for the live BTC/EURe system so routine supervision no longer depends on ad hoc `curl`, `kubectl`, and SQL. The dashboard must read real durable and live state, make freshness obvious, and answer `are we making money` before it answers lower-level operator questions. It must also provide a narrow set of real operator controls without pretending that risky treasury actions are routine.
|
|
|
|
## Selected backlog items
|
|
- [I010] Operator dashboard foundation: dedicated dashboard pod plus backend API that reads PostgreSQL and service `/state`, with a global live status bar, arm/disarm controls, stale-state display, and `Funds` as the default home page.
|
|
- [I011] Funds desk: current balances, EUR-equivalent value, deposit-handle reveal with retrieval timestamps, emergency stop plus withdraw-all control path, deposit/withdraw history, trade-driven asset-change history, and flexible withdrawal forms by asset and EUR equivalent.
|
|
- [I014] Strategy and system operator pages: active strategy settings, skip reasons, alert state, service health, topic lag, persistence health, and cluster-operator stats.
|
|
|
|
## Design rule
|
|
The dashboard is a reader and operator surface over existing truths. It must not invent a second truth model.
|
|
|
|
- PostgreSQL remains the durable store for historical records and portfolio metrics.
|
|
- Existing services remain the owners of live state and control actions.
|
|
- The dashboard backend aggregates those sources and exposes one browser-facing API.
|
|
- The frontend must not talk to Kafka, Redpanda, PostgreSQL, or internal app services directly.
|
|
- Frontend polling is prohibited. Live updates must flow over WebSockets from the dashboard backend.
|
|
|
|
## Profitability-first rule
|
|
The default dashboard experience should answer these operator questions in order:
|
|
- what is the portfolio worth in EUR right now
|
|
- are we up or down versus the original deposits
|
|
- are we beating simple hold of the original deposits
|
|
- how much of the result appears to come from market move versus trading
|
|
- are trades actually happening right now
|
|
|
|
Operational telemetry should support that answer, not displace it.
|
|
|
|
## Deployment shape
|
|
Add one new service:
|
|
- `operator-dashboard`
|
|
|
|
Preferred shape:
|
|
- one Node service in the existing image
|
|
- serves static frontend assets plus JSON API plus WebSocket endpoint
|
|
- cluster-local by default, operator-accessed by port-forward or explicitly approved exposure later
|
|
|
|
Expected config additions:
|
|
- dashboard host or port
|
|
- PostgreSQL connection string
|
|
- Kafka brokers and a dedicated dashboard consumer group
|
|
- internal base URLs for:
|
|
- `market-reference-ingest`
|
|
- `inventory-sync`
|
|
- `liquidity-manager`
|
|
- `history-writer`
|
|
- `ops-sentinel`
|
|
- `strategy-engine`
|
|
- `trade-executor`
|
|
- optionally `near-intents-ingest`
|
|
- optional namespace or Kubernetes API settings if cluster state is included
|
|
- auth mode and any future operator-auth placeholders needed to keep the backend auth contract stable
|
|
|
|
## Backend architecture
|
|
The dashboard backend should have four cooperating parts:
|
|
|
|
1. REST API for bootstrap, history, and control actions
|
|
2. WebSocket hub for live push updates to the browser
|
|
3. PostgreSQL query layer for durable history and metrics
|
|
4. Kafka consumer layer for live event ingestion without polling
|
|
|
|
High-level flow:
|
|
- browser loads the page and calls dashboard REST endpoints for initial bootstrap data
|
|
- browser opens one backend WebSocket connection
|
|
- backend fans live events from Kafka into normalized WebSocket messages
|
|
- backend uses PostgreSQL for historical lists, pagination, and durable baseline data
|
|
- backend uses internal service REST calls for current control state and to execute allowlisted actions
|
|
|
|
The browser should never need its own polling timers.
|
|
|
|
## Auth skeleton
|
|
Add backend auth and authorization from the start, even if the first provider is permissive.
|
|
|
|
Required shape:
|
|
- a request-level auth middleware for REST
|
|
- a connection-level auth gate for WebSockets
|
|
- a structured auth context on every request or socket, for example:
|
|
- `authenticated`
|
|
- `subject`
|
|
- `mode`
|
|
- `roles` or equivalent
|
|
|
|
Initial implementation may be something like:
|
|
- `mode = stub`
|
|
- `authenticated = true`
|
|
- `subject = local-operator`
|
|
|
|
But route handlers and socket handlers must already depend on auth context rather than bypass auth entirely.
|
|
|
|
## Backend responsibilities
|
|
|
|
### 0. Authenticated API and socket surface
|
|
The backend should expose:
|
|
- a bootstrap or session route that tells the frontend whether it is authenticated
|
|
- authenticated JSON API routes
|
|
- an authenticated WebSocket channel for live dashboard updates
|
|
|
|
The goal is that future real auth can replace the provider, not the route architecture.
|
|
|
|
### 1. Aggregate durable history from PostgreSQL
|
|
The backend should query PostgreSQL directly for:
|
|
- latest portfolio metric from `portfolio_metrics_snapshots`
|
|
- deposit-time baseline and simple-hold benchmark inputs from credited deposit records plus reference-price history, or from durable precomputed portfolio metrics if those already exist
|
|
- recent deposits and withdrawals from the latest inventory snapshot
|
|
- recent funding observations from `funding_observations`
|
|
- active or recent alerts from `ops_alerts`
|
|
- recent raw or normalized quotes from:
|
|
- `raw_near_intents_quotes`
|
|
- or `swap_demand_events`, whichever is cleaner for the operator surface
|
|
- recent trade commands, results, and linked decision metadata from:
|
|
- `execute_trade_commands`
|
|
- `trade_execution_results`
|
|
- `trade_decisions`
|
|
- latest market price and latest inventory snapshots as durable fallback truth
|
|
|
|
The backend should normalize these into dashboard-oriented response shapes rather than leaking raw SQL rows to the frontend.
|
|
If benchmark or attribution values are derived from incomplete data, the response shape should carry explicit caveats rather than silently presenting them as fully precise.
|
|
|
|
### 2. Aggregate live state from service control APIs
|
|
The backend should read `/state` and `/healthz` from the live services and normalize:
|
|
- freshness timestamps
|
|
- stale status
|
|
- pause state
|
|
- armed state
|
|
- last errors
|
|
- active alerts
|
|
- funding observer status
|
|
- writer offsets and database connectivity
|
|
|
|
The backend should make failures explicit per upstream service so one dead app does not blank the whole dashboard.
|
|
These reads should happen on demand for bootstrap and action-result flows, not through periodic polling loops.
|
|
|
|
### 3. Live event fan-out from Kafka to WebSockets
|
|
To satisfy the no-polling rule, the dashboard backend should consume Kafka directly and publish normalized WebSocket events.
|
|
|
|
Initial live topics should include at least:
|
|
- `raw.near_intents.quote` or `norm.swap_demand` for recent quote activity
|
|
- `ref.market_price` for price freshness and top-bar updates
|
|
- `state.intent_inventory` for balance freshness and valuation updates
|
|
- `ops.alert` for alert count and transition updates
|
|
- `exec.trade_result` for successful trade feed updates
|
|
|
|
The backend should maintain small in-memory current views such as:
|
|
- recent quote ring buffer limited to 10 items
|
|
- latest market price summary
|
|
- latest inventory summary
|
|
- active alert count or latest transitions
|
|
|
|
This keeps the browser live without polling while PostgreSQL remains the durable history source.
|
|
|
|
### 4. Optional read-only cluster state
|
|
For the system page, add a narrow read-only cluster view if practical:
|
|
- deployment readiness
|
|
- pod restart counts
|
|
- pod age
|
|
- PVC presence and mount health where useful
|
|
|
|
If this is included, use namespace-scoped read-only access only. Do not add broad cluster-admin behavior to satisfy a dashboard.
|
|
If live updates are needed here, prefer Kubernetes watch semantics or explicit refresh actions over polling loops.
|
|
|
|
## Frontend shape
|
|
Use the smallest frontend that can produce a real operator experience.
|
|
|
|
Preferred implementation:
|
|
- server-served static HTML, CSS, and JavaScript
|
|
- no dashboard-specific heavyweight frontend stack unless it materially reduces risk
|
|
- explicit operator refresh actions are acceptable
|
|
- polling is not acceptable
|
|
|
|
Expected browser behavior:
|
|
- one initial bootstrap fetch for page data
|
|
- one authenticated WebSocket connection for live updates
|
|
- no interval timers that refetch API routes
|
|
|
|
The UI should feel like a serious operator surface, not a placeholder admin page.
|
|
|
|
## Page plan
|
|
|
|
### Global status bar
|
|
The shared top bar should show:
|
|
- active pair
|
|
- latest EUR or EURe per BTC price
|
|
- market freshness and inventory freshness
|
|
- active alert count
|
|
- strategy armed state
|
|
- executor armed state
|
|
- total EUR portfolio value
|
|
|
|
The bar should color or label stale and degraded conditions explicitly.
|
|
The values in the bar should update from WebSocket events.
|
|
|
|
### Funds page
|
|
This is the default home page.
|
|
|
|
Sections to include:
|
|
- profitability summary at the top:
|
|
- current total portfolio value in EUR
|
|
- profit or loss versus deposit-time baseline
|
|
- profit or loss versus simple hold of the original deposits
|
|
- compact attribution between market move and trading contribution
|
|
- last successful trade time and total successful trade count
|
|
- explicit caveat text when fees or per-trade realized net profit are not fully tracked yet
|
|
- current balances by asset in nominal units
|
|
- EUR-equivalent valuation by asset and in total
|
|
- latest synced inventory timestamp and reconciliation status
|
|
- current funding handles and current funding observation state
|
|
- pre-credit funding visibility separated from spendable balances
|
|
- recent deposits and withdrawals
|
|
- recent trade-driven asset changes
|
|
- portfolio metric summary from the durable metric table
|
|
|
|
Control affordances on this page should prioritize safe actions already supported by the system:
|
|
- refresh funds or liquidity state
|
|
- pause or resume funding observer
|
|
- freeze or unfreeze withdrawals
|
|
- withdrawal estimate form by asset and amount
|
|
|
|
Actual live withdrawal submission is not part of this turn unless the user explicitly approves that risk during implementation.
|
|
|
|
### Activity section
|
|
Add a small operator activity surface, either as its own page or as a clearly named section.
|
|
|
|
It must include:
|
|
- recent incoming quotes: 10 most recent, live updated over WebSockets
|
|
- successful trades ledger: all successful trades, paginated from PostgreSQL
|
|
|
|
Keep this narrow:
|
|
- no bucket analytics
|
|
- no quote histograms
|
|
- no hindsight trade grading
|
|
|
|
Useful fields for recent quotes:
|
|
- observed time
|
|
- pair or asset direction
|
|
- request kind
|
|
- amount in and amount out when available
|
|
|
|
Useful fields for successful trades:
|
|
- time
|
|
- pair
|
|
- request kind
|
|
- submitted size
|
|
- result code
|
|
- linked decision edge when available
|
|
|
|
This activity surface should support the profitability view rather than compete with it. Recent trade count and last-trade recency should also feed the top-level profit summary.
|
|
|
|
### Strategy page
|
|
Sections to include:
|
|
- strategy armed and paused state
|
|
- executor armed, paused, and draining state
|
|
- threshold and notional settings
|
|
- recent decisions and skip reasons
|
|
- recent trade submission results
|
|
- alert state relevant to execution safety
|
|
|
|
If arm, disarm, pause, resume, or drain controls are shown here, the backend must classify them as risky or non-risky and require explicit confirmation for the risky ones.
|
|
|
|
### System page
|
|
Sections to include:
|
|
- service health summary across all runtime apps
|
|
- latest writer offsets and persistence health
|
|
- latest error surfaces by service
|
|
- current active alerts and recent transitions
|
|
- pod or deployment readiness and restart counts if cluster state is included
|
|
- any explicitly computed stale-state or degraded-state flags
|
|
|
|
This page should replace routine namespace health spot-checks for day-to-day operation, not full cluster administration.
|
|
|
|
## Control proxy plan
|
|
The backend should expose a small allowlisted control surface such as:
|
|
- `POST /api/control/:service/:action`
|
|
|
|
The backend should also expose WebSocket event shapes such as:
|
|
- `status_bar.updated`
|
|
- `quotes.recent`
|
|
- `alerts.updated`
|
|
- `inventory.updated`
|
|
- `trades.successful.appended`
|
|
|
|
The exact event names may differ, but they should be stable and explicit.
|
|
|
|
Rules:
|
|
- only known service/action pairs are allowed
|
|
- the backend maps them to existing internal control routes
|
|
- the response must include the updated live state when practical
|
|
- risky actions must require an explicit confirmation payload
|
|
- unknown or blocked actions must fail loudly
|
|
|
|
Safe controls to prioritize first:
|
|
- refresh actions
|
|
- pause or resume on non-fund-moving services
|
|
- funding observer pause or resume
|
|
- withdrawals freeze or unfreeze
|
|
- withdrawal estimate
|
|
|
|
Risky controls that may be shown but not exercised without explicit approval:
|
|
- strategy arm or disarm
|
|
- executor arm, disarm, or drain
|
|
- any control that can move funds
|
|
|
|
## Service changes expected in this turn
|
|
The dashboard should reuse existing service truth where possible, but a few additive changes are acceptable if they remove ambiguity:
|
|
|
|
### `history-writer`
|
|
May need extra read helpers or SQL query functions, but should remain the owner of portfolio-metric durability.
|
|
|
|
### `liquidity-manager`
|
|
May need clearer deposit-handle timestamps, withdrawal-freeze state detail, or withdrawal estimate response shaping for dashboard consumption.
|
|
|
|
### `strategy-engine` and `trade-executor`
|
|
May need cleaner state fields or explicit risk metadata for dashboard control rendering.
|
|
|
|
### `ops-sentinel`
|
|
Should remain the owner of current alert state; the dashboard should not re-implement alert logic in the browser.
|
|
|
|
### `operator-dashboard`
|
|
This new service becomes the owner of:
|
|
- browser authentication context
|
|
- dashboard REST contracts
|
|
- dashboard WebSocket contracts
|
|
- live quote ring buffer and other in-memory dashboard views fed from Kafka
|
|
- durable pagination queries for trade history and other operator lists
|
|
|
|
If a field is already available from an existing `/state`, do not duplicate it elsewhere just for convenience.
|
|
|
|
## Query and presentation requirements
|
|
The dashboard backend should present stable API responses that already answer operator questions such as:
|
|
- what do we hold right now?
|
|
- what is it worth in EUR right now?
|
|
- are we making money versus the deposit baseline?
|
|
- are we beating or lagging simple hold of the original deposits?
|
|
- how much of the current result appears to come from market move versus trading?
|
|
- what deposits are credited versus merely observed?
|
|
- what alerts are active right now and why?
|
|
- are strategy and executor armed?
|
|
- what happened in the most recent trades?
|
|
- what are the 10 most recent incoming quotes right now?
|
|
- what are all successful trades so far, across pages?
|
|
- which service is stale, unhealthy, or paused?
|
|
|
|
Avoid pushing interpretation complexity into the frontend when the backend can answer it once.
|
|
|
|
## Validation plan
|
|
- Deploy the dashboard service to k3s.
|
|
- Verify that the default page loads and shows live balances matching `inventory-sync` and PostgreSQL.
|
|
- Verify that the total EUR valuation matches the current reference price and current spendable inventory.
|
|
- Verify that the profitability summary matches a manual deposit-baseline and simple-hold calculation from durable history, including any caveat text where data is incomplete.
|
|
- Verify that the browser receives live updates over WebSockets with no polling timers or polling API loops.
|
|
- Verify that recent incoming quotes update live in the dashboard and remain capped to the 10 most recent items.
|
|
- Verify that the successful-trades list paginates through all successful trades from PostgreSQL.
|
|
- Verify that funding visibility shown on the dashboard matches `liquidity-manager` and `inventory-sync` current state.
|
|
- Safely induce a stale-price or stale-inventory condition and confirm the status bar and relevant page reflect it without manual SQL.
|
|
- Execute at least one safe control action through the dashboard backend and verify the underlying service state changes.
|
|
- Verify that a dead or unreachable upstream service degrades only its section rather than blanking the entire dashboard.
|
|
- Confirm that risky controls are visibly confirmation-gated and do not fire by accident.
|
|
- Verify that auth context exists on both REST and WebSocket paths even when the auth provider is permissive.
|
|
|
|
## Tests
|
|
Required automated coverage:
|
|
- REST auth middleware and WebSocket auth gating
|
|
- backend aggregation of PostgreSQL rows plus live service state into dashboard responses
|
|
- valuation logic for nominal and EUR-equivalent balances
|
|
- deposit-baseline, simple-hold, and attribution summary calculations, including caveat behavior when some inputs are incomplete
|
|
- active-alert and freshness normalization
|
|
- Kafka-to-WebSocket fan-out for recent quotes and other live dashboard updates
|
|
- recent-quote ring buffer behavior
|
|
- successful-trade pagination queries
|
|
- control routing allowlist and rejection behavior for unsupported actions
|
|
- confirmation gating for risky actions
|
|
|
|
If a dashboard rendering layer is non-trivial, add at least smoke coverage for the key pages or payload contracts that feed them.
|
|
|
|
## Kubernetes and runtime work
|
|
Expected deployment additions:
|
|
- `operator-dashboard` deployment and service
|
|
- config wiring for internal service URLs
|
|
- config wiring for Kafka access and dashboard consumer group
|
|
- optional RBAC if the dashboard reads Kubernetes readiness state
|
|
|
|
Keep RBAC narrow and read-only. No new privileged operator pod should be introduced just to make the dashboard convenient.
|
|
|
|
## Out of scope on purpose
|
|
- No full quote analytics workbench page beyond the narrow recent-quotes and successful-trades views
|
|
- No benchmark or hindsight analytics page
|
|
- No strategy redesign
|
|
- No browser-facing auth or multi-user access model
|
|
- No public ingress hardening work
|
|
- No one-click live withdrawal submit path without explicit approval
|
|
|
|
## Still fake at turn open
|
|
- There is no single operator dashboard service yet.
|
|
- Operators still depend on manual service HTTP calls and SQL for routine checks.
|
|
- No browser-safe backend aggregation layer exists yet.
|
|
- No backend auth layer exists yet for operator UI traffic.
|
|
- No dashboard WebSocket path exists yet.
|
|
- Cluster-level health is still surfaced through scripts and `kubectl`, not an in-product operator page.
|