Skip to content

Main-window UI refresh (Phases 0–6) - #18

Merged
Xander-git merged 27 commits into
mainfrom
feature/gui-imrprovement-v2
May 30, 2026
Merged

Main-window UI refresh (Phases 0–6)#18
Xander-git merged 27 commits into
mainfrom
feature/gui-imrprovement-v2

Conversation

@Xander-git

@Xander-git Xander-git commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

A visual + interaction refresh of the main window (/main), landed in six gated
phases. 20 commits since main: the UI refresh itself plus a few small
docs/chore commits (a sample-data design spec, a .gitignore for Playwright
outputs, a CI-on-every-PR workflow tweak).

The panes become elevated, titled cards on a grey canvas; the centre file
list gets zebra striping + a defined row-state precedence; selecting a file/folder
shows its metadata nested in the current tree context; and a set of low-cost
intuitiveness wins land on top (unified selection, sync tooltips + legend, live
footer segments, empty states, toolbar grouping + density, search).

Design spec: docs/superpowers/specs/2026-05-29-main-window-ui-refresh-design.md
(reconciled for the shipped divergences); phase log:
…-main-window-ui-refresh-remaining-work.md.

What's in it, by phase

  • 0–1 — Tokens. Design tokens + fixed three referenced-but-undefined CSS
    vars
    (--color-highlight / --color-link / --color-bg-subtle) that were
    silently dropping declarations.
  • 2 — Pure resolvers. row_background (Selected > New > Tombstone > Zebra),
    tolerant sync_status_props(strict=False), worst-of sync_rollup.
  • 3 — Panel framing. framed_pane (new) wraps Explorer / Files / metadata /
    footer; panes pin to the viewport height and scroll internally.
  • 4 — File selection (Option B). Single-click a file/folder row → highlight
    (data-selected) + a metadata sub-card beneath the node content; per-folder
    refresh; tolerant per-file status icons. Metadata floats as a popover over
    the Files pane (operator-directed; reconciled in the design doc). Also fixes a
    latent bug:
    register_theme was never injected by the running app — now wired
    app-wide (shared=True), so design tokens resolve live on every page (the inline
    var(--x, #literal) fallbacks stay as a headless/cron-render safety net).
  • 5 — Polish. Search wiring (the box was inert) + result-count / no-matches
    pill; shared empty-state placeholders; toolbar group divider; live footer
    segments
    (Validator → WARNING on hard findings, LIMS → DANGER when
    unreachable); file-list density toggle; unified tree selection (selected
    node gets the file-row fill + accent bar); sync tooltips + a legend
    popover.
  • 6 — e2e + verification. No existing flow broke (testids were preserved);
    added flow_28 (file selection + search / density / legend) and catalogued the
    new affordances. Every Phase-5/6 surface was live-verified via Playwright
    (DOM inspection + screenshots), incl. the app-wide theme on welcome/settings/
    problems.

Verification

  • Coverage gate: pytest tests/unit tests/integration --cov --cov-fail-under=91
    91.70%, 2338 passed (the CI command — unit first; e2e runs in a subprocess,
    excluded from coverage).
  • e2e: pytest tests/e2e → 70 passed, 3 skipped (skips pre-existing).
  • Static: ruff check + ruff format --check + mypy src all clean.
  • flow_06 / flow_09 are intermittently order-flaky under the shared session
    server (pass in isolation) — pre-existing, worth a follow-up.

Reviewer note

Run the suite as CI does — pytest tests/unit tests/integration (unit first).
Plain pytest tests collects integration before unit and pollutes NiceGUI's
global auto-index render state, failing the in-process UI render tests. It's a
collection-order constraint, not a product bug.

🤖 Generated with Claude Code

Xander-git and others added 21 commits May 29, 2026 16:15
Design spec for the main-window refresh: elevated titled panel framing
(B+C hybrid), Excel-style zebra striping with a defined row-state
precedence, Option-B metadata pane (selected file/folder nested under the
tree context), and the intuitiveness wins (unified selection, sync
tooltips/legend, live status segments, empty states, toolbar grouping +
density, search wiring). Token-first; rides the existing URL/navigate
render model. Includes the eight resolved questions from code review and
the standalone HTML mockups approved during brainstorming.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the main-window UI refresh. Adds the surface tokens the refresh
consumes (zebra stripe, row-selected fill + accent bar, pane-header tint,
new-file highlight) to design.py and emits them from build_root_css; DESIGN.md
§02 + §07 kept in lock-step.

Also fixes four CSS vars that shipping components already referenced but the
:root block never declared, so they silently resolved to empty and were
dropped -- most visibly the new-file row highlight (file_list.py) was invisible,
plus the breadcrumb link/bar tint and the relay-badge text colour:
--color-highlight, --color-link, --color-bg-subtle, --color-on-info. The spec
named three; a var-audit during implementation found the fourth
(--color-on-info, metadata_pane.py:130).

A new test_theme.py regression guard scans the ui/ tree for any var(--...)
referenced but not declared, so this bug class can't reappear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 of the main-window UI refresh -- pure, NiceGUI-free logic landed and
tested before any component consumes it (Phase 4).

- file_list.row_background(): resolver for the row-state precedence
  Selected > New-file > Tombstone > Zebra. Tombstone contributes no fill
  (suppresses the zebra stripe) but its dim/italic decoration is additive, so
  a selected tombstone keeps both treatments. Zebra parity is index-based, not
  CSS :nth-child, so interleaved state rows never shift the stripe.
- sync_status_icon: add strict=False tolerant mode returning neutral props
  (muted dash) for None/unknown status; strict=True default preserves the
  original raise-on-unknown contract for existing callers.
- sync_rollup.sync_rollup(): worst-of folder rollup over SyncStatus values
  (failed > blocked_by_validation > pending > synced > cleaned); None/unknown
  ignored, empty -> None. UI-only ordering (the enum is schema-committed and
  deliberately order-free).

25 new unit tests; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 of the main-window UI refresh (spec §4.1, the approved B+C hybrid).

- New components/framed_pane.py: a context-manager helper rendering an
  elevated card (surface fill, hairline border, radius-md, shadow-sm) topped
  by an uppercase title strip with an optional right-aligned count pill, then
  a scrollable body it yields -- so children of the with-block nest inside the
  body, not the card root. Pure style-fragment helpers (card_style /
  header_style / body_style) stay unit-testable without NiceGUI; each ends
  with ';' so callers can append overrides, and every token carries a literal
  fallback since register_theme isn't on every route.
- main.py: wrap Explorer, Files (with a count pill -- '0 items' for a
  selected-but-empty folder, suppressed when nothing is selected), and the
  footer in the framed shell; add a gap between splitter panels. The metadata
  pane gets the card frame via card_style() but keeps its existing tabs in
  place of a title strip (approved default), appending overflow:auto so the
  tab panels scroll within the frame. All existing data-testids preserved.

17 unit tests (incl. a body-nesting invariant assertion); ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4 (Option B): single-clicking a file/folder row in the Files pane
highlights it and shows its metadata as a sub-card beneath the right
pane's node context -- all carried on the existing URL/navigate model
(?file=/?q=/?density=), no new reactive paradigm.

- mount: resolve ?file= against the in-memory folder feed
  (_build_selected_file / _build_selected_folder, one-level scan +
  sync_rollup, degrade-don't-raise); thread file/q/density through _main
  and _build_main_state; add on_select_file + per-folder refresh
  (_refresh_selected_folder, OQ-1/A) that also records the coordinator tick.
- file_list: selectable rows (files AND folders) styled via the Phase-2
  row_background resolver with a tolerant sync-status icon;
  FileListState.selected_path.
- metadata_pane: SELECTED FILE / SELECTED FOLDER sub-card that appends
  beneath the node dispatch (and the empty state) via a fall-through chain;
  pure selected_file_card_title.
- framed_pane: optional header_extra slot (hosts the Files refresh button).

ruff + ruff format + mypy clean; tests/unit/ui 603 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live-demo refinements on top of the Phase 4 selection work:

- theme: call register_theme() app-wide (shared=True) from mount_ui so the
  :root design tokens finally reach the browser -- the grey app canvas and
  resolved var(--color-*) tokens were dead before (register_theme was never
  wired). file_list.row_background gains literal token fallbacks so the
  selection fill / accent bar / zebra still render pre-theme.
- main: the metadata pane now floats as an overlay popover over the right of
  the Files pane (was a docked column); the vertical "Metadata" tab toggles
  it (right_pane param) and sits on the popover's left edge. NB: diverges
  from the approved spec's docked Option B -- a deliberate UX change.
- main: the Files header groups its count + per-folder refresh on the left
  (framed_pane gains count_left).
- metadata_pane: kv values overflow instead of wrapping (long paths stay on
  one line; the pane scrolls horizontally).
- e2e harness: _test_app /main wires Phase 4 selection + register_theme so
  the popover/selection demo mirrors production (a thin Phase 6 slice).

Unit gate green: ruff + ruff format + mypy clean; tests/unit/ui 603 passed.
e2e not yet updated for the popover/theme (Phase 6 follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design for replacing the single hardcoded --add-test-samples equipment
entry with a list-driven generator that seeds a full equipment/project/run
tree, each folder carrying real metadata via the production file producers.
Captures the resolved decisions: code-embedded Pydantic SAMPLES list,
shared metadata-assembly refactor of creation.py, dev-command wipe vs
tray first-launch-only seeding, and a dataset spanning sync-status badges
and payload files for UI coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…croll

The Explorer / Files / metadata panes sized to their content and grew the
page (NiceGUI's q-layout / q-page chain is min-height-driven, so nothing was
pinned to the viewport). Give that chain a definite viewport height and
flex-fill down to the splitter -- page-scoped ui.query overrides on
.q-page-container / .q-page / .nicegui-content, and the splitter switched from
h-full to flex:1 / min-height:0. The three panes now fill the height between
the fixed header and footer and scroll within their own overflow:auto bodies
instead of the whole page scrolling.

Verified live: page no longer scrolls; the Explorer pane scrolls internally
(607px of tree inside a 454px body). Unit gate green (603 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- fixed graphify outputs being commited
Captures what shipped (Phases 1-4 + the post-demo popover/theme/full-height
batch, with commit hashes), the two deliberate divergences from the approved
design spec (metadata popover instead of the docked Option B; register_theme
now wired app-wide), and the remaining work: spec reconciliation, Phase 5
polish (reconciled with the popover), Phase 6 e2e + final verification (with
the specific DOM break points to fix), cleanup/tech-debt, and risks
(notably the app-wide visual regression from finally injecting the theme).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the previously-inert /main search box (OQ-2): the query now reaches
TreeFilters.search via chip_state_to_tree_filters, the input carries its
value + clearable + a 300ms Quasar debounce, and on_change re-navigates
?q= through the existing URL/navigate model (mount._on_search + the e2e
harness mirror). A result-count / no-matches affordance, sourced from the
same build_nodes the tree renders via the new pure count_search_results
helper, sits under the box while a query is active.

Add a shared empty_state(icon, message, testid) placeholder (centred icon
+ one-line hint) and route the three bare-label empties through it
(select-a-folder, empty-folder, select-a-node), preserving each testid.

Group the header toolbar with a vertical divider between the creation
actions and the utility actions (button order + testids unchanged).

Gate: ruff check + ruff format --check + mypy src clean; tests/unit/ui
609 passed (search-count + empty_state unit tests added); code-reviewer
pass found no high-confidence issues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Validator / LIMS / Staging footer segments were hardcoded NORMAL.
Add a pure derive_footer_segment_states(problems_count_hard, lims_reachable,
staging_pending) helper (Frontend §3.5.5): Validator → WARNING on a hard
finding, LIMS → DANGER when the endpoint is unreachable, Staging → WARNING
when runs are pending (no cheap cached count today, so the caller passes 0
and it stays NORMAL until a cached signal exists). mount._build_main_state
sources problems_count_hard (audit rollup) + deps.lims_reachable and writes
the derived states onto three new MainPageState fields the footer reads.
Sync already derives live from the operations counts.

Gate: ruff + mypy clean; tests/unit/ui 614 passed (5 derivation tests added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tick the four Phase-5 items shipped this session (commits 01edee7, 1bb3c8d)
in the remaining-work spec; split density out of the toolbar item with its
open UX question; flag the remaining items (density, unified selection, sync
tooltips/legend) as the visual/theme-CSS work that wants a live pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egend

Finishes the remaining Phase-5 items (all live-verified via Playwright DOM
inspection on the seeded app):

- File-list density (§4.8): a compact/comfortable toggle in the Files header
  rides ?density=; density_card_class() puts .exlab-density-compact on the
  Files card and a scoped theme rule tightens only the file-row vertical
  padding (--sp-1 vs the default --sp-2 from .p-2). framed_pane gains a
  card_classes param. on_toggle_density wired through mount + the e2e harness.

- Unified tree selection (OQ-6): build_tree seeds Quasar's v-model:selected
  from ?selected= and a theme rule gives the selected node header the same
  --color-row-selected fill + 3px --color-row-selected-bar inset as a selected
  file row. Verified: computed bg #dceaff + inset bar #1b75bc on the selected
  node only.

- Sync tooltips: run rows carry a friendly sync_title (`:title`) — "Data on
  local disk" / "Cleared -- data on NAS only" — mirroring the local/cloud icon.

- Sync-status legend: a "?" popover in the Files header lists every state's
  icon + meaning, sourced from sync_status_icon.sync_legend_entries()
  (_STATUS_TO_PROPS, single source of truth).

Gate: ruff + mypy clean; tests/unit/ui 623 passed (9 new tests). code-reviewer
pass; its one flag (legend menu anchoring) verified a false positive — the
`with (button, menu):` form nests the menu in the button (count-pill click
does not open it; button click does).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the final three Phase-5 items done (commit a31b916), record the live
Playwright verification of every Phase-5 item, and note Phase 6 (e2e suite +
screenshots) + cleanup as the remaining work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…talog

The Phase 3-5 DOM changes didn't break any existing flow (testids were
preserved), so the e2e suite was already green. Add the coverage the spec
calls for:

- New flow 28 (test_flow_28_selection_search_density): single-click file-row
  selection → ?file= + data-selected + metadata-selected-file sub-card; search
  result-count + no-matches pill; density toggle → ?density=; sync-status
  legend popover; the selected tree node's .q-tree__node--selected hook.
- Catalog the new operator affordances (file-list-row, toggle-right-pane,
  main-search, files-density-toggle, files-legend) in ux_catalog.py and
  regenerate docs/UX_INTERACTIONS.md; the testid-exists-in-source and
  e2e-covered checks pass.

Gate: tests/e2e 70 passed / 3 skipped; unit+integration coverage 91.70%
(>= 91%); ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record that the Phase 3-5 DOM changes broke no flow, flow 28 adds the
file-selection + search/density/legend coverage, the CI coverage command hits
91.70%, and the app-wide theme spot-check (welcome/settings/problems) shows no
regression. Note the collection-order gotcha (run unit before integration) and
that only §D cleanup + §A spec reconciliation remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hide (not remove) the per-equipment `stage` sync mode and the staging-PC
relay. Every equipment now syncs directly to the NAS (sync_mode="nas");
the staging backend stays dormant and tested for reversibility. No config
schema change, no feature flag.

UI surfaces gated:
- Add-Equipment wizard: drop the sync-mode step (identity -> paths ->
  review); dormant _render_sync_mode_step kept as the reversibility hook.
- Settings: rename "Orchestrator Mode" -> "Workstation" (section id kept,
  so the setup gate / routing are untouched); remove the staging-root input.
- mount.py: remove the /staging route + footer clear-verified wiring and
  the helpers it orphaned (_build_staging_state, _bulk_clear_verified,
  _render_unavailable); keep the per-run tree context-menu actions (they
  serve nas runs) and reword their toasts off "staging".
- main.py: remove the footer "Staging" segment + "Clear verified runs".

Backend left intact and dormant: orchestrator/ (incl. the quiescence
poller, the auto-sync engine for BOTH nas and stage runs),
api/routers/staging.py, SyncMode.STAGE, orchestrator.staging_* fields.

Tests: skip the staging-driven UI flows with reversible markers citing the
spec; update wizard/settings/footer/persist tests; drop mount glue-fn tests
for the removed helpers. Backend staging tests stay green, proving dormancy.

Add a root CLAUDE.md documenting the hidden-but-present state so future
work does not "fix" the missing UI or delete the shared poller.

Spec: docs/superpowers/specs/2026-05-29-hide-orchestrator-staging-design.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… cleanup

Amend the original main-window design spec to record the two shipped
divergences as the approved behaviour: a reconciliation banner up top + inline
notes at §3.2 (register_theme wired app-wide), §4.1 (metadata is a floating
popover, not a docked card), §4.4 (only the container changed), and §5
(rationale). Record the popover as THE direction (popover-only).

In the remaining-work doc, tick §A (done) and §D: decided to KEEP the literal
var() fallbacks as a documented headless/cron-render safety net; scratch
artifacts (RESUME.md, phase*.png, .playwright-mcp/) deleted; register_theme
confirmed single-injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Xander-git Xander-git changed the title Main-window UI refresh (Phases 0–6) + rclone-only NAS-sync migration Main-window UI refresh (Phases 0–6) May 30, 2026
Xander-git and others added 6 commits May 29, 2026 23:28
Replace the single hardcoded `--add-test-samples` equipment literal with a
declarative `SAMPLES` list expanded by a generator that seeds a full demo
tree (equipment -> projects -> runs). Every folder's metadata is written
through the real production producers (ReadmeGenerator, CreationWriter,
EquipmentCacheWriter) so it cannot drift from what the wizard writes.

- sample_data/{spec,generator,__init__}: Pydantic SAMPLES + generator +
  generate_samples() facade with a guardrailed destructive wipe.
- controller/metadata_assembly: extract build_readme_context /
  build_creation_json (+ moved helpers) shared by the controller and the
  seeder; creation.py now delegates (behavior-preserving, parity-tested).
- config/test_bootstrap: shared write_starter_test_config /
  bootstrap_test_config (light module; no pystray import) used by both the
  tray flag and the new dev command.
- dev/seed: `python -m exlab_wizard.dev.seed` (always wipe + regenerate).
- tray/main: delete the inline TESTRIG literal; seed via the shared
  bootstrap on first launch only (never wipes an existing sandbox).

Deterministic (fixed clock; README/creation.json/payload bytes identical
across runs; only equipment.json's writer-stamped timestamps differ).
Seeded equipment roots are base paths so run-walking (quiescence poller /
validator) discovers the tree. Destructive wipe is fenced to the -test
sandbox with strict-subpath containment. Full suite green (2362 passed,
3 skipped); ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(sample-data): list-driven sample data generation for --test
`test_cancel_with_discard_files_removes_partial_dir` races the creation
pipeline against a mid-flight cancel (it polls with asyncio.sleep and
branches on whether the directory landed first). Under load the race can
transiently lose, flaking the suite.

Add pytest-rerunfailures to the test extra and mark the test
`@pytest.mark.flaky(reruns=3, reruns_delay=0.5)` so a transient timing miss
retries instead of failing CI. Verified the marker re-runs on failure
(fails twice -> passes on the third attempt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing the @ui.page("/staging") block left a single blank line before
the "Helpers" divider; ruff format wants two. No behavioural change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Xander-git
Xander-git merged commit 43a5508 into main May 30, 2026
8 checks passed
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