Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
14 KiB
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 3–6):
| 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 (ex–A1 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:294–325:
/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_customladder_custom— public ladder +is_custom(Enterprise)custom—is_customdeal— 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:
- Staff RBAC — extend
checkPlatformAdmin/RequirePlatformAdmin(or siblingRequireStaffRole) reading a newusers.staff_role(or replace boolean carefully). - Legacy features — extend
DefaultPlanFeatures/IsCustomPackage/ seed (default_plan_features_seed.go) with alegacyprofile; detection: plan name patterns (A1, …) and/oris_legacyflag. - Admin nav —
AdminNav.menuItemsfiltered by staff capability; support_staff → Support (+ maybe Overview read-only if needed). - Support — keep
/api/admin/support/*but authorizeadmin|developer|support_staff; optionally filterListAdminby assignee for least privilege. - 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.
10. Related docs
docs/plan-permissions/02-plans-permissions-current.md— plans / features / roles orthogonalitydocs/plan-permissions/08-admin-ui.md— PlanPermissionsPanel contractdocs/demo-user.md— Local Demo Co / A1 migrationdocs/migrate-from-descrybe-new.md— A1-only migrator scope- Sibling agents will add
03-roles-matrix,04-contract,05-legacy-seed,06-staff-roles, …