Skip to content

feat(graph): phase 3 graph view — Slice 2 (JSON endpoint) - #582

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

feat(graph): phase 3 graph view — Slice 2 (JSON endpoint)#582
CybotTM merged 7 commits into
mainfrom
feat/phase-3-graph-slice-2

Conversation

@CybotTM

@CybotTM CybotTM commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

Slice 2 of 6 for the Phase 3 relationship graph view. Adds the JSON endpoint that the upcoming HTML/SVG slices will consume.

Plan: docs/superpowers/plans/2026-04-24-phase-3-graph-view.md Tasks 10-14. Slice 1 (the in-memory graph builder) is already on main as #581.

What ships

  • GET /api/graph.json?entity=<dn>&depth=<N> — registered in protected group of internal/web/server.go.
  • Validation: missing entity (400), invalid DN (400), non-numeric depth (400), unknown DN (404).
  • Caching: sha256-based ETag (matches /api/search-index.json pattern); If-None-Match short-circuits to 304; Cache-Control: private, must-revalidate.
  • Default depth: 2. Out-of-range integer values (-5, 0, 99) clamp to [1, 3] inside BuildGraph.
  • Error hygiene: 500 returns generic "internal error"; underlying error logged via zerolog with entity + depth context (no DN/internals leakage).

Test infrastructure changes

To let internal/web/ tests seed internal/ldap_cache.Manager from outside the package:

  • NEW internal/ldap_cache/cachetest/ sub-package — exports NewUserWithDN, NewGroupWithDN, NewComputerWithDN, Seed. Uses reflection+unsafe to populate the unexported Object.dn/cn fields on simple-ldap-go types. Test-only by convention; not imported by any production code.
  • NEW internal/ldap_cache/seed_for_tests.go — adds Manager.SetUsersForTesting / SetGroupsForTesting / SetComputersForTesting exported methods. Thin wrappers around the unexported setAll. ForTesting suffix per Go convention.

Tests (7 total, all green)

  • TestHandleGraphJSON_MissingEntity — 400 on missing ?entity=.
  • TestHandleGraphJSON_InvalidDN — 400 on garbage DN.
  • TestHandleGraphJSON_UnknownDN — 404 when DN not in cache.
  • TestHandleGraphJSON_InvalidDepth — 400 on non-numeric depth.
  • TestHandleGraphJSON_ETagStable — first call returns 200+ETag, second with If-None-Match returns 304.
  • TestHandleGraphJSON_DepthClamping — depth=0/99/-5 all return 200 with Depth ∈ [1,3].
  • TestGraphJSON_IntegrationUserFocus — exercises the full stack against real OpenLDAP (skipped if no container at 127.0.0.1:1389).

Test plan

  • go test ./internal/web/ -run TestHandleGraphJSON -count=1 -v → 6 unit tests PASS
  • Integration test PASS against osixia/openldap:1.5.0 with dc=test,dc=local
  • go test ./internal/ldap_cache/... -count=1 → no Slice 1 regression
  • golangci-lint run ./internal/web/... ./internal/ldap_cache/... → 0 issues
  • go build ./... clean
  • CI verification on push

What's next

Slice 3: HTML template + SSR-rendered SVG (no-JS fallback path). Slice 4: interactive canvas. Slices 5-6: list-page mode + drawer pivots + AAA parallel list view + docs.

CybotTM added 6 commits April 25, 2026 01:21
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Adds 5 unit tests for the /api/graph.json handler covering missing
entity, invalid DN, unknown DN, ETag stability with If-None-Match, and
depth clamping for out-of-range values.

To make Manager seedable from outside ldap_cache, exposes three
ForTesting hooks (SetUsers/Groups/ComputersForTesting) and a new
internal/ldap_cache/cachetest package with reflection-based constructors
that populate simple-ldap-go's unexported dn/cn fields.

Registers /api/graph.json in setupFullTestApp without RequireAuth,
mirroring the existing pattern for /api/search-index.json — the handler
only reads from app.ldapCache and needs no session state.

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Copilot AI review requested due to automatic review settings April 24, 2026 23:45
@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 added the tests label Apr 24, 2026
@github-actions

github-actions Bot commented Apr 24, 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 24, 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)

Comment thread internal/ldap_cache/cachetest/helpers.go Fixed
Comment thread internal/ldap_cache/cachetest/helpers.go Fixed
@codecov

