Skip to content

Admin navigation: ten items with section tabs - #514

Merged
mchestr merged 53 commits into
mainfrom
feat/admin-nav-restructure
Nov 3, 2026
Merged

mchestr merged 53 commits into
mainfrom
feat/admin-nav-restructure

Conversation

@mchestr

@mchestr mchestr commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Why

The admin area had grown to 17 sidebar items under four group headings, a hamburger sheet plus a tab bar on phones, and one /admin/settings page with 16 sections in six groups. Finding a feature's controls meant knowing whether they lived on the feature's page or somewhere on Settings. This PR implements the restructure from the usability audit: ten flat items, each feature's controls on its own page as a Settings tab, and /admin/settings kept for what is global.

What changes

Sidebar — ten items, no group headings

Item Tabs
Overview
Assistant
Members Users · Invites · Affiliates
Announcements
Rewind Season · Playground · Shares · Settings
Discord Activity · Settings
AI Activity · Cost · Settings
Automations Watchlist sync · Inactive members · Request limits · Queues
Audit log
Settings Connections · Billing · System
  • lib/admin/nav.ts stays the one list: items carry children (the tab strip) and also (sibling paths that light the item). The sidebar, the mobile tab bar, page titles, the command menu and each section's tab strip read it. CLAUDE.md gains an "Admin navigation" subsection with the rules: a feature's controls live on the feature's page; the sidebar budget is ten items; settingsSection(id) is the only way to link to a settings section.
  • URLs are unchanged. Sibling routes keep their paths through a route group per section (app/admin/(members)/, (rewind)/, (ai)/, (automations)/, and the discord/ segment). Each group's layout.tsx is export default sectionLayout("<id>"), which draws SectionTabs from the item's permitted children; detail routes (/admin/users/[userId], …) render no strip. Layouts are not authorisation boundaries — every page keeps its own assertAdmin()/permission check.
  • Four new pages host the sections that used to sit on Settings, with the same components, LastSaved footers and test ids: /admin/rewind/settings, /admin/discord/settings (incl. channel stats), /admin/ai/settings (providers, caps, judge, assistant sources) and /admin/request-limits. The actions that save those sections revalidate the new pages. Old /admin/settings#<moved id> links redirect to the section's new page.
  • /admin/settings keeps Connections (Plex, Tautulli, Overseerr, other), Billing (Stripe) and System (time zone, API access, privacy, system). "Test all connections" counts only the sections the page shows.
  • Phones: one menu. The hamburger sheet is gone; the tab bar is Overview · Members · Rewind · Automations · More (capped at four link tabs; More lists every item). The failed-jobs dot sits on the tab that carries the badge (Automations) instead of on More. A section strip wider than the screen scrolls its selected tab into view.
  • Command menu lists "Page › Tab" entries, so "request" finds Automations › Request limits. The nav provider keeps the per-role href and permitted children the server computed, so a Support user's folded item opens its first permitted tab.
  • Support role sees Assistant, Members (Users and Affiliates tabs, no Invites) and Audit log; the refused-paths E2E list gains the four new admin-only pages.
  • Every section tab has a loading.tsx, so switching tabs shows the skeleton instead of a frozen strip.
  • Along the way: LastSaved … by <admin> links to the audit event; the unused "Sound" flag row leaves the Season page; a shared useLeaveGuard keeps the reload/close prompt for sections that no longer sit inside the Settings layout (and gives it to settings sheets, which never had it); the time-zone note inside the open Rewind form no longer offers a link that would drop the draft; settingsSection() ignores keys inherited from Object; the Watchlist page gets "Sync now"; dead icons and the unread invites count are removed; the Members badge reads "N to review" for pending applications; docs, setup-wizard and assistant copy name the new locations.

Follow-ups (not in this PR)

