Two changes in one commit because they were prepared together and the component demos depend on the new self-hosted fonts.css. Tier 3 wave 2 — 12 new components --------------------------------- Adds components-tier3-supplement.css (886 lines) and 12 isolated demo HTML pages under shared/playground-examples/components/: toxic-flow chain, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed. Reuses existing tokens — no new CSS custom properties. Honors the Phase 1 feedback rules: no large pink areas for body text, severity-red distinct from failure-red, dark mode via existing [data-theme="dark"]. Provenance: components-tier3-supplement.css and the 12 demo bodies were authored by claude.ai/design (separate Anthropic instance) on 2026-05-03. This commit only integrates them — path rewrites, font swap, generic name substitution in fleet-overview demo data, README updates. base.css from the export was deliberately NOT taken in because it reverted the inline-message contrast fix from v0.1. Self-hosted fonts (Inter, JetBrains Mono, Source Serif 4) --------------------------------------------------------- Replaces all fonts.googleapis.com / fonts.gstatic.com requests with .woff2 files bundled at shared/playground-design-system/fonts/. Why: - No data leaked to Google about end-user IPs and User-Agents. - GDPR-safe for Norwegian public-sector deployments. - Works offline / behind air-gapped firewalls. - Forkers downloading the marketplace get a complete bundle. All three families are SIL Open Font License 1.1 — license texts included alongside the woff2 files. Source Serif 4 woff2 generated locally from the upstream OTF release using fonttools ttLib.woff2 compress; Inter and JetBrains Mono are unmodified upstream webfont releases. Total bundle: 9 woff2 files, ~940 KB. New fonts.css declares all @font-face rules with font-display: swap. All 6 example HTMLs and 12 new component demos load it via a single relative path. Verified -------- - Privacy grep returns empty across plugins/ and shared/ - Google Fonts grep returns empty across shared/*.html - Smoke test via python -m http.server: HTML + 7 stylesheets + Inter-Regular.woff2 all return 200 Doc updates ----------- - shared/playground-design-system/README.md: file tree updated, Quick start snippet shows fonts.css link, "Self-hosted fonts" section added - shared/playground-design-system/fonts/LICENSES.md: combined attribution - README.md (root): Tier 3 wave 1+2 component list, Privacy-first bullet - CLAUDE.md (root): tree entry expanded for new components + fonts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
102 lines
5.9 KiB
HTML
102 lines
5.9 KiB
HTML
<!doctype html>
|
|
<html lang="nb">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Fleet-Overview · Tier 3 supp</title>
|
|
<link rel="stylesheet" href="../../playground-design-system/tokens.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/base.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components-tier2.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
|
|
</head>
|
|
<body>
|
|
<header class="app-header">
|
|
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
|
|
<span class="app-header__breadcrumb">/ Komponenter / Fleet-Overview</span>
|
|
</header>
|
|
|
|
<main class="container container--wide" style="padding: var(--space-8) 0;">
|
|
<div style="margin-bottom: var(--space-6);">
|
|
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · /security dashboard</span>
|
|
<h1 style="margin: 4px 0 6px;">Fleet-Overview</h1>
|
|
<p class="text-secondary" style="max-width: 65ch;">Cross-project posture på én skjerm. 4 kolonner desktop → 2 → 1.</p>
|
|
</div>
|
|
|
|
<div class="fleet-toolbar">
|
|
<span class="fleet-toolbar__label">Sortér</span>
|
|
<button class="chip" aria-pressed="true" onclick="sortFleet('worst')">Verste først</button>
|
|
<button class="chip" aria-pressed="false" onclick="sortFleet('alpha')">Alfabetisk</button>
|
|
<button class="chip" aria-pressed="false" onclick="sortFleet('recent')">Sist skannet</button>
|
|
<span class="fleet-toolbar__label" style="margin-left: var(--space-4);">Filter</span>
|
|
<button class="chip" aria-pressed="false" onclick="filterFleet('failing')">Kun F + E</button>
|
|
<button class="chip" aria-pressed="false" onclick="filterFleet('changed')">Kun med endringer</button>
|
|
<span class="fleet-toolbar__spacer"></span>
|
|
<span class="fleet-toolbar__count" id="fleetCount">12 prosjekter</span>
|
|
</div>
|
|
|
|
<div class="fleet-grid" id="fleetGrid"></div>
|
|
</main>
|
|
|
|
<script>
|
|
const projects = [
|
|
{ name: "lier-kommune/copilot-onboarding", grade: "A", risk: 12, band: 1, worst: "info-disclosure", scanned: "2026-05-02 14:11", trend: "stable", changed: false },
|
|
{ name: "baerum-kommune/okr-portal", grade: "B", risk: 28, band: 1, worst: "missing-rate-limit", scanned: "2026-05-02 09:32", trend: "better", changed: true },
|
|
{ name: "direktorat/sak-arkiv-mcp", grade: "C", risk: 44, band: 2, worst: "weak-auth", scanned: "2026-05-01 18:04", trend: "worse", changed: true },
|
|
{ name: "direktorat/llm-saksbehandler", grade: "F", risk: 87, band: 4, worst: "TFA chain (BLOCK)", scanned: "2026-05-02 02:55", trend: "worse", changed: true },
|
|
{ name: "trondheim/dpia-helper", grade: "B", risk: 22, band: 1, worst: "log-leakage", scanned: "2026-04-30 11:18", trend: "stable", changed: false },
|
|
{ name: "skatteetaten/intern-kb", grade: "D", risk: 61, band: 3, worst: "prompt-injection", scanned: "2026-05-02 07:42", trend: "better", changed: true },
|
|
{ name: "nav/saksbehandler-co", grade: "C", risk: 39, band: 2, worst: "ssrf-risk", scanned: "2026-05-01 23:01", trend: "stable", changed: false },
|
|
{ name: "udi/ai-translator", grade: "E", risk: 73, band: 3, worst: "data-residency", scanned: "2026-05-02 12:30", trend: "worse", changed: true },
|
|
{ name: "dsb/krise-bot", grade: "A", risk: 8, band: 1, worst: "minor-typo", scanned: "2026-04-29 16:50", trend: "stable", changed: false },
|
|
{ name: "domstol/dom-summary", grade: "B", risk: 25, band: 1, worst: "context-leakage", scanned: "2026-05-01 10:14", trend: "better", changed: true },
|
|
{ name: "helsedir/symptomsjekk", grade: "F", risk: 91, band: 4, worst: "PHI exfiltration", scanned: "2026-05-02 04:18", trend: "worse", changed: true },
|
|
{ name: "kommune/innsyn-mcp", grade: "C", risk: 47, band: 2, worst: "broad-scope", scanned: "2026-05-01 19:55", trend: "stable", changed: false },
|
|
];
|
|
|
|
const trendArrow = { better: "↗ bedre", worse: "↘ verre", stable: "→ stabil" };
|
|
const grid = document.getElementById('fleetGrid');
|
|
let mode = 'worst', filter = 'none';
|
|
|
|
function render() {
|
|
let list = projects.slice();
|
|
if (filter === 'failing') list = list.filter(p => p.grade === 'F' || p.grade === 'E');
|
|
if (filter === 'changed') list = list.filter(p => p.changed);
|
|
if (mode === 'worst') list.sort((a,b) => b.risk - a.risk);
|
|
if (mode === 'alpha') list.sort((a,b) => a.name.localeCompare(b.name));
|
|
if (mode === 'recent') list.sort((a,b) => b.scanned.localeCompare(a.scanned));
|
|
grid.innerHTML = list.map(p => `
|
|
<button class="fleet-tile" onclick="alert('Naviger til posture for ${p.name}')">
|
|
<div class="fleet-tile__row">
|
|
<span class="fleet-tile__name" title="${p.name}">${p.name}</span>
|
|
<span class="fleet-tile__grade" data-grade="${p.grade}">${p.grade}</span>
|
|
</div>
|
|
<div class="fleet-tile__meter"><div class="fleet-tile__meter-fill" data-band="${p.band}" style="width:${p.risk}%"></div></div>
|
|
<span class="fleet-tile__chip">${p.worst}</span>
|
|
<div class="fleet-tile__meta">
|
|
<span>${p.scanned}</span>
|
|
<span class="fleet-tile__trend--${p.trend}">${trendArrow[p.trend]}</span>
|
|
</div>
|
|
</button>
|
|
`).join('');
|
|
document.getElementById('fleetCount').textContent = list.length + ' prosjekter';
|
|
}
|
|
|
|
function sortFleet(m) {
|
|
mode = m;
|
|
document.querySelectorAll('.fleet-toolbar .chip').forEach(c => {
|
|
if (['Verste først', 'Alfabetisk', 'Sist skannet'].includes(c.textContent)) c.setAttribute('aria-pressed', 'false');
|
|
});
|
|
event.target.setAttribute('aria-pressed', 'true');
|
|
render();
|
|
}
|
|
function filterFleet(f) {
|
|
filter = filter === f ? 'none' : f;
|
|
event.target.setAttribute('aria-pressed', filter === f ? 'true' : 'false');
|
|
render();
|
|
}
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|