mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
Merge pull request #58 from zvx-echo6/feat/visual-rewrite-v075
feat(visual-rewrite): drop CSS framework, adopt gui_preview design system across every page (v0.7.5)
This commit is contained in:
commit
6546db0144
24 changed files with 828 additions and 408 deletions
632
src/central/gui/static/css/central.css
Normal file
632
src/central/gui/static/css/central.css
Normal 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; }
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
{% if preview_error %}
|
||||
<article aria-label="Preview Unavailable" style="background-color: var(--pico-del-color); margin-bottom: 1rem;">
|
||||
<strong>{{ preview_error }}</strong>
|
||||
</article>
|
||||
<div class="flash flash-error"><strong>{{ preview_error }}</strong></div>
|
||||
{% elif preview_rows is not none %}
|
||||
<fieldset>
|
||||
<legend>Preview ({{ preview_rows|length }} rows)</legend>
|
||||
{% if preview_rows %}
|
||||
<table class="preview-table" role="grid">
|
||||
<div class="table-wrap"><table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in preview_rows[0].keys() %}<th>{{ col }}</th>{% endfor %}
|
||||
|
|
@ -19,7 +17,7 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
{% macro chip_picker(field, label, options, selected, grouped=False, searchable=False, with_swatch=False) %}
|
||||
<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">
|
||||
{{ label }}<span class="chip-count" data-count-for="{{ field }}">{{ (' (' ~ selected | length ~ ')') if selected else '' }}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
{% if filter_error %}
|
||||
<article aria-label="Filter Error" style="background-color: var(--pico-del-color); padding: 1rem; margin-bottom: 1rem;">
|
||||
<strong>Filter Error:</strong> {{ filter_error }}
|
||||
</article>
|
||||
<div class="flash flash-error" role="alert"><strong>Filter Error:</strong> {{ filter_error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if events %}
|
||||
<div class="table-wrap">
|
||||
<table class="events-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -69,6 +68,7 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Real offset paginator (v0.7.3). Each link carries offset + the filter
|
||||
query_string (which excludes cursor/offset); limit persists via query_string. #}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
data-tile-url="{{ tile_url }}"
|
||||
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>
|
||||
<label for="region_north">North</label>
|
||||
<input type="number" id="region_north" name="region_north" step="0.0001" min="-90" max="90" readonly
|
||||
|
|
@ -32,10 +32,10 @@
|
|||
</div>
|
||||
|
||||
{% if errors and errors.region %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors.region }}</small>
|
||||
<small class="field-error">{{ errors.region }}</small>
|
||||
{% 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>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -11,25 +11,21 @@
|
|||
|
||||
{% block content %}
|
||||
<h1>{{ adapter.display_name }}</h1>
|
||||
<p class="secondary">{{ adapter.description }}</p>
|
||||
<p class="muted">{{ adapter.description }}</p>
|
||||
|
||||
{% if adapter.paused_at %}
|
||||
<article aria-label="Adapter Paused" style="background-color: var(--pico-mark-background-color); margin-bottom: 1rem;">
|
||||
<strong>⏸️ Paused</strong> since {{ adapter.paused_at }}
|
||||
</article>
|
||||
<div class="flash flash-warn"><strong>⏸️ Paused</strong> since {{ adapter.paused_at }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if adapter.last_error %}
|
||||
<article aria-label="Last Error" style="background-color: var(--pico-del-color); margin-bottom: 1rem;">
|
||||
<strong>Last Error:</strong> {{ adapter.last_error }}
|
||||
</article>
|
||||
<div class="flash flash-error"><strong>Last Error:</strong> {{ adapter.last_error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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.
|
||||
<a href="/api-keys">Configure API Keys</a>
|
||||
</article>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/adapters/{{ adapter.name }}">
|
||||
|
|
@ -48,7 +44,7 @@
|
|||
value="{{ form_data.cadence_s if form_data else adapter.cadence_s }}"
|
||||
required>
|
||||
{% 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 %}
|
||||
</fieldset>
|
||||
|
||||
|
|
@ -68,7 +64,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "number" %}
|
||||
|
|
@ -80,7 +76,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "checkbox" %}
|
||||
|
|
@ -95,7 +91,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "csv" %}
|
||||
|
|
@ -105,7 +101,7 @@
|
|||
{% if field.required %}required{% endif %}>
|
||||
<small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small>
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "select" %}
|
||||
|
|
@ -122,7 +118,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "checkboxes" %}
|
||||
|
|
@ -139,7 +135,7 @@
|
|||
<small style="display: block;">{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "api_key_select" %}
|
||||
|
|
@ -157,7 +153,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
|
|
@ -181,6 +177,6 @@
|
|||
{% include "_adapter_preview.html" %}
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<td>
|
||||
{{ adapter.display_name or adapter.name }}
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>{% if adapter.enabled %}Yes{% else %}No{% endif %}</td>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
placeholder="Paste your new API key here" required maxlength="4096"
|
||||
aria-invalid="{{ 'true' if errors and errors.new_plaintext_key else 'false' }}">
|
||||
{% 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 %}
|
||||
<small>The key will be encrypted before storage.</small>
|
||||
{% endif %}
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
<header><strong>Delete Key</strong></header>
|
||||
|
||||
{% 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(', ') }}.
|
||||
Remove these references from the adapters first.
|
||||
</p>
|
||||
|
|
@ -51,12 +51,12 @@
|
|||
<p>Permanently delete this API key. This action cannot be undone.</p>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="/api-keys/{{ key.alias }}/delete" method="post">
|
||||
<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>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<p><a href="/api-keys/new" role="button">Add New Key</a></p>
|
||||
|
||||
{% if keys %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<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>{% if key.used_by %}{{ key.used_by | join(', ') }}{% else %}<em>(none)</em>{% endif %}</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No API keys configured.</p>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
placeholder="e.g., firms_production" required maxlength="64"
|
||||
aria-invalid="{{ 'true' if errors and errors.alias else 'false' }}">
|
||||
{% if errors and errors.alias %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors.alias }}</small>
|
||||
<small class="field-error">{{ errors.alias }}</small>
|
||||
{% else %}
|
||||
<small>Letters, numbers, and underscores only. Max 64 characters.</small>
|
||||
{% endif %}
|
||||
|
|
@ -23,14 +23,14 @@
|
|||
placeholder="Paste your API key here" required maxlength="4096"
|
||||
aria-invalid="{{ 'true' if errors and errors.plaintext_key else 'false' }}">
|
||||
{% 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 %}
|
||||
<small>The key will be encrypted before storage. You will not be able to view it again.</small>
|
||||
{% endif %}
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<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>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><strong>Central</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<nav class="nav">
|
||||
<div class="brand">Central</div>
|
||||
{% if operator %}
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/adapters">Adapters</a></li>
|
||||
<li><a href="/events">Events</a></li>
|
||||
<li><a href="/telemetry">Telemetry</a></li>
|
||||
<li><a href="/streams">Streams</a></li>
|
||||
<li><a href="/enrichment">Enrichment</a></li>
|
||||
<li><a href="/api-keys">API Keys</a></li>
|
||||
<li>{{ operator.username }}</li>
|
||||
<li><a href="/change-password">Change Password</a></li>
|
||||
<li>
|
||||
<div class="nav-links">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/adapters">Adapters</a>
|
||||
<a href="/events">Events</a>
|
||||
<a href="/telemetry">Telemetry</a>
|
||||
<a href="/streams">Streams</a>
|
||||
<a href="/enrichment">Enrichment</a>
|
||||
<a href="/api-keys">API Keys</a>
|
||||
<span class="sep">·</span>
|
||||
<span class="who">{{ operator.username }}</span>
|
||||
<a href="/change-password">Change Password</a>
|
||||
<form action="/logout" method="post" style="margin: 0;">
|
||||
<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>
|
||||
</li>
|
||||
</div>
|
||||
{% else %}
|
||||
<li><a href="/login">Login</a></li>
|
||||
<div class="nav-links">
|
||||
<a href="/login">Login</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container">
|
||||
<main class="page">
|
||||
{% if error %}
|
||||
<article aria-label="Error">
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
</article>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<article aria-label="Success">
|
||||
<p style="color: var(--pico-color-green-500);">{{ success }}</p>
|
||||
</article>
|
||||
<div class="flash flash-ok" role="status">{{ success }}</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><strong>Central</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>Setup Wizard</li>
|
||||
</ul>
|
||||
<nav class="nav">
|
||||
<div class="brand">Central</div>
|
||||
<div class="nav-links"><span class="who">Setup Wizard</span></div>
|
||||
</nav>
|
||||
<main class="container">
|
||||
<main class="page" style="max-width: 760px;">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,38 +3,30 @@
|
|||
{% block title %}Central - Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<div class="auth-card card">
|
||||
<h1>Change Password</h1>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/change-password" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
<label for="current_password">
|
||||
Current Password
|
||||
<label for="current_password">Current Password</label>
|
||||
<input type="password" id="current_password" name="current_password" required
|
||||
autocomplete="current-password" autofocus>
|
||||
</label>
|
||||
|
||||
<label for="new_password">
|
||||
New Password
|
||||
<label for="new_password" style="margin-top: 12px;">New Password</label>
|
||||
<input type="password" id="new_password" name="new_password" required
|
||||
autocomplete="new-password" minlength="8">
|
||||
<small>Minimum 8 characters</small>
|
||||
</label>
|
||||
|
||||
<label for="confirm_password">
|
||||
Confirm New Password
|
||||
<label for="confirm_password" style="margin-top: 12px;">Confirm New Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required
|
||||
autocomplete="new-password">
|
||||
</label>
|
||||
|
||||
<button type="submit">Change Password</button>
|
||||
<button type="submit" style="width: 100%; margin-top: 18px;">Change Password</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
<h1>Enrichment</h1>
|
||||
<p class="secondary">
|
||||
<p class="muted">
|
||||
Central-side event enrichment. Results are attached to each event under
|
||||
<code>data._enriched.<enricher></code>. Changes hot-reload into the
|
||||
supervisor; switching backend invalidates the enrichment cache. Backend
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
{% if field.required %}required{% endif %}>
|
||||
{% if field.description %}<small>{{ field.description }}</small>{% endif %}
|
||||
{% 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 %}
|
||||
{% elif field.widget == "number" %}
|
||||
<label for="{{ field.name }}">{{ field.label }}</label>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
{% if field.required %}required{% endif %}>
|
||||
{% if field.description %}<small>{{ field.description }}</small>{% endif %}
|
||||
{% 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 %}
|
||||
{% 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 '' }}">
|
||||
{% if field.description %}<small>{{ field.description }}</small>{% endif %}
|
||||
{% if errors and errors[fk] %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small>
|
||||
<small class="field-error">{{ errors[fk] }}</small>
|
||||
{% endif %}
|
||||
{% elif field.widget == "number" %}
|
||||
<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 '' }}">
|
||||
{% if field.description %}<small>{{ field.description }}</small>{% endif %}
|
||||
{% if errors and errors[fk] %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small>
|
||||
<small class="field-error">{{ errors[fk] }}</small>
|
||||
{% endif %}
|
||||
{% elif field.widget == "checkbox" %}
|
||||
<label>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</label>
|
||||
{% if field.description %}<small>{{ field.description }}</small>{% endif %}
|
||||
{% if errors and errors[fk] %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors[fk] }}</small>
|
||||
<small class="field-error">{{ errors[fk] }}</small>
|
||||
{% endif %}
|
||||
{% elif field.widget == "json" %}
|
||||
<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>
|
||||
<small>JSON object{% if field.description %} — {{ field.description }}{% endif %}</small>
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -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.markercluster@1.5.3/dist/MarkerCluster.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 %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -206,9 +18,7 @@
|
|||
<h1>{{ "Telemetry" if base_path == "/telemetry" else "Events" }}</h1>
|
||||
|
||||
{% if filter_error %}
|
||||
<article aria-label="Filter Error" style="background-color: var(--pico-del-color); padding: 1rem; margin-bottom: 1rem;">
|
||||
<strong>Filter Error:</strong> {{ filter_error }}
|
||||
</article>
|
||||
<div class="flash flash-error" role="alert"><strong>Filter Error:</strong> {{ filter_error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% from "_chip_picker.html" import chip_picker %}
|
||||
|
|
@ -232,7 +42,7 @@
|
|||
|
||||
{# Time preset dropdown (bespoke; not a chip-picker). #}
|
||||
<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>
|
||||
<input type="hidden" name="time" id="filter-time" value="{{ filter_state.time_token }}">
|
||||
<div class="chip-picker-panel" data-panel="time" hidden>
|
||||
|
|
@ -260,21 +70,32 @@
|
|||
{{ '' if filter_state.map_filter else 'disabled' }}>
|
||||
|
||||
<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>
|
||||
<a href="{{ base_path }}" role="button" class="outline" id="filter-clear-all">Clear all</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# Active filter pills — server-rendered; updated out-of-band on each swap. #}
|
||||
<div id="active-pills">{% include "_active_pills.html" %}</div>
|
||||
|
||||
<div id="events-map"></div>
|
||||
<div class="map-controls">
|
||||
{# Adapter legend: collapsed by default; expands to domain-grouped chips
|
||||
<div class="map-container">
|
||||
<div id="events-map"></div>
|
||||
{# 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
|
||||
(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). #}
|
||||
<div class="map-legend">
|
||||
<button type="button" id="legend-toggle" class="legend-toggle outline secondary"
|
||||
<div class="map-legend">
|
||||
<button type="button" id="legend-toggle"
|
||||
aria-expanded="false">{{ adapters | length }} adapters · Show legend ▾</button>
|
||||
<div id="legend-body" class="legend-body" hidden>
|
||||
{% for group_label, items in adapters_grouped %}
|
||||
|
|
@ -289,12 +110,6 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
<div class="grid">
|
||||
<div class="cols">
|
||||
<article>
|
||||
<header>Events (24h)</header>
|
||||
<div hx-get="/dashboard/events" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||
|
|
|
|||
|
|
@ -3,31 +3,25 @@
|
|||
{% block title %}Central - Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<div class="auth-card card">
|
||||
<h1>Login</h1>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/login" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
<label for="username">
|
||||
Username
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
autocomplete="username" autofocus>
|
||||
</label>
|
||||
|
||||
<label for="password">
|
||||
Password
|
||||
<label for="password" style="margin-top: 12px;">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
autocomplete="current-password">
|
||||
</label>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
<button type="submit" style="width: 100%; margin-top: 18px;">Login</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/setup" method="post">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/setup/adapters" method="post">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<details open style="margin-bottom: 2rem;">
|
||||
<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>
|
||||
<input type="checkbox" name="{{ adapter.name }}_enabled"
|
||||
{% if form_data and form_data.get(adapter.name + '_enabled') %}checked
|
||||
|
|
@ -39,14 +39,14 @@
|
|||
Enabled
|
||||
</label>
|
||||
{% 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 %}
|
||||
|
||||
<label for="{{ adapter.name }}_cadence_s">Cadence (seconds)</label>
|
||||
<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 }}">
|
||||
{% 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 %}
|
||||
|
||||
{% for field in adapter.fields %}
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "api_key_select" %}
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "number" %}
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "checkbox" %}
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "csv" %}
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
{% if field.required %}required{% endif %}>
|
||||
<small>Comma-separated values{% if field.description %} — {{ field.description }}{% endif %}</small>
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "select" %}
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
<small>{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "checkboxes" %}
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
<small style="display: block;">{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
{% elif field.widget == "region" %}
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
|
||||
<div id="region-map-{{ adapter.name }}" style="height: 300px; margin-bottom: 1rem;"></div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="cols">
|
||||
<div>
|
||||
<label>North</label>
|
||||
<input type="number" name="{{ region_key }}_north" step="0.0001" min="-90" max="90" readonly
|
||||
|
|
@ -191,7 +191,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -200,8 +200,8 @@
|
|||
</details>
|
||||
{% endfor %}
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<a href="/setup/keys" role="button" class="outline">← Back</a>
|
||||
<div class="wizard-actions">
|
||||
<a href="/setup/keys" role="button" class="btn-outline">← Back</a>
|
||||
<button type="submit">Next →</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>Summary</h2>
|
||||
|
||||
<table>
|
||||
<div class="table-wrap"><table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Operators</th>
|
||||
|
|
@ -34,10 +34,10 @@
|
|||
<td style="word-break: break-all;">{{ system.map_tile_url }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
|
||||
<h3>Adapters</h3>
|
||||
<table>
|
||||
<div class="table-wrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Adapter</th>
|
||||
|
|
@ -51,21 +51,21 @@
|
|||
<td><strong>{{ adapter.name }}</strong></td>
|
||||
<td>
|
||||
{% if adapter.enabled %}
|
||||
<span style="color: var(--pico-color-green-500);">Enabled</span>
|
||||
<span class="status-ok">Enabled</span>
|
||||
{% else %}
|
||||
<span style="color: var(--pico-color-grey-500);">Disabled</span>
|
||||
<span class="muted">Disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ adapter.cadence_s }}s</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
|
||||
<form action="/setup/finish" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
||||
<a href="/setup/adapters" role="button" class="outline">← Back</a>
|
||||
<div class="wizard-actions">
|
||||
<a href="/setup/adapters" role="button" class="btn-outline">← Back</a>
|
||||
<button type="submit">Finish Setup</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<p style="color: var(--pico-color-green-500);">{{ success }}</p>
|
||||
<div class="flash flash-ok" role="status">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if keys %}
|
||||
<h2>Existing Keys</h2>
|
||||
<table>
|
||||
<div class="table-wrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
{% else %}
|
||||
<p><em>No API keys configured yet.</em></p>
|
||||
{% endif %}
|
||||
|
|
@ -48,13 +48,13 @@
|
|||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="add">
|
||||
|
||||
<div class="grid">
|
||||
<div class="cols">
|
||||
<div>
|
||||
<label for="alias">Alias</label>
|
||||
<input type="text" id="alias" name="alias" placeholder="e.g., firms"
|
||||
value="{{ form_data.alias if form_data else '' }}" maxlength="64">
|
||||
{% if errors and errors.alias %}
|
||||
<small style="color: var(--pico-color-red-500);">{{ errors.alias }}</small>
|
||||
<small class="field-error">{{ errors.alias }}</small>
|
||||
{% else %}
|
||||
<small>Letters, numbers, and underscores only.</small>
|
||||
{% endif %}
|
||||
|
|
@ -64,14 +64,14 @@
|
|||
<input type="password" id="plaintext_key" name="plaintext_key"
|
||||
placeholder="Paste your API 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 %}
|
||||
<small>Will be encrypted before storage.</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="outline">Add Key</button>
|
||||
<button type="submit" class="btn-outline" style="margin-top: 12px;">Add Key</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
|
@ -79,8 +79,8 @@
|
|||
<form action="/setup/keys" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="next">
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<a href="/setup/system" role="button" class="outline">← Back</a>
|
||||
<div class="wizard-actions">
|
||||
<a href="/setup/system" role="button" class="btn-outline">← Back</a>
|
||||
<button type="submit">Next →</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/setup/operator" method="post">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</header>
|
||||
|
||||
{% if error %}
|
||||
<p style="color: var(--pico-color-red-500);">{{ error }}</p>
|
||||
<div class="flash flash-error" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
</label>
|
||||
{% 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 %}
|
||||
|
||||
<label for="map_attribution">
|
||||
|
|
@ -37,11 +37,11 @@
|
|||
<small>Credit the map provider (required by most tile services).</small>
|
||||
</label>
|
||||
{% 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 %}
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<a href="/setup/operator" role="button" class="outline">← Back</a>
|
||||
<div class="wizard-actions">
|
||||
<a href="/setup/operator" role="button" class="btn-outline">← Back</a>
|
||||
<button type="submit">Next →</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@
|
|||
{% block content %}
|
||||
<h1>Streams</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="cols">
|
||||
{% for stream in streams %}
|
||||
<article>
|
||||
<header style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<strong>{{ stream.name }}</strong>
|
||||
{% 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 %}
|
||||
</header>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<strong>Live Data:</strong>
|
||||
{% if stream.error %}
|
||||
<span style="color: var(--pico-color-red-500);">({{ stream.error }})</span>
|
||||
<span class="error">({{ stream.error }})</span>
|
||||
{% else %}
|
||||
<ul style="margin: 0.5rem 0;">
|
||||
<li>Messages: {{ stream.live_messages }}</li>
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
<div>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<form action="/streams/{{ stream.name }}" method="post" style="margin: 0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<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 }}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
<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="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>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue