Skip to content

feat(diagnostics): doctor next steps as data, acting chips, consequence-first status copy - #2240

Draft
thinmintdev wants to merge 4 commits into
mainfrom
feat/doctor-next-steps
Draft

thinmintdev wants to merge 4 commits into
mainfrom
feat/doctor-next-steps

Conversation

@thinmintdev

Copy link
Copy Markdown
Contributor

Summary

health_report.py's 7 hal0 doctor verify classifiers (API, mDNS,
runners, capability slots, memory engine, OpenWebUI, Hermes) hard-coded
next_steps=[] on every row (src/hal0/health_report.py:282 before this
PR) even though the typed Diagnosis.next_steps /
NextStep(kind="command"|"manual"|"doc") shape already existed
(src/hal0/diagnostics.py:41-56) and ui/src/dash/diagnostics/DiagnosisPanel.jsx:31-44
already rendered chips for it — so GET /api/doctor always answered with
an empty list and a remediation command was reachable only via a title=
tooltip. This PR:

  1. Populates a real NextStep for every fail/warn branch each classifier
    can reach — the exact command an operator would run (hal0 serve,
    hal0 slot restart <name>, hal0 slot create, or systemctl restart hal0-api|hindsight-api|hal0-openwebui|hal0-agent@hermes), a doc link,
    or a manual instruction where no single command exists — built at the
    same point each classifier already produces its human-readable detail
    string, so message and remedy can't drift apart.
  2. Makes DiagnosisPanel.jsx's chips act: command chips show the
    command inline in mono with Copy, plus Run when it maps onto an
    existing typed mutation (useServiceRepair — the same one-click repair
    RestartApiPanel.jsx uses, restricted server-side to
    installer.py's _REPAIRABLE_UNITS — or useSlotRestart); doc
    chips open the link; manual chips expand.
  3. Adds a reusable StepsDrawer (numbered steps, same affordances) that
    opens for any diagnosis with ≥2 next steps — generic enough to reuse
    later for extension enable steps.
  4. Adds status-copy.ts: a one-sentence, consequence-first "what this
    means for you" line for every slot lifecycle state
    (offline/pulling/starting/warming/ready/serving/idle/
    unloading/error) and companion-service health word
    (up/stopped/down), shown as a tooltip beside the existing precise
    vocabulary in the slot drawer, the slot card's status dot, and the
    Services card — the precise words themselves are unchanged.

The auth-posture check's remedy is intentionally not included — it
depends on teammate w0a's check landing in the shared classifier first;
noted as a follow-up, not stubbed in code.

Risk grade

  • med — new code path (real remediation data + acting UI), no schema
    break (NextStep's 3-field shape is unchanged, existing pinned
    tests confirm it)

Touched surfaces

  • API (src/hal0/api/routes/doctor.py unchanged — consumes
    to_diagnosis() verbatim; src/hal0/health_report.py is the
    classifier module it composes)
  • UI (ui/src/, Playwright specs)
  • Docs (docs/)

§14.1 high-risk surfaces

None apply — no new routes, no AUTONOMOUS_WRITE_TOOLS changes, no
installer/updater shell-out.

Rollback

Revert this PR's 4 commits; Check.next_steps defaults to an empty tuple
and to_diagnosis() reverting to next_steps=[] restores prior behavior
exactly (no data migration, no persisted state).

Test tiers run

  • α unit — uv run pytest tests/ -q -n 8: 12787 passed, 21 skipped, 1
    xfailed, 3 failed — the same 3 pre-existing failures this dev box
    always has (test_moonshine_server.py needs ffmpeg the box lacks;
    test_pressure_eviction.py::test_pressure_evict_noop_when_probe_fails
    is flaky on this box per the parity program's baseline notes), none
    touched by this diff.
    uv run ruff check/format, uv run mypy, python3 scripts/check_sunset.py all clean.
  • β integration — not run (no local Lemonade/systemd harness available
    in this worktree; nothing in this diff touches the integration
    surface — pure classifier logic + UI).
  • UI: npm run lint && npm run typecheck && npm run test:unit && npm run build all green (497 unit tests). A new Playwright spec
    (diagnostics-next-steps-v3.spec.ts) covers copy-to-clipboard,
    Run, doc-open, and the StepsDrawer against a mocked /api/doctor
    feed, but could not be executed on this dev box
    chromium_headless_shell fails to launch here
    (libnspr4.so: cannot open shared object file, no sudo available to
    install it); the two pre-existing diagnostics Playwright specs fail
    identically unmodified, confirming a host-level gap rather than a
    regression in this change. CI has full browser deps and should
    confirm.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbAfiRnPCuPwn3E7YMMgan

thinmintdev and others added 4 commits September 5, 2026 12:50
health_report.py's to_diagnosis() hard-coded next_steps=[] for every one of
the 7 hal0-doctor-verify checks (API, mDNS, runners, capability slots,
memory engine, OpenWebUI, Hermes), even though the typed
Diagnosis.next_steps / NextStep(kind="command"|"manual"|"doc") shape
already existed and DiagnosisPanel.jsx already rendered chips for it — so
GET /api/doctor and `hal0 doctor verify --json` always answered with an
empty list, and a command's remediation text was reachable only via a
tooltip.

Extend Check with a next_steps field and populate it at the same point each
classifier already builds its human-readable detail string, so the exact
sub-state that produced the detail also produces the matching remedy (one
owner per fact, no drift between what the message says and what the step
recommends). Every fail/warn branch now carries at least one NextStep: a
literal command (`hal0 serve`, `hal0 slot restart <name>`, `hal0 slot
create`, or a `systemctl restart <unit>` for the four repairable companion
services), a doc link, or a manual instruction where no single command
applies. The auth-posture check's remedy is intentionally left out here —
noted in status, not stubbed in code — since it depends on w0a's check
landing in the shared classifier first.

to_diagnosis() now threads Check.next_steps through instead of discarding
it; GET /api/doctor needed no changes since it already calls to_diagnosis()
verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbAfiRnPCuPwn3E7YMMgan
Signed-off-by: thinmintdev <alexander@awideweb.com>
DiagnosisPanel.jsx's next-step chips showed a command only in a `title=`
tooltip and offered no interaction. Every chip now acts on its own kind:
`command` shows the exact command inline in mono with a Copy button
(clipboard.ts, real success/failure toast), plus a Run button when the
command maps onto an existing typed mutation; `doc` opens the target (an
external hal0.dev link or an in-app hash route); `manual` expands the full
instruction inline instead of sitting inert.

next-step-actions.ts is the pure command-string -> action mapping ("Run"
support without inventing new endpoints): the four `systemctl restart
<unit>` commands health_report.py emits map to useServiceRepair (the same
one-click repair RestartApiPanel.jsx already uses, restricted server-side
to installer.py's _REPAIRABLE_UNITS), and `hal0 slot restart <name>` maps
to useSlotRestart (the same mutation the slot card's own Restart button
uses). An unmapped command still copies fine, it just has no Run button —
deliberately not string-sniffing every possible command.

StepsDrawer.jsx is a reusable numbered-steps drawer (eyebrow/title/numbered
rows, same command/doc/manual affordances) that DiagnosisPanel opens via a
new "Steps (N)" button whenever a diagnosis carries two or more next steps;
built generic enough to reuse as-is for a future extension-enable-steps
surface.

Tests: next-step-actions.test.ts (action mapping + doc-target resolution),
a new Playwright spec covering the copy/Run/doc-open/StepsDrawer paths
against a mocked /api/doctor feed. The Playwright spec could not be run on
this dev box — chromium_headless_shell fails to launch here
(libnspr4.so missing, no sudo to install it) and the two pre-existing
diagnostics specs fail identically unmodified, confirming a host gap, not
a regression; CI has the full browser deps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbAfiRnPCuPwn3E7YMMgan
Signed-off-by: thinmintdev <alexander@awideweb.com>
Slot lifecycle words (offline, pulling, starting, warming, ready, serving,
idle, unloading, error) and companion-service health words (up, stopped,
down) are precise but internal — accurate for an operator who already
knows the state machine, opaque to one who doesn't.

status-copy.ts is the single owner of a one-sentence "what this means for
you" line per word, deliberately kept separate from the vocabulary itself:
callers keep rendering the precise word unchanged and add this sentence
alongside it as a tooltip. Wired into the slot edit drawer's state chip,
the slot card's status dot (appended after the existing precise-phase
tooltip — window.slotIndicator's return value stays byte-for-byte pinned,
only the rendered DOM title gains the extra sentence), and the Services
card's status pill.

tests/ui_contracts/test_status_copy_mirror.py mirrors
tests/ui_contracts/test_flag_aliases_mirror.py's approach: it parses the
TS object literal out of status-copy.ts and asserts its keys equal
hal0.slots.state.SlotState's members exactly, so a new lifecycle state
can't ship without a status line. The service-health words (up/stopped/
down) have no backing Python enum — they're literals in
services_health.py — so status-copy.test.ts's completeness check is their
whole contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbAfiRnPCuPwn3E7YMMgan
Signed-off-by: thinmintdev <alexander@awideweb.com>
Documents the Doctor panel's new acting next-step chips and Steps drawer in
docs/guides/dashboard-and-settings.mdx, and adds the CHANGELOG [Unreleased]
### Added entry for the whole feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbAfiRnPCuPwn3E7YMMgan
Signed-off-by: thinmintdev <alexander@awideweb.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant