feat(graph): phase 3 graph view — Slice 3 (SSR template + edge table + CSS) - #584
Conversation
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Add graph view CSS tokens and component styles (spec §5/§6): - graph-edge tokens (4.5+ light, 9.4+ dark on bg) — non-text UI per WCAG 2.2 §1.4.11 (3:1 floor) - graph-edge-focus and graph-node-border meet AAA 7:1 in both themes - node, edge, table, slider, canvas styles for the graph view Also fix latent asymmetry in TestAppCSSContrastAAA: the dark-block parser used FindStringSubmatch (first only) while light used FindAll. The file already has multiple :root[data-theme="dark"] blocks (radius overrides, terminal-personality block); the new graph tokens are the first to live in a later block and exposed the bug. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
The selector `.graph-node:hover ~ .graph-edge[data-source]` could never match: edges render before nodes inside .graph-viewport (Task 17 templ iterates Edges first, then Nodes), and the general-sibling combinator `~` only matches following siblings. Slice 4's JS will handle interactive node-edge highlighting. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #584 +/- ##
==========================================
+ Coverage 68.23% 68.31% +0.08%
==========================================
Files 32 32
Lines 3167 3194 +27
==========================================
+ Hits 2161 2182 +21
- Misses 854 858 +4
- Partials 152 154 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds the Phase 3 relationship graph HTML page (/graph) using a new SSR templ template that renders an SVG graph plus a no-JS accessible edge table, and wires it into the protected web routes with supporting CSS and tests.
Changes:
- Add
GraphPageV2SSR template rendering SVG canvas, depth slider, and relationships table + inline graph payload. - Register
/graphhandler/route and add unit test coverage for HTML rendering. - Extend CSS theme tokens + contrast tests (including a fix to dark-token parsing).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/web/templates/graph_v2.templ | New SSR templ page for /graph (SVG + edge table + helpers + inline graph payload). |
| internal/web/static/app.css | Adds graph view CSS variables and styling for canvas, nodes, edges, and table. |
| internal/web/server_test.go | Registers /graph in the test app router setup. |
| internal/web/server.go | Wires /graph route into the protected group. |
| internal/web/graph_v2_handler_test.go | Adds TestHandleGraphV2_RendersHTML for basic SSR markers. |
| internal/web/graph_v2_handler.go | Adds handleGraphV2 HTML handler + focus label/type helpers. |
| internal/web/contrast_test.go | Adds graph contrast assertions and fixes dark token parsing across multiple blocks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review feedback on Slice 3 (PR #584). All seven findings are real correctness/a11y issues: 1. Sort headers: drop <a href="?sort=..."> from graphEdgeTable — edgeLess ignores the sort query param so the links did nothing. The graphSortHeader / buildSortHref / graphSortClass helpers are kept (with a Slice-5 reservation note) for the upcoming sort wiring. 2. Table accessible name: switch <table> from aria-describedby to aria-labelledby — the <h2 id="graph-table-title">Relationships</h2> is the table's NAME, not its description. 3+4. Node interactivity is fake in Slice 3 (no JS handlers) — drop role="button"/tabindex="0" from the <g class="graph-node"> and strip the "Press Enter to open." suffix from graphNodeLabel for all four node types. Slice 4 will reinstate them once keyboard handlers exist. 5. <tr tabindex="0"> in graphEdgeRow is a dead Tab stop — the inner <a> links are the real actionable elements. Drop the tabindex but keep data-dn / data-type for Slice 4 highlighting. 6. .graph-node--ou .graph-node__disc { rx: 12; ry: 12 } targets a <circle>; rx/ry only apply to <rect>. Drop the no-op declarations and keep the fill override that visually distinguishes OUs. 7. Switch the inline JSON envelope from <script type="application/json"> to <template id="graph-data"> — strict CSP audits flag the script form even though CSP L3 exempts it. <template> is an inert HTML data island, never executed. Renamed graphInlineScript → graphInlineDataBlock; Slice 4 reads via .content.textContent. Test markers (id="graph-canvas", id="graph-data", class="graph-table", "Relationships: bob") all preserved; TestHandleGraphV2_RendersHTML passes unchanged. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
## Summary Slice 4 of 6 for the Phase 3 relationship graph view. Adds the client-side JS layer that turns Slice 3's SSR view into an interactive canvas (pan/zoom/keyboard nav/click-to-expand) without breaking the no-JS fallback. Plan: `docs/superpowers/plans/2026-04-24-phase-3-graph-view.md` Tasks 23-29. Slices 1-3 already on main as #581 / #582 / #584. ## What ships - **`internal/web/static/js/v2-graph.js`** (~290 lines, IIFE, plain JS, CSP-safe) — pan/zoom (mouse drag, wheel+ctrl/meta, arrow keys, +/-), keyboard nav (Tab/Shift-Tab cycles nodes), click-to-pivot (node body → drawer route), click-to-expand (`+` badge → fetch `/api/graph.json?depth=1`, merge nodes/edges, announce via aria-live), depth slider auto-submit on change. - **Activates SVG nodes for interactivity** at runtime (re-adds `tabindex`, `role="button"`, "Press Enter to open" suffix that Slice 3 deliberately dropped because there was no JS handler then). - **Reads inline graph data from Slice 3's `<template id="graph-data">`** via `template.content.textContent` — works under strict `script-src 'self'` CSP. - **Renders dynamically added expandable nodes with their `+` badge** so they're discoverable with the mouse, not just the keyboard. - **Handles fetch errors gracefully** — bad responses get logged + announced "Expand failed." rather than silently no-op. - Script wired in `base_v2.templ` head with `defer`; bails immediately on pages without `#graph-data`. - **`internal/e2e/graph_test.go`** — Playwright happy-path: login → /users → grab DN → /graph?entity=…&depth=2 → assert SVG + table + parseable inline data → click expandable badge → assert aria-live announces → reduced-motion repeat. ## Notable design decisions - **No build step, no transpilation, no module system** — matches `v2-bulk.js` / `v2-drawer.js` precedent. IIFE wrapper, `'use strict'`, `var` (not `let`/`const`), `function ()` callbacks (no arrows). - **Ring multiplier 150** in `renderNode`/`renderEdge` matches Slice 3's SSR `concentricXY` so dynamically-added nodes line up with SSR-positioned ones. - **`CSS.escape(dn)` before selector use** — DNs contain commas/equals that would break attribute selectors. - **`fetch` with `credentials: 'same-origin'`** — required for the session-authed endpoint. - **Reduced-motion: nothing extra in JS** — the CSS already disables transitions; JS doesn't introduce new ones. ## Tests - Existing Slice 1-3 unit tests still pass. - `TestGraphHappyPath` (Playwright, behind `//go:build e2e`) — runs in CI. ## Test plan - [x] `go build ./...` — clean - [x] `go build -tags e2e ./internal/e2e/...` — clean - [x] `go test ./internal/web/ ./internal/ldap_cache/...` — pass, no regressions - [x] `golangci-lint run ./...` — 0 issues - [x] `node -c internal/web/static/js/v2-graph.js` — clean syntax - [ ] CI verification on push (unit + e2e) - [ ] Visual smoke (after merge): browse to `/graph?entity=<DN>` and try drag, ctrl+wheel zoom, Tab between nodes, click `+` badge to expand ## What's next - **Slice 5:** list-page Graph mode (`/users?view=graph`, `/groups?view=graph`, `/computers?view=graph`) — Tasks 30-34. - **Slice 6:** drawer "View relationships" pivot, axe-core E2E ratchet, README conformance statement — Tasks 35-40.
…t + docs) (#587) ## Summary Final slice of the Phase 3 relationship graph view. Adds the drawer entry point, the axe-core a11y ratchet, and the README conformance statement. Plan: `docs/superpowers/plans/2026-04-24-phase-3-graph-view.md` Tasks 35-40. Slices 1-5 already on main (#581 / #582 / #584 / #585 / #586). ## What ships - **"View relationships" drawer pivot** on user, group, and computer drawers (Tasks 35-36). Inserted as the second pivot after "Open full page", with a new `iconGraph` SVG (4 nodes connected as a square). Links to `/graph?entity=<DN>`. - **`internal/e2e/axe_graph_test.go`** (Task 37) — runs axe-core against `/graph?entity=<seeded-DN>&depth=2`, `/users?view=graph`, `/groups?view=graph`, `/computers?view=graph`. Uses the WCAG 2.2 AA ruleset (graph view's stated conformance level — the AAA login ratchet stays in place separately). Each subtest waits for `svg#graph-canvas` so axe sees the JS-mutated DOM. - **README accessibility section updated** (Task 39) — adds a sentence about the graph view's WCAG 2.2 AA conformance and the AAA-equivalent flat edge table fallback. ## Deferred - **Task 38 (tab-order snapshot):** plan called for adding graph pages to a "tab-order snapshot suite" — but no such suite exists in the codebase. Building one from scratch for just three pages is a separate a11y-tooling effort. Deferred — file as a future enhancement. ## Tests - All Slice 1-5 tests still pass. - New axe-core ratchet runs in CI under the `e2e` build tag. ## Test plan - [x] `go build ./...` — clean - [x] `go build -tags e2e ./internal/e2e/...` — clean - [x] `go test ./... -count=1` — all packages pass - [x] `golangci-lint run ./...` — 0 issues - [ ] CI verification on push (unit + integration + e2e) - [ ] Manual smoke (after merge): open a user drawer → click "View relationships" → confirm graph renders → use the `List | Graph` toggle to switch back ## Phase 3 complete This PR closes out the Phase 3 relationship graph view per spec `docs/superpowers/specs/2026-04-24-phase-3-graph-view-design.md`. With this merged: - `BuildGraph(focusDN, depth)` + `BuildListGraph(users, computers)` in-memory builders (Slice 1, Slice 5) - `/api/graph.json?entity=…&depth=…` JSON endpoint with ETag/304/RFC 7232 (Slice 2) - `/graph` HTML page with SSR SVG + edge table (Slice 3) - `v2-graph.js` interactive client: pan/zoom, keyboard nav, click-to-pivot, click-to-expand, depth slider auto-submit, ARIA live announce (Slice 4) - `?view=graph` list-page mode for users/groups/computers with `List | Graph` segmented toggle (Slice 5) - "View relationships" drawer pivot + axe-core ratchet + README conformance (this PR)
Summary
Slice 3 of 6 for the Phase 3 relationship graph view. Adds the server-side-rendered HTML page that consumes the JSON endpoint from Slice 2.
Plan:
docs/superpowers/plans/2026-04-24-phase-3-graph-view.mdTasks 15-22. Slice 1 (in-memory builder) is on main as #581; Slice 2 (JSON endpoint) is on main as #582.What ships
GET /graph?entity=<dn>&depth=<N>— HTML page rendering the same graph data as/api/graph.jsonvia the templGraphPageV2template.<noscript>Apply button, SVG nodes render at SSR-computed positions, edge table is fully functional. Slice 4 will add interactivity on top.role="img"+aria-labelledby/aria-describedby; nodes getrole="button"+tabindex="0"+ descriptivearia-label.prefers-reduced-motion: reducehonored.Files
internal/web/templates/graph_v2.templ— full SSR template + helpersinternal/web/graph_v2_handler.go—handleGraphV2HTML handler + focus label/type helpersinternal/web/server.go—/graphroute in protected groupinternal/web/static/app.css— graph styles + light/dark tokens + motion-reduce overrideinternal/web/contrast_test.go— 6 new contrast pair assertions, fixed latent dark-block parser bugTests
TestHandleGraphV2_RendersHTML— verifies status 200 + 4 HTML markers (graph-canvas,graph-data,graph-table,Relationships: bob).TestAppCSSContrastAAA— extended with 6 graph pairs (3 light, 3 dark) covering edge/edge-focus/node-border. Latent parser bug fixed (FindStringSubmatch→FindAllStringSubmatchfor dark blocks).TestHandleGraphJSON_*,TestBuildGraph_*).Notable design decisions
@templ.Raw(graphInlineScript(data))— templ v0.3 treats<script>contents as opaque text, so the script element is built in Go and emitted as raw HTML outside any script block. JSON is</>/&-escaped to Unicode byjson.Marshal, so DN values cannot break out of the script tag.GraphPageVM.Sort*andBackHreffields reserved for Slice 5 list-page mode wiring./graph400/404 returns plain text — consistent with/api/graph.json. Slice 5 may add a styled HTML error page.Test plan
go test ./internal/web/ -count=1→ okgo test ./internal/ldap_cache/...→ no regressionsgolangci-lint run ./...→ 0 issuesgo build ./...→ cleantempl generate→ producesgraph_v2_templ.go(45 KB), gitignoreddocker compose --profile dev up -dthen browse to/graph?entity=<some-DN>to confirm SSR-only rendering looks reasonableWhat's next
static/js/v2-graph.js— pan/zoom, keyboard navigation, click-to-pivot, depth-slider on input, edge highlighting, ARIA live announcements.