refactor(server): split admin moderation, bot-activity and activity into capability routers (PR 2) #103

Merged
whitlocktech merged 1 commits from refactor/admin-router-split-2 into main 2026-07-28 00:12:04 +00:00
Member

What & why

PR 2 of 5 of the admin router domain split (docs/website/API_V2_PLAN.md § Phase 2). Carves 18 more routes out of admin.routes.js into one router file per business capability — in place, with every URL unchanged.

Router Routes Prefix Extra gate
moderation.router.js 15 /admin/moderation modAccess (admin + moderator) at router level
botActivity.router.js 2 /admin/bot-activity adminOnly per route
activity.router.js 1 /admin/activity none — staff-wide audit log
admin.routes.js (residual) 64 group root, mounted last unchanged

Two decisions worth a reviewer's eye:

  • modAccess moved to a router-level use; bot-activity's adminOnly deliberately did not. Moderation was already gated by a prefix mount (adminRouter.use('/moderation', modAccess)), so moderationRouter.use(modAccess) is the exact equivalent now that the router is mounted at a prefix (the PR 1 users case). Bot-activity's gate was per-route, and keeping it per-route is what preserves the per-route handler count in routes.guards.json — the only signal that would catch a dropped gate, since requireRole(...) returns an anonymous arrow and never appears by name.
  • /activity gets its own file, deviating from the plan's target tree, which parked it as a singleton inside dashboard.router.js (PR 4). Honouring the tree would have meant leaving one route in the residual file for two more PRs to satisfy a filename, and it is a genuinely separate capability: /activity is the staff audit log, /dashboard is a stats overview, /bot-activity is the botScore middleware's in-memory ban state. PR 4 now mounts dashboard + site-mode only; the plan doc is updated to match.

