Skip to content

feat(graph): phase 3 graph view — Slice 3 (SSR template + edge table + CSS) - #584

Merged
CybotTM merged 10 commits into
mainfrom
feat/phase-3-graph-slice-3
Apr 25, 2026
Merged

feat(graph): phase 3 graph view — Slice 3 (SSR template + edge table + CSS)#584
CybotTM merged 10 commits into
mainfrom
feat/phase-3-graph-slice-3

Conversation

@CybotTM

@CybotTM CybotTM commented Apr 25, 2026

Copy link
Copy Markdown
Member

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.md Tasks 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.json via the templ GraphPageV2 template.
  • No-JS fallback path is the primary deliverable: depth slider has <noscript> Apply button, SVG nodes render at SSR-computed positions, edge table is fully functional. Slice 4 will add interactivity on top.
  • WCAG 2.2 compliance baseline:
    • SSR edge table mirrors the visual graph (AAA-mandated text alternative).
    • SVG role="img" + aria-labelledby/aria-describedby; nodes get role="button" + tabindex="0" + descriptive aria-label.
    • prefers-reduced-motion: reduce honored.
    • All graph color tokens hit AAA (7:1) for text and AA (3:1) for non-text per §1.4.11.

Files

  • internal/web/templates/graph_v2.templ — full SSR template + helpers
  • internal/web/graph_v2_handler.gohandleGraphV2 HTML handler + focus label/type helpers
  • internal/web/server.go/graph route in protected group
  • internal/web/static/app.css — graph styles + light/dark tokens + motion-reduce override
  • internal/web/contrast_test.go — 6 new contrast pair assertions, fixed latent dark-block parser bug

Tests

  • 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 (FindStringSubmatchFindAllStringSubmatch for dark blocks).
  • All Slice 1+2 tests still pass (TestHandleGraphJSON_*, TestBuildGraph_*).

Notable design decisions

  • Inline JSON via @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 by json.Marshal, so DN values cannot break out of the script tag.
  • Concentric ring multiplier 150 (was 180 in plan) — keeps ring-3 nodes inside the SVG viewBox.
  • GraphPageVM.Sort* and BackHref fields reserved for Slice 5 list-page mode wiring.
  • /graph 400/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 → ok
  • go test ./internal/ldap_cache/... → no regressions
  • golangci-lint run ./... → 0 issues
  • go build ./... → clean
  • templ generate → produces graph_v2_templ.go (45 KB), gitignored
  • CI verification on push
  • Visual smoke (after merge): docker compose --profile dev up -d then browse to /graph?entity=<some-DN> to confirm SSR-only rendering looks reasonable

What's next

  • Slice 4: static/js/v2-graph.js — pan/zoom, keyboard navigation, click-to-pivot, depth-slider on input, edge highlighting, ARIA live announcements.
  • Slice 5+6: list-page Graph mode, drawer pivots, AAA parallel-list verification, docs.

CybotTM added 9 commits April 25, 2026 07:14
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>
Copilot AI review requested due to automatic review settings April 25, 2026 05:40
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

github-actions Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

github-actions[bot]
github-actions Bot previously approved these changes Apr 25, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Mutation Testing Results

Mutation Score: 0% (threshold: 60%)

⚠️ Score is below threshold. Consider improving test coverage or test quality.

What is mutation testing?

Mutation testing measures test quality by introducing small changes (mutations) to the code and checking if tests detect them. A higher score means better test effectiveness.

  • Killed mutants: Tests caught the mutation (good!)
  • Survived mutants: Tests missed the mutation (needs improvement)

@codecov

codecov Bot commented Apr 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.31%. Comparing base (cea1767) to head (97781d6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/web/graph_v2_handler.go 76.92% 4 Missing and 2 partials ⚠️
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     
Flag Coverage Δ
e2e 58.99% <ø> (ø)
unittests 68.73% <77.77%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GraphPageV2 SSR template rendering SVG canvas, depth slider, and relationships table + inline graph payload.
  • Register /graph handler/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.

Comment thread internal/web/templates/graph_v2.templ Outdated
Comment thread internal/web/templates/graph_v2.templ Outdated
Comment thread internal/web/templates/graph_v2.templ Outdated
Comment thread internal/web/templates/graph_v2.templ Outdated
Comment thread internal/web/templates/graph_v2.templ Outdated
Comment thread internal/web/static/app.css Outdated
Comment thread internal/web/templates/graph_v2.templ
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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

@CybotTM
CybotTM merged commit 03c4c8e into main Apr 25, 2026
25 checks passed
@CybotTM
CybotTM deleted the feat/phase-3-graph-slice-3 branch April 25, 2026 05:58
CybotTM added a commit that referenced this pull request Apr 25, 2026
## 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.
CybotTM added a commit that referenced this pull request Apr 25, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants