pingone-aic-manager TUI design note

TUI Async Request Design

The TUI should stay responsive while tenant requests, log fetches, script syncs, restarts, and alert checks run in the background. The rule is simple: key handlers update local state and start work; only completion events apply authoritative tenant results.

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

  1. Every long-lived task carries enough identity to prove where it belongs.
  2. Switching tenant or resource increments the relevant generation.
  3. A result with an old generation is ignored except for optional debug logging.
  4. Request start never clears cached authoritative state.
  5. 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.