17-task plan for replacing hardcoded mock data in fxt watch with real gRPC streaming data from the API Gateway. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
670 lines
24 KiB
Markdown
670 lines
24 KiB
Markdown
# TUI Live Data Wiring — Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Replace all hardcoded mock data in `fxt watch` with real gRPC streaming data from the API Gateway, using poll-to-stream adapters in the gateway for RPCs that lack native streaming.
|
|
|
|
**Architecture:** 9 new streaming RPCs added to protos, implemented as poll-to-stream adapters in the API Gateway proxies. 3 existing unimplemented monitoring streams get real implementations. TUI gets a `DataFetcher` module that opens all streams and sends `StateUpdate` enum variants through an mpsc channel to the event loop, which owns `AppState` exclusively.
|
|
|
|
**Tech Stack:** Rust, tonic 0.14, async-stream, tokio-stream, ratatui 0.29, crossterm 0.28
|
|
|
|
---
|
|
|
|
## Stream Inventory
|
|
|
|
### New streaming RPCs (to be added to protos + gateway adapters):
|
|
|
|
| # | Proto | New RPC | Wraps | Interval | Cockpit |
|
|
|---|---|---|---|---|---|
|
|
| 1 | `broker_gateway.proto` | `StreamAccountState` | `GetAccountState` | 3s | Trading |
|
|
| 2 | `broker_gateway.proto` | `StreamSessionStatus` | `GetSessionStatus` | 5s | Trading |
|
|
| 3 | `risk.proto` | `StreamCircuitBreakerStatus` | `GetCircuitBreakerStatus` | 2s | Risk |
|
|
| 4 | `risk.proto` | `StreamRiskMetrics` | `GetRiskMetrics` | 3s | Risk |
|
|
| 5 | `data_acquisition.proto` | `StreamDownloadStatus` | `ListDownloadJobs` | 5s | Data |
|
|
| 6 | `ml.proto` | `StreamModelStatus` | `GetModelStatus` | 5s | Overview |
|
|
| 7 | `trading_agent.proto` | `StreamAgentStatus` | `GetAgentStatus` | 3s | Overview |
|
|
| 8 | `trading.proto` | `StreamPortfolioSummary` | `GetPortfolioSummary` | 3s | Trading |
|
|
| 9 | `trading.proto` | `StreamOrderBook` | `GetOrderBook` | 1s | Trading |
|
|
|
|
### Existing unimplemented monitoring streams (implement in monitoring_handler.rs):
|
|
|
|
| # | RPC | Status | Cockpit |
|
|
|---|---|---|---|
|
|
| 10 | `monitoring.StreamSystemStatus` | Proto exists, returns `Status::unimplemented` | Services |
|
|
| 11 | `monitoring.StreamMetrics` | Proto exists, returns `Status::unimplemented` | Services |
|
|
| 12 | `monitoring.StreamAlerts` | Proto exists, returns `Status::unimplemented` | Services |
|
|
|
|
### Existing working streams (already proxied, just need TUI clients):
|
|
|
|
| Proto | Stream | Cockpit |
|
|
|---|---|---|
|
|
| `trading.proto` | `StreamPositions` | Trading |
|
|
| `trading.proto` | `StreamOrders` | Trading |
|
|
| `trading.proto` | `StreamExecutions` | Trading |
|
|
| `trading.proto` | `StreamMarketData` | Data |
|
|
| `risk.proto` | `StreamVaRUpdates` | Risk |
|
|
| `risk.proto` | `StreamRiskAlerts` | Risk |
|
|
| `monitoring.proto` | `StreamTrainingMetrics` | Training |
|
|
| `ml.proto` | `StreamModelMetrics` | Training |
|
|
| `ml.proto` | `StreamSignalStrength` | Overview |
|
|
| `trading_agent.proto` | `StreamAgentActivity` | Overview |
|
|
|
|
---
|
|
|
|
## Task 1: Add 9 new streaming RPCs to proto files
|
|
|
|
**Files:**
|
|
- Modify: `proto/broker_gateway.proto`
|
|
- Modify: `proto/risk.proto`
|
|
- Modify: `proto/data_acquisition.proto`
|
|
- Modify: `proto/ml.proto`
|
|
- Modify: `proto/trading_agent.proto`
|
|
- Modify: `proto/trading.proto`
|
|
|
|
**What:** Add `rpc Stream*` methods to existing service definitions. Each new stream reuses existing request/response types (no new messages needed). Use empty request messages for streams that don't need parameters.
|
|
|
|
**Changes per proto:**
|
|
|
|
**`broker_gateway.proto`** — add after `HealthCheck` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls GetAccountState at gateway level
|
|
rpc StreamAccountState(StreamAccountStateRequest) returns (stream GetAccountStateResponse);
|
|
// Server-streaming: polls GetSessionStatus at gateway level
|
|
rpc StreamSessionStatus(StreamSessionStatusRequest) returns (stream GetSessionStatusResponse);
|
|
}
|
|
|
|
message StreamAccountStateRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (3s)
|
|
}
|
|
|
|
message StreamSessionStatusRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (5s)
|
|
}
|
|
```
|
|
|
|
**`risk.proto`** — add after `GetCircuitBreakerStatus` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls GetCircuitBreakerStatus at gateway level
|
|
rpc StreamCircuitBreakerStatus(StreamCircuitBreakerStatusRequest) returns (stream GetCircuitBreakerStatusResponse);
|
|
// Server-streaming: polls GetRiskMetrics at gateway level
|
|
rpc StreamRiskMetrics(StreamRiskMetricsRequest) returns (stream GetRiskMetricsResponse);
|
|
}
|
|
|
|
message StreamCircuitBreakerStatusRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (2s)
|
|
}
|
|
|
|
message StreamRiskMetricsRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (3s)
|
|
}
|
|
```
|
|
|
|
**`data_acquisition.proto`** — add after `HealthCheck` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls ListDownloadJobs at gateway level
|
|
rpc StreamDownloadStatus(StreamDownloadStatusRequest) returns (stream ListDownloadJobsResponse);
|
|
}
|
|
|
|
message StreamDownloadStatusRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (5s)
|
|
}
|
|
```
|
|
|
|
**`ml.proto`** — add after `StreamSignalStrength` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls GetModelStatus at gateway level
|
|
rpc StreamModelStatus(StreamModelStatusRequest) returns (stream GetModelStatusResponse);
|
|
}
|
|
|
|
message StreamModelStatusRequest {
|
|
string model_name = 1; // empty = all models
|
|
uint32 interval_seconds = 2; // 0 = server default (5s)
|
|
}
|
|
```
|
|
|
|
**`trading_agent.proto`** — add after `HealthCheck` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls GetAgentStatus at gateway level
|
|
rpc StreamAgentStatus(StreamAgentStatusRequest) returns (stream GetAgentStatusResponse);
|
|
}
|
|
|
|
message StreamAgentStatusRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (3s)
|
|
}
|
|
```
|
|
|
|
**`trading.proto`** — add after `GetRegimeTransitions` RPC:
|
|
```protobuf
|
|
// Server-streaming: polls GetPortfolioSummary at gateway level
|
|
rpc StreamPortfolioSummary(StreamPortfolioSummaryRequest) returns (stream GetPortfolioSummaryResponse);
|
|
// Server-streaming: polls GetOrderBook at gateway level
|
|
rpc StreamOrderBook(StreamOrderBookRequest) returns (stream GetOrderBookResponse);
|
|
}
|
|
|
|
message StreamPortfolioSummaryRequest {
|
|
uint32 interval_seconds = 1; // 0 = server default (3s)
|
|
}
|
|
|
|
message StreamOrderBookRequest {
|
|
string symbol = 1;
|
|
int32 depth = 2;
|
|
uint32 interval_seconds = 3; // 0 = server default (1s)
|
|
}
|
|
```
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt 2>&1 | head -5` — new proto types should compile but gateway will have errors (expected — server trait now requires new methods).
|
|
|
|
**Commit:** `feat(proto): add 9 streaming RPCs for TUI live data`
|
|
|
|
---
|
|
|
|
## Task 2: Gateway stream adapters — broker_gateway + data_acquisition
|
|
|
|
**Files:**
|
|
- Modify: `services/api_gateway/src/grpc/broker_gateway_proxy.rs`
|
|
- Modify: `services/api_gateway/src/grpc/data_acquisition_proxy.rs`
|
|
|
|
**What:** Add poll-to-stream adapter methods to existing proxy structs. Each adapter spawns an `async_stream::stream!` loop that polls the backend unary RPC and yields responses.
|
|
|
|
**Pattern** (used by all gateway stream adapters):
|
|
```rust
|
|
use std::time::Duration;
|
|
|
|
// In the impl block, add stream type alias:
|
|
type StreamAccountStateStream =
|
|
Pin<Box<dyn Stream<Item = Result<GetAccountStateResponse, Status>> + Send>>;
|
|
|
|
// Then the method:
|
|
async fn stream_account_state(
|
|
&self,
|
|
request: Request<StreamAccountStateRequest>,
|
|
) -> Result<Response<Self::StreamAccountStateStream>, Status> {
|
|
let req = request.into_inner();
|
|
let interval_secs = if req.interval_seconds == 0 { 3 } else { req.interval_seconds.clamp(1, 60) };
|
|
let mut client = self.client.clone();
|
|
|
|
let stream = async_stream::stream! {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_secs)));
|
|
loop {
|
|
interval.tick().await;
|
|
match client.get_account_state(Request::new(GetAccountStateRequest {})).await {
|
|
Ok(resp) => yield Ok(resp.into_inner()),
|
|
Err(e) => {
|
|
error!("Backend GetAccountState failed: {}", e);
|
|
yield Err(e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Response::new(Box::pin(stream)))
|
|
}
|
|
```
|
|
|
|
**broker_gateway_proxy.rs** — add 2 methods:
|
|
- `stream_account_state` (default interval 3s, polls `get_account_state`)
|
|
- `stream_session_status` (default interval 5s, polls `get_session_status`)
|
|
- Add `use std::time::Duration;` at top
|
|
|
|
**data_acquisition_proxy.rs** — add 1 method:
|
|
- `stream_download_status` (default interval 5s, polls `list_download_jobs` with empty request)
|
|
- Add `use std::time::Duration;` at top
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p api_gateway 2>&1 | grep "error\[" | head -10` — broker_gateway and data_acquisition errors should be gone. Other proxy errors expected.
|
|
|
|
**Commit:** `feat(api_gateway): add broker_gateway + data_acquisition stream adapters`
|
|
|
|
---
|
|
|
|
## Task 3: Gateway stream adapters — risk + ml
|
|
|
|
**Files:**
|
|
- Modify: `services/api_gateway/src/grpc/risk_proxy.rs`
|
|
- Modify: `services/api_gateway/src/grpc/ml_proxy.rs`
|
|
|
|
**risk_proxy.rs** — add 2 methods:
|
|
- `stream_circuit_breaker_status` (default interval 2s, polls `get_circuit_breaker_status`)
|
|
- `stream_risk_metrics` (default interval 3s, polls `get_risk_metrics`)
|
|
- Import: `use std::time::Duration;`
|
|
|
|
**ml_proxy.rs** — add 1 method:
|
|
- `stream_model_status` (default interval 5s, polls `get_model_status`)
|
|
- Forward `model_name` from the stream request to each `GetModelStatusRequest`
|
|
- Import: `use std::time::Duration;`
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p api_gateway 2>&1 | grep "error\[" | head -10`
|
|
|
|
**Commit:** `feat(api_gateway): add risk + ml stream adapters`
|
|
|
|
---
|
|
|
|
## Task 4: Gateway stream adapters — trading + trading_agent
|
|
|
|
**Files:**
|
|
- Modify: `services/api_gateway/src/grpc/trading_direct_proxy.rs`
|
|
- Modify: `services/api_gateway/src/grpc/trading_agent_proxy.rs`
|
|
|
|
**trading_direct_proxy.rs** — add 2 methods:
|
|
- `stream_portfolio_summary` (default interval 3s, polls `get_portfolio_summary`)
|
|
- `stream_order_book` (default interval 1s, polls `get_order_book`, forwards `symbol` + `depth`)
|
|
- Import: `use std::time::Duration;`
|
|
|
|
**trading_agent_proxy.rs** — add 1 method:
|
|
- `stream_agent_status` (default interval 3s, polls `get_agent_status`)
|
|
- Import: `use std::time::Duration;`
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p api_gateway 2>&1 | grep "error\[" | head -10`
|
|
|
|
**Commit:** `feat(api_gateway): add trading + trading_agent stream adapters`
|
|
|
|
---
|
|
|
|
## Task 5: Implement monitoring handler streams
|
|
|
|
**Files:**
|
|
- Modify: `services/api_gateway/src/grpc/monitoring_handler.rs`
|
|
|
|
**What:** Replace 3 `Status::unimplemented` stubs with real implementations. Follow the existing `stream_training_metrics` pattern (clone internal state, `async_stream::stream!` loop).
|
|
|
|
**`stream_system_status`** (replaces stub at ~line 497):
|
|
- Clone `self.backends`, `self.start_time`
|
|
- In stream loop: fan-out health checks (same logic as `get_system_status`), yield `SystemStatusEvent` wrapping the `GetSystemStatusResponse` fields
|
|
- Default interval from request or 5s
|
|
|
|
**`stream_metrics`** (replaces stub at ~line 609):
|
|
- Clone `self.prom`
|
|
- In stream loop: query configured metric names from Prometheus, yield `MetricsEvent` wrapping metric list
|
|
- Default interval from request or 5s
|
|
- Use fixed metric list: `["cpu_usage", "memory_usage", "gpu_utilization"]`
|
|
|
|
**`stream_alerts`** (replaces stub at ~line 639):
|
|
- This is a placeholder — Prometheus has an alertmanager API but the handler doesn't currently scrape alerts
|
|
- Implement as a stream that yields an empty `AlertEvent` each interval (no-data is better than unimplemented)
|
|
- Default interval from request or 10s
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p api_gateway` — should compile with 0 errors now
|
|
|
|
**Commit:** `feat(api_gateway): implement monitoring stream RPCs`
|
|
|
|
---
|
|
|
|
## Task 6: Verify full gateway build
|
|
|
|
**Verify:**
|
|
```bash
|
|
SQLX_OFFLINE=true cargo check -p api_gateway
|
|
SQLX_OFFLINE=true cargo test -p api_gateway --lib
|
|
SQLX_OFFLINE=true cargo clippy -p api_gateway -- -D warnings
|
|
```
|
|
|
|
All must pass: 0 errors, 0 clippy warnings.
|
|
|
|
**Commit (if any fixups needed):** `fix(api_gateway): clippy and test fixes for stream adapters`
|
|
|
|
---
|
|
|
|
## Task 7: TUI state.rs — ConnectionStatus, empty defaults, new data types
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/state.rs`
|
|
|
|
**What:**
|
|
1. Add `ConnectionStatus` enum
|
|
2. Add missing data types needed by streams (`OrderData`, `VaRData`, `AlertData`, etc.)
|
|
3. Change `AppState::default()` to start with empty vecs (not mock data)
|
|
4. Add `connection_status: ConnectionStatus` field to `AppState`
|
|
|
|
**Add to top of state.rs:**
|
|
```rust
|
|
/// Connection state for the gRPC streaming layer.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ConnectionStatus {
|
|
Connecting,
|
|
Connected,
|
|
Reconnecting { attempt: u32 },
|
|
Error(String),
|
|
}
|
|
|
|
impl Default for ConnectionStatus {
|
|
fn default() -> Self { Self::Connecting }
|
|
}
|
|
```
|
|
|
|
**Replace** `impl Default for AppState` — all vecs become `Vec::new()`, account defaults to zeros, risk defaults to zeros. Add `connection_status: ConnectionStatus::default()`.
|
|
|
|
**Add** any new sub-struct types needed for streams not covered by existing types (e.g., `AlertData`, `VaRData` if not already present). Keep minimal — only fields the cockpits actually render.
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `refactor(fxt): empty TUI state defaults + ConnectionStatus`
|
|
|
|
---
|
|
|
|
## Task 8: Create TUI data_fetcher.rs
|
|
|
|
**Files:**
|
|
- Create: `bin/fxt/src/tui/data_fetcher.rs`
|
|
- Modify: `bin/fxt/src/tui/mod.rs` (add `pub mod data_fetcher;`)
|
|
|
|
**What:** Core streaming data layer. `StateUpdate` enum + `DataFetcher` struct.
|
|
|
|
**`StateUpdate` enum** — one variant per data source, carrying the parsed TUI-layer types:
|
|
```rust
|
|
pub enum StateUpdate {
|
|
// Trading cockpit
|
|
Positions(Vec<PositionData>),
|
|
Executions(Vec<ExecutionData>),
|
|
AccountState(AccountData),
|
|
PortfolioSummary(/* fields */),
|
|
SessionStatus(/* fields */),
|
|
// Training cockpit
|
|
TrainingSessions(Vec<TrainingSessionData>),
|
|
Gpu(GpuData),
|
|
// Services cockpit
|
|
Services(Vec<ServiceData>),
|
|
SystemResources(SystemResources),
|
|
ClusterEvents(Vec<ClusterEventData>),
|
|
// Risk cockpit
|
|
CircuitBreakers(Vec<CircuitBreakerData>),
|
|
PositionLimits(Vec<PositionLimitData>),
|
|
RiskMetrics(/* drawdown, VaR */),
|
|
// Data cockpit
|
|
DataFeeds(Vec<DataFeedData>),
|
|
DownloadJobs(Vec</* download job data */>),
|
|
// Meta
|
|
Connection(ConnectionStatus),
|
|
}
|
|
```
|
|
|
|
**`DataFetcher` struct:**
|
|
```rust
|
|
pub struct DataFetcher {
|
|
tx: mpsc::UnboundedSender<StateUpdate>,
|
|
cancel: tokio_util::sync::CancellationToken,
|
|
}
|
|
|
|
impl DataFetcher {
|
|
pub fn new(
|
|
client: &FoxhuntClient,
|
|
tx: mpsc::UnboundedSender<StateUpdate>,
|
|
) -> Self { ... }
|
|
|
|
/// Spawn all stream tasks. Returns immediately.
|
|
pub fn start(&self, client: &FoxhuntClient) { ... }
|
|
|
|
/// Cancel all streams gracefully.
|
|
pub fn stop(&self) { self.cancel.cancel(); }
|
|
}
|
|
```
|
|
|
|
**Each stream task** follows this pattern:
|
|
```rust
|
|
fn spawn_positions_stream(client: &FoxhuntClient, tx: UnboundedSender<StateUpdate>, cancel: CancellationToken) {
|
|
let mut trading = client.trading();
|
|
tokio::spawn(async move {
|
|
let mut backoff = Duration::from_secs(1);
|
|
loop {
|
|
match trading.stream_positions(Request::new(StreamPositionsRequest {})).await {
|
|
Ok(response) => {
|
|
backoff = Duration::from_secs(1); // reset on success
|
|
let mut stream = response.into_inner();
|
|
loop {
|
|
tokio::select! {
|
|
_ = cancel.cancelled() => return,
|
|
msg = stream.message() => {
|
|
match msg {
|
|
Ok(Some(event)) => {
|
|
let positions = convert_positions(event);
|
|
let _ = tx.send(StateUpdate::Positions(positions));
|
|
}
|
|
Ok(None) => break, // stream ended, reconnect
|
|
Err(e) => {
|
|
let _ = tx.send(StateUpdate::Connection(
|
|
ConnectionStatus::Error(format!("positions: {e}"))
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
let _ = tx.send(StateUpdate::Connection(
|
|
ConnectionStatus::Reconnecting { attempt: backoff.as_secs() as u32 }
|
|
));
|
|
}
|
|
}
|
|
// Exponential backoff before reconnect
|
|
tokio::select! {
|
|
_ = cancel.cancelled() => return,
|
|
_ = tokio::time::sleep(backoff) => {}
|
|
}
|
|
backoff = (backoff * 2).min(Duration::from_secs(30));
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
**Conversion functions** — map proto types to TUI state types. One per stream. Keep simple: extract fields, convert enums to strings, format numbers.
|
|
|
|
**Spawn order in `start()`:** monitoring first (service health), then trading, risk, training, data, agent — so the user sees connection status early.
|
|
|
|
**Dependencies:** Add `tokio-util` (for `CancellationToken`) to `bin/fxt/Cargo.toml` if not already present.
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): add TUI DataFetcher with streaming gRPC clients`
|
|
|
|
---
|
|
|
|
## Task 9: Wire event_loop.rs + watch.rs
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/event_loop.rs`
|
|
- Modify: `bin/fxt/src/commands/watch.rs`
|
|
|
|
**event_loop.rs changes:**
|
|
|
|
1. Change `run` signature:
|
|
```rust
|
|
pub async fn run(api_url: &str) -> anyhow::Result<()> {
|
|
```
|
|
(remove the `_` prefix from `api_url`)
|
|
|
|
2. Inside `run()`, before `event_loop()`:
|
|
```rust
|
|
// Connect to API Gateway
|
|
let client = FoxhuntClient::connect(api_url).await?;
|
|
|
|
// Auto-refresh auth token
|
|
let storage = FileTokenStorage::new()?;
|
|
let manager = AuthTokenManager::new(storage);
|
|
if manager.needs_refresh().await {
|
|
let login_client = LoginClient::new(client.channel());
|
|
if login_client.silent_login(&manager).await? {
|
|
eprintln!("Token refreshed.");
|
|
}
|
|
}
|
|
|
|
// Start streaming data layer
|
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let fetcher = DataFetcher::new(&client, tx);
|
|
fetcher.start(&client);
|
|
```
|
|
|
|
3. Pass `rx` to `event_loop()`.
|
|
|
|
4. Inside `event_loop()`, at the start of each loop iteration, drain the channel:
|
|
```rust
|
|
// Drain all pending state updates
|
|
while let Ok(update) = rx.try_recv() {
|
|
apply_update(&mut state, update);
|
|
}
|
|
```
|
|
|
|
5. Add `apply_update` function that matches on `StateUpdate` variants and updates `AppState` fields.
|
|
|
|
6. On `'r'` keypress (refresh), cancel and restart the fetcher.
|
|
|
|
**watch.rs** — no changes needed (already passes `api_url` to `run()`).
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): wire DataFetcher into TUI event loop`
|
|
|
|
---
|
|
|
|
## Task 10: Wire Overview + Training cockpits
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/cockpits/overview.rs`
|
|
- Modify: `bin/fxt/src/tui/cockpits/training.rs`
|
|
|
|
**What:** Handle empty/connecting state in rendering. When data hasn't arrived yet, show "Connecting..." instead of empty tables.
|
|
|
|
**Overview cockpit changes:**
|
|
- `render_services`: If `state.services.is_empty()`, show centered "Connecting..."
|
|
- `render_resources`: If all zero, show "Waiting for data..."
|
|
- `render_training_summary`: If `state.training_sessions.is_empty()`, show "No active training"
|
|
- `render_portfolio`: If account equity is 0.0, show "Connecting..."
|
|
|
|
**Training cockpit changes:**
|
|
- `render_sessions_table`: If `state.training_sessions.is_empty()`, show "No active training sessions"
|
|
- `render_gpu_panel`: If `state.gpu.name == "N/A"`, show "No GPU data"
|
|
- `render_epoch_metrics`: Similar empty-state handling
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): wire Overview + Training cockpits to live data`
|
|
|
|
---
|
|
|
|
## Task 11: Wire Trading cockpit
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/cockpits/trading.rs`
|
|
|
|
**What:** Handle empty state for all 4 panels (positions, executions, account, broker health).
|
|
|
|
- `render_positions`: Empty → "No open positions"
|
|
- `render_executions`: Empty → "No recent executions"
|
|
- `render_account`: All zeros → "Connecting..."
|
|
- `render_broker_health`: Currently hardcoded. Wire to `state.session_status` or similar field. If no data → "Connecting..."
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): wire Trading cockpit to live data`
|
|
|
|
---
|
|
|
|
## Task 12: Wire Services + Risk cockpits
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/cockpits/services.rs`
|
|
- Modify: `bin/fxt/src/tui/cockpits/risk.rs`
|
|
|
|
**Services cockpit:**
|
|
- `render_service_grid`: Empty → "Connecting..."
|
|
- `render_cluster_events`: Empty → "No events"
|
|
- `render_cluster_resources`: All zeros → "Waiting for metrics..."
|
|
|
|
**Risk cockpit:**
|
|
- `render_kill_switches`: Handle default false values (already works)
|
|
- `render_drawdown`: All zeros → "No drawdown data"
|
|
- `render_circuit_breakers`: Empty → "No circuit breakers"
|
|
- `render_position_limits`: Empty → "No position limits"
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): wire Services + Risk cockpits to live data`
|
|
|
|
---
|
|
|
|
## Task 13: Wire Data cockpit
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/cockpits/data.rs`
|
|
|
|
**What:**
|
|
- `render_feeds`: If `state.data_feeds.is_empty()`, show "No data feeds" (market data stream may not be active if no symbols subscribed)
|
|
- `render_cache_stats`: All zeros → "No cache data"
|
|
- `render_pipeline`: Wire to download job status from `DataFetcher`. Show actual job states (PENDING, DOWNLOADING, etc.) instead of hardcoded "IDLE"/"RUNNING"
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): wire Data cockpit to live data`
|
|
|
|
---
|
|
|
|
## Task 14: Add connection status to status bar
|
|
|
|
**Files:**
|
|
- Modify: `bin/fxt/src/tui/event_loop.rs`
|
|
|
|
**What:** Update `render_status_bar` to show connection status from `state.connection_status`:
|
|
- `Connecting` → yellow "Connecting..."
|
|
- `Connected` → green "Connected"
|
|
- `Reconnecting { attempt }` → yellow "Reconnecting (attempt N)..."
|
|
- `Error(msg)` → red "Error: msg"
|
|
|
|
Place between the keymap hints and the "Updated: X ago" text.
|
|
|
|
**Verify:** `SQLX_OFFLINE=true cargo check -p fxt`
|
|
|
|
**Commit:** `feat(fxt): show connection status in TUI status bar`
|
|
|
|
---
|
|
|
|
## Task 15: Full verification — tests + clippy
|
|
|
|
**Verify:**
|
|
```bash
|
|
SQLX_OFFLINE=true cargo test -p fxt --lib
|
|
SQLX_OFFLINE=true cargo test -p api_gateway --lib
|
|
SQLX_OFFLINE=true cargo clippy -p fxt -p api_gateway -- -D warnings
|
|
```
|
|
|
|
Fix any issues. All must pass with 0 errors, 0 warnings.
|
|
|
|
**Commit:** `fix(fxt): test and clippy fixes for TUI live data`
|
|
|
|
---
|
|
|
|
## Task 16: Build + deploy api_gateway
|
|
|
|
**Steps:**
|
|
1. `SQLX_OFFLINE=true cargo build --release -p api_gateway`
|
|
2. Deploy using pod-writer-deploy skill (see `/.claude/skills/pod-writer-deploy/`)
|
|
3. Verify with: `fxt service health`
|
|
|
|
**Commit:** none (deployment only)
|
|
|
|
---
|
|
|
|
## Task 17: Build fxt + test in tmux
|
|
|
|
**Steps:**
|
|
1. `SQLX_OFFLINE=true cargo build --release -p fxt`
|
|
2. `cp target/release/fxt ~/.cargo/bin/fxt`
|
|
3. `fxt auth login --username admin` (or use FXT_PASSWORD env var)
|
|
4. Launch: `fxt watch --api-url https://api.fxhnt.ai`
|
|
5. Test all 6 views (keys 1-6):
|
|
- Overview: Should show real service health, real portfolio if authenticated
|
|
- Training: Real training sessions if any running, real GPU data
|
|
- Trading: Real positions from IBKR/broker-gateway, real executions
|
|
- Services: Real health check results from all backends
|
|
- Risk: Real circuit breaker status, VaR data if risk service running
|
|
- Data: Real download job status, market data feed status
|
|
6. Test reconnection: temporarily disconnect, verify TUI shows "Reconnecting..."
|
|
7. Test help overlay (?), quit (q)
|
|
|
|
**Verification checklist:**
|
|
- [ ] All 6 views render without panic
|
|
- [ ] Connected status shows in status bar
|
|
- [ ] Data updates in real-time (positions change, metrics refresh)
|
|
- [ ] Empty/connecting state shown gracefully when backend data unavailable
|
|
- [ ] 'r' key forces reconnection
|
|
- [ ] Quit works cleanly (no terminal corruption)
|