feat(ui): protected bands panel + modal edit dialog + JSON import/export (ticket 45) #44

Merged
ykp merged 2 commits from protected-bands-ui into master 2026-09-28 20:43:58 +02:00
Owner

Round 9 W3 — the operator surface for protected bands (spec §4.2.5 is the authority; #42 shipped the W1 data layer, #43 the W2 notch stage).

The panel (spec §4.2.5, as requested)

A "Protected bands" LabelFrame under the frequency-bands panel (grid row 2, col 1; the action buttons shift down one row) — compact, exactly as specified: four buttons — Import / Export / Clear / Edit — plus the count label "x protected bands" where x = count_protected_in_span (records overlapping the signal's occupied span [fc−BW/2, fc+BW/2]). When out-of-span records exist the label stays honest with a (N defined) suffix. The label refreshes on every refresh() and after each dialog closes; Clear enables only when records exist.

The modal edit dialog (operator decision #5)

protected_bands_dialog.py (new): the full 4-column table (Fmin (MHz) / Fmax (MHz) / Transition (MHz) / Rejection (dB)) with add/delete rows and inline editing (the frequency-bands double-click Entry pattern). Edits happen on a copy; L17/L18 feedback renders in a status label (red when invalid, with the exact finding messages); OK commits to project.ProtectedBands (marks dirty), Cancel discards.

JSON import/export (operator decision #4)

protected_bands_io.py (new, core/io): the protected-bands file is a UTF-8 JSON list of {Fmin, Fmax, Transition, Rejection} records in the project-JSON shape. The reader is strict — non-JSON, non-list, non-object entries and malformed records raise ProtectedBandsParseError carrying the record index; import is atomic (the whole file validates before the model changes, so no partial imports). Import offers a replace/append choice when the current list is non-empty; Export writes via the save-as dialog.

Tests — tests/test_ui_protected_bands.py (18)

Headless tier (10): JSON round-trip, project-JSON shape, empty list, all four rejection paths (with record index), missing file, atomic import, count-helper agreement. Widget tier (8, skips headless like the other UI tests): panel builds (4 buttons + label), count label update with the (2 defined) suffix, clear enablement, edit-dialog wiring, export→import round-trip, malformed-import error + model preservation, replace/append both paths, clear confirmation (accept/decline/no-dialog-when-empty).

Gate

  • pytest: 1278 passed / 64 skipped (headless tier), coverage 93.51 %
  • ruff + mypy: clean

Next: ticket 46 (W4) — the integration close-out.

Round 9 W3 — the operator surface for protected bands (spec §4.2.5 is the authority; #42 shipped the W1 data layer, #43 the W2 notch stage). ## The panel (spec §4.2.5, as requested) A **"Protected bands" LabelFrame under the frequency-bands panel** (grid row 2, col 1; the action buttons shift down one row) — compact, exactly as specified: **four buttons** — **Import / Export / Clear / Edit** — plus the count label **"x protected bands"** where x = `count_protected_in_span` (records overlapping the signal's occupied span `[fc−BW/2, fc+BW/2]`). When out-of-span records exist the label stays honest with a `(N defined)` suffix. The label refreshes on every `refresh()` and after each dialog closes; Clear enables only when records exist. ## The modal edit dialog (operator decision #5) `protected_bands_dialog.py` (new): the full **4-column table** (`Fmin (MHz)` / `Fmax (MHz)` / `Transition (MHz)` / `Rejection (dB)`) with add/delete rows and **inline editing** (the frequency-bands double-click Entry pattern). Edits happen on a **copy**; L17/L18 feedback renders in a status label (red when invalid, with the exact finding messages); **OK commits** to `project.ProtectedBands` (marks dirty), **Cancel discards**. ## JSON import/export (operator decision #4) `protected_bands_io.py` (new, `core/io`): the protected-bands file is a UTF-8 JSON list of `{Fmin, Fmax, Transition, Rejection}` records in the project-JSON shape. The reader is **strict** — non-JSON, non-list, non-object entries and malformed records raise `ProtectedBandsParseError` carrying the record index; **import is atomic** (the whole file validates before the model changes, so no partial imports). Import offers a **replace/append** choice when the current list is non-empty; Export writes via the save-as dialog. ## Tests — `tests/test_ui_protected_bands.py` (18) Headless tier (10): JSON round-trip, project-JSON shape, empty list, all four rejection paths (with record index), missing file, atomic import, count-helper agreement. Widget tier (8, skips headless like the other UI tests): panel builds (4 buttons + label), count label update with the `(2 defined)` suffix, clear enablement, edit-dialog wiring, export→import round-trip, malformed-import error + model preservation, replace/append both paths, clear confirmation (accept/decline/no-dialog-when-empty). ## Gate - `pytest`: **1278 passed / 64 skipped** (headless tier), coverage **93.51 %** - `ruff` + `mypy`: clean Next: ticket 46 (W4) — the integration close-out.
feat(ui): protected bands panel + modal edit dialog + JSON import/export (ticket 45)
Some checks failed
test / test (ubuntu-latest) (pull_request) Failing after 3m10s
c97f87a9dd
Round 9 W3 — the operator surface for protected bands (spec §4.2.5):

- main_window.py: the Protected bands LabelFrame sits UNDER the
  frequency-bands panel (grid row 2, col 1; buttons shift to row 3):
  four buttons — Import / Export / Clear / Edit — + the count label
  "x protected bands" (x = count_protected_in_span: records overlapping
  the signal's occupied span [fc−BW/2, fc+BW/2]; a suffix
  "(N defined)" appears when out-of-span records exist so the label
  stays honest). Label refreshes on every refresh() and after each
  dialog closes. Clear is enabled only when records exist.
- protected_bands_dialog.py (new): the modal edit dialog — 4-column
  table (Fmin/Fmax/Transition/Rejection MHz/MHz/MHz/dB), add/delete
  rows, inline editing (the bands-table double-click Entry pattern),
  L17/L18 feedback in a status label (red when invalid), commit-on-OK
  (edits happen on a COPY; Cancel discards; OK assigns to
  project.ProtectedBands and marks dirty).
- protected_bands_io.py (new, core/io): the protected-bands JSON file —
  read (strict: not-a-list / non-object entry / malformed record raise
  ProtectedBandsParseError with the record index; import is atomic —
  the file fully validates before the model changes) and write (the
  project-JSON record shape, UTF-8, 2-space indent).
- Import flow: file picker → full-file validation → replace/append
  (messagebox choice) only when the current list is non-empty →
  commit + refresh; malformed files show the error and keep the model.
- Export flow: save-as picker → write → status label feedback.
- tests/test_ui_protected_bands.py (18 tests): headless tier — JSON
  round-trip, project-JSON shape, empty list, the four rejection paths
  (non-JSON / non-list / non-object entry / malformed record with
  index), missing file, atomic import, count agreement; widget tier
  (skips headless) — panel builds (4 buttons + label), count label
  updates (1 protected bands (2 defined)), clear enablement, edit
  dialog wiring, export→import round-trip, malformed-import error +
  model preservation, replace/append choices, clear confirmation
  (accept/decline/no-dialog-when-empty).

Gate: pytest 1278 passed / 64 skipped (headless tier), coverage
93.51 %; ruff + mypy clean.
fix(ui): wire the Clear-button enablement; fix the count-label fixtures (CI widget tier)
All checks were successful
test / test (ubuntu-latest) (pull_request) Successful in 2m46s
17f4ce2cb0
CI (Python 3.11, tkinter installed) caught two widget-tier failures the
headless tier cannot run (tkinter absent locally):

1. test_clear_button_enabled_only_when_non_empty — a REAL production
   gap: the Clear button was never wired to the records' presence (the
   refresh loop's policy lookup defaults unknown keys to enabled). Now
   _update_protected_bands_label (which runs after the widget-enable
   loop in refresh()) enables Clear exactly when records exist.
2. test_panel_count_label_updates — a fixture gap: the default window
   project has NO frequency bands, so count_protected_in_span correctly
   returns 0. The test now sets the bands fixture first. Also fixes the
   expected text to the module's own singular/plural logic (count == 1
   → "1 protected band (2 defined)") and adds a plural case
   ("2 protected bands", no suffix).

Headless simulation of both label paths verified; full gate green
(1278 passed / 64 skipped, coverage 93.51 %; ruff + mypy clean).
ykp merged commit 763dc31864 into master 2026-09-28 20:43:58 +02:00
Sign in to join this conversation.
No description provided.