Skip to content

ExLab-Wizard remaining work: T1–T13 (README generator, session-control epic, settings UI, cleanups) - #16

Merged
Xander-git merged 15 commits into
fix/debug-with-playwrightfrom
claude/nice-gauss-ZBnL6
May 29, 2026
Merged

Xander-git merged 15 commits into
fix/debug-with-playwrightfrom
claude/nice-gauss-ZBnL6

Conversation

@Xander-git

Copy link
Copy Markdown
Collaborator

Implements the full remaining-work backlog in docs/REMAINING_WORK.md / docs/REMAINING_WORK_TASKS.md (all 13 tasks, 13/13 complete). Each phase was followed by a code-review agent and the findings fixed; a final /simplify pass applied reuse/dedup cleanups. Full unit+integration suite green (2215 passed, 3 skipped); ruff check, ruff format --check, and mypy all clean across the package.

Phase 1 — release-blocking correctness

  • T1 — Inject the real ReadmeGenerator in tray.dependencies._build_controller (sole production constructor). Replaced the controller's flat placeholder ReadmeContext with the canonical layered type; added _build_readme_context (partitions readme_extra across the template/config/custom layers, fills the §10.6 system block — created_by = OS user, project = LIMS short id, run = run dir). GUI-created projects/runs now ship a §10 README.md + readme_fields.json instead of a 3-line stub. (Review fix: system.project is the short id per §3.1, not the folder name.)

Phase 2 — session-control epic

  • T2 — Live wizard phase progress: consume controller.subscribe() into a @ui.refreshable bar. Also fixed a real bug — session_progress.PHASES used post_validation/queueing_sync while the controller emits validating_post_creation/queueing_nas_sync (two phases silently no-op'd).
  • T3 — Operations panel: [Operations…] toolbar button + footer Sync segment, populated from a shared SessionStore.iter_sorted() / project_identifier() (the /operations route was refactored onto them, dropping its private store._sessions reach-in).
  • T4 — Cancel via the §9.4 Discard/Keep dialog (discard_files); §9.6 single-equipment lock disables the creation buttons while a session is in flight.
  • T5 — New INPUT_REQUIRED escalation dialog (Frontend §9.1): renders the plugin's fields, Submit → controller.resume, Cancel → §9.4. The consumer opens it on an input_required frame and force-closes on terminal frames. (Review fix: the abandoned-session GC now publishes a terminal frame so the consumer can't park forever; FAILED ops get a distinct panel bucket.)

Phase 3 — visibility & usability

  • T6 — Real Problems counts (tab badge + right-pane) sourced from the 30 s background audit; /problems footer shows "Last audit: HH:MM:SS · Next refresh in Ns". (See "Deferred" below.)
  • T7 / T10 — Reusable chip editor → Operators allowlist (case-sensitive, non-gating) + content_scan_extensions with reset-to-defaults.
  • T8 / T9 / T11 — Application section wired: autostart checkbox reflects real registration (reverts on failure); Quit button triggers graceful shutdown behind a confirm (run off-loop on a worker thread so request_quit's asyncio.run doesn't deadlock the server loop — review fix); real tray-availability + window-on-close labels.

Phase 4 — cleanups

  • T12 — Offline-catalogue schema-version mismatch is now treated as absent (WARN + None) per §7.2.9.3 (decision confirmed); endpoint/parse errors stay hard errors; TODO(spec) removed.
  • T13 — Rewrote the stale "registry not yet committed / Agent A" comments in plugins/host.py.

Deferred (documented in the tracker)

T6's §11.5 override-and-allow-sync wiring and the full live WS-delta stream are not included: render_problems_page renders a view-model shape (finding.severity/.path/.state/.finding_id) that the raw Validator Finding (tier/rule/run_path/…) doesn't provide — a pre-existing, # pragma: no cover (e2e-only) mismatch. Reconciling that view-model plus the in-process override-write path (mirroring POST /problems/{run_path}/override) is a follow-up; the counts + last-audit core lands here.

