Browse catalog dialog (issue #10 phase 2) #67

Open
the_og wants to merge 2 commits from feat/10-browse-dialog into main
Owner

Implements the user-facing half of #10, on top of the already-merged signed-catalog core (#61/#65). Follows the final design from the 2026-07-12 "Decision + final design" comment on #10.

What's here

bcc_core.py (pure, GUI-free, per the design comment):

  • CATALOG_CATEGORY_GROUPS / catalog_category_group() — collapses the raw 20-value taxonomy to the 7 UI chips (Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other). Unknown categories fall back to Other rather than dropping the entry.
  • catalog_entry_matches_query() / filter_catalog_entries() / catalog_entries_in_group() — search-as-you-type (id/display/description/category) + category-chip filtering.
  • format_freshness_hint()last_release ISO date → "Last updated N months/years ago"; "" when missing/unparseable/in the future.
  • first_unfilled_focus_target() — finds the first <PLACEHOLDER> arg or blank env_required key so the GUI can focus it right after Add.
  • load_bundled_catalog_entries() — reads+verifies+validates the bundled catalog.json/.sig pair; returns the servers list or an empty list on any failure (missing file, bad signature, invalid schema). This is the load-bearing guarantee behind the dialog's "Catalog unavailable" empty state — never a half-trusted list.

Bug fix in already-merged code: catalog_entry_to_paste_json() only copied config.env, ignoring env_required — but in the shipped seed data, env_required is where 9 of 19 entries (postgres, github, notion, obsidian, brave-search, tavily, home-assistant, n8n, grafana) actually declare their secret var names; config.env is essentially unused for that purpose. Clicking Add on any of these would have silently produced a server with no env field at all for the user to fill in. Fixed to seed env_required keys as empty-string placeholders unless config.env already sets them (never overwrites a real value, still never ships a real secret — validate_catalog already enforces env_required values must be "").

bcc.py:

  • BrowseCatalogDialog — search box, category chips, list (display/description/category/★/✓ official), detail pane (description, notes, homepage link, freshness hint, and the exact command rendered verbatim). Per-tier action: Add (basic) routes through the existing _import_server path so the duplicate-name prompt (#8) applies for free; Open setup docs (link-only) calls QDesktopServices.openUrl and never auto-adds, and doesn't close the dialog.
  • Rendering safety: every catalog-derived string goes through a plain_label() helper (Qt.PlainText + html.escape, matching catalog_console.py's existing approach) or an inherently-plain QPlainTextEdit for the command preview — catalog.json takes community PRs, so every field is attacker-influenceable even though it's signed.
  • "Browse catalog…" button next to Paste JSON…
  • ServerEditor.focus_target() — selects the first unfilled arg line, or opens the env-table cell editor on the first blank env row, after a catalog Add.
  • Save-time placeholder guard: warns (Yes/No, defaults No) if any server still has an unfilled <PLACEHOLDER> — never blocks a deliberate save, never lets one slip through unnoticed.

bcc.spec: added data/catalog.json.sig to datas — it was missing, so a frozen build would have shipped a catalog.json with no matching .sig for resolve_catalog() to verify against, meaning the bundled catalog would always fail signature verification in a real build.

Tests

+82 tests in tests/test_core.py: category-group mapping (all 19 taxonomy values + unknown fallback + case-insensitivity), catalog search/filter, freshness-hint formatting (including the 23-vs-24-month rounding boundary), first_unfilled_focus_target, the env_required fix (incl. a regression against the real shipped postgres entry), and load_bundled_catalog_entries under valid/tampered-byte/wrong-key/missing-file/invalid-but-signed conditions, plus an end-to-end check against the real data/catalog.json + .sig (19 entries, signature verifies).

Status: 374 passed, 1 skipped (pre-existing). ruff check . and ruff format --check . both clean.

What I could not verify

PySide6 cannot import in this build sandbox (ImportError: libEGL.so.1, no system GL libs, no root to install them). So BrowseCatalogDialog, ServerEditor.focus_target, and MainWindow.browse_catalog are not exercised at runtime — only py_compile/ast.parse-checked and reviewed by hand. All decision logic they depend on (filtering, grouping, freshness, focus-target selection, catalog loading) is pushed into bcc_core.py and is unit-tested. Please eyeball the dialog live before shipping, especially:

  • The two-line QListWidgetItem row text (display\ndescription · group) — I couldn't confirm Qt renders the embedded \n the way I expect across platforms; worth a visual check.
  • ServerEditor.focus_target's QTextCursor line-selection for the args page, and the env table's editItem() call opening the inline editor as expected.
  • Overall dialog sizing/splitter proportions (880×560, list 360 / detail 480).

Out of scope (per the task)

Remote fetch/cache (#61 follow-up), and no changes to catalog_console.py, catalog_review.py, scripts/, workflows, or data/catalog.json itself.

Implements the user-facing half of #10, on top of the already-merged signed-catalog core (#61/#65). Follows the final design from the 2026-07-12 "Decision + final design" comment on #10. ## What's here **`bcc_core.py`** (pure, GUI-free, per the design comment): - `CATALOG_CATEGORY_GROUPS` / `catalog_category_group()` — collapses the raw 20-value taxonomy to the 7 UI chips (Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other). Unknown categories fall back to Other rather than dropping the entry. - `catalog_entry_matches_query()` / `filter_catalog_entries()` / `catalog_entries_in_group()` — search-as-you-type (id/display/description/category) + category-chip filtering. - `format_freshness_hint()` — `last_release` ISO date → "Last updated N months/years ago"; `""` when missing/unparseable/in the future. - `first_unfilled_focus_target()` — finds the first `<PLACEHOLDER>` arg or blank `env_required` key so the GUI can focus it right after Add. - `load_bundled_catalog_entries()` — reads+verifies+validates the bundled `catalog.json`/`.sig` pair; returns the servers list or an **empty list on any failure** (missing file, bad signature, invalid schema). This is the load-bearing guarantee behind the dialog's "Catalog unavailable" empty state — never a half-trusted list. **Bug fix in already-merged code:** `catalog_entry_to_paste_json()` only copied `config.env`, ignoring `env_required` — but in the shipped seed data, `env_required` is where 9 of 19 entries (postgres, github, notion, obsidian, brave-search, tavily, home-assistant, n8n, grafana) actually declare their secret var names; `config.env` is essentially unused for that purpose. Clicking Add on any of these would have silently produced a server with **no env field at all** for the user to fill in. Fixed to seed `env_required` keys as empty-string placeholders unless `config.env` already sets them (never overwrites a real value, still never ships a real secret — `validate_catalog` already enforces `env_required` values must be `""`). **`bcc.py`**: - `BrowseCatalogDialog` — search box, category chips, list (display/description/category/★/✓ official), detail pane (description, notes, homepage link, freshness hint, and the **exact command rendered verbatim**). Per-tier action: `Add` (basic) routes through the existing `_import_server` path so the duplicate-name prompt (#8) applies for free; `Open setup docs` (link-only) calls `QDesktopServices.openUrl` and never auto-adds, and doesn't close the dialog. - **Rendering safety**: every catalog-derived string goes through a `plain_label()` helper (`Qt.PlainText` + `html.escape`, matching `catalog_console.py`'s existing approach) or an inherently-plain `QPlainTextEdit` for the command preview — `catalog.json` takes community PRs, so every field is attacker-influenceable even though it's signed. - "Browse catalog…" button next to Paste JSON… - `ServerEditor.focus_target()` — selects the first unfilled arg line, or opens the env-table cell editor on the first blank env row, after a catalog Add. - Save-time placeholder guard: warns (Yes/No, defaults No) if any server still has an unfilled `<PLACEHOLDER>` — never blocks a deliberate save, never lets one slip through unnoticed. **`bcc.spec`**: added `data/catalog.json.sig` to `datas` — it was missing, so a frozen build would have shipped a `catalog.json` with no matching `.sig` for `resolve_catalog()` to verify against, meaning the bundled catalog would always fail signature verification in a real build. ## Tests +82 tests in `tests/test_core.py`: category-group mapping (all 19 taxonomy values + unknown fallback + case-insensitivity), catalog search/filter, freshness-hint formatting (including the 23-vs-24-month rounding boundary), `first_unfilled_focus_target`, the `env_required` fix (incl. a regression against the real shipped `postgres` entry), and `load_bundled_catalog_entries` under valid/tampered-byte/wrong-key/missing-file/invalid-but-signed conditions, plus an end-to-end check against the real `data/catalog.json` + `.sig` (19 entries, signature verifies). **Status: 374 passed, 1 skipped (pre-existing). `ruff check .` and `ruff format --check .` both clean.** ## What I could not verify PySide6 cannot import in this build sandbox (`ImportError: libEGL.so.1`, no system GL libs, no root to install them). So `BrowseCatalogDialog`, `ServerEditor.focus_target`, and `MainWindow.browse_catalog` are **not exercised at runtime** — only `py_compile`/`ast.parse`-checked and reviewed by hand. All decision logic they depend on (filtering, grouping, freshness, focus-target selection, catalog loading) is pushed into `bcc_core.py` and is unit-tested. Please eyeball the dialog live before shipping, especially: - The two-line `QListWidgetItem` row text (`display\ndescription · group`) — I couldn't confirm Qt renders the embedded `\n` the way I expect across platforms; worth a visual check. - `ServerEditor.focus_target`'s `QTextCursor` line-selection for the args page, and the `env` table's `editItem()` call opening the inline editor as expected. - Overall dialog sizing/splitter proportions (880×560, list 360 / detail 480). ## Out of scope (per the task) Remote fetch/cache (#61 follow-up), and no changes to `catalog_console.py`, `catalog_review.py`, `scripts/`, workflows, or `data/catalog.json` itself.
the_og added 1 commit 2026-07-12 19:01:24 -04:00
Browse catalog dialog (issue #10 phase 2)
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 23s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 7s
dffa0e152f
Adds the user-facing half of the server catalog: a searchable, signed
catalog browser wired to the existing paste/import path.

bcc_core.py (pure, GUI-free, per the design comment on #10):
- CATALOG_CATEGORY_GROUPS / catalog_category_group(): collapses the
  raw taxonomy to the 7 UI chips (unknown categories fall back to
  Other, never drop an entry).
- catalog_entry_matches_query() / filter_catalog_entries() /
  catalog_entries_in_group(): search-as-you-type + category chip
  filtering over catalog entries.
- format_freshness_hint(): last_release ISO date -> "Last updated N
  months/years ago", '' when missing/unparseable/future.
- first_unfilled_focus_target(): finds the first <PLACEHOLDER> arg or
  blank env_required key so the GUI can focus it after Add.
- load_bundled_catalog_entries(): reads+verifies+validates the
  bundled catalog.json/.sig pair, returns servers or an EMPTY list on
  ANY failure -- the load-bearing guarantee behind the dialog's empty
  state.
- Bug fix: catalog_entry_to_paste_json() only copied config.env,
  ignoring env_required -- the field where 9 of the 19 seed entries
  (postgres, github, notion, obsidian, brave-search, tavily,
  home-assistant, n8n, grafana) actually declare their secret var
  names. Add would have silently added these servers with no env
  field for the user to fill in. Now env_required keys are seeded as
  empty-string placeholders unless config.env already sets them.

bcc.py:
- BrowseCatalogDialog: search box, category chips, list, detail pane
  (description/notes/homepage/freshness hint/verbatim command
  preview), per-tier action (Add for basic, Open setup docs for
  link-only). Every catalog-derived string goes through plain_label()
  (Qt.PlainText + html.escape, matching catalog_console.py's
  approach) or an inherently-plain QPlainTextEdit for the command
  preview -- catalog.json takes community PRs, so every field is
  attacker-influenceable.
- "Browse catalog…" button next to Paste JSON.
- ServerEditor.focus_target(): selects the first unfilled arg line or
  opens the env-table cell editor for the first blank env row.
- Save-time placeholder guard: warns (Yes/No, defaults No) when any
  server still has an unfilled <PLACEHOLDER>; never blocks a
  deliberate save, never saves one unnoticed.

bcc.spec: bundle data/catalog.json.sig alongside catalog.json -- the
signature file was missing from datas, so a frozen build would have
had a catalog.json with no matching .sig for resolve_catalog() to
verify against.

tests/test_core.py: +82 tests covering category-group mapping,
catalog search/filter, freshness-hint formatting (including the
month/year rounding boundary), first_unfilled_focus_target, the
catalog_entry_to_paste_json env_required fix (incl. a regression
against the real shipped postgres entry), and
load_bundled_catalog_entries under valid/tampered/wrong-key/missing-
file/invalid-but-signed conditions plus an end-to-end check against
the real data/catalog.json + .sig (19 entries).

GUI code (BrowseCatalogDialog, ServerEditor.focus_target,
MainWindow.browse_catalog) is unavoidably untested here -- PySide6
cannot import in this sandbox (missing libEGL/system GL libs) -- but
all the logic it depends on is pushed into bcc_core.py and covered
above.
the_og added 1 commit 2026-07-12 21:30:22 -04:00
Merge branch 'main' into feat/10-browse-dialog
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 23s
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 7s
cfe05b4324
Some checks are pending
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 23s
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 7s
This pull request has changes conflicting with the target branch.
  • bcc.py
  • tests/test_core.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/10-browse-dialog:feat/10-browse-dialog
git checkout feat/10-browse-dialog
Sign in to join this conversation.