Notes for review

  • .claude/skills/rewind-design-system/SKILL.md changes one path token (app/admin/users/… → app/admin/(members)/users/…) because scripts/__tests__/project-skills.test.ts checks that named paths exist. CLAUDE.md asks for a /security-review of any .claude/skills change: two reviews inspected it (prose only; no instructions, shell, links or frontmatter), but no separate /security-review has run — that is yours to run or waive before merge.
  • sectionLayout() returns the async server layout component; typecheck (incl. next typegen) passes and CI's Build job exercises next build.
  • main was merged in after the skill-sources work (Assistant skills, phase 1 #513) landed: the new block renders on the AI Settings tab through the shared loader; its action, E2E spec, executor copy and docs now name /admin/ai/settings.
  • Checks on the final head: typecheck, lint (0 errors), design-system --strict, knip, env:docs --check, mcp:docs --check; full jest — the only failures (get-share-safe "is null after the season closes", retention fallback "runs once per 24 hours") fail identically on main. Playwright runs in CI.

How it was built

Planned from the usability audit, then executed as thirteen tasks on separate worktrees by implementer agents with a task-scoped review each (spec compliance + code quality), fix rounds where a review found an Important issue, merges as the union of both sides, a whole-branch review at the end and one fix wave with a scoped re-review. Per-task commits are kept so pieces can be cherry-picked or reverted individually.

https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF


Generated by Claude Code

claude added 30 commits November 2, 2026 16:50
The mechanism for the admin navigation restructure. AdminNavItem gains
`children` (section tabs) and `also` (extra prefixes that light the item),
childrenOf and navFor(subject, items?) honour tab permissions, and
SectionTabs draws the strip. ADMIN_NAV itself is unchanged. CLAUDE.md
gets the Admin navigation rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The strip now draws its own container in the width and gutters the admin
pages use (mx-auto w-full max-w-7xl px-4 md:px-6), so it lines up with the
page header below it. The documented layout wrapper (px-4 pt-4 sm:px-6) hugged
the left edge above 1280px, had an sm/md gutter mismatch between 640 and 768px,
and left an empty box on detail pages where the strip renders nothing. Layouts
now render <SectionTabs /> bare.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Replace the AI activity and Cost analysis sidebar items with one AI item
that has three tabs. URLs do not change: the pages move into an (ai)
route group whose layout draws the tab strip, and the item also lights
on /admin/cost-analysis.

AI settings is a new page at /admin/ai/settings. It holds the AI provider,
conversation review and assistant sources sections, moved from the
settings page's AI group with the same props. settingsSection() now maps
those section ids to the new page, so the links to them follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Discord becomes one nav item with two tabs: Activity, the bot dashboard
at /admin/discord, and Settings at /admin/discord/settings, which holds
the Discord integration and the channel stats that moved out of the
Settings page. Both pages sit under one layout that draws the tab strip,
and the new page checks for an admin itself.

settingsSection() sends the "discord" and "channel-stats" ids to the new
page, the empty activity period links to it, and the bot's rejected-token
message names the new place to save a token.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…ngs tabs

Season, Playground and Share analytics become one Rewind nav item with
four tabs (Season, Playground, Shares, Settings), drawn by SectionTabs in
the layout of a new (rewind) route group. The routes keep their URLs:
/admin/rewind, /admin/rewind/playground and /admin/shares move into the
group, and `also` lights the Rewind item on /admin/shares.

The Rewind settings leave /admin/settings for /admin/rewind/settings, the
Settings tab. The page loads only what the section needs and checks for an
admin itself. settingsSection("rewind") now resolves through SETTINGS_HOMES,
the Season facts panel links to the new page, the runbook and the E2E specs
follow, and updateRewindSettings revalidates the page it is saved from.

mobileTabs() takes every `mobile: "tab"` item in nav order, so the Rewind
tab is a plain item instead of a special case. The zone note's "Change the
server time zone" is now a link to the Time zone section on the settings
page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…ings page

Rewind's "Change the server time zone" was the one place that opened a
settings section from elsewhere on the page. It now links to the Time zone
section from the Rewind settings page, so the hook has no caller (knip
would fail on it). The settings-section test keeps the focus-on-edit
coverage through the view's `edit`, the path the service, AI, Discord and
Stripe sections use, and the Time zone test drops the Rewind link case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The Discord integration save, the channel stats save and "Update now"
revalidated /admin/settings, a page that no longer renders those
sections. They worked only because Next refreshes the calling page on
any revalidatePath. Revalidate /admin/discord/settings instead, as the
watchlist and inactivity actions do for their own pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The provider, spend caps, conversation review and assistant source saves
still revalidated /admin/settings, a page that no longer renders those
sections. Point them at /admin/ai/settings, as Watchlist and Inactivity
do for their own pages. The Stripe saves in admin-config stay on
/admin/settings.

updateLlmCaps and the provider saves had no test of the path; each now
has one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…s, Request limits and Queues tabs

Watchlist sync, Inactive members and Job queues become one Automations nav
item with four tabs (Watchlist sync, Inactive members, Request limits,
Queues), drawn by SectionTabs in the layout of a new (automations) route
group. The routes keep their URLs: /admin/watchlist, /admin/inactivity and
/admin/queues move into the group with git mv, and `also` lights the item
on the sibling paths. The failed-jobs badge moves from Job queues to
Automations, and the item is a mobile tab, placed after the Rewind items.

/admin/request-limits is the new Request limits tab. It renders
RequestTiersSection with the props the settings page passed, checks for an
admin itself and renders per request.

Automations gets its own icon concept (Workflow); Layers stays for the queues
page's empty state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The Members group of /admin/settings keeps Stripe only: the Request limits
section, its loader call and its nav entry leave the settings page for
/admin/request-limits. settingsSection("request-tiers") resolves there
through SETTINGS_HOMES, which every later move extends.

The settings save and Run now revalidated /admin/settings, a page that no
longer shows these fields; they revalidate /admin/request-limits instead,
as the watchlist and inactivity actions do for their own pages.

The axe scan covers the new page, the settings layout spec drops the
section, and the request limits and privacy docs name the new place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The watchlist card gets Sync now in view mode, beside the last save, as
Request limits and Inactive members have Run now. It calls the same action
as the Queues page's "Sync watchlists" (triggerWatchlistSyncJob), which
stays where it is, and toasts what the action returns.

It is disabled, with a line saying why, while watchlist sync is off (the
job would skip every member) or Redis is not set up (the action refuses).
A Redis that does not answer shows in the action's error toast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…ive members, Request limits and Queues tabs

Resolves the shared nav, routes, settings page/groups and E2E spec
conflicts as the union of both sides. The More-sheet E2E step keeps
admin-nav-automations-mobile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The three groups left on /admin/settings take their final names: Media
becomes Connections (Plex server, Tautulli, Overseerr, Sonarr/Radarr/
Jellyfin), Members becomes Billing (Stripe billing), and System keeps
Time zone, API access, Privacy and System information.

Section ids, anchors and section test ids do not change; only the group
ids and labels do, so the nav's group test ids are now
settings-nav-group-connections and settings-nav-group-billing. The
layout falls back to Connections when it has no items.

The settings groups, nav and layout tests and the settings layout spec
follow the new ids. Comments and docs/stripe.md that named the old
groups name the new ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
… flag row

"by <name>" in a settings footer now links to the audit event that recorded
the save: LastSaved carries the newest audit row's id as eventId. The Season
facts panel no longer lists Sound, since nothing in the UI can change
rewindAudio (deferred in docs/rewind.md).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…tions

SettingsSection now asks the browser to confirm before the tab closes or
reloads while its form has unsaved changes and no SettingsEditProvider
wraps it. The settings pages that moved onto feature pages (Rewind,
Discord, AI, Request limits) render their sections stand-alone and had no
guard, so a reload mid-edit silently discarded the draft.

On the settings page nothing changes: the layout still owns the one
listener, fed by reportDirty.

The Watchlist sync and Automatic removal cards each carried a private copy
of the guard. Both are deleted, and their leave-guard tests now exercise
the shared one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…System

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Review nits from Task 6: the Stripe E2E comment named a group that no
longer exists, and the service-form test header described Connections
as read-only although its Sonarr, Radarr and Jellyfin section edits in
a sheet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The admin nav loses its groups. The sidebar and the More sheet list every
item in nav order as one list, with no headings or dividers, and the command
menu no longer matches on or shows a group. AdminNavGroup, ADMIN_NAV_GROUPS,
groupSlug, itemsIn and every item's group field are gone.

The phone gets one menu. The top bar drops its hamburger and Sheet, and the
navCounts prop that only fed it; the tab bar's More lists every item. The tab
bar is Overview plus the next mobile "tab" items in nav order, four link tabs
at most, then More, so Settings becomes a More item and Automations takes its
tab.

The failed-jobs dot follows the item's badge instead of the old
Queues-in-More layout: a link tab shows admin-tab-<id>-failed while an item it
covers has a destructive badge, and More shows its dot only for such an item
that no link tab covers. With today's nav the dot sits on Automations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…dropped

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Review nit from Task 9: the component test asserted only the link, so a
dropped space or a doubled "by" would have passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…s and sheets

The settings layout, SettingsSection and SettingsSheet each need the same
beforeunload backstop, so it moves into hooks/use-leave-guard.ts.

SettingsSheet now guards the tab itself when no settings layout reports for
it. AssistantSection's Sheets render stand-alone on the AI settings tab, so
a reload with a Sheet open and edited used to discard the draft silently.
On the settings page the Sheet still reports to the layout, which owns the
one listener.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
A tab item past the four-tab cap opens from More, so its page must light More
and none of the link tabs: the tab bar test now opens such a page and checks
both. A guard in the nav test fails when the real nav gets a fifth mobile
"tab" item, which the cap would otherwise demote to More without a word.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…ings sections

useLeaveGuard is shared by the settings layout, sections and sheets, so
the pages moved off Settings keep the reload and close prompt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
navFor opens a role's folded item on its first permitted tab, but the
client provider rebuilt items from ADMIN_NAV by id, so the rewritten href
never reached the sidebar, the tab bar or the More sheet.

The shell now sends nav: { id, href, childIds }[] in place of navIds: the
href as navFor returned it and the ids of the item's tabs the user may
open (childrenOf). AdminNavProvider rebuilds each item from ADMIN_NAV by
id, applies the server's href and keeps only those tabs in the item's
children, so every consumer of useAdminNav sees the permission-aware nav
the server resolved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The "Go to" list now holds every page, then one entry per section tab
labelled "Page › Tab" (Automations › Request limits) that opens the tab's
own href, so a tab such as Request limits, Shares or Cost is a keystroke
away. A tab entry matches the search term against its combined label, so
"request" finds Request limits and "automations" finds the page and all of
its tabs.

Pages come before tabs so a page's name still selects the page itself
("set" opens Settings, not Discord › Settings). The menu lists each item's
children from the nav context, which keeps only the tabs the user may open;
Settings groups are not nav tabs and are not indexed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The Users, Invites and Affiliates nav items become one Members item, the
third after Overview and Assistant, with three tabs (Users, Invites,
Affiliates). It opens on Users at /admin/users, and `also` lights it on
/admin/invites and /admin/affiliates, which sit beside it. It is the mobile
tab after Overview, so the tab bar is Overview, Members, Rewind,
Automations and More: four link tabs, at the cap.

Its badge shows the affiliate applications waiting for a decision, else the
member count; the active-invites badge is dropped. Invites carries no
permission, so only an admin gets that tab, and Support gets Users and
Affiliates, with Members opening on Users.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
/admin/users, /admin/invites and /admin/affiliates move into a (members)
route group with git mv, so their URLs are unchanged. The group's layout
draws SectionTabs from childrenOf("members"): every tab for an admin, Users
and Affiliates for Support, none without a session. A page below a tab (a
user, a user's Rewind preview, an invite) has no strip, and the Affiliates
page keeps it on its own ?tab= tabs, which change only the query.

The tests that import the moved pages by path, the page-guard list and the
rewind-design-system skill's admin preview path follow the move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
claude added 16 commits November 2, 2026 22:50
The Rewind, Discord, AI, Automations and Members layouts were the same
body five times: getSession, childrenOf, SectionTabs, then the page.
sectionLayout(id) in components/admin/section-layout.tsx returns that
layout and reads the strip's label from the nav item, so the two cannot
drift; each route group's layout.tsx is now one line.

The shared behaviour tests move to the factory (all tabs for an admin,
permitted tabs only for a role, no strip without a session or below a
tab). The five near-identical layout tests give way to one table in
app/admin/__tests__/section-layouts.test.tsx, which also fails when a
nav item with tabs has no layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
…nings

The beforeunload probe ("would the browser ask before the tab closes?")
was spelled six ways across the settings tests, one of them with the
opposite sense. Move it to __tests__/helpers/leave-guard.ts and import it
from all six.

The settings-section test that calls requestSubmit() on a blocked form
let the save settle outside act(), which printed three warnings. Await
the submit inside act().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Feature settings moved to the feature's own page (Rewind, Discord, AI,
Automations) and /admin/settings now holds only connections, billing and
system, but the docs and a few strings still sent readers to "Admin >
Settings" for all of it. Name the page each thing lives on now:

- the docs and README (the conversation judge, assistant sources, the
  Discord bot and stat channels, the AI providers, Watchlist sync,
  Inactive members and Queues under Automations);
- the two lines the admin assistant and the Discord /assistant reply read
  out to admins (the assistant is off; no knowledge base), and the
  Rewind generation tab's pointer to the Rewind settings tab, with tests
  that pin the new words;
- comments that still described the one-page Settings.

docs/roles.md says what a Support user sees in the sidebar (Assistant,
Members and Audit log) without the dangling clause. CLAUDE.md's Admin
navigation adds that nav.ts also feeds the command menu's "Page > Tab"
entries and that a section's layout is sectionLayout(id).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The Affiliates page now carries two stacked tablists (the Members strip
and its own tabs) and was the only Members tab the axe route table
skipped. Add the route and its baseline row, kept in sorted order, so the
accessibility, landmarks and CSP specs cover it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Docs and assistant copy name the new admin locations; the unread
invites count and orphaned icons are gone; one sectionLayout factory
replaces five identical layouts; one leaveIsGuarded test helper; an
axe row for /admin/affiliates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
recheckConnections() returns every check getSettingsHealth() reports,
including Discord and Rewind (which inherits Tautulli's failure through
dependsOn). Neither section renders on /admin/settings any more, so the
toast could say "2 connections are not working" with one red section, or
report a failure with every visible section green.

The toast now counts only the checks behind the sections the page shows.
SECTION_HEALTH_KEYS in settings-groups.ts names them, keyed by the nav's own
section ids (a Record over SettingsNavStatuses, so adding or removing a
section with a status fails the build until it is mapped), and the page
reads the same constant for its "Sonarr, Radarr, Jellyfin" status.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The zone note in the open Rewind settings form linked to the Time zone
section on /admin/settings. A client-side navigation fires no beforeunload,
so clicking it mid-edit dropped the draft without asking (on main the same
click kept it, because every section sat on one page).

TimeZoneNote takes `editing`: the form passes it and the note reads "Change
the server time zone in Settings, System." as plain text; the summary keeps
the link, with its test id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The plan, the usability audit and CLAUDE.md list Rewind before Discord, but
ADMIN_NAV had them the other way round. Rewind moves above Discord in the
sidebar, the More sheet order and the command menu (all read ADMIN_NAV);
mobileTabs() is unchanged, because Discord is not a tab.

The nav test pins the order against ADMIN_NAV, the module's header diagram
follows, and the E2E sidebar walk lists Rewind first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
On an item called Members, "2 pending" read as two pending members. The
badge counts affiliate applications waiting for a decision, so it now says
"2 to review". The nav and shell tests that pin the text follow; no E2E spec
asserts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
The tabs of a section share its route group's layout, so app/admin/loading.tsx
no longer suspends again on a tab switch: a tab page without its own
loading.tsx showed nothing until the next page was ready.

