Compare commits

...

2 commits

Author SHA1 Message Date
philipp
7ddefb500e Stop calling submissions trades in dashboard
All checks were successful
deploy / deploy (push) Successful in 32s
Proof: Dashboard copy now describes durable submitted quote responses as submissions rather than successful trades or asset deltas, and regression coverage locks the new wording in place.

Assumptions: The current durable  execution result still means quote-response submission, not settled execution or realized inventory change.

Still fake: The backend still stores and aggregates these rows under trade-oriented names, and the full quote lifecycle model is still the active follow-up turn.
2026-04-09 01:13:14 +02:00
philipp
7ea1576ba7 Plan quote lifecycle truth turn
Proof: Archive the completed runtime-health turn and open a new implementation turn that replaces ambiguous quote verdicts with explicit lifecycle truth and durable execution explanation.

Assumptions: Recent decision and execution records already contain enough repo-owned evidence to derive a truthful first-pass quote lifecycle for the active pair.

Still fake: The new turn is planning state only; the dashboard still contains the existing lifecycle ambiguity until this proof is implemented.
2026-04-09 00:39:02 +02:00
10 changed files with 863 additions and 331 deletions

View file

@ -8,9 +8,20 @@ Legacy note:
## Implementation Turns ## Implementation Turns
- 2026-04-02: `first-non-mocked-tradeable-loop-for-one-pair` closed with status `passed`. A live active-pair quote flowed through funding, inventory sync, strategy, and real mainnet quote responses with durable history. - 2026-04-02: `first-non-mocked-tradeable-loop-for-one-pair` closed with status `passed`. A live active-pair quote flowed through funding, inventory sync, strategy, and real mainnet quote responses with durable history.
- 2026-04-04: `pre-credit-funding-visibility-and-operator-alerts` closed with status `passed`. Live BTC funding visibility, durable operator alerts, and rollout-safe armed-state persistence were proven on k3s without loosening spendable truth.
- 2026-04-07: `operator-dashboard-foundation-funds-desk-and-operator-controls` closed with status `passed`. A real operator dashboard shipped with authenticated REST and WebSocket surfaces, truthful funded-capital profitability, live quote and trade visibility, and safe operator controls over the BTC/EURe system.
- 2026-04-08: `cow-protocol-intent-based-venue-integration` closed with status `paused`. The CoW venue integration turn is paused after cluster investigation showed a real NEAR Intents ingest outage exposed a larger observability gap: the dashboard did not escalate stale quote flow or disconnected websocket clients, so runtime health and alerting need to be strengthened before adding a second venue.
- 2026-04-08: `runtime-health-sentinel-alert-routing-and-anomaly-detection` closed with status `passed`. Runtime health is now sentinel-owned, stale truth no longer renders healthy, alert delivery and safe containment exist, and deployment automation rolls all repo-owned services from push.
## Research Turns ## Research Turns
## Planning Events ## Planning Events
- 2026-04-01: workflow files initialized for thesis, implementation proof, backlog, archive, and research lane. - 2026-04-01: workflow files initialized for thesis, implementation proof, backlog, archive, and research lane.
- 2026-04-01: active implementation proof rewritten from durable-history scaffolding to the first executable trade loop for one pair. - 2026-04-01: active implementation proof rewritten from durable-history scaffolding to the first executable trade loop for one pair.
- 2026-04-02: opened implementation turn `pre-credit-funding-visibility-and-operator-alerts` from backlog items O003, O004. - 2026-04-02: opened implementation turn `pre-credit-funding-visibility-and-operator-alerts` from backlog items O003, O004.
- 2026-04-04: opened implementation turn `operator-dashboard-foundation-funds-desk-and-operator-controls` from backlog items I010, I011, I014.
- 2026-04-04: revised the operator dashboard turn to require backend WebSockets, auth scaffolding from day one, and a narrow live quote plus successful-trade activity surface without polling.
- 2026-04-04: revised the operator dashboard turn again to make profitability the primary operator question, with explicit deposit-baseline and simple-hold views plus attribution caveats where data is still incomplete.
- 2026-04-04: added follow-on backlog items for treasury cashflow and fee truth, per-trade realized attribution, and quote-to-outcome markout analytics to support later profit analysis turns.
- 2026-04-07: opened implementation turn `cow-protocol-intent-based-venue-integration` from backlog items I019.
- 2026-04-08: opened implementation turn `runtime-health-sentinel-alert-routing-and-anomaly-detection` from backlog items I020.
- 2026-04-08: opened implementation turn `quote-lifecycle-truth-and-execution-explanation` from backlog items I021.

View file

@ -17,7 +17,13 @@ Rules:
- [I007] Inventory-aware execution rule: implement both directions, but only fire the side backed by credited internal source-asset inventory. - [I007] Inventory-aware execution rule: implement both directions, but only fire the side backed by credited internal source-asset inventory.
- [I008] Inventory-sync service for NEAR Intents internal balances and pending funding state. - [I008] Inventory-sync service for NEAR Intents internal balances and pending funding state.
- [I009] Liquidity-manager service for deposit addresses, funding actions, and treasury visibility. - [I009] Liquidity-manager service for deposit addresses, funding actions, and treasury visibility.
- [I012] Quotes and execution analytics workbench: live quote tape, quote count and volume windows, size-bucket distributions, oracle-deviation views, per-quote decision and execution trace, and matched-only filters.
- [I013] Benchmark and PnL analytics: compare trade PnL versus mark-to-market, all-BTC hold, all-EURe hold, passive 50/50 hold, and hindsight trade quality against later benchmarks.
- [I015] (soon) Benchmark-aware strategy thresholds and inventory-skewed fees: compare live inventory against deposit-time hold and target BTC/EURe mix before quoting away preferred inventory. tags=strategy,inventory,pnl
- [I016] (soon) Treasury cashflow and fee ledger: durably record deposits, withdrawals, bridge or network costs, and other non-trade cashflows so dashboard profit can move from mark-to-market to true net PnL. tags=pnl,fees,treasury
- [I017] (soon) Per-trade realized attribution: link each successful trade to actual settled inventory deltas and attributable costs so the system can report realized net contribution per trade instead of only expected edge. tags=trades,pnl,settlement
- [I018] (later) Quote-to-outcome and markout analytics: link quote, response, decision, execution result, and settlement, then store later reference-price markouts to measure adverse selection and quote quality. tags=quotes,analytics,markout
## Research Candidates ## Research Candidates
- [R001] Compare Kraken and CoinGecko drift and freshness for the assets needed to price the active pair. - [R001] Compare Kraken and CoinGecko drift and freshness for the assets needed to price the active pair.
- [R002] Test whether the active pair's implied rate diverges from external reference prices enough to justify execution after a simple 2% gross threshold. - [R002] Test whether the active pair's implied rate diverges from external reference prices enough to justify execution after a simple 2% gross threshold.

View file

