Skip to content

Commit ee2315e

Browse files
author
Alexey Panfilov
committed
refactor(admin): adopt dzarlax DS components (tab-nav, navbar, kpi-bar, divider)
The admin UI now uses first-class design-system components instead of locally-redefined equivalents. Any visual regressions get fixed in DS once, propagate everywhere. Swaps: - .admin-tabs / .admin-tabs__tab (underline variant) → .tab-nav / .tab-nav a.active (DS background-highlight variant) - .admin-header / .admin-header__brand → .navbar / .navbar__brand (sticky top bar with blur, same across book/health/news sites) - .usage-grid / .usage-card* (analytics cards) → .kpi-bar / .kpi-item* - .chat-break / .chat-break__label → .divider--labeled / .divider__label Also pinned DS_VERSION to the commit hash that introduces the divider component and the [hidden] / input-font-size base rules, so jsdelivr's @main cache can't serve a stale bundle missing those pieces. Dropped local CSS: admin-tabs, admin-header, usage-grid/card, chat-break, the [hidden] !important override, the input font-size rule, and the mobile admin-tabs block — all live in DS now.
1 parent 696c981 commit ee2315e

4 files changed

Lines changed: 72 additions & 97 deletions

File tree

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ ARG TARGETARCH
88
#
99
# Override for a one-off local refresh:
1010
# docker build --build-arg ASSETS_CACHEBUST=$(date +%s) .
11-
ARG DS_VERSION=main
11+
# Pin to a commit SHA rather than @main so jsdelivr's aggressive branch-tip
12+
# caching can't serve a stale bundle for hours after a DS push. Bump this
13+
# when you want the latest DS changes; confirm the hash exists at
14+
# github.com/dzarlax/design-system before committing.
15+
ARG DS_VERSION=f36e79e50e9341ef5780fb89ec41c1f49e447811
1216
ARG HTMX_VERSION=2.0.3
1317
ARG ASSETS_CACHEBUST=pinned
1418

