Skip to content

feat: local web control panel (openadapt panel)#1008

Open
tamiso999 wants to merge 12 commits into
OpenAdaptAI:mainfrom
tamiso999:feature/control-panel-blueprint
Open

feat: local web control panel (openadapt panel)#1008
tamiso999 wants to merge 12 commits into
OpenAdaptAI:mainfrom
tamiso999:feature/control-panel-blueprint

Conversation

@tamiso999

Copy link
Copy Markdown

Summary

Adds a local web control panel for OpenAdapt — a browser UI over the
record → train → evaluate CLI, so the pipeline is usable without the terminal.
Implements the blueprint (docs/control-panel-blueprint.md) end to end (P0–P3).

New in-repo sibling openadapt-panel (FastAPI backend + React/Vite SPA),
wired via a new lazy-imported openadapt panel command and a panel extra.

What's included

  • openadapt panel command (lazy import; find_spec distinguishes
    "not installed" from "installed but broken" so wiring bugs surface — openadapt-ml 0.14.1: multiple broken internal imports prevent serve, train, and demo commands #999
    discipline). Flags: --app (tray app), --install-shortcut (Desktop icon).
  • Six pages: Dashboard/System (read-only, importlib only — never imports a
    sibling), Captures, Train, Eval, Models, Settings.
  • Seam, not a reimplementation: routes call the same sibling functions the
    CLI calls; long-running capture/train run as openadapt CLI subprocesses
    (train stop = STOP_TRAINING sentinel; capture stop = interrupt signal),
    eval runs in a worker thread returning structured metrics. Background jobs
    stream logs over SSE.
  • Loopback + per-session token on every /api route.
  • React (Vite) frontend under openadapt-panel/frontend/; builds into
    openadapt_panel/static/ (committed + wheel-bundled, so pip/CI/end-users
    never need Node — verified the wheel ships static/assets/*).
  • Desktop app: --install-shortcut writes a Windows Desktop .lnk that
    launches a tray-resident app via pythonw (no console), pure Python
    (pystray/pillow) — no Rust/Node toolchain.

Fixes found while testing the eval flow (against openadapt-evals 0.87.2)

  • WAALiveAdapter(server_url=...)WAALiveAdapter(config=WAALiveConfig(server_url=...))
    in both the CLI and the panel (the old call TypeErrors on current evals).
  • metrics['total_tasks'] → tolerate num_tasks (KeyError at the end of
    openadapt eval run).

Tests

tests/test_panel_seams.py (runs under the existing pytest tests/): behavior
tests (auth, routes, settings round-trip, 503-when-absent, desktop helpers) run
everywhere; seam contracts against sibling signatures skip locally and run in CI
(which now installs ./openadapt-panel[dev]). Also added an eval metrics-keys
test that would have caught the total_tasks bug. Locally: 20 passed / 7
skipped, ruff + format clean.

Prerequisites handled

  • Fixed the root .gitignore bare src/src/ (it was silently dropping any
    src/ dir repo-wide, incl. the panel frontend).

Not verified headless (need a machine with siblings + GPU/display)

Live capture (screenshots at import), real training, and live WAA eval. The
mock-eval flow was verified end-to-end through the panel.

🤖 Generated with Claude Code

tamiso999 and others added 8 commits July 10, 2026 08:59
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A bare `src` ignored any src/ dir repo-wide, which silently dropped the
Miso React UI from a commit and would drop the panel frontend. Scope to
/src/ (the editable-install location) only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scaffold the control panel (blueprint P0): a local FastAPI web UI over the
CLI, shipped as the in-repo sibling `openadapt-panel` and wired via a new
`openadapt panel` command.

- openadapt-panel/: FastAPI app factory, run_panel launcher (loopback only),
  and a read-only Dashboard/System page. system.py mirrors doctor — it inspects
  ecosystem state via importlib.metadata/find_spec and imports no sibling, so
  the app boots headless with zero siblings installed.
- cli.py: `openadapt panel` lazy-imports the sibling; uses find_spec to tell
  "not installed" from "installed but broken" so a phantom symbol surfaces
  instead of being masked (the OpenAdaptAI#999 bug class).
- pyproject.toml: new `panel` extra, added to the `all` bundle.
- tests/test_cli_smoke.py: seam contracts — panel calls run_panel with kwargs
  that exist in its signature, and system_report imports no sibling packages.
- CI installs ./openadapt-panel from the local path so those seam tests run
  rather than skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ettings)

Turns the P0 shell into a usable panel: FastAPI backend + vanilla-JS SPA with
six pages, background jobs, SSE log streaming, and per-session token auth.

Backend (openadapt-panel), all sibling imports lazy inside handlers:
- routes/captures: list (openadapt_capture.Capture), viewer (create_html),
  record (CLI subprocess job).
- routes/train: start (CLI subprocess), status (training_log.json), stop
  (STOP_TRAINING sentinel) — same seams as `openadapt train`.
- routes/evals: mock + live/checkpoint runs (openadapt_evals) in worker-thread
  jobs returning structured metrics.
- routes/models: browse checkpoints on disk. routes/settings: view/edit a
  curated config subset, persisted to .env (secrets never echoed back).
- jobs.py: JobManager for subprocess + thread jobs; stop via interrupt signal
  (capture) or sentinel (train). auth.py: loopback token dependency.
- deps.ensure_installed: find_spec-based 503 for "not installed" that still lets
  an installed-but-broken symbol surface as a real error (OpenAdaptAI#999 discipline).

Frontend: SPA (index.html/app.js/styles.css) served from the wheel; reads the
launch-URL token, streams job logs over SSE.

Tests (tests/test_panel_seams.py): behavior tests (auth, routes, settings
round-trip, 503-when-absent) run everywhere; seam contracts against sibling
signatures skip locally, run in CI. CI installs ./openadapt-panel[dev].

Verified headless: full test suite green, ruff clean, live uvicorn smoke test
(auth 401/200, static assets, 503 path). Live capture/train/eval need a machine
with the siblings + GPU/display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frontend is now a React app under openadapt-panel/frontend/ (six pages +
shared JobLog SSE component + api client), unchanged in behavior. Vite builds
into openadapt_panel/static/ (base "/static/", matching the FastAPI mount); the
built index.html + assets/ are committed and bundled into the wheel, so pip
installs / CI / end-users never need Node.

- frontend/: package.json, vite.config.js (outDir ../openadapt_panel/static,
  emptyOutDir, base /static/), src/{main,App,api}.jsx + pages/ + components/.
- static/: hand-written app.js/styles.css removed; Vite output (index.html +
  assets/index-*.{js,css}) committed in their place.
- node_modules/ and dist/ gitignored via frontend/.gitignore (root /src/ fix
  already keeps frontend/src/ tracked).

Verified: npm run build clean (39 modules), wheel contains static/assets/*,
live uvicorn serves index + both hashed assets (200) with auth intact, and the
Python suite stays green (15 passed / 9 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… panel

Testing the eval flow against the installed openadapt-evals (0.87.2) exposed two
real seam bugs, in both the meta-package CLI and the panel that mirrors it:

- WAALiveAdapter no longer takes server_url directly; the URL lives on
  WAALiveConfig. `openadapt eval run --server ...` (and the panel's live eval)
  called WAALiveAdapter(server_url=...) → TypeError. Now
  WAALiveAdapter(config=WAALiveConfig(server_url=...)).
- compute_metrics reports the task count as 'num_tasks'; cli.py's eval run read
  metrics['total_tasks'] → KeyError at the end of a live run. Now tolerant of
  either key; the panel UI shows num_tasks (falling back to total_tasks).

Tests (tests/test_panel_seams.py): the eval seam contract now encodes the real
API (WAALiveAdapter takes config; WAALiveConfig takes server_url), and a new
test runs a tiny real mock eval and asserts the metric keys the UI/CLI read —
which is what would have caught the total_tasks KeyError.

Verified end-to-end: live mock-eval flow through the panel (POST /api/eval/mock
→ thread job → SSE log stream → real metrics) succeeds; suite 18 passed /
7 skipped (eval seam + metrics-keys tests now run, having installed
openadapt-evals locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running eval/train from the repo root writes benchmark_results/,
benchmark_live.json, and training_output/ into the tree; ignore them so they
aren't accidentally committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the panel feel like a clickable desktop app without any native toolchain
(no Rust/Node) — pure Python via pystray/pillow.

- openadapt_panel/desktop.py:
  - run_app(): serve in a background thread, open the browser, and sit in the
    system tray with Open/Quit. Falls back to the plain server if tray deps are
    missing.
  - install_desktop_shortcut(): create a Desktop .lnk that runs the tray app via
    pythonw (no console window), with a generated .ico. Windows-only for now;
    raises NotImplementedError elsewhere with guidance.
  - render_icon(): simple generated app/tray icon (no binary asset in git).
- cli.py: `openadapt panel --app` (tray mode) and `--install-shortcut`. Default
  `panel` behavior (and the run_panel seam) unchanged.
- pyproject: pystray + pillow added to openadapt-panel deps.
- tests: desktop helpers exposed + render_icon; README documents the flow.

Verified on Windows: --install-shortcut writes ~/Desktop/OpenAdapt Panel.lnk
(Target=pythonw, Args=`panel --app`, custom icon); launching exactly that
command serves (health 200) and stays resident.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tamiso999 tamiso999 requested a review from abrichr as a code owner July 13, 2026 08:13
tamiso999 and others added 4 commits July 13, 2026 08:23
New Frontend CI workflow (path-filtered to openadapt-panel/frontend/** and the
built static/): sets up Node, npm ci + npm run build, then fails if the
committed openadapt_panel/static/ differs from a fresh build — catching edits to
frontend/src/ that weren't rebuilt and committed. Keeps the Python CI Node-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Double-clicking the Desktop shortcut did nothing: it runs via pythonw, where
sys.stdout/stderr are None, and uvicorn's logging setup calls
sys.stdout.isatty() → AttributeError at Config construction, killing the process
before the server or tray ever started. (Earlier tests missed it by redirecting
stdout to a file, which makes it non-None.)

- Point missing std streams at ~/.openadapt/panel-console.log so uvicorn (and
  print/pystray) are safe under pythonw.
- Log startup + errors to ~/.openadapt/panel.log so failures aren't invisible.
- Write the launch URL (with token) to ~/.openadapt/panel-url.txt — under pythonw
  the token is otherwise never shown, so a blocked auto-open left no way in.
- Open the browser only after polling the server is ready (fixes the race that
  opened the browser to a "can't reach this page" error), and open more robustly
  (webbrowser then os.startfile). Best-effort tray startup notification.

Verified via pythonw exactly as the shortcut runs: health 200, resident, log +
url file written.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openadapt-capture's recorder module imports matplotlib (via its plotting
module) but doesn't declare it, so on a fresh install `from openadapt_capture
import Recorder` yields Recorder = None (the package guards the import). The CLI
then called Recorder(...) -> "NoneType is not callable", and the panel's record
button failed the same way. Root cause of "I cannot record".

- capture start: if Recorder is None, print an actionable error (install
  matplotlib) instead of a cryptic TypeError.
- pyproject: add matplotlib to the `capture` extra so `pip install
  openadapt[capture]` pulls it and recording works out of the box.

Diagnosed by installing openadapt-capture and tracing the guarded import to
ModuleNotFoundError: matplotlib; after installing it, Recorder loads and a
recording DB is produced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Settings page wrote keys to .env, but agents read them via os.getenv
(e.g. openadapt_evals.ApiAgent reads ANTHROPIC_API_KEY) — so a saved key never
reached them and live eval failed with "ANTHROPIC_API_KEY is required".

- settings.load_env_file(): load .env into os.environ (without overriding
  already-set vars); called at create_app startup.
- settings PUT: also set os.environ for updated keys, so a freshly saved key
  works immediately without restarting the panel.

Verified: PUT a key -> visible in os.environ; reload from .env after clearing
also restores it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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