Notes

  • A pre-existing, order-dependent import cycle (api/__init__ eager-imports api.app → routers → the controller package mid-init) was left out of scope; operations.py was made cycle-robust via submodule imports, but sessions.py shares the latent pattern (it only bites when a controller submodule is the very first import in a process — never in the full test run / app startup).

Per-task detail and impl notes: docs/REMAINING_WORK_TASKS.md.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn


Generated by Claude Code

claude added 15 commits May 29, 2026 02:28
Production built the creation controller without a readme_generator, so
GUI-created projects/runs ran on NoOpReadmeGenerator and shipped a 3-line
stub README with no YAML front matter and no readme_fields.json cache.

- Inject ReadmeGenerator() in tray.dependencies._build_controller (the
  sole production constructor; covers the lifespan build and the
  apply_live_config fresh-build branch).
- Replace the controller's flat placeholder ReadmeContext with the
  canonical layered type from exlab_wizard.readme; the controller's
  ReadmeGeneratorProtocol and NoOpReadmeGenerator now use the
  tuple[Path, Path] contract.
- Add CreationController._build_readme_context: partitions readme_extra
  across the template/config/custom layers by id, maps template + config
  field declarations, and fills the §10.6 system block (created_by = OS
  user, project = folder name, run = run dir / null).
- Presence is already gated by _validate_inputs and the GUI submits no
  typed extra fields yet, so the generator's strict validation adds no
  new failures for current creations.
- Add end-to-end test asserting four-layer front matter + readme_fields.json.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Code review flagged that the §10.6 system block's `project` field must be
the machine-safe LIMS short id (e.g. PROJ-0042) recorded in README metadata
(§3.1), not the human-readable <project>/ folder segment. Use _short_id_for
(matching the original flat ReadmeContext) and correct the acceptance test.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Confirm & Create progress bar was static (active_phase=None): nothing
consumed CreationController.subscribe(), and the session_progress component
keyed two of its six phases on the wrong strings.

- Fix phase-name mismatch: session_progress PHASES/PHASE_LABELS used
  'post_validation'/'queueing_sync' while the controller emits the
  wire-format 'validating_post_creation'/'queueing_nas_sync'
  (state_machine.Phase). Align the component to the wire-format so a live
  phase frame maps onto a row without translation.
- Add SessionProgressState + apply_frame(state, frame): folds phase/
  progress/done frames into render args; ignores input_required (T5) and
  unknown phases.
- Render the confirm-step bar through a @ui.refreshable bound to
  state.progress, exposing state.progress_refresh.
- mount._run_creation consumes controller.subscribe() via
  _consume_session_progress while the pipeline runs; race-free because
  _launch creates the event queue before the pipeline starts and the
  queue buffers early phases.

The controller emits no 'progress' frame yet, so the §9.3 per-plugin
sub-row stays dormant until the plugin host emits one (handled defensively).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The operations_modal component was built and exported but rendered
nowhere; there was no way to inspect in-flight sessions from the GUI.

- Add SessionStore.iter_sorted() (public, sorted accessor) and a shared
  project_identifier(request) helper; refactor the /operations route onto
  them so neither the route nor the UI reaches into store._sessions, and
  rows are labelled identically.
- Add OperationRow.from_session(): maps the §4.7 state machine onto the
  panel's running/suspended/completed buckets.
- main.py: [Operations…] toolbar button (visible when >0 in flight,
  warning-colored when any need input) and a footer Sync segment that
  flips to 'N operations need input' and opens the same modal (§3.5.5);
  MainPageState gains operations_count / operations_input_required.
- mount.py: _operation_counts, _build_operation_rows, _open_operations_modal
  (fresh snapshot per open), and an _open_operation_details 'view log'
  dialog. Resume/cancel are baseline here (cancel keeps files); the §9.4
  dialog and §9.6 disable rule land in T4, the input dialog in T5.
- Fix a latent circular import: operations.py now imports SessionState /
  project_identifier from the controller submodules, not the package.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- _cancel_operation opens a Discard/Keep confirm dialog mapped to
  controller.cancel(id, discard_files=...): Discard removes the partial
  directory (shutil.rmtree), Keep leaves it as an orphan. Errors are
  toasted; cancel is a no-op on an already-terminal session.