internal/adminapi/templates/chat.html

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,9 @@
7070
/* Drag-over visual cue on the whole chat wrap. */
7171
.chat-wrap.drop-active { outline: 2px dashed var(--accent, #3b82f6); outline-offset: -4px; }
7272

73-
/* Session break divider — rendered between groups of messages separated
74-
by the "New session" button or the 4-hour idle reset. Flat horizontal
75-
line with a small date pill centred on it. */
76-
.chat-break { display: flex; align-items: center; gap: 0.6rem; margin: 0.75rem 0.25rem; opacity: 0.65; align-self: stretch; }
77-
.chat-break::before, .chat-break::after { content: ''; flex: 1; height: 1px; background: rgba(0,0,0,0.15); }
78-
.chat-break__label { font-size: 0.72em; padding: 0.1em 0.6em; border-radius: 999px; background: rgba(0,0,0,0.06); white-space: nowrap; }
73+
/* Session breaks use the DS .divider--labeled component. The only local
74+
touch is align-self: stretch so the divider spans the flex column. */
75+
.chat-messages .divider--labeled { align-self: stretch; margin: 0.75rem 0.25rem; }
7976

8077
/* "+ New session" link-style button placed just above the input — visually
8178
a divider you can create yourself. Muted so it doesn't compete with Send. */
@@ -110,7 +107,7 @@
110107
<div id="chat-messages" class="chat-messages">
111108
{{range .History}}
112109
{{if eq .Role "break"}}
113-
<div class="chat-break"><span class="chat-break__label">{{.BreakDate}}{{if .Body}} &middot; {{.Body}}{{end}}</span></div>
110+
<div class="divider--labeled"><span class="divider__label">{{.BreakDate}}{{if .Body}} &middot; {{.Body}}{{end}}</span></div>
114111
{{else}}
115112
<div class="chat-msg chat-msg--{{.Role}}">
116113
<div class="chat-msg__meta">
@@ -608,9 +605,9 @@
608605
var now = new Date();
609606
var stamp = now.toISOString().slice(0,10) + ' ' + now.toTimeString().slice(0,5);
610607
var div = document.createElement('div');
611-
div.className = 'chat-break';
612-
div.innerHTML = '<span class="chat-break__label"></span>';
613-
div.querySelector('.chat-break__label').textContent = stamp + ' \u00B7 Cleared';
608+
div.className = 'divider--labeled';
609+
div.innerHTML = '<span class="divider__label"></span>';
610+
div.querySelector('.divider__label').textContent = stamp + ' \u00B7 Cleared';
614611
messagesEl.appendChild(div);
615612
scrollToBottom();
616613
updateLastActions();

internal/adminapi/templates/partials_layout.html

Lines changed: 35 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
<script src="/static/dzarlax.js" defer></script>
66
<script src="/static/htmx.min.js" defer></script>
77
<style>
8-
body { padding: 1.5rem 2rem; margin: 0; max-width: none; }
9-
h1 { margin: 0; }
10-
.admin-header { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 0.75rem; }
11-
.admin-header__brand { font-size: 1.35rem; font-weight: 600; letter-spacing: -0.01em; }
12-
.admin-tabs { display: flex; gap: 0.25rem; margin-bottom: 1.5rem; border-bottom: 1px solid rgba(0,0,0,0.1); }
13-
.admin-tabs__tab { padding: 0.5rem 1rem; color: var(--text-secondary, rgba(0,0,0,0.6)); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
14-
.admin-tabs__tab:hover { color: var(--text-primary, #000); }
15-
.admin-tabs__tab--active { color: var(--text-primary, #000); border-bottom-color: var(--accent, #3b82f6); font-weight: 600; }
8+
/* Admin UI overrides on top of the dzarlax design system. Anything that
9+
duplicates DS is deliberately absent — we rely on DS components for:
10+
• .navbar / .navbar__brand (page header)
11+
• .tab-nav / .tab-nav a.active (page tabs)
12+
• .kpi-bar / .kpi-item (metric row on Analytics)
13+
• .divider--labeled / .divider__label (chat session breaks)
14+
• .card / .card__body / .card__header (content panels)
15+
• [hidden] !important + input 16px (iOS zoom fix)
16+
Admin pages are full-width — design system navbar/container padding is
17+
intentionally bypassed so data-dense tables use every pixel. */
18+
body { padding: 0 1.5rem 2rem; margin: 0; max-width: none; }
19+
1620
.admin-section { margin-bottom: 2rem; }
1721
.filters { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-bottom: 1rem; }
1822
.filters label { display: inline-flex; gap: 0.25rem; align-items: center; }
@@ -21,75 +25,45 @@
2125
.assign-buttons { margin-top: 0.25rem; }
2226
.assign-buttons .btn { margin-right: 0.25rem; margin-bottom: 0.25rem; }
2327
.price-cell { white-space: nowrap; font-variant-numeric: tabular-nums; }
24-
.preset-banner { padding: 0.75rem 1rem; border-radius: var(--radius, 8px); background: var(--surface-2, #E8E6E3); margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
25-
.preset-banner .preset-meta { color: var(--text-secondary, rgba(0,0,0,0.6)); font-size: 0.875rem; margin-top: 0.25rem; }
28+
.preset-banner { padding: 0.75rem 1rem; border-radius: var(--radius); background: var(--surface-2); margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
29+
.preset-banner .preset-meta { color: var(--text-secondary); font-size: 0.875rem; margin-top: 0.25rem; }
2630
.preset-banner details { margin-top: 0.5rem; font-size: 0.85em; }
27-
.preset-banner details > summary { cursor: pointer; color: var(--text-secondary, rgba(0,0,0,0.6)); user-select: none; list-style: none; }
31+
.preset-banner details > summary { cursor: pointer; color: var(--text-secondary); user-select: none; list-style: none; }
2832
.preset-banner details > summary::-webkit-details-marker { display: none; }
2933
.preset-banner details > summary::before { content: "▸ "; display: inline-block; transition: transform 0.15s; }
3034
.preset-banner details[open] > summary::before { content: "▾ "; }
31-
.preset-banner details .formula { margin-top: 0.5rem; padding: 0.5rem 0.75rem; background: rgba(0,0,0,0.04); border-left: 3px solid var(--accent, #888); border-radius: 4px; line-height: 1.45; font-size: 0.9em; }
35+
.preset-banner details .formula { margin-top: 0.5rem; padding: 0.5rem 0.75rem; background: rgba(0,0,0,0.04); border-left: 3px solid var(--accent); border-radius: 4px; line-height: 1.45; font-size: 0.9em; }
3236
.preset-banner details .formula code { background: rgba(0,0,0,0.06); padding: 0 0.25rem; border-radius: 3px; }
3337
.suggest-btn { margin-left: 0.5rem; }
3438
.table-wrap { overflow-x: auto; }
3539
.table-wrap table { width: 100%; }
3640
.usage-toolbar { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
37-
.usage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; margin-bottom: 1.25rem; }
38-
.usage-card { padding: 0.75rem 1rem; border-radius: var(--radius, 8px); background: var(--surface-2, #E8E6E3); }
39-
.usage-card__label { font-size: 0.8em; color: var(--text-secondary, rgba(0,0,0,0.6)); text-transform: uppercase; letter-spacing: 0.05em; }
40-
.usage-card__value { font-size: 1.5em; font-weight: 600; font-variant-numeric: tabular-nums; margin-top: 0.25rem; }
41-
.usage-card__sub { font-size: 0.8em; color: var(--text-secondary, rgba(0,0,0,0.6)); margin-top: 0.25rem; }
42-
.usage-chart-wrap { margin-bottom: 1.5rem; padding: 0.75rem; background: rgba(0,0,0,0.03); border-radius: var(--radius, 8px); max-width: 640px; }
43-
.usage-chart-title { font-size: 0.85em; margin-bottom: 0.5rem; color: var(--text-secondary, rgba(0,0,0,0.7)); }
41+
.usage-chart-wrap { margin-bottom: 1.5rem; padding: 0.75rem; background: rgba(0,0,0,0.03); border-radius: var(--radius); max-width: 640px; }
42+
.usage-chart-title { font-size: 0.85em; margin-bottom: 0.5rem; color: var(--text-secondary); }
4443
.usage-chart { width: 100%; height: auto; display: block; max-height: 140px; }
4544
.usage-chart + .usage-chart { margin-top: 0.25rem; }
4645
.usage-chart rect:hover { opacity: 1 !important; }
4746
.usage-section-h { font-size: 0.95em; margin-top: 1.5rem; margin-bottom: 0.5rem; font-weight: 600; }
4847
.usage-turns td { vertical-align: top; }
49-
.usage-turn__q { color: var(--text-primary, #000); margin-bottom: 0.25rem; font-size: 0.9em; }
50-
.usage-turn__a { color: var(--text-secondary, rgba(0,0,0,0.7)); font-size: 0.9em; }
51-
52-
/* Form input sizing — 16px minimum on iOS stops the page zooming in on
53-
focus. Applied generously so every tab's inputs behave the same. */
54-
input, select, textarea { font-size: max(16px, 0.95rem); }
55-
56-
/* The dzarlax design-system assigns display: inline-flex to .btn, which
57-
outranks the browser's default [hidden]{display:none}. Without this,
58-
hidden toggles on buttons (e.g. Stop in the chat form) do nothing. */
59-
[hidden] { display: none !important; }
48+
.usage-turn__q { color: var(--text); margin-bottom: 0.25rem; font-size: 0.9em; }
49+
.usage-turn__a { color: var(--text-secondary); font-size: 0.9em; }
6050

61-
/* Responsive base — tuned for phone portrait (<=640). Tabs with many
62-
entries become horizontally scrollable; body padding shrinks so
63-
content uses the full viewport. Tables inherit scrolling via
64-
.table-wrap (already wrapped where needed). */
6551
@media (max-width: 640px) {
66-
body { padding: 0.75rem 0.6rem; }
67-
h1 { font-size: 1.35rem; margin-bottom: 0.5rem; }
68-
.admin-tabs { overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; margin-bottom: 1rem; }
69-
.admin-tabs::-webkit-scrollbar { height: 3px; }
70-
.admin-tabs__tab { white-space: nowrap; padding: 0.45rem 0.7rem; font-size: 0.92em; }
52+
body { padding: 0 0.75rem 1.5rem; }
7153
.admin-section { margin-bottom: 1.25rem; }
7254
.filters { gap: 0.5rem; }
7355
.filters input[type="text"], .filters input[type="search"] { flex: 1 1 100%; min-width: 0; }
74-
.usage-grid { gap: 0.5rem; }
75-
.usage-card { padding: 0.6rem 0.75rem; }
76-
.usage-card__value { font-size: 1.25em; }
77-
.usage-card__label { font-size: 0.72em; }
7856
.usage-chart-wrap { padding: 0.5rem; }
7957
.preset-banner { padding: 0.6rem 0.75rem; flex-direction: column; align-items: flex-start; }
80-
/* Min 40px touch target on primary actions. */
58+
/* Min 40px touch target. */
8159
.btn { min-height: 40px; }
82-
/* Tables that didn't opt into .table-wrap still scroll safely. */
60+
/* Tables that didn't opt into .table-wrap scroll safely. */
8361
table { display: block; overflow-x: auto; max-width: 100%; }
84-
85-
/* Settings form: stack label/input/button vertically. Same grid but
86-
each cell full-width. Save button becomes full-width action too. */
62+
/* Settings form stacks label/input/button vertically. */
8763
.settings-row { grid-template-columns: 1fr; gap: 0.4rem; padding: 0.6rem 0; }
8864
.settings-row > :nth-child(2) { width: 100%; }
8965
.settings-input { width: 100% !important; }
90-
91-
/* MCP server cards: stack label above field so 9rem labels don't
92-
squeeze mobile inputs; tighter padding. */
66+
/* MCP server cards: labels become small captions above fields. */
9367
.mcp-card { padding: 0.75rem; }
9468
.mcp-field { grid-template-columns: 1fr; gap: 0.2rem; }
9569
.mcp-field label { padding-top: 0; font-size: 0.78em; text-transform: uppercase; letter-spacing: 0.03em; }
@@ -98,19 +72,19 @@
9872
{{end}}
9973

10074
{{define "admin_header"}}
101-
<header class="admin-header">
102-
<h1 class="admin-header__brand">Personal Assistant</h1>
103-
</header>
75+
<nav class="navbar">
76+
<a class="navbar__brand" href="/">Personal Assistant</a>
77+
</nav>
10478
{{template "admin_tabs" .}}
10579
{{end}}
10680

10781
{{define "admin_tabs"}}
108-
<nav class="admin-tabs">
109-
<a class="admin-tabs__tab {{if eq .ActiveTab "chat"}}admin-tabs__tab--active{{end}}" href="/chat">Chat</a>
110-
<a class="admin-tabs__tab {{if eq .ActiveTab "routing"}}admin-tabs__tab--active{{end}}" href="/">Routing & Models</a>
111-
<a class="admin-tabs__tab {{if eq .ActiveTab "analytics"}}admin-tabs__tab--active{{end}}" href="/analytics">Analytics</a>
112-
<a class="admin-tabs__tab {{if eq .ActiveTab "prompts"}}admin-tabs__tab--active{{end}}" href="/prompts">Prompts</a>
113-
<a class="admin-tabs__tab {{if eq .ActiveTab "mcp"}}admin-tabs__tab--active{{end}}" href="/mcp">MCP</a>
114-
<a class="admin-tabs__tab {{if eq .ActiveTab "settings"}}admin-tabs__tab--active{{end}}" href="/settings">Settings</a>
82+
<nav class="tab-nav">
83+
<a class="{{if eq .ActiveTab "chat"}}active{{end}}" href="/chat">Chat</a>
84+
<a class="{{if eq .ActiveTab "routing"}}active{{end}}" href="/">Routing & Models</a>
85+
<a class="{{if eq .ActiveTab "analytics"}}active{{end}}" href="/analytics">Analytics</a>
86+
<a class="{{if eq .ActiveTab "prompts"}}active{{end}}" href="/prompts">Prompts</a>
87+
<a class="{{if eq .ActiveTab "mcp"}}active{{end}}" href="/mcp">MCP</a>
88+
<a class="{{if eq .ActiveTab "settings"}}active{{end}}" href="/settings">Settings</a>
11589
</nav>
11690
{{end}}

internal/adminapi/templates/usage.html

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,44 @@
1313
</div>
1414

1515
<!-- Headline cards -->
16-
<div class="usage-grid">
17-
<div class="usage-card" title="Sum of input + output tokens over the active window">
18-
<div class="usage-card__label">Tokens ({{.Period}})</div>
19-
<div class="usage-card__value">{{tokenShort (add .Totals.PromptTokens .Totals.CompletionTokens)}}</div>
20-
<div class="usage-card__sub">
16+
<div class="kpi-bar">
17+
<div class="kpi-item" title="Sum of input + output tokens over the active window">
18+
<div class="kpi-item__label">Tokens ({{.Period}})</div>
19+
<div class="kpi-item__value">{{tokenShort (add .Totals.PromptTokens .Totals.CompletionTokens)}}</div>
20+
<div class="kpi-item__note">
2121
{{tokenShort .Totals.PromptTokens}} in / {{tokenShort .Totals.CompletionTokens}} out
2222
</div>
2323
</div>
24-
<div class="usage-card" title="LLM calls (including classifier, compaction, fallback retries)">
25-
<div class="usage-card__label">Calls ({{.Period}})</div>
26-
<div class="usage-card__value">{{intFmt .Totals.Calls}}</div>
27-
<div class="usage-card__sub">
24+
<div class="kpi-item" title="LLM calls (including classifier, compaction, fallback retries)">
25+
<div class="kpi-item__label">Calls ({{.Period}})</div>
26+
<div class="kpi-item__value">{{intFmt .Totals.Calls}}</div>
27+
<div class="kpi-item__note">
2828
{{if gt .Totals.ErrorCount 0}}{{.Totals.ErrorCount}} errors{{else}}all ok{{end}}
2929
</div>
3030
</div>
31-
<div class="usage-card" title="Authoritative USD reported by OpenRouter (usage.cost). Claude bridge reports total_cost_usd. Other providers don't report cost.">
32-
<div class="usage-card__label">Cost ({{.Period}})</div>
33-
<div class="usage-card__value">{{priceFmt .Totals.CostUSD}}</div>
34-
<div class="usage-card__sub">
31+
<div class="kpi-item" title="Authoritative USD reported by OpenRouter (usage.cost). Claude bridge reports total_cost_usd. Other providers don't report cost.">
32+
<div class="kpi-item__label">Cost ({{.Period}})</div>
33+
<div class="kpi-item__value">{{priceFmt .Totals.CostUSD}}</div>
34+
<div class="kpi-item__note">
3535
24h: {{priceFmt .TotalsToday.CostUSD}} · 7d: {{priceFmt .Totals7d.CostUSD}}
3636
</div>
3737
</div>
38-
<div class="usage-card" title="Projected monthly spend = 7d cost × (30/7)">
39-
<div class="usage-card__label">Forecast 30d</div>
40-
<div class="usage-card__value">{{priceFmt .ForecastUSD}}</div>
41-
<div class="usage-card__sub">from 7d trailing</div>
38+
<div class="kpi-item" title="Projected monthly spend = 7d cost × (30/7)">
39+
<div class="kpi-item__label">Forecast 30d</div>
40+
<div class="kpi-item__value">{{priceFmt .ForecastUSD}}</div>
41+
<div class="kpi-item__note">from 7d trailing</div>
4242
</div>
43-
<div class="usage-card" title="Fraction of prompt tokens that hit the provider's cache (Anthropic / Gemini / OpenAI prompt caching)">
44-
<div class="usage-card__label">Cache hit</div>
45-
<div class="usage-card__value">{{printf "%.1f%%" .CacheHitPct}}</div>
46-
<div class="usage-card__sub">
43+
<div class="kpi-item" title="Fraction of prompt tokens that hit the provider's cache (Anthropic / Gemini / OpenAI prompt caching)">
44+
<div class="kpi-item__label">Cache hit</div>
45+
<div class="kpi-item__value">{{printf "%.1f%%" .CacheHitPct}}</div>
46+
<div class="kpi-item__note">
4747
{{tokenShort .Totals.CachedPromptTokens}} cached
4848
</div>
4949
</div>
50-
<div class="usage-card" title="Fraction of completion tokens that went into internal reasoning (thinking models). Billed as completion but hidden from the user.">
51-
<div class="usage-card__label">Reasoning overhead</div>
52-
<div class="usage-card__value">{{printf "%.1f%%" .ReasoningPct}}</div>
53-
<div class="usage-card__sub">
50+
<div class="kpi-item" title="Fraction of completion tokens that went into internal reasoning (thinking models). Billed as completion but hidden from the user.">
51+
<div class="kpi-item__label">Reasoning overhead</div>
52+
<div class="kpi-item__value">{{printf "%.1f%%" .ReasoningPct}}</div>
53+
<div class="kpi-item__note">
5454
{{tokenShort .Totals.ReasoningTokens}} reasoning
5555
</div>
5656
</div>

0 commit comments

Comments
 (0)