@ -1,235 +1,251 @@
# Implementation Turn: pre-credit funding visibility and operator alerts # Implementation Turn: quote lifecycle truth and execution explanation
Status: open Status: open
Opened: 2026-04-02 Opened: 2026-04-09
## Goal ## Goal
Make the already-live BTC/EURe loop operationally understandable between external funding and spendable credit, and make critical stale or failed states queryable as durable alert records instead of transient logs. Replace ambiguous quote and decision wording with a truthful per-quote lifecycle that tells the operator exactly why a quote was filtered, rejected, blocked, submitted, failed, not filled, or completed.
## Selected backlog items ## Selected backlog items
- [O003] Alerts for stale reference prices, stale inventory state, stuck funding actions, and failed executor submissions. - [I021] Quote lifecycle truth and execution explanation: replace ambiguous dashboard verdicts with a per-quote state machine that shows exactly why a quote was filtered, rejected, blocked, submitted, not filled, or executed, with durable reason codes and operator-facing traceability.
- [O004] Pre-credit funding visibility for slow chains: watch configured deposit addresses at chain level, track inbound transfers through mempool and on-chain confirmation before bridge credit, persist that state separately from spendable inventory, and alert operators when funding is seen, delayed, or stuck.
## Design rule ## Design rules
Keep the already-proven execution and inventory truth path intact: - Treat quote lifecycle as product truth, not UI decoration.
- verifier and bridge credit remain the only spendable truth - Strategy verdict is not the final operator answer.
- pre-credit visibility is additive observability - Prefer one explicit lifecycle derivation path shared by backend and dashboard over ad hoc page-specific wording.
- alerts are additive operability - Do not invent downstream certainty where durable evidence is absent.
- Remove `Actionable` completely from operator-facing copy.
## Event backbone ## Problem statement for this turn
Retain Kafka as the backbone. Add only the minimal new topics required: The current dashboard still forces operators to infer too much:
- `Actionable` does not say whether a command was emitted or submitted
- the Strategy page mixes strategy and execution truth
- executor-rejected rows are not clearly distinguishable from strategy-rejected rows
- quote ids are truncated and awkward to use during debugging
- `ops.funding_observation` The repo already stores enough of the real lifecycle to do better:
- `ops.alert` - quote id
- decision id
- emitted command id
- execution result status and result code
These topics are append-only evidence streams, not control planes. The turn therefore needs to improve:
- lifecycle derivation
- durable reason mapping
- recent-row rendering
- trace affordances
## Durable store ## Lifecycle model for this turn
Extend PostgreSQL with new append-only families: Implement one repo-owned lifecycle derivation for recent rows, using durable evidence in this order:
- `funding_observations`
- `ops_alerts`
If a current-state materialization is needed, derive it from append-only records or keep it as a clearly named snapshot table. Do not replace the append-only record. 1. Quote observed
2. Strategy evaluated
3. Command emitted or not emitted
4. Executor result observed or absent
5. Venue downstream outcome when available
## Service changes The first mandatory states are:
- `Filtered`
- `Rejected`
- `Blocked`
- `Submitted`
- `Failed`
- `Awaiting outcome`
- `Completed`
### 1. `liquidity-manager` Suggested meanings:
Extend the existing treasury owner instead of inventing a broad new funding stack. - `Filtered`
quote never entered the active trade path or was excluded before strategy decision
- `Rejected`
strategy evaluated the quote and decided not to trade
- `Blocked`
strategy approved or emitted a command, but execution did not proceed due to control state or another repo-owned gate
- `Submitted`
executor accepted the command and successfully submitted a quote response
- `Failed`
execution submission failed technically
- `Awaiting outcome`
submitted to venue, but no later durable terminal venue outcome exists yet
- `Completed`
durable evidence shows the trade completed successfully
New responsibilities: Do not show states we cannot support yet for a given row.
- retain active funding handles by chain and asset
- poll configured chain observers for those handles
- emit `ops.funding_observation`
- correlate chain observations with bridge `recent_deposits`
- expose latest observations and credit-correlation state through `/state`
Expected state shape additions: ## Reason-code model
- `funding_observations_by_handle` For each lifecycle state, map durable payload fields to a small operator reason taxonomy.
- `latest_funding_observation_at`
- `uncredited_funding_total_by_asset`
- `credit_correlation`
Control additions: Examples:
- `POST /refresh-funding-observations` - strategy reason codes:
- optional `POST /pause-funding-observer` - unsupported_pair
- optional `POST /resume-funding-observer` - below_edge_threshold
- inventory_unavailable
- stale_reference_price
- executor reason codes:
- executor_disarmed
- executor_paused
- submission_failed
- quote_response_ok
- downstream outcome reasons if available:
- expired
- not_filled
- completed
Important implementation constraints: If the exact reason is missing:
- do not change withdrawal behavior - expose `reason_unknown`
- do not reuse spendable inventory fields for pre-credit state - keep the row truthful instead of synthesizing an explanation
- keep BTC and EURe observation records in one shared schema
### 2. `inventory-sync` ## Backend changes
Keep current spendable accounting intact.
Possible additions: ### 1. Add a lifecycle derivation helper
- read the latest funding observations Create or extend a backend module that derives quote lifecycle from:
- expose a separate `pre_credit_inbound` or `funding_visibility` field in `/state` - recent trade decisions
- recent execution results
- successful trade records
- any available quote-status or venue result surfaces
Hard rule: It should emit a normalized row object with:
- `spendable`, `pending_inbound`, and strategy-facing credited truth must not become looser - `quote_id`
- `decision_id`
- `command_id`
- `pair`
- `direction`
- `lifecycle_state`
- `lifecycle_label`
- `reason_code`
- `reason_text`
- timestamps for the latest known stage
- stage details for tooltips or drilldown
### 3. `history-writer` ### 2. Join decision and execution truth explicitly
Consume and persist the new topics: The backend should no longer leave the frontend to infer execution from isolated tables.
- `ops.funding_observation`
- `ops.alert`
Expose through `/state`: For each recent quote/decision row:
- latest funding-observation write time - attach the matching execution result by `command_id`, `decision_id`, or `quote_id`
- latest alert write time - attach successful-trade or later terminal evidence where available
- counts or offsets for the new topics - expose whether the row is strategy-only, strategy-plus-command, or strategy-plus-execution
Add query-friendly indexes for: ### 3. Preserve operator drilldown identifiers
- `tx_hash` Ensure the bootstrap payload exposes:
- `funding_handle` - full quote id
- `alert_code` - full decision id
- `ingested_at` - full command id
### 4. `ops-sentinel` or equivalent alert evaluator Avoid requiring the frontend to reconstruct or guess identifiers from formatted strings.
Add one small service only if needed to keep alert logic separate and testable.
Responsibilities: ## Dashboard changes
- consume:
- `ref.market_price`
- `state.intent_inventory`
- `ops.liquidity_action`
- `ops.funding_observation`
- `exec.trade_result`
- evaluate policy windows for stale and stuck conditions
- emit `ops.alert` raise/clear transitions
- expose current alert state
Preferred alert model: ### 4. Remove forbidden language
- stable `alert_code` Remove `Actionable` from:
- `status`: `raised` or `cleared` - Strategy page tables
- `severity` - any lifecycle badge or verdict cell
- `reason` - any supporting labels or legends
- `first_raised_at`
- `last_evaluated_at`
- correlation IDs when available
Minimal alert set for this turn: Replace it with explicit state labels driven by lifecycle derivation.
- `reference_price_stale`
- `inventory_snapshot_stale`
- `funding_seen_unconfirmed`
- `funding_confirmed_credit_pending`
- `funding_stuck`
- `executor_submission_failed`
Do not add Slack, email, or paging integrations in this turn unless required to prove the path. Durable alert records plus HTTP state are sufficient. ### 5. Make recent rows self-explanatory
For each row, render:
- primary lifecycle state
- secondary reason text
- quote id with copy action
- command id if emitted
- timestamps
## Chain observer plan The operator should be able to scan rows and answer:
- why no trade happened
- whether the system tried to trade
- whether failure was strategic, operational, or downstream
### BTC ### 6. Add trace affordances
Must be the first-class proof path. At minimum:
- copy button for quote id
- avoid over-truncating ids without recovery path
- show linked ids in a dedicated trace column or expanded detail panel
Implementation expectations: If the row layout gets crowded, prefer an expandable detail tray over hiding identifiers.
- configurable observer endpoint
- look up the configured BTC deposit address
- detect:
- mempool appearance when available
- confirmation count
- credited transition once bridge/verifier catches up
Assumption to keep explicit in code: ## Page-level application
- a chain observer can disappear or lag independently of the bridge
### Gnosis / EURe ### Strategy page
Nice-to-have within the same schema, but BTC is the proof-critical path. This page should become the primary recent quote-decision-execution lifecycle surface.
If included this turn: It should show:
- watch the configured deposit address for EURe token transfers - the latest recent rows for the active pair
- represent observation state in the same event model - lifecycle state rather than strategy-only verdict
- explicit explanation text
## Record shapes If a strategy-only summary remains, it must be visually separate from per-quote lifecycle truth.
### `ops.funding_observation` ### Related quote surfaces
Required fields: Inspect quote and system surfaces for similar ambiguity and align the wording if they expose the same concepts.
- `funding_observation_id`
- `account_id`
- `asset_id`
- `chain`
- `funding_handle`
- `source`
- `tx_hash`
- `status`
- `amount`
- `confirmations`
- `first_seen_at`
- `last_seen_at`
- `credited_at` when known
- `bridge_deposit_tx_hash` when correlated
### `ops.alert` Do not let one page say `Submitted` while another page still says `Actionable` for the same row.
Required fields:
- `alert_event_id`
- `alert_code`
- `status`
- `severity`
- `reason`
- `service_scope`
- `pair` when relevant
- `asset_id` when relevant
- `tx_hash` when relevant
- `raised_at`
- `cleared_at`
- `details`
## Control surface expectations ## Data and state edge cases
- Strategy decision exists, no command emitted:
render as `Rejected` with strategy reason
- Command emitted, no execution result yet:
render as `Blocked` or `Awaiting executor` only if that distinction is durably supportable; otherwise use a truthful pending label
- Execution result `executor_disarmed`:
render as `Blocked` with reason `executor disarmed`
- Execution result `submission_failed`:
render as `Failed`
- Execution result `submitted`:
render as `Submitted` or `Awaiting outcome`
- Successful trade summary exists but no explicit per-quote completion event:
only promote to `Completed` where the durable linkage is real
### `liquidity-manager` ## Concrete implementation order
Must expose:
- active deposit handles
- latest pre-credit funding observations
- latest credit correlation
- whether the funding observer is healthy or paused
### alert evaluator ### Phase 1. Define lifecycle derivation
Must expose: - inspect current durable decision and execution payloads
- current active alerts - write the normalized lifecycle state mapping
- latest cleared alerts - define forbidden and allowed operator labels
- per-alert evaluation timestamps
- pause state
### `history-writer` ### Phase 2. Implement backend aggregation
Must expose the new topic offsets and write status for funding observations and alerts. - derive unified recent lifecycle rows
- expose full identifiers and reason codes
- keep old consumers working until the frontend is switched
## Tests ### Phase 3. Update Strategy page rendering
Required automated coverage: - replace verdict column with lifecycle state
- BTC funding observation remains non-spendable before credit - add reason text
- alert transitions raise then clear on recovered stale state - add quote-id copy affordance
- funding observation correlates to a later credited deposit without losing the original tx hash - surface command id and execution state where relevant
- executor failure produces an alert event
If a meaningful automated test cannot be written for a subpath, stop and record why instead of hand-waving. ### Phase 4. Tighten wording and consistency
- remove `Actionable`
- align supporting labels
- ensure blocked vs rejected vs submitted are clearly distinct
## Validation plan ### Phase 5. Validate with live recent rows
- Safe induced stale-price alert: - verify a row rejected due to executor disarmed renders as blocked with reason
- pause `market-reference-ingest` - verify a submitted row renders as submitted
- wait past freshness window - verify quote ids can be copied and used for tracing
- observe `reference_price_stale`
- resume and observe clear
- Safe induced stale-inventory alert:
- pause `inventory-sync`
- wait past freshness window
- observe `inventory_snapshot_stale`
- resume and observe clear
- Funding visibility proof:
- use a real deposit address
- observe pre-credit chain state before bridge credit where timing allows
- later observe credit correlation
- Executor failure alert proof:
- use a controlled non-destructive failure mode such as temporary relay endpoint override in a safe environment or a replayable failure fixture
- verify `executor_submission_failed`
## Out of scope on purpose ## Test plan
- No new trading strategy - unit tests for lifecycle derivation from:
- No historical backtest engine - strategy-rejected rows
- No broad observability stack - executor-disarmed rows
- No polished dashboard frontend - submission-failed rows
- No automated treasury refills - submitted rows
- dashboard bootstrap tests for:
- forbidden `Actionable` removal
- explicit lifecycle labels
- reason text rendering
- identifier exposure
- frontend component tests if needed for copy affordance or row rendering logic
## Still fake at turn open No lifecycle ambiguity fix is complete without a regression test proving the old ambiguous wording cannot return.
- Pre-credit funding visibility is still missing from the live cluster.
- Alert state is still mostly implicit in service logs and manual inspection. ## Validation checklist against the proof
- There is no durable operator-facing record yet for "funds are on the way but not spendable." - `Actionable` no longer appears
- strategy approval is visibly distinct from execution submission
- recent blocked rows explain why they did not trade
- recent submitted rows show that they were submitted
- quote ids are directly usable from the dashboard
## Failure modes to plan for
- the backend joins rows incorrectly and attributes the wrong execution result
- the UI uses softer wording than the backend lifecycle state
- older rows lack enough evidence and the UI pretends certainty
- ids are still truncated without a copy or expand path

