# M10 — UI: Admin CRUD Pages > Fresh Claude Code agent prompt. M09 must be complete and committed. > Estimated effort: large (lots of CRUD). ## Mission Add UI pages for every admin domain: Subnets (manual blocks), Allowlist, Policies (with the threshold matrix editor), Reporters, Consumers, Tokens (raw token shown once), Categories (with decay-curve preview). Wire manual block/allowlist actions into the IP detail page. **Audit log UI and Settings page are still M12.** ## Before you start 1. Verify M09: ```bash git log --oneline -9 cd api && composer test && cd .. cd ui && composer test && cd .. ``` 2. Read `SPEC.md` §6 (every Admin endpoint), §7 (every Page in the Pages section), §8 (RBAC matrix — confirm which roles can do what). 3. The api endpoints needed mostly exist already (M04 reporters/consumers/tokens, M06 manual_blocks/allowlist, M07 policies). The new ones this milestone adds are categories CRUD. ## Tasks ### 1. API: Categories admin endpoints In `api/src/Application/Admin/CategoriesController.php`: - `GET /api/v1/admin/categories` - `POST /api/v1/admin/categories` — body `{slug, name, description, decay_function: "linear"|"exponential", decay_param: number, is_active: bool}`. Slug must be unique, lowercase, kebab-case. - `PATCH /api/v1/admin/categories/{id}` - `DELETE /api/v1/admin/categories/{id}` — refuse if `policy_category_thresholds` references it OR `reports.category_id` references it (409 with usage info). Soft-delete via `is_active=false` is preferred when in use. RBAC: `Admin` for write, `Viewer` for read. ### 2. UI: AdminClient extensions Add methods for everything the new pages need: list/get/create/update/delete on policies, reporters, consumers, tokens, categories, manual_blocks, allowlist. Use the typed exceptions established in M08. ### 3. UI Pages Build under `ui/resources/views/pages/`: - `subnets/index.twig` — list of manual blocks with kind=`subnet`. Create form: CIDR input, reason, optional expiry. Single-IP manual blocks are still managed (use the same controller, separate "single IPs" tab if it helps clarity). - `manual-blocks/index.twig` — combined list (or two tabs: IPs and Subnets). Recommended: one page with a `kind` filter pill at the top. - `allowlist/index.twig` — same shape as manual-blocks but for allowlist entries. No expiry field. - `policies/index.twig` — list view. - `policies/edit.twig` — the threshold matrix editor: rows = categories, columns = ["threshold"]. Numeric input per category; "remove from policy" button to delete the threshold row. Below the matrix: a live "preview" panel that calls `GET /api/v1/admin/policies/{id}/preview` and shows the resulting count + first 50 entries. Debounce the preview (e.g., htmx + 500ms hx-trigger). - `reporters/index.twig`, `reporters/edit.twig` — list and edit. Show `trust_weight` prominently; a small explainer ("0–2.0; 1.0 default; affects how heavily this reporter influences scores"). - `consumers/index.twig`, `consumers/edit.twig` — list and edit. Show assigned policy with a dropdown. - `tokens/index.twig` — list of all non-service tokens. Columns: kind, prefix, target (reporter/consumer name or "admin role"), expires_at, revoked_at, last_used_at. Actions: revoke. Top of page: "+ New token" button → modal with kind selector and conditional fields. On creation: success modal showing the raw token in a monospace block, a copy-to-clipboard button, and a clear "this is the only time you'll see this token" warning. Modal must require explicit dismissal. - `categories/index.twig`, `categories/edit.twig` — list and edit. Edit page includes: - decay_function radio (linear / exponential). - decay_param numeric input with appropriate unit label ("days to zero" / "half-life days"). - Live preview chart: a small SVG (or Chart.js) showing the decay curve over 0–60 days. Pure client-side math; no API call. Must update reactively as the user changes inputs. ### 4. IP Detail action buttons On `ui/resources/views/pages/ips/detail.twig`, add (visible per RBAC): - "Add to allowlist" button (Operator+) — opens a modal with a reason field, POSTs to `/api/v1/admin/allowlist`. - "Manually block" button (Operator+) — opens a modal with a reason field and an optional expiry, POSTs to `/api/v1/admin/manual-blocks`. - If the IP is already manually blocked: "Remove manual block" button. - If the IP is on the allowlist: "Remove from allowlist" button. After any mutation, re-render the page (or do an htmx swap) so status reflects immediately. The CidrEvaluator from M06 should already invalidate; just make sure the api round-trip retrieves fresh data. ### 5. RBAC enforcement (UI side) - Hide buttons/links the user can't use. - Always treat UI hiding as cosmetic; the api enforces. Test that an Operator clicking through to a forbidden URL gets a friendly error page rather than an exception. ### 6. Sidebar updates Wire each section as a working link. Active highlighting per current section. ### 7. Confirmation modals All destructive actions (delete reporter, delete consumer, revoke token, delete category, delete policy, remove manual block, remove allowlist entry) require a confirmation modal. Modal pattern: small Twig partial reused everywhere, Alpine for show/hide, HTML `