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.
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.