Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
6.3 KiB
operator-list-scale Specification
Purpose
Cross-cutting contract in the style of empty-state-guidance and form-conventions: how operator lists behave at deployment scale. It governs the lists that grow with the deployment — organizations, grants, people, and the four billing views (accounts, subscriptions, invoices, payments). Small operator-curated configuration lists (org types, entitlement sets, plan ladders, the products catalog) are exempt and stay flat until they earn controls. It implements the URL shape that operator-panel-navigation reserved ("Browse routes reserve query-parameter real estate for filters").
Requirements
Requirement: Governed lists paginate with a true total count
Each governed operator list SHALL render server-side pages of at most 50 rows with previous/next controls and a current-position indication, and SHALL state the true total row count for the current search and filter ("Showing X–Y of N"), never rendering the whole table at once and never presenting a page's row count as the total. Page state lives in the page query parameter; an out-of-range or malformed page value clamps to a valid page rather than erroring.
Scenario: A large list pages instead of dumping
- WHEN an operator opens a governed list whose row count exceeds the page size
- THEN at most one page of rows renders, with controls to move between pages
- AND the stated total is the count of all matching rows, not the page's
Scenario: Malformed page values clamp
- WHEN an operator loads a governed list with
?page=9999or?page=abc - THEN the list renders a valid page without error
Requirement: List state is URL-addressable and server-rendered
Search, filter, and page state on a governed list SHALL live entirely in that page URL's query parameters (q, capability-appropriate facet names, page) using the reserved shape from operator-panel-navigation, submitted as plain form GETs under the panel's hx-boost; copying the URL SHALL reproduce the exact view, and no list control SHALL require JavaScript beyond the boost that every panel navigation already uses.
Scenario: A filtered view is shareable
- WHEN an operator copies the URL of a searched, filtered, paged list view and opens it in a fresh session
- THEN the same rows, controls, and stated total render
Requirement: Governed lists carry a scoped server-side search
Each governed list SHALL carry one search input that matches case-insensitively against that list's named fields — organizations: name, slug, and owner name or email; grants: organization and grant target; people: display name and email; billing views: organization name and the customer-facing identifier the view leads with — and a search with no matches SHALL render the no-match state (per empty-state-guidance, distinct from the list's true-empty state) with a one-click way to clear the search.
Scenario: Search narrows the list server-side
- WHEN an operator submits a search term on a governed list
- THEN only matching rows render, the stated total reflects the match count, and the term stays visible in the input and the URL
Scenario: No matches is not empty
- WHEN a search matches nothing on a list that has rows
- THEN the list renders a no-match state naming the term, with a clear-search affordance
- AND not the list's true-empty state
Requirement: Status filters exist where a status vocabulary exists
A governed list whose rows carry a status vocabulary SHALL offer a filter over exactly that vocabulary — grants: the derived Live/Superseded/Inactive states; invoices: their status set plus the derived Overdue state (operator-billing-views); subscriptions: their status set; organizations: org type — composing with search and pagination, with the active filter visibly marked and clearable in one click. Filter values use the reserved facet parameter names.
Scenario: Filters compose with search and paging
- WHEN an operator applies a status filter and a search term together
- THEN rows match both, the total reflects the combination, and paging moves within the filtered set
Scenario: The active filter is visible and clearable
- WHEN a filter is active on a governed list
- THEN the control shows which value is active
- AND one click returns to the unfiltered view
Requirement: Embedded lists can adopt the shared controls
A governed list embedded in a page that hosts more than one (the organization composite's grants ledger and Tier changes) SHALL use the same search, facet, and pager mechanics under namespaced parameter names (a per-list prefix on q, page, and per; the facet parameter is always a full name), and every URL such a list builds — pager links, facet links, page-size links, and the search form's submission — SHALL preserve its sibling lists' state on the shared URL. Embedded lists default to a page size of 10 and offer a picker over the shared option set (10, 25, 50); an unknown size is ignored like an unknown facet, and the size parameter is omitted at the list's default so canonical views keep canonical URLs. Standalone pages keep the unprefixed names and the standard size. The flat "latest 16" cap remains only for embedded lists that have not adopted the controls (Members). An embedded list's controls SHALL re-render only their own panel, never scrolling the page (maintainer 2026-08-24): each control issues a scoped request targeting and selecting the panel's wrapper, pushes the resulting URL so views still deep-link, keeps the plain URL as the no-JS fallback, and refreshes its sibling panels out-of-band from the same response so no panel's links ever carry stale sibling state.
Scenario: Interacting with an embedded list holds the viewport
- WHEN an operator scrolled mid-page switches a tab, searches, resizes, or pages an embedded list
- THEN only that list's panel (and its siblings' control state) re-renders, the scroll position is unchanged, and the address bar carries the combined state of every embedded list on the page
Scenario: Sibling state survives navigation
- WHEN an operator has searched the grants ledger and then pages the Tier changes list on the same organization page
- THEN the Tier changes link carries its own namespaced page parameter and the ledger's search and tab state ride along unchanged