Files
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

252 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 — Current inventory: admin, roles, plans (A1/legacy), support
**Agent:** 2/20 · **Mode:** read-only inventory (docs only)
**Tools:** codehelper `project_context` (short) → `kickoff``investigate`/`query`/`context`; Read/Grep/Glob fallback where workspace file tools were unavailable.
**Desired target (from parent brief / sibling agents 36):**
| Persona / package | Desired access |
|-------------------|----------------|
| **Legacy plan (A1 + legacy-marked)** | Limited product nav only (Dashboard, Products, Feeds, Export Feeds, Categories, Attributes, Standard Fields, Billing, Settings). **No** Background Tasks / `processing.monitor`, stores, marketing extras |
| **Platform admin + developer** | Full `/admin` + full ops capabilities |
| **support_staff** | Support ticket queue / assign / reply only — **not** billing/plan mutation |
---
## 1. Executive summary
Today the platform has a **binary** staff gate (`users.is_platform_admin`) and **company membership** roles (`admin` | `member`, plus API-key role `api`). There is **no** `staff_role` / `support_staff` / `developer` enum in code or schema (`is_legacy`, `staff_role`, `support_staff`, `developer`**zero** matches).
**A1** is modeled as a **client deal / custom package** (non-public plan name → `IsCustomPackage` → default **all features ON**), not as a limited “legacy nav” profile. Demo tenant **Local Demo Co** (exA1 Slovenija) is on public **Enterprise** (`is_custom=true`), which also resolves to all-ON features.
Support desk exists (tenant + admin APIs, assignee field, agent replies) but **admin support routes require full platform admin**. No CSAT/rating fields found under `apps/api/internal/support`.
---
## 2. Role model (what exists)
### 2.1 Platform staff — single boolean
| Symbol / field | Path | Notes |
|----------------|------|-------|
| `User.IsPlatformAdmin` | `apps/api/internal/auth/service.go` (`User`) | JSON `is_platform_admin` |
| `Service.IsPlatformAdmin` | `apps/api/internal/auth/invites.go:209` | DB: `SELECT is_platform_admin FROM users WHERE id = $1 AND is_active` |
| `RequirePlatformAdmin` | `apps/api/internal/httpapi/middleware.go:245` | Session + DB check; **not** a client claim |
| `checkPlatformAdmin` | `apps/api/internal/httpapi/middleware.go:262` | Test hook `testPlatformAdmin` or `Auth.IsPlatformAdmin` |
| `requirePlatformAdmin` | `apps/web/src/lib/admin-gate.ts:8` | Client gate via `GET /api/auth/me` |
| `authSession.isPlatformAdmin` | `apps/web/src/lib/auth-session.svelte.ts` | UI session mirror |
| `applyPlatformAdmins` | `apps/api/cmd/migrator/admins.go:13` | Legacy MySQL `admin_users``users.is_platform_admin` |
| `seed-demo` | `apps/api/cmd/seed-demo/main.go` | Forces demo user `is_platform_admin = true` |
**Missing vs desired:** `admin` / `developer` / `support_staff` staff roles; APIs to list/assign staff roles; middleware that allows support-only routes without full platform admin.
### 2.2 Company membership roles
| Concept | Where | Values |
|---------|-------|--------|
| Membership `role` | `memberships.role` (auth/company handlers) | `admin`, `member` (invite normalize rejects `owner`) |
| `CompanyAdminAllowed` | `middleware.go:43` | `admin` \|\| `api` |
| `requireCompanyAdmin` | `middleware.go:48` | 403 `"admin required"` |
| `allowCompanyAdminOrPlatform` | `middleware.go:58` | Company admin **or** platform admin (cutover: all-member tenants) |
| Team promote/demote | `company_handlers.go`, `company_member_role_test.go` | Last-admin guards |
Orthogonal to plan features (also noted in `docs/plan-permissions/02-plans-permissions-current.md` § membership).
### 2.3 Support message “agent” role (not staff RBAC)
Ticket messages use `author_role``user` | `agent` | `system` (`apps/web/src/lib/support/admin-api.ts`). `ReplyAsAgent` (`support/tickets.go:325`) writes `author_role='agent'`. That is **message authorship**, not a user staff role.
---
## 3. Admin panel — web routes
All under `apps/web/src/routes/admin/`. Pages call `requirePlatformAdmin()` (except layout which only shows chrome when `me.user.is_platform_admin`).
| Route | File | In `AdminNav`? | Purpose |
|-------|------|----------------|---------|
| `/admin` | `+page.svelte` | Overview | Analytics summary + tool cards |
| `/admin/users` | `users/+page.svelte` | Yes | User list, set-password emails, **dev** set-password / impersonate |
| `/admin/analytics` | `analytics/+page.svelte` | Yes | Tokens / credits / jobs |
| `/admin/billing` | `billing/+page.svelte` | Yes | Plans, assign, credits, cycles + **PlanPermissionsPanel** |
| `/admin/support` | `support/+page.svelte` | Yes | Ticket queue |
| `/admin/support/[id]` | `support/[id]/+page.svelte` | (via Support) | Ticket detail / reply |
| `/admin/stuck-products` | `stuck-products/+page.svelte` | Yes | Stuck jobs |
| `/admin/settings` | `settings/+page.svelte` | Yes | Platform settings |
| `/admin/tasks-cleanup` | `tasks-cleanup/+page.svelte` | No (overview card only) | Same stuck cleanup |
| `/admin/logs` | `logs/+page.svelte` | No (deep link; nav comment: no logs API) | Placeholder |
| `/admin/bootstrap` | `bootstrap/+page.svelte` | No | Check admin flag; cannot create admins via API |
| `/admin/migrate-organizations` | `migrate-organizations/+page.svelte` | No | Migration helper UI |
**Nav chrome:** `AdminNav``apps/web/src/lib/components/AdminNav.svelte` (`menuItems`).
**Layout wiring:** `apps/web/src/routes/+layout.svelte``showAdminNav = Boolean(me?.user?.is_platform_admin)`; main app `Nav` gets `showAdmin` for “Platform admin” link (`adminOnly: true`).
---
## 4. Admin panel — API routes
Mounted in `Server.Router``apps/api/internal/httpapi/server.go:294325`:
```
/api/admin/* → RequireSession + RequirePlatformAdmin
```
| Method | Path | Handler (approx) |
|--------|------|------------------|
| GET | `/users` | `handleAdminListUsers` |
| POST | `/users/{id}/dev-password` | `handleAdminDevSetPassword` (non-prod) |
| POST | `/users/{id}/impersonate` | `handleAdminDevImpersonate` (non-prod) |
| GET | `/companies` | `handleAdminListCompanies` |
| GET | `/readiness` | `handleAdminReadiness` |
| GET | `/analytics` | `handleAdminAnalytics` |
| GET | `/jobs` | `handleAdminListJobs` |
| POST | `/jobs/stuck-cleanup` | `handleAdminStuckCleanup` |
| GET/PUT | `/settings` | platform settings |
| POST | `/settings/mail/test` | mail probe |
| GET/POST | `/plans` | list / upsert |
| GET/PUT | `/plans/{planID}/features` | plan feature overrides |
| POST | `/plans/{planID}/features/enable-all` | convenience |
| POST | `/plans/{planID}/features/disable-all` | convenience |
| GET/PUT | `/feature-gates` | global gates |
| PUT | `/feature-gates/sections/{section}` | section toggle |
| POST | `/plans/assign` | assign plan to company |
| POST | `/credits` | add credits |
| POST | `/billing/run-cycles` | renewals |
| POST | `/emails/set-password` | cutover emails |
| GET | `/support/tickets` | admin queue |
| GET | `/support/tickets/{id}` | admin get |
| POST | `/support/tickets/{id}/messages` | `ReplyAsAgent` |
| PATCH | `/support/tickets/{id}` | `UpdateAdmin` (status/priority/assignee) |
**Dev-only staff tooling** (not a “developer” role): `admin_dev_handlers.go` — blocked when `Config.IsProduction()`.
---
## 5. PlanPermissionsPanel & plan features
| Piece | Path / symbol |
|-------|----------------|
| UI panel | `apps/web/src/lib/components/admin/PlanPermissionsPanel.svelte` |
| Host page | `/admin/billing` Permissions tab — `billing/+page.svelte` |
| Client API | `apps/web/src/lib/admin-plan-permissions.ts` (`listAdminPlansWithFeatures`, `loadFeatureGates`, enable/disable-all, …) |
| Catalog | `apps/web/src/lib/plan-feature-catalog.ts` (`isDefaultPublicPlanName`, sections) |
| Prior design doc | `docs/plan-permissions/08-admin-ui.md` |
**Package kind badges** (panel `$derived packageKind`):
- `default` — public ladder name + `!is_custom`
- `ladder_custom` — public ladder + `is_custom` (Enterprise)
- `custom``is_custom`
- `deal` — non-ladder name without treating as custom badge path
Stub mode when feature APIs return 404/501 (`isPlanPermissionsApiUnavailable`).
Backend resolve path: `DefaultPlanFeatures``PlanAllowsFeature``ResolveEffectiveFeatures` (`apps/api/internal/billing/plan_features.go`). Custom packages default **all catalog keys ON**.
---
## 6. Plans: `is_custom`, public ladder, A1 / “legacy”
### 6.1 Public vs client deals
| Symbol | Path | Behavior |
|--------|------|----------|
| `IsPublicProductPlan` | `billing/service.go:485` | `free\|starter\|growth\|business\|enterprise` only |
| `ListPublicPlans` | `billing/service.go:600` | Marketing / self-serve; **excludes** client deals |
| Admin `ListPlans` | via `/api/admin/plans` | **All** plan rows (incl. A1, Merkur, …) |
| `EnsureDefaultPlans` | `service.go:496` | Syncs public ladder only; named deals untouched |
Comments explicitly name **A1**, Merkur trial, legacy Basic/Professional as non-public client deals.
### 6.2 `IsCustomPackage` / `is_custom`
| Symbol | Path | Rule |
|--------|------|------|
| `IsCustomPackage(name, isCustom)` | `billing/custom_package_features.go:19` | `isCustom \|\| !IsPublicProductPlan(name)` |
| `prepareCustomPackageCreateFeatures` | same file `:49` | Non-public names force `IsCustom=true`; empty features → all-ON map |
| Enterprise seed | `defaultPublicPlans()` | Public name **with** `IsCustom: true` |
**Tests:** `custom_package_features_test.go``"A1", false, true` (name alone ⇒ custom treatment).
### 6.3 Company A1 / Local Demo Co
| Fact | Source |
|------|--------|
| Legacy MySQL company | `A1 Slovenija` / `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` |
| Postgres id | `ee246275-dec0-4446-9e83-58d0c16c258a` |
| Display name after seed | **Local Demo Co** (`docs/demo-user.md`, `docs/migrate-from-descrybe-new.md`) |
| Demo plan | **Enterprise** (`is_custom=true`, 1M credits) — **not** a plan named `A1` |
| Migrated teammate emails | `…@legacy.local` (dev password / impersonate) |
**Implication:** Even if a DB row named `A1` exists as a client plan, feature defaults are **enable-all**, opposite of desired **legacy limited nav**. There is **no** `is_legacy` column or legacy feature profile in billing today.
### 6.4 Product nav gating (tenant UI)
`Nav.svelte` filters items via `planCapabilities.can(featureKey)`.
`processing.monitor` (Background Tasks / Processing) is a normal feature key — currently ON for custom/Enterprise. Desired legacy matrix would turn it **OFF** (and hide stores/marketing extras).
---
## 7. Support — tenant + admin
### 7.1 Tenant (company-scoped)
| Surface | Path |
|---------|------|
| UI | `/support`, `/support/new`, `/support/[ticketId]` under `apps/web/src/routes/support/` |
| API | `server.go:472+``GET/POST /api/support/tickets`, messages, notifications |
| Handlers | `handleListSupportTickets`, `Create`, `Get`, `Reply` — session + **RequireCompany** |
| Service | `ListForUser` / `Create` / `GetForUser` / `ReplyAsUser` |
Nav item: `/support` gated by feature `support.center`.
### 7.2 Admin / staff queue
| Surface | Path |
|---------|------|
| UI | `/admin/support`, `/admin/support/[id]` |
| Client | `apps/web/src/lib/support/admin-api.ts` |
| API | `/api/admin/support/tickets*` (platform admin only) |
| Service | `ListAdmin`, `GetAdmin`, `ReplyAsAgent`, `UpdateAdmin` |
| Schema | `apps/api/sql/schema/025_support_center.sql` (indexed; includes `assignee_admin_user_id`) |
**Assignee:** `UpdateAdmin` / PATCH body `assignee_admin_user_id` / `clear_assignee`. No separate “support staff inbox” or claim-only policy — any platform admin sees the full queue.
**Ratings / CSAT:** not present in support package types/handlers (gap for agent 11+).
---
## 8. Gaps vs desired state
| Desired | Current | Gap |
|---------|---------|-----|
| Legacy limited nav (A1 / legacy plans) | Custom/non-public → **all features ON**; demo A1 tenant on **Enterprise** all-ON | Need legacy profile / `is_legacy` / seed matrix; map A1 (+ similar) to sparse features; turn off `processing.monitor`, stores, marketing extras |
| Admin + developer full admin | Only `is_platform_admin` boolean | Need staff role enum; map admin+developer → full `/api/admin`; optionally keep prod-safe “dev tools” separate from role |
| support_staff ticket access only | Support admin APIs behind **same** `RequirePlatformAdmin` as billing | Need least-privilege middleware + AdminNav subset (Support only); block plans/settings/credits |
| Role chips / assign staff from orgs UI | Users list shows `is_platform_admin` badge only | No assign APIs; bootstrap page cannot create admins |
| Support assign + staff inbox + CSAT | Assignee field exists; no staff role, no CSAT | Extend UpdateAdmin + UI; add rating schema |
| Differentiate legacy vs `is_custom` | `is_custom` / deal name ⇒ enable-all | Contract must separate **custom deal packaging** from **legacy feature profile** |
---
## 9. Extension points (summary)
Prefer extending existing gates rather than parallel systems:
1. **Staff RBAC** — extend `checkPlatformAdmin` / `RequirePlatformAdmin` (or sibling `RequireStaffRole`) reading a new `users.staff_role` (or replace boolean carefully).
2. **Legacy features** — extend `DefaultPlanFeatures` / `IsCustomPackage` / seed (`default_plan_features_seed.go`) with a `legacy` profile; detection: plan name patterns (`A1`, …) and/or `is_legacy` flag.
3. **Admin nav**`AdminNav.menuItems` filtered by staff capability; support_staff → Support (+ maybe Overview read-only if needed).
4. **Support** — keep `/api/admin/support/*` but authorize `admin|developer|support_staff`; optionally filter `ListAdmin` by assignee for least privilege.
5. **PlanPermissionsPanel** — already the UI to inspect/set matrices; add Legacy badge + apply-legacy-profile action once backend exists.
Machine-readable companion: [`02-extension-points.json`](./02-extension-points.json).
---
## 10. Related docs
- `docs/plan-permissions/02-plans-permissions-current.md` — plans / features / roles orthogonality
- `docs/plan-permissions/08-admin-ui.md` — PlanPermissionsPanel contract
- `docs/demo-user.md` — Local Demo Co / A1 migration
- `docs/migrate-from-descrybe-new.md` — A1-only migrator scope
- Sibling agents will add `03-roles-matrix`, `04-contract`, `05-legacy-seed`, `06-staff-roles`, …