feat(visual-rewrite): drop the CSS framework, adopt the gui_preview design system across every page (v0.7.5)

PR #6 of the v0.7.x GUI rework arc, and its close-out. Production code;
central-gui restart only (templates + a static stylesheet; supervisor untouched).

The v0.7.0-v0.7.4 arc shipped the FEATURES the gui_preview.html mock shows
(chip-pickers, active pills, markercluster + shape/opacity markers, collapsed
grouped legend, real paginator, telemetry tab) but on top of the old CSS
framework. v0.7.5 closes the visual gap: the framework is removed entirely and
replaced by a hand-authored design system that IS the preview (neutral greys, a
single blue accent, 6px radii, 14px type, bordered cards/tables).

- New static/css/central.css: design tokens + element base (forms, buttons,
  tables, cards, nav) + every events-feed component, authored so the existing
  template class names + element IDs the page JS/HTMX depends on (.chip-picker*,
  .events-table, .event-row, .legend-chip, .evt-marker, #fit-to-results,
  #map-filter-toggle, ...) render in the preview's language unchanged. Mounted at
  /static (already wired in __init__.py); linked from base.html + base_wizard.html.
- base.html / base_wizard.html: framework <link> removed; nav restructured to the
  preview shell (.nav / .brand / .nav-links / .logout-btn); flash messages moved
  off framework color vars to .flash-* classes.
- events_list.html: 190-line inline <style> deleted (moved to central.css); map
  wrapped in .map-container with a floating .map-toolbar (Fit / map-filter toggle),
  legend below. HTMX wiring (hx-get/hx-target/hx-push-url), the v0.7.1 chip-picker
  JS, the v0.7.2 marker shapes/opacity, and the v0.7.4 base_path split are
  untouched.
- Every other page (dashboard, adapters, api-keys, enrichment, streams, login,
  change-password, and the full setup wizard) de-framework'd: framework class
  names (outline/secondary/contrast/grid/container) renamed to neutral
  .btn-*/.cols; no framework default rendering; same tokens/typography/forms/tables.
- /events.json + the v0.7.1 registry-index adapter palette + v0.7.2 severity
  opacity + v0.7.4 stable cross-tab chip colors are all unchanged -- this is a
  visual shell swap, not a behavior or data change.

grep -rn pico over templates + static returns zero. All GUI routes return 200
(authed) / 302 (unauthed); no 5xx. Full suite: 682 passed, 1 skipped (central and
unprivileged zvx, 3x each).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-25 08:28:06 +00:00
commit e20a8398c6
24 changed files with 828 additions and 408 deletions

View file

@ -0,0 +1,632 @@
/* ============================================================================
Central design system v0.7.5
Hand-authored stylesheet that replaces the former CSS framework entirely.
The visual language is the canonical gui_preview.html mock: neutral greys,
a single blue accent, 6px radii, compact 14px type, bordered cards/tables.
Existing template class names (.chip-picker, .events-table, .legend-chip,
.evt-marker, ) are kept and styled here so the page JS/HTMX stays intact;
only framework class names were renamed (.btn-outline etc.).
========================================================================== */
:root {
--ink: #111;
--ink-muted: #666;
--ink-faint: #999;
--rule: #e5e5e5;
--rule-strong: #cccccc;
--bg: #ffffff;
--bg-soft: #fafafa;
--bg-chip: #f1f3f5;
--bg-active: #e7f1ff;
--bg-code: #f5f5f5;
--link: #1f6feb;
--link-hover: #0a4cb0;
--accent: #1f6feb;
--danger: #d72d2d;
--warn: #cc8400;
--ok: #1a7f37;
--shadow: 0 1px 2px rgba(0,0,0,0.04);
--shadow-pop: 0 4px 16px rgba(0,0,0,0.08);
--radius: 6px;
--radius-sm: 4px;
}
/* ─── element base ──────────────────────────────────────────────────────── */
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background: var(--bg);
font-size: 14px;
line-height: 1.5;
}
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--link-hover); text-decoration: underline; }
h1 { font-size: 28px; font-weight: 500; margin: 0 0 18px; }
h2 { font-size: 20px; font-weight: 500; margin: 24px 0 12px; }
h3 { font-size: 16px; font-weight: 500; margin: 18px 0 10px; }
p { margin: 0 0 12px; }
small { color: var(--ink-muted); }
code {
font-family: ui-monospace, "SF Mono", Consolas, monospace;
font-size: 0.92em;
background: var(--bg-code);
padding: 1px 5px;
border-radius: var(--radius-sm);
}
pre {
background: var(--bg-code);
border: 1px solid var(--rule);
border-radius: var(--radius-sm);
padding: 10px 12px;
overflow: auto;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
font-size: 12px;
}
hr { border: none; border-top: 1px solid var(--rule); margin: 18px 0; }
/* ─── form controls ─────────────────────────────────────────────────────── */
label { display: block; font-size: 13px; margin-bottom: 4px; color: var(--ink); }
input, select, textarea {
font: inherit;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--rule-strong);
border-radius: var(--radius);
padding: 8px 10px;
width: 100%;
max-width: 100%;
}
input[type="checkbox"], input[type="radio"] {
width: auto;
margin: 0;
accent-color: var(--accent);
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(31,111,235,0.12);
}
select {
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none' stroke='%23666' stroke-width='1.5'><path d='M1 1l4 4 4-4'/></svg>");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 28px;
}
fieldset {
border: 1px solid var(--rule);
border-radius: var(--radius);
padding: 14px 16px;
margin: 0 0 16px;
}
legend { font-size: 13px; font-weight: 500; color: var(--ink-muted); padding: 0 6px; }
details {
border: 1px solid var(--rule);
border-radius: var(--radius);
padding: 8px 14px;
margin-bottom: 12px;
background: var(--bg-soft);
}
summary { cursor: pointer; font-weight: 500; }
/* ─── buttons ───────────────────────────────────────────────────────────── */
:where(button, .btn, a[role="button"]) {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 34px;
padding: 0 18px;
background: var(--accent);
color: #fff;
border: 1px solid var(--accent);
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
line-height: 1;
cursor: pointer;
white-space: nowrap;
text-decoration: none;
}
:where(button, .btn, a[role="button"]):hover {
background: var(--link-hover);
border-color: var(--link-hover);
color: #fff;
text-decoration: none;
}
button:disabled, .btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* neutral framework-class replacements (formerly outline / secondary / contrast) */
.btn-outline {
background: transparent;
color: var(--link);
border-color: var(--link);
}
.btn-outline:hover { background: var(--bg-active); color: var(--link-hover); border-color: var(--link-hover); }
.btn-secondary {
background: var(--bg-soft);
color: var(--ink);
border-color: var(--rule-strong);
}
.btn-secondary:hover { background: var(--bg-chip); color: var(--ink); border-color: var(--ink-muted); }
.btn-contrast {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
.btn-contrast:hover { background: #000; border-color: #000; color: #fff; }
.btn-danger { background: var(--danger); border-color: var(--danger); }
.btn-danger:hover { background: #b71f1f; border-color: #b71f1f; }
/* ─── layout: nav shell + page + grid + cards ───────────────────────────── */
.nav {
display: flex;
align-items: center;
padding: 14px 32px;
border-bottom: 1px solid var(--rule);
gap: 22px;
}
.nav .brand { font-weight: 500; font-size: 16px; margin-right: auto; }
.nav-links { display: flex; gap: 22px; align-items: center; flex-wrap: wrap; }
.nav-links a { color: var(--link); font-size: 14px; }
.nav-links a.active { color: var(--ink); font-weight: 500; }
.nav-links .sep { color: var(--ink-faint); font-size: 13px; }
.nav-links .who { color: var(--ink-muted); font-size: 13px; }
.logout-btn {
height: 30px;
padding: 0 14px;
border: 1px solid var(--link);
border-radius: var(--radius);
color: var(--link);
background: transparent;
font-size: 13px;
font-weight: 400;
}
.logout-btn:hover { background: var(--bg-active); color: var(--link-hover); border-color: var(--link-hover); }
.page { max-width: 1500px; margin: 0 auto; padding: 24px 32px 64px; }
.page-narrow { max-width: 460px; margin: 0 auto; padding: 48px 24px; }
.cols { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
/* cards (formerly the framework's <article>) */
article, .card {
border: 1px solid var(--rule);
border-radius: var(--radius);
background: var(--bg);
padding: 16px 18px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
article > :first-child, .card > :first-child { margin-top: 0; }
article > :last-child, .card > :last-child { margin-bottom: 0; }
article > header, .card > header {
margin: -16px -18px 14px;
padding: 12px 18px;
background: var(--bg-soft);
border-bottom: 1px solid var(--rule);
border-radius: var(--radius) var(--radius) 0 0;
font-weight: 500;
font-size: 14px;
}
article > header h1, article > header h2, .card > header h1, .card > header h2 { margin: 0; }
article > footer, .card > footer {
margin: 14px -18px -16px;
padding: 12px 18px;
background: var(--bg-soft);
border-top: 1px solid var(--rule);
border-radius: 0 0 var(--radius) var(--radius);
}
.error, p.error, td.error { color: var(--danger); }
.field-error { display: block; color: var(--danger); font-size: 12px; margin-top: 2px; }
.warn { color: var(--warn); }
/* form spacing inside cards/forms */
form label { margin-top: 10px; }
form > label:first-child, fieldset > label:first-child { margin-top: 0; }
form small, fieldset small { display: block; color: var(--ink-muted); font-size: 12px; margin-top: 2px; }
fieldset + button, fieldset + .btn, form > button, form > .btn, form > [role="button"] { margin-top: 14px; margin-right: 8px; }
.section-divider {
margin: 28px 0 14px;
padding-top: 14px;
border-top: 1px solid var(--rule);
color: var(--ink-muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
/* flash / banner messages (formerly framework color vars) */
.flash { padding: 10px 14px; border-radius: var(--radius); margin-bottom: 16px; font-size: 13px; border: 1px solid; }
.flash-error { background: #fdecec; border-color: #f3c2c2; color: #8a1414; }
.flash-ok { background: #e9f6ec; border-color: #bfe3c7; color: #14622a; }
.flash-warn { background: #fff8e1; border-color: #f0d57a; color: #5a4500; }
/* ─── generic tables (dashboard, adapters, api-keys, streams, setup) ─────── */
table { width: 100%; border-collapse: collapse; }
.table-wrap { border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; background: var(--bg); }
table th {
text-align: left;
padding: 10px 14px;
background: var(--bg-soft);
border-bottom: 1px solid var(--rule);
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-muted);
}
table td { padding: 9px 14px; border-bottom: 1px solid var(--rule); font-size: 13px; vertical-align: middle; }
table tbody tr:last-child td { border-bottom: none; }
table tbody tr:hover { background: var(--bg-soft); }
/* status dots used on dashboard/adapter tables */
.status-ok { color: var(--ok); }
.status-err { color: var(--danger); }
.status-muted { color: var(--ink-faint); }
.mono { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: 12px; }
.muted { color: var(--ink-muted); }
.errortext { color: var(--danger); font-family: ui-monospace, monospace; font-size: 12px; }
/* =================== EVENTS / TELEMETRY FEED ============================== */
/* search box */
.filter-search {
height: 38px;
padding: 0 14px 0 40px;
margin-bottom: 14px;
border: 1px solid var(--rule-strong);
border-radius: var(--radius);
font-size: 14px;
background: var(--bg) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='8'/><line x1='21' y1='21' x2='16.65' y2='16.65'/></svg>") no-repeat 12px center;
}
/* filter chip-picker row */
.filter-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.filter-actions { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; align-items: center; }
.chip-picker { position: relative; display: inline-block; }
.chip-picker-toggle {
height: 34px;
padding: 0 12px;
background: var(--bg);
color: var(--ink);
border: 1px solid var(--rule-strong);
border-radius: var(--radius);
font-size: 13px;
font-weight: 400;
white-space: nowrap;
}
.chip-picker-toggle:hover { background: var(--bg-soft); border-color: var(--ink-muted); color: var(--ink); }
/* active state when the toggle carries a non-empty count badge */
.chip-picker-toggle:has(.chip-count:not(:empty)) {
border-color: var(--accent);
background: var(--bg-active);
color: var(--link-hover);
font-weight: 500;
}
.chip-count { color: var(--accent); font-weight: 500; }
.filter-apply { /* primary; inherits button */ min-width: 80px; }
.chip-picker-panel {
position: absolute;
top: 38px;
left: 0;
z-index: 1000;
width: 320px;
max-height: 380px;
overflow-y: auto;
background: var(--bg);
border: 1px solid var(--rule-strong);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
padding: 0;
}
.chip-picker-search {
position: sticky;
top: 0;
width: 100%;
height: 34px;
padding: 0 10px;
border: none;
border-bottom: 1px solid var(--rule);
border-radius: var(--radius) var(--radius) 0 0;
font-size: 13px;
background: var(--bg);
}
.chip-picker-list { padding: 4px 0 6px; }
.chip-group-header {
padding: 8px 14px 4px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ink-muted);
font-weight: 500;
}
.chip-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
margin: 0;
cursor: pointer;
font-size: 13px;
font-weight: 400;
}
.chip-row:hover { background: var(--bg-soft); }
.chip-row.chip-hidden { display: none; }
.chip-swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
/* time-preset dropdown (lives inside a chip-picker panel) */
.time-picker .chip-picker-panel { width: 240px; }
.time-preset {
display: block;
width: 100%;
height: auto;
text-align: left;
background: none;
border: none;
border-radius: 0;
padding: 7px 14px;
color: var(--ink);
font-weight: 400;
font-size: 13px;
}
.time-preset:hover, .time-preset.selected { background: var(--bg-soft); color: var(--ink); }
.time-custom { border-top: 1px solid var(--rule); margin-top: 4px; padding: 8px 14px 10px; display: flex; flex-direction: column; gap: 6px; }
.time-custom label { font-size: 12px; color: var(--ink-muted); }
.time-custom-apply { width: 100%; }
/* active filter pills */
.active-pills { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 16px; min-height: 24px; align-items: center; }
.active-pills-label { color: var(--ink-faint); font-size: 12px; margin-right: 4px; }
.filter-pill {
display: inline-flex;
align-items: center;
gap: 6px;
height: 24px;
padding: 0 8px 0 10px;
background: var(--bg-chip);
border-radius: 12px;
font-size: 12px;
color: var(--ink);
}
.pill-remove {
height: auto;
padding: 0 2px;
background: none;
border: none;
color: var(--ink-faint);
font-size: 14px;
line-height: 1;
font-weight: 400;
}
.pill-remove:hover { background: none; color: var(--danger); }
.pill-clear-all {
height: auto;
padding: 0 4px;
background: none;
border: none;
color: var(--ink-muted);
font-size: 12px;
text-decoration: underline;
font-weight: 400;
}
.pill-clear-all:hover { background: none; color: var(--danger); }
/* map */
.map-container { position: relative; border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; margin-bottom: 16px; }
#events-map { height: 400px; width: 100%; }
.map-toolbar { position: absolute; top: 10px; right: 10px; z-index: 1000; display: flex; gap: 6px; }
.map-toolbar .tb, #fit-to-results.tb, .map-filter-toggle.tb {
height: auto;
background: var(--bg);
border: 1px solid var(--rule-strong);
border-radius: var(--radius-sm);
padding: 5px 10px;
font-size: 12px;
font-weight: 400;
color: var(--ink);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
}
.map-toolbar .tb:hover { background: var(--bg-soft); color: var(--ink); border-color: var(--ink-muted); }
.map-filter-toggle.tb input { margin: 0; }
.map-filter-toggle.tb:has(input:checked), .map-toolbar .tb.on {
background: var(--bg-active);
border-color: var(--accent);
color: var(--link-hover);
}
/* adapter color legend (collapsible) */
.map-legend { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--bg-soft); margin-bottom: 16px; }
#legend-toggle {
width: 100%;
height: auto;
justify-content: flex-start;
padding: 10px 14px;
background: none;
border: none;
border-radius: var(--radius);
color: var(--ink-muted);
font-size: 13px;
font-weight: 400;
text-align: left;
}
#legend-toggle:hover { background: var(--bg-chip); color: var(--ink-muted); }
.legend-body {
border-top: 1px solid var(--rule);
padding: 12px 14px 14px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
gap: 4px 12px;
}
.legend-body[hidden] { display: none; }
.legend-group { break-inside: avoid; }
.legend-group-header {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ink-faint);
font-weight: 500;
margin: 8px 0 6px;
}
.legend-chip {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
height: auto;
padding: 5px 8px;
border: none;
border-radius: var(--radius-sm);
background: none;
color: var(--ink);
font-size: 12px;
font-weight: 400;
text-align: left;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
}
.legend-chip:hover { background: var(--bg-chip); color: var(--ink); }
.legend-chip.muted { opacity: 0.35; }
.legend-chip-swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.legend-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* events table */
.events-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.table-wrap .events-table { border: none; }
.events-table thead th {
text-align: left;
padding: 10px 14px;
background: var(--bg-soft);
border-bottom: 1px solid var(--rule);
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-muted);
}
.events-table th:nth-child(1), .events-table td:nth-child(1) { width: 2rem; }
.events-table th:nth-child(2), .events-table td:nth-child(2) { width: 8.5rem; }
.events-table th:nth-child(3), .events-table td:nth-child(3) { width: 22%; }
.events-table th:nth-child(5), .events-table td:nth-child(5) { width: 15rem; }
.events-table tbody tr { border-bottom: 1px solid var(--rule); }
.events-table tbody tr.event-row { height: 38px; }
.events-table tbody tr.event-row:hover { background: var(--bg-soft); cursor: pointer; }
.events-table tbody tr.event-row.highlighted { background: var(--bg-active); }
.events-table tbody tr.event-row > td {
padding: 0 14px;
height: 38px;
vertical-align: middle;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cell-time { color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.expand-row {
height: auto;
background: transparent;
border: none;
padding: 2px 4px;
cursor: pointer;
color: var(--ink-faint);
font-size: 12px;
}
.expand-row:hover { background: none; color: var(--link); }
.adapter-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 2px 8px;
background: var(--bg-chip);
border-radius: 10px;
font-size: 12px;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.adapter-chip-swatch { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.sev-pill { display: inline-block; font-size: 11px; padding: 1px 6px; border-radius: 3px; margin-left: 6px; font-weight: 500; }
.sev-low { background: #eef; color: #335; }
.sev-moderate { background: #fff3d9; color: #7a5500; }
.sev-high { background: #ffe1d4; color: #8a3a14; }
.sev-critical { background: #ffd4d4; color: #8a1414; }
/* expanded detail row */
.event-detail td { background: #f7f7f7; padding: 12px 18px 14px 44px; white-space: normal; }
.event-detail-list { display: grid; grid-template-columns: 160px 1fr; gap: 4px 16px; margin: 0; }
.event-detail-list dt { color: var(--ink-faint); font-size: 12px; }
.event-detail-list dd { margin: 0; color: var(--ink); font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: 12px; }
.event-data-pre { max-height: 300px; overflow: auto; font-size: 12px; margin: 4px 0 0; grid-column: 1 / -1; }
/* leaflet divIcon markers: shape encodes event_type, opacity encodes severity */
.evt-marker { width: 15px; height: 15px; box-sizing: border-box; border: 1px solid rgba(0,0,0,0.55); }
.evt-circle { border-radius: 50%; }
.evt-square { border-radius: 1px; }
.evt-triangle { border: none; clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
.evt-star { border: none; clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); }
.evt-marker.evt-hl { filter: drop-shadow(0 0 4px #ff3333); transform: scale(1.35); }
/* pagination */
.paginator { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 16px; font-size: 13px; }
.paginator-pages { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.page-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
padding: 0 8px;
border: 1px solid var(--rule-strong);
background: var(--bg);
border-radius: var(--radius-sm);
color: var(--ink);
font-size: 13px;
font-weight: 400;
cursor: pointer;
}
.page-link:hover { background: var(--bg-soft); color: var(--ink); border-color: var(--ink-muted); text-decoration: none; }
.page-link.current {
border-color: var(--accent);
background: var(--bg-active);
color: var(--link-hover);
font-weight: 500;
}
.page-link.disabled { opacity: 0.4; pointer-events: none; }
.page-ellipsis { padding: 0 4px; color: var(--ink-faint); }
.paginator-meta { display: flex; align-items: center; gap: 12px; margin-left: auto; font-size: 13px; color: var(--ink-muted); }
.per-page { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; color: var(--ink-muted); }
.per-page select { width: auto; height: 30px; padding: 0 28px 0 8px; font-size: 13px; }
.sort-ind { color: var(--ink-faint); }
/* ─── auth pages (login / change password) ──────────────────────────────── */
.auth-card { max-width: 380px; margin: 64px auto; }
.auth-card h1 { font-size: 22px; text-align: center; }
.auth-brand { text-align: center; font-size: 18px; font-weight: 500; margin-bottom: 24px; }
/* ─── progress (setup wizard) ───────────────────────────────────────────── */
progress {
width: 100%;
height: 8px;
appearance: none;
border: none;
border-radius: 999px;
background: var(--bg-chip);
overflow: hidden;
}
progress::-webkit-progress-bar { background: var(--bg-chip); border-radius: 999px; }
progress::-webkit-progress-value { background: var(--accent); border-radius: 999px; }
progress::-moz-progress-bar { background: var(--accent); border-radius: 999px; }
.wizard-actions { display: flex; gap: 12px; margin-top: 18px; }

View file

@ -1,12 +1,10 @@
{% if preview_error %} {% if preview_error %}
<article aria-label="Preview Unavailable" style="background-color: var(--pico-del-color); margin-bottom: 1rem;"> <div class="flash flash-error"><strong>{{ preview_error }}</strong></div>
<strong>{{ preview_error }}</strong>
</article>
{% elif preview_rows is not none %} {% elif preview_rows is not none %}
<fieldset> <fieldset>
<legend>Preview ({{ preview_rows|length }} rows)</legend> <legend>Preview ({{ preview_rows|length }} rows)</legend>
{% if preview_rows %} {% if preview_rows %}
<table class="preview-table" role="grid"> <div class="table-wrap"><table class="preview-table">
<thead> <thead>
<tr> <tr>
{% for col in preview_rows[0].keys() %}<th>{{ col }}</th>{% endfor %} {% for col in preview_rows[0].keys() %}<th>{{ col }}</th>{% endfor %}
@ -19,7 +17,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table></div>
{% endif %} {% endif %}
</fieldset> </fieldset>
{% endif %} {% endif %}

View file

@ -20,7 +20,7 @@
{% macro chip_picker(field, label, options, selected, grouped=False, searchable=False, with_swatch=False) %} {% macro chip_picker(field, label, options, selected, grouped=False, searchable=False, with_swatch=False) %}
<div class="chip-picker" data-field="{{ field }}"> <div class="chip-picker" data-field="{{ field }}">
<button type="button" class="chip-picker-toggle outline" data-toggle="{{ field }}" <button type="button" class="chip-picker-toggle" data-toggle="{{ field }}"
aria-haspopup="true" aria-expanded="false"> aria-haspopup="true" aria-expanded="false">
{{ label }}<span class="chip-count" data-count-for="{{ field }}">{{ (' (' ~ selected | length ~ ')') if selected else '' }}</span> {{ label }}<span class="chip-count" data-count-for="{{ field }}">{{ (' (' ~ selected | length ~ ')') if selected else '' }}</span>
</button> </button>

View file

@ -1,10 +1,9 @@
{% if filter_error %} {% if filter_error %}
<article aria-label="Filter Error" style="background-color: var(--pico-del-color); padding: 1rem; margin-bottom: 1rem;"> <div class="flash flash-error" role="alert"><strong>Filter Error:</strong> {{ filter_error }}</div>
<strong>Filter Error:</strong> {{ filter_error }}
</article>
{% endif %} {% endif %}
{% if events %} {% if events %}
<div class="table-wrap">
<table class="events-table"> <table class="events-table">
<thead> <thead>
<tr> <tr>
@ -69,6 +68,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{# Real offset paginator (v0.7.3). Each link carries offset + the filter {# Real offset paginator (v0.7.3). Each link carries offset + the filter
query_string (which excludes cursor/offset); limit persists via query_string. #} query_string (which excludes cursor/offset); limit persists via query_string. #}

View file

@ -6,9 +6,9 @@
data-tile-url="{{ tile_url }}" data-tile-url="{{ tile_url }}"
data-tile-attr="{{ tile_attribution }}"> data-tile-attr="{{ tile_attribution }}">
<div id="region-map" style="height: 400px; margin-bottom: 1rem;"></div> <div id="region-map" style="height: 400px; margin-bottom: 1rem; border: 1px solid var(--rule); border-radius: var(--radius);"></div>
<div class="grid"> <div class="cols">
<div> <div>
<label for="region_north">North</label> <label for="region_north">North</label>
<input type="number" id="region_north" name="region_north" step="0.0001" min="-90" max="90" readonly <input type="number" id="region_north" name="region_north" step="0.0001" min="-90" max="90" readonly
@ -32,10 +32,10 @@
</div> </div>
{% if errors and errors.region %} {% if errors and errors.region %}
<small style="color: var(--pico-color-red-500);">{{ errors.region }}</small> <small class="field-error">{{ errors.region }}</small>
{% endif %} {% endif %}
<button type="button" id="region-reset-btn" class="outline secondary">Reset to Saved</button> <button type="button" id="region-reset-btn" class="btn-secondary" style="margin-top: 12px;">Reset to Saved</button>
</div> </div>
<script> <script>

View file

@ -11,25 +11,21 @@
{% block content %} {% block content %}
<h1>{{ adapter.display_name }}</h1> <h1>{{ adapter.display_name }}</h1>
<p class="secondary">{{ adapter.description }}</p> <p class="muted">{{ adapter.description }}</p>
{% if adapter.paused_at %} {% if adapter.paused_at %}
<article aria-label="Adapter Paused" style="background-color: var(--pico-mark-background-color); margin-bottom: 1rem;"> <div class="flash flash-warn"><strong>⏸️ Paused</strong> since {{ adapter.paused_at }}</div>
<strong>⏸️ Paused</strong> since {{ adapter.paused_at }}
</article>
{% endif %} {% endif %}
{% if adapter.last_error %} {% if adapter.last_error %}
<article aria-label="Last Error" style="background-color: var(--pico-del-color); margin-bottom: 1rem;"> <div class="flash flash-error"><strong>Last Error:</strong> {{ adapter.last_error }}</div>
<strong>Last Error:</strong> {{ adapter.last_error }}
</article>
{% endif %} {% endif %}
{% if api_key_missing %} {% if api_key_missing %}
<article aria-label="API Key Required" style="background-color: var(--pico-mark-background-color); margin-bottom: 1rem;"> <div class="flash flash-warn">
<strong>⚠️ API Key Required:</strong> This adapter requires the <code>{{ requires_api_key_alias }}</code> API key to be configured before it can be enabled. <strong>⚠️ API Key Required:</strong> This adapter requires the <code>{{ requires_api_key_alias }}</code> API key to be configured before it can be enabled.
<a href="/api-keys">Configure API Keys</a> <a href="/api-keys">Configure API Keys</a>
</article> </div>
{% endif %} {% endif %}
<form method="post" action="/adapters/{{ adapter.name }}"> <form method="post" action="/adapters/{{ adapter.name }}">
@ -48,7 +44,7 @@
value="{{ form_data.cadence_s if form_data else adapter.cadence_s }}" value="{{ form_data.cadence_s if form_data else adapter.cadence_s }}"
required> required>
{% if errors and errors.cadence_s %} {% if errors and errors.cadence_s %}
<small style="color: var(--pico-color-red-500);">{{ errors.cadence_s }}</small> <small class="field-error">{{ errors.cadence_s }}</small>
{% endif %} {% endif %}
</fieldset> </fieldset>
@ -68,7 +64,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "number" %} {% elif field.widget == "number" %}
@ -80,7 +76,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "checkbox" %} {% elif field.widget == "checkbox" %}
@ -95,7 +91,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "csv" %} {% elif field.widget == "csv" %}
@ -105,7 +101,7 @@
{% if field.required %}required{% endif %}> {% if field.required %}required{% endif %}>
<small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small> <small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small>
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "select" %} {% elif field.widget == "select" %}
@ -122,7 +118,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "checkboxes" %} {% elif field.widget == "checkboxes" %}
@ -139,7 +135,7 @@
<small style="display: block;">{{ field.description }}</small> <small style="display: block;">{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500); display: block;">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "api_key_select" %} {% elif field.widget == "api_key_select" %}
@ -157,7 +153,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -181,6 +177,6 @@
{% include "_adapter_preview.html" %} {% include "_adapter_preview.html" %}
<button type="submit">Save Changes</button> <button type="submit">Save Changes</button>
<a href="/adapters" role="button" class="outline">Cancel</a> <a href="/adapters" role="button" class="btn-outline">Cancel</a>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -20,7 +20,7 @@
<td> <td>
{{ adapter.display_name or adapter.name }} {{ adapter.display_name or adapter.name }}
{% if adapter.api_key_missing %} {% if adapter.api_key_missing %}
<span style="color: var(--pico-color-orange-500); margin-left: 0.5rem;" title="Missing API key: {{ adapter.requires_api_key_alias }}">⚠️ API Key Missing</span> <span class="warn" style="margin-left: 0.5rem;" title="Missing API key: {{ adapter.requires_api_key_alias }}">⚠️ API Key Missing</span>
{% endif %} {% endif %}
</td> </td>
<td>{% if adapter.enabled %}Yes{% else %}No{% endif %}</td> <td>{% if adapter.enabled %}Yes{% else %}No{% endif %}</td>

View file

@ -27,7 +27,7 @@
placeholder="Paste your new API key here" required maxlength="4096" placeholder="Paste your new API key here" required maxlength="4096"
aria-invalid="{{ 'true' if errors and errors.new_plaintext_key else 'false' }}"> aria-invalid="{{ 'true' if errors and errors.new_plaintext_key else 'false' }}">
{% if errors and errors.new_plaintext_key %} {% if errors and errors.new_plaintext_key %}
<small style="color: var(--pico-color-red-500);">{{ errors.new_plaintext_key }}</small> <small class="field-error">{{ errors.new_plaintext_key }}</small>
{% else %} {% else %}
<small>The key will be encrypted before storage.</small> <small>The key will be encrypted before storage.</small>
{% endif %} {% endif %}
@ -40,7 +40,7 @@
<header><strong>Delete Key</strong></header> <header><strong>Delete Key</strong></header>
{% if key.used_by %} {% if key.used_by %}
<p style="color: var(--pico-color-red-500);"> <p class="error">
<strong>Cannot delete:</strong> This key is used by: {{ key.used_by | join(', ') }}. <strong>Cannot delete:</strong> This key is used by: {{ key.used_by | join(', ') }}.
Remove these references from the adapters first. Remove these references from the adapters first.
</p> </p>
@ -51,12 +51,12 @@
<p>Permanently delete this API key. This action cannot be undone.</p> <p>Permanently delete this API key. This action cannot be undone.</p>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <p class="error">{{ error }}</p>
{% endif %} {% endif %}
<form action="/api-keys/{{ key.alias }}/delete" method="post"> <form action="/api-keys/{{ key.alias }}/delete" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="contrast">Delete Key</button> <button type="submit" class="btn-danger">Delete Key</button>
</form> </form>
{% endif %} {% endif %}
</article> </article>

View file

@ -8,6 +8,7 @@
<p><a href="/api-keys/new" role="button">Add New Key</a></p> <p><a href="/api-keys/new" role="button">Add New Key</a></p>
{% if keys %} {% if keys %}
<div class="table-wrap">
<table> <table>
<thead> <thead>
<tr> <tr>
@ -28,12 +29,13 @@
<td>{{ key.last_used_at.strftime('%Y-%m-%d %H:%M') if key.last_used_at else '(never)' }}</td> <td>{{ key.last_used_at.strftime('%Y-%m-%d %H:%M') if key.last_used_at else '(never)' }}</td>
<td>{% if key.used_by %}{{ key.used_by | join(', ') }}{% else %}<em>(none)</em>{% endif %}</td> <td>{% if key.used_by %}{{ key.used_by | join(', ') }}{% else %}<em>(none)</em>{% endif %}</td>
<td> <td>
<a href="/api-keys/{{ key.alias }}" role="button" class="outline" style="padding: 0.3em 0.6em; font-size: 0.9em;">Manage</a> <a href="/api-keys/{{ key.alias }}" role="button" class="btn-outline" style="height: 28px; padding: 0 12px;">Manage</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% else %} {% else %}
<p>No API keys configured.</p> <p>No API keys configured.</p>
{% endif %} {% endif %}

View file

@ -13,7 +13,7 @@
placeholder="e.g., firms_production" required maxlength="64" placeholder="e.g., firms_production" required maxlength="64"
aria-invalid="{{ 'true' if errors and errors.alias else 'false' }}"> aria-invalid="{{ 'true' if errors and errors.alias else 'false' }}">
{% if errors and errors.alias %} {% if errors and errors.alias %}
<small style="color: var(--pico-color-red-500);">{{ errors.alias }}</small> <small class="field-error">{{ errors.alias }}</small>
{% else %} {% else %}
<small>Letters, numbers, and underscores only. Max 64 characters.</small> <small>Letters, numbers, and underscores only. Max 64 characters.</small>
{% endif %} {% endif %}
@ -23,14 +23,14 @@
placeholder="Paste your API key here" required maxlength="4096" placeholder="Paste your API key here" required maxlength="4096"
aria-invalid="{{ 'true' if errors and errors.plaintext_key else 'false' }}"> aria-invalid="{{ 'true' if errors and errors.plaintext_key else 'false' }}">
{% if errors and errors.plaintext_key %} {% if errors and errors.plaintext_key %}
<small style="color: var(--pico-color-red-500);">{{ errors.plaintext_key }}</small> <small class="field-error">{{ errors.plaintext_key }}</small>
{% else %} {% else %}
<small>The key will be encrypted before storage. You will not be able to view it again.</small> <small>The key will be encrypted before storage. You will not be able to view it again.</small>
{% endif %} {% endif %}
<div style="display: flex; gap: 1rem; margin-top: 1rem;"> <div style="display: flex; gap: 1rem; margin-top: 1rem;">
<button type="submit">Save Key</button> <button type="submit">Save Key</button>
<a href="/api-keys" role="button" class="outline">Cancel</a> <a href="/api-keys" role="button" class="btn-outline">Cancel</a>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,50 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Central{% endblock %}</title> <title>{% block title %}Central{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> <link rel="stylesheet" href="/static/css/central.css">
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<nav class="container"> <nav class="nav">
<ul> <div class="brand">Central</div>
<li><strong>Central</strong></li>
</ul>
<ul>
{% if operator %} {% if operator %}
<li><a href="/">Dashboard</a></li> <div class="nav-links">
<li><a href="/adapters">Adapters</a></li> <a href="/">Dashboard</a>
<li><a href="/events">Events</a></li> <a href="/adapters">Adapters</a>
<li><a href="/telemetry">Telemetry</a></li> <a href="/events">Events</a>
<li><a href="/streams">Streams</a></li> <a href="/telemetry">Telemetry</a>
<li><a href="/enrichment">Enrichment</a></li> <a href="/streams">Streams</a>
<li><a href="/api-keys">API Keys</a></li> <a href="/enrichment">Enrichment</a>
<li>{{ operator.username }}</li> <a href="/api-keys">API Keys</a>
<li><a href="/change-password">Change Password</a></li> <span class="sep">·</span>
<li> <span class="who">{{ operator.username }}</span>
<a href="/change-password">Change Password</a>
<form action="/logout" method="post" style="margin: 0;"> <form action="/logout" method="post" style="margin: 0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="outline">Logout</button> <button type="submit" class="logout-btn">Logout</button>
</form> </form>
</li> </div>
{% else %} {% else %}
<li><a href="/login">Login</a></li> <div class="nav-links">
<a href="/login">Login</a>
</div>
{% endif %} {% endif %}
</ul>
</nav> </nav>
<main class="container"> <main class="page">
{% if error %} {% if error %}
<article aria-label="Error"> <div class="flash flash-error" role="alert">{{ error }}</div>
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
</article>
{% endif %} {% endif %}
{% if success %} {% if success %}
<article aria-label="Success"> <div class="flash flash-ok" role="status">{{ success }}</div>
<p style="color: var(--pico-color-green-500);">{{ success }}</p>
</article>
{% endif %} {% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>

View file

@ -1,23 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Central - Setup{% endblock %}</title> <title>{% block title %}Central - Setup{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> <link rel="stylesheet" href="/static/css/central.css">
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<nav class="container"> <nav class="nav">
<ul> <div class="brand">Central</div>
<li><strong>Central</strong></li> <div class="nav-links"><span class="who">Setup Wizard</span></div>
</ul>
<ul>
<li>Setup Wizard</li>
</ul>
</nav> </nav>
<main class="container"> <main class="page" style="max-width: 760px;">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
</body> </body>

View file

@ -3,38 +3,30 @@
{% block title %}Central - Change Password{% endblock %} {% block title %}Central - Change Password{% endblock %}
{% block content %} {% block content %}
<article> <div class="auth-card card">
<header>
<h1>Change Password</h1> <h1>Change Password</h1>
</header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/change-password" method="post"> <form action="/change-password" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label for="current_password"> <label for="current_password">Current Password</label>
Current Password
<input type="password" id="current_password" name="current_password" required <input type="password" id="current_password" name="current_password" required
autocomplete="current-password" autofocus> autocomplete="current-password" autofocus>
</label>
<label for="new_password"> <label for="new_password" style="margin-top: 12px;">New Password</label>
New Password
<input type="password" id="new_password" name="new_password" required <input type="password" id="new_password" name="new_password" required
autocomplete="new-password" minlength="8"> autocomplete="new-password" minlength="8">
<small>Minimum 8 characters</small> <small>Minimum 8 characters</small>
</label>
<label for="confirm_password"> <label for="confirm_password" style="margin-top: 12px;">Confirm New Password</label>
Confirm New Password
<input type="password" id="confirm_password" name="confirm_password" required <input type="password" id="confirm_password" name="confirm_password" required
autocomplete="new-password"> autocomplete="new-password">
</label>
<button type="submit">Change Password</button> <button type="submit" style="width: 100%; margin-top: 18px;">Change Password</button>
</form> </form>
</article> </div>
{% endblock %} {% endblock %}

View file

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<h1>Enrichment</h1> <h1>Enrichment</h1>
<p class="secondary"> <p class="muted">
Central-side event enrichment. Results are attached to each event under Central-side event enrichment. Results are attached to each event under
<code>data._enriched.&lt;enricher&gt;</code>. Changes hot-reload into the <code>data._enriched.&lt;enricher&gt;</code>. Changes hot-reload into the
supervisor; switching backend invalidates the enrichment cache. Backend supervisor; switching backend invalidates the enrichment cache. Backend
@ -25,7 +25,7 @@
{% if field.required %}required{% endif %}> {% if field.required %}required{% endif %}>
{% if field.description %}<small>{{ field.description }}</small>{% endif %} {% if field.description %}<small>{{ field.description }}</small>{% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "number" %} {% elif field.widget == "number" %}
<label for="{{ field.name }}">{{ field.label }}</label> <label for="{{ field.name }}">{{ field.label }}</label>
@ -34,7 +34,7 @@
{% if field.required %}required{% endif %}> {% if field.required %}required{% endif %}>
{% if field.description %}<small>{{ field.description }}</small>{% endif %} {% if field.description %}<small>{{ field.description }}</small>{% endif %}
{% if errors and errors[field.name] %} {% if errors and errors[field.name] %}
<small style="color: var(--pico-color-red-500);">{{ errors[field.name] }}</small> <small class="field-error">{{ errors[field.name] }}</small>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -53,7 +53,7 @@
value="{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else field.current_value or '' }}"> value="{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else field.current_value or '' }}">
{% if field.description %}<small>{{ field.description }}</small>{% endif %} {% if field.description %}<small>{{ field.description }}</small>{% endif %}
{% if errors and errors[fk] %} {% if errors and errors[fk] %}
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small> <small class="field-error">{{ errors[fk] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "number" %} {% elif field.widget == "number" %}
<label for="{{ fk }}">{{ field.label }}</label> <label for="{{ fk }}">{{ field.label }}</label>
@ -61,7 +61,7 @@
value="{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else field.current_value or '' }}"> value="{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else field.current_value or '' }}">
{% if field.description %}<small>{{ field.description }}</small>{% endif %} {% if field.description %}<small>{{ field.description }}</small>{% endif %}
{% if errors and errors[fk] %} {% if errors and errors[fk] %}
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small> <small class="field-error">{{ errors[fk] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "checkbox" %} {% elif field.widget == "checkbox" %}
<label> <label>
@ -71,7 +71,7 @@
</label> </label>
{% if field.description %}<small>{{ field.description }}</small>{% endif %} {% if field.description %}<small>{{ field.description }}</small>{% endif %}
{% if errors and errors[fk] %} {% if errors and errors[fk] %}
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small> <small class="field-error">{{ errors[fk] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "json" %} {% elif field.widget == "json" %}
<label for="{{ fk }}">{{ field.label }}</label> <label for="{{ fk }}">{{ field.label }}</label>
@ -79,7 +79,7 @@
placeholder="{}">{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else (field.current_value | tojson if field.current_value else '') }}</textarea> placeholder="{}">{{ backend_form_data[fk] if backend_form_data and fk in backend_form_data else (field.current_value | tojson if field.current_value else '') }}</textarea>
<small>JSON object{% if field.description %} — {{ field.description }}{% endif %}</small> <small>JSON object{% if field.description %} — {{ field.description }}{% endif %}</small>
{% if errors and errors[fk] %} {% if errors and errors[fk] %}
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small> <small class="field-error">{{ errors[fk] }}</small>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View file

@ -6,194 +6,6 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<style>
#events-map {
height: 400px;
margin-bottom: 0.5rem;
border-radius: var(--pico-border-radius);
}
.map-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.map-legend {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
font-size: 0.85rem;
}
.map-legend-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.map-legend-swatch {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
}
#fit-to-results {
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
}
.events-table {
font-size: 0.9rem;
}
.events-table td {
vertical-align: middle;
}
.events-table tr.event-row:hover {
background-color: var(--pico-primary-focus);
cursor: pointer;
}
.events-table tr.event-row.highlighted {
background-color: var(--pico-primary-background);
}
.expand-row {
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
font-size: 0.9rem;
color: var(--pico-color);
}
.expand-row:hover {
color: var(--pico-primary);
}
.event-detail td {
background-color: var(--pico-card-background-color);
padding: 1rem;
}
.event-detail-list {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1rem;
margin: 0;
}
.event-detail-list dt {
font-weight: 600;
color: var(--pico-muted-color);
}
.event-detail-list dd {
margin: 0;
}
.event-data-pre {
max-height: 300px;
overflow: auto;
font-size: 0.8rem;
background: var(--pico-code-background-color);
padding: 0.5rem;
border-radius: var(--pico-border-radius);
margin: 0;
}
.filter-form .grid {
margin-bottom: 0.5rem;
}
.filter-form label {
margin-bottom: 0.25rem;
}
.filter-form input, .filter-form select {
margin-bottom: 0.5rem;
}
.pagination-info {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
/* --- v0.7.1 filter row (functional layout; full polish in a later pass) --- */
.filter-search { width: 100%; margin-bottom: 0.5rem; }
.filter-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-start; }
.filter-actions { display: flex; gap: 0.5rem; margin-top: 0.5rem; justify-content: flex-end; }
.filter-apply { width: 120px; }
.chip-picker { position: relative; }
.chip-picker-toggle { white-space: nowrap; }
.chip-picker-panel {
position: absolute; z-index: 1000; top: 100%; left: 0; margin-top: 0.25rem;
min-width: 16rem; max-height: 22rem; overflow-y: auto;
background: var(--pico-card-background-color);
border: 1px solid var(--pico-muted-border-color);
border-radius: 0.375rem; padding: 0.5rem; box-shadow: 0 4px 14px rgba(0,0,0,0.18);
}
.chip-picker-search { width: 100%; margin-bottom: 0.5rem; }
.chip-group-header { font-size: 0.75rem; font-weight: 600; color: var(--pico-muted-color);
text-transform: uppercase; margin: 0.4rem 0 0.2rem; }
.chip-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0;
cursor: pointer; font-weight: 400; }
.chip-row input[type="checkbox"] { width: auto; margin: 0; }
.chip-swatch { width: 0.85rem; height: 0.85rem; border-radius: 2px; flex: 0 0 auto; }
.chip-row.chip-hidden { display: none; }
.time-preset { display: block; width: 100%; text-align: left; background: none;
border: none; padding: 0.35rem 0.4rem; color: var(--pico-color); cursor: pointer; }
.time-preset:hover, .time-preset.selected { background: var(--pico-primary-focus); }
.time-custom { border-top: 1px solid var(--pico-muted-border-color); margin-top: 0.4rem;
padding-top: 0.4rem; display: flex; flex-direction: column; gap: 0.3rem; }
#active-pills .active-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center;
margin: 0.5rem 0; }
.active-pills-label { font-size: 0.8rem; color: var(--pico-muted-color); }
.filter-pill { display: inline-flex; align-items: center; gap: 0.35rem;
background: var(--pico-primary-focus); border-radius: 999px; padding: 0.15rem 0.6rem;
font-size: 0.85rem; }
.pill-remove, .pill-clear-all { background: none; border: none; cursor: pointer;
color: var(--pico-color); padding: 0; font-size: 1rem; line-height: 1; width: auto; }
.pill-clear-all { font-size: 0.8rem; text-decoration: underline; }
/* --- v0.7.2 map markers: per-event_type shape + per-severity opacity --- */
.evt-marker { width: 15px; height: 15px; box-sizing: border-box;
border: 1px solid rgba(0,0,0,0.55); }
.evt-circle { border-radius: 50%; }
.evt-square { border-radius: 1px; }
.evt-triangle { border: none; clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
.evt-star { border: none; clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%,
79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); }
.evt-marker.evt-hl { filter: drop-shadow(0 0 4px #ff3333); transform: scale(1.35); }
.map-filter-toggle { display: inline-flex; align-items: center; gap: 0.35rem;
font-size: 0.85rem; cursor: pointer; }
.map-filter-toggle input { width: auto; margin: 0; }
/* --- v0.7.3 row stability: fixed layout, single-line cells, ellipsis --- */
.events-table { table-layout: fixed; width: 100%; }
.events-table th:nth-child(1), .events-table td:nth-child(1) { width: 2rem; }
.events-table th:nth-child(2), .events-table td:nth-child(2) { width: 8.5rem; }
.events-table th:nth-child(3), .events-table td:nth-child(3) { width: 22%; }
.events-table th:nth-child(5), .events-table td:nth-child(5) { width: 9rem; }
.events-table tbody tr.event-row > td {
height: 37px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cell-time { font-variant-numeric: tabular-nums; }
.adapter-chip { display: inline-flex; align-items: center; gap: 0.35rem;
max-width: 100%; overflow: hidden; }
.adapter-chip-swatch { width: 0.7rem; height: 0.7rem; border-radius: 2px; flex: 0 0 auto; }
.adapter-chip { white-space: nowrap; text-overflow: ellipsis; }
/* --- v0.7.3 collapsible grouped legend --- */
.legend-toggle { font-size: 0.8rem; padding: 0.25rem 0.6rem; }
.legend-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
gap: 0.2rem 0.75rem; margin-top: 0.5rem; }
.legend-group { break-inside: avoid; }
.legend-group-header { font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
color: var(--pico-muted-color); margin: 0.3rem 0 0.15rem; }
.legend-chip { display: flex; align-items: center; gap: 0.4rem; width: 100%;
background: none; border: none; padding: 0.12rem 0.2rem; cursor: pointer;
color: var(--pico-color); font-size: 0.82rem; text-align: left; }
.legend-chip:hover { background: var(--pico-primary-focus); }
.legend-chip-swatch { width: 0.8rem; height: 0.8rem; border-radius: 2px; flex: 0 0 auto; }
.legend-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* --- v0.7.3 paginator --- */
.paginator { display: flex; flex-wrap: wrap; justify-content: space-between;
align-items: center; gap: 0.5rem; margin-top: 1rem; }
.paginator-pages { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
.page-link { padding: 0.2rem 0.55rem; font-size: 0.85rem; border-radius: 0.3rem;
text-decoration: none; }
.page-link.current { background: var(--pico-primary); color: var(--pico-primary-inverse);
font-weight: 600; }
.page-link.disabled { opacity: 0.4; pointer-events: none; }
.page-ellipsis { padding: 0 0.2rem; color: var(--pico-muted-color); }
.paginator-meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.85rem;
color: var(--pico-muted-color); }
.per-page select { width: auto; display: inline-block; padding: 0.1rem 1.5rem 0.1rem 0.4rem;
margin: 0; height: auto; }
</style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -206,9 +18,7 @@
<h1>{{ "Telemetry" if base_path == "/telemetry" else "Events" }}</h1> <h1>{{ "Telemetry" if base_path == "/telemetry" else "Events" }}</h1>
{% if filter_error %} {% if filter_error %}
<article aria-label="Filter Error" style="background-color: var(--pico-del-color); padding: 1rem; margin-bottom: 1rem;"> <div class="flash flash-error" role="alert"><strong>Filter Error:</strong> {{ filter_error }}</div>
<strong>Filter Error:</strong> {{ filter_error }}
</article>
{% endif %} {% endif %}
{% from "_chip_picker.html" import chip_picker %} {% from "_chip_picker.html" import chip_picker %}
@ -232,7 +42,7 @@
{# Time preset dropdown (bespoke; not a chip-picker). #} {# Time preset dropdown (bespoke; not a chip-picker). #}
<div class="chip-picker time-picker" data-field="time"> <div class="chip-picker time-picker" data-field="time">
<button type="button" class="chip-picker-toggle outline" aria-expanded="false" <button type="button" class="chip-picker-toggle" aria-expanded="false"
data-toggle="time" id="time-toggle">Time</button> data-toggle="time" id="time-toggle">Time</button>
<input type="hidden" name="time" id="filter-time" value="{{ filter_state.time_token }}"> <input type="hidden" name="time" id="filter-time" value="{{ filter_state.time_token }}">
<div class="chip-picker-panel" data-panel="time" hidden> <div class="chip-picker-panel" data-panel="time" hidden>
@ -260,21 +70,32 @@
{{ '' if filter_state.map_filter else 'disabled' }}> {{ '' if filter_state.map_filter else 'disabled' }}>
<div class="filter-actions"> <div class="filter-actions">
<a href="{{ base_path }}" role="button" class="btn-outline" id="filter-clear-all">Clear all</a>
<button type="submit" class="filter-apply">Apply</button> <button type="submit" class="filter-apply">Apply</button>
<a href="{{ base_path }}" role="button" class="outline" id="filter-clear-all">Clear all</a>
</div> </div>
</form> </form>
{# Active filter pills — server-rendered; updated out-of-band on each swap. #} {# Active filter pills — server-rendered; updated out-of-band on each swap. #}
<div id="active-pills">{% include "_active_pills.html" %}</div> <div id="active-pills">{% include "_active_pills.html" %}</div>
<div class="map-container">
<div id="events-map"></div> <div id="events-map"></div>
<div class="map-controls"> {# Map toolbar overlays the map (top-right). JS binds #fit-to-results +
#map-filter-toggle; the label carries .on styling when the box is checked. #}
<div class="map-toolbar">
<button type="button" id="fit-to-results" class="tb">Fit to results</button>
<label class="map-filter-toggle tb">
<input type="checkbox" id="map-filter-toggle" {{ 'checked' if filter_state.map_filter else '' }}>
Filter table by map view
</label>
</div>
</div>
{# Adapter legend: collapsed by default; expands to domain-grouped chips {# Adapter legend: collapsed by default; expands to domain-grouped chips
(same grouping as the v0.7.1 chip-picker). Clicking a chip toggles that (same grouping as the v0.7.1 chip-picker). Clicking a chip toggles that
adapter's filter (reuses the chip-picker's hidden CSV via syncField). #} adapter's filter (reuses the chip-picker's hidden CSV via syncField). #}
<div class="map-legend"> <div class="map-legend">
<button type="button" id="legend-toggle" class="legend-toggle outline secondary" <button type="button" id="legend-toggle"
aria-expanded="false">{{ adapters | length }} adapters · Show legend ▾</button> aria-expanded="false">{{ adapters | length }} adapters · Show legend ▾</button>
<div id="legend-body" class="legend-body" hidden> <div id="legend-body" class="legend-body" hidden>
{% for group_label, items in adapters_grouped %} {% for group_label, items in adapters_grouped %}
@ -290,12 +111,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<label class="map-filter-toggle">
<input type="checkbox" id="map-filter-toggle" {{ 'checked' if filter_state.map_filter else '' }}>
Filter table by map view
</label>
<button type="button" id="fit-to-results" class="outline secondary">Fit map to results</button>
</div>
<div id="events-rows"> <div id="events-rows">
{% include "_events_rows.html" %} {% include "_events_rows.html" %}

View file

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div class="grid"> <div class="cols">
<article> <article>
<header>Events (24h)</header> <header>Events (24h)</header>
<div hx-get="/dashboard/events" hx-trigger="load, every 10s" hx-swap="innerHTML"> <div hx-get="/dashboard/events" hx-trigger="load, every 10s" hx-swap="innerHTML">

View file

@ -3,31 +3,25 @@
{% block title %}Central - Login{% endblock %} {% block title %}Central - Login{% endblock %}
{% block content %} {% block content %}
<article> <div class="auth-card card">
<header>
<h1>Login</h1> <h1>Login</h1>
</header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/login" method="post"> <form action="/login" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label for="username"> <label for="username">Username</label>
Username
<input type="text" id="username" name="username" required <input type="text" id="username" name="username" required
autocomplete="username" autofocus> autocomplete="username" autofocus>
</label>
<label for="password"> <label for="password" style="margin-top: 12px;">Password</label>
Password
<input type="password" id="password" name="password" required <input type="password" id="password" name="password" required
autocomplete="current-password"> autocomplete="current-password">
</label>
<button type="submit">Login</button> <button type="submit" style="width: 100%; margin-top: 18px;">Login</button>
</form> </form>
</article> </div>
{% endblock %} {% endblock %}

View file

@ -10,7 +10,7 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/setup" method="post"> <form action="/setup" method="post">

View file

@ -21,7 +21,7 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/setup/adapters" method="post"> <form action="/setup/adapters" method="post">
@ -31,7 +31,7 @@
<details open style="margin-bottom: 2rem;"> <details open style="margin-bottom: 2rem;">
<summary><strong>{{ adapter.display_name or adapter.name }}</strong></summary> <summary><strong>{{ adapter.display_name or adapter.name }}</strong></summary>
<div style="padding: 1rem; border-left: 3px solid var(--pico-primary);"> <div style="padding: 1rem; border-left: 3px solid var(--accent);">
<label> <label>
<input type="checkbox" name="{{ adapter.name }}_enabled" <input type="checkbox" name="{{ adapter.name }}_enabled"
{% if form_data and form_data.get(adapter.name + '_enabled') %}checked {% if form_data and form_data.get(adapter.name + '_enabled') %}checked
@ -39,14 +39,14 @@
Enabled Enabled
</label> </label>
{% if errors and errors.get(adapter.name + '_enabled') %} {% if errors and errors.get(adapter.name + '_enabled') %}
<small style="color: var(--pico-color-red-500); display: block;">{{ errors[adapter.name + '_enabled'] }}</small> <small class="field-error">{{ errors[adapter.name + '_enabled'] }}</small>
{% endif %} {% endif %}
<label for="{{ adapter.name }}_cadence_s">Cadence (seconds)</label> <label for="{{ adapter.name }}_cadence_s">Cadence (seconds)</label>
<input type="number" id="{{ adapter.name }}_cadence_s" name="{{ adapter.name }}_cadence_s" <input type="number" id="{{ adapter.name }}_cadence_s" name="{{ adapter.name }}_cadence_s"
value="{{ form_data.get(adapter.name + '_cadence_s') if form_data else adapter.cadence_s }}"> value="{{ form_data.get(adapter.name + '_cadence_s') if form_data else adapter.cadence_s }}">
{% if errors and errors.get(adapter.name + '_cadence_s') %} {% if errors and errors.get(adapter.name + '_cadence_s') %}
<small style="color: var(--pico-color-red-500);">{{ errors[adapter.name + '_cadence_s'] }}</small> <small class="field-error">{{ errors[adapter.name + '_cadence_s'] }}</small>
{% endif %} {% endif %}
{% for field in adapter.fields %} {% for field in adapter.fields %}
@ -61,7 +61,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "api_key_select" %} {% elif field.widget == "api_key_select" %}
@ -79,7 +79,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "number" %} {% elif field.widget == "number" %}
@ -91,7 +91,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "checkbox" %} {% elif field.widget == "checkbox" %}
@ -105,7 +105,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "csv" %} {% elif field.widget == "csv" %}
@ -115,7 +115,7 @@
{% if field.required %}required{% endif %}> {% if field.required %}required{% endif %}>
<small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small> <small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small>
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "select" %} {% elif field.widget == "select" %}
@ -132,7 +132,7 @@
<small>{{ field.description }}</small> <small>{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "checkboxes" %} {% elif field.widget == "checkboxes" %}
@ -149,7 +149,7 @@
<small style="display: block;">{{ field.description }}</small> <small style="display: block;">{{ field.description }}</small>
{% endif %} {% endif %}
{% if errors and errors.get(form_key) %} {% if errors and errors.get(form_key) %}
<small style="color: var(--pico-color-red-500); display: block;">{{ errors[form_key] }}</small> <small class="field-error">{{ errors[form_key] }}</small>
{% endif %} {% endif %}
{% elif field.widget == "region" %} {% elif field.widget == "region" %}
@ -168,7 +168,7 @@
<div id="region-map-{{ adapter.name }}" style="height: 300px; margin-bottom: 1rem;"></div> <div id="region-map-{{ adapter.name }}" style="height: 300px; margin-bottom: 1rem;"></div>
<div class="grid"> <div class="cols">
<div> <div>
<label>North</label> <label>North</label>
<input type="number" name="{{ region_key }}_north" step="0.0001" min="-90" max="90" readonly <input type="number" name="{{ region_key }}_north" step="0.0001" min="-90" max="90" readonly
@ -191,7 +191,7 @@
</div> </div>
</div> </div>
{% if errors and errors.get(region_key) %} {% if errors and errors.get(region_key) %}
<small style="color: var(--pico-color-red-500);">{{ errors[region_key] }}</small> <small class="field-error">{{ errors[region_key] }}</small>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -200,8 +200,8 @@
</details> </details>
{% endfor %} {% endfor %}
<div style="display: flex; gap: 1rem; margin-top: 1rem;"> <div class="wizard-actions">
<a href="/setup/keys" role="button" class="outline">&larr; Back</a> <a href="/setup/keys" role="button" class="btn-outline">&larr; Back</a>
<button type="submit">Next &rarr;</button> <button type="submit">Next &rarr;</button>
</div> </div>
</form> </form>

View file

@ -14,12 +14,12 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<h2>Summary</h2> <h2>Summary</h2>
<table> <div class="table-wrap"><table>
<tbody> <tbody>
<tr> <tr>
<th>Operators</th> <th>Operators</th>
@ -34,10 +34,10 @@
<td style="word-break: break-all;">{{ system.map_tile_url }}</td> <td style="word-break: break-all;">{{ system.map_tile_url }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table></div>
<h3>Adapters</h3> <h3>Adapters</h3>
<table> <div class="table-wrap"><table>
<thead> <thead>
<tr> <tr>
<th>Adapter</th> <th>Adapter</th>
@ -51,21 +51,21 @@
<td><strong>{{ adapter.name }}</strong></td> <td><strong>{{ adapter.name }}</strong></td>
<td> <td>
{% if adapter.enabled %} {% if adapter.enabled %}
<span style="color: var(--pico-color-green-500);">Enabled</span> <span class="status-ok">Enabled</span>
{% else %} {% else %}
<span style="color: var(--pico-color-grey-500);">Disabled</span> <span class="muted">Disabled</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ adapter.cadence_s }}s</td> <td>{{ adapter.cadence_s }}s</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table></div>
<form action="/setup/finish" method="post"> <form action="/setup/finish" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display: flex; gap: 1rem; margin-top: 2rem;"> <div class="wizard-actions">
<a href="/setup/adapters" role="button" class="outline">&larr; Back</a> <a href="/setup/adapters" role="button" class="btn-outline">&larr; Back</a>
<button type="submit">Finish Setup</button> <button type="submit">Finish Setup</button>
</div> </div>
</form> </form>

View file

@ -14,16 +14,16 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
{% if success %} {% if success %}
<p style="color: var(--pico-color-green-500);">{{ success }}</p> <div class="flash flash-ok" role="status">{{ success }}</div>
{% endif %} {% endif %}
{% if keys %} {% if keys %}
<h2>Existing Keys</h2> <h2>Existing Keys</h2>
<table> <div class="table-wrap"><table>
<thead> <thead>
<tr> <tr>
<th>Alias</th> <th>Alias</th>
@ -38,7 +38,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table></div>
{% else %} {% else %}
<p><em>No API keys configured yet.</em></p> <p><em>No API keys configured yet.</em></p>
{% endif %} {% endif %}
@ -48,13 +48,13 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="add"> <input type="hidden" name="action" value="add">
<div class="grid"> <div class="cols">
<div> <div>
<label for="alias">Alias</label> <label for="alias">Alias</label>
<input type="text" id="alias" name="alias" placeholder="e.g., firms" <input type="text" id="alias" name="alias" placeholder="e.g., firms"
value="{{ form_data.alias if form_data else '' }}" maxlength="64"> value="{{ form_data.alias if form_data else '' }}" maxlength="64">
{% if errors and errors.alias %} {% if errors and errors.alias %}
<small style="color: var(--pico-color-red-500);">{{ errors.alias }}</small> <small class="field-error">{{ errors.alias }}</small>
{% else %} {% else %}
<small>Letters, numbers, and underscores only.</small> <small>Letters, numbers, and underscores only.</small>
{% endif %} {% endif %}
@ -64,14 +64,14 @@
<input type="password" id="plaintext_key" name="plaintext_key" <input type="password" id="plaintext_key" name="plaintext_key"
placeholder="Paste your API key"> placeholder="Paste your API key">
{% if errors and errors.plaintext_key %} {% if errors and errors.plaintext_key %}
<small style="color: var(--pico-color-red-500);">{{ errors.plaintext_key }}</small> <small class="field-error">{{ errors.plaintext_key }}</small>
{% else %} {% else %}
<small>Will be encrypted before storage.</small> <small>Will be encrypted before storage.</small>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<button type="submit" class="outline">Add Key</button> <button type="submit" class="btn-outline" style="margin-top: 12px;">Add Key</button>
</form> </form>
<hr> <hr>
@ -79,8 +79,8 @@
<form action="/setup/keys" method="post"> <form action="/setup/keys" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="next"> <input type="hidden" name="action" value="next">
<div style="display: flex; gap: 1rem;"> <div class="wizard-actions">
<a href="/setup/system" role="button" class="outline">&larr; Back</a> <a href="/setup/system" role="button" class="btn-outline">&larr; Back</a>
<button type="submit">Next &rarr;</button> <button type="submit">Next &rarr;</button>
</div> </div>
</form> </form>

View file

@ -14,7 +14,7 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/setup/operator" method="post"> <form action="/setup/operator" method="post">

View file

@ -14,7 +14,7 @@
</header> </header>
{% if error %} {% if error %}
<p style="color: var(--pico-color-red-500);">{{ error }}</p> <div class="flash flash-error" role="alert">{{ error }}</div>
{% endif %} {% endif %}
<form action="/setup/system" method="post"> <form action="/setup/system" method="post">
@ -27,7 +27,7 @@
<small>Use {z}, {x}, {y} placeholders. Example: https://tile.openstreetmap.org/{z}/{x}/{y}.png</small> <small>Use {z}, {x}, {y} placeholders. Example: https://tile.openstreetmap.org/{z}/{x}/{y}.png</small>
</label> </label>
{% if errors and errors.map_tile_url %} {% if errors and errors.map_tile_url %}
<small style="color: var(--pico-color-red-500);">{{ errors.map_tile_url }}</small> <small class="field-error">{{ errors.map_tile_url }}</small>
{% endif %} {% endif %}
<label for="map_attribution"> <label for="map_attribution">
@ -37,11 +37,11 @@
<small>Credit the map provider (required by most tile services).</small> <small>Credit the map provider (required by most tile services).</small>
</label> </label>
{% if errors and errors.map_attribution %} {% if errors and errors.map_attribution %}
<small style="color: var(--pico-color-red-500);">{{ errors.map_attribution }}</small> <small class="field-error">{{ errors.map_attribution }}</small>
{% endif %} {% endif %}
<div style="display: flex; gap: 1rem; margin-top: 1rem;"> <div class="wizard-actions">
<a href="/setup/operator" role="button" class="outline">&larr; Back</a> <a href="/setup/operator" role="button" class="btn-outline">&larr; Back</a>
<button type="submit">Next &rarr;</button> <button type="submit">Next &rarr;</button>
</div> </div>
</form> </form>

View file

@ -5,20 +5,20 @@
{% block content %} {% block content %}
<h1>Streams</h1> <h1>Streams</h1>
<div class="grid"> <div class="cols">
{% for stream in streams %} {% for stream in streams %}
<article> <article>
<header style="display: flex; justify-content: space-between; align-items: center;"> <header style="display: flex; justify-content: space-between; align-items: center;">
<strong>{{ stream.name }}</strong> <strong>{{ stream.name }}</strong>
{% if stream.managed_max_bytes %} {% if stream.managed_max_bytes %}
<span style="font-size: 0.8em; background: var(--pico-secondary-background); padding: 0.2em 0.5em; border-radius: 4px;">Managed by supervisor</span> <span style="font-size: 0.8em; background: var(--bg-chip); color: var(--ink-muted); padding: 0.2em 0.5em; border-radius: 4px;">Managed by supervisor</span>
{% endif %} {% endif %}
</header> </header>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<strong>Live Data:</strong> <strong>Live Data:</strong>
{% if stream.error %} {% if stream.error %}
<span style="color: var(--pico-color-red-500);">({{ stream.error }})</span> <span class="error">({{ stream.error }})</span>
{% else %} {% else %}
<ul style="margin: 0.5rem 0;"> <ul style="margin: 0.5rem 0;">
<li>Messages: {{ stream.live_messages }}</li> <li>Messages: {{ stream.live_messages }}</li>
@ -59,7 +59,7 @@
</div> </div>
{% if errors and errors[stream.name] %} {% if errors and errors[stream.name] %}
<p style="color: var(--pico-color-red-500);">{{ errors[stream.name] }}</p> <p class="error">{{ errors[stream.name] }}</p>
{% endif %} {% endif %}
<div> <div>
@ -71,7 +71,7 @@
<form action="/streams/{{ stream.name }}" method="post" style="margin: 0;"> <form action="/streams/{{ stream.name }}" method="post" style="margin: 0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="max_age_s" value="{{ preset_seconds }}"> <input type="hidden" name="max_age_s" value="{{ preset_seconds }}">
<button type="submit" class="{% if stream.max_age_s == preset_seconds %}contrast{% else %}outline{% endif %}" style="padding: 0.3em 0.6em; font-size: 0.9em;"> <button type="submit" class="{% if stream.max_age_s == preset_seconds %}btn-contrast{% else %}btn-outline{% endif %}" style="height: 30px; padding: 0 12px;">
{{ label }} {{ label }}
</button> </button>
</form> </form>
@ -83,7 +83,7 @@
<label for="custom_days_{{ stream.name }}" style="margin: 0;">Custom:</label> <label for="custom_days_{{ stream.name }}" style="margin: 0;">Custom:</label>
<input type="number" id="custom_days_{{ stream.name }}" name="custom_days" min="1" max="1825" placeholder="days" style="width: 100px;" onchange="this.form.max_age_s.value = this.value * 86400;"> <input type="number" id="custom_days_{{ stream.name }}" name="custom_days" min="1" max="1825" placeholder="days" style="width: 100px;" onchange="this.form.max_age_s.value = this.value * 86400;">
<input type="hidden" name="max_age_s" value=""> <input type="hidden" name="max_age_s" value="">
<button type="submit" class="outline" style="padding: 0.3em 0.6em;">Save</button> <button type="submit" class="btn-outline" style="height: 34px;">Save</button>
</form> </form>
</div> </div>
</article> </article>