Core Contract
A TUI screen never waits directly on tenant IO inside a draw path or key handler.
It records intent, stores enough identity to recognize the result later, spawns
an async task, and applies the result through the central event loop.
This keeps the interface usable during long operations such as ESV restarts,
which can take ten minutes or more, and it lets the dashboard show durable state
from the last authoritative response while new checks are still pending.
Request Lifecycle
1. Intent
A keybind, timer, watcher, or dashboard action asks for work.
2. Guard
The screen validates selection, tenant, dirty state, production confirmation,
and duplicate in-flight work.
3. Local State
The screen records pending state without clearing the last useful display.
4. Spawn
A task receives cloned inputs, request identity, and a channel sender.
5. Tenant IO
The task uses the shared AIC request path to talk to the tenant.
6. Event
The task sends an AppEvent with data, error, and identity.
7. Reduce
The screen drops stale results, then updates only the state owned by that result.
8. Surface
Panels, banners, and dashboard alerts render from stored domain state.
Important Behavior
Starting a refresh must not blank the old state. The screen should continue
showing NoChanges, Unapplied, or Restarting
until the tenant returns a newer authoritative answer.
Current Event Loop Shape
The app has a single reducer-style path for terminal events and background task
completions. Key handlers and async completions both become events, and screen
modules own the domain-specific state transitions.
App::run
draw current state
receive Event
App::handle_event
key event -> App::handle_key -> screen handler
app event -> screen completion handler
tick -> periodic refresh decisions
Task Identity
Any result that can arrive after the user switches tenant, leaves a panel, or
starts a newer request needs identity. The reducer uses that identity to decide
whether the result still belongs to the current screen state.
TaskIdentity {
task_id: u64,
tenant_name: String,
resource_key: String,
generation: u64,
started_at: Instant
}
Domain State Pattern
cache
Last useful data to render immediately when the user returns to a panel.
A refresh can be pending without replacing this with a spinner.
authoritative state
Last result confirmed by the tenant, such as ESV startup status or a remote
script revision.
in-flight map
Work keyed by tenant and resource. This prevents duplicate saves, repeated
restarts, and overlapping sync checks for the same resource.
generation
Incremented when the meaning of a result changes, for example when switching
tenants, changing filters, or opening a newer sync session.
alerts
Durable issues derived from domain state. Alerts are not transient toasts; they
remain visible until a newer authoritative result resolves them or the user
performs an explicit action.
ESV State Example
The ESV screen keeps the last known apply state per tenant. Returning to the panel
should immediately show the stored state and then refresh it in the background.
| State |
Meaning |
UI behavior |
NoChanges |
The tenant reports no unapplied ESV changes. |
Hide or disable restart/apply actions. |
Unapplied |
The tenant reports changes that need restart/apply. |
Show the restart/apply keybind when no conflicting write is in flight. |
Restarting |
The tenant reports startup work is running. |
Show restart progress state; do not enable another restart. |
Partial Refresh
ESV list data and startup status are separate facts. If one request fails and the
other succeeds, the reducer should update only the successful side and preserve
the last authoritative value for the failed side.
Conflict Alerts
A future script sync should treat local and cloud edits as separate sources over a
remembered base revision. If both changed independently, the sync task should
produce a persistent dashboard alert instead of overwriting either side.
base revision
local changed? yes
cloud changed? yes
same content? no
result conflict alert
Alert Model
Alerts should be stored as application state, not inferred only during rendering.
That lets the dashboard, status bar, and owning panel agree on what needs
attention.
Alert {
id: AlertId,
severity: Info | Warning | Error,
scope: Tenant | ESV | Script | Logs,
title: String,
detail: String,
actions: Vec<ActionId>,
source_generation: u64
}
Future Background Work
| Feature |
Async state |
Dashboard signal |
| Log fetch |
Cursor, filter generation, last page, in-flight fetch, last error. |
Fetch failed, new matching errors, paused stream, stale cursor. |
| Script sync |
Base revision, local hash, cloud revision, sync generation, conflict set. |
Local-only change, cloud-only change, conflict, failed push or pull. |
| ESV restart |
Last startup status, restart request time, refresh in flight. |
Unapplied changes, restarting, restart failed, status unknown. |
| Tenant health |
Last check, in-flight probe, degraded components. |
Auth failure, tenant unavailable, rate limiting, stale health data. |
Stale Result Rules
- Every long-lived task carries enough identity to prove where it belongs.
- Switching tenant or resource increments the relevant generation.
- A result with an old generation is ignored except for optional debug logging.
- Request start never clears cached authoritative state.
- Errors update error fields or alerts; they do not silently reset domain state.
Cancellation
Cancellation is an optimization, not the correctness mechanism. If a task cannot
be cancelled because it is waiting on the tenant, stale-result checks still protect
the UI when it eventually returns.
For expensive polling features, keep a task handle per domain and abort it when
the owning panel or filter generation is replaced.
Add-a-Feature Checklist
- Define the domain state machine before adding UI controls.
- Store last authoritative state separately from in-flight request state.
- Key in-flight work by tenant and resource, not by screen alone.
- Attach task identity or generation to every completion event that can arrive late.
- Apply partial successes without clearing unrelated cached data.
- Convert durable problems into alerts with action IDs.
- Make the dashboard render from the same alert store used by the owning panel.
- Add tests for stale result drops, duplicate request guards, and conflict handling.