fix(admin): rebalance profiles page hierarchy and add per-profile icons #56

Merged
alee merged 1 commits from fix/admin-profiles-page-polish into main 2026-09-08 03:04:20 +00:00

View File

@@ -87,17 +87,26 @@ header.navbar{padding-top:2px!important;padding-bottom:2px!important}
.navbar-brand a:hover .brand-speedlines line{animation:none}
}
/* ── Profile card specifics ── */
.profile-stat{font-size:1.75rem;font-weight:700;line-height:1;font-variant-numeric:tabular-nums}
.profile-stat-label{font-size:.75rem;color:var(--tblr-secondary)}
.profile-field-label{color:var(--tblr-secondary);font-size:.75rem}
.profile-field-val{font-size:.78rem}
/* Admitted/interactive counts -- compact badge in card header */
.profile-admit-badge{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:.82rem}
/* Promoted definition rows -- larger/brighter than the old .78rem muted styling */
.profile-field-label{color:var(--tblr-secondary);font-size:.8rem;font-weight:600;letter-spacing:.01em}
.profile-field-val{color:rgba(255,255,255,.8);font-size:.92rem}
.profile-zero-alert{margin-left:auto}
.profile-coverage{font-size:.72rem;color:var(--tblr-secondary);margin-bottom:.75rem;line-height:1.35}
/* Coverage note -- compact key:value row */
.profile-coverage{font-size:.78rem;color:var(--tblr-secondary);margin-bottom:.5rem;line-height:1.3}
.profile-coverage .profile-field-label{font-size:.76rem;font-weight:500;letter-spacing:normal}
.profile-coverage .profile-field-val{font-size:.8rem;color:var(--tblr-secondary)}
.profile-coverage-info{display:inline-flex;align-items:center;margin-left:4px;opacity:.6;cursor:help;vertical-align:middle}
.profile-coverage-info:hover{opacity:1}
.empty{color:var(--tblr-secondary);text-align:center;padding:2rem 1rem}
/* ── Read-only lock hint on built-in cards ── */
.profile-readonly-hint{color:var(--tblr-secondary);font-size:.72rem;display:inline-flex;align-items:center;gap:4px}
/* ── Card header actions ── */
.profile-actions{display:flex;align-items:center;gap:6px}
/* ── Profile grid: CSS auto-fill so 5 cards don't leave orphans ── */
#profile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1rem;max-width:1200px;margin-left:auto!important;margin-right:auto!important}
#profile-grid>.col-12{grid-column:1/-1}
/* ── Modal glass treatment ── */
.modal-content{
background: rgba(31,41,55,.75);
@@ -317,10 +326,31 @@ function icon(name, size) {
trash: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
'map-pin': `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
'dollar-sign': `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>`,
shield: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
};
return svgs[name] || svgs.default;
}
/* ── Select a per-profile card-header icon based on definition filters.
Precedence (first match wins):
1. latency_tolerance set -> clock (time/latency)
2. max_cost_per_1m_completion set -> dollar-sign (cost)
3. min_tier / max_tier set -> shield (tier/scope)
4. provider set OR category-gated -> map-pin (provider/locality)
5. no non-null filters -> profile (generic default) ── */
function profileIcon(def, cov) {
def = def || {};
if (def.latency_tolerance != null) return 'clock';
if (def.max_cost_per_1m_completion != null) return 'dollar-sign';
if (def.min_tier != null || def.max_tier != null) return 'shield';
if (def.provider != null) return 'map-pin';
if (cov && cov.admitting > 0 && cov.admitting < cov.total) return 'map-pin';
return 'profile';
}
function renderStaticIcons() {
document.querySelectorAll('[data-icon]').forEach(el => {
el.innerHTML = icon(el.dataset.icon);
@@ -475,16 +505,19 @@ function renderProfiles(profiles) {
const headlineInteractive = gated ? Number(cov.max_interactive_admitted || 0) : interactive;
const headlineModels = gated ? cov.models : p.admitted_models;
const admitBadge = `<span class="badge bg-secondary profile-admit-badge" title="${escapeHtml(tooltipForAdmitted(headlineModels))}">${headline.toLocaleString()} admitted &middot; ${headlineInteractive.toLocaleString()} interactive</span>`;
const zeroBadge = zeroAdmit
? `<span class="badge bg-warning profile-zero-alert" title="This profile admits zero models for every task category under the live catalog">admits 0 models</span>`
: '';
const coverageNote = gated
? `<div class="profile-coverage">
Category-gated: admits ${headline.toLocaleString()}
${headline === 1 ? 'model' : 'models'} for
${cov.admitting} of ${cov.total} task categories.
<span class="text-muted">${admitted.toLocaleString()} for an unspecified category &mdash;
its models name the categories they serve, so a category-less probe admits none of them.</span>
? `<div class="profile-coverage row g-0 mb-1">
<div class="col-5 profile-field-label">categories</div>
<div class="col-7 profile-field-val">${cov.admitting} of ${cov.total}
<span class="profile-coverage-info" title="Category-gated: admits ${headline.toLocaleString()} ${headline === 1 ? 'model' : 'models'} for ${cov.admitting} of ${cov.total} task categories. ${admitted.toLocaleString()} for an unspecified category &mdash; its models name the categories they serve, so a category-less probe admits none of them.">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</span>
</div>
</div>`
: '';
const readonlyHint = isBuiltin
@@ -502,15 +535,16 @@ function renderProfiles(profiles) {
<button class="btn btn-outline-secondary btn-sm" title="Edit profile" data-action="edit" data-profile-name="${name}"><span data-icon="edit"></span></button>
<button class="btn btn-danger btn-sm" title="Delete profile" data-action="delete" data-profile-name="${name}"><span data-icon="trash"></span></button>
</div>`;
return `<div class="col-md-6 col-xl-4" data-profile-name="${name}">
const cardIcon = profileIcon(p.definition, cov);
return `<div data-profile-name="${name}">
<div class="card h-100">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<span data-icon="profile"></span>
<span data-icon="${cardIcon}"></span>
<h3 class="card-title mb-0">${escapeHtml(String(p.name || '—'))}</h3>
</div>
${zeroBadge}
<div class="d-flex align-items-center gap-2">${admitBadge}${zeroBadge}</div>
</div>
<div class="d-flex align-items-center justify-content-between mt-1">
<div class="card-subtitle text-muted">${sourceBadge(p.source)} ${readonlyHint}</div>
@@ -518,18 +552,8 @@ function renderProfiles(profiles) {
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-2">
<div class="col-6">
<div class="profile-stat" title="${escapeHtml(tooltipForAdmitted(headlineModels))}">${headline.toLocaleString()}</div>
<div class="profile-stat-label">admitted models</div>
</div>
<div class="col-6">
<div class="profile-stat">${headlineInteractive.toLocaleString()}</div>
<div class="profile-stat-label">interactive models</div>
</div>
</div>
${coverageNote}
${formatDefinition(p.definition)}
${coverageNote}
</div>
</div>
</div>`;