Operator credentials roster UX fixes at scale
Follow-up to Task 9378 (operator credentials page UX). That task shipped server-side sort (Created/Name/Type/Org), a "1–25 of N" count, and shows the tenant once per row. These items remain for an operator working across all tenants' credentials, up to millions of rows:
1. **Secrets filter scales.** `?secrets=` decrypts every row in scope on each request (`apps/api/app/routes/credentials/reads.py::_secrets_filter`) and puts every id into an `IN` list (Postgres allows at most 65535). Store a "has stored secret" boolean when the credential is written, so the filter becomes an indexed predicate.
2. **Paging without full counts.** Replace exact `COUNT(*)` + OFFSET with keyset (cursor) paging and a capped/estimated total ("10,000+"). Add indexes behind each sort order (`lower(name)`, `type`, org name).
3. **Search that finds things at scale.** Add a pg_trgm index on name/description. For operators, also match org name/slug and owner email.
4. **Owner picker as an async typeahead.** A native select cannot hold thousands of owners across tenants. Make it multi-select.
5. **Tree mode beyond 200 rows.** Build tree mode from a server-side folder listing (folders with counts, children loaded on open) instead of one 200-row page.
6. **Bulk actions.** Let operators select rows (per page, plus "all N matching") and then delete, change org or move them to a folder.
7. **Health columns and filters.** Add last used, last test result, and expired/expiring/never used, each also available as a filter.
Out of scope: the three items Task 9378 shipped; credential editor pages; runner credential resolution.
Done means:
- Each item above is shipped or explicitly declined with a reason.
- With 1M credentials in a dev DB, the roster's first page with any filter or sort loads in under 1s (EXPLAIN shows an index scan).
- An operator can bulk-delete 500 matching credentials in one action.
Subtasks
- [business_analysis] Operator credentials roster UX fixes at scale PRD settled: 7 roster-at-scale items in scope with oracles (stored has-secret flag, keyset + 10,000+ cap, trigram/operator search, async multi-owner, server tree, all-or-nothing bulk ≤10k, stored health data). Depends on 9378 merging. Memory: none applied.
- [architecture] Operator credentials roster UX fixes at scale Plan: derive roster facts and health in the config setter; tree on generated columns; keyset paging with 10k-capped counts; indexed and trigram filters; set-based all-or-nothing bulk. 8 children: C1a→{C1b,C2}→{C3,C4,C5,C6}→C7 (dev walk; dev unreachable from this node, deferred). Memory: none applied.
- [plan] Operator credentials roster UX fixes at scale Split along the architecture's seams into 8 children: stored facts/indexes → {stamps, keyset roster API} → {pager/search/health UI, owner typeahead, tree, bulk} → dev walk. Done-checks tightened (A3 expiry, phone legibility, O(1) bulk, skip-scan property test). Dev reach still unverified, so the 500-row dev check may fall back to the slot API test. Memory: none applied.
- Store credential roster facts and indexes Stored has_stored_secret/expires_at (setter-derived, backfilled), health stamps, generated home_key/parent_path and all roster indexes, plus the 1M seed (EXPLAIN: index scans, <17ms). PR #6743 merged 787bef7, deploy green. Deferred: re-run the backfill at the reader switch. Memory: none applied.
- Stamp credential last-used and test results Credential last_used_at stamp (5 runner fetch routes, ≤1/h, best-effort) + Test result stamp (saved row only); trigger narrowed so stamps aren't edits. Quick gate green, security/code/verify lanes pass. PR #6748 merged ec37e03; deploy superseded. Memory: proposed mem-24626.
- Keyset roster query builder and API Keyset roster builder (roster.py): indexed secrets/health/owner filters, capped total, cursor paging for all 5 sorts, operator org/email search, and migration 202610240200. AC-P 120/120 PASS (≤400 ms). Review lane passed. Merged #6750 as 8c3bd8e; its deploy was superseded by a newer one. Deferred: opt-in CI job for the explain script. Memory: none applied.
- Roster keyset pager, search, health UI Credentials roster: keyset Prev/Next, "10,000+" count, per-caller search placeholder, Health filter, Last used/Last test/expiry cells, reader doc. Quick gate and 23 e2e green; visual check at 448/1024/1440 fine. PR #6756 queued after rebase onto bulk (#6754). Memory: none applied.
- Multi-select async owner typeahead filter
- Server-side lazy credential folder tree
- Bulk delete, move, change-org credentials Shipped bulk Delete / Move to folder / Change org [op] for credentials, each one all-or-nothing call (ids or match, cap 10k, expect_count). Flat roster has ticks, Select all N matching, and a toast. Quick gate green. Review lanes passed (security, code, verifier). PR #6754 queued on the merge lane. Visual check done on desktop and phone. Explain bulk-resolve on 1M rows: slowest 85 ms. Deferred: one advisory lock per name on capped sets. Memory: none applied.
- Dev acceptance walk and disposition
- [finish] Operator credentials roster UX fixes at scale
Activity
- Created
- Runner started
- Ready
- Ready
- Ready
- Runner started
- Runner ended (done)
- Runner started
- Runner ended (done)
- Runner started
- Draft
- Draft
- Draft
- Draft
- Draft
- Draft
- Draft
- Draft
- Draft
- Ready
- Ready
- Ready
- Ready
- Ready
- Ready
- Ready
- Ready
- Ready
- Runner ended (done)
- Runner started
- Runner ended (done)
- Runner started
- Runner started
- Runner ended (done)
- Runner ended (done)
- Runner started
- Runner started
- Runner started
- Runner started
- Runner ended (done)
- Runner ended (done)