- §9.6 single-equipment concurrency: _operation_counts now also reports
  an 'active' (strictly non-terminal) count; _build_main_state sets
  MainPageState.creation_in_flight, and the New Project / Run / Test Run
  buttons disable (with a tooltip) while any creation is in flight.
- Tests for _operation_counts (panel vs active vs input_required).

Resume row action still routes through the INPUT_REQUIRED dialog landing
in T5.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
When a plugin suspended mid-creation the wizard hung indefinitely with no
way to answer; only the HTTP /resume route could.

- New component ui/components/input_required_dialog.py: the §9.1
  'Additional input required' dialog -- plugin pill, reason line, one
  widget per pending_input field (string/text/choice/boolean) two-way
  bound to a values dict; persistent so it must resolve before the next
  frame (§9.2).
- The T2 progress consumer opens the dialog on an input_required frame
  and force-closes it on a terminal done/failed frame (plugin timeout).
  Submit -> controller.resume(sid, values) (errors surfaced as a toast;
  plugin re-rejection re-emits input_required and re-opens the dialog),
  Cancel -> the §9.4 cancel dialog.
- Split _cancel_operation into a controller-driven _cancel_session core
  (reused by the dialog) plus a deps resolver; the Operations modal's
  Resume reads the parked pending_input and re-opens the same dialog.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Settings had no editor for OperatorsConfig.allowlist (the controller's
allowlist gate was a permanent no-op for GUI users) and rendered
content_scan_extensions read-only.

- Add a reusable _render_chip_editor(values, ...) in settings.py
  (add / per-chip delete / optional reset; mutates the draft list in
  place so persistence rides the existing draft -> finalize -> Save path).
- T7: add 'operators' to SETTINGS_SECTIONS (between nas_cleanup and
  validator) + SECTION_TITLES and an operators section with §7.9 helper
  text + a chip editor bound to draft.operators.allowlist. Stored
  verbatim (case-sensitive; trimmed on add). Non-gating.
- T10: replace the read-only extensions label with the same chip editor
  bound to draft.validator.content_scan_extensions, with Reset to
  defaults and an on-add '.'-prefix validator.
- Update the settings section-count test (8 -> 9, operators present).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- Consumer leak (Major): the abandoned-session GC closed INPUT_REQUIRED
  sessions directly on the store, bypassing the controller's _publish, so
  the in-process subscribe() consumer parked forever on queue.get(). The
  GC now pushes a terminal {kind: failed} frame onto the session's event
  queue so the wizard progress loop wakes and exits.
- FAILED mislabeled (Major): OperationRow.from_session mapped FAILED to
  the running bucket (play glyph, no action). Add a STATE_FAILED bucket
  (error glyph) so a failed op is labelled distinctly; sort places it
  after running, before completed.
- Footer Sync segment is only clickable when operations_count > 0, so a
  click never opens an empty Operations panel.
- Fix an inaccurate _open_input_required_dialog docstring (resume does
  not reject an empty payload).

Tests for the FAILED bucket and the GC terminal-frame wake-up.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Application section's three controls were inert: the autostart
checkbox was unbound, the tray-status label was a static literal, and
the Quit button had no handler.

- T8: tray/dependencies seeds deps.autostart_is_registered; _apply_autostart
  returns the toggle's real is_registered(); the checkbox seeds from it and
  applies immediately on change (exempt from the draft, §7.13), reverting to
  the actual post-op state on mismatch.
- T9: the tray builder attaches deps.request_quit = tray_app.request_quit;
  the Quit button is gated behind a confirm and scheduled non-blocking via
  ui.timer so the HTTP response flushes before shutdown.
- T11: the tray builder sets deps.tray_available from a pystray-import
  probe; the label reflects available / unavailable (window-only) and the
  window-on-close behavior copy is added.
- AppDependencies gains typed autostart_is_registered / request_quit /
  tray_available fields. Controls disable cleanly when their hook is absent
  (headless / tests).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The Problems tab badge and right-pane summary were hardcoded to 0 and
