fix(admin): replace the profiles card grid with a list + detail pane #58

Merged
alee merged 2 commits from fix/admin-profiles-header-cleanup into main 2026-09-08 03:56:50 +00:00
Owner

Why

The card shape was the problem, not the styling on it. A ~340px card had to hold a name, an icon, admitted/interactive counts, a source badge, a read-only hint and up to three action buttons. Three rounds of polish (#56 plus two follow-ups) each moved the collision somewhere else rather than removing it.

A list plus a detail pane removes the constraint: the list carries one line per profile, the pane has the full width, nothing competes for the same row.

What

  • Master list -- per-filter icon, name, and a muted one-line summary derived from the same precedence profileIcon() uses, so the icon and the line never describe two different things. Zero-admit rows keep a warning flag; built-ins show a lock, which now reads as contrast against config profiles rather than noise on every row.
  • Detail pane -- name, source and read-only state, the counts as labelled figures rather than headline numbers (what distinguishes a profile is its filters, not how many models it admits), the full filter set as aligned rows, the category-coverage line when gated, and the actions in one place.
  • Long keys no longer split mid-word -- breakableLabel() adds <wbr> after each underscore plus a fixed-width label column, so max_cost_per_1m_completion fits on one line instead of wrapping to max_cost_per_1m_completio / n.
  • 4 mojibake sequences fixed (a triple-encoded em-dash that shipped in #56) via byte-level replacement; this file's own punctuation is now ASCII.

What is untouched

All CRUD is reused as-is: both modals, openEditModal, openDuplicateModal, promptDelete, saveProfile, collectProfileBody. The const actions = isBuiltin ? ... ternary keeps its exact shape, so the built-in duplicate-only rule and the test pinning it are unchanged. Editing still goes through the existing modal -- inline editing in the pane is a clean follow-up, deliberately not bundled here.

Verification

Live against the real catalog on a throwaway 8081 instance (never 8080):

  • Selection switches the pane; state survives SSE refreshes.
  • Duplicate modal opens pre-filled from a real click through the delegated listener (Duplicate default / default-copy).
  • Config-profile branch renders all three actions; built-ins render duplicate only.
  • max_cost_per_1m_completion on one line.

Full suite: 1521 passed. Note the 9 failures seen on other branches do not occur here -- this worktree has no config.local.yaml overlay, confirming they are overlay artifacts (plans/pending-ops-fixes.md #5), not real failures.

## Why The card shape was the problem, not the styling on it. A ~340px card had to hold a name, an icon, admitted/interactive counts, a source badge, a read-only hint and up to three action buttons. Three rounds of polish (#56 plus two follow-ups) each moved the collision somewhere else rather than removing it. A list plus a detail pane removes the constraint: the list carries one line per profile, the pane has the full width, nothing competes for the same row. ## What - **Master list** -- per-filter icon, name, and a muted one-line summary derived from the same precedence `profileIcon()` uses, so the icon and the line never describe two different things. Zero-admit rows keep a warning flag; built-ins show a lock, which now reads as contrast against config profiles rather than noise on every row. - **Detail pane** -- name, source and read-only state, the counts as labelled figures rather than headline numbers (what distinguishes a profile is its filters, not how many models it admits), the full filter set as aligned rows, the category-coverage line when gated, and the actions in one place. - **Long keys no longer split mid-word** -- `breakableLabel()` adds `<wbr>` after each underscore plus a fixed-width label column, so `max_cost_per_1m_completion` fits on one line instead of wrapping to `max_cost_per_1m_completio` / `n`. - **4 mojibake sequences fixed** (a triple-encoded em-dash that shipped in #56) via byte-level replacement; this file's own punctuation is now ASCII. ## What is untouched All CRUD is reused as-is: both modals, `openEditModal`, `openDuplicateModal`, `promptDelete`, `saveProfile`, `collectProfileBody`. The `const actions = isBuiltin ? ...` ternary keeps its exact shape, so the built-in duplicate-only rule and the test pinning it are unchanged. Editing still goes through the existing modal -- inline editing in the pane is a clean follow-up, deliberately not bundled here. ## Verification Live against the real catalog on a throwaway 8081 instance (never 8080): - Selection switches the pane; state survives SSE refreshes. - Duplicate modal opens pre-filled from a **real click** through the delegated listener (`Duplicate default` / `default-copy`). - Config-profile branch renders all three actions; built-ins render duplicate only. - `max_cost_per_1m_completion` on one line. Full suite: **1521 passed**. Note the 9 failures seen on other branches do not occur here -- this worktree has no `config.local.yaml` overlay, confirming they are overlay artifacts (`plans/pending-ops-fixes.md` #5), not real failures.
alee added 1 commit 2026-09-08 03:46:18 +00:00
The card shape was the problem, not the styling on it. A ~340px card had to
hold a name, an icon, admitted/interactive counts, a source badge, a
read-only hint and up to three action buttons, and no amount of spacing kept
those six from colliding -- three rounds of polish (#56 plus two follow-ups)
each moved the collision somewhere else rather than removing it.

A list plus a detail pane removes the constraint instead of working around
it: the list carries one line per profile, and the pane has the full width,
so nothing competes for the same row.

- Master list: per-filter icon, name, and a muted one-line summary derived
  from the same precedence profileIcon() uses, so the icon and the line
  never describe two different things. Zero-admit rows keep a warning flag;
  built-ins show a lock, which now reads as contrast against config
  profiles rather than noise on every row.
- Detail pane: name, source and read-only state, the counts as labelled
  figures rather than headline numbers (what distinguishes a profile is its
  filters, not how many models it happens to admit), the full filter set as
  aligned rows, the category-coverage line when gated, and the actions in
  one consistent place.
- Long snake_case keys no longer split mid-word: breakableLabel() adds
  <wbr> after each underscore, and the label column is fixed-width, so
  max_cost_per_1m_completion fits on one line instead of wrapping to
  "max_cost_per_1m_completio" / "n".
- Fixed 4 mojibake sequences (a triple-encoded em-dash, shipped in #56)
  by byte-level replacement, and kept this file's own punctuation ASCII.

All CRUD is untouched and reused: both modals, openEditModal,
openDuplicateModal, promptDelete, saveProfile and the rest. The actions
ternary keeps its exact shape, so the built-in duplicate-only rule and the
test that pins it are unchanged.

Verified live against the real catalog on a throwaway 8081 instance:
selection, the duplicate modal opening pre-filled from a real click through
the delegated listener, and the config-profile branch rendering all three
actions. Full suite 1521 passed.
alee added 1 commit 2026-09-08 03:56:17 +00:00
Editing a profile no longer opens a dialog stacked on top of the record you
are reading -- the pane itself becomes the form. Duplicate and delete still
use their modals, because both act on a record other than the one on screen.

The modal's field set is reused rather than duplicated:
collectProfileBody() now takes an id prefix ("profile-" for the modal,
"inline-profile-" for the pane) and loadCatalogModels() takes a target
select, so both editors build the same request body and cannot drift apart.

Two things found while testing this against the live API:

- The update endpoint CANNOT rename. _ProfileUpdateBody has no name field
  and _profile_block_dict() does exclude={"name"}, so a submitted name is
  silently dropped and the profile keeps its old one. The inline name field
  is therefore readonly, and says why plus what to do instead (duplicate
  under the new name, then delete). The existing modal still presents an
  editable name box on edit that silently does nothing -- pre-existing, not
  touched here.
- Selection deliberately stays on the original name after a save rather
  than following the submitted one, which would land the pane on a profile
  that does not exist.

A refresh will not clobber a half-filled form: renderProfiles() re-renders
the list but leaves the pane alone while editing. Switching profiles
abandons the edit rather than carrying the form to another record.

Verified end to end against the real API on a throwaway 8081 instance,
writing to this worktree's own gitignored overlay -- confirmed by checksum
that the machine's real config.local.yaml was untouched throughout. Save
persisted latency_tolerance and max_cost changes, the restart hint appears,
and the scratch profile was deleted afterwards. Full suite 1521 passed.
alee merged commit 504ca7c6f7 into main 2026-09-08 03:56:50 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/6krrt#58