The local FastAPI operations dashboard — a workflow runner with per-feature scope, persisted run history, workflow chaining, and live SSE log streaming
Failure modes
| Symptom | Cause | Fix | Severity |
|---|---|---|---|
RuntimeWarning: coroutine 'RunnerService.start' was never awaited |
start() called without await |
It's async — await it / asyncio.run |
high |
| Workflows won't run from the UI | Config.allow_run is False (e.g. --read-only) |
Start without --read-only |
high |
RunnerBusyError on start |
A run is already active (one at a time) | Wait for / inspect runner.current |
medium |
TypeError calling config.runs_dir() |
runs_dir is a property |
Drop the () |
medium |
| Cost panel shows an error | fetch_summary returned a CostFetchError (no admin key, network) |
Inspect the error kind/message; cost is an adjacent surface |
low |
Risk areas
RunnerService.startandRun.subscribeare async. They are the two awaitable surfaces; everything else on the runner is sync.allow_runis a real gate. Off by default inConfig; the CLI turns it on unless--read-only.- Scope confusion. Cost/telemetry/help shown on the dashboard are adjacent surfaces — owned by other features, only read here.
Diagnosis order
- Confirm the app builds:
create_app(build_config(Path("."))). - Confirm runs are allowed:
config.allow_run(and no--read-only). - For a run that won't start: check
runner.current/RunnerBusyError. - For async warnings:
awaitstart()/ iteratesubscribe(). - For a data panel: that data is owned by its feature (telemetry / help-system / cost).