docs(website): record the PR 3 admin router split (posts, uploads, wiki, pages)

Adds a "PR 3 — as landed" section to API_V2_PLAN.md and ticks the sequencing
list. 31 routes extracted, 33 left in admin.routes.js; all four zero-diff gates
came back clean and 434 server tests passed.

Findings carried forward:

- The residual 33 is exactly PR 4's list, so admin.routes.js is deleted by
  PR 4 rather than PR 5.
- First shared module in the split: the multer config, because POST
  /posts/upload and POST /uploads no longer live in the same file.
- POST /uploads keeps its Admin · Posts swagger tag — retagging is a real
  OpenAPI diff and does not belong in a route-move PR.
- The wiki router has load-bearing intra-file route order (/categories and
  /tags ahead of /:slug) that no gate can catch, because the manifest sorts
  its entries. Verified by introspecting the built router stack instead.

BACKEND_DESIGN.md §2 gets the four new routers plus imageUpload.js in the
folder tree, and §4 notes that the content capabilities add no gate beyond
staffOnly.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-27 19:24:58 -05:00
parent d034c6f673
commit d515d42b7c
2 changed files with 67 additions and 10 deletions

View File

@@ -376,6 +376,51 @@ Notes:
- **`modAccess` stays in the residual file** — the `/shard/*` in-game staff operations still use it - **`modAccess` stays in the residual file** — the `/shard/*` in-game staff operations still use it
and do not move until PR 4. Its comment there was retargeted rather than deleted. and do not move until PR 4. Its comment there was retargeted rather than deleted.
### PR 3 — as landed
`posts`, `uploads`, `wiki` and `pages` — 31 routes, leaving 33 in the residual file. The content tier,
and the first split PR where no gate moved at all: all four capabilities are editor-tier, so the shared
`staffOnly` in `admin/index.js` is their whole gate.
| Router | Routes | Prefix | Extra gate |
|---|---|---|---|
| `posts.router.js` | 9 | `/admin/posts` | none — editor tier |
| `uploads.router.js` | 1 | `/admin/uploads` | none — editor tier |
| `wiki.router.js` | 14 | `/admin/wiki` | none — editor tier |
| `pages.router.js` | 7 | `/admin/pages` | none — editor tier |
| `admin.routes.js` (residual) | 33 | group root, mounted last | unchanged |
All four gates zero-diff: `routes.manifest.json` (200 public + 2 internal), `routes.guards.json`,
`swagger-output.json` (198 operations), and `docs/website/api-route-inventory.json` was already in
sync. 434 server tests green.
Notes:
- **The residual 33 is exactly PR 4's list** — `shard` 16, `email` 6, `uo-link` 5, `settings` 2,
`discord-bot` 2, `dashboard` 1, `site-mode` 1. So `admin.routes.js` is deleted by PR 4, one PR
earlier than the sequencing list implies, and PR 5 touches only `public/*`, `player/*` and `auth/*`.
- **A shared module was unavoidable here, and it is the first one in the split.** The multer config
(upload dir, mimetype→extension allowlist, 8 MB cap) was defined inline in `admin.routes.js` and used
by *two* routes that this PR puts in different files: `POST /posts/upload` (→ `{image_url}`) and
`POST /uploads` (→ `{url}`). It moved to `admin/imageUpload.js` rather than being duplicated —
duplicating a security allowlist is how the two copies drift. It stays in `admin/` deliberately:
`UPLOAD_DIR` is resolved `__dirname`-relative, so relocating the file would silently repoint the
upload directory. Guard freshness is unaffected — multer's middleware is named `multerMiddleware`
wherever it is constructed, so `routes.guards.json` did not move.
- **`POST /uploads` keeps its `Admin · Posts` swagger tag**, which now disagrees with its filename. The
acceptance criterion is a byte-identical spec, so retagging is a real OpenAPI diff and does not
belong in a route-move PR. Same call as PR 1's `/shard/*` tag mismatch: fix tags in a PR that is
*about* tags.
- **The wiki router is the first one with load-bearing intra-file route order.** `/categories` and
`/tags` are literal paths that must stay ahead of `/:slug`, or `GET /admin/wiki/categories` gets
dispatched as a page whose slug is "categories". **The manifest cannot catch this — it sorts its
entries, so a reordering is invisible in all three gates.** It was verified separately by
introspecting the built router stack and asserting the last literal layer precedes the first `/:slug`
layer. Any future PR moving `/:slug`-style routes needs the same explicit check.
- **`/admin/pages` (CMS page builder) and `/admin/shard/pages` (in-game help-page queue) are unrelated
capabilities that read alike** — the latter stays with `shard` in PR 4. Same trap as PR 2's
`activity` / `dashboard` / `bot-activity` trio.
### The swagger path-normalization prerequisite (landed before PR 1) ### The swagger path-normalization prerequisite (landed before PR 1)
swagger-autogen builds a path by string-concatenating the mount prefix with the route argument, so a swagger-autogen builds a path by string-concatenating the mount prefix with the route argument, so a
@@ -485,9 +530,10 @@ deliberate `+1` in the manifest — which is exactly the mechanism working as de
as a `report-to` group on the enforced policy. as a `report-to` group on the enforced policy.
4. **PR 1 — admin:** `users`, `account`, `invites`, `auth` (providers). ✅ landed 4. **PR 1 — admin:** `users`, `account`, `invites`, `auth` (providers). ✅ landed
5. **PR 2 — admin:** `moderation`, `bot-activity`, `activity`. ✅ landed 5. **PR 2 — admin:** `moderation`, `bot-activity`, `activity`. ✅ landed
6. **PR 3 — admin (content):** `posts`, `pages`, `wiki`, `uploads`. 6. **PR 3 — admin (content):** `posts`, `pages`, `wiki`, `uploads`. ✅ landed
7. **PR 4 — admin (ops/config):** `shard`, `uo-link`, `email`, `discord-bot`, `settings`, `site-mode`, 7. **PR 4 — admin (ops/config):** `shard`, `uo-link`, `email`, `discord-bot`, `settings`, `site-mode`,
`dashboard`. (`activity` went with PR 2 — see § PR 2 — as landed.) `dashboard`. (`activity` went with PR 2 — see § PR 2 — as landed.) **This is the whole residual
file** — `admin.routes.js` is deleted here, not by PR 5.
8. **PR 5 — `public/*` + `player/*`** (and the residual `auth/*` grouping). 8. **PR 5 — `public/*` + `player/*`** (and the residual `auth/*` grouping).
Each PR: **zero-line diff in `routes.manifest.json`**, server tests green Each PR: **zero-line diff in `routes.manifest.json`**, server tests green

