Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
3.0 KiB
3.0 KiB
18 — Performance (capabilities, permission matrices, support queues)
Date: 2026-08-05
Owner: agent 18/20
Scope
Concrete query/index/cache fixes for:
- Capabilities resolution (
GET /api/billing/capabilities) - Admin permission matrices (
GET /api/admin/plans, plan features, feature gates) - 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_activesupport_tickets_status_activity_idx—(status, COALESCE(last_message_at, updated_at) DESC)support_tickets_assignee_activity_idx— staff inbox partial on assigneesupport_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-revalidateETag: "sha256:…"viabilling.CapabilitiesResponseETag(features plus plan id / name / remaining credits)304 Not ModifiedwhenIf-None-Matchmatches
Body feature_etag is unchanged (feature-map only) for the web client.
3. Admin permission matrices
ListPlansalready selectsfeaturesonce and resolves matrices in-process (no per-plan round-trip).- Added
Cache-Control: private, no-storeon:GET /api/admin/plansGET /api/admin/plans/{id}/featuresGET /api/admin/feature-gates
4. Support queues
- Count query skips user/company joins unless
search/qis set. - Service-layer
clampListBounds(default 50 / max 200) as defense in depth. - Admin list accepts
assignee_idfilter (uses new assignee index). - Handlers continue to use
ParseLimitOffset.
Tests
apps/api/internal/billing/capabilities_etag_test.goapps/api/internal/support/list_bounds_test.go
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.mdP0)