256
PROOF.md
View file

@ -1,154 +1,154 @@
# Implementation Proof: pre-credit funding visibility and operator alerts # Implementation Proof: quote lifecycle truth and execution explanation
Status: open Status: open
Opened: 2026-04-02 Opened: 2026-04-09
## Target outcome ## Target outcome
The next turn is complete only when `unrip` can show operators the gap between "funds sent" and "funds spendable" with durable evidence, and can surface actionable alert state for the live loop without requiring log-diving or manual SQL every time something stalls. This turn proves that `unrip` can explain every quote outcome in operator-visible, durable terms instead of forcing the operator to infer meaning from ambiguous labels.
This turn does not expand the trade hot path. It makes the existing live system more explainable and more operable. The concrete target is the live NEAR Intents BTC/EURe system:
- each quote row must expose its current lifecycle state
- each non-trade outcome must expose the decisive reason code
- execution submission must be distinguishable from strategy approval
- blocked, rejected, submitted, failed, and not-filled paths must be visibly different
- quote identifiers must be directly usable by operators for tracing and support
## Why this is a meaningful architecture test
The current operator surface still fails a core thesis requirement:
- the Strategy page shows `Actionable`, which does not tell the operator whether a trade was actually submitted
- an operator looking at one quote cannot answer, at a glance, why it did or did not become a trade
- quote ids are hidden behind truncation with no direct copy affordance
- execution truth exists durably in PostgreSQL, but the UI does not surface the lifecycle coherently
That is not just a copy problem. It is an observability gap in the trading product itself. If the system cannot explain a quote outcome precisely, execution is outrunning observability.
## Hypothesis ## Hypothesis
`unrip` becomes materially safer to operate once it can: `unrip` becomes more trustworthy if quote handling is modeled and rendered as an explicit lifecycle instead of a single strategy verdict:
- strategy evaluation is only one stage in the lifecycle
- executor acceptance or rejection must be first-class state
- venue submission and downstream outcome must be represented separately
- durable reason codes must drive operator labels
- forbidden labels that collapse multiple meanings, especially `Actionable`, must be removed
1. observe configured funding handles before NEAR Intents credit The turn passes only if an operator can inspect a quote and immediately understand whether it was filtered, rejected, blocked, submitted, failed, awaiting venue outcome, not filled, or completed, and why.
2. persist chain-level funding observations separately from spendable inventory
3. link pre-credit observations to later bridge and verifier credit where possible
4. emit durable alert state for stale prices, stale inventory, stuck funding, and failed execution submissions
5. expose that state through the same small control surfaces and PostgreSQL audit trail as the rest of the system
If pre-credit funding remains invisible, or alert state still lives only in transient logs, the live loop is still too opaque for routine funded operation.
## Scope ## Scope
- [O003] Alerts for stale reference prices, stale inventory state, stuck funding actions, and failed executor submissions. - [I021] Quote lifecycle truth and execution explanation: replace ambiguous dashboard verdicts with a per-quote state machine that shows exactly why a quote was filtered, rejected, blocked, submitted, not filled, or executed, with durable reason codes and operator-facing traceability.
- [O004] Pre-credit funding visibility for slow chains: watch configured deposit addresses at chain level, track inbound transfers through mempool and on-chain confirmation before bridge credit, persist that state separately from spendable inventory, and alert operators when funding is seen, delayed, or stuck. - Cover the live path from:
- raw or normalized quote observation
- strategy decision
- execute-trade command emission
- executor result
- solver relay or venue outcome where available
- Update the dashboard surfaces that currently expose quote or strategy truth:
- Strategy page
- any quote or recent-decision tables tied to the active pair
- quote-id presentation and operator trace affordances
## Assumptions
- The existing durable stores already contain enough information for at least the current live path through strategy decision and executor result.
- Some downstream venue-outcome states may still be partially fake or unavailable for older rows; if so, the UI must say that plainly rather than implying more certainty.
- The immediate turn should prioritize truthful lifecycle explanation over broader analytics such as markout or long-window outcome attribution.
## Turn-shaping rules
- `Actionable` is forbidden as an operator-facing state or label.
- Do not add a second analytics product. Stay focused on per-quote lifecycle truth for the live active pair.
- Do not invent lifecycle states that cannot be backed by durable repo-owned evidence.
- If a state transition is inferred rather than durably observed, the UI must make that distinction explicit.
- Prefer a small, explicit state machine over a long list of loosely related badges.
## Non-goals ## Non-goals
- No new venue, pair, or strategy logic. - No new venue integrations.
- No dashboard or polished UI. - No broad historical markout analytics turn.
- No automatic treasury actions or auto-refunding. - No new execution automation or risk widening.
- No attempt to treat chain-level observations as spendable inventory. - No redesign of the entire dashboard visual system beyond what is needed to make the quote lifecycle understandable.
- No change to the live execution arming model.
## Source-of-truth rule ## Required operator behavior
Spendable inventory remains the existing truth:
- bridge and verifier credit determine spendable balances
- chain-level observations are visibility only
The new pre-credit path must never be allowed to make a direction tradable earlier than the verifier does. ### Lifecycle truth
For each visible quote or decision row, the operator must be able to identify the current lifecycle state from a bounded set such as:
- `Filtered`
- `Rejected by strategy`
- `Blocked before submit`
- `Submission failed`
- `Submitted`
- `Awaiting venue outcome`
- `Not filled` or equivalent final non-fill state, if durable evidence exists
- `Completed` or equivalent successful terminal state, if durable evidence exists
## Required runtime behavior Exact labels may vary, but they must be specific and mutually meaningful.
### Funding visibility ### Reason truth
- The system must know the currently active funding handles for BTC and EURe. Each non-terminal or terminal non-trade state must expose a clear decisive reason, such as:
- For configured chains, it must watch those handles before NEAR Intents credit appears. - unsupported pair
- It must distinguish at least these states where applicable: - below edge threshold
- `SEEN_UNCONFIRMED` - inventory unavailable
- `SEEN_CONFIRMED` - executor disarmed
- `CREDIT_PENDING` - executor paused
- `CREDITED` - submission failed
- `FAILED_OR_STUCK` - venue timeout
- BTC is the must-prove chain because that is where live funding latency was operationally visible. - quote expired
- Gnosis support may share the same event model even if its confirmation behavior is simpler.
### Alerts If the decisive reason is not known, the surface must say that plainly instead of inventing confidence.
- Alerts must be durable records, not only log lines.
- At minimum the system must raise and clear alert state for:
- stale reference price
- stale inventory snapshot
- funding seen but not credited within policy
- execution submission failure
- Alert transitions must be inspectable through HTTP state and PostgreSQL.
## Service expectations ### Traceability
For each quote row the operator must be able to access:
- quote id in a non-hidden, copyable form
- decision id where present
- command id where present
- execution result where present
- pair and direction
### `liquidity-manager` The operator must be able to reason from a single row without manually cross-correlating multiple pages.
Must become the owner of chain-level funding observations because it already owns deposit handles and treasury state.
It must: ### UI language
- refresh and retain active funding handles The UI must not render `Actionable`.
- ingest chain-level funding observations
- reconcile them against bridge deposit state
- publish durable funding-observation records
- expose current per-handle funding state
It must not: Any replacement label must answer a concrete operator question, such as:
- mark funds spendable - did strategy approve this?
- trade on pre-credit observations - was a command emitted?
- was the command blocked?
### `inventory-sync` - was it submitted?
May surface pre-credit funding context, but only under a clearly separate non-spendable field. - did it fail?
It must not:
- merge pre-credit observations into `spendable`
### `history-writer`
Must persist the new record families:
- funding observations
- alert events
- optionally current alert snapshots if the implementation separates events from state
### Alert evaluator
This may be a new small service or a tightly scoped extension of an existing one, but it must:
- evaluate staleness and stuck conditions from durable inputs
- emit durable alert events
- expose current alert state and the latest reasons
No broad orchestration or dashboard service should be introduced just to satisfy this proof.
## Required durable storage
PostgreSQL must store at least:
- funding observations before bridge credit
- alert events or alert snapshots
- enough timestamps and IDs to correlate:
- funding handle
- chain tx hash
- later bridge tx hash or deposit record
- resulting verifier credit snapshot when available
Kafka remains the event backbone.
## Required control surface
At minimum operators must be able to inspect:
- active funding handles
- latest pre-credit observations by handle
- confirmation depth or equivalent chain state when available
- whether a funding action is still pending credit
- current active alerts and their reasons
If a new alert service exists, it must expose:
- `GET /healthz`
- `GET /state`
- `POST /pause`
- `POST /resume`
## Definition of done ## Definition of done
- The live cluster still runs the previously proven funded trade loop unchanged for spendable truth. - `Actionable` is removed from operator-facing dashboard surfaces.
- At least one real funding handle is watched at chain level before bridge credit. - A durable quote lifecycle model exists in repo-owned code and is used by the dashboard.
- For at least one real deposit path, the system records a pre-credit observation before or during confirmation and later records the credited state separately. - At least the current live quote path through strategy decision and executor result is rendered coherently per quote.
- PostgreSQL contains durable records for the pre-credit funding path and alert path. - The operator can tell, from one row, why a recent quote did or did not turn into a submitted trade.
- Operators can inspect current funding observations and current alerts through control APIs. - Quote ids are copyable and clearly visible enough for tracing.
- A stale price or stale inventory condition can be induced safely and becomes a durable alert. - Regression tests cover at least:
- A funding delay or manually injected stuck condition can be represented as a durable alert with explicit reason fields. - strategy-approved but executor-disarmed rows
- A failed execution submission path is represented as an alert without inventing fake venue traffic. - submitted rows
- Tests cover: - forbidden ambiguous label removal
- pre-credit observations staying non-spendable
- alert raise and clear transitions For this turn to close with status `passed`, the specific operator question:
- correlation of funding observation to later credit when identifiers are available
`Why did this quote not trade?`
must be answerable directly from the dashboard for recent rows without needing manual database inspection.
## Validation evidence required
- direct UI or bootstrap evidence that recent rows show explicit lifecycle states instead of `Actionable`
- direct evidence that a strategy-approved but executor-disarmed row renders as blocked or rejected with reason
- direct evidence that a submitted row renders as submitted
- direct evidence that quote ids are directly usable for tracing
- automated test evidence for lifecycle derivation and dashboard rendering
## Failure conditions ## Failure conditions
- Funding observations exist only in logs and are not queryable later. - `Actionable` still appears in the dashboard
- Pre-credit observations leak into spendable inventory or strategy gating. - the operator still cannot distinguish strategy approval from execution submission
- Alerts cannot be queried as current state. - non-trade rows still lack a decisive reason
- The only proof of stuck funding is a human manually watching a block explorer. - quote ids remain hidden or non-copyable
- The implementation adds a dashboard shell without stronger runtime truth. - lifecycle labels are only cosmetic and not backed by durable repo-owned state
## Current real ## Current real before this turn
- The first funded BTC/EURe live loop is already proven: - strategy decisions are stored durably
- real quote ingest - execution results are stored durably
- real reference pricing - command ids, decision ids, and quote ids already exist in the durable path
- real credited inventory - the operator dashboard already serves recent decisions and execution-adjacent state
- real strategy decisions
- real `quote_response` submissions ## Deliberately not built by this proof
- durable event chain in PostgreSQL - full venue settlement attribution for all historic trades
- Portfolio metrics are now durably computed and exposed, but alerting and pre-credit funding visibility are still incomplete. - generalized quote analytics beyond lifecycle explanation
- multi-venue lifecycle harmonization