Season, Playground, Shares and Settings (Rewind), Settings (Discord and AI),
Request limits and Queues (Automations) get a loading.tsx that re-exports
the generic admin loader. Next puts each loading.tsx in a Suspense boundary
around its folder's page and nested segments, so the new one under settings/
is the narrower boundary and takes precedence over the Activity skeleton of
ai/loading.tsx and discord/loading.tsx on those routes.

tab-loading.test.ts walks the tabs in ADMIN_NAV and fails for a tab whose
page has no loading.tsx beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
On a phone the Automations strip is wider than the screen, and TabsList
scrolls inside itself, so on /admin/queues the selected tab started
off-screen. SectionTabs now keeps a ref on the selected tab and calls
scrollIntoView({ block: "nearest", inline: "nearest" }) on it after mount
and whenever the selection changes. The strip renders nothing on a page
below a tab, and then nothing scrolls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
A bookmark or release note that links /admin/settings#rewind (or #ai,
#discord, #request-tiers, #channel-stats, #conversation-review,
#assistant) found no section there and landed on Connections without a
word. The hash handler in SettingsLayout now replaces the URL with the
page that section moved to, on load and on hashchange.

lib/admin/routes.ts exports movedSettingsHome(id), the SETTINGS_HOMES
entry or null (own keys only, so "constructor" is not a moved section),
and settingsSection reads it too, so the layout does not parse a path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Controls that moved to AI, Discord and Rewind settings were still sent
to "Settings" in a few places. The setup wizard's AI steps say Admin, AI,
Settings; the summary's hint no longer names one page, because a skipped
step may now live on Settings, AI or Discord. Season says the audience
switch is by hand in Rewind settings, the preflight row names the switch
that turned the copy off (AI features in AI settings, the mode in Rewind
settings), and the season-open job logs the same. The get_admin_settings
tool description, the JSDoc of the three loaders and the assistant's
canConfigure prop name the pages that use them.