modAccess stays in the residual file — the /shard/* in-game staff operations still use it and do not move until PR 4.

No controller logic changed. No client, bot or Android change is needed: every URL is byte-identical.

How it was tested

Four zero-diff gates, all clean:

  • npm run routes:manifestroutes.manifest.json zero-diff (200 public + 2 internal)
  • routes.guards.json zero-diff — no route lost or gained a gate
  • npm run swaggerswagger-output.json zero-diff (198 operations, all 18 moved ones present at unchanged paths)
  • docs/website/api-route-inventory.json already in sync with the manifest

Plus:

  • cd website/server && npm test434 passing, 0 failing
  • Role gates verified identical to main. routes.guards.json cannot see this on its own: requireRole('admin') and requireRole('admin','moderator') both return an anonymous arrow, so attaching a gate with the wrong role set would be a zero-diff change there. So I patched requireRole to tag each arrow it returns with its role set, walked the live Express stack, and dumped the full middleware chain (mount-chain use gates included) for every moved route plus untouched controls (/admin/users, /admin/dashboard, /admin/shard/kick, /admin/posts). Ran it against this branch and against a main worktree — the two tables are identical, same handler counts and same role sets.

Docs PR: RunicGateway/docs#54.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code. I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why PR 2 of 5 of the admin router domain split (`docs/website/API_V2_PLAN.md` § Phase 2). Carves 18 more routes out of `admin.routes.js` into one router file per business capability — **in place, with every URL unchanged**. | Router | Routes | Prefix | Extra gate | |---|---|---|---| | `moderation.router.js` | 15 | `/admin/moderation` | `modAccess` (admin + moderator) at router level | | `botActivity.router.js` | 2 | `/admin/bot-activity` | `adminOnly` per route | | `activity.router.js` | 1 | `/admin/activity` | none — staff-wide audit log | | `admin.routes.js` (residual) | 64 | group root, mounted last | unchanged | Two decisions worth a reviewer's eye: - **`modAccess` moved to a router-level `use`; bot-activity's `adminOnly` deliberately did not.** Moderation was already gated by a *prefix* mount (`adminRouter.use('/moderation', modAccess)`), so `moderationRouter.use(modAccess)` is the exact equivalent now that the router is mounted at a prefix (the PR 1 `users` case). Bot-activity's gate was per-route, and keeping it per-route is what preserves the per-route handler count in `routes.guards.json` — the only signal that would catch a dropped gate, since `requireRole(...)` returns an anonymous arrow and never appears by name. - **`/activity` gets its own file**, deviating from the plan's target tree, which parked it as a singleton inside `dashboard.router.js` (PR 4). Honouring the tree would have meant leaving one route in the residual file for two more PRs to satisfy a filename, and it is a genuinely separate capability: `/activity` is the **staff audit log**, `/dashboard` is a stats overview, `/bot-activity` is the botScore middleware's in-memory ban state. PR 4 now mounts `dashboard` + `site-mode` only; the plan doc is updated to match. `modAccess` stays in the residual file — the `/shard/*` in-game staff operations still use it and do not move until PR 4. No controller logic changed. No client, bot or Android change is needed: every URL is byte-identical. ## How it was tested Four zero-diff gates, all clean: - `npm run routes:manifest` → **`routes.manifest.json` zero-diff** (200 public + 2 internal) - **`routes.guards.json` zero-diff** — no route lost or gained a gate - `npm run swagger` → **`swagger-output.json` zero-diff** (198 operations, all 18 moved ones present at unchanged paths) - `docs/website/api-route-inventory.json` already in sync with the manifest Plus: - `cd website/server && npm test` → **434 passing, 0 failing** - **Role gates verified identical to `main`.** `routes.guards.json` cannot see this on its own: `requireRole('admin')` and `requireRole('admin','moderator')` both return an anonymous arrow, so attaching a gate with the wrong role set would be a zero-diff change there. So I patched `requireRole` to tag each arrow it returns with its role set, walked the live Express stack, and dumped the full middleware chain (mount-chain `use` gates included) for every moved route plus untouched controls (`/admin/users`, `/admin/dashboard`, `/admin/shard/kick`, `/admin/posts`). Ran it against this branch and against a `main` worktree — **the two tables are identical**, same handler counts and same role sets. Docs PR: RunicGateway/docs#54. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-07-27 23:55:09 +00:00
refactor(server): split admin moderation, bot-activity and activity into capability routers
All checks were successful
PR Checks / bot-install (pull_request) Successful in 24s
PR Checks / client-build (pull_request) Successful in 32s
PR Checks / server-tests (pull_request) Successful in 46s
bd53a0b8a4
PR 2 of the domain split (docs/website/API_V2_PLAN.md § Phase 2). Carves 18 more
routes out of admin.routes.js into one router file per business capability,
in place, with every URL unchanged:

  moderation.router.js   (15)  /admin/moderation    modAccess at router level
  botActivity.router.js   (2)  /admin/bot-activity  adminOnly per route
  activity.router.js      (1)  /admin/activity      staff-wide, no extra gate

The residual admin.routes.js drops from 82 routes to 64.

Moderation was already gated by a prefix mount (adminRouter.use('/moderation',
modAccess)), so moderationRouter.use(modAccess) is the exact equivalent now that
the router is mounted at a prefix. Bot-activity's adminOnly was per-route and is
deliberately kept per-route: that is what holds the per-route handler count in
routes.guards.json, the only signal that would catch a dropped gate, since
requireRole(...) returns an anonymous arrow and never appears by name.

/activity gets its own file rather than waiting for dashboard.router.js in PR 4
— it is the staff audit log, a different capability from the dashboard's stats
overview and from the botScore middleware's in-memory ban state.

Acceptance:
  - routes.manifest.json  zero-diff (200 public + 2 internal)
  - routes.guards.json    zero-diff
  - swagger-output.json   zero-diff (198 operations)
  - api-route-inventory.json already in sync
  - 434 server tests green
  - role gates verified identical to main by reading the requireRole role sets
    off the live Express stack for every moved route plus untouched controls

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-28 00:11:57 +00:00
whitlocktech merged commit 493843241e into main 2026-07-28 00:12:04 +00:00
whitlocktech deleted branch refactor/admin-router-split-2 2026-07-28 00:12:05 +00:00
Sign in to join this conversation.
No description provided.