Commit fa34dac
authored
feat(ui): UI revamp — Phase 1 foundation + Phase 2 power-browsing + Phase 3 bulk actions (#571)
## Summary
Complete implementation of the three-phase UI revamp from
`docs/superpowers/specs/2026-04-20-ui-revamp-design.md` — Phase 1
foundation, Phase 2 power-browsing, and Phase 3 bulk actions — in a
single branch of 104 atomic commits on top of `main`. Everything the
design doc scoped is shipped, minus the two items the plan itself
flagged as deferred (relationship graph view, saved views).
Despite the misleading `feat/ui-revamp-phase-1a` branch name (the branch
was created when scope was still Phase 1 only, and the name stuck), the
actual delivery covers all three phases.
Every commit builds, vets, and tests independently — split by concern so
bisect stays useful. Individual `fix(ui):` commits ship with Playwright
regression guards where the fix is observable (scroll, flex shrinkage,
alignment, flash). Full branch green on `golangci-lint`, `go test ./...`
(skipping `TestLDAPIntegration` which needs a live AD), and `go test
-tags e2e ./internal/e2e` (202-user seed, 71 s full run).
## Phase 1 — Foundation
Stack migration and new visual system.
- **Pico CSS v2 + htmx + Alpine.js (CSP build)**, vendored.
Tailwind/TypeScript/PostCSS/bun toolchain dropped.
- **New `app.css`** with AAA-verified hybrid theme tokens (see
`TestAppCSSTokensMeetAAAContrast`). Dark: monospace IDE palette; Light:
sans neutral.
- **Command palette** (`⌘K` / `/`) with live search across all loaded
users, groups, computers — served from an ETag-cached
`/api/search-index.json`.
- **Side-panel detail drawer** for users/groups/computers. Three-pane
layout at wide viewports (OU rail · list · drawer), disclosure-collapsed
at narrow.
- **Pivot links** inside every drawer (open full page, member-of,
manages, OU peers, etc.).
- **Recents** (client-side, localStorage) and **pinned** (server-side
bbolt store, per-user).
- **Dark/light theme + compact/comfortable density** toggles;
preferences initialised via external JS (CSP-compliant).
- **Full-page detail views** on F5 of any `/users/:dn`, `/groups/:dn`,
`/computers/:dn?fragment=drawer` URL.
- **AAA conformance** on every new surface. `@media
(prefers-reduced-motion)` respected across transitions.
- **Reimplementation of all existing routes** in the new system (home,
list, detail, login, logout, error pages).
## Phase 2 — Power browsing
From `2026-04-20-ui-revamp-phase-2-3-consolidated.md`, everything except
saved views (flagged low-priority).
- **Faceted filters** on every list page: OU, enabled/disabled,
last-logon window (24 h / 7 d / 30 d / never), member-of. Chips wire up
to query-string params.
- **OU tree rail** — distinct immediate-OU values populated from the
cache, toggleable secondary filter on `/users`, `/groups`, `/computers`.
- **Inline attribute edit** in the drawer for users (email, description,
displayName, title, department, company, telephone, mobile, office).
- Saved views / named searches: deferred.
## Phase 3 — Advanced
Bulk multi-select + batch actions shipped; graph view explicitly
deferred in
`docs/superpowers/specs/2026-04-20-ui-revamp-phase-3-graph-view-deferred.md`
("AAA for node-graph visualisations is its own research").
- **`/users/bulk`**: `add-to-group`, `remove-from-group`, `delete`,
`disable` (AD-gated — OpenLDAP returns 501 because `userAccountControl`
is not portable).
- **`/groups/bulk`**: `add-members`, `delete` (via `simple-ldap-go`
v1.12's generic `DeleteByDN`).
- **`/computers/bulk`**: `delete` (same generic) and `disable`
(AD-gated).
- **Session-backed flash** via `setFlash` / `takeFlash` — every bulk
path queues a `templates.Flash` before the redirect; the receiving list
handler pops it and renders "Deleted N / M groups." / "Disabled N …" /
partial-failure / total-failure banners above the filter row. Flash
consumed on read.
- Relationship graph view: deferred.
## Beyond the design doc
Work that fell out of implementation and isn't in any of the three phase
specs:
- **Nested group management** — parent ↔ child edges on `/groups/:dn`
with cycle detection via BFS over `collectAncestorDNs` /
`collectDescendantDNs`, depth cap 32.
- **`AdminCount`-based Privileged badge** — replaces the English CN
allowlist. Reads `User.AdminCount` (new in `simple-ldap-go` v1.12)
mapped from AD's `adminCount=1`. Catches localised AD directories, no
caller-side walks.
- **Humanised LDAP-error flash** — modify handlers now pipe AD errors
(Code 50 / 20 / 32) through `humaniseLDAPError` into a drawer-level
`drawer__flash--error` banner. "Insufficient Access Rights" no longer
looks like a silent success.
- **Header stability under long lists** — `flex-shrink: 0` on topnav and
topnav-secondary so body-flex pressure from 8k-row lists can't collapse
their padding. Regression guard at
`TestHeaderDoesNotShrinkWithLongLists`.
- **Scroll invariants** — `html:has(> body.has-page-scroll) { height:
auto; overflow: visible }` releases the list-page shell's clip when body
opts into page scroll. F5 on fragment URLs produces a scrollbar again.
Guarded at `TestFragmentURL_DirectNavIsScrollable`.
- **Alignment fixes** — reset Pico's default `label { margin-bottom }`
on inline chip labels; select-all chip now baselines with the search
input.
- **`simple-ldap-go` upgraded to v1.12.0** — new `DisableUser` /
`EnableUser` / `DisableComputer` / `EnableComputer` methods, new
`User.AdminCount` field, exported `ACCOUNTDISABLE` constant. [Release
notes.](https://github.com/netresearch/simple-ldap-go/releases/tag/v1.12.0)
## Layout
```
internal/web/
├── static/app.css # AAA-verified tokens, list-page/drawer/topnav/flash styles
├── static/vendor/ # Pinned Pico, htmx, Alpine (CSP)
├── templates/ # *_v2.templ shells + Pico layout
│ ├── base_v2.templ
│ ├── home_v2.templ
│ ├── users_v2.templ / groups_v2.templ / computers_v2.templ
│ ├── list_flashes.templ
│ └── palette_v2.templ
├── bulk_handlers.go # add/remove/delete/disable + finaliseBulk*
├── flash.go # setFlash / takeFlash (session-backed)
├── *_v2_handler.go # list + detail handlers, per-entity
├── users_edit.go # inline drawer KV edits
└── users.go / groups.go # single-entity modify + humaniseLDAPError
```
## Test plan
- [ ] `make check` — lint + vet + unit tests (currently: 0 lint issues,
all unit tests pass)
- [ ] `go test -tags e2e ./internal/e2e/` — Playwright harness against
OpenLDAP testcontainer, 202-user bulk seed. Target: green (last local
run: 71 s)
- [ ] Exercise against a live AD:
- [ ] Bulk-delete a throwaway group from `/groups`, confirm "Deleted N
group(s)." flash
- [ ] Disable a test user from the drawer on an AD account, confirm
flash + bit 0x2 set in `userAccountControl` via another tool
- [ ] Confirm the Privileged shield renders next to users with
`adminCount=1`
- [ ] F5 on `/users/:dn?fragment=drawer` — scrollbar + full shell
present
- [ ] Resize `/users` viewport to 700 px with >100 rows — topnav /
topnav-secondary stay at natural height
- [ ] Dark mode + compact density — AAA contrast holds on every banner /
badge
- [ ] Verify `simple-ldap-go` v1.12.0 resolves (`go mod why
github.com/netresearch/simple-ldap-go`)
## Explicitly not in this PR (deferred by the design docs)
- **Relationship graph view** — needs its own AAA research before
implementation. See
`docs/superpowers/specs/2026-04-20-ui-revamp-phase-3-graph-view-deferred.md`.
- **Saved searches / named views** — low priority once the command
palette landed.
## Notes
- Disable is AD-only, gated in both the handler
(`a.ldapConfig.IsActiveDirectory`) and the template (button renders only
when `vm.IsAD`). Non-AD deployments keep the 501 `bulkNotImplemented`
contract.
- `AdminCount` is sticky in AD — `true` means "is or was privileged",
not a real-time check. Good for badging, not for authorisation
decisions. Documented inline in upstream `User.AdminCount` and in
`adminUserDNs`.
- No backwards-compatibility shims — the old `_v1` templates and
handlers are gone in favour of `_v2`. Out-of-tree code referencing them
needs a follow-up.
- Branch name (`feat/ui-revamp-phase-1a`) predates the scope expansion;
renaming a branch mid-PR is disruptive, so leaving it.141 files changed
Lines changed: 19042 additions & 11611 deletions
File tree
- .github
- workflows
- docs/superpowers
- plans
- specs
- internal
- e2e
- testdata
- ldap_cache
- options
- web
- static
- js
- ts
- vendor
- templates
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
17 | 10 | | |
18 | | - | |
19 | | - | |
20 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
40 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
41 | 47 | | |
42 | 48 | | |
43 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
10 | 24 | | |
11 | 25 | | |
12 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | 60 | | |
66 | 61 | | |
67 | 62 | | |
| |||
90 | 85 | | |
91 | 86 | | |
92 | 87 | | |
93 | | - | |
| 88 | + | |
94 | 89 | | |
95 | | - | |
96 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
97 | 93 | | |
98 | 94 | | |
99 | 95 | | |
| |||
204 | 200 | | |
205 | 201 | | |
206 | 202 | | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
207 | 208 | | |
208 | 209 | | |
209 | 210 | | |
| |||
247 | 248 | | |
248 | 249 | | |
249 | 250 | | |
250 | | - | |
| 251 | + | |
251 | 252 | | |
252 | | - | |
253 | | - | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
254 | 256 | | |
255 | 257 | | |
256 | 258 | | |
| |||
346 | 348 | | |
347 | 349 | | |
348 | 350 | | |
349 | | - | |
350 | 351 | | |
351 | 352 | | |
352 | 353 | | |
| |||
358 | 359 | | |
359 | 360 | | |
360 | 361 | | |
361 | | - | |
| 362 | + | |
362 | 363 | | |
363 | 364 | | |
364 | 365 | | |
365 | 366 | | |
366 | | - | |
367 | 367 | | |
368 | 368 | | |
369 | 369 | | |
| |||
459 | 459 | | |
460 | 460 | | |
461 | 461 | | |
462 | | - | |
| 462 | + | |
463 | 463 | | |
464 | | - | |
465 | | - | |
466 | | - | |
467 | | - | |
468 | | - | |
469 | | - | |
470 | | - | |
471 | | - | |
472 | | - | |
473 | | - | |
474 | | - | |
475 | | - | |
476 | | - | |
| 464 | + | |
| 465 | + | |
477 | 466 | | |
478 | 467 | | |
479 | 468 | | |
480 | 469 | | |
481 | | - | |
| 470 | + | |
482 | 471 | | |
483 | 472 | | |
484 | 473 | | |
485 | 474 | | |
486 | 475 | | |
487 | | - | |
| 476 | + | |
488 | 477 | | |
489 | 478 | | |
490 | 479 | | |
| |||
493 | 482 | | |
494 | 483 | | |
495 | 484 | | |
496 | | - | |
497 | | - | |
498 | | - | |
499 | | - | |
500 | | - | |
501 | | - | |
502 | | - | |
503 | | - | |
| 485 | + | |
| 486 | + | |
504 | 487 | | |
505 | 488 | | |
506 | 489 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
73 | 73 | | |
74 | 74 | | |
75 | | - | |
| 75 | + | |
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
88 | 96 | | |
89 | 97 | | |
90 | 98 | | |
| |||
155 | 163 | | |
156 | 164 | | |
157 | 165 | | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
158 | 172 | | |
159 | 173 | | |
160 | 174 | | |
161 | 175 | | |
162 | | - | |
| 176 | + | |
163 | 177 | | |
164 | 178 | | |
165 | 179 | | |
| |||
0 commit comments