# 10 — Admin users & organizations UI (agent 10/20) **Owns:** `/admin/users` — users directory, company list with plan view/assign, platform staff role assignment. **Does not own:** Billing plans table (agent 9), Permissions panel (agent 8), staff middleware/model (agent 6), admin shell chrome (agent 7 — nav label only). **Gate:** `requirePlatformAdmin` (full admin / developer). `support_staff` is excluded. --- ## PROBLEM `/admin/users` was a thin client-filtered user list (no companies, no plan view, no staff role edits, no server pagination). Operators needed one place to manage orgs and staff after cutover. --- ## Delivered 1. **Users tab** — server-paginated (`limit`/`offset`, page size 25), search (`q`), staff-only filter, set-password invite + local dev tools preserved. 2. **Staff roles** — dialog → `PATCH /api/admin/users/{id}/staff-role` with `admin` | `developer` | `support_staff` | clear. Uses agent 6 API; self-change blocked server-side. 3. **Companies tab** — paginated list with active plan name, legacy/public/custom badge, credits remaining/total, “without plan” filter. 4. **Assign plan** — dialog lists all admin plans (ladder + legacy + custom) → `POST /api/admin/plans/assign`. 5. **API enrichments** — `GET /api/admin/users` returns `total`, `staff_role`, `resolved_role`, search/filters; `GET /api/admin/companies` returns `total`, `plan_id` / `plan_name` / `plan_is_*`, search. 6. **Fix A1 catalog** — Companies tab button + confirm → `POST /api/admin/companies/{id}/fix-catalog` (prompt repair + hygiene; flash counts `prompts`/`hashes`/`categories`). 7. **Clone catalog** — existing sandbox clone flow (separate from Fix A1). --- ## Source files | File | Intent | |------|--------| | `apps/web/src/routes/admin/users/+page.svelte` | Orgs UI (users + companies tabs) | | `apps/web/src/lib/admin-orgs.ts` | Client helpers + types | | `apps/api/internal/httpapi/admin_orgs_handlers.go` | Paginated list handlers | | `apps/api/internal/httpapi/admin_staff_handlers.go` | Staff role PATCH (agent 6) | | `apps/web/src/lib/components/AdminNav.svelte` | Nav label “Users & orgs” | --- ## API contract | Method | Path | Use | |--------|------|-----| | `GET` | `/api/admin/users?limit&offset&q&staff_only&active_only&inactive_only` | User directory | | `PATCH` | `/api/admin/users/{id}/staff-role` | Body `{ "staff_role": "admin"\|"developer"\|"support_staff"\|null }` | | `GET` | `/api/admin/companies?limit&offset&q&without_active_plan` | Companies + plan summary | | `GET` | `/api/admin/plans` | Plan picker | | `POST` | `/api/admin/plans/assign` | `{ company_id, plan_id }` | | `POST` | `/api/admin/emails/set-password` | Existing invite tooling | | `POST` | `/api/admin/companies/{id}/fix-catalog` | In-place Fix A1 / catalog hygiene (`confirm=true`) | | `POST` | `/api/admin/companies/{id}/clone-catalog` | Clone source catalog into admin sandbox | All under `RequireSession` + `RequirePlatformAdmin` (full admin only). --- ## Fix A1 catalog (`POST …/fix-catalog`) In-place hygiene for one company (prefer **Platform Demo**; A1 allowed with confirm). Never clears catalog, never mass-reprocesses, never uses A1 as a clone destination. **Body:** `{ "confirm": true, "backfill_categories"?: true, "reprocess_sample_limit"?: 25 }` **Sequence** (`processing.FixCompanyCatalog`): 1. Ensure `category_attributes` links (orphan purge only) 2. `RepairCompanyCategoryEnhancePrompts` — same map as `RepairA1DemoCategoryEnhancePrompts` / `go run ./cmd/repair-category-prompts -apply` 3. Re-apply BuiltIn `product_enhance` templates (when AI prompts service is wired) 4. `FixCatalogHygieneWithIDs` — weak enhance hashes, optional category backfill, description/meta hygiene 5. Sanitize `processed_attributes` 6. Return recommended reprocess `raw_product_id` sample (no auto reprocess) **Flash success** (`flash.admin.fixA1Success` — keys `admin.users.fixA1*`): | Flash param | `result` field | Also as | |-------------|----------------|---------| | `{prompts}` | `prompts` | `category_prompts_updated` | | `{hashes}` | `hashes` | `weak_hashes_cleared` | | `{categories}` | `categories` | `categories_backfilled` | | `{name}` | company name from UI row | `company_name` | UI: Companies tab wrench → confirm dialog → `fixAdminCompanyCatalog` in `$lib/admin-orgs.ts`. --- ## Security / performance - AuthZ enforced server-side; UI gate is UX only. - Cannot change own staff role (agent 6 handler). - Lists use SQL `LIMIT`/`OFFSET` + `COUNT(*)` — no full-table client filter. - Company plan join is a single query (no N+1). **ASSUMPTION:** `users.staff_role` migration (`029_staff_roles.sql`) applied. UI shows an info banner if PATCH returns 404/501. --- ## Manual verify 1. Platform admin → `/admin/users`. 2. Search users; toggle Staff only; page Next/Previous. 3. Assign staff role (admin / developer / support_staff); confirm badge. 4. Companies tab → see plan badge; filter Without plan; Assign plan (legacy + public). 5. Companies tab → Fix A1 catalog on Platform Demo (or A1 with confirm); success flash shows prompts/hashes/categories counts. 6. As `support_staff`, confirm `/admin/users` is forbidden (full-admin-only nav). --- ## Coordination - Agent 6: staff model + `PATCH …/staff-role` + `RequirePlatformAdmin` excluding support_staff. - Agent 9: billing assign UX remains; orgs page is the directory-focused assign path. - Agent 14: support_staff queue access after role grant from this UI.