View File

@@ -34,10 +34,10 @@ Skeleton from the spec, with a small number of justified additions marked **(+)*
> every URL unchanged**. This section and §4 get updated as each split PR lands. See > every URL unchanged**. This section and §4 get updated as each split PR lands. See
> [API_V2_PLAN.md](./API_V2_PLAN.md) § Phase 2. > [API_V2_PLAN.md](./API_V2_PLAN.md) § Phase 2.
> >
> **Landed so far:** admin `users`, `account`, `invites`, `auth/providers` (PR 1, 28 routes) and > **Landed so far:** admin `users`, `account`, `invites`, `auth/providers` (PR 1, 28 routes),
> `moderation`, `bot-activity`, `activity` (PR 2, 18 routes) now live in their own routers under > `moderation`, `bot-activity`, `activity` (PR 2, 18 routes) and `posts`, `uploads`, `wiki`, `pages`
> `admin/`, behind a new `admin/index.js`. The remaining 64 admin routes are still in > (PR 3, 31 routes) now live in their own routers under `admin/`, behind a new `admin/index.js`. The
> `admin.routes.js`, and `public/` and `player/` are untouched. > remaining 33 admin routes are still in `admin.routes.js`, and `public/` and `player/` are untouched.
> >
> "Every URL unchanged" is enforced mechanically, not by review: `server/scripts/routeManifest.js` > "Every URL unchanged" is enforced mechanically, not by review: `server/scripts/routeManifest.js`
> (`npm run routes:manifest`) walks the live Express stack and writes the sorted > (`npm run routes:manifest`) walks the live Express stack and writes the sorted
@@ -74,7 +74,16 @@ server/
botActivity.router.js (2) /admin/bot-activity — adminOnly botActivity.router.js (2) /admin/bot-activity — adminOnly
activity.router.js (1) /admin/activity — staff-wide activity.router.js (1) /admin/activity — staff-wide
audit log, no extra gate audit log, no extra gate
admin.routes.js (64) everything not yet split, mounted posts.router.js (9) /admin/posts — editor tier, no
gate beyond staffOnly
uploads.router.js (1) /admin/uploads — rich-text editor
image upload
wiki.router.js (14) /admin/wiki — pages, revisions,
categories, tags
pages.router.js (7) /admin/pages — CMS page builder
imageUpload.js shared multer config for the two
upload routes above (not a router)
admin.routes.js (33) everything not yet split, mounted
last at the group root; goes away last at the group root; goes away
when the final split PR lands when the final split PR lands
admin.controller.js + the per-capability controllers admin.controller.js + the per-capability controllers
@@ -467,9 +476,11 @@ Public content GETs pass through the **siteMode** gate (§5).
`admin/index.js` applies the shared gate and mounts each capability router at the prefix it owns; `admin/index.js` applies the shared gate and mounts each capability router at the prefix it owns;
`users`, `invites`, `auth/providers` and `bot-activity` add `adminOnly` on top, and `moderation` adds `users`, `invites`, `auth/providers` and `bot-activity` add `adminOnly` on top, and `moderation` adds
`modAccess` (admin + moderator, so editors are excluded). Routes not yet extracted still live `modAccess` (admin + moderator, so editors are excluded). The content capabilities — `posts`,
in `admin.routes.js`, mounted last at the group root. The URLs below are unaffected by which file a `uploads`, `wiki`, `pages` — add nothing: managing content is the editor tier's job, so `staffOnly` is
route currently sits in — that is the property the route manifest freezes. the whole gate. Routes not yet extracted still live in `admin.routes.js`, mounted last at the group
root. The URLs below are unaffected by which file a route currently sits in — that is the property the
route manifest freezes.
| Method | Path | Purpose | | Method | Path | Purpose |
|---|---|---| |---|---|---|
| GET | `/dashboard` | current mode, last change time + who, content counts, recent activity | | GET | `/dashboard` | current mode, last change time + who, content counts, recent activity |