diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index ec80902f0..f57c111b2 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -34,9 +34,10 @@ paths-ignore: - analysis/ghidra/service/server.py - sandbox/ghosts/vendor/ghosts-src/Ghosts.Api/wwwroot/lib - arcane/home/honeypot-cowrie/honeyfs-implant/main.go - - docs/design-lab - # #1763: docs/design-lab/ is the preserved 2026-08-17 design-lab record - # (restored from preserve-design-lab-1763 branch, kept byte-identical). + - branding/design-lab + # #1763: branding/design-lab/ is the preserved 2026-08-17 design-lab record + # (restored from preserve-design-lab-1763 branch) and the single home for it + # since #3310 retired the duplicate copy this exclusion used to name. # playground/compare.html is a local-only harness (loads 127.0.0.1: # frames into an iframe for side-by-side theme comparison); it is not # shipped, not served by any route, and reachable only by opening the file diff --git a/branding/design-lab/README.md b/branding/design-lab/README.md index a5a6a81fa..932c44ff1 100644 --- a/branding/design-lab/README.md +++ b/branding/design-lab/README.md @@ -11,6 +11,12 @@ The 28M of variant trees and ~150M of `logskel` and harness binaries were deliberately left out — they are reproducible, and the small files here are the part that is not. +Real hostnames, the internal server address and a captured attacker IP were +replaced with documentation-range placeholders when these files were moved +into the public repository. Nothing else was edited. This tree is the published +one — `pages.yml` builds `branding/` — so the redaction lives here and not in +the fork that used to hold it; #3310 removed that fork. + ## What the pick letters mean Two rounds of review, and they are easy to get backwards — one local note @@ -41,6 +47,18 @@ still apply: - **One page width** across every page. - **SPA-feel navigation**: only the centre content refreshes. +## Colour research + +The palettes came from a 2026 UI colour-trend pass, not a brand benchmark. +Its findings were mostly about **grounds and neutrals** — elevated neutrals +(soft greys, warm sand, stone, muted clay, oatmeal, taupe), zinc/slate as the +dominant direction for technology products, lime with cool whites for +dashboards, "avoid grey-on-grey fatigue" in dark mode, and earth tones for a +sense of long-term value. + +Only the accent half was implementable at the time, because a preset could +reach eight accent tokens. Finishing the other half is #1753. + ## The files | file | what it is | diff --git a/branding/design-lab/design-notes.md b/branding/design-lab/design-notes.md index 820bc1e23..b18a76c6b 100644 --- a/branding/design-lab/design-notes.md +++ b/branding/design-lab/design-notes.md @@ -1,4 +1,19 @@ -# APIARY dashboard design review — the deployed dashboard — 2026-08-17 +# APIARY dashboard design review — dashboard.example — 2026-08-17 + +> **Public, redacted copy — status as of 2026-09-27.** The address redaction in +> this file is deliberate and correct as it stands: the only address literals +> here are `127.0.0.1` (loopback) and `203.0.113.1` (RFC 5737 TEST-NET-3), and +> the only hostname is a reserved `.example` domain. Do not substitute real +> values for them, and do not restore anything redacted out of the source copy. +> +> The findings below are a **snapshot of a review session on 2026-08-17, not a +> description of the current code**. Several cite the Go dashboard's static +> assets and route table by name (`hp-app.js`, `hp-dynamic-nav.js`, +> `routes.go`); that dashboard was deleted in #1628 on 2026-08-22, five days +> after this review, and none of those files exist in the repository any more. +> The current route authority is the Rust `axum` service at +> `arcane/home/honeypot-dashboard/backend-service/src/main.rs`, so re-locate a +> finding's code there before acting on it. ## Findings (running) - Overview (light): loads fast, authenticated. Heatmap "Activity — last 24h" dominates; lower rows (multipot, conpot-kamstrup, endlessh) appear near-empty/pale — visual weight wasted? - Theme toggle: monitor icon top-right (left of LIVE). Dark theme renders correctly on Overview. @@ -6,7 +21,7 @@ - Heatmap uses one global color scale: dionaea (~90k/h) saturates; low-volume sensors (multipot ~240/h, endlessh, galah) render near-empty. Per-sensor normalization would make rows readable. - Header alert badge shows "99+" — alert count overflow, arguably alarming-by-default. - Overview "Recent events": every row renders its FULL normalized-event JSON article inline (#1447 intentional). On Overview this makes the stream ~18 x ~600px of JSON inside a fixed-height card__scroll; the table header (time/sensor/source ip/port/detail) no longer matches what the eye sees. Suggest: compact rows on Overview stream (detail on /events only), or max-height+inner-scroll on the article, or collapse-with-summary that keeps content in DOM for a11y. -- Events table: "source ip" column too narrow → IP wraps mid-address (85.14.245.1 / 22); country badge wraps below. Port cell renders ":25" with leading colon — stylistic, looks like a typo when cell wraps. +- Events table: "source ip" column too narrow → IP wraps mid-address (203.0.113.1 / 22); country badge wraps below. Port cell renders ":25" with leading colon — stylistic, looks like a typo when cell wraps. - Heatmap card keeps internal scroll position after page scroll; no affordance that rows are hidden above/below inside the card. - Sections on Live operations tab: Current activity (heatmap), Attack origins — live geographic view (map), Collection status (Sensor feeds, Protocols probed), ML classification backlog, Live event stream. - "Attack origins" Leaflet map (dark): world tiles don't fill card width at zoom 2 — large blank light-gray gutters left/right clash with dark theme; "World" reset control top-right is clipped mid-word; Leaflet attribution overlaps the card's bottom edge. Suggest maxBounds/fitBounds or ocean-colored background + unclipped control. @@ -52,7 +67,7 @@ - Pages must fill viewport height/width with modest padding; Event explorer card called out as "very small, hard to see". - Filter fields should auto-populate on click (works, but broken by session decay; add to attack-path/since too). - Nav list should include hidden routes (found in routes.go, not in sidebar): /alerts, /source-health, /search, /sensors, /history, /dead-letters, /canarytokens, /ghidra, /revdeck, /cape, /github-analysis, /sandbox, /payload-workbench, /problem-reports(?), settings modal. Decide which become nav entries (grouped), which stay contextual. -- Build local variant dashboard w/ real data (against the homeserver, over the `homeserver` ssh alias) for side-by-side design choices incl. PDF design. +- Build local variant dashboard w/ real data (homeserver xore@) for side-by-side design choices incl. PDF design. ## Investigate pages (rest) - /ips: solid table; date cols wrap to 2 lines at 1568px; source-ip wraps mid-IP occasionally. - /campaigns: worst column-crush — network CIDR wraps mid-value ("169.58.1/70.0/24"), provider wraps "networ/k", dates wrap "2026-/08-16", trailing "ES →" col clipped by card edge; 15 columns is too many for one table. Card shows ~4.5 rows, half viewport dead below. diff --git a/branding/design-lab/playground/elements.html b/branding/design-lab/playground/elements.html index e52f34061..33c118dde 100644 --- a/branding/design-lab/playground/elements.html +++ b/branding/design-lab/playground/elements.html @@ -328,17 +328,17 @@

Design Picker

4 · Data table rows

4Abase / v1

Stock

Hairline row separators.

-
timesensorsource ip
00:28:23cowrie85.14.245.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
+
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
4Bnew

Zebra

Alternating row wash for long-row tracking.

-
timesensorsource ip
00:28:23cowrie85.14.245.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
+
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
4Cnew

Card rows

Each row its own rounded slab — modern SaaS lists.

-
timesensorsource ip
00:28:23cowrie85.14.245.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
+
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
4Dv3

Dense mono grid

Column rules + mono cells; maximum rows per screen.

-
timesensorsource ip
00:28:23cowrie85.14.245.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
+
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
4Enew · claude

Open rows + hover pill

No chrome: airy padding, hairline separators, whole row lifts on hover — claude list feel.

-
timesensorsource ip
00:28:23mailoney85.14.245.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114
+
timesensorsource ip
00:28:23mailoney203.0.113.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114
4Fclaude × v3

Open dense mono

Same chrome-free rows at v3 density: mono cells, 5px padding, micro-caps headers.

-
timesensorsource ip
00:28:23mailoney85.14.245.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114
+
timesensorsource ip
00:28:23mailoney203.0.113.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114

5 · Badges & status

@@ -450,11 +450,11 @@

Page layouts

3.1M events in 24h (+21%, normal) · 29/29 sensors active · 0 open incidents

Investigate anything — IP, session, hash, credential, country…
-
recent: 85.14.245.122sess 76439b93
Investigate
+
recent: 203.0.113.122sess 76439b93
Investigate
Threat landscape · Attacker behavior · Evidence · Collection health

Happening now

-
00:28:23mailoney85.14.245.122DATA: 6 bytes saved
00:28:19cowrie123.188.73.228login.success: root/juantech
00:28:11dionaea213.176.26.114smb connect :445
+
00:28:23mailoney203.0.113.122DATA: 6 bytes saved
00:28:19cowrie123.188.73.228login.success: root/juantech
00:28:11dionaea213.176.26.114smb connect :445
OV-Cnew

Bento grid — everything above the fold

Mixed-size tile mosaic: hero stat with trend, map and heatmap as tiles, live stream as a tall cell. One screen, no scrolling, no tabs.

@@ -465,7 +465,7 @@

Page layouts

Attack origins

Sources

13,334

Logins

28,722
-

Live stream

00:28mailoney85.14.245.122
00:28cowrie123.188.73.228
00:27dionaea213.176.26.114
+

Live stream

00:28mailoney203.0.113.122
00:28cowrie123.188.73.228
00:27dionaea213.176.26.114

Payloads

1,286

Alerts

200 open
@@ -487,17 +487,17 @@

Page layouts

OV-Enew · claude

Focus column

Everything in one centered 900px column: serif greeting, boxless stat strip, then a single feed — claude conversation geometry.

-
+
OV-Fclaude × v3

Focus column × dense grid

Serif greeting stays, but below it a two-column mono card grid packs heatmap, stream, sensors and map on one screen.

-
+

Event explorer page

EV-Abase

Current — inline JSON per row

Every row carries its full normalized record inline (#1447). Total evidence, but one event fills the screen.

@@ -505,8 +505,8 @@

Page layouts

@@ -515,7 +515,7 @@

Page layouts

Events
Filter
- +
timesensorsource ipportdetail
00:28:23mailoney85.14.245.12225DATA: 6 bytes saved
00:28:23mailoney203.0.113.12225DATA: 6 bytes saved
00:28:19cowrie123.188.73.22823login.success: root/juantech
@@ -532,15 +532,15 @@

Page layouts

EV-Enew · claude

Floating inspector overlay

Full-width table; the selected record opens as a floating claude-style panel overlapping the right edge — table width never shrinks.

-
+
EV-Fclaude × v3

Dense mono + bottom drawer

v3 mono table on top; the selected record expands a pinned bottom drawer with mono field groups — vertical, never side-by-side.

-
+

Attack sources page

@@ -550,7 +550,7 @@

Page layouts

-
eventssource ipccloginssensors hitlast seen
73,829123.188.73.228CN14,319beelzebub cisco-asa conpot cowrie dionaea +500:15
27,263172.110.223.159HK0dionaea21:49
6,97785.14.245.122DE1,589beelzebub conpot cowrie mailoney +400:28
+ 6,977203.0.113.122DE1,589beelzebub conpot cowrie mailoney +400:28
AS-Bnew · claude

Leaderboard cards

Ranked slabs with share-bars and chips (claude projects-card feel). Instantly shows the whales; less columnar data per row.

@@ -558,7 +558,7 @@

Page layouts

Attack sources
Sort by events ▾
#1 · 123.188.73.228 CN 14,319 logins 12 sensors
73,829
#2 · 172.110.223.159 HK dionaea only
27,263
-
#3 · 85.14.245.122 DE 1,589 logins 8 sensors
6,977
+
#3 · 203.0.113.122 DE 1,589 logins 8 sensors
6,977
AS-Cnew

Map-first split

Geography as the hero, table beneath it. Where-then-who ordering; the map finally earns its size.

@@ -568,9 +568,9 @@

Page layouts

eventssource ipcclast seen
73,829123.188.73.228CN00:15
27,263172.110.223.159HK21:49
AS-Dnew · claude

Profile card grid

Every source is a project-style card in a 3-across grid — claude’s projects page applied to attackers.

-
+
AS-Eclaude × v3

Mono leaderboard

Ranked mono rows with inline share bars — v3 density with claude’s single calm column.

-
+

Reports studio page

@@ -632,13 +632,13 @@

Shell components

12Abase

Stock

Brand, MONITOR/INVESTIGATE/… labels, flat links, profile card bottom.

🐝 APIARY
Monitor
OverviewML anomalies
Investigate
Event explorerAttack sources
XO xore · admin
12Bnew · claude

Claude rail

Segmented mode toggle, one primary action ("Investigate…"), quiet pills, recents; profile bottom.

-
+
12Cnew

Icon rail + flyout

52px icon-only rail; group flyout on hover. Maximum content width all the time.

🏠⚡🌍📄⚙
Investigate ▸
Events · Sources · Campaigns · Clusters
12Dnew

Accordion groups + counts

Collapsible sections with live count chips (alerts, anomalies) — nav doubles as status.

12Enew · claude

Claude minimal rail

Borderless rail: serif brand, raised-pill active, recents — the quietest possible sidebar.

-
+
12Fclaude × v3

Mono tree

Terminal tree: lowercase mono labels, accent chevron marks the active node, group micro-caps.

apiary//ops
monitor
▸ overviewml-anomalies
investigate
eventssources
diff --git a/docs/README.md b/docs/README.md index fb71dd31d..a6c2966e7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -99,8 +99,11 @@ Analysis and sandbox components: era references inside are historical)](dashboard-manual-ip-block-design.md) Subdirectories (`analysis/`, `research/`, `sandbox/`, `vps/`, `autoinstall/`, -`deploy-profiles/`, `design-lab/`) hold the same kinds of documents scoped to +`deploy-profiles/`) hold the same kinds of documents scoped to their component. Every doc must be reachable from this page through links; -dated record trees (`research/`, `benchmarks/`, the VM-detection results, -`design-lab/`, kept as a near-duplicate of `branding/design-lab/`) are +dated record trees (`research/`, `benchmarks/`, the VM-detection results) are exempt. `scripts/check-docs-reachable.py` enforces this in CI (#3332). + +The design lab is not a `docs/` subdirectory: it lives at +[`branding/design-lab/`](../branding/design-lab/README.md), the single copy +since #3310 removed the duplicate fork this tree used to hold. diff --git a/docs/design-lab/README.md b/docs/design-lab/README.md deleted file mode 100644 index 160ab84bb..000000000 --- a/docs/design-lab/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Design lab - -The tooling that produced the dashboard's current design and its colour -palettes. Recovered from a local scratch directory where it was the only -copy — see #1763. - -Nothing here runs in production. It is kept because the stylesheet's own -comments refer to decisions ("pick 6E, supersedes 6D") whose evidence lived -nowhere durable, and because the palette generator is the only executable -record of *why* the themes are the colours they are. - -Real hostnames, the internal server address and a captured attacker IP were -replaced with documentation-range placeholders when these files were moved -into the public repository. Nothing else was edited. - -## Contents - -| file | what it is | -|---|---| -| `gen_palettes.py` | Generates the accent palettes and auto-tunes each until it clears WCAG AA. Holds the seed hues and the pair table. | -| `contrast_scan.js` | The scanning half of the same. | -| `palettes.css` | Generated output — the block that was pasted into `theme.css`. | -| `playground/` | `elements.html` is the option gallery Xore picked from: per-component variants, choices captured in `localStorage`, with a "copy picks" export. `compare.html` is a drag-split variant comparison. | -| `v5-picks-override.css` | A named variant as a token-override block layered on the vendored `theme.css` — the authoring pattern the variants were built with. | -| `design-notes.md` | The raw review log the picks came out of: per-page findings, the reference-app pattern catalog, and the directives collected during review. | - -## The two rounds - -The pick letters in `theme.css` comments refer to the **second** round. Both -are recorded in merged PRs: - -| round | picks | shipped as | -|---|---|---| -| Design refresh | elements `1D/2D/3B/4C/5D/6D/7D/8B/9C/10C`, layouts `OV-B/EV-B/AS-C/RP-C`, shell `11C/12B/13B/14B` | #1573 + `Xore/theme#79` | -| **Design refresh 2 — "claude-pure"** *(current)* | all-**E** element set, with `scroll:D events:D sources:D reports:D` | #1588 + `Xore/theme#84`–`#89`, pin `68e3f09` | - -The live design is the second: floating pill topbar with breadcrumb and -avatar, focus-column overview, minute-grouped event feed with the normalized -record pane opening only on row click, profile-card source grid, -document-style reports studio, circular scroll button, serif empty states -with line icons. - -## Standing rules set during that session - -- No lazy-loading. A "view more" button instead. -- Anything populated from Elasticsearch renders skeletons first, then - hydrates. -- One page width across all pages; the overview is full-width but keeps its - focus elements. -- SPA feel: only the centre content refreshes, never a full browser reload. - -## Colour research - -The palettes came from a 2026 UI colour-trend pass, not a brand benchmark. -Its findings were mostly about **grounds and neutrals** — elevated neutrals -(soft greys, warm sand, stone, muted clay, oatmeal, taupe), zinc/slate as the -dominant direction for technology products, lime with cool whites for -dashboards, "avoid grey-on-grey fatigue" in dark mode, and earth tones for a -sense of long-term value. - -Only the accent half was implementable at the time, because a preset could -reach eight accent tokens. Finishing the other half is #1753. - -## Rebuilding the harness - -The original lab served variant builds against real Elasticsearch data on -ports 19201–19205, driven by an env-guarded Go test that booted the dashboard -with a stubbed OIDC session, a `STATIC_DIR` override and nil write-services -so the real index stayed read-only. That harness depended on the Go dashboard -and went away with it. - -It has since been rebuilt for `frontend-next` as -[`branding/design-lab/lab.mjs`](../../branding/design-lab/lab.mjs) (#1828, -#1935), which serves variants on the same 19201–19205 range and the elements -playground on 19300. `frontend-next` has no nil-write-services handle — it -reaches data over HTTP through two bases — so the read-only guarantee is made -at that seam instead: `BACKEND_URL` goes through a gate that forwards -GET/HEAD and answers 405 to everything else, and `BACKEND_MOUNTED_URL` is -pointed at a stub that answers 503 to every request. That is stronger than the original, which relied -on remembering to pass nil. - -This directory is the redacted public copy and does not carry the harness -itself; run it from `branding/design-lab/`. diff --git a/docs/design-lab/contrast_scan.js b/docs/design-lab/contrast_scan.js deleted file mode 100644 index b484f3a39..000000000 --- a/docs/design-lab/contrast_scan.js +++ /dev/null @@ -1,59 +0,0 @@ -// WCAG contrast scan for the current page (design lab). -// Returns unique low-contrast (fg,bg) pairs with a sample selector + text. -(() => { - const lum = (r, g, b) => { - const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; - return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); - }; - const parse = c => { - const m = c.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)/); - return m ? [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]] : null; - }; - const blend = (top, bottom) => { - const a = top[3]; - return [top[0] * a + bottom[0] * (1 - a), top[1] * a + bottom[1] * (1 - a), top[2] * a + bottom[2] * (1 - a), 1]; - }; - const effBg = el => { - let bg = [32, 32, 31, 1]; // app ground fallback - const chain = []; - for (let n = el; n && n !== document.documentElement; n = n.parentElement) chain.push(n); - chain.reverse().forEach(n => { - const c = parse(getComputedStyle(n).backgroundColor); - if (c && c[3] > 0) bg = blend(c, bg); - }); - return bg; - }; - const ratio = (a, b) => { - const l1 = lum(a[0], a[1], a[2]), l2 = lum(b[0], b[1], b[2]); - const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1]; - return (hi + 0.05) / (lo + 0.05); - }; - const seen = new Map(); - document.querySelectorAll("body *").forEach(el => { - if (!el.offsetParent && getComputedStyle(el).position !== "fixed") return; - const text = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).join(" ").trim(); - if (!text) return; - const cs = getComputedStyle(el); - const fg = parse(cs.color); - if (!fg || fg[3] < 0.1) return; - const bg = effBg(el); - const fgB = fg[3] < 1 ? blend(fg, bg) : fg; - const r = ratio(fgB, bg); - const size = parseFloat(cs.fontSize); - const bold = parseInt(cs.fontWeight, 10) >= 700; - const large = size >= 24 || (size >= 18.66 && bold); - const threshold = large ? 3 : 4.5; - if (r >= threshold) return; - const key = cs.color + "|" + JSON.stringify(bg.map(Math.round)) + "|" + (el.className || el.tagName); - if (seen.has(key)) return; - seen.set(key, { - ratio: Math.round(r * 100) / 100, - need: threshold, - color: cs.color, - bg: "rgb(" + bg.slice(0, 3).map(Math.round).join(",") + ")", - el: el.tagName.toLowerCase() + (el.className ? "." + String(el.className).trim().split(/\s+/).slice(0, 2).join(".") : ""), - text: text.slice(0, 40), - }); - }); - return [...seen.values()].sort((a, b) => a.ratio - b.ratio).slice(0, 25); -})() diff --git a/docs/design-lab/design-notes.md b/docs/design-lab/design-notes.md deleted file mode 100644 index b18a76c6b..000000000 --- a/docs/design-lab/design-notes.md +++ /dev/null @@ -1,95 +0,0 @@ -# APIARY dashboard design review — dashboard.example — 2026-08-17 - -> **Public, redacted copy — status as of 2026-09-27.** The address redaction in -> this file is deliberate and correct as it stands: the only address literals -> here are `127.0.0.1` (loopback) and `203.0.113.1` (RFC 5737 TEST-NET-3), and -> the only hostname is a reserved `.example` domain. Do not substitute real -> values for them, and do not restore anything redacted out of the source copy. -> -> The findings below are a **snapshot of a review session on 2026-08-17, not a -> description of the current code**. Several cite the Go dashboard's static -> assets and route table by name (`hp-app.js`, `hp-dynamic-nav.js`, -> `routes.go`); that dashboard was deleted in #1628 on 2026-08-22, five days -> after this review, and none of those files exist in the repository any more. -> The current route authority is the Rust `axum` service at -> `arcane/home/honeypot-dashboard/backend-service/src/main.rs`, so re-locate a -> finding's code there before acting on it. -## Findings (running) -- Overview (light): loads fast, authenticated. Heatmap "Activity — last 24h" dominates; lower rows (multipot, conpot-kamstrup, endlessh) appear near-empty/pale — visual weight wasted? -- Theme toggle: monitor icon top-right (left of LIVE). Dark theme renders correctly on Overview. -- Overview heatmap card ("Activity — last 24h") is a fixed-height inner scroll container: mouse wheel over it scrolls the card, not the page (scroll trap); inner scrollbar is very subtle. ~13+ sensor rows hidden below the fold of the card. -- Heatmap uses one global color scale: dionaea (~90k/h) saturates; low-volume sensors (multipot ~240/h, endlessh, galah) render near-empty. Per-sensor normalization would make rows readable. -- Header alert badge shows "99+" — alert count overflow, arguably alarming-by-default. -- Overview "Recent events": every row renders its FULL normalized-event JSON article inline (#1447 intentional). On Overview this makes the stream ~18 x ~600px of JSON inside a fixed-height card__scroll; the table header (time/sensor/source ip/port/detail) no longer matches what the eye sees. Suggest: compact rows on Overview stream (detail on /events only), or max-height+inner-scroll on the article, or collapse-with-summary that keeps content in DOM for a11y. -- Events table: "source ip" column too narrow → IP wraps mid-address (203.0.113.1 / 22); country badge wraps below. Port cell renders ":25" with leading colon — stylistic, looks like a typo when cell wraps. -- Heatmap card keeps internal scroll position after page scroll; no affordance that rows are hidden above/below inside the card. -- Sections on Live operations tab: Current activity (heatmap), Attack origins — live geographic view (map), Collection status (Sensor feeds, Protocols probed), ML classification backlog, Live event stream. -- "Attack origins" Leaflet map (dark): world tiles don't fill card width at zoom 2 — large blank light-gray gutters left/right clash with dark theme; "World" reset control top-right is clipped mid-word; Leaflet attribution overlaps the card's bottom edge. Suggest maxBounds/fitBounds or ocean-colored background + unclipped control. -- Uniform 340px card__scroll heights force inner scrolling on nearly every Overview card (heatmap 1357px, sensor feeds 1073px, recent events 11018px inside 340px!). Consider taller defaults or per-card sizing. -- Sensor feeds: count column (accent-colored links) sits far from sensor badge; ACTIVE status + relative time right-aligned — scannable but the wide empty middle reads as a layout gap at 1568px. -## Threat landscape tab -- 127.0.0.1 ranks #6 in "Top source IPs" (94k events) — loopback/tunnel artifact polluting top-N; filter or label it. -- "Network/provider classes" card: 4 short rows in a full-height card next to a packed 340px-scroll "Top autonomous systems" — unbalanced pair. -- Traffic volume bytes + packets: two stacked near-identical spiky linear charts; raw axis numbers ("25,000,000,000") — humanize units (GB, M pkts), consider log scale or merged dual-axis card. -- "Protocol-conformance violations": empty state renders "No data yet." twice (chart center + caption below). -- "Top exploited CVEs": with 2 buckets, first bar is a giant unstyled gray slab ~40% of card width; second bar invisible (zero height rendering); rotated x-labels truncated ("DoublePulsar connection at…"). Needs barMaxWidth, readable labels, non-fallback color. -## Attacker behavior tab (pre-refresh snapshot) -- "Top commands" list shows visually duplicate entries: "enable" x2, "linuxshell" x2, "shell" x2, each 623 — likely trailing-whitespace/variant strings rendered identically; dedupe or make the difference visible. -- Attacker OS donut: callout labels for tiny slices pile up/overlap at the top (7 labels stacked, leader lines crossing); legend below already covers them — drop callouts for <2% slices. -- "TLS scanner fingerprints (JA4)" bar chart: 15 rotated x-labels are long truncated hashes ("t13i1910q0_9dc949149365…") — unreadable; bars all same gray. Horizontal bars + copyable full hash tooltip would serve better. -- (paused here: user is redeploying a layout change; Evidence & campaigns tab + light-theme pass still pending) -## Post-refresh session -- BUG (repro'd): clicking the theme toggle shortly after first page load does not persist — hp-app.js savePrefs() early-returns while prefState.ready=false, so only localStorage is set; the subsequent server prefs sync ("system") then overwrites it on next load. Once prefs are ready the toggle persists fine (verified r14->r15). Fix: queue the pending patch until ready, or apply localStorage as source-of-truth when server still has defaults. -- Theme cycle is system -> dark -> light with no visible state indication beyond a title tooltip; 3-state cycle on an icon button is hard to discover (user had to tell me where it is). Consider a small menu or showing the active mode. -## Evidence & campaigns tab -- "Suricata alerts" empty state leaks an ops diagnostic ("no alerts — is the VPS eve.json mount at /logs/suricata alive?") while sensor feeds show suricata ACTIVE with 1.4M events — either a data-path regression or wrong empty-state copy; sibling card says "no suricata alerts yet" (inconsistent phrasing). -- "Correlated campaigns": score column shows 100 for every row (capped) — a column of identical values carries no information; consider hiding or showing the raw components. Header link copy "Every column, every network →" is cryptic. -## ML anomalies page (dark) -- Top bar center label reads "Operations" on this page (and page category is DETECTION) — topbar/page title mapping inconsistent (Overview page correctly says "Overview"). -- KPI tile "0 Anomalies, 24h" directly above a long table of OPEN anomalies from 6 days ago — leading stat contradicts what the eye sees; show open-backlog count alongside. -- Table: explanation column repeats "Statistical outlier (composite score X)" on every row (redundant with score column); timestamps are raw ISO-with-millis ("2026-08-11T12:35:43.666Z") unlike the rest of the app; ~40 identical "acknowledge" buttons with no bulk action; several exact-duplicate rows (same second, same IP) differing only by 0.01 score. -- Chart legend: "lstm_ae" series color is a dim gray-blue, label+points nearly invisible in dark theme. -- Filter popover: fine (native selects), no explicit close affordance. -## LLM analysis / Agent campaigns / Auth-failure events (dark) -- Topbar center label says "Operations" on all Monitor pages regardless of the page (title mapping bug/inconsistency). -- Auth-failure events: the ONLY content is a table, yet it's confined to a ~300px card__scroll with a clipped half-row at its edge while the rest of the viewport is blank — single-table pages should let the table grow. Timestamps are raw "2026-08-11T15:23:03.350000+00:00" (microseconds+offset). "0 Failed logins, 24h" tile above a card full of older rows (same pattern as ML anomalies). -- LLM analysis: pre-search the semantic-search card shows an empty results table with headers + placeholder row — could hide the table until a query runs. -- Empty states are inconsistent across pages: "No LLM analysis documents yet." / "No campaign has crossed a criticality-rule threshold yet." / "no suricata alerts yet" / "No data yet." — different tones and capitalization. -## SPA-feel (user goal) -- Requirement from Xore: the dashboard should feel like ONE page — persistent shell, only center content swaps; never a full browser reload. Today only the #1139/#1141 payload/results family uses fetch-and-swap (hp-dynamic-nav.js DYNAMIC_ROUTES + hp-app.js mountPage/replaceHoneypotPage); every sidebar link is a full navigation. Proposal: generalize the existing mechanism to all shell routes (sidebar, topbar links, back/forward via pushState), with per-page script re-init handled the way hp-dynamic-nav already does for its family. -## CRITICAL: session decay silently kills all JS features -- Repro: leave any page open ~5-10 min. Every /api/* fetch starts returning 401 "authentication required" (filter-values, settings/me, stats, whoami). Full-page navigation still works (redirect re-auth), so the operator sees a live-looking page whose filters return nothing, theme/preference saves are dropped (explains #1561's visible symptom), SSE stream dead. After manual reload, everything works again (filter-values returns 20 sensors). -- Fix directions: on first 401 from any fetch, transparently re-auth (hidden iframe/redirect w/ prompt=none) or show a "session expired — click to renew" toast; make refreshSession actually keep API sessions alive under an open page (the 1-min proactive window may be misconfigured vs Keycloak token lifetimes). -## Events filter popover -- Autocomplete (#303) works when session is fresh: click sensor field → real values+counts. Missing on "attack path" and "since" fields (no data-hp-filter-field). Popover styling consistent. -## User directives collected during review -- One flawless page: SPA-feel, only center content refreshes (generalize hp-dynamic-nav to all routes). -- Pages must fill viewport height/width with modest padding; Event explorer card called out as "very small, hard to see". -- Filter fields should auto-populate on click (works, but broken by session decay; add to attack-path/since too). -- Nav list should include hidden routes (found in routes.go, not in sidebar): /alerts, /source-health, /search, /sensors, /history, /dead-letters, /canarytokens, /ghidra, /revdeck, /cape, /github-analysis, /sandbox, /payload-workbench, /problem-reports(?), settings modal. Decide which become nav entries (grouped), which stay contextual. -- Build local variant dashboard w/ real data (homeserver xore@) for side-by-side design choices incl. PDF design. -## Investigate pages (rest) -- /ips: solid table; date cols wrap to 2 lines at 1568px; source-ip wraps mid-IP occasionally. -- /campaigns: worst column-crush — network CIDR wraps mid-value ("169.58.1/70.0/24"), provider wraps "networ/k", dates wrap "2026-/08-16", trailing "ES →" col clipped by card edge; 15 columns is too many for one table. Card shows ~4.5 rows, half viewport dead below. -- /clusters: fine; "1 sensors:" grammar; investigate→/ES→ twin link columns could be one action menu. -- /attackers: long skeleton load (~10s+); first/last timestamps wrap mid-CHARACTER across 3 lines ("2026-08-1 / 6T23:17:0 / 1Z"); verdict badge only on some rows; entity ids as bare hex links. -- /kill-chain: sankey good; "Execution/Initial Access" node labels overlap; ATT&CK coverage grid uses LIGHT-GRAY zebra columns in dark theme (clashes hard); campaign timeline chart is nice. -- /commands: best data page — horizontal bars readable; sources column crams 5 IPs + "+15"; dates wrap "2026-/08-16". -- /recordings: clean; consistent. -## Reports studio -- Strongest page overall: numbered step tabs, template gallery, sticky action bar, PDF theme toggle pills. Template grid leaves a hole (9 cards in 6-wide grid). -- Library: generated-report card exposes Delete right next to Download (destructive adjacency, no confirm visible); saved-definitions empty-state fine. -## PDF (Payload Analysis Report, dark theme) -- Branded header + stat tiles look good; BUT tile values truncate mid-word with ellipsis ("Windows DLL / pe-d…", "6.380 (not packed-l…") — unacceptable in a print artifact; tiles need wrap/autosize. -- Copy bug: "Observed window: not available to not available". -- 3 pages; body typography consistent; consider light-theme default for print friendliness (dark PDF prints badly). -## Header/menus/dialogs -- Search overlay: no live suggestions while session decayed; pressing Enter did nothing (silently dead). With fresh session untested-live but code has preview. -- /alerts: 200 identical-class YARA alerts flood the list (source of the perpetual "99+" badge); per-row acknowledge + "acknowledge all (200)"; consider grouping by rule with counts. -- /source-health: good page (KPI row, two-column detail, inline footnote). ES health chip "ES yellow" plain-text — could be a status badge. -- Account menu (Dashboard settings / Account & security / Log out) fine; settings modal is well-structured (search, Personal/Administration groups, Appearance has theme/density/motion/high-contrast/large-text). -- BUG "Report a problem" dialog: form fields clipped at viewport top, title "Report a problem" renders detached mid-dialog BELOW the form, bottom half of modal empty — broken layout order/positioning. -- Sensor detail page exists in nav now (new deploy); session ids/timestamps wrap; card 340px again. -## claude.ai exhaustive audit (patterns catalog, 2026-08-17) -Pressed/inspected: sidebar collapse (+tooltip w/ Ctrl+B hint), sidebar search → centered overlay (first result preselected, "Enter" chip, relative ages right), Home/Code segmented toggle, + attach menu (grouped, submenu chevrons, shortcut hints, checkmark toggles), model selector (name + one-line description, Effort submenu, "More models" disclosure), incognito ghost icon (distinct darker full-screen mode w/ serif headline + explainer + chrome-less top bar), Recents page (serif title + right toolbar: search/filter/Select/primary), Artifacts (scope text-tabs All/Yours/Shared; illustrated empty state w/ headline+copy+CTA), Scheduled (empty state + dashed divider + TEMPLATE GALLERY: icon/title/desc/schedule chip), Customize → settings modal deep-link (Skills table w/ Browse+Add toolbar), settings modal (search-topped rail, label-left/control-right rows, 3-icon appearance segmented), chat-row hover kebab + section-label hover controls, humane 404 ("…finding this page isn't one of them" + Go back home). Skipped: logout, billing/payment/upgrade, mic/dictation, sending messages. -Adoption ideas for APIARY (follow-ups): template-gallery empty states (reports/canarytokens/scheduled-reports), witty 404 page, "Enter" hint chip in palette results, tooltip+shortcut hints on icon buttons, hover kebab on table rows (partially done via 14B), settings deep-links from sidebar. diff --git a/docs/design-lab/gen_palettes.py b/docs/design-lab/gen_palettes.py deleted file mode 100644 index d2cfc8c1e..000000000 --- a/docs/design-lab/gen_palettes.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -"""Generate AA-validated palette preset CSS for theme.css. - -Per palette x theme(dark/light) we need: - accent, accent-hover, accent-pressed, accent-soft, - accent-text-on-soft, text-on-accent, text-link, text-link-hover -Checks (WCAG AA, >=4.5:1 for text): - accent-text-on-soft vs surface-1 (badge/soft ink) - text-link vs surface-1 (links on cards) - text-on-accent vs accent (primary button ink) -Auto-tunes lightness until every check passes. -""" -import colorsys - -DARK_S1 = "#2c2c2a" -LIGHT_S1 = "#f4f2ed" - -def hex2rgb(h): - h = h.lstrip("#") - return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) - -def rgb2hex(r, g, b): - return "#%02x%02x%02x" % (round(r), round(g), round(b)) - -def rel_lum(rgb): - def f(c): - c = c / 255 - return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 - r, g, b = (f(c) for c in rgb) - return 0.2126 * r + 0.7152 * g + 0.0722 * b - -def contrast(a, b): - la, lb = rel_lum(hex2rgb(a)), rel_lum(hex2rgb(b)) - hi, lo = max(la, lb), min(la, lb) - return (hi + 0.05) / (lo + 0.05) - -def adjust_l(h, dl): - r, g, b = hex2rgb(h) - hh, ll, ss = colorsys.rgb_to_hls(r/255, g/255, b/255) - ll = min(1, max(0, ll + dl)) - r, g, b = colorsys.hls_to_rgb(hh, ll, ss) - return rgb2hex(r*255, g*255, b*255) - -def tune(color, bg, target=4.5, direction=+1, max_steps=60): - c = color - for _ in range(max_steps): - if contrast(c, bg) >= target: - return c - c = adjust_l(c, 0.015 * direction) - return c - -def ink_for(accent): - # pick near-black or near-white ink for text-on-accent, whichever passes better - dark_ink = tune("#1c1613", accent, direction=-1) - light_ink = tune("#ffffff", accent, direction=+1) - cd, cl = contrast(dark_ink, accent), contrast(light_ink, accent) - return dark_ink if cd >= cl else light_ink - -# name -> (dark accent, light accent) seed hues -PALETTES = { - "slate": ("#8aa2c0", "#44618a"), - "ocean": ("#55a7d8", "#1f6fa8"), - "sage": ("#8fb27b", "#4d7a42"), - "lavender": ("#ab93e3", "#6d4fc4"), - "lime": ("#b3cf5a", "#5f7d1f"), - "amber": ("#d9a842", "#96690e"), - "rose": ("#d98298", "#b04a66"), - "neon": ("#3ee6c8", "#0f8f7a"), -} - -def block(name, accent, s1, theme): - # soft ink must clear AA on surface-1; links likewise - soft_ink = tune(accent, s1, direction=(+1 if theme == "dark" else -1)) - link = tune(accent, s1, direction=(+1 if theme == "dark" else -1)) - link_hover = adjust_l(link, +0.06 if theme == "dark" else -0.06) - hover = adjust_l(accent, +0.05 if theme == "dark" else -0.04) - pressed = adjust_l(accent, -0.05 if theme == "dark" else -0.08) - r, g, b = hex2rgb(accent) - soft = f"rgba({r}, {g}, {b}, {0.16 if theme == 'dark' else 0.13})" - ink = ink_for(accent) - toks = [ - ("--accent", accent), ("--accent-hover", hover), ("--accent-pressed", pressed), - ("--accent-soft", soft), ("--accent-text-on-soft", soft_ink), - ("--text-on-accent", ink), ("--text-link", link), ("--text-link-hover", link_hover), - ] - body = " ".join(f"{k}: {v};" for k, v in toks) - # report - checks = { - "soft-ink": contrast(soft_ink, s1), - "link": contrast(link, s1), - "btn-ink": contrast(ink, accent), - } - assert all(v >= 4.5 for v in checks.values()), (name, theme, checks) - return body, checks - -out = [] -report = [] -out.append("/* ── Palette presets (per Xore): one-click accent palettes, selected in") -out.append(" Settings → Appearance and applied as data-hp-palette on .") -out.append(' "claude" is the default token set above (no attribute). Every pair') -out.append(" below is WCAG-AA validated (soft ink + links vs surface-1, button") -out.append(" ink vs accent) in BOTH themes by scripts in the APIARY design lab;") -out.append(" status colors stay semantic across palettes. */") -for name, (dark, light) in PALETTES.items(): - d_body, d_checks = block(name, dark, DARK_S1, "dark") - l_body, l_checks = block(name, light, LIGHT_S1, "light") - out.append(f':root[data-hp-palette="{name}"] {{ {d_body} }}') - out.append(f'[data-theme="light"][data-hp-palette="{name}"] {{ {l_body} }}') - out.append("@media (prefers-color-scheme: light) {") - out.append(f' :root:not([data-theme])[data-hp-palette="{name}"] {{ {l_body} }}') - out.append("}") - report.append(f"{name}: dark {d_checks} | light {l_checks}") - -open("/home/adminuser/.claude/jobs/e78c0b17/tmp/palettes.css", "w").write("\n".join(out) + "\n") -print("\n".join(report)) -print("CSS written") diff --git a/docs/design-lab/palettes.css b/docs/design-lab/palettes.css deleted file mode 100644 index 9b5748403..000000000 --- a/docs/design-lab/palettes.css +++ /dev/null @@ -1,46 +0,0 @@ -/* ── Palette presets (per Xore): one-click accent palettes, selected in - Settings → Appearance and applied as data-hp-palette on . - "claude" is the default token set above (no attribute). Every pair - below is WCAG-AA validated (soft ink + links vs surface-1, button - ink vs accent) in BOTH themes by scripts in the APIARY design lab; - status colors stay semantic across palettes. */ -:root[data-hp-palette="slate"] { --accent: #8aa2c0; --accent-hover: #9bafc9; --accent-pressed: #7995b7; --accent-soft: rgba(138, 162, 192, 0.16); --accent-text-on-soft: #8aa2c0; --text-on-accent: #1c1613; --text-link: #8aa2c0; --text-link-hover: #9eb2cb; } -[data-theme="light"][data-hp-palette="slate"] { --accent: #44618a; --accent-hover: #3d577c; --accent-pressed: #374e6f; --accent-soft: rgba(68, 97, 138, 0.13); --accent-text-on-soft: #44618a; --text-on-accent: #ffffff; --text-link: #44618a; --text-link-hover: #3a5376; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="slate"] { --accent: #44618a; --accent-hover: #3d577c; --accent-pressed: #374e6f; --accent-soft: rgba(68, 97, 138, 0.13); --accent-text-on-soft: #44618a; --text-on-accent: #ffffff; --text-link: #44618a; --text-link-hover: #3a5376; } -} -:root[data-hp-palette="ocean"] { --accent: #55a7d8; --accent-hover: #6ab2dd; --accent-pressed: #409cd3; --accent-soft: rgba(85, 167, 216, 0.16); --accent-text-on-soft: #55a7d8; --text-on-accent: #1c1613; --text-link: #55a7d8; --text-link-hover: #6eb4de; } -[data-theme="light"][data-hp-palette="ocean"] { --accent: #1f6fa8; --accent-hover: #1c6497; --accent-pressed: #195886; --accent-soft: rgba(31, 111, 168, 0.13); --accent-text-on-soft: #1f6fa8; --text-on-accent: #ffffff; --text-link: #1f6fa8; --text-link-hover: #1a5e8e; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="ocean"] { --accent: #1f6fa8; --accent-hover: #1c6497; --accent-pressed: #195886; --accent-soft: rgba(31, 111, 168, 0.13); --accent-text-on-soft: #1f6fa8; --text-on-accent: #ffffff; --text-link: #1f6fa8; --text-link-hover: #1a5e8e; } -} -:root[data-hp-palette="sage"] { --accent: #8fb27b; --accent-hover: #9dbb8b; --accent-pressed: #81a96b; --accent-soft: rgba(143, 178, 123, 0.16); --accent-text-on-soft: #8fb27b; --text-on-accent: #1c1613; --text-link: #8fb27b; --text-link-hover: #9fbd8e; } -[data-theme="light"][data-hp-palette="sage"] { --accent: #4d7a42; --accent-hover: #456d3b; --accent-pressed: #3c6034; --accent-soft: rgba(77, 122, 66, 0.13); --accent-text-on-soft: #4a753f; --text-on-accent: #ffffff; --text-link: #4a753f; --text-link-hover: #3d6134; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="sage"] { --accent: #4d7a42; --accent-hover: #456d3b; --accent-pressed: #3c6034; --accent-soft: rgba(77, 122, 66, 0.13); --accent-text-on-soft: #4a753f; --text-on-accent: #ffffff; --text-link: #4a753f; --text-link-hover: #3d6134; } -} -:root[data-hp-palette="lavender"] { --accent: #ab93e3; --accent-hover: #bba7e8; --accent-pressed: #9b7fde; --accent-soft: rgba(171, 147, 227, 0.16); --accent-text-on-soft: #ab93e3; --text-on-accent: #1c1613; --text-link: #ab93e3; --text-link-hover: #beabe9; } -[data-theme="light"][data-hp-palette="lavender"] { --accent: #6d4fc4; --accent-hover: #6040bf; --accent-pressed: #593baf; --accent-soft: rgba(109, 79, 196, 0.13); --accent-text-on-soft: #6d4fc4; --text-on-accent: #ffffff; --text-link: #6d4fc4; --text-link-hover: #5d3db7; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="lavender"] { --accent: #6d4fc4; --accent-hover: #6040bf; --accent-pressed: #593baf; --accent-soft: rgba(109, 79, 196, 0.13); --accent-text-on-soft: #6d4fc4; --text-on-accent: #ffffff; --text-link: #6d4fc4; --text-link-hover: #5d3db7; } -} -:root[data-hp-palette="lime"] { --accent: #b3cf5a; --accent-hover: #bcd56e; --accent-pressed: #aac946; --accent-soft: rgba(179, 207, 90, 0.16); --accent-text-on-soft: #b3cf5a; --text-on-accent: #1c1613; --text-link: #b3cf5a; --text-link-hover: #bed672; } -[data-theme="light"][data-hp-palette="lime"] { --accent: #5f7d1f; --accent-hover: #536d1b; --accent-pressed: #465c17; --accent-soft: rgba(95, 125, 31, 0.13); --accent-text-on-soft: #5a771d; --text-on-accent: #ffffff; --text-link: #5a771d; --text-link-hover: #475e17; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="lime"] { --accent: #5f7d1f; --accent-hover: #536d1b; --accent-pressed: #465c17; --accent-soft: rgba(95, 125, 31, 0.13); --accent-text-on-soft: #5a771d; --text-on-accent: #ffffff; --text-link: #5a771d; --text-link-hover: #475e17; } -} -:root[data-hp-palette="amber"] { --accent: #d9a842; --accent-hover: #ddb257; --accent-pressed: #d59e2d; --accent-soft: rgba(217, 168, 66, 0.16); --accent-text-on-soft: #d9a842; --text-on-accent: #1c1613; --text-link: #d9a842; --text-link-hover: #deb45b; } -[data-theme="light"][data-hp-palette="amber"] { --accent: #96690e; --accent-hover: #835c0c; --accent-pressed: #714f0b; --accent-soft: rgba(150, 105, 14, 0.13); --accent-text-on-soft: #8f640d; --text-on-accent: #ffffff; --text-link: #8f640d; --text-link-hover: #73500a; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="amber"] { --accent: #96690e; --accent-hover: #835c0c; --accent-pressed: #714f0b; --accent-soft: rgba(150, 105, 14, 0.13); --accent-text-on-soft: #8f640d; --text-on-accent: #ffffff; --text-link: #8f640d; --text-link-hover: #73500a; } -} -:root[data-hp-palette="rose"] { --accent: #d98298; --accent-hover: #df96a8; --accent-pressed: #d36e88; --accent-soft: rgba(217, 130, 152, 0.16); --accent-text-on-soft: #d98298; --text-on-accent: #1c1613; --text-link: #d98298; --text-link-hover: #e099ab; } -[data-theme="light"][data-hp-palette="rose"] { --accent: #b04a66; --accent-hover: #a2445e; --accent-pressed: #933e55; --accent-soft: rgba(176, 74, 102, 0.13); --accent-text-on-soft: #b04a66; --text-on-accent: #ffffff; --text-link: #b04a66; --text-link-hover: #9a415a; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="rose"] { --accent: #b04a66; --accent-hover: #a2445e; --accent-pressed: #933e55; --accent-soft: rgba(176, 74, 102, 0.13); --accent-text-on-soft: #b04a66; --text-on-accent: #ffffff; --text-link: #b04a66; --text-link-hover: #9a415a; } -} -:root[data-hp-palette="neon"] { --accent: #3ee6c8; --accent-hover: #55e9ce; --accent-pressed: #27e3c2; --accent-soft: rgba(62, 230, 200, 0.16); --accent-text-on-soft: #3ee6c8; --text-on-accent: #1c1613; --text-link: #3ee6c8; --text-link-hover: #59ead0; } -[data-theme="light"][data-hp-palette="neon"] { --accent: #0f8f7a; --accent-hover: #0d7d6a; --accent-pressed: #0b6a5a; --accent-soft: rgba(15, 143, 122, 0.13); --accent-text-on-soft: #0c7a68; --text-on-accent: #171210; --text-link: #0c7a68; --text-link-hover: #095e50; } -@media (prefers-color-scheme: light) { - :root:not([data-theme])[data-hp-palette="neon"] { --accent: #0f8f7a; --accent-hover: #0d7d6a; --accent-pressed: #0b6a5a; --accent-soft: rgba(15, 143, 122, 0.13); --accent-text-on-soft: #0c7a68; --text-on-accent: #171210; --text-link: #0c7a68; --text-link-hover: #095e50; } -} diff --git a/docs/design-lab/playground/compare.html b/docs/design-lab/playground/compare.html deleted file mode 100644 index 66fcf8bca..000000000 --- a/docs/design-lab/playground/compare.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - -Split compare — APIARY Design Lab - - - -
- ← lab - left - right - - path - - - -
-
- -
-
- - -
- - - diff --git a/docs/design-lab/playground/elements.html b/docs/design-lab/playground/elements.html deleted file mode 100644 index 33c118dde..000000000 --- a/docs/design-lab/playground/elements.html +++ /dev/null @@ -1,700 +0,0 @@ - - - - - -Design Picker — APIARY Design Lab - - - - -
-

Design Picker

-

Elements AND page layouts in one place. The small tag on each option shows where it comes from (base = current design, v1–v4 = a lab variant, new = drafted here). Click to pick one per row. Picks persist in this browser — hit “copy picks” and paste the result back to Claude. ← back to lab

-
-
Picks:none yet
- -

1 · Card container

-
1Abase / v1

Stock

Hairline border, flat surface — current theme.

-

Sensor feeds

29 sensors reporting. Active = recent traffic.

-
1Bv2 · v4

Soft elevated

Borderless, larger radius, layered shadow (v2/claude feel).

-

Sensor feeds

29 sensors reporting. Active = recent traffic.

-
1Cnew

Accent-top

2px accent keyline marks every card; tighter radius.

-

Sensor feeds

29 sensors reporting. Active = recent traffic.

-
1Dnew

Header strip

Title in a contrasting header band — stronger card anatomy.

-

Sensor feeds

29 sensors reporting. Active = recent traffic.

-
1Enew · claude

Claude floating

Borderless, big radius, soft layered shadow, serif title — pure claude surface.

-

Sensor feeds

29 sensors reporting. Active = recent traffic.

-
1Fclaude × v3

Floating dense

Claude’s shadowed borderless slab at v3 density: tight padding, mono micro-caps title.

-

sensor feeds

29 sensors reporting. Active = recent traffic.

-
- -

2 · Skeleton / loading card

-
2Abase

Static lines

Current: plain gray bars, no motion.

-
-
2Bnew

Shimmer

Light sweep signals active loading.

-
-
2Cnew

Pulse

Gentle opacity breathing; calmer than shimmer, still alive.

-
-
2Dnew

Content-shaped ghost

Ghost mirrors the final layout (tiles + rows) — zero layout shift.

-
-
2Enew · claude

Breathing card ghost

The whole placeholder card breathes gently — claude’s thinking rhythm, serif-sized title bar.

-
-
2Fclaude × v3

Mono scanlines

Label-column + value bars per row — dense table ghost at v3 rhythm, quick pulse.

-
-
- -

3 · KPI stat tile

-
3Abase

Stock

Big number, quiet label below.

-
3,076,059
Events in 24h
-
3Bnew

Delta + sparkline

Trend at a glance: change chip and 24h micro-trend.

-
3,076,059+21%
Events in 24h
-
3Cnew

Icon-led

Category icon anchors the tile; friendlier scan.

-
⚡
3,076,059
Events in 24h
-
3Dv3

Dense mono

Label-first micro-caps, mono numerals (terminal).

-
3,076,059
Events · 24h
-
3Enew · claude

Boxless serif strip

No tiles at all: serif numerals over quiet labels, hairline dividers — claude stat strip.

-
3,076,059Events · 24h
13,334Attack sources
1,286Payloads
-
3Fclaude × v3

Boxless mono strip

Same open strip, v3 data treatment: micro-caps labels first, mono numerals, inline delta.

-
events 24h3,076,059+12%
sources13,334
payloads1,286
-
- -

4 · Data table rows

-
4Abase / v1

Stock

Hairline row separators.

-
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
-
4Bnew

Zebra

Alternating row wash for long-row tracking.

-
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
-
4Cnew

Card rows

Each row its own rounded slab — modern SaaS lists.

-
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
-
4Dv3

Dense mono grid

Column rules + mono cells; maximum rows per screen.

-
timesensorsource ip
00:28:23cowrie203.0.113.122
00:27:11dionaea123.188.73.228
00:26:40multipot213.176.26.114
-
4Enew · claude

Open rows + hover pill

No chrome: airy padding, hairline separators, whole row lifts on hover — claude list feel.

-
timesensorsource ip
00:28:23mailoney203.0.113.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114
-
4Fclaude × v3

Open dense mono

Same chrome-free rows at v3 density: mono cells, 5px padding, micro-caps headers.

-
timesensorsource ip
00:28:23mailoney203.0.113.122
00:28:19cowrie123.188.73.228
00:28:11dionaea213.176.26.114
-
- -

5 · Badges & status

-
5Abase

Stock soft

Tinted background chips.

-
ACTIVE cowrie stale open
-
5Bnew

Outline

Transparent, current-color ring — lighter surface noise.

-
ACTIVE cowrie stale open
-
5Cnew

Dot + label

Status dot, no chip at all — quietest option.

-
ACTIVE cowrie stale open
-
5Dnew

Solid

Full-strength fills — loudest, best for few statuses.

-
ACTIVE cowrie stale open
-
5Enew · claude

Neutral chip + dot

One quiet surface chip everywhere; only the dot carries the status color — claude’s restraint.

-
healthy tcp/445 quiet critical
-
5Fclaude × v3

Bracket mono

No chip at all: [ bracketed ] mono words in the status color — terminal-dense.

-
healthy tcp/445 quiet critical
-
- -

6 · Buttons

-
6Abase

Stock

Rounded-9 controls.

-
-
6Bv2 · v4

Pill

Fully rounded (claude.ai feel).

-
-
6Cv3

Compact square

Tight radius + smaller hit area (dense/terminal).

-
-
6Dnew

Ghost + outline primary

Minimal chrome; only the primary carries color.

-
-
6Enew · claude

Claude solid pill

claude.ai’s actual buttons: solid accent pill primary, quiet surface pills for the rest, zero borders.

-
-
6Fclaude × v3

Compact mono pill

Pill silhouette at v3 scale: shorter, mono labels — dense toolbars stay calm.

-
-
- -

7 · Tabs / section switcher

-
7Abase

Stock pills

Numbered pills in a container (current Overview).

-
Live operationsThreat landscapeAttacker behavior
-
7Bnew

Underline

Browser-classic; lightest visual weight.

-
Live operationsThreat landscapeAttacker behavior
-
7Cnew

Segmented

One bordered control, macOS-style.

-
Live operationsThreat landscapeAttacker behavior
-
7Dnew

Vertical rail

Left rail with accent bar — suits many-tab pages (settings).

-
Live operationsThreat landscapeAttacker behavior
-
7Enew · claude

Floating pills

Free-standing pills, active one raised with a soft shadow — no container box (claude toggle feel).

-
Live operationsThreat landscapeAttacker behavior
-
7Fclaude × v3

Mono micro underline

v3 micro-caps mono labels with an accent underline — smallest possible switcher.

-
live opsthreat landscapeattacker behavior
-
- -

8 · Empty state

-
8Abase

Stock mono

Terminal one-liner (current).

-
No canarytokens created yet — use the Create bait tab.
-
8Bnew

Icon + action

Explains and offers the next step.

-
🪤
No canarytokens yetPlant tokens outside the honeypot and track what fires.
Create bait →
-
8Cnew

Dashed invite

Dropzone-style: the emptiness itself looks intentional.

-
No canarytokens yet — create your first from the Create bait tab.
-
8Dnew

Inline note

Compact info banner; no vertical cost.

-
ℹ No canarytokens yet — use the Create bait tab.
-
8Enew · claude

Serif sentence

A calm serif sentence and one quiet pill — claude’s empty-conversation tone.

-
Nothing captured yet.
Canarytokens report here the moment one fires.Create bait →
-
8Fclaude × v3

Prompt line

One mono line with a prompt chevron — costs a single row of space.

-
> no canarytokens — create bait to begin
-
- -

9 · Scrollable region affordance

-
9Abase

Thin scrollbar

Current: always-rendered thin bar.

-
dionaea — 3,149,755
suricata — 1,462,592
cowrie — 768,107
multipot — 131,279
conpot-kamstrup — 11,610
endlessh — 11,501
conpot — 8,981
-
9Bv1

Edge fades

Content visibly dissolves at the cut — universally readable.

-
dionaea — 3,149,755
suricata — 1,462,592
cowrie — 768,107
multipot — 131,279
conpot-kamstrup — 11,610
endlessh — 11,501
conpot — 8,981
-
9Cnew

“More” pill

Explicit count of hidden rows; disappears at the end.

-
dionaea — 3,149,755
suricata — 1,462,592
cowrie — 768,107
multipot — 131,279
conpot-kamstrup — 11,610
endlessh — 11,501
conpot — 8,981
22 more ↓
-
9Dnew · claude

Fade + jump button

Edge fade plus claude’s floating circular ↓ — click scrolls a page.

-
dionaea — 3,149,755
suricata — 1,462,592
cowrie — 768,107
multipot — 131,279
conpot-kamstrup — 11,610
endlessh — 11,501
conpot — 8,981
↓
-
9Eclaude × v3

Corner counter

Tiny mono +N counter pinned to the corner — dense, precise, zero overlay on content.

-
dionaea — 3,149,755
suricata — 1,462,592
cowrie — 768,107
multipot — 131,279
conpot-kamstrup — 11,610
endlessh — 11,501
conpot — 8,981
+22
-
- -

10 · Sidebar nav item

-
10Abase

Stock

Underlined links, filled active block.

-
-
10Bv4

Pills

Rounded hover/active (claude.ai sidebar).

-
-
10Cnew

Accent bar

Left keyline + soft gradient marks the active page.

-
-
10Dv3

Compact text

Smallest footprint; active = accent text only.

-
-
10Enew · claude

Raised pill active

Active page floats on a raised pill with a soft shadow — claude sidebar’s current-chat look.

-
-
10Fclaude × v3

Mono chevron

v3 compact mono; active = accent text with a ▸ chevron, no fills anywhere.

-
-
- - -

Page layouts

-

Different ordering and structure per page — claude.ai-inspired patterns. Same picking mechanism.

-

Overview page

- -
OV-Abase

Current — KPI row, tabs, stacked cards

What ships today: 5 tiles, tab pills, then every card full-width below the fold.

-
- -
OV-Bnew · claude

Claude focus — greeting + command bar

claude.ai home translated: a humane serif status line, one investigate bar as the centerpiece, the rest tucked below as quiet chips and one live feed.

-
- -
OV-Cnew

Bento grid — everything above the fold

Mixed-size tile mosaic: hero stat with trend, map and heatmap as tiles, live stream as a tall cell. One screen, no scrolling, no tabs.

-
- -
OV-Dnew

Main + live rail

Charts own the left 2/3; a sticky right rail carries what changes second-to-second: live stream, open alerts, sensor status. Monitoring-wall feel.

-
-
OV-Enew · claude

Focus column

Everything in one centered 900px column: serif greeting, boxless stat strip, then a single feed — claude conversation geometry.

-
-
OV-Fclaude × v3

Focus column × dense grid

Serif greeting stays, but below it a two-column mono card grid packs heatmap, stream, sensors and map on one screen.

-
-
-

Event explorer page

- -
EV-Abase

Current — inline JSON per row

Every row carries its full normalized record inline (#1447). Total evidence, but one event fills the screen.

-
- -
EV-Bnew · claude

Master–detail — list + sticky record pane

Dense table left, full normalized record permanently open on the right (claude's artifact-panel pattern). Keeps #1447's "no click to see evidence" while restoring table density.

-
- -
EV-Cnew

Compact rows + evidence drawer

Table stays a table; the selected row opens a structured drawer (meta groups + pivots, JSON behind a toggle). Most rows per screen with evidence one keystroke away.

-
- -
EV-Dnew · claude

Timeline stream — chat-style grouping

Events as a time-grouped feed (claude conversation rhythm): minute headers, event cards under each. Reads as a story; best for tailing, not for bulk triage.

-
-
EV-Enew · claude

Floating inspector overlay

Full-width table; the selected record opens as a floating claude-style panel overlapping the right edge — table width never shrinks.

-
-
EV-Fclaude × v3

Dense mono + bottom drawer

v3 mono table on top; the selected record expands a pinned bottom drawer with mono field groups — vertical, never side-by-side.

-
-
-

Attack sources page

- -
AS-Abase

Current — flat table

One sortable table: events, ip, cc, logins, sessions, sensors hit, first/last seen.

-
- -
AS-Bnew · claude

Leaderboard cards

Ranked slabs with share-bars and chips (claude projects-card feel). Instantly shows the whales; less columnar data per row.

-
- -
AS-Cnew

Map-first split

Geography as the hero, table beneath it. Where-then-who ordering; the map finally earns its size.

-
-
AS-Dnew · claude

Profile card grid

Every source is a project-style card in a 3-across grid — claude’s projects page applied to attackers.

-
-
AS-Eclaude × v3

Mono leaderboard

Ranked mono rows with inline share bars — v3 density with claude’s single calm column.

-
-
-

Reports studio page

- -
RP-Abase

Current — numbered tabs + stacked sections

Horizontal step tabs, all sections stacked, sticky action bar at the bottom.

-
- -
RP-Bnew · claude

Settings-rail — vertical steps left

claude.ai settings pattern: steps as a left rail, one section at a time on the right, actions pinned top-right. Calmer, more room per section.

-
- -
RP-Cnew

Wizard — one decision per screen

Centered single step with progress dots and a big next action (claude onboarding feel). Slowest but most guided; good for rarely-used flows.

-
-
RP-Dnew · claude

Document editor

The report definition reads as a claude-style document: serif section headings in one scrolling column, outline rail left, actions top-right.

-
-
RP-Eclaude × v3

Split live preview

Mono config summary left, an actual page preview right — every change repaints the thumbnail (v3 precision, claude calm).

-
-
- - -

Shell components

-

Top bar, navbar, settings surface and action pop-outs.

- -

11 · Top bar

-
11Abase

Stock

Bordered bar: sidebar toggle, search, centered page title, icon cluster + LIVE right.

-
☰🔍Overview🕐📈🔔🖥● LIVE
-
11Bnew · claude

Minimal breadcrumb

Transparent, blends into the page: breadcrumb left, LIVE + avatar right. Icons move into the sidebar/page.

-
Monitor / Overview● LIVEXO
-
11Cnew · claude

Command bar

Global search/jump front and center (⌘K energy); title lives in the page, icons right.

-
☰🔍 Search or jump to… ⌘K🔔● LIVEXO
-
11Dnew · claude

No top bar

claude.ai-style: no bar at all. LIVE + alerts live in the sidebar; each page carries its own serif title row with actions.

-
🏠 Overview🔔 Alerts 200
● LIVE · 29 sensors
Overview

Page owns its title + actions — nothing above it.

-
11Enew · claude

Floating pill bar

Detached rounded bar floating over the page: breadcrumb, LIVE, avatar — nothing else.

-
Monitor / Overview● LIVEXO
-
11Fclaude × v3

Mono status strip

One thin terminal strip: path, sensor count, live dot, UTC clock — maximum content height.

-
apiary/overview29 sensors● live13:37:02Z
-
- -

12 · Navbar (sidebar)

-
12Abase

Stock

Brand, MONITOR/INVESTIGATE/… labels, flat links, profile card bottom.

-
🐝 APIARY
Monitor
OverviewML anomalies
Investigate
Event explorerAttack sources
XO xore · admin
-
12Bnew · claude

Claude rail

Segmented mode toggle, one primary action ("Investigate…"), quiet pills, recents; profile bottom.

-
-
12Cnew

Icon rail + flyout

52px icon-only rail; group flyout on hover. Maximum content width all the time.

-
🏠⚡🌍📄⚙
Investigate ▸
Events · Sources · Campaigns · Clusters
-
12Dnew

Accordion groups + counts

Collapsible sections with live count chips (alerts, anomalies) — nav doubles as status.

-
-
12Enew · claude

Claude minimal rail

Borderless rail: serif brand, raised-pill active, recents — the quietest possible sidebar.

-
-
12Fclaude × v3

Mono tree

Terminal tree: lowercase mono labels, accent chevron marks the active node, group micro-caps.

-
apiary//ops
monitor
▸ overviewml-anomalies
investigate
eventssources
-
- -

13 · Dashboard settings surface

-
13Abase

Stock modal

Centered overlay dialog, left rail (Personal/Administration), content right.

-
PERSONAL
Appearance
Account
ADMIN
Users
Appearance

Theme, density, motion.

SystemDarkLight
-
13Bnew · claude

Full settings page

claude.ai pattern: /settings is a normal page in the shell — deep-linkable, no overlay, more room.

-
Settings
Color theme
SystemDarkLight
-
13Cnew

Right drawer

Slide-over from the right; the page stays visible behind — quick tweaks without losing context.

-
Settings
Appearance ▾
Theme · Density · Motion
Navigation ▸
Live data ▸
-
13Dnew · claude

Search-first palette

Type to jump to any setting (the modal already has search — make it the primary way in).

-
🎨 Appearance → Color theme
📄 Report Studio → PDF theme
🗺 Map → Basemap style
-
13Enew · claude

Airy settings page

Full page, serif title, floating borderless section cards, pill rail — claude.ai/settings verbatim.

-
Settings
Color theme
SystemDarkLight
-
13Fclaude × v3

Dense config grid

All knobs on one screen as mono key:value tiles — v3’s everything-at-once philosophy.

-
settings
theme: dark ▾
density: compact ▾
tz: UTC ▾
live: on ▾
clock: 24h ▾
motion: off ▾
-
- -

14 · Action buttons (pop-out)

-
14Abase

Stock ⋮ dropdown

Kebab opens a headed list (Analysis / Open in) with icons and external targets.

-
-
14Bnew · claude

Hover row actions

claude message pattern: icons fade in on row hover — copy, replay, profile, open-in. No menu for the common three.

-
00:28:23cowrie123.188.73.228
⧉▶👤⋮
-
14Cnew

Icon-grid sheet

Click opens a compact labeled grid — faster to scan than a list, still one target per tool.

-
-
14Dnew

Split button

Most-likely action visible (Investigate), caret for the rest — one click for the 90% case.

-
Investigate▾
  ▾ opens: replay · static analysis · Kibana · Arkime
-
14Enew · claude

Floating toolbar

claude’s hover toolbar: a raised pill of icons floats above the hovered row.

-
⧉▶👤📊⋮
floats above the hovered row
-
14Fclaude × v3

Inline mono verbs

Actions as [ bracketed ] mono verbs in the row itself — zero chrome, zero popups for the common three.

-
00:28:23cowrie123.188.73.228[copy] [replay] [profile] [+4]
-
- - - - diff --git a/docs/design-lab/playground/index.html b/docs/design-lab/playground/index.html deleted file mode 100644 index 07496a839..000000000 --- a/docs/design-lab/playground/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - -APIARY Design Lab - - - -

APIARY Design Lab

-

Live dashboard variants running against the real homeserver Elasticsearch (read-only). Pick pages, compare split-screen, choose winners.

- -

Variants

-
- -

Design Picker — elements + page layouts, click your choices →

-

Split compare (drag the divider)

-
- -

Notes

-
-

- Every variant is the real dashboard binary — full navigation works inside each. The only difference is - theme.css (Xore/theme). v1 keeps the stock look and only fixes layout (viewport-filling cards, - sticky table headers, no mid-value wraps). v2 and v3 are taste directions layered on top of the same fixes. - Winners get ported into Xore/theme properly (tokens + structured rules, light theme included) and PR'd. -

-
-Ports: base 19201 · v1 19202 · v2 19203 · v3 19204 · v4 19205 · v5 picks 19206 · v6 claude-pure 19207 — ES via SSH tunnel 19200 → supermicro. - - - - diff --git a/docs/design-lab/playground/layouts.html b/docs/design-lab/playground/layouts.html deleted file mode 100644 index d03a58dd5..000000000 --- a/docs/design-lab/playground/layouts.html +++ /dev/null @@ -1 +0,0 @@ -moved into the Design Picker diff --git a/docs/design-lab/v5-picks-override.css b/docs/design-lab/v5-picks-override.css deleted file mode 100644 index 67e72c0c6..000000000 --- a/docs/design-lab/v5-picks-override.css +++ /dev/null @@ -1,145 +0,0 @@ -/* ══════════════════════════════════════════════════════════════════════ - VARIANT v5 — "Your Picks" - claude.ai schema on the Xore palette, with the chosen elements: - 1D header-strip cards · 2D pulse skeletons (ghost shapes need templates) - 4C card-row tables · 5D solid status badges · 6D ghost buttons w/ - outline primary · 7D vertical-rail tabs · 9C "more" pill (JS) · - 10C accent-bar nav. Layout picks (OV-B/EV-B/AS-C/RP-C) are template - work, tracked separately. Includes v1 viewport-filling fixes. - ══════════════════════════════════════════════════════════════════════ */ - -/* ── claude base ── */ -:root { - --radius-panel: 16px; - --radius-control: 12px; - --radius-dialog: 24px; - --border-subtle: rgba(255, 255, 255, 0.045); - --border-strong: rgba(255, 255, 255, 0.10); -} -.app-sidebar { background: transparent; border-right: 1px solid var(--border-subtle); } -.app-toolbar { background: transparent; border-bottom: 1px solid var(--border-subtle); } -.app-main { padding: 26px 34px; } -.app-main > * { max-width: 1680px; margin-inline: auto; } -h1 { letter-spacing: -0.015em; } - -/* ── 10C nav: accent bar + soft gradient ── */ -.sidebar__item { border-left: 2px solid transparent; border-radius: 0 8px 8px 0; transition: background .15s ease-out; } -.sidebar__item:hover { background: var(--surface-1); } -.sidebar__item.active, -.sidebar__item[aria-current], -.sidebar__item[aria-current="page"] { - border-left-color: var(--accent); - background: linear-gradient(90deg, var(--accent-soft), transparent 70%); -} - -/* ── 1D card: title as header strip ── */ -.card > h2:first-child, -.card > h3:first-child { - margin: calc(-1 * var(--space-lg)) calc(-1 * var(--space-xl)) 14px; - padding: 11px var(--space-xl); - background: var(--surface-2); - border-bottom: 1px solid var(--border-subtle); - border-radius: var(--radius-panel) var(--radius-panel) 0 0; - font-size: 13px; - font-weight: 600; -} -.card { border-color: var(--border-subtle); } -.card:hover { border-color: var(--border-strong); } - -/* ── 4C table: card rows ── */ -.data-table { border-collapse: separate; border-spacing: 0 8px; } -.data-table thead th { - border-bottom: 0; - text-transform: none; - letter-spacing: 0; - font-size: 12.5px; - font-weight: 500; - color: var(--text-muted); - padding: 4px 12px; -} -.data-table td { background: var(--surface-2); border-bottom: 0; padding: 10px 12px; } -.data-table td:first-child { border-radius: 10px 0 0 10px; } -.data-table td:last-child { border-radius: 0 10px 10px 0; } -.data-table tbody tr:hover td { background: var(--surface-hover); } -.data-table tbody tr:last-child td { border-bottom: 0; } - -/* ── 5D badges: solid status fills (sensor b-* chips keep theme colors) ── */ -.badge--info { background: var(--info); color: #0f1b26; } -.badge--success { background: var(--success); color: #0d2417; } -.badge--warning { background: var(--warning); color: #2a2008; } -.badge--danger { background: var(--danger); color: #2b1312; } - -/* ── 6D buttons: ghost by default, outline primary ── */ -.btn, button.secondary { - background: transparent; - border-color: transparent; - border-radius: 10px; -} -.btn:hover, button.secondary:hover { background: var(--surface-2); } -.btn-primary, button.primary { - background: transparent; - color: var(--accent); - border: 1px solid var(--accent); -} -.btn-primary:hover, button.primary:hover { background: var(--accent-soft); } -.btn-danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); } -.btn-danger:hover { background: var(--danger-soft); } - -/* ── 7D tabs: vertical rail (float layout; real grid comes with templates) ── */ -.tabs { - flex-direction: column; - align-items: stretch; - width: 200px; - float: left; - position: sticky; - top: 16px; - margin: 0 22px 18px 0; - background: transparent; - border: 0; - padding: 0; - gap: 2px; -} -.tabs .tab { - justify-content: flex-start; - text-align: left; - border-left: 2px solid transparent; - border-radius: 0 8px 8px 0; -} -.tabs .tab.active { - border-left-color: var(--accent); - background: linear-gradient(90deg, var(--accent-soft), transparent 75%); - color: var(--text-primary); -} -[role="tabpanel"] { overflow: hidden; } - -/* ── 2D skeletons: pulse now; content-shaped ghosts need templates ── */ -@keyframes hp5-pulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } } -.skeleton-line { animation: hp5-pulse 1.6s ease-in-out infinite; } - -/* ── 9C scroll pill (paired JS in hp-app.js) ── */ -.hp5-more { - position: sticky; - bottom: 8px; - display: block; - width: fit-content; - margin: -26px auto 4px; - background: var(--surface-3); - border: 1px solid var(--border-strong); - border-radius: 999px; - font-size: 11px; - padding: 2px 12px; - color: var(--text-secondary); - pointer-events: none; - opacity: 0; - transition: opacity .2s ease-out; - z-index: 3; -} - -/* ── v1 viewport-filling fixes ── */ -.card__scroll { max-height: max(340px, calc(100vh - 320px)); overscroll-behavior: contain; } -.card__scroll .data-table thead th { - position: sticky; top: 0; z-index: 2; - background: var(--surface-1); -} -td.v, .data-table td[data-label="time"], .data-table td[data-label="port"] { white-space: nowrap; } -td.v .eventdetail, td.v article { white-space: normal; } diff --git a/scripts/check-docs-reachable.py b/scripts/check-docs-reachable.py index 584ca762b..e028ba06e 100755 --- a/scripts/check-docs-reachable.py +++ b/scripts/check-docs-reachable.py @@ -17,12 +17,17 @@ docs/research/ per-CVE / per-topic research notes docs/benchmarks/ dated benchmark plans, runs, claim pools docs/sandbox/windows/vm-detection-results/ per-run VM-detection captures - docs/design-lab/ duplicate of branding/design-lab (#3310) docs/archive/ retired docs kept for history Anything else unreachable fails: link it from the map (or from a README the map reaches), or move it into a record tree if that is what it is. +The design lab is not listed here because it is no longer a docs/ tree at all: +#3310 removed the duplicate copy this exemption used to name, leaving +branding/design-lab/ as the single home. A record-tree exemption for a path +with no tracked file under it is an exemption that exempts nothing, and it is +one more place a re-added duplicate could hide. + Usage: python scripts/check-docs-reachable.py """ from __future__ import annotations @@ -38,7 +43,6 @@ "docs/research/", "docs/benchmarks/", "docs/sandbox/windows/vm-detection-results/", - "docs/design-lab/", "docs/archive/", ) # Relative link targets ending in .md, anchors allowed and ignored. diff --git a/tests/docs/test_3310_design_lab_dedupe.py b/tests/docs/test_3310_design_lab_dedupe.py new file mode 100644 index 000000000..dc61be010 --- /dev/null +++ b/tests/docs/test_3310_design_lab_dedupe.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +"""Regression test for #3310: the design lab was tracked twice. + +`main` carried two trees of the same 2026-08-17 design lab: + +* `branding/design-lab/` -- 12 files. #1827 (f9575d6e) preserved it, #1935 + (450c32af) added `lab.mjs` / `lab.test.mjs`. This is the canonical tree: + `branding/` is the visual spec, `pages.yml` builds and publishes it, and + `quality.yml` runs `lab.test.mjs` out of it in both the self-hosted and the + GitHub-hosted lane. +* `docs/design-lab/` -- 10 files, added incidentally by the #3182 a11y PR + (526eaf5c). It was the #1763 snapshot and had drifted: neither `lab.mjs` nor + `lab.test.mjs`, and the weaker of the two `safePath` guards in + `playground/compare.html` (both copies added one; the canonical one + whitelists the path character by character and keeps `|| '/'` at every + call site, the other resolves against an origin and drops the fallback). + +It was *not* a pure duplicate, which is the part worth pinning. The `docs/` +copy had been edited since: the 2026-09-27 markdown reconciliation (b2b230cd) +gave it the redaction notice and the `design-notes.md` staleness banner, and +it carried a redaction of one captured attacker IP (`85.14.245.122` to RFC 5737 +`203.0.113.122`) that `branding/` never received. That content is folded into +the canonical tree here rather than dropped -- see +`test_the_redaction_that_existed_only_in_the_duplicate_survived`. + +What this file prevents is the recurrence, not the original. Three of the +tests below are deliberately path-agnostic. `SENTINELS` are basenames that +exist nowhere in the repository except the design lab, so a second copy is +caught wherever it is planted -- `docs/design-lab/` again, `branding/docs/`, a +fresh `lab/` -- rather than only at the path this issue names. A gate that +asserted nothing about `docs/design-lab` would go green the moment the fork +came back one directory over, which is the failure mode #3182 was. + +Deliberately NOT asserted: that anything under the lab holds a particular +address, or that the lab is byte-stable. It is a preserved record, the +redaction is documented in the files themselves, and the two trees never were +the same bytes. + +Runs under `python -m pytest tests/docs/` (quality.yml), which installs only +pytest -- so the CodeQL assertion reads YAML as text, the same trade +`tests/docs/test_3331_fix.py` makes for a workflow. +""" +from __future__ import annotations + +import pathlib +import subprocess + +import pytest + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +SELF = pathlib.Path(__file__).resolve().relative_to(REPO_ROOT).as_posix() + +CANONICAL = "branding/design-lab" +DUPLICATE = "docs/design-lab" + +# The ten files the duplicate carried, relative to its own root. Written out +# rather than read from git history, so the record of what was dropped is part +# of the gate and a future reader can see it without a `git show`. +DROPPED_TREE_FILES = ( + "README.md", + "contrast_scan.js", + "design-notes.md", + "gen_palettes.py", + "palettes.css", + "playground/compare.html", + "playground/elements.html", + "playground/index.html", + "playground/layouts.html", + "v5-picks-override.css", +) + +# The two files that made branding/design-lab the canonical tree and are why +# the duplicate was the one to go: CI runs the second of them, and the first +# is the harness README documents. +CANONICAL_ONLY_FILES = ("lab.mjs", "lab.test.mjs") + +# Basenames that exist in exactly one place in the repository: the design lab. +# Any second tree holding one of them is a second copy of the lab, whatever it +# is called or wherever it is mounted -- which is why these are basenames and +# not a path. All five sit at the root of the tree that holds them, so one +# level of dirname is the tree root. +SENTINELS = frozenset({ + "contrast_scan.js", + "design-notes.md", + "gen_palettes.py", + "palettes.css", + "v5-picks-override.css", +}) + + +def _git(*args: str) -> str: + result = subprocess.run( + ["git", "-C", str(REPO_ROOT), *args], capture_output=True, text=True, + ) + if result.returncode not in (0, 1): + raise AssertionError( + f"git {' '.join(args)} failed ({result.returncode}): {result.stderr.strip()}" + ) + return result.stdout + + +def _tracked(*pathspecs: str) -> list[str]: + """Tracked paths, optionally narrowed. `-z` so nothing splits on a space.""" + args = ["ls-files", "-z", *pathspecs] + return [path for path in _git(*args).split("\0") if path] + + +def _text(rel: str) -> str: + return (REPO_ROOT / rel).read_text(encoding="utf-8", errors="replace") + + +def test_the_duplicate_tree_is_gone(): + """The fork itself: no tracked file, and nothing left on disk. + + Both halves on purpose. The tracked check is what a reviewer sees in the + diff; the exists() check is what a stale symlink or an untracked leftover + would pass, and an untracked leftover is exactly how a "deleted" tree + comes back for the next person who commits `docs/ -rf`. + """ + tracked = _tracked(DUPLICATE) + assert not tracked, ( + f"{DUPLICATE}/ is tracked again ({len(tracked)} files: {tracked[:5]}). The " + f"design lab has one home, {CANONICAL}/; a second tree is a dead fork " + "someone will edit instead of the real one (#3310)." + ) + assert not (REPO_ROOT / DUPLICATE).exists(), ( + f"{DUPLICATE}/ exists on disk. If it is untracked it will be picked up " + "by the next `git add`, which is how this came back the first time (#3310)." + ) + + +def test_nothing_tracked_references_the_removed_path(): + """Zero remaining references. A deleted tree with live links is worse. + + `git grep` rather than a Python walk: it is binary-safe, so the scan does + not have to decide what a `.pcap` is, and it reads the same tracked set the + gate is about. This file is skipped because it necessarily spells the + path in order to forbid it. + """ + hits = [ + path for path in _git("grep", "-l", "-I", "-z", "--fixed-strings", DUPLICATE).split("\0") + if path and path != SELF + ] + assert not hits, ( + f"{len(hits)} tracked file(s) still reference {DUPLICATE}: {hits}. Repoint " + f"them at {CANONICAL}/ in the same commit that removes the tree (#3310)." + ) + + +def test_the_canonical_tree_still_holds_every_file_the_duplicate_had(): + """No content lost. Ten filenames, and the two only the canonical tree has. + + A deletion that quietly drops a file is indistinguishable from a dedupe + until somebody needs the file, so the shape of the dropped tree is asserted + rather than described in a commit message nobody reads back. + """ + present = set(_tracked(f"{CANONICAL}/*")) + missing = [name for name in DROPPED_TREE_FILES if f"{CANONICAL}/{name}" not in present] + assert not missing, ( + f"{CANONICAL}/ is missing {missing}, which {DUPLICATE}/ carried. The " + "duplicate is being removed, not its content (#3310)." + ) + # And the reason the other tree was the one to delete, asserted so the + # direction of the dedupe cannot be quietly reversed. + for name in CANONICAL_ONLY_FILES: + assert f"{CANONICAL}/{name}" in present, ( + f"{CANONICAL}/{name} is gone. It never existed in {DUPLICATE}/, so a " + "dedupe cannot account for its absence -- see #1828/#1935 (#3310)." + ) + + +def test_the_redaction_that_existed_only_in_the_duplicate_survived(): + """The one way the two trees were not interchangeable, folded and pinned. + + `docs/design-lab/` had been maintained after the fact: the redaction + notice, the review-snapshot banner, the one attacker IP replaced, and the + colour-research section the canonical README had never received. Those are + edits, not duplication, so the fold had to carry them. This test is the + evidence that it did -- it is the assertion a plain `git rm -r` would + have failed. + """ + notes = _text(f"{CANONICAL}/design-notes.md") + readme = _text(f"{CANONICAL}/README.md") + + assert "Public, redacted copy" in notes, ( + f"{CANONICAL}/design-notes.md lost the public-copy banner the duplicate " + "carried. It is what stops a later reader restoring the real addresses " + "the redaction removed (#3310)." + ) + assert "#1628" in notes, ( + f"{CANONICAL}/design-notes.md lost the note that the Go dashboard these " + "findings cite was deleted in #1628, so the findings are re-locatable " + "before they are acted on (#3310)." + ) + assert "Colour research" in readme, ( + f"{CANONICAL}/README.md lost the colour-research section only the " + "duplicate carried. It is the account of where the palettes came from " + "that `gen_palettes.py` is kept for (#3310)." + ) + assert "documentation-range placeholders" in readme, ( + f"{CANONICAL}/README.md lost the redaction notice the duplicate " + "carried, so a reader no longer knows the addresses here are " + "deliberately placeholders (#3310)." + ) + + # The redaction itself, in the two files the duplicate redacted. Asserted + # as an absence because that is the shape of the fix: the address is gone + # from the tree that is now the published one, not merely moved. + for rel in (f"{CANONICAL}/design-notes.md", f"{CANONICAL}/playground/elements.html"): + assert "85.14.245." not in _text(rel), ( + f"{rel} carries the unredacted captured address the duplicate had " + f"already replaced with 203.0.113.x. branding/ is what pages.yml " + "publishes; the redaction has to live there (#3310)." + ) + assert "203.0.113.122" in _text(f"{CANONICAL}/playground/elements.html"), ( + f"{CANONICAL}/playground/elements.html has neither the real address nor " + "the placeholder the duplicate substituted. Check the redaction was " + "applied here rather than the file reverted wholesale (#3310)." + ) + + +def test_codeql_path_ignore_names_the_tree_that_exists(): + """The exclusion moved with the tree; nothing new was excluded. + + `paths-ignore` carried `docs/design-lab` for a reason that is a property of + the lab, not of the directory: `playground/compare.html` is a local-only + harness whose CodeQL js/html findings are documentation noise. That + reasoning is unchanged and the files are still there, so dropping the entry + would break the CodeQL job and keeping the old path would exempt nothing + while silently blessing a re-added duplicate. + """ + config = _text(".github/codeql/codeql-config.yml") + ignores = _codeql_path_ignores(config) + assert CANONICAL in ignores, ( + f"{CANONICAL} is missing from .github/codeql/codeql-config.yml's " + f"paths-ignore ({ignores}). The lab's local-only playground is what the " + "entry is for, and it lives in the canonical tree now (#3310)." + ) + assert DUPLICATE not in ignores, ( + f"{DUPLICATE} is still in paths-ignore. The path holds no tracked file, " + "so the entry exempts nothing -- and if a duplicate is added back under " + "it, CodeQL will skip the fork nobody is reading (#3310)." + ) + + +def test_exactly_one_design_lab_tree_is_tracked(): + """The recurrence gate, and the one that does not name a path. + + This is the assertion that outlasts #3310. It is checked over sentinel + basenames rather than over `docs/design-lab`, so it fails for a copy at + any location -- `docs/design-lab/`, `branding/docs/`, `lab/`, a branch + merge that brings the old tree back under a new name. + """ + roots = { + str(pathlib.PurePosixPath(path).parent) + for path in _tracked() + if pathlib.PurePosixPath(path).name in SENTINELS + } + assert roots == {CANONICAL}, ( + f"design-lab sentinels ({', '.join(sorted(SENTINELS))}) are tracked under " + f"{sorted(roots)}, not only under {CANONICAL}. The lab has one home; a " + "second tree is a dead fork (#3310)." + ) + + +def _codeql_path_ignores(config: str) -> list[str]: + """The `paths-ignore:` list items, read as text. + + quality.yml's tests/docs row installs only pytest, so this is a line + reader rather than a YAML parse -- the same trade test_3331_fix.py makes + for a workflow. Items are ` - path` lines under the key; blank lines and + `#` comments between them are skipped, and the first line that is neither + ends the block. + """ + lines = config.splitlines() + start = next( + (i for i, line in enumerate(lines) if line.rstrip() == "paths-ignore:"), None + ) + assert start is not None, ( + ".github/codeql/codeql-config.yml has no `paths-ignore:` key; this gate's " + "excerptor is stale and would pass vacuously (#3310)" + ) + ignores = [] + for line in lines[start + 1:]: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if not line.startswith(" - "): + break + # `stripped` is already at the dash, so the item is everything after + # `- `. Slicing the unstripped line instead would eat three characters + # of the first path component. + ignores.append(stripped.removeprefix("- ").strip()) + assert ignores, ( + ".github/codeql/codeql-config.yml's paths-ignore parsed as empty; the " + "excerptor is stale and the assertions above would pass vacuously (#3310)" + ) + return ignores + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"]))