1060 lines
59 KiB
HTML
1060 lines
59 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>T-Systems Regulation Hub</title>
|
|
<style>
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
DESIGN TOKENS
|
|
Note for developers: swap these six rail + six canvas tokens to re-skin.
|
|
Accent is always #e20074 (T-Systems Magenta).
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
:root {
|
|
/* Sidebar rail */
|
|
--rail-bg: #ffffff;
|
|
--rail-surface: #f7f8fa;
|
|
--rail-fg: #111827;
|
|
--rail-muted: #8b929e;
|
|
--rail-border: #e8eaed;
|
|
--rail-hover: rgba(0,0,0,.04);
|
|
--rail-active: rgba(226,0,116,.07);
|
|
|
|
/* Content canvas */
|
|
--bg: #f2f4f7;
|
|
--surface: #ffffff;
|
|
--fg: #111827;
|
|
--muted: #6b7280;
|
|
--border: #e5e7eb;
|
|
|
|
/* Brand */
|
|
--accent: #e20074;
|
|
--accent-dim: rgba(226,0,116,.10);
|
|
--accent-hover: #c8006a;
|
|
--success: #16a34a;
|
|
--warn: #d97706;
|
|
--danger: #dc2626;
|
|
|
|
/* Typography */
|
|
--font-display: "TeleNeoWeb-Bold","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
--font-body: "TeleNeoWeb-Regular","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
--font-mono: ui-monospace,"JetBrains Mono",Menlo,monospace;
|
|
|
|
/* Geometry */
|
|
--radius-sm: 6px;
|
|
--radius-md: 10px;
|
|
--radius-pill: 9999px;
|
|
--sidebar-w: 232px;
|
|
--shadow-card: 0 1px 4px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.04);
|
|
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
|
}
|
|
|
|
/* ─── Dark mode ────────────────────────────────────────────────────── */
|
|
[data-theme="dark"] {
|
|
--rail-bg: #1a1c22;
|
|
--rail-surface: #22242c;
|
|
--rail-fg: #f0f2f5;
|
|
--rail-muted: #7a8390;
|
|
--rail-border: #2d3038;
|
|
--rail-hover: rgba(255,255,255,.05);
|
|
--rail-active: rgba(226,0,116,.12);
|
|
--bg: #111318;
|
|
--surface: #1a1c22;
|
|
--fg: #f0f2f5;
|
|
--muted: #7a8390;
|
|
--border: #2d3038;
|
|
}
|
|
[data-theme="dark"] body { color-scheme: dark; }
|
|
[data-theme="dark"] .topbar { background: rgba(17,19,24,.9); }
|
|
[data-theme="dark"] .filter-bar,
|
|
[data-theme="dark"] .stats-bar-flush { background: var(--surface); }
|
|
[data-theme="dark"] .stat-cell { background: var(--surface); }
|
|
[data-theme="dark"] .ev-card { background: var(--surface); }
|
|
[data-theme="dark"] .detail-card,
|
|
[data-theme="dark"] .output-card,
|
|
[data-theme="dark"] .docs-card { background: var(--surface); }
|
|
[data-theme="dark"] .kpi { background: #1e2028; }
|
|
[data-theme="dark"] .task-row,
|
|
[data-theme="dark"] .program-row,
|
|
[data-theme="dark"] .event-row { background: #1e2028; }
|
|
|
|
/* ─── Reset ─────────────────────────────────────────────────────── */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
background: var(--bg); color: var(--fg);
|
|
font-family: var(--font-body); font-size: 14px; line-height: 1.6;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
h1,h2,h3,h4 { font-family: var(--font-display); line-height: 1.2; letter-spacing: -0.015em; text-wrap: balance; }
|
|
a { color: inherit; text-decoration: none; }
|
|
button, input, select { font: inherit; cursor: pointer; }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
APP SHELL
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
.app-shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
SIDEBAR
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
.sidebar {
|
|
position: sticky; top: 0; height: 100vh;
|
|
overflow-y: auto; overflow-x: hidden;
|
|
display: flex; flex-direction: column;
|
|
background: var(--rail-bg);
|
|
border-right: 1px solid var(--rail-border);
|
|
z-index: 20;
|
|
}
|
|
.sidebar-brand {
|
|
display: flex; align-items: center; gap: 10px;
|
|
height: 54px; padding: 0 16px;
|
|
border-bottom: 1px solid var(--rail-border); flex-shrink: 0;
|
|
}
|
|
.brand-logo {
|
|
width: 28px; height: 28px; background: var(--accent);
|
|
border-radius: 7px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
}
|
|
.brand-logo svg { color: #fff; }
|
|
.brand-name { font-family: var(--font-display); font-size: 13px; color: var(--rail-fg); font-weight: 700; line-height: 1.2; }
|
|
.brand-sub { font-size: 10px; color: var(--rail-muted); font-family: var(--font-mono); letter-spacing: .04em; margin-top: 2px; }
|
|
|
|
.sidebar-nav { flex: 1; padding: 10px 0; }
|
|
.nav-group { padding: 0 8px 4px; }
|
|
.nav-group + .nav-group { margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--rail-border); }
|
|
.nav-group-label {
|
|
display: block; font-family: var(--font-mono); font-size: 10px;
|
|
text-transform: uppercase; letter-spacing: .12em;
|
|
color: var(--rail-muted); padding: 0 8px 6px;
|
|
}
|
|
.nav-item {
|
|
display: flex; align-items: center; gap: 9px;
|
|
height: 34px; padding: 0 8px;
|
|
border-radius: var(--radius-sm); color: #4b5563;
|
|
font-size: 13px; position: relative;
|
|
transition: background 120ms, color 120ms;
|
|
}
|
|
.nav-item:hover { background: var(--rail-hover); color: var(--rail-fg); text-decoration: none; }
|
|
.nav-item.active { background: var(--rail-active); color: var(--accent); font-weight: 600; }
|
|
.nav-item.active::before {
|
|
content: ""; position: absolute; left: 0; top: 6px; bottom: 6px;
|
|
width: 3px; border-radius: 0 3px 3px 0; background: var(--accent);
|
|
}
|
|
.nav-icon { width: 15px; height: 15px; flex-shrink: 0; opacity: .55; }
|
|
.nav-item:hover .nav-icon, .nav-item.active .nav-icon { opacity: 1; }
|
|
.nav-badge {
|
|
margin-left: auto; min-width: 18px; height: 17px; padding: 0 5px;
|
|
border-radius: var(--radius-pill);
|
|
background: var(--accent-dim); color: var(--accent);
|
|
font-size: 10px; font-family: var(--font-mono); font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
|
|
.sidebar-footer { border-top: 1px solid var(--rail-border); padding: 10px 8px; flex-shrink: 0; }
|
|
.sidebar-user {
|
|
display: flex; align-items: center; gap: 9px;
|
|
padding: 8px; border-radius: var(--radius-sm); cursor: pointer;
|
|
transition: background 120ms;
|
|
}
|
|
.sidebar-user:hover { background: var(--rail-hover); }
|
|
.avatar {
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|
background: var(--accent); color: #fff;
|
|
font-size: 11px; font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
}
|
|
.user-name { font-size: 12px; font-weight: 600; color: var(--rail-fg); }
|
|
.user-role { font-size: 10px; color: var(--rail-muted); font-family: var(--font-mono); margin-top: 1px; }
|
|
.sidebar-action {
|
|
display: flex; align-items: center; gap: 9px;
|
|
height: 32px; padding: 0 8px; border-radius: var(--radius-sm);
|
|
color: var(--rail-muted); font-size: 12px;
|
|
border: none; background: transparent; width: 100%; text-align: left;
|
|
transition: background 120ms, color 120ms;
|
|
}
|
|
.sidebar-action:hover { background: var(--rail-hover); color: var(--rail-fg); }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
CONTENT AREA SHARED
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
.content-area { display: flex; flex-direction: column; min-width: 0; min-height: 100vh; }
|
|
|
|
/* Page views — JS toggles .view-active */
|
|
.page-view { display: none; flex-direction: column; flex: 1; }
|
|
.page-view.view-active { display: flex; }
|
|
|
|
.topbar {
|
|
position: sticky; top: 0; z-index: 10;
|
|
display: flex; align-items: center; gap: 10px;
|
|
height: 54px; padding: 0 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: rgba(242,244,247,.9);
|
|
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
flex-shrink: 0;
|
|
}
|
|
[data-theme="dark"] .topbar { background: rgba(17,19,24,.9); }
|
|
.topbar-title { font-weight: 600; font-size: 14px; flex: 1; color: var(--fg); }
|
|
.topbar-sub { color: var(--muted); font-family: var(--font-mono); font-size: 10px; margin-left: 4px; font-weight: 400; }
|
|
|
|
.search {
|
|
display: flex; align-items: center; gap: 8px;
|
|
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
background: var(--surface); padding: 0 10px; height: 32px; width: 240px;
|
|
transition: border-color 140ms;
|
|
}
|
|
.search:focus-within { border-color: color-mix(in oklab, var(--accent), transparent 60%); }
|
|
.search input { border: 0; outline: none; background: transparent; width: 100%; color: var(--fg); font-size: 13px; }
|
|
.search input::placeholder { color: var(--muted); }
|
|
|
|
.btn {
|
|
height: 32px; border-radius: var(--radius-sm); border: 1px solid var(--border);
|
|
padding: 0 12px; background: var(--surface); color: var(--fg);
|
|
font-size: 13px; font-weight: 500;
|
|
transition: background 120ms, border-color 120ms;
|
|
white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
|
|
}
|
|
.btn:hover { background: var(--bg); border-color: #b0b7c0; }
|
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
.btn-primary:disabled { opacity: .45; cursor: not-allowed; }
|
|
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
|
|
|
|
.footer {
|
|
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
|
min-height: 34px; padding: 0 20px; border-top: 1px solid var(--border);
|
|
color: var(--muted); font-size: 10px; font-family: var(--font-mono);
|
|
letter-spacing: .1em; text-transform: uppercase; flex-shrink: 0;
|
|
}
|
|
.footer-live { display: inline-flex; align-items: center; gap: 7px; }
|
|
.footer-dot {
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|
background: #22c55e; box-shadow: 0 0 0 3px rgba(34,197,94,.2);
|
|
}
|
|
|
|
/* Shared utility */
|
|
.status {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 2px 8px; border-radius: var(--radius-pill);
|
|
font-size: 11px; font-family: var(--font-mono); font-weight: 600;
|
|
width: fit-content; white-space: nowrap;
|
|
}
|
|
.status::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
|
|
.status.ok { color: var(--success); background: color-mix(in oklab,var(--success),transparent 90%); }
|
|
.status.warn { color: var(--warn); background: color-mix(in oklab,var(--warn),transparent 90%); }
|
|
.status.risk { color: var(--danger); background: color-mix(in oklab,var(--danger),transparent 90%); }
|
|
.status.info { color: #3b82f6; background: color-mix(in oklab,#3b82f6,transparent 90%); }
|
|
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
.note { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
|
.ghost-link { color: var(--muted); font-size: 12px; padding: 2px 0; border-radius: 4px; transition: color 120ms; }
|
|
.ghost-link:hover { color: var(--fg); text-decoration: none; }
|
|
.pill {
|
|
display: inline-flex; align-items: center; height: 20px; padding: 0 8px;
|
|
border-radius: var(--radius-pill); border: 1px solid var(--border);
|
|
color: var(--muted); font-size: 10px; font-family: var(--font-mono); font-weight: 500;
|
|
}
|
|
.eyebrow { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--accent); margin-bottom: 6px; }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
VIEW 1 — SYSTEM STATUS (dashboard-sidebar)
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
.page { padding: 20px; display: grid; gap: 18px; flex: 1; }
|
|
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
|
|
.page-head h1 { font-size: clamp(20px, 2.4vw, 28px); }
|
|
.page-head-desc { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 520px; line-height: 1.55; }
|
|
|
|
.card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 16px 18px; }
|
|
|
|
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
|
|
.stat-card { border-top: 2px solid var(--border); transition: border-color 200ms; }
|
|
.stat-card:hover { border-top-color: var(--accent); }
|
|
.stat-card .s-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
|
|
.stat-card .s-value { margin-top: 10px; font-size: 32px; line-height: 1; font-family: var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; color: var(--fg); }
|
|
.stat-card .s-sub { margin-top: 8px; font-size: 11px; color: var(--muted); line-height: 1.5; }
|
|
|
|
.panel-grid { display: grid; grid-template-columns: 1.4fr 0.9fr; gap: 18px; }
|
|
.stack { display: grid; gap: 18px; }
|
|
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
|
.section-head h2 { font-size: 16px; }
|
|
|
|
.task-list, .program-list, .event-list { display: grid; gap: 8px; }
|
|
.task-row, .program-row, .event-row {
|
|
display: grid; gap: 10px; border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm); padding: 10px 12px;
|
|
background: var(--surface); transition: border-color 120ms, box-shadow 120ms;
|
|
}
|
|
.task-row:hover, .program-row:hover { border-color: #c8cdd8; box-shadow: var(--shadow-sm); }
|
|
.task-row { grid-template-columns: 1.6fr 0.8fr 0.8fr 0.6fr; align-items: center; }
|
|
.program-row { grid-template-columns: 1fr auto; align-items: start; }
|
|
.event-row { grid-template-columns: 76px 1fr; align-items: start; }
|
|
|
|
.kpi-strip { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; margin-top: 12px; }
|
|
.kpi { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; background: #f8f9fb; }
|
|
.kpi strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 17px; color: var(--fg); }
|
|
.kpi-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; }
|
|
.meter { height: 3px; border-radius: 999px; background: #e5e7eb; overflow: hidden; margin-top: 8px; }
|
|
.meter > span { display: block; height: 100%; background: var(--accent); border-radius: inherit; }
|
|
|
|
.task-cta {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
height: 26px; padding: 0 10px; border-radius: var(--radius-sm);
|
|
border: 1px solid var(--border); background: transparent; color: var(--muted);
|
|
font-size: 11px; white-space: nowrap; cursor: pointer;
|
|
transition: border-color 120ms, color 120ms, background 120ms;
|
|
}
|
|
.task-cta:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); text-decoration: none; }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
VIEW 2 — REGULATORY SIGNALS (perception)
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
.stats-bar-flush {
|
|
display: grid; grid-template-columns: repeat(4,1fr);
|
|
gap: 1px; background: var(--border);
|
|
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
}
|
|
.stat-cell { background: var(--surface); padding: 14px 20px; }
|
|
.stat-cell .s-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
|
|
.stat-cell .s-value { margin-top: 6px; font-size: 28px; line-height: 1; font-family: var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
|
|
.stat-cell .s-sub { margin-top: 4px; font-size: 11px; color: var(--muted); }
|
|
|
|
.filter-bar {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 10px 20px; border-bottom: 1px solid var(--border);
|
|
background: var(--surface); flex-shrink: 0; flex-wrap: wrap;
|
|
}
|
|
.filter-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
|
.chip {
|
|
padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border);
|
|
background: transparent; color: var(--muted);
|
|
font-size: 11px; font-family: var(--font-mono); transition: all 100ms;
|
|
}
|
|
.chip:hover { border-color: var(--fg); color: var(--fg); }
|
|
.chip.on { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); font-weight: 600; }
|
|
.sep { width: 1px; height: 16px; background: var(--border); flex-shrink: 0; margin: 0 2px; }
|
|
|
|
.perception-split { flex: 1; display: grid; grid-template-columns: 360px 1fr; min-height: 0; }
|
|
|
|
.feed-pane { display: flex; flex-direction: column; border-right: 1px solid var(--border); min-height: 0; background: var(--bg); }
|
|
.feed-pane-head {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 12px 16px 10px; border-bottom: 1px solid var(--border);
|
|
background: var(--surface); flex-shrink: 0;
|
|
}
|
|
.feed-pane-head h2 { font-size: 13px; font-weight: 700; }
|
|
.feed-count { font-family: var(--font-mono); font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
|
|
.feed-scroll { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
.feed-scroll::-webkit-scrollbar { width: 4px; }
|
|
.feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
|
|
.ev-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius-md); padding: 11px 13px;
|
|
cursor: pointer; transition: border-color 100ms, box-shadow 100ms;
|
|
}
|
|
.ev-card:hover { border-color: #c4c9d4; }
|
|
.ev-card.selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
|
|
.ev-head { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; }
|
|
.src-tag { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
|
|
.std-code { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
|
|
.ev-title { font-weight: 600; font-size: 12px; line-height: 1.4; margin-bottom: 4px; }
|
|
.ev-summary { font-size: 11px; color: var(--muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
.ev-foot { display: flex; align-items: center; gap: 6px; margin-top: 7px; }
|
|
.ev-date { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
|
|
.ev-tag { font-size: 10px; font-family: var(--font-mono); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); color: var(--muted); }
|
|
.imp-dot { margin-left: auto; font-size: 10px; font-family: var(--font-mono); }
|
|
.loading-msg { font-family: var(--font-mono); font-size: 12px; color: var(--muted); text-align: center; padding: 40px 0; }
|
|
|
|
.analysis-pane { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: 16px 20px; gap: 12px; }
|
|
.analysis-pane::-webkit-scrollbar { width: 4px; }
|
|
.analysis-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
|
|
.analysis-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; text-align: center; color: var(--muted); min-height: 300px; }
|
|
.analysis-empty-ring { width: 40px; height: 40px; border-radius: 50%; border: 1.5px solid var(--border); display: flex; align-items: center; justify-content: center; }
|
|
.analysis-empty-label { font-size: 13px; }
|
|
.analysis-empty-hint { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
|
|
|
|
.detail-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 14px 16px; }
|
|
.detail-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
|
|
.detail-title { font-weight: 700; font-size: 14px; line-height: 1.3; margin-bottom: 5px; }
|
|
.detail-summary { font-size: 13px; color: var(--muted); line-height: 1.6; }
|
|
.detail-meta { display: flex; gap: 14px; margin-top: 10px; flex-wrap: wrap; }
|
|
.meta-item { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
|
|
.meta-item strong { color: var(--fg); }
|
|
|
|
.action-row { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
|
|
|
|
.output-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 14px 16px; display: none; }
|
|
.output-head { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 10px; display: flex; align-items: center; gap: 7px; }
|
|
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
|
.blink { animation: blink 1s step-end infinite; }
|
|
.md-h2 { font-size: 13px; font-weight: 700; color: var(--accent); margin: 14px 0 5px; font-family: var(--font-display); }
|
|
.md-h3 { font-size: 12px; font-weight: 700; margin: 10px 0 3px; }
|
|
.md-li { display: flex; gap: 7px; margin-bottom: 3px; padding-left: 3px; font-size: 12px; line-height: 1.6; }
|
|
.md-li-dot { color: var(--accent); flex-shrink: 0; }
|
|
.md-p { font-size: 12px; line-height: 1.7; margin-bottom: 3px; }
|
|
.md-empty { height: 5px; }
|
|
|
|
.docs-card { background: var(--surface); border-radius: var(--radius-md); box-shadow: var(--shadow-card); padding: 12px 16px; display: none; }
|
|
.docs-head { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 8px; }
|
|
.doc-row { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border); }
|
|
.doc-row:first-of-type { border-top: none; }
|
|
.doc-score { font-family: var(--font-mono); font-size: 11px; color: var(--accent); font-weight: 700; flex-shrink: 0; width: 32px; }
|
|
.doc-name { font-size: 12px; font-weight: 600; line-height: 1.4; }
|
|
.doc-clause { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
|
|
.doc-snippet { font-size: 11px; color: var(--muted); line-height: 1.5; margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
RESPONSIVE
|
|
═══════════════════════════════════════════════════════════════════════ */
|
|
@media (max-width: 1200px) {
|
|
.stats-grid { grid-template-columns: repeat(2,1fr); }
|
|
.panel-grid { grid-template-columns: 1fr; }
|
|
.kpi-strip { grid-template-columns: 1fr 1fr; }
|
|
.task-row { grid-template-columns: 1fr auto; }
|
|
}
|
|
@media (max-width: 1100px) {
|
|
.stats-bar-flush { grid-template-columns: 1fr 1fr; }
|
|
.perception-split { grid-template-columns: 300px 1fr; }
|
|
}
|
|
@media (max-width: 800px) {
|
|
.app-shell { grid-template-columns: 1fr; }
|
|
.sidebar { display: none; }
|
|
.stats-grid, .kpi-strip { grid-template-columns: 1fr 1fr; }
|
|
.stats-bar-flush { grid-template-columns: 1fr 1fr; }
|
|
.perception-split { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
SIDEBAR
|
|
═══════════════════════════════════════════════════════════════════════ -->
|
|
<aside class="sidebar" aria-label="Primary navigation">
|
|
<div class="sidebar-brand">
|
|
<div class="brand-logo">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
<path d="M2 3.5h12v1.5H2zm5.25 1.5h1.5v8h-1.5zm-3 2h2.25v1.5H4.25zm5.25 0h2.25v1.5H9.5zm-3 3h2.5v1.5H6.5z" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="brand-name">T-Systems</div>
|
|
<div class="brand-sub">Regulation Hub</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav" aria-label="Primary">
|
|
<div class="nav-group">
|
|
<span class="nav-group-label">Main</span>
|
|
<a class="nav-item" href="#" onclick="navigate('overview');return false;">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" fill="currentColor" opacity=".6"/></svg>
|
|
Overview
|
|
</a>
|
|
<a class="nav-item" id="nav-signals" href="#" onclick="navigate('signals');return false;">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none">
|
|
<circle cx="8" cy="8" r="2.5" fill="currentColor"/>
|
|
<path d="M8 2.5C4.91 2.5 2.5 5.42 2.5 8S4.91 13.5 8 13.5 13.5 10.58 13.5 8 11.09 2.5 8 2.5zm0 9.5C5.52 12 3.5 10.24 3.5 8S5.52 4 8 4s4.5 1.76 4.5 4-2.02 4-4.5 4z" fill="currentColor" opacity=".45"/>
|
|
</svg>
|
|
Regulatory Signals
|
|
<span class="nav-badge" id="badge-high">—</span>
|
|
</a>
|
|
<a class="nav-item active" id="nav-status" href="#" onclick="navigate('status');return false;">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M1.5 2.5h13v1H1.5zm0 3h13v1H1.5zm0 3h8v1h-8zm0 3h6v1h-6z" fill="currentColor"/></svg>
|
|
System Status
|
|
<span class="nav-badge">3</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="nav-group">
|
|
<span class="nav-group-label">Workbench</span>
|
|
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M3 1h7l3 3v11H3V1zm1 1v12h8V5h-3V2H4zm5 .5V4h1.5L9 1.5zM6 7h4v1H6zm0 2h4v1H6zm0 2h3v1H6z" fill="currentColor"/></svg>
|
|
Documents
|
|
</a>
|
|
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M8 1l7 3-1 6a7 7 0 01-6 5A7 7 0 011 10L0 4l8-3zm0 1.2L1.3 4.8l.8 5.1A6 6 0 008 14.8a6 6 0 005.9-4.9l.8-5.1L8 2.2zM7.5 5h1v4.5h-1V5zm0 5.5h1v1h-1v-1z" fill="currentColor"/></svg>
|
|
Compliance Analysis
|
|
</a>
|
|
</div>
|
|
|
|
<div class="nav-group">
|
|
<span class="nav-group-label">Chat</span>
|
|
<a class="nav-item" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/regulation-chat.html">
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none"><path d="M2 2h12a1 1 0 011 1v8a1 1 0 01-1 1H5l-3 2.5V3a1 1 0 011-1zm0 1v9.5L4.5 11H14V3H2zm2 2h8v1H4zm0 2h6v1H4z" fill="currentColor"/></svg>
|
|
Regulation Q&A
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<div class="sidebar-user">
|
|
<div class="avatar">TS</div>
|
|
<div>
|
|
<div class="user-name">T-Systems User</div>
|
|
<div class="user-role">Compliance Analyst</div>
|
|
</div>
|
|
</div>
|
|
<button class="sidebar-action" type="button" onclick="toggleTheme()">
|
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3a5 5 0 100 10A5 5 0 008 3zM2 8a6 6 0 1112 0A6 6 0 012 8z" fill="currentColor"/></svg>
|
|
<span id="theme-label">Dark mode</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
CONTENT AREA
|
|
═══════════════════════════════════════════════════════════════════════ -->
|
|
<div class="content-area">
|
|
|
|
<!-- ─── VIEW: SYSTEM STATUS ───────────────────────────────────────── -->
|
|
<div class="page-view view-active" id="view-status">
|
|
<header class="topbar">
|
|
<span class="topbar-title">System Status</span>
|
|
<div class="search">
|
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6.5 1a5.5 5.5 0 014.23 9.02l3.62 3.62-.7.71-3.63-3.63A5.5 5.5 0 116.5 1zm0 1a4.5 4.5 0 100 9 4.5 4.5 0 000-9z" fill="currentColor" opacity=".4"/></svg>
|
|
<input type="search" placeholder="Search regulations, documents…" aria-label="Search" />
|
|
</div>
|
|
<button class="btn">Export status</button>
|
|
<a class="btn btn-primary" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/upload-modal.html">New upload</a>
|
|
</header>
|
|
|
|
<main class="page">
|
|
<section class="page-head">
|
|
<div>
|
|
<div class="eyebrow">System Status</div>
|
|
<h1>System Status</h1>
|
|
<p class="page-head-desc">Ingestion pipeline, active compliance programs, and regulatory watch — all in one place.</p>
|
|
</div>
|
|
<span class="pill">v1.0.0</span>
|
|
</section>
|
|
|
|
<!-- Stats -->
|
|
<section class="stats-grid">
|
|
<article class="card stat-card">
|
|
<div class="s-label">Documents total</div>
|
|
<div class="s-value mono" id="ds-docs">—</div>
|
|
<div class="s-sub">Ingested into the knowledge base</div>
|
|
</article>
|
|
<article class="card stat-card">
|
|
<div class="s-label">Vector chunks</div>
|
|
<div class="s-value mono" id="ds-chunks">—</div>
|
|
<div class="s-sub">regulations_dense_1024_v2 serving retrieval</div>
|
|
</article>
|
|
<article class="card stat-card">
|
|
<div class="s-label">High-impact signals</div>
|
|
<div class="s-value mono" id="ds-high">—</div>
|
|
<div class="s-sub">Regulatory signals requiring immediate review</div>
|
|
</article>
|
|
<article class="card stat-card">
|
|
<div class="s-label">Last 90 days</div>
|
|
<div class="s-value mono" id="ds-90d">—</div>
|
|
<div class="s-sub">Recent regulatory publications</div>
|
|
</article>
|
|
</section>
|
|
|
|
<!-- Two-column -->
|
|
<section class="panel-grid">
|
|
<div class="stack">
|
|
<article class="card">
|
|
<div class="section-head">
|
|
<h2>Workflow queue</h2>
|
|
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">Open documents →</a>
|
|
</div>
|
|
<div class="task-list">
|
|
<div class="task-row">
|
|
<div>
|
|
<strong>GB/T 31484-2015 battery density revision</strong>
|
|
<div class="note">Uploaded by EV Safety Team · version 2026-04 addendum</div>
|
|
</div>
|
|
<span class="status warn">Embedding</span>
|
|
<span class="mono">chunk build active</span>
|
|
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-detail.html">Inspect →</a>
|
|
</div>
|
|
<div class="task-row">
|
|
<div>
|
|
<strong>UNECE R155 annex interpretation note</strong>
|
|
<div class="note">Parser artifacts ready · waiting for analyst assignment</div>
|
|
</div>
|
|
<span class="status ok">Ready</span>
|
|
<span class="mono">19 clauses linked</span>
|
|
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">Analyze →</a>
|
|
</div>
|
|
<div class="task-row">
|
|
<div>
|
|
<strong>GB 26112-2010 roof strength scan</strong>
|
|
<div class="note">OCR confidence dropped below threshold on 6 pages</div>
|
|
</div>
|
|
<span class="status risk">Failed</span>
|
|
<span class="mono">Retry #2</span>
|
|
<a class="task-cta" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/document-management.html">Resolve →</a>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<div class="section-head">
|
|
<h2>Active compliance programs</h2>
|
|
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/compliance-analysis.html">Review findings →</a>
|
|
</div>
|
|
<div class="program-list">
|
|
<div class="program-row">
|
|
<div>
|
|
<strong>Intelligent cockpit homologation</strong>
|
|
<p class="note">42 related standards across driver monitoring, EMC, and child safety. Four findings still open for MY27 platform.</p>
|
|
</div>
|
|
<span class="status risk">High risk</span>
|
|
</div>
|
|
<div class="program-row">
|
|
<div>
|
|
<strong>Battery swap certification dossier</strong>
|
|
<p class="note">Clause mapping complete. Thermal event test evidence package awaiting supplier document refresh.</p>
|
|
</div>
|
|
<span class="status warn">Pending</span>
|
|
</div>
|
|
<div class="program-row">
|
|
<div>
|
|
<strong>Connected fleet cybersecurity</strong>
|
|
<p class="note">RAG checks aligned with UNECE R155. Chat follow-up requested on remote key rotation obligations.</p>
|
|
</div>
|
|
<span class="status ok">On track</span>
|
|
</div>
|
|
</div>
|
|
<div class="kpi-strip">
|
|
<div class="kpi">
|
|
<div class="kpi-label">Retrieval hit rate</div>
|
|
<strong>87%</strong>
|
|
<div class="meter"><span style="width:87%"></span></div>
|
|
</div>
|
|
<div class="kpi">
|
|
<div class="kpi-label">Evidence coverage</div>
|
|
<strong>72%</strong>
|
|
<div class="meter"><span style="width:72%"></span></div>
|
|
</div>
|
|
<div class="kpi">
|
|
<div class="kpi-label">Reviewer SLA</div>
|
|
<strong>18h</strong>
|
|
<div class="meter"><span style="width:64%"></span></div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="stack">
|
|
<article class="card">
|
|
<div class="section-head"><h2>System health</h2><a class="ghost-link" href="#">Refresh</a></div>
|
|
<div class="task-list">
|
|
<div class="task-row" style="grid-template-columns:1fr auto">
|
|
<div><strong>Aliyun parser backend</strong><div class="note">Poll interval 5 s · timeout 900 s</div></div>
|
|
<span class="status warn">Queue depth 7</span>
|
|
</div>
|
|
<div class="task-row" style="grid-template-columns:1fr auto">
|
|
<div><strong>Embedding model</strong><div class="note">text-embedding-v3 · dimension 1024</div></div>
|
|
<span class="status ok">Healthy</span>
|
|
</div>
|
|
<div class="task-row" style="grid-template-columns:1fr auto">
|
|
<div><strong>Vector store</strong><div class="note">Milvus regulations_dense_1024_v2</div></div>
|
|
<span class="status ok">Serving</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<div class="section-head">
|
|
<h2>Regulatory watch</h2>
|
|
<a class="ghost-link" href="cc29bcb0-df2d-4d50-9428-7caa406ecb29/regulation-chat.html">Ask chat →</a>
|
|
</div>
|
|
<div class="event-list">
|
|
<div class="event-row">
|
|
<span class="mono note">2d ago</span>
|
|
<div>
|
|
<strong>GB 38031 thermal propagation draft updated</strong>
|
|
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">Potential impact on current battery enclosure narrative. Evidence gap flagged in two supplier submissions.</p>
|
|
</div>
|
|
</div>
|
|
<div class="event-row">
|
|
<span class="mono note">5d ago</span>
|
|
<div>
|
|
<strong>UNECE R155 Q&A added note on incident response logs</strong>
|
|
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">Connected fleet program must confirm retention windows and ownership controls.</p>
|
|
</div>
|
|
</div>
|
|
<div class="event-row">
|
|
<span class="mono note">12d ago</span>
|
|
<div>
|
|
<strong>GB/T 18487 charging interface interpretation circulated</strong>
|
|
<p class="note" style="-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden">No blocker yet, but three documents should be re-run against the new clause wording.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer class="footer">
|
|
<span>T-Systems Regulation Hub</span>
|
|
<div class="footer-live"><span class="footer-dot"></span><span>Online</span></div>
|
|
</footer>
|
|
</div><!-- /view-status -->
|
|
|
|
<!-- ─── VIEW: REGULATORY SIGNALS ─────────────────────────────────── -->
|
|
<div class="page-view" id="view-signals">
|
|
<div class="topbar">
|
|
<span class="topbar-title">
|
|
Regulatory Signals
|
|
<span class="topbar-sub">Real-time monitoring · Knowledge-base impact analysis</span>
|
|
</span>
|
|
<div class="search" style="width:220px">
|
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6.5 1a5.5 5.5 0 014.23 9.02l3.62 3.62-.7.71-3.63-3.63A5.5 5.5 0 116.5 1zm0 1a4.5 4.5 0 100 9 4.5 4.5 0 000-9z" fill="currentColor" opacity=".4"/></svg>
|
|
<input type="search" placeholder="Search signals…" aria-label="Search signals" />
|
|
</div>
|
|
<button class="btn btn-sm" onclick="loadFeed()">
|
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M13.5 8a5.5 5.5 0 11-1.1-3.3" stroke="currentColor" stroke-width="1.4"/><path d="M10 4.5l2.5.2.3-2.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div class="stats-bar-flush">
|
|
<div class="stat-cell"><div class="s-label">Total signals</div><div class="s-value" id="stat-total">—</div><div class="s-sub">All regulatory events in feed</div></div>
|
|
<div class="stat-cell"><div class="s-label">High impact</div><div class="s-value" id="stat-high" style="color:var(--danger)">—</div><div class="s-sub">Requires immediate review</div></div>
|
|
<div class="stat-cell"><div class="s-label">Medium impact</div><div class="s-value" id="stat-med" style="color:var(--warn)">—</div><div class="s-sub">Scheduled for assessment</div></div>
|
|
<div class="stat-cell"><div class="s-label">Last 90 days</div><div class="s-value" id="stat-90d" style="color:var(--accent)">—</div><div class="s-sub">Recent publications</div></div>
|
|
</div>
|
|
|
|
<div class="filter-bar">
|
|
<span class="filter-label">Source</span>
|
|
<button class="chip on" data-src="" onclick="setSource(this,'')">All</button>
|
|
<button class="chip" data-src="MIIT" onclick="setSource(this,'MIIT')">MIIT</button>
|
|
<button class="chip" data-src="UN-ECE" onclick="setSource(this,'UN-ECE')">UN-ECE</button>
|
|
<button class="chip" data-src="ISO" onclick="setSource(this,'ISO')">ISO</button>
|
|
<button class="chip" data-src="国标委" onclick="setSource(this,'国标委')">GB Comm.</button>
|
|
<button class="chip" data-src="EUR-Lex" onclick="setSource(this,'EUR-Lex')">EUR-Lex</button>
|
|
<button class="chip" data-src="IATF" onclick="setSource(this,'IATF')">IATF</button>
|
|
<div class="sep"></div>
|
|
<span class="filter-label">Impact</span>
|
|
<button class="chip on" data-imp="" onclick="setImpact(this,'')">All</button>
|
|
<button class="chip" data-imp="high" onclick="setImpact(this,'high')">High</button>
|
|
<button class="chip" data-imp="medium" onclick="setImpact(this,'medium')">Medium</button>
|
|
<button class="chip" data-imp="low" onclick="setImpact(this,'low')">Low</button>
|
|
</div>
|
|
|
|
<div class="perception-split" style="flex:1;min-height:0">
|
|
<div class="feed-pane">
|
|
<div class="feed-pane-head">
|
|
<h2>Signal feed</h2>
|
|
<span class="feed-count" id="feed-count"></span>
|
|
</div>
|
|
<div class="feed-scroll" id="feed-scroll">
|
|
<div class="loading-msg">Loading…</div>
|
|
</div>
|
|
</div>
|
|
<div class="analysis-pane" id="analysis-pane">
|
|
<div class="analysis-empty" id="analysis-empty">
|
|
<div class="analysis-empty-ring">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2a6 6 0 100 12A6 6 0 008 2zm0 2v4l3 1.5-.5 1-3.5-1.75V4H8z" fill="currentColor" opacity=".3"/></svg>
|
|
</div>
|
|
<div class="analysis-empty-label">Select a signal to view impact analysis</div>
|
|
<div class="analysis-empty-hint">← Choose from the signal feed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<div class="footer-live"><span class="footer-dot"></span><span>Live feed</span></div>
|
|
<span>T-Systems Regulation Hub</span>
|
|
</div>
|
|
</div><!-- /view-signals -->
|
|
|
|
</div><!-- /content-area -->
|
|
</div><!-- /app-shell -->
|
|
|
|
<script>
|
|
/* ══════════════════════════════════════════════════════════════════════
|
|
NAVIGATION — JS view switching (no page reload)
|
|
══════════════════════════════════════════════════════════════════════ */
|
|
const VIEWS = {
|
|
status: { view: 'view-status', nav: 'nav-status' },
|
|
signals: { view: 'view-signals', nav: 'nav-signals' },
|
|
};
|
|
let currentView = 'status';
|
|
|
|
function navigate(id) {
|
|
if (!VIEWS[id]) return;
|
|
// Hide all views
|
|
document.querySelectorAll('.page-view').forEach(v => v.classList.remove('view-active'));
|
|
// Remove all active nav states (only within sidebar nav-items that have IDs)
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
// Show target view
|
|
document.getElementById(VIEWS[id].view).classList.add('view-active');
|
|
document.getElementById(VIEWS[id].nav).classList.add('active');
|
|
currentView = id;
|
|
// Bootstrap data for signals view on first open
|
|
if (id === 'signals' && document.getElementById('feed-scroll').querySelector('.loading-msg')) {
|
|
loadStats(); loadFeed();
|
|
}
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════
|
|
THEME TOGGLE
|
|
══════════════════════════════════════════════════════════════════════ */
|
|
function toggleTheme() {
|
|
const html = document.documentElement;
|
|
const next = html.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
html.dataset.theme = next;
|
|
localStorage.setItem('theme', next);
|
|
document.getElementById('theme-label').textContent = next === 'dark' ? 'Light mode' : 'Dark mode';
|
|
}
|
|
(function () {
|
|
const saved = localStorage.getItem('theme');
|
|
if (saved) {
|
|
document.documentElement.dataset.theme = saved;
|
|
document.getElementById('theme-label').textContent = saved === 'dark' ? 'Light mode' : 'Dark mode';
|
|
}
|
|
})();
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════
|
|
API — SYSTEM STATUS
|
|
══════════════════════════════════════════════════════════════════════ */
|
|
const API_BASE = 'http://6.86.80.9:5173/api/v1';
|
|
|
|
async function loadDashboardStats() {
|
|
try {
|
|
const r = await fetch(`${API_BASE}/perception/stats`);
|
|
if (!r.ok) return;
|
|
const s = await r.json();
|
|
const set = (id, val) => { const el = document.getElementById(id); if (el && val != null) el.textContent = val; };
|
|
set('ds-high', s.high_impact);
|
|
set('ds-90d', s.recent_90d);
|
|
set('ds-docs', s.total);
|
|
} catch (e) { /* silent */ }
|
|
}
|
|
loadDashboardStats();
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════
|
|
API — REGULATORY SIGNALS
|
|
══════════════════════════════════════════════════════════════════════ */
|
|
const SRC_COLOR = {
|
|
MIIT: '#e20074', 'UN-ECE': '#3b82f6', ISO: '#7c3aed',
|
|
'国标委': '#059669', 'EUR-Lex': '#d97706', IATF: '#7c3aed'
|
|
};
|
|
const IMP_COLOR = { high: 'var(--danger)', medium: 'var(--warn)', low: 'var(--success)' };
|
|
const IMP_LABEL = { high: 'High', medium: 'Medium', low: 'Low' };
|
|
const STA_LABEL = { enacted: 'Enacted', draft: 'Draft', consultation: 'Consultation' };
|
|
const STA_CLASS = { enacted: 'ok', draft: 'warn', consultation: 'info' };
|
|
|
|
let currentSource = '', currentImpact = '', selectedId = null, abortCtrl = null;
|
|
let allEvents = [];
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const r = await fetch(`${API_BASE}/perception/stats`);
|
|
if (!r.ok) return;
|
|
const s = await r.json();
|
|
document.getElementById('stat-total').textContent = s.total ?? '—';
|
|
document.getElementById('stat-high').textContent = s.high_impact ?? '—';
|
|
document.getElementById('stat-med').textContent = s.medium_impact ?? '—';
|
|
document.getElementById('stat-90d').textContent = s.recent_90d ?? '—';
|
|
const badge = document.getElementById('badge-high');
|
|
if (badge) badge.textContent = s.high_impact ?? '—';
|
|
// Also update dashboard card
|
|
const h = document.getElementById('ds-high');
|
|
if (h && s.high_impact != null) h.textContent = s.high_impact;
|
|
} catch (e) { console.warn('stats:', e); }
|
|
}
|
|
|
|
async function loadFeed() {
|
|
const scroll = document.getElementById('feed-scroll');
|
|
scroll.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
const params = new URLSearchParams();
|
|
if (currentSource) params.set('source', currentSource);
|
|
if (currentImpact) params.set('impact_level', currentImpact);
|
|
try {
|
|
const r = await fetch(`${API_BASE}/perception/events?${params}`);
|
|
if (!r.ok) throw new Error(r.status);
|
|
const data = await r.json();
|
|
allEvents = data.events || [];
|
|
const countEl = document.getElementById('feed-count');
|
|
if (countEl) countEl.textContent = `${allEvents.length} / ${data.total}`;
|
|
renderFeed(allEvents);
|
|
} catch (e) {
|
|
scroll.innerHTML = `<div class="loading-msg" style="color:var(--danger)">Failed to load: ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderFeed(events) {
|
|
const scroll = document.getElementById('feed-scroll');
|
|
if (!events.length) { scroll.innerHTML = '<div class="loading-msg">No signals match the current filters.</div>'; return; }
|
|
scroll.innerHTML = events.map(ev => {
|
|
const sc = SRC_COLOR[ev.source] || '#888';
|
|
const stc = STA_CLASS[ev.status] || 'info';
|
|
const sel = ev.id === selectedId;
|
|
return `<div class="ev-card${sel ? ' selected' : ''}" onclick="selectEvent('${ev.id}')" id="card-${ev.id}">
|
|
<div class="ev-head">
|
|
<span class="src-tag" style="color:${sc};background:${sc}18">${ev.source}</span>
|
|
<span class="std-code">${ev.standard_code}</span>
|
|
<span class="status ${stc}" style="margin-left:auto">${STA_LABEL[ev.status] || ev.status}</span>
|
|
</div>
|
|
<div class="ev-title">${ev.title}</div>
|
|
<div class="ev-summary">${ev.summary}</div>
|
|
<div class="ev-foot">
|
|
<span class="ev-date">${ev.published_at}</span>
|
|
${(ev.tags||[]).slice(0,2).map(t=>`<span class="ev-tag">${t}</span>`).join('')}
|
|
<span class="imp-dot" style="color:${IMP_COLOR[ev.impact_level]||'var(--muted)'}">● ${IMP_LABEL[ev.impact_level]||ev.impact_level}</span>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function selectEvent(id) {
|
|
if (id === selectedId) return;
|
|
selectedId = id;
|
|
document.querySelectorAll('.ev-card').forEach(c => c.classList.remove('selected'));
|
|
const card = document.getElementById(`card-${id}`);
|
|
if (card) card.classList.add('selected');
|
|
const ev = allEvents.find(e => e.id === id);
|
|
if (ev) renderDetail(ev);
|
|
}
|
|
|
|
function renderDetail(ev) {
|
|
const sc = SRC_COLOR[ev.source] || '#888';
|
|
const stc = STA_CLASS[ev.status] || 'info';
|
|
const pane = document.getElementById('analysis-pane');
|
|
pane.innerHTML = `
|
|
<div class="detail-card">
|
|
<div class="detail-head">
|
|
<span class="src-tag" style="color:${sc};background:${sc}18;font-size:10px;padding:2px 7px;border-radius:4px">${ev.source}</span>
|
|
<span style="font-family:var(--font-mono);font-size:10px;color:var(--muted)">${ev.standard_code}</span>
|
|
<span class="status ${stc}" style="margin-left:auto">${STA_LABEL[ev.status]||ev.status}</span>
|
|
</div>
|
|
<div class="detail-title">${ev.title}</div>
|
|
<div class="detail-summary">${ev.summary}</div>
|
|
<div class="detail-meta">
|
|
<span class="meta-item">Published <strong>${ev.published_at}</strong></span>
|
|
${ev.effective_at ? `<span class="meta-item">Effective <strong>${ev.effective_at}</strong></span>` : ''}
|
|
<span class="meta-item">Category <strong>${ev.category}</strong></span>
|
|
<span class="meta-item">Impact <strong style="color:${IMP_COLOR[ev.impact_level]||'var(--muted)'}">${IMP_LABEL[ev.impact_level]||ev.impact_level}</strong></span>
|
|
</div>
|
|
</div>
|
|
<div class="action-row">
|
|
<button class="btn btn-primary" id="btn-analyze" onclick="startAnalysis('${ev.id}')">
|
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg>
|
|
Run impact analysis
|
|
</button>
|
|
<button class="btn" id="btn-abort" style="display:none" onclick="stopAnalysis()">Stop</button>
|
|
${ev.source_url ? `<a href="${ev.source_url}" target="_blank" class="btn"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M9 2h5v5M9.5 6.5L14 2M3 4h4M3 8h8M3 12h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Source</a>` : ''}
|
|
</div>
|
|
<div class="docs-card" id="docs-card"><div class="docs-head">Affected documents</div><div id="docs-list"></div></div>
|
|
<div class="output-card" id="output-card">
|
|
<div class="output-head" id="output-head">
|
|
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg>
|
|
AI impact analysis
|
|
</div>
|
|
<div id="output-body"></div>
|
|
</div>`;
|
|
}
|
|
|
|
async function startAnalysis(eventId) {
|
|
if (abortCtrl) abortCtrl.abort();
|
|
abortCtrl = new AbortController();
|
|
const btnA = document.getElementById('btn-analyze');
|
|
const btnX = document.getElementById('btn-abort');
|
|
btnA.disabled = true; btnX.style.display = '';
|
|
const outputCard = document.getElementById('output-card');
|
|
const outputHead = document.getElementById('output-head');
|
|
const outputBody = document.getElementById('output-body');
|
|
const docsCard = document.getElementById('docs-card');
|
|
outputCard.style.display = '';
|
|
outputBody.innerHTML = '';
|
|
outputHead.innerHTML = `<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg> AI impact analysis <span class="blink" style="color:var(--accent)">▋</span>`;
|
|
try {
|
|
const res = await fetch(`${API_BASE}/perception/events/${eventId}/analyze`, {
|
|
method: 'POST', headers: { Accept: 'text/event-stream' }, signal: abortCtrl.signal
|
|
});
|
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
const reader = res.body.getReader();
|
|
const dec = new TextDecoder();
|
|
let buf = '', rawText = '';
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buf += dec.decode(value, { stream: true });
|
|
const parts = buf.split('\n\n');
|
|
buf = parts.pop() ?? '';
|
|
for (const block of parts) {
|
|
if (!block.trim()) continue;
|
|
let evtName = 'message';
|
|
const dataLines = [];
|
|
for (const line of block.split('\n')) {
|
|
if (line.startsWith('event:')) evtName = line.slice(6).trim();
|
|
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
|
}
|
|
const payload = dataLines.join('\n');
|
|
if (!payload) continue;
|
|
if (evtName === 'sources') { try { renderDocs(JSON.parse(payload), docsCard); } catch {} }
|
|
else if (evtName === 'content') { rawText += payload; outputBody.innerHTML = renderMarkdown(rawText); }
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (e.name !== 'AbortError') {
|
|
const ob = document.getElementById('output-body');
|
|
if (ob) ob.innerHTML += `<div style="color:var(--danger);font-size:12px;margin-top:8px">Analysis error: ${e.message}</div>`;
|
|
}
|
|
} finally {
|
|
const blink = document.querySelector('#output-head .blink');
|
|
if (blink) blink.remove();
|
|
const ab = document.getElementById('btn-abort'); if (ab) ab.style.display = 'none';
|
|
const ba = document.getElementById('btn-analyze');
|
|
if (ba) { ba.disabled = false; ba.innerHTML = `<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.8 5.5H15l-4.8 3.5 1.8 5.5L8 12l-4 3.5 1.8-5.5L1 6.5h5.2z" fill="currentColor"/></svg> Re-analyse`; }
|
|
}
|
|
}
|
|
|
|
function stopAnalysis() {
|
|
if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; }
|
|
const ab = document.getElementById('btn-abort'); if (ab) ab.style.display = 'none';
|
|
const ba = document.getElementById('btn-analyze'); if (ba) ba.disabled = false;
|
|
const h = document.getElementById('output-head'); if (h) { const b = h.querySelector('.blink'); if (b) b.remove(); }
|
|
}
|
|
|
|
function renderDocs(docs, card) {
|
|
if (!docs || !docs.length) return;
|
|
card.style.display = '';
|
|
document.getElementById('docs-list').innerHTML = docs.map(d => `
|
|
<div class="doc-row">
|
|
<div class="doc-score">${Math.round(d.score * 100)}%</div>
|
|
<div>
|
|
<div class="doc-name">${d.doc_name}</div>
|
|
<div class="doc-clause">${d.clause || ''}</div>
|
|
<div class="doc-snippet">${d.snippet || ''}</div>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function renderMarkdown(text) {
|
|
return text.split('\n').map(line => {
|
|
if (line.startsWith('## ')) return `<div class="md-h2">${line.slice(3)}</div>`;
|
|
if (line.startsWith('### ')) return `<div class="md-h3">${line.slice(4)}</div>`;
|
|
if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
const c = line.slice(2).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
|
|
return `<div class="md-li"><span class="md-li-dot">·</span><span>${c}</span></div>`;
|
|
}
|
|
if (/^\d+\./.test(line)) return `<div class="md-p" style="padding-left:8px">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</div>`;
|
|
if (!line.trim()) return '<div class="md-empty"></div>';
|
|
return `<div class="md-p">${line.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function setSource(btn, src) {
|
|
currentSource = src;
|
|
document.querySelectorAll('[data-src]').forEach(c => c.classList.toggle('on', c.dataset.src === src));
|
|
loadFeed();
|
|
}
|
|
|
|
function setImpact(btn, imp) {
|
|
currentImpact = imp;
|
|
document.querySelectorAll('[data-imp]').forEach(c => c.classList.toggle('on', c.dataset.imp === imp));
|
|
loadFeed();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|