codecov Bot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.66667% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.23%. Comparing base (383047a) to head (b4417ca).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
internal/ldap_cache/cachetest/helpers.go 53.84% 10 Missing and 2 partials ⚠️
internal/web/graph_v2_handler.go 71.42% 11 Missing and 1 partial ⚠️
internal/ldap_cache/seed_for_tests.go 33.33% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #582      +/-   ##
==========================================
+ Coverage   68.07%   68.23%   +0.15%     
==========================================
  Files          29       32       +3     
  Lines        3092     3167      +75     
==========================================
+ Hits         2105     2161      +56     
- Misses        836      854      +18     
- Partials      151      152       +1     
Flag Coverage Δ
e2e 58.99% <ø> (ø)
unittests 68.65% <62.66%> (+0.15%) ⬆️

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 JSON API endpoint (GET /api/graph.json) in the protected web server routes, plus supporting test infrastructure to seed the in-memory LDAP cache from internal/web tests.

Changes:

  • Register /api/graph.json in the protected route group and in test harnesses.
  • Implement handleGraphJSON with query validation, ETag-based conditional GET, and graph building via ldap_cache.Manager.BuildGraph.
  • Add unit/integration tests and test-only cache seeding helpers (cachetest + Set*ForTesting).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/web/server.go Registers the new protected /api/graph.json route.
internal/web/server_test.go Exposes /api/graph.json without auth in the test harness (mirrors search-index test approach).
internal/web/ldap_integration_test.go Registers /api/graph.json on the integration-test app’s protected router.
internal/web/graph_v2_handler.go Implements the JSON endpoint, validation, ETag caching, and BuildGraph invocation/logging.
internal/web/graph_v2_handler_test.go Adds unit tests for validation, ETag stability, and depth clamping behavior.
internal/web/graph_integration_test.go Adds an LDAP-backed integration test for the endpoint behind auth.
internal/ldap_cache/seed_for_tests.go Adds Manager.Set*ForTesting hooks to allow seeding caches without LDAP I/O.
internal/ldap_cache/cachetest/helpers.go Adds cross-package test helpers to construct LDAP objects and seed the manager.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/web/graph_v2_handler.go
Comment thread internal/web/graph_v2_handler.go Outdated
- handleGraphJSON: on json.Marshal failure, log via zerolog with the
  entity from the query string and return a plain "internal error"
  (500) instead of propagating to Fiber's HTML ErrorHandler. Mirrors
  the BuildGraph error path so the JSON API endpoint stays consistent.
- handleGraphJSON: per RFC 7232 §4.1, set ETag and Cache-Control
  before the If-None-Match check so 304 responses also carry the
  validators clients/proxies need to refresh caches. Content-Type is
  now set only on the 200 path (304 has no body).
- cachetest helpers: add per-occurrence justification on the two
  unsafe.Pointer //nolint:gosec calls so GitHub Advanced Security
  has the required reason inline. Hoisted the unsafe.Pointer
  computations onto separate lines to keep within the project's lll
  line-length limit.
- Added TestHandleGraphJSON_NotModifiedKeepsHeaders to lock in the
  RFC 7232 behaviour as a regression guard.

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.

dnField := obj.FieldByName("dn")
cnField := obj.FieldByName("cn")
// test-only writers for unexported simple-ldap-go fields; never used in production code.
dnPtr := unsafe.Pointer(dnField.UnsafeAddr()) //nolint:gosec // test-only DN field write
cnField := obj.FieldByName("cn")
// test-only writers for unexported simple-ldap-go fields; never used in production code.
dnPtr := unsafe.Pointer(dnField.UnsafeAddr()) //nolint:gosec // test-only DN field write
cnPtr := unsafe.Pointer(cnField.UnsafeAddr()) //nolint:gosec // test-only CN field write
@CybotTM
CybotTM merged commit cea1767 into main Apr 25, 2026
25 checks passed
@CybotTM
CybotTM deleted the feat/phase-3-graph-slice-2 branch April 25, 2026 05:02
CybotTM added a commit that referenced this pull request Apr 25, 2026
…+ CSS) (#584)

## 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.go` — `handleGraphV2` 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
(`FindStringSubmatch` → `FindAllStringSubmatch` 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

- [x] `go test ./internal/web/ -count=1` → ok
- [x] `go test ./internal/ldap_cache/...` → no regressions
- [x] `golangci-lint run ./...` → 0 issues
- [x] `go build ./...` → clean
- [x] `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 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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants