feat: Admin section redesign — Concept C (Master-Detail Command Center) #156
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overview
The current admin section (
/admin) is a tab-based layout that has significant UX and accessibility problems at mobile and tablet widths. This issue tracks a full redesign to a Master-Detail Command Center layout (Concept C) covering all four entity types: Users, Groups, Tags, and System.A complete wireframe spec has been committed to the repo:
📄 docs/specs/admin-redesign-concept-c.html
Open it locally in a browser for the full interactive wireframes.
Problems with the current design (UX audit findings)
opacity-0 group-hover:opacity-100— completely broken on mobile/touchProposed layout: Three-panel Master-Detail
URL scheme
State is encoded in query params so deep links and browser back work:
/admin?v=users— Users list, no selection/admin?v=users&id=<uuid>— Users list, user selected/admin?v=users&id=new— Create new user form open/admin?v=system— System tab (list panel hidden)Breakpoints
< 768pxMobile768–1023pxTablet≥ 1024pxDesktopTablet: Collapseable panels (addendum)
At 768px the original three-panel spec leaves only 408px for the detail — too cramped for a form. The tablet layout introduces three states:
localStoragekeyadmin_list_collapsedtransform: translateXanimation (180ms ease-out), role="dialog", focus-trappedPer-entity spec summary
Users
Groups
Tags
System
Edge cases (all entities)
Interaction specification (key rules)
Acceptance criteria
?v=,&id=) reflect panel state; direct URL loads workImplementation notes
/adminroute useslet activeTab = $state('users')with conditional rendering — this needs to be replaced by the new URL-param-driven panel system/admin/users/[id]and/admin/users/newroutes may be absorbed into the single/adminroute via the detail panel, or kept as separate routes that the detail panel renders intoisTablet = $derived(windowWidth >= 768 && windowWidth < 1024)let listCollapsed = $state(false)in the admin layout, synced to localStorageSpec authored by Leonie Voss (UI/UX persona) · v1.1 · 2026-03
Full wireframe spec: docs/specs/admin-redesign-concept-c.html
Architectural Review — Concept C Implementation
@mkeller · 2026-03-29
I have read the spec and the current code. The UX problems are real and worth fixing. Before implementation starts, four structural decisions need to be locked down. I am recording them here so they are not re-litigated during the PR.
Decision 1: Sub-routes with a shared layout shell ✅
Use Option B:
/admin/+layout.svelteas the three-panel shell, sub-routes render into the detail slot.The spec leaves this open ("may be absorbed… or kept as separate routes"). It should not be left open — this is the load-bearing decision for everything else.
Proposed route tree:
Why this is correct:
beforeNavigate(see Decision 4 below) works cleanly on route changes./admin/users/[id],/admin/users/new). This is an evolution of what is there, not a rewrite.Decision 2: Move the auth guard to
+layout.server.tsCurrently the
hasAdmincheck is duplicated in every load function. Bothroutes/admin/+page.server.ts:17androutes/admin/users/[id]/+page.server.ts:8contain identical copies of:With a shared layout, this belongs in
+layout.server.tsonce:All child
loadfunctions inherit this guard. Remove the duplicates.Decision 3: Fetch only what each route needs — no upfront triple-fetch
The current
routes/admin/+page.server.ts:24fetches users + groups + tags in parallel on every page load, regardless of which tab was active. With sub-routes, this is gone entirely:+layout.server.tsadmin/users/+page.server.ts/api/usersadmin/users/[id]/+page.server.ts/api/users/{id}+/api/groups(for group assignment — already correct)admin/groups/+page.server.ts/api/groupsadmin/groups/[id]/+page.server.ts/api/groups/{id}+/api/users(for member management)admin/tags/+page.server.ts/api/tagsadmin/system/+page.svelteThis gives you genuinely lazy loading: opening the tags panel does not fetch the users list.
Decision 4: Unsaved-changes guard via
beforeNavigateThe spec says "inline warning in detail panel when navigating away (not a modal)." With sub-routes,
beforeNavigatefrom$app/navigationfires on every route change — exactly what you need.Pattern to use in detail panel components:
The
isDirtyflag is set totrueon anyoninputevent in the form, and reset tofalseafter a successful save or explicit discard. The warning renders inline in the detail panel — no modal, as the spec requires.Important:
isDirtymust be reset tofalseafterenhancecompletes successfully. Use theupdatecallback:Decision 5: No persistence for collapse state
The spec calls for
localStoragepersistence of the list panel collapse state. Drop this.Initialising from
localStorageserver-side is impossible; reading it inonMountcauses a visible layout shift on every page load (panel renders collapsed → expands, or vice versa). The admin panel is used infrequently enough that re-collapsing on each visit is not a meaningful UX cost.Implementation:
let listCollapsed = $state(false)in the layout. Resets on navigation. Done.What to fix before starting the layout redesign
Three of the critical defects in the UX audit are one-line fixes that should not wait for the full redesign milestone. They are production accessibility defects:
1. Tag hover buttons —
routes/admin/TagsTab.svelte:792. Users table — no horizontal scroll,
routes/admin/UsersTab.svelte:323. ADMIN badge — color-only indicator,
routes/admin/GroupsTab.svelte:124These three can be committed to
mainindependently today. Do not hold them for the layout redesign milestone.Form action re-homing
The current
routes/admin/+page.server.tsowns all mutations:deleteUser,updateTag,deleteTag,createGroup,updateGroup,deleteGroup. With sub-routes, these need to move to their natural homes:deleteUseradmin/users/[id]/+page.server.ts(spec puts delete in the detail danger zone)updateTag,deleteTagadmin/tags/+page.server.tscreateGroupadmin/groups/+page.server.tsupdateGroup,deleteGroupadmin/groups/[id]/+page.server.tsThe current
routes/admin/+page.server.tsbecomes+layout.server.ts(auth guard only) plus+page.server.ts(redirect to/admin/users). Its actions are distributed to the routes above and can be deleted from the current file.Summary of implementation order
main: tag hover visibility, users table scroll, ADMIN badge non-color indicator.admin/+layout.svelte+admin/+layout.server.ts(auth guard). Get the three-panel chrome rendering at all three breakpoints with static placeholder content before touching any entity logic.beforeNavigatepattern to each detail panel once the routing is stable.— @mkeller
Test Plan — Admin Redesign (Concept C)
@saraholt · 2026-03-29
Read the spec, read Markus's architectural review, read the existing test files. Here is what the suite needs to look like after this feature lands.
What changes in the existing suite
admin/page.server.spec.tsadmin/page.svelte.spec.ts+page.svelte— the tab layout is goneadmin/users/new/page.svelte.spec.ts/admin— that changes to/admin/usersadmin/users/[id]/page.svelte.spec.tsLayer 2 — Load Function Tests (Vitest)
admin/+layout.server.spec.ts(new — absorbs auth tests frompage.server.spec.ts)admin/users/+page.server.spec.ts(new)admin/groups/+page.server.spec.ts(new)admin/groups/[id]/+page.server.spec.ts(new)admin/tags/+page.server.spec.ts(new)Layer 2 — Component Tests (Vitest + vitest-browser-svelte)
admin/+layout.svelte.spec.ts(new — the three-panel shell)admin/users/+page.svelte.spec.ts(new — replaces users section of current page.svelte.spec.ts)admin/groups/+page.svelte.spec.ts(new)admin/groups/[id]/+page.svelte.spec.ts(new)admin/tags/+page.svelte.spec.ts(new)admin/users/[id]/+page.svelte.spec.ts(extend existing)admin/system/+page.svelte.spec.ts(new)Layer 4 — E2E (Playwright) —
frontend/e2e/admin.spec.tscheckA11yruns on every page visit. Critical journeys only — permutations belong in unit/component layer.CI impact estimate
All within pyramid targets.
Pre-condition before any of the above
The three hotfixes Markus flagged (tag
opacity-0removal, users tableoverflow-x-auto, ADMIN badge non-color indicator) ship tomainfirst as independent commits. The tag button visibility test gets written red first before the fix lands — that is the one that has been failing in production silently.— @saraholt
Security Review — Concept C Admin Redesign
Reviewer: Nora "NullX" Steiner · OSWE · BSCP
The feature spec is well-structured and the WCAG fixes are welcome. The backend permission enforcement via
@RequirePermission/PermissionAspectis a solid centralized control point. However, I found one pre-existing IDOR that this feature will amplify, and one authorization model mismatch that the new multi-panel UI will expose more sharply than the current tab layout does.🔴 CRITICAL — C1: IDOR on
GET /api/users/{id}(CWE-862)Location:
UserController.java:64-68Why this matters now: The new spec encodes user UUIDs in the URL (
/admin?v=users&id=<uuid>). Once any authenticated user sees that URL scheme (shared link, browser history, dev tools), they can enumerate any user's profile directly:The response reveals group membership and permission sets for every user in the system. This was already exploitable, but it becomes significantly more discoverable with UUIDs in the address bar.
Fix:
🟠 HIGH — H1: Frontend gate checks
ADMIN, backend uses granular permissions — the new UI exposes the mismatchLocation:
+page.server.ts:17-20vs.GroupController.java:25,TagController.java:33The backend has four distinct admin permissions:
ADMIN,ADMIN_USER,ADMIN_TAG,ADMIN_PERMISSION. The current tab layout blurs this because everything is on one page. The new three-panel layout makes each entity first-class — a user with onlyADMIN_TAGis supposed to manage tags but currently can't even reach/admin.Two options — pick one and document it explicitly:
Option A — flatten to
ADMINonly (current behaviour, explicit):Document that
ADMIN_USER/TAG/PERMISSIONare back-channel API-layer protections; the/adminfrontend is gated on the omnibusADMINflag. Add a comment in the load function so future developers don't expect partial-admin access to work end-to-end.Option B — honour granularity in the new UI:
🟡 MEDIUM — M1:
?v=parameter needs an allowlistThe
?v=param drives panel rendering. Without a validation step, unexpected values reach conditional rendering logic (JS errors, blank panels).🟡 MEDIUM — M2: Verify
SameSiteon session cookie for maintenance operationsSecurityConfigdisables CSRF with the justification that Basic Auth requires a customAuthorizationheader (which browsers block cross-origin). This reasoning is sound as long as the auth model stays Basic Auth.Spring Session JDBCis in the stack. If session cookies are ever used for auth propagation, the CSRF justification breaks silently — and the backfill endpoints (POST /api/admin/backfill-*) are high-impact targets.Two concrete checks for the System tab implementation:
server.servlet.session.cookie.same-site=Lax(orStrict) inapplication.properties.Authorizationheader), not bare client-sidefetch().🔵 LOW — L1:
localStorage— scope explicitly to UI preferencesadmin_list_collapsedis fine. The risk is future developers adding convenient caching (last-selected user ID, API response caches) to the same storage area.✅ Positive findings (keep these)
PermissionAspectwith class-level@RequirePermissiononAdminControllerandGroupControlleris correct — not missing.user.setPassword(null)is consistently applied before returningAppUser— the new detail panel won't accidentally leak password hashes.Action Items
@RequirePermission(Permission.ADMIN_USER)toGET /api/users/{id}UserController.java:64ADMINvs. granular-perm policy; update frontend gate to match+page.server.ts?v=against allowlist inload()+page.server.tsSameSiteon session cookie; route System tab POSTs through server actionsapplication.properties+ System tab component