Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
3dd9d47
docs(dashboard): survey shadcn migration routes
Xore Sep 16, 2026
ea431e6
feat(dashboard): establish shadcn Tailwind foundation
Xore Sep 16, 2026
c1ac3a8
feat(dashboard): map legacy palettes to shadcn tokens
Xore Sep 16, 2026
8ff97d8
feat(dashboard): migrate shell and overview to shadcn
Xore Sep 16, 2026
f87ff4e
feat(dashboard): shadcn reference slice - settings and IP investigation
Xore Sep 17, 2026
96ca00c
feat(dashboard): shadcn toolbar on alerts (refresh, acknowledge-all, …
Xore Sep 17, 2026
43089ef
feat(dashboard): rebuild alerts page on shadcn primitives
Xore Sep 17, 2026
341c669
feat(dashboard): migrate event detail cards and tables to shadcn
Xore Sep 17, 2026
e79cfa6
feat(dashboard): migrate event failure and missing states to shadcn
Xore Sep 17, 2026
79af399
refactor(dashboard): migrate shared investigate header layout
Xore Sep 17, 2026
1908db5
fix(dashboard): restore event-route typography, sensor badge classes …
Xore Sep 17, 2026
3808050
feat(dashboard): migrate settings, investigate lookup/cluster and sea…
Xore Sep 18, 2026
7ea1a26
feat(dashboard): migrate sensor, session and payload routes to shadcn
Xore Sep 18, 2026
f52ba51
feat(dashboard): migrate investigate CIDR detail route to shadcn
Xore Sep 18, 2026
3a55dab
chore(dashboard): untrack vendored .claude dir and ignore it
Xore Sep 18, 2026
b0b9b5a
fix(dashboard): complete 3218 visual-acceptance fixes for event fixtu…
Xore Sep 18, 2026
79e5de4
fix(dashboard): migrate CIDR sensors panel to shadcn card
Xore Sep 18, 2026
a04a0e5
fix(dashboard): migrate shared MasterDetailTable chrome to shadcn
Xore Sep 18, 2026
e7e9030
fix(dashboard): align c2 route test mock with shadcn chrome
Xore Sep 18, 2026
31e2f43
feat(dashboard): migrate credentials, dead-letters, commands, history…
Xore Sep 18, 2026
7df5d6b
feat(dashboard): migrate agent-campaigns, ml-anomalies and attackers …
Xore Sep 19, 2026
b8e8486
feat(dashboard): migrate events, payloads and canarytokens to shadcn
Xore Sep 19, 2026
aa5876e
feat(dashboard): migrate llm-analysis, auth-events, problem-reports a…
Xore Sep 19, 2026
4afbc6d
feat(dashboard): register echarts theme from shadcn CSS tokens
Xore Sep 19, 2026
8bdf09a
feat(dashboard): register xterm theme from shadcn CSS tokens
Xore Sep 19, 2026
972401d
feat(dashboard): add default shadcn palette, add alerts 390 card brea…
Xore Sep 19, 2026
62eaad5
feat(dashboard): register cytoscape theme from shadcn CSS tokens
Xore Sep 19, 2026
be87fb8
feat(dashboard): register leaflet theme from shadcn CSS tokens
Xore Sep 19, 2026
3272ebd
feat(dashboard): register novnc theme from shadcn CSS tokens
Xore Sep 19, 2026
7c82e5b
fix(tests): update stale .hp-rp-template selector to .hp-rp-templates
Xore Sep 19, 2026
fe0adca
feat(dashboard): migrate 3 legacy card components to shadcn Card
Xore Sep 19, 2026
b394dff
feat(dashboard): align sidebar with shadcn template patterns — add Si…
Xore Sep 19, 2026
8af2a77
feat(dashboard): align topbar with shadcn Breadcrumb
Xore Sep 19, 2026
4efe6a9
feat(dashboard): align AppShell with shadcn collapsible variants
Xore Sep 19, 2026
cd92a06
fix(dashboard): sweep legacy <button> elements to shadcn Button
Xore Sep 19, 2026
c9d7819
fix(dashboard): close Playwright sweep layout gaps
Xore Sep 19, 2026
6a9276a
test(dashboard): sweep migrated routes across palettes
Xore Sep 19, 2026
5942573
cleanup: remove Codex-generated scratch files from branch
Xore Sep 19, 2026
d9963b1
cleanup: remove graphify-out generated cache and snapshots, add to gi…
Xore Sep 19, 2026
005cca0
cleanup: remove dash-shots and design-lanes generated artifacts, add …
Xore Sep 19, 2026
80dc09e
feat(dashboard): replace raw HTML tables with shadcn Table component
Xore Sep 19, 2026
bef5e4d
feat(dashboard): remove legacy card/half/hp-field/card__meta patterns
Xore Sep 19, 2026
9d7e61e
feat(dashboard): replace hard <a href> navs with TanStack Router <Link>
Xore Sep 19, 2026
26e84c7
feat(dashboard): replace custom modals/dialogs with shadcn AlertDialo…
Xore Sep 19, 2026
0bce26a
feat(dashboard): replace legacy badge spans and custom tabs with shad…
Xore Sep 19, 2026
038285f
feat(dashboard): replace metric-grid, custom Tabs, dashboard-panel wi…
Xore Sep 19, 2026
819efbb
feat(dashboard): replace bare input/select elements with shadcn Input…
Xore Sep 19, 2026
f1da19a
feat(dashboard): replace bare textarea elements with shadcn Textarea
Xore Sep 19, 2026
c274ede
feat(dashboard): retire theme.css xore/theme token handoff in favor o…
Xore Sep 19, 2026
6820847
feat(dashboard): replace bare label elements with shadcn Label
Xore Sep 19, 2026
7b32dbd
feat(dashboard): replace btn class patterns with shadcn Button asChild
Xore Sep 19, 2026
2efde88
feat(dashboard): replace remaining card wide/half/scroll/label patter…
Xore Sep 19, 2026
c192b0d
feat(dashboard): replace note class, __desc, label-section patterns w…
Xore Sep 19, 2026
c820db2
refactor(hp-toolbar-actions): replace with shadcn Menubar
Xore Sep 19, 2026
0c40eed
refactor(hp-row-actions): replace with shadcn DropdownMenu
Xore Sep 19, 2026
bbc736e
feat(dashboard): replace remaining internal <a> links with TanStack R…
Xore Sep 19, 2026
1260084
fix: revert query-param Link to <a> in payload-analysis (typecheck)
Xore Sep 19, 2026
42408b3
Revert "fix: revert query-param Link to <a> in payload-analysis (type…
Xore Sep 19, 2026
fe1aa34
Reapply "fix: revert query-param Link to <a> in payload-analysis (typ…
Xore Sep 19, 2026
14bfd08
refactor(btn-classes): replace remaining btn/btn-primary/btn-secondar…
Xore Sep 19, 2026
ca41b94
refactor(hp-toast-stack): replace with shadcn Toast
Xore Sep 19, 2026
32a9f9a
feat(dashboard): replace template literal <a href> links with TanStac…
Xore Sep 19, 2026
8bc5702
refactor(skeleton-line): replace with shadcn Skeleton
Xore Sep 19, 2026
f8d2268
feat(dashboard): replace remaining internal route links with Link
Xore Sep 20, 2026
329be6b
feat(dashboard): replace remaining note patterns with Label/CardDescr…
Xore Sep 20, 2026
7e1f21c
feat(dashboard): replace remaining subtitle patterns with Label/CardD…
Xore Sep 20, 2026
9f68f87
refactor(hp-prefix): replace remaining hp-prefixed div classes with s…
Xore Sep 20, 2026
f1afae5
refactor(empty-state): replace empty-state patterns with shadcn Card
Xore Sep 20, 2026
62cec0f
refactor(hp-field): replace remaining hp-field patterns with shadcn
Xore Sep 20, 2026
33544bd
refactor(project-card): replace project-card patterns with shadcn Card
Xore Sep 20, 2026
2d66db2
refactor(hp-flow): replace hp-flow patterns with Tailwind space-y
Xore Sep 20, 2026
7a62a08
refactor(chip): replace chip patterns with shadcn Badge
Xore Sep 20, 2026
3446f19
fix: use shadcn Dialog import in alerts.tsx
Xore Sep 20, 2026
792b4a8
fix(ci): resolve CI failures on shadcn migration branch
Xore Sep 20, 2026
0566da7
fix(ci): finish shadcn browser matrix fixes
Xore Sep 20, 2026
e2dd58f
chore: remove test files from src/ (tests live in e2e/)
Xore Sep 20, 2026
5c47973
chore: remove graphify-out and test files from src/
Xore Sep 20, 2026
1d27768
fix(ci): resolve CI failures on shadcn migration branch
Xore Sep 20, 2026
79d6b5e
fix(ci): resolve remaining CI failures on shadcn migration branch
Xore Sep 20, 2026
b61b943
feat(layout): phase 1 — topbar nav row uses shadcn Menubar
Xore Sep 20, 2026
23ec05d
fix: apply CSP nonce to TanStack Start bootstrap inline scripts
Xore Sep 20, 2026
48e11e6
feat(theme): scope shadcn CSS variables per palette
Xore Sep 20, 2026
946f93d
feat(layout): phase 3 — sidebar uses pure shadcn primitives
Xore Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ node_modules/
.nitro/
.tanstack/
dist/
Dash-shots/
Design-lanes/
.env
target/

Expand Down Expand Up @@ -33,3 +35,5 @@ analysis/ghidra/revdeck/

.grit
.orchestrator/

arcane/home/honeypot-dashboard/frontend-next/.claude/
19 changes: 19 additions & 0 deletions arcane/home/honeypot-dashboard/frontend-next/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { expect, test, type Page } from '@playwright/test'
import { join } from 'node:path'

const cases = [
{ palette: 'claude', mode: 'light', width: 1280, height: 800 },
{ palette: 'claude', mode: 'dark', width: 390, height: 844 },
{ palette: 'ocean', mode: 'light', width: 390, height: 844 },
{ palette: 'ocean', mode: 'dark', width: 1280, height: 800 },
] as const

async function openGraph(page: Page, palette: string, mode: string) {
await page.addInitScript(
({ palette, mode }) => {
localStorage.setItem('hp-palette', palette)
localStorage.setItem('hp-theme', mode)
},
{ palette, mode },
)
await page.goto('/attackers')
await page.locator('table tbody tr').first().click()
const graph = page.getByRole('img', { name: 'Attacker entity graph around one entity node' })
await expect(graph).toBeVisible()
await expect.poll(() => graph.evaluate(host => Boolean((host as HTMLElement & { __xoreCytoscape?: unknown }).__xoreCytoscape))).toBe(true)
}

async function graphTheme(page: Page) {
return page.getByRole('img', { name: 'Attacker entity graph around one entity node' }).evaluate((host) => {
const cy = (host as HTMLElement & { __xoreCytoscape?: import('cytoscape').Core }).__xoreCytoscape
if (!cy) throw new Error('cytoscape seam missing')

const probe = document.createElement('span')
const canvas = document.createElement('canvas')
canvas.width = canvas.height = 1
const context = canvas.getContext('2d')!
document.documentElement.appendChild(probe)
const color = (value: string) => {
probe.style.color = value
context.clearRect(0, 0, 1, 1)
context.fillStyle = getComputedStyle(probe).color
context.fillRect(0, 0, 1, 1)
const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data
return alpha === 255 ? `rgb(${red}, ${green}, ${blue})` : `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`
}
const channels = (value: string) => color(value).match(/[\d.]+/g)?.map(Number)
const token = (name: string) => color(`var(${name})`)
const rules = Object.fromEntries((cy.json().style as Array<{ selector: string; style: Record<string, string> }>).map(rule => [rule.selector, rule.style]))
const ruleColor = (selector: string, property: string) => color(String(rules[selector][property]))
const hub = cy.nodes('[kind = "hub"]').first()
const spoke = cy.nodes('[kind = "spoke"]').first()
const edge = cy.edges().first()

spoke.emit('mouseover')
edge.emit('mouseover')
const interaction = {
nodeHover: color(spoke.style('background-color')),
edgeHover: color(edge.style('line-color')),
nodeClass: spoke.hasClass('hover'),
edgeClass: edge.hasClass('hover'),
}
spoke.emit('mouseout')
edge.emit('mouseout')
hub.select()

const actual = {
nodeBackground: color(spoke.style('background-color')),
nodeBorder: color(spoke.style('border-color')),
nodeText: color(spoke.style('color')),
hubBackground: color(hub.style('background-color')),
hubBorder: color(hub.style('border-color')),
hubText: color(hub.style('color')),
edge: (edge as unknown as { pstyle(name: string): { value: number[] } }).pstyle('line-color').value,
selected: color(hub.style('overlay-color')),
overflow: ruleColor('node[kind = "overflow"]', 'border-color'),
}
const expected = {
nodeBackground: token('--card'),
nodeBorder: token('--chart-1'),
nodeText: token('--muted-foreground'),
hubBackground: token('--chart-1'),
hubBorder: token('--chart-2'),
hubText: token('--foreground'),
edge: channels('var(--border)'),
selected: token('--chart-4'),
overflow: token('--chart-5'),
}
const hoverColor = token('--chart-3')
probe.remove()
return {
actual,
expected,
interaction,
hoverColor,
counts: { nodes: cy.nodes().length, edges: cy.edges().length },
}
})
}

for (const entry of cases) {
test(`${entry.palette}/${entry.mode}/${entry.width} uses the exact shadcn cytoscape theme`, async ({ page }) => {
await page.setViewportSize({ width: entry.width, height: entry.height })
await openGraph(page, entry.palette, entry.mode)

const theme = await graphTheme(page)
expect(theme.actual).toEqual(theme.expected)
expect(theme.counts).toEqual({ nodes: 2, edges: 1 })
expect(theme.interaction).toEqual({
nodeHover: theme.hoverColor,
edgeHover: theme.hoverColor,
nodeClass: true,
edgeClass: true,
})

const nextMode = entry.mode === 'light' ? 'dark' : 'light'
await page.evaluate((mode) => {
localStorage.setItem('hp-theme', mode)
window.dispatchEvent(new StorageEvent('storage', { key: 'hp-theme', newValue: mode }))
}, nextMode)
await expect(page.locator('html')).toHaveAttribute('data-theme', nextMode)
const updatedTheme = await graphTheme(page)
expect(updatedTheme.actual).toEqual(updatedTheme.expected)
expect(updatedTheme.counts).toEqual(theme.counts)

if (process.env.EVIDENCE_DIR) {
await page.screenshot({
path: join(process.env.EVIDENCE_DIR, `cytoscape-${entry.palette}-${entry.mode}-${entry.width}.png`),
fullPage: true,
})
}

await page.getByRole('img', { name: 'Attacker entity graph around one entity node' }).evaluate((host) => {
const cy = (host as HTMLElement & { __xoreCytoscape?: import('cytoscape').Core }).__xoreCytoscape
cy?.nodes('[kind = "spoke"]').emit('tap')
})
await expect(page).toHaveURL(/\/events\?ip=203\.0\.113\.7/)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SESSION_COOKIE_NAME, fixtureSid } from "./fixture-session";
// The route set is the sidebar's own registry: a new nav entry joins this
// sweep without touching the spec, and a removed one drops out of it.
const NAV_ROUTES = NAV_SECTIONS.flatMap((section) => section.items.map((item) => item.to));
const PALETTE = process.env.E2E_PALETTE === "ocean" ? "ocean" : "claude";

// Failure mode the matrix exists to catch (issue #60's words): a page
// renders something broken and nothing fails. These are the two cheap
Expand Down Expand Up @@ -62,13 +63,13 @@ test.describe("route smoke across theme x viewport", () => {
test(`${route} renders its shell`, async ({ browser }) => {
const context = await browser.newContext({ viewport });
await context.addInitScript(
(t) => {
({ theme, palette }) => {
try {
localStorage.setItem("hp-theme", t);
localStorage.setItem("hp-palette", "claude");
localStorage.setItem("hp-theme", theme);
localStorage.setItem("hp-palette", palette);
} catch {}
},
theme,
{ theme, palette: PALETTE },
);
const page = await context.newPage();

Expand Down Expand Up @@ -105,7 +106,7 @@ test.describe("reports studio content (#2507)", () => {
await page.goto("/reports");
// Design step: the wizard's template gallery lists the fixture catalog
// instead of "No report templates are available.".
await expect(page.locator(".hp-rp-template", { hasText: "Executive report" })).toBeVisible();
await expect(page.getByRole("group", { name: "Report template" }).getByText("Executive report")).toBeVisible();
await expect(page.getByText("No report templates are available.")).toHaveCount(0);

// Library step: saved definition with its schedule, plus the generated
Expand All @@ -122,7 +123,7 @@ test.describe("modal core", () => {
test("command palette opens, filters, and Escape closes", async ({ page }) => {
await page.goto("/");
await page.click('button[aria-label="Search and investigate"]');
const dialog = page.locator('[role="dialog"][aria-label="Investigate an indicator"]');
const dialog = page.getByRole('dialog', { name: 'Investigate an indicator' });
await expect(dialog).toBeVisible();
// The palette's filter field is a textarea (multi-line query support).
await dialog.locator("textarea").fill("203.0.113.7");
Expand Down Expand Up @@ -185,7 +186,7 @@ test.describe("role-aware action visibility", () => {
// and exposes no view-state API a test could read instead.
test.describe("topology sankey roam (#2130)", () => {
const SETTLE = 1_500; // layout animation + the labelLayout reflow pass
const zoomPct = (page: Page) => page.locator(".chip[aria-live]").innerText();
const zoomPct = (page: Page) => page.getByText(/^[0-9]+%$/).innerText();
const canvasShot = (page: Page) => page.locator("canvas").first().screenshot();

// zrender marks elements it considers draggable by putting move/grab on
Expand Down Expand Up @@ -292,7 +293,7 @@ test.describe("topology sankey roam (#2130)", () => {
await page.goto("/kill-chain");
await page.getByRole("heading", { level: 1 }).waitFor();
await page.waitForTimeout(SETTLE);
expect(await page.locator(".chip[aria-live]").count()).toBe(0);
expect(await page.getByText(/^[0-9]+%$/).count()).toBe(0);
expect(await page.getByText("scroll to zoom").count()).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect, test, type Page } from '@playwright/test'

const palettes = ['claude', 'ocean', 'neon'] as const
const modes = ['light', 'dark'] as const
const viewports = [
{ name: 'desktop', width: 1280, height: 800 },
{ name: 'mobile', width: 390, height: 844 },
] as const

async function openChart(page: Page, palette: string, mode: string) {
await page.addInitScript(
({ palette, mode }) => {
localStorage.setItem('hp-palette', palette)
localStorage.setItem('hp-theme', mode)
},
{ palette, mode },
)
await page.goto('/topology')
await expect(page.locator('html')).toHaveAttribute('data-hp-theme', palette)
await expect(page.locator('html')).toHaveAttribute('data-theme', mode)
await expect(page.locator('canvas').first()).toBeVisible({ timeout: 20_000 })
await expect(page.getByText('scroll to zoom')).toBeVisible()
}

async function chartColors(page: Page) {
return page.evaluate(() => {
const host = document.querySelector('[_echarts_instance_]') as
| (HTMLElement & { __xoreChart?: { getOption: () => { color?: string[] } } })
| null
if (!host?.__xoreChart) throw new Error('chart seam missing')
return host.__xoreChart.getOption().color
})
}

async function shadcnChartColors(page: Page) {
return page.evaluate(() => {
const probe = document.createElement('span')
document.documentElement.appendChild(probe)
const colors = [1, 2, 3, 4, 5].map((index) => {
probe.style.color = `var(--chart-${index})`
return getComputedStyle(probe).color
})
probe.remove()
return colors
})
}

for (const viewport of viewports) {
for (const mode of modes) {
for (const palette of palettes) {
test(`${palette}/${mode}/${viewport.name} renders from shadcn chart tokens`, async ({ page }) => {
await page.setViewportSize(viewport)
await openChart(page, palette, mode)
const colors = await chartColors(page)
expect(colors).toEqual(await shadcnChartColors(page))
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
if (process.env.ECHART_EVIDENCE_DIR) {
await page.waitForTimeout(1_500)
await page.screenshot({ path: `${process.env.ECHART_EVIDENCE_DIR}/${palette}-${mode}-${viewport.name}.png`, fullPage: true })
}
})
}
}
}

test('mode switch re-registers the theme and preserves resize and zoom', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 })
await openChart(page, 'claude', 'dark')
const before = await chartColors(page)

await page.evaluate(() => {
localStorage.setItem('hp-theme', 'light')
window.dispatchEvent(new StorageEvent('storage', { key: 'hp-theme', newValue: 'light' }))
})
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
await expect.poll(() => chartColors(page)).not.toEqual(before)

const initialWidth = await page.locator('canvas').first().evaluate((canvas) => (canvas as HTMLCanvasElement).width)
await page.setViewportSize({ width: 390, height: 844 })
await expect.poll(() => page.locator('canvas').first().evaluate((canvas) => (canvas as HTMLCanvasElement).width)).not.toBe(initialWidth)

await page.getByRole('button', { name: 'Zoom in' }).click()
await expect(page.getByText('120%', { exact: true })).toBeVisible()
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from '@playwright/test'
import { join } from 'node:path'
const palette = process.env.E2E_PALETTE === 'ocean' ? 'ocean' : 'claude'

for (const width of [1280, 390]) for (const mode of ['light', 'dark']) {
test(`event detail ${width} ${mode} loads without console errors`, async ({ page }, testInfo) => {
const errors: string[] = []
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()) })
page.on('pageerror', error => errors.push(error.message))
await page.setViewportSize({ width, height: 844 })
await page.addInitScript(({ mode, palette }) => {
localStorage.setItem('hp-palette', palette)
localStorage.setItem('hp-theme', mode)
}, { mode, palette })
await page.goto('/event/e2e-event-0')
await expect(page.getByRole('heading', { name: 'citrix-honeypot event' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'The complete record' })).toBeVisible()
await expect(page.getByText('cve_2019_19781_payload', { exact: true })).toBeVisible()
const shot = process.env.EVIDENCE_DIR
? join(process.env.EVIDENCE_DIR, 'shots', `event-${width}-${mode}.png`)
: testInfo.outputPath(`event-${width}-${mode}.png`)
await page.screenshot({ path: shot, fullPage: true })
expect(errors).toEqual([])
})
}

test('missing event renders the not-found state', async ({ page }, testInfo) => {
await page.goto('/event/missing-e2e-event')
await expect(page.getByText('Event not found')).toBeVisible()
const shot = process.env.EVIDENCE_DIR
? join(process.env.EVIDENCE_DIR, 'shots', 'event-missing.png')
: testInfo.outputPath('event-missing.png')
await page.screenshot({ path: shot, fullPage: true })
})
Loading
Loading