Skip to content

Split Administrator into Bookings and a gated Management page (#64) - #67

Merged
pataniaeli merged 4 commits into
devfrom
feat/issue-64-management-page
Aug 30, 2026
Merged

Split Administrator into Bookings and a gated Management page (#64)#67
pataniaeli merged 4 commits into
devfrom
feat/issue-64-management-page

Conversation

@pataniaeli

Copy link
Copy Markdown
Collaborator

Closes #64.

Administrator had outgrown one tab strip: five tabs, one of which opened a second strip of five underneath it. Those inner five also have a different audience — they configure the system rather than run the week's bookings.

What changed

Management is a new root page. Advanced Settings' sub-tabs (Users, Bodies, Audit, Archive, Other Settings) are now its main tabs; the five components moved as-is, so they show up as renames. advanced-settings-tab.tsx is gone.

It is gated to the four high-access roles — EVP, VP of Operational Affairs, Digital Innovation Manager, Information Manager — through a new ManagementGuard, built the same way as AdminGuard: the identity is already resolved on the server and shipped with the document, so the check is synchronous. That set is exactly the existing ROLE_EDITORS, which is the point — those four already decide who is an admin at all, so everything else on the page is downstream of a power they hold anyway. Comptroller and Digital Innovation Project Member keep everything else.

Administrator is renamed Bookings, and moved to /bookings. The sidebar, the page heading, ManagementGuard's fallback, and the four cross-directory imports of time-picker / booking-modal all follow. next.config.ts redirects /administrator/bookings so year-old bookmarks keep resolving — 307 rather than 308, since a permanent redirect is cached by the browser indefinitely and there is nothing here worth making that hard to walk back.

Membership requests leave Pending Actions for admins who cannot resolve them. They are worked on Management > Users, so an admin outside the four roles was being shown a task with nowhere to go. fetchPendingActions() takes the caller's admin_role and skips the query outright rather than fetching rows to discard; omitting the role reads as "not a management role", so a caller that forgets to pass it hides the actions rather than leaking them. Its now parameter moved into that same options object — nothing was passing it.

Supporting changes

  • lib/admin-roles.ts holds the role lists in one place. It has no imports, so client components can share it with the API route; ROLE_EDITORS was the same four names written twice.
  • ShellIdentity carries adminRole, already present in the row resolveShellIdentity() reads.
  • The booking forms' "create a semester in Advanced Settings" hint now points at Management → Other Settings.

Scope

ManagementGuard governs what the dashboard renders. The endpoints behind these tabs still authorize as they did before — role editing is restricted to these same four roles, the rest accept any admin — so this is not a regression, but it is not enforcement either. Narrowing them is a separate change, with the caveat that GET /api/administrator/bodies must stay open: the Bookings page uses it for its body picker.

Verification

npx tsc --noEmit clean and npm run build succeeds. Against a dev server, /administrator 307s to /bookings, and /bookings and /management both resolve (307 to login when signed out). npm run lint reports only the errors the moved files already had on dev. The authenticated admin views were not exercised in a browser.

Note

This branch and the #63/#65 branch both touch lib/pending-actions.ts — different hunks, but whichever merges second is worth a rebase check.

🤖 Generated with Claude Code

pataniaeli and others added 2 commits August 30, 2026 17:51
)

Administrator had outgrown one tab strip: five tabs, one of which opened a
second strip of five underneath it. Those five inner tabs also have a different
audience -- they configure the system rather than run the week's bookings.

Advanced Settings' sub-tabs (Users, Bodies, Audit, Archive, Other Settings) are
now the main tabs of a new root page, Management, reachable only by the four
high-access roles: EVP, VP of Operational Affairs, Digital Innovation Manager
and Information Manager. That is the set already allowed to grant and revoke
admin roles, so everything else on the page is downstream of a power they hold
anyway; the other admin roles keep everything else.

Administrator is renamed Bookings, in the sidebar and on the page itself. Its
URL stays /administrator so existing links keep working.

Supporting changes:
  - lib/admin-roles.ts holds the role lists in one place. It has no imports, so
    the client components can share it with the API route; ROLE_EDITORS in
    app/api/administrator/users/route.ts and users-tab.tsx were the same four
    names written twice, and are now that constant.
  - ShellIdentity carries adminRole, already present in the row it reads, so
    ManagementGuard resolves synchronously like AdminGuard does.
  - The booking forms' "create a semester in Advanced Settings" hint now points
    at Management -> Other Settings.

ManagementGuard governs what the dashboard renders. The endpoints behind these
tabs still authorize as they did before -- role editing is restricted to these
same four roles, the rest accept any admin -- so narrowing them is a separate
change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…64)

Two follow-ups to the split.

The route matches the name now: /administrator is /bookings, and the sidebar,
ManagementGuard's fallback and the four cross-directory imports of its
time-picker and booking-modal all point at the new path. next.config.ts
redirects the old URL so year-old bookmarks keep resolving -- 307 rather than
308, because a permanent redirect is cached by the browser indefinitely and
there is nothing here worth making that hard to walk back.

Membership requests no longer appear in Pending Actions for admins who cannot
resolve them. They are worked on Management > Users, so an admin outside
MANAGEMENT_ROLES was being shown a task with nowhere to go. fetchPendingActions
takes the caller's admin_role and skips the query outright rather than fetching
rows to discard; omitting the role reads as "not a management role", so a caller
that forgets to pass it hides the actions rather than leaking them.

Its `now` parameter moved into that same options object. Nothing passed it, and
two positional arguments where the second is a clock would have read badly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chambers Ready Ready Preview Aug 30, 2026 10:28pm

… admin

ManagementGuard decides what the dashboard renders. The endpoints behind those
tabs were still authorizing on `app_metadata.is_admin` alone, so a Comptroller
or Digital Innovation Project Member could read the user list, the audit log and
the archive, edit app settings and bodies, resolve membership requests, and
invite users -- by calling the routes directly. The gate was a rendering
decision.

Each of those handlers now runs isManagementRole() against the live admin_role
that getAuthedUserWithLiveRoles() resolves from the users row, and answers 403.
Users (GET/POST/PATCH), user memberships, resend-invite, membership requests,
audit logs, archive and settings are gated whole, because every caller of each
is under /management.

Bodies is the exception: only POST and PATCH are gated. GET stays open to any
admin because the Bookings page reads it for its body picker, and that page is
open to every admin by design.

POST /api/administrator/users is a role-grant path, not just a read: it accepts
an admin_role and writes it to both the users row and the auth metadata, so an
ungated Comptroller could have invited themselves a second account as Executive
Vice President.

The membership-request routes are the pair 54f5b7a stopped surfacing in Pending
Actions for admins outside MANAGEMENT_ROLES. That hid the task; this closes the
endpoints it pointed at.

With the route-wide check in place, the ROLE_EDITORS branch inside PATCH is
unreachable, so it and the const come out. Role editing is unchanged; the
is_active and full_name branches are now narrowed too, which matches the one
place they are called from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pataniaeli pataniaeli self-assigned this Aug 30, 2026
@pataniaeli pataniaeli linked an issue Aug 30, 2026 that may be closed by this pull request
@pataniaeli
pataniaeli merged commit 0a6889e into dev Aug 30, 2026
4 checks passed
@pataniaeli
pataniaeli deleted the feat/issue-64-management-page branch August 30, 2026 22:36
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.

Split the administrator tab

1 participant