View file

@ -0,0 +1,284 @@
# Implementation Turn: runtime health sentinel alert routing and anomaly detection
Status: open
Opened: 2026-04-08
## Goal
Make the live NEAR Intents system loudly and durably aware of broken runtime truth.
The system must detect stale or disconnected quote flow, grade service health from actual runtime behavior instead of mere reachability, surface that severity aggressively in the dashboard, emit durable alerts plus at least one external notification, and support tightly scoped safe containment for non-fund-moving failures.
## Selected backlog items
- [I020] Runtime health sentinel, alert routing, and anomaly detection: detect stale or broken service behavior across ingest, execution, persistence, and operator surfaces; surface critical failures loudly in the dashboard; emit durable alerts plus external notifications; and support tightly scoped safe remediation for non-fund-moving failures.
## Design rules
- Keep runtime health truth repo-owned and tied to the existing Kafka plus PostgreSQL pipeline.
- Prefer deterministic invariants first. Add anomaly detection as a secondary layer for things we did not name ahead of time.
- Do not trust service-local `/healthz` alone for end-to-end health.
- Safe containment beats speculative auto-repair.
- No new fund-moving actions or risk widening.
## Problem statement for this turn
The live incident showed three separate weaknesses:
- `near-intents-ingest` could remain running while quote truth stopped
- the dashboard could display large freshness ages while still calling the service healthy
- no durable alert or external notification made the issue impossible to miss
The turn therefore needs to improve:
- runtime-state collection
- cross-service health evaluation
- durable alert generation
- operator severity rendering
- external alert delivery
- bounded anomaly detection
## Minimal architecture changes for this turn
- `ops-sentinel` becomes the runtime health authority, not just a narrow stale-data checker.
- services expose richer state and truthful health hints through existing control APIs.
- `operator-dashboard` consumes alert severity and derived health rather than inferring “healthy” from reachability.
- one outbound notifier module sends raised or cleared alerts to an external webhook-style sink.
- anomaly logic runs in repo-owned code over service state and durable or live counters rather than free-text logs.
## Architecture changes
### 1. Extend service-local state and health surfaces
Each relevant service should expose enough state for sentinel evaluation.
For `near-intents-ingest`:
- websocket connected flag
- last websocket message time
- last matching quote time
- last published quote time
- publish error count
- pair filter state
For `trade-executor`:
- solver relay connected flag
- pending requests or in-flight count
- last quote-status or relay message time
- last error
- armed and paused state
For `history-writer`:
- last write time
- last alert write time
- offsets by topic
- database connectivity
- last error
For `operator-dashboard`:
- recent upstream source errors
- websocket fan-out status if available
- backend bootstrap-source failures
Do not rely on Kubernetes liveness alone.
### 2. Expand `ops-sentinel` into runtime-health evaluation
Add a service-state polling loop inside `ops-sentinel` for the existing control APIs.
The sentinel should evaluate at least:
- service reachability
- service-local freshness
- disconnected websocket clients
- pipeline stalls where source activity stops or downstream persistence stops
- unsafe armed-state combinations
- self-health staleness
Prefer deriving named condition objects first, then reconciling them into alert state.
### 3. Introduce a runtime health model
Define explicit derived states per service:
- `healthy`
- `warning`
- `critical`
- `offline`
- `paused`
Service severity must be derived from:
- explicit `/healthz ok: false`
- freshness thresholds
- disconnected state
- active alert severity for that service
- pipeline mismatches tied to that service
This logic should live in repo-owned code shared or mirrored between sentinel and dashboard so the UI cannot drift into a softer interpretation.
### 4. Add new deterministic alert classes
Extend `src/core/alert-engine.mjs` to cover at least:
- ingest disconnected
- ingest last quote stale
- ingest last publish stale
- executor relay disconnected
- history writer stalled
- dashboard upstream source degraded
- sentinel stale
- strategy or executor armed while critical input truth is stale
Each alert should include:
- `alert_code`
- `severity`
- `service_scope`
- clear reason text
- relevant timestamps
- threshold values used
- enough details for replay and operator debugging
### 5. Add a quote-flow path check
The recent incident was not merely “service disconnected.” It was “quote truth stopped.”
Add explicit checks for the active pair path:
- last matching quote age
- last raw quote persisted age if available
- last normalized quote persisted age if available
- mismatch between service-local quote timestamps and history-writer progress
This should allow the system to say whether the failure is:
- upstream relay disconnected
- ingest receiving traffic but not publishing
- history path stalled after publish
- dashboard path stale despite durable writes
### 6. Add bounded anomaly detection
Do not build a free-text log learner. Use recent history and rolling baselines.
Initial anomaly detectors:
- quote rate collapse over recent windows versus a rolling baseline
- reconnect frequency spike over recent windows
- durable-topic advancement collapse relative to recent baseline
- raw-to-normalized or normalized-to-durable mismatch if both sides normally advance together
Implementation shape:
- compute rolling counters in sentinel state or read recent durable counts from PostgreSQL
- emit warning-level anomaly alerts or operator notices
- keep deterministic alerts as the primary critical path
### 7. Add external alert delivery
Add one notifier module and config for a webhook-oriented external sink.
Required behaviors:
- deliver raised alerts
- optionally deliver clear events
- dedupe by alert identity
- record delivery success or failure in service state
- never block core alert state on delivery failure
Prefer a generic webhook shape that can feed:
- Alertmanager receiver
- Slack or Discord webhook
- another downstream router
If a direct Alertmanager integration is convenient, keep the repo-owned payload explicit and narrow.
### 8. Add safe containment and remediation hooks
Add only narrowly safe actions.
Allowed first-pass actions:
- reconnect or refresh a service-local websocket client
- pause or resume ingest-side or alert-side routines
- disarm or pause strategy or executor when critical stale-data conditions are active
Containment policy for this turn:
- if quote truth is critically stale, the system must at least surface a truthful safe control
- if automatic containment is added, it must be non-fund-moving and clearly auditable
Do not add:
- restarts of arbitrary deployments without explicit approval
- trade retries
- bridge, approval, or funding actions
### 9. Make the dashboard “angry”
The dashboard should no longer use friendly reachability heuristics for serious failures.
Required UI changes:
- global critical banner when critical alerts are active
- service cards reflect derived severity, not just `health.ok`
- stale services show exact age and why they are stale
- funds page or relevant page clearly flags stale quotes
- armed-state plus stale-data conditions render as critical
The specific failure mode `Freshness 30.8 h` plus `healthy` must become impossible.
### 10. Preserve durable truth
Runtime health changes must remain durable and replayable.
Use existing paths where practical:
- `ops.alert` stays the durable alert topic
- `ops_alerts` remains the canonical alert store
If anomaly notices need separate storage, keep them closely aligned with existing alerting rather than creating a parallel history system without need.
## Concrete implementation order
### Phase 1. Define the health contract
- enumerate service-local fields needed from each control API
- define severity thresholds for each relevant service
- define the first deterministic alert set
- define the first anomaly set and its data sources
### Phase 2. Strengthen service state surfaces
- update relevant `/state` and `/healthz` providers
- expose websocket connection and freshness timestamps where missing
- expose notifier and sentinel delivery state where needed
### Phase 3. Expand sentinel evaluation
- add service polling
- evaluate new deterministic conditions
- reconcile raised and cleared alert state
- keep current alert engine behavior passing
### Phase 4. Add quote-flow and pipeline checks
- compare ingest-local state to durable progression
- detect stalled or mismatched quote flow
- emit new alerts with precise reasons
### Phase 5. Add anomaly detection
- implement rolling or recent-window rate calculations
- compare against baseline windows
- emit warning-level anomaly alerts or notices
### Phase 6. Add external delivery
- add notifier config and delivery module
- send alert transitions to one external sink
- expose delivery health and failures
### Phase 7. Make the dashboard severity truthful
- replace soft healthy classification with derived severity
- add loud banners and clearer service-card rendering
- show freshness and cause directly
### Phase 8. Add safe containment
- wire at least one truthful safe containment action
- optionally auto-disarm or auto-pause on critical stale-data conditions if implemented cleanly
- keep changes auditable and non-fund-moving
### Phase 9. Validate against the recent incident
- reproduce or simulate a stranded ingest websocket condition
- confirm durable alert, dashboard critical state, external notification, and containment behavior
## Test plan
- unit tests for alert-engine raise and clear transitions for each new runtime-health alert
- unit tests for derived service severity from freshness and disconnected state
- unit tests for anomaly detectors on quote-rate collapse and reconnect spikes
- unit tests for notifier dedupe and failure handling
- integration tests for sentinel polling and alert emission using mocked service states
- integration tests for dashboard bootstrap or reducer behavior under critical alerts and stale service snapshots
- regression tests proving current price, inventory, funding, and executor submission alerts still work
No runtime-health bug fix is complete without a regression test covering the missed condition.
## Validation checklist against the proof
- stale ingest is detected by policy, not by manual interpretation
- dashboard shows warning or critical instead of healthy for stale or disconnected ingest
- alert event is durable and replayable
- external notification is emitted
- at least one safe containment path exists
- anomaly layer flags at least one non-predeclared abnormal pattern
- live NEAR spendable truth remains unchanged
## Failure modes to plan for
- service `/healthz` still says `ok` while runtime truth is stale
- dashboard severity logic diverges from sentinel logic
- alert delivery blocks or breaks alert generation
- anomaly detection floods noise and hides real failures
- containment silently changes arming state without durable evidence
- service polling itself goes stale and no sentinel-stale alert fires

View file

@ -0,0 +1,174 @@
# Implementation Proof: runtime health sentinel alert routing and anomaly detection
Status: open
Opened: 2026-04-08
## Target outcome
This turn proves that `unrip` can detect and loudly escalate broken runtime behavior before operators mistake stale or disconnected systems for healthy trading state.
The concrete target is the currently live NEAR Intents BTC/EURe system:
- detect when quote flow stops or becomes stale
- detect when websocket-backed services are disconnected or stranded
- detect when durable pipeline components are reachable but no longer advancing truth
- surface those failures as durable alerts and obvious dashboard state
- deliver at least one external alert notification through a repo-controlled routing path
- contain risk safely by exposing or applying only explicitly approved non-fund-moving remediation
## Why this is a meaningful architecture test
The cluster incident on 2026-04-07 and 2026-04-08 showed a real failure mode:
- `near-intents-ingest` stopped receiving and publishing quotes after `2026-04-07T09:06:38Z`
- the service remained running in Kubernetes
- the dashboard still rendered the service as effectively healthy despite `30h+` freshness age
- no durable alert or external notification made the failure hard to miss
That is an architecture failure, not just an isolated websocket bug. If the system cannot detect that its truth pipeline has gone stale, it is not safe enough to broaden venue scope.
## Hypothesis
`unrip` becomes more trustworthy if runtime health is treated as first-class product truth:
- service-local state is not enough; cross-service health must be derived from the shared pipeline
- stale or broken quote flow must become durable, replayable alert truth
- dashboard health must be severity-driven, not merely reachability-driven
- anomaly detection should complement hard invariants, not replace them
- safe containment is more important than optimistic self-healing
The turn passes only if a quote-flow or service-health failure like the recent ingest outage would now produce obvious dashboard severity plus durable and external alert evidence within bounded time.
## Scope
- [I020] Runtime health sentinel, alert routing, and anomaly detection: detect stale or broken service behavior across ingest, execution, persistence, and operator surfaces; surface critical failures loudly in the dashboard; emit durable alerts plus external notifications; and support tightly scoped safe remediation for non-fund-moving failures.
- Extend the existing `ops-sentinel` into the runtime health authority for the current single-pair NEAR Intents system.
- Cover at least these services and paths:
- `near-intents-ingest`
- `trade-executor`
- `history-writer`
- `operator-dashboard`
- the quote-flow path from ingest to PostgreSQL-backed operator visibility
- Add one repo-controlled external alert delivery path. Initial assumption: a webhook-oriented sink suitable for Alertmanager, Slack, Discord, or equivalent downstream routing.
- Add bounded anomaly detection based on rolling baselines and pipeline gaps for the active pair and live services. This is support for operator review, not a replacement for deterministic safety alerts.
## Assumptions
- The current control APIs remain the primary service-local truth surface for runtime state and safe controls.
- External notification should be added through one simple sink first, not a broad escalation matrix in the repo itself.
- Alertmanager or another downstream fan-out layer may exist later, but this turn only needs a truthful outbound integration point plus at least one working delivery path.
- The first anomaly layer should use deterministic rolling windows or baseline comparisons over repo-owned metrics and history, not opaque model training over free-text logs.
- Safe remediation in this turn means non-fund-moving actions only, such as reconnect, refresh, pause, resume, or explicit trading containment.
## Turn-shaping rules
- This is a runtime-truth turn, not a generic observability platform buildout.
- Do not add a broad metrics stack, tracing system, or large log-ML pipeline unless required by the proof.
- Do not pretend anomaly detection is safety-complete. Deterministic invariants remain the primary alert path.
- Do not let automatic remediation widen risk. If a condition is unsafe, containment or explicit alerting is preferred to speculative recovery.
- Browser clients must still talk only to repo-owned backend services.
## Non-goals
- No broad CoW Protocol implementation work in this turn beyond preserving the paused archive.
- No generalized self-healing orchestration across arbitrary workloads.
- No live-funds-moving automation such as bridge actions, funding, approvals, or trade submission retries.
- No full-blown ML log classifier or LLM-operated runtime manager.
- No replacement of Loki, Grafana, or external alert systems already in the cluster.
## Required runtime behavior
### Health truth
The system must be able to derive runtime health from more than mere pod liveness.
At minimum it must detect and represent:
- service reachable but stale
- websocket disconnected
- durable writer reachable but topic flow stalled
- operator dashboard upstream source degraded
- alerting subsystem stale or failing
- strategy or executor armed while critical upstream truth is stale
### Alert truth
The system must durably emit alert events for critical runtime failures.
At minimum it must raise and clear alerts for:
- `near_intents_ingest_disconnected`
- `near_intents_quotes_stale`
- `near_intents_publish_stale`
- `trade_executor_relay_disconnected`
- `history_writer_stalled` or equivalent durable-write stall
- `operator_dashboard_source_degraded`
- `sentinel_stale`
The exact names may vary, but the semantic coverage must be equivalent and replayable from stored records.
### Operator surface
Operators must be able to answer:
- which service is degraded or critical
- why it is degraded
- how stale the underlying truth is
- whether the issue is local-service, pipeline, or downstream-surface related
- whether alert delivery succeeded
- what safe control actions are available
The dashboard must no longer label a service healthy if its runtime truth is stale by policy.
### External notification
At least one external notification path must receive a raised alert from repo-controlled code.
That path must:
- dedupe by alert identity or equivalent key
- include severity, scope, reason, and timestamps
- support clear or recovery notification, or explicitly record why clear delivery is not supported
### Safe containment
For at least one critical stale-data condition, the system must have a truthful containment path.
Initial target:
- if ingest or quote truth is critically stale, operators can see a real safe action and the system can optionally force or recommend a non-fund-moving containment state such as pausing or disarming trade-driving components
### Bounded anomaly detection
The system must add one anomaly layer beyond named invariants.
At minimum it must be able to flag:
- quote rate collapse versus recent baseline
- reconnect frequency spike versus recent baseline
- topic-flow mismatch, such as raw or service-local activity without downstream durable progression
These anomaly signals may be warning-level and operator-facing rather than pager-level.
## Definition of done
- The paused CoW turn is archived and the live turn files govern this runtime-health turn.
- `ops-sentinel` or equivalent repo-owned runtime health authority evaluates service and pipeline health for the active live system.
- Durable alert events represent the critical stale or disconnected conditions that the recent incident exposed.
- The dashboard shows critical or stale health for those conditions rather than merely reporting large freshness ages under healthy badges.
- At least one external alert route is wired and demonstrated from repo-owned code.
- At least one safe containment action exists and is truthful.
- At least one anomaly signal is implemented using recent-history comparisons or rolling baselines.
- Tests cover alert raising and clearing, health classification, dashboard severity rendering, and alert delivery behavior.
For this turn to close with status `passed`, a reproduced or simulated quote-ingest failure must produce:
- a durable raised alert
- a clearly non-healthy dashboard state
- an external notification
- evidence of safe containment or explicit safe-control availability
## Validation evidence required
- direct evidence that a stale or disconnected ingest condition is detected within a bounded threshold
- direct evidence that the dashboard severity changes to warning or critical instead of healthy for that condition
- direct evidence that the corresponding alert is stored durably
- direct evidence that an external alert notification is emitted
- automated test evidence for alert raise and clear transitions
- automated test evidence for dashboard health classification from stale service state
- automated test evidence for anomaly detection logic on at least one rolling-baseline case
## Failure conditions
- The system still shows healthy or equivalent status while quote truth is stale by policy.
- Alerts exist only in logs and are not durable or operator-visible.
- External notification is absent or only manual.
- Anomaly detection is described but not implemented in repo-owned code.
- Safe containment would move funds or widen risk without explicit approval.
- The turn produces only docs, dashboards, or thresholds without stronger runtime truth.
## Current real before this turn
- The NEAR Intents BTC/EURe loop is live and already has durable history, strategy, execution, and dashboard surfaces.
- `ops-sentinel` already emits some durable alerts for price, inventory, funding, and executor submission failures.
- Grafana and Loki already exist in the cluster.
- Service control APIs already expose state and safe controls.
## Deliberately not built by this proof
- full multi-channel escalation policy management inside the repo
- ML-first anomaly detection over raw free-text logs
- broad auto-remediation beyond narrowly approved safe runtime actions
- second-venue CoW execution work

View file

@ -1,4 +1,5 @@
import { formatAge, formatBoolean, formatEur, formatTimestamp, signedClass, truncateMiddle } from '../lib/format.js'; import { formatAge, formatBoolean, formatEur, formatTimestamp, signedClass, truncateMiddle } from '../lib/format.js';
import { SUBMISSION_COPY } from '../lib/submissionCopy.js';
function statusSubtitle(label, status, websocketState) { function statusSubtitle(label, status, websocketState) {
switch (label) { switch (label) {
@ -8,8 +9,8 @@ function statusSubtitle(label, status, websocketState) {
return formatTimestamp(status.market_observed_at); return formatTimestamp(status.market_observed_at);
case 'Inventory Freshness': case 'Inventory Freshness':
return formatTimestamp(status.inventory_observed_at); return formatTimestamp(status.inventory_observed_at);
case 'Trading': case SUBMISSION_COPY.statusTileLabel:
return 'Successful submissions from durable history'; return SUBMISSION_COPY.statusTileSubtitle;
default: default:
return ''; return '';
} }
@ -25,8 +26,8 @@ export default function StatusBar({ status, websocketState }) {
['Alerts', `${status.active_alert_count || 0} ${status.highest_alert_severity ? `(${status.highest_alert_severity})` : ''}`.trim()], ['Alerts', `${status.active_alert_count || 0} ${status.highest_alert_severity ? `(${status.highest_alert_severity})` : ''}`.trim()],
['Strategy Armed', formatBoolean(status.strategy_armed)], ['Strategy Armed', formatBoolean(status.strategy_armed)],
['Executor Armed', formatBoolean(status.executor_armed)], ['Executor Armed', formatBoolean(status.executor_armed)],
['Trading', `${status.recent_trade_count || 0} trades`], [SUBMISSION_COPY.statusTileLabel, `${status.recent_trade_count || 0} ${SUBMISSION_COPY.statusTileValueSuffix}`],
['Last Trade', formatTimestamp(status.last_successful_trade_at)], [SUBMISSION_COPY.lastStatusTileLabel, formatTimestamp(status.last_successful_trade_at)],
]; ];
return ( return (

View file

@ -0,0 +1,15 @@
export const SUBMISSION_COPY = {
statusTileLabel: 'Submissions',
statusTileSubtitle: 'Successful quote-response submissions from durable history',
statusTileValueSuffix: 'submissions',
lastStatusTileLabel: 'Last Submission',
recentMetricLabel: 'Recent submissions',
recentMetricValueSuffix: 'submissions',
termsEyebrow: 'Submission activity',
termsTitle: 'Recent submitted quote terms',
termsEmpty: 'No submitted quote responses are available yet.',
ledgerTitle: 'Submitted quote responses',
ledgerSubtitle: 'PostgreSQL-backed pagination of executor submissions',
ledgerEmpty: 'No submitted quote responses are stored yet.',
ledgerCountLabel: (page, totalPages, total) => `Page ${page} of ${totalPages} - ${total} submitted quote responses`,
};

View file

@ -5,6 +5,7 @@ import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx'; import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx'; import TableFrame from '../components/TableFrame.jsx';
import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js'; import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js';
import { SUBMISSION_COPY } from '../lib/submissionCopy.js';
function buildInitialEstimateForm(balances, withdrawalDefaults) { function buildInitialEstimateForm(balances, withdrawalDefaults) {
const firstAssetId = balances?.[0]?.asset_id || ''; const firstAssetId = balances?.[0]?.asset_id || '';
@ -204,7 +205,7 @@ function QuotesTable({ items }) {
} }
function AssetChangeTable({ items }) { function AssetChangeTable({ items }) {
if (!items?.length) return <EmptyState>No successful trades are available yet.</EmptyState>; if (!items?.length) return <EmptyState>{SUBMISSION_COPY.termsEmpty}</EmptyState>;
return ( return (
<TableFrame> <TableFrame>
@ -213,8 +214,8 @@ function AssetChangeTable({ items }) {
<tr> <tr>
<th>Observed</th> <th>Observed</th>
<th>Quote</th> <th>Quote</th>
<th>Spend</th> <th>Input</th>
<th>Receive</th> <th>Output</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -233,7 +234,7 @@ function AssetChangeTable({ items }) {
} }
function TradesTable({ items }) { function TradesTable({ items }) {
if (!items?.length) return <EmptyState>No successful trades are stored yet.</EmptyState>; if (!items?.length) return <EmptyState>{SUBMISSION_COPY.ledgerEmpty}</EmptyState>;
return ( return (
<TableFrame> <TableFrame>
@ -446,9 +447,9 @@ export default function FundsPage({
value={formatEur(profitability.trading_contribution_eure)} value={formatEur(profitability.trading_contribution_eure)}
/> />
<MetricCard <MetricCard
label="Recent trading" label={SUBMISSION_COPY.recentMetricLabel}
meta={formatTimestamp(profitability.last_successful_trade_at)} meta={formatTimestamp(profitability.last_successful_trade_at)}
value={`${profitability.recent_trade_count || 0} trades`} value={`${profitability.recent_trade_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
/> />
</div> </div>
<div className="panel-subtitle"> <div className="panel-subtitle">
@ -568,8 +569,8 @@ export default function FundsPage({
<div className="panel"> <div className="panel">
<div className="panel-head"> <div className="panel-head">
<div> <div>
<div className="eyebrow">Trade-driven changes</div> <div className="eyebrow">{SUBMISSION_COPY.termsEyebrow}</div>
<h3>Recent asset deltas</h3> <h3>{SUBMISSION_COPY.termsTitle}</h3>
</div> </div>
</div> </div>
<AssetChangeTable items={funds.trade_asset_changes} /> <AssetChangeTable items={funds.trade_asset_changes} />
@ -580,13 +581,13 @@ export default function FundsPage({
<div className="panel-head"> <div className="panel-head">
<div> <div>
<div className="eyebrow">Durable ledger</div> <div className="eyebrow">Durable ledger</div>
<h3>Successful trades</h3> <h3>{SUBMISSION_COPY.ledgerTitle}</h3>
</div> </div>
<div className="status-subtle">PostgreSQL-backed pagination</div> <div className="status-subtle">{SUBMISSION_COPY.ledgerSubtitle}</div>
</div> </div>
<TradesTable items={trades.items} /> <TradesTable items={trades.items} />
<div className="pagination"> <div className="pagination">
<div className="status-subtle">{`Page ${trades.page} of ${trades.total_pages} - ${trades.total} successful trades`}</div> <div className="status-subtle">{SUBMISSION_COPY.ledgerCountLabel(trades.page, trades.total_pages, trades.total)}</div>
<div className="button-row"> <div className="button-row">
<button <button
className="button secondary" className="button secondary"

View file

@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { SUBMISSION_COPY } from '../src/operator-dashboard/static/lib/submissionCopy.js';
test('submission copy does not present submissions as trades', () => {
const text = [
SUBMISSION_COPY.statusTileLabel,
SUBMISSION_COPY.statusTileSubtitle,
SUBMISSION_COPY.recentMetricLabel,
SUBMISSION_COPY.termsEyebrow,
SUBMISSION_COPY.termsTitle,
SUBMISSION_COPY.termsEmpty,
SUBMISSION_COPY.ledgerTitle,
SUBMISSION_COPY.ledgerSubtitle,
SUBMISSION_COPY.ledgerEmpty,
SUBMISSION_COPY.ledgerCountLabel(1, 2, 3),
].join(' ').toLowerCase();
assert.ok(text.includes('submission'));
assert.ok(!text.includes('successful trades'));
assert.ok(!text.includes('asset deltas'));
assert.ok(!text.includes('trade-driven'));
});