The wizard's Discord step now revalidates /admin/discord/settings, where
the integration is shown, instead of /admin/settings.

Settings nav test: the Discord and AI rows that covered the Needs
attention glyph left with their sections, so a test covers incomplete and
degraded again, beside Not working (Tautulli was already unreachable).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
CLAUDE.md: the Admin navigation bullet says the tab strip is drawn by
the layout of the item's route group or segment, because Discord's is a
segment of its own. The File Organization tree shows what lives under
app/admin: the route groups (overview, members, rewind, ai,
automations) and the Discord segment, one line each, in nav order.

docs/operations.md, Lost key: re-entering the secrets covers the AI
provider keys too, which are on Admin, AI, Settings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Test all connections counts only the sections Settings shows; the
time-zone note links only outside the open form; Rewind before Discord;
Members badge says "to review"; loading skeletons for every section tab;
the strip scrolls its selected tab into view; old Settings anchors open
the section's new page; in-app copy and docs name the new locations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
Brings in the assistant skill sources (#513). Their settings block renders
on the AI Settings tab with the rest of the assistant sources, so the new
action revalidates /admin/ai/settings, its E2E spec opens that page, and
the executor copy and docs name the new location.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF
@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @mchestr's task in 42s —— View job


Review of #514: Admin navigation, ten items with section tabs

Verdict: no blocking issues found. I read the code only. I did not run tests or the build, so CI is the check for those.

What I checked

  • lib/admin/nav.ts: isActive, navFor and childrenOf are consistent with the documented behaviour. navFor rewrites an item's href to the first permitted tab, which is what the Support role needs. Admin nav: keep a folded item lit when a role may open only a later, nested tab #517 already tracks the nested-tab edge case.
  • sectionLayout: it throws on an unknown id when called at module load, so a renamed item fails early. The layout only draws tabs. The module and the PR description both say it is not an authorisation boundary.
  • New pages (rewind/ai/discord settings, request-limits): each calls assertAdmin() first, then connection(), then loads data. This matches the CLAUDE.md rule that every page keeps its own check.
  • Revalidation: the moved sections' actions now target the new paths: request tiers, Discord, the setup Discord save, Rewind settings, AI providers and judge settings. The remaining /admin/settings revalidations (Stripe, API tokens, servers) are for sections that stayed on Settings.
  • app/admin/settings/page.tsx: the old data loads are removed, so the page no longer fetches data it doesn't show. otherStatus now uses SECTION_HEALTH_KEYS, which removes the duplicated key list.

Non-blocking notes

  1. Health on the Settings page after moved-section saves. getSettingsHealth() is cached by tag. The Discord and AI save paths expire the tag (expireTag(CONNECTION_HEALTH_TAG) in actions/discord.ts). I did not confirm that every moved-section save that changes health does the same. Worth a quick look at the Rewind and AI settings actions, since those pages and the Overview tile now read health. This is probably fine given the per-request rendering you described.
  2. /security-review for .claude/skills/rewind-design-system/SKILL.md. CLAUDE.md requires one for any change under .claude/skills/**. The change is a single path token, so waiving it is reasonable, but it is your call as the description says.
  3. Dirty-draft loss on tab switch (Admin settings: guard client-side navigation away from a dirty section #515). This is a UX regression from the old one-page Settings, and it is tracked. Consider whether the inline editors in SettingsSection could warn on in-app navigation before the next release, since users now move between tabs more often.
  4. Unit Tests failure. The share-safe.property.test.ts failure is unrelated to this diff, and Random share-safe titles never contain one another (flaky property test) #519 is the fix. Merge after that lands and the job re-runs green.

Test coverage looks thorough: section layouts, tab loading states for every tab, the command menu, the "every role lights every tab" nav test, and per-page tests for the four new pages.
· branch feat/admin-nav-restructure

navFor opens a folded item at the role's first permitted tab, and isActive
matches that href by prefix, so a role allowed only a later tab nested
under the first tab's path would land on a page that never lights its
item. No role has that shape; this fails the moment one does. Also drops
"later the command menu" from the nav JSDoc, since it reads the nav now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF

mchestr commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Checked the six points against the code:

  1. Revalidation readers. getSettingsAuditMeta() is read by the five settings-hosting pages, each rendering only its own sections' LastSaved, so a save revalidates the one page that shows it. getRequestTierSettings() is also read by lib/data/community-stats.ts (Discord settings) and the MCP read tool; getSettingsHealth() by the Settings, Rewind-settings and AI-settings pages and the Overview tile. Those pages are uncached and render per request, and Next 16's client router keeps dynamic pages at staleTimes.dynamic = 0, so navigating to them after a save shows fresh data without an extra revalidatePath. Nothing is left stale.
  2. sectionLayout fail-fast. components/admin/__tests__/section-layout.test.tsx and app/admin/__tests__/tab-loading.test.ts both iterate every children-bearing ADMIN_NAV item, so a renamed id fails in jest before next build.
  3. Dirty-draft navigation. Tracked as Admin settings: guard client-side navigation away from a dirty section #515, linked from the description.
  4. First permitted tab vs. isActive. Added every role lights every tab it may open to lib/admin/__tests__/nav.test.ts (runs over ROLE_KEYS); the latent nested-tab case is Admin nav: keep a folded item lit when a role may open only a later, nested tab #517.
  5. Agreed; tab-loading.test.ts guards it.
  6. /security-review has not run here; the description now says so plainly for @mchestr to run or waive.

Nits: the nav.ts module JSDoc is fixed in the same commit; "Test all connections" already has exact-count tests in components/admin/settings/__tests__/settings-page-actions.test.tsx (success, one failure through the Tautulli→Rewind dependency, five failures, non-failure statuses).


Generated by Claude Code

mchestr commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Unit Tests is red on d1cb98e for a reason outside this PR. The failing suite is lib/wrapped/data/__tests__/share-safe.property.test.ts ("never carries a hidden title"), a fast-check property over random Rewind rows in lib/wrapped/data, which this PR does not touch. The shrunk counterexample (seed -1167034155) draws a first title ZqtoLocaa and a last title ZqtoLoca; with only the last hidden, the test's substring check matches the visible first title and reports a leak that is not one. It reproduces locally with that seed on main too, so it is a seed-dependent weakness of the generator, not a regression here. The other 961 suites pass.

Fix: #519 makes the generator reject title sets where one title contains another (reproduced with the seed, passes with it, soaked for 2,000 runs). I'll re-run the Unit Tests job once the rest of the workflow finishes; a different seed should pass.


Generated by Claude Code

Element.scrollIntoView on the selected tab also moved Chromium's
sequential-focus starting point, so on every page with a tab strip the
first Tab after load landed past the skip link and e2e/landmarks.spec.ts
failed for ten admin routes. The strip now moves its list's own
scrollLeft by the overflow of the selected tab's box, which brings an
off-screen tab into view on phones and leaves keyboard focus alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TLtULGdZXTe6L61jNKqRF

mchestr commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

E2E Tests was red for a reason that was this PR's, now fixed in d8f1727. Ten e2e/landmarks.spec.ts cases (every admin route with a section tab strip) failed at "first Tab focuses the skip link": the strip called scrollIntoView on its selected tab on mount, and in Chromium that also moves the sequential-focus starting point, so the first Tab landed past the skip link. Reproduced the browser behaviour in isolation with Playwright (a visible element that is scrollIntoView'd steals the Tab start), then changed the strip to move its list's own scrollLeft by the selected tab's overflow instead — same effect on phones, no focus side effect. Unit tests cover the three geometries and pin that scrollIntoView is never called.

The other failure in that run was the unrelated share-safe property seed (see above, fix in #519); the only flaky case (analytics.spec.ts "page 2 of AI activity keeps the member filter") passed on retry. Everything else — Build, Integration, Schema Drift, Rewind E2E, Rewind Performance, Image Smoke Test, Visual Diffs — was green.


Generated by Claude Code

mchestr commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

On note 1 (health cache after moved-section saves): getConnectionHealth() is the 5-minute unstable_cache of the service probes plus the ai entry (llmDisabled, active-provider count); discord, stripe and rewind health are read live on every call, so no Discord or Rewind settings save can leave them stale. The saves that change the cached part expire CONNECTION_HEALTH_TAG: server saves (admin-servers.ts), the AI master switch (setLLMDisabled) and the Discord token (discord.ts). The one that does not is saving an LLM provider (admin-llm-providers.ts), so the AI entry can read "not configured" for up to five minutes after the first provider is added — the same as on main, where that action revalidated /admin/settings without expiring the tag either. A one-line expireTag(CONNECTION_HEALTH_TAG) there is the fix; I'll fold it into the next code push on this branch if one is needed, otherwise it goes with #518 as a follow-up rather than restarting CI for it now.


Generated by Claude Code

@mchestr
mchestr merged commit 288314c into main Nov 3, 2026
22 checks passed
@mchestr
mchestr deleted the feat/admin-nav-restructure branch November 3, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants