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

78 lines
3.0 KiB
Markdown

# 18 — Performance (capabilities, permission matrices, support queues)
Date: 2026-08-05
Owner: agent 18/20
## Scope
Concrete query/index/cache fixes for:
1. **Capabilities resolution** (`GET /api/billing/capabilities`)
2. **Admin permission matrices** (`GET /api/admin/plans`, plan features, feature gates)
3. **Support queues** (user list + admin/staff inbox)
No million-SKU product-list work (already covered in `docs/perf-notes.md`).
## Findings (before)
| Path | Issue | Severity |
|------|--------|----------|
| `CapabilitiesForCompany` | Active plan lookup used only `company_plans(company_id)` | Medium |
| `handleGetCapabilities` | Body had `feature_etag` but no HTTP `ETag` / 304 (OpenAPI already patterned) | Medium |
| Admin `ListPlans` | Already one query + in-memory `resolved_features` (no N+1) | OK |
| Admin matrices | No `Cache-Control` — risk of intermediary caching | Low |
| `ListAdmin` count | Always joined `users` + `companies` even without search | Medium |
| Support indexes | Missing assignee + COALESCE(activity) indexes for queue sorts | Medium |
| Pagination | Handlers already use `ParseLimitOffset` (default 50, max 200) | OK |
## Changes
### 1. Indexes — goose `027_capabilities_support_perf.sql`
- `company_plans_company_active_created_idx` — partial `(company_id, created_at DESC) WHERE is_active`
- `support_tickets_status_activity_idx``(status, COALESCE(last_message_at, updated_at) DESC)`
- `support_tickets_assignee_activity_idx` — staff inbox partial on assignee
- `support_tickets_user_activity_idx` — user ticket list activity sort
Apply via existing migrate script after deploy.
### 2. Capabilities HTTP cache headers
`handleGetCapabilities` now sets:
- `Cache-Control: private, max-age=30, must-revalidate`
- `ETag: "sha256:…"` via `billing.CapabilitiesResponseETag` (features **plus** plan id / name / remaining credits)
- `304 Not Modified` when `If-None-Match` matches
Body `feature_etag` is unchanged (feature-map only) for the web client.
### 3. Admin permission matrices
- `ListPlans` already selects `features` once and resolves matrices in-process (no per-plan round-trip).
- Added `Cache-Control: private, no-store` on:
- `GET /api/admin/plans`
- `GET /api/admin/plans/{id}/features`
- `GET /api/admin/feature-gates`
### 4. Support queues
- Count query skips user/company joins unless `search`/`q` is set.
- Service-layer `clampListBounds` (default 50 / max 200) as defense in depth.
- Admin list accepts `assignee_id` filter (uses new assignee index).
- Handlers continue to use `ParseLimitOffset`.
## Tests
- `apps/api/internal/billing/capabilities_etag_test.go`
- `apps/api/internal/support/list_bounds_test.go`
```powershell
cd apps/api
go test ./internal/billing/ ./internal/support/ ./internal/httpapi/ -count=1
```
## Out of scope / deferred
- In-process TTL cache for `GetFeatureGates` (tiny table; premature)
- Keyset pagination for support (volume still small)
- Changing product-list OFFSET pagination (see `docs/perf-notes.md` P0)