the /problems footer showed 'Last audit: --'; nothing read the audit.

- The 30s background _audit_loop now caches tier counts on
  deps.last_audit_hard / last_audit_soft (single source; avoids a
  per-render O(tree) re-audit). _build_main_state reads them into
  MainPageState.problems_count_hard/soft, so the tab badge and the
  right-pane summary are real (right-pane no longer hardcodes 'Showing 0').
- render_problems_page takes last_audit_at and renders
  'Last audit: HH:MM:SS · Next refresh in Ns' with a 1s ui.timer
  countdown. start_audit_task=True confirmed in the tray build.
- AppDependencies gains typed last_audit_hard/soft fields.

Deferred (documented in the tracker): wiring the §11.5 override action and
the full live WS-delta stream -- render_problems_page expects a view-model
shape the raw Validator Finding doesn't provide (a pre-existing, e2e-only
mismatch); reconciling that + the in-process override-write path is a
follow-up. The counts/last-audit core lands here.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
…ents (T12, T13)

T12 (§D1): resolve the offline-catalogue schema-version policy as
treat-as-absent / WARN (§7.2.9.3, user-confirmed). read_catalogue now
returns OfflineCatalogue | None -- a schema_version mismatch logs a WARN
and returns None (the consumer falls through) instead of raising
ConfigError; endpoint mismatch and missing/parse errors stay hard errors.
The decision is recorded in the docstring; TODO(spec) removed. The caller
treats None as []. Test flipped to assert None + WARN.

T13 (§D2): rewrite the three stale 'plugins/registry.py owned by Agent A /
not yet committed' comments in plugins/host.py to state that
plugins.registry.PluginRegistry is the committed, wired production registry
and _ListBackedRegistry is test-only. Pure cleanup.

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
- T9 Quit deadlock (Critical): ui.timer ran the quit callback on the
  server's running event loop, where request_quit's asyncio.run(...)
  raised 'loop already running' and was swallowed -- the app never shut
  down. Run the hook on a fresh daemon thread instead (no running loop,
  so asyncio.run works; the click handler still returns immediately so
  the HTTP response flushes).
- Problems footer countdown (Minor): clamp 'Next refresh in Ns' at 0 once
  the interval elapses instead of re-cycling via modulo (which misreported
  the wait when an audit pass slipped past 30s).
- Chip editor (Nit): a duplicate add now notifies 'already in the list'
  and keeps the typed text, matching the validator branch (was silently
  cleared).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
The class docstring still said a schema_version mismatch surfaces as
ConfigError; under T12 it is treated as catalogue-absent (WARN + None).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Quality-only cleanups from the /simplify review (no behavior change):
- Extract the §9.5 Operations-panel membership rule into one shared
  on_operations_panel(session) (controller/session_store.py); use it from
  the /operations route and the in-process _panel_sessions, replacing three
  copies of the DONE/ABORTED filter.
- _operation_counts: derive panel from the shared _panel_sessions and use
  Session.is_terminal() for the active count instead of a re-derived
  terminal-state tuple.
- Drop the box_holder dict in the settings autostart handler (the closure
  reads autostart_box directly; the re-entrancy guard stays).
- Factor apply_frame's duplicated 'mark predecessors complete' loop into a
  local _complete_through helper.
- Count audit findings against Tier.HARD/SOFT.value instead of bare string
  literals, matching the rest of the codebase.

Full unit+integration suite green (2215 passed).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
Run the project formatter over the changed files (line-wrapping only, no
behavior change) and remove the now-inaccurate 'operators is deferred'
note from the settings module docstring (operators is wired in T7).

https://claude.ai/code/session_01AeEGo2KMn5xq7UNDuap1Wn
@Xander-git
Xander-git merged commit b81f575 into fix/debug-with-playwright May 29, 2026
1 check passed
@Xander-git
Xander-git deleted the claude/nice-gauss-ZBnL6 branch May 29, 2026 04:37
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.

2 participants