Files
cgalo5758 12f1d3fc00 Fix the four Slice 3 walk findings and page every operator list
Archives openspec change slice3-walk-fixes and syncs its five delta
specs (fedwiki-sites, entitlements, operator-panel-navigation,
operator-list-scale, ui-quality-gate).

- FedWiki site usage is read from active site rows in both quota
  readers; the reservation counter converges on the rows: raise-only
  after farm sync and inside the create quota check, exact at boot.
  The understated production counters repair on the first boot.
- The People tile caption excludes the reserved system person through
  the same query parameter the directory uses.
- The operator Domains live-claims list is a governed list: pages of
  50, true total, search over root name and organization, a
  pending/active facet.
- New lint rule table-without-list-controls refuses an unpaged
  page-body table unless it carries a list-scale exempt marker with a
  reason; six curated or detail tables carry one. Its first run caught
  the operator FedWiki sites list, which is now governed the same way.
- Entitlement-set rule copy: "Per unit", "Multiplied by the quantity
  purchased or granted."
2026-09-12 01:16:17 -05:00

53 KiB

operator-panel-navigation Specification

Purpose

TBD - created by archiving change fix-operator-panel-tabs. Update Purpose after archive.

Requirements

Requirement: Operator panel groups capabilities under a three-layer information architecture

The operator panel SHALL organize all capabilities under exactly three conceptual groups: Catalog, Runtime, and Integration. Catalog contains configuration entities edited rarely with wide blast radius (org_types, products, entitlement_sets, plan_ladders). Runtime contains per-organization operational surfaces (organizations, persons, grants, billing read-views). Integration contains system-specific surfaces (FedWiki sites today; other integrations later).

The groups govern URL grouping, conceptual categorization, breadcrumb derivation, and the one-way catalog → runtime → integration dependency direction. Capabilities SHALL NOT appear at the operator panel's top level outside one of these three groups; the conceptual grouping is carried by each page's machine-readable IA-position declaration.

Sidebar presentation is a flat list of exactly eight section entries — no children, no indentation, no headed groups (maintainer decisions 2026-08-23: the sidebar names the sections; the sections navigate themselves; People joined as the eighth entry with the directory) — followed by exactly one non-section entry: the surface switch "Member dashboard", separated from the sections by the rail's border-top idiom, linking to /, never marked active (unify-shell; the only separated entry the rail may carry). The render order SHALL be: Overview, People, Organizations, Grants, Billing, Products, Domains, Integrations, then the surface switch. Second-level surfaces SHALL be reached in-page, not from the sidebar: person detail from the People directory's rows, org_types from the Organizations page's header affordance, the billing read-views from the Billing wrapper's in-page pill row, entitlement_sets and plan_ladders from the Products page's header affordances, and provider surfaces from the Integrations home's provider rows. The conceptual layer lives in the IA declarations; a second-level page SHALL mark its section's sidebar entry active. Below the lg breakpoint the same sidebar renders as a drawer opened from the shell's top bar (chrome-conventions, "One shell for both surfaces"); no other navigation to the sections exists.

Scenario: Catalog capabilities are reachable from within their task surfaces

  • WHEN an operator views the Organizations page or the Products page
  • THEN the Organizations page carries an affordance to org_types, and the Products page carries affordances to entitlement_sets and plan_ladders
  • AND none of them appear in the sidebar
  • AND their IA-position declarations still carry the Catalog conceptual group

Scenario: The sidebar is a flat list of section entries

  • WHEN an operator views the operator panel sidebar
  • THEN it renders exactly the eight entries Overview, People, Organizations, Grants, Billing, Products, Domains, Integrations in that order
  • AND no child entries, indentation, or headed group blocks are rendered
  • AND the only entry after the eight is the separated "Member dashboard" surface switch

Scenario: Sections navigate themselves

  • WHEN an operator views an operator section with sub-surfaces
  • THEN the sub-surfaces are reachable from within the section (the Billing wrapper's pill row, header affordances on the section's page, or the section's own rows)
  • AND not from sidebar children

Scenario: Second-level pages mark their section active

  • WHEN an operator is on a second-level page (for example person detail, org types, a billing view, or entitlement sets)
  • THEN the sidebar marks that section's single entry (People, Organizations, Billing, or Products) as active, and no other

Scenario: The sidebar is reachable on phones

  • WHEN an operator views any operator page below the lg breakpoint
  • THEN the top bar's toggler opens the same sidebar as a drawer, sections first and the surface switch last

Requirement: Operator panel exposes a curated landing surface at /operator

The operator panel SHALL render a curated landing surface at /operator that answers "what is this deployment doing right now?" rather than defaulting to the first capability tab. The landing surface SHALL be the default destination when an operator navigates to /operator without further path or query parameters. The surface SHALL present, coarsest-first: the setup banner while it applies (operator-setup-checklist), a lookup affordance, an at-a-glance overview region, a system-status region, and a recent-activity timeline (the order is the Landing surface orders attention before totals requirement's).

The landing-surface lookup affordance SHALL be a text input under the contract "every record that has a page of its own is findable by the identifier its page shows". The resolvers form one ordered registry in code, tried in order so a typed identifier wins over a substring: an exact key (organization, plan ladder, entitlement set, product); an exact invoice number; a person (name substring, email exact); an organization name (case-insensitive substring). Submitting the input SHALL resolve to the matched record's page (/operator/persons/{personID}, /operator/organizations/{orgID}, the keyed record's page, /operator/billing/invoices/{invoiceID}) when the match is unique, or to an inline disambiguation listing when several records match (persons by name substring; organizations by name; invoices sharing a number across billing accounts, each row showing number, organization, and date). Persons take precedence over organization names because every personal organization's name contains its owner's display name. The input's placeholder SHALL say what to type, not the record classes ("Find anything by its name, key, or number"); the no-match sentence SHALL name the classes, generated from the resolver registry so it cannot disagree with the code. A test SHALL assert that every detail route in the capture manifest has a resolver, so a record class joins the set when it gains a page. Resolution SHALL be performed server-side; no client-side autocomplete is required.

The at-a-glance region SHALL present three headline tiles: People, Team organizations, and Monthly recurring. The People tile SHALL count real people only, excluding the reserved system person the directory also hides, with its caption stating how many persons joined in the last 30 days. The organizations tile SHALL count deliberately created organizations: active organizations whose type is neither the auto-created personal type nor a reserved type. Personal orgs exist one-per-member by structural convention (an organizations count that includes them tracks the people count and reads as an error), and the negative-space predicate means team, enterprise, and deployment-defined types all count without code changes while the reserved system org never does. The Monthly recurring tile SHALL show the deployment's monthly recurring revenue in its largest currency bucket with its existing caption. The headline row is reserved for these three daily reads: open-invoice, delivery, and catalog counts SHALL NOT render as headline tiles (maintainer, 2026-09-02: "not that important"; the money tile stays), and per-feature counts (such as domain claims) SHALL NOT either — those surfaces are reached from the sidebar. Each tile SHALL link to its browse surface. Count sources SHALL degrade independently: a failed count query SHALL be logged and rendered as an explicit unavailable marker (an em dash) — NEVER as a zero, since a zero read off a broken query invites exactly the wrong operational conclusion — and SHALL NOT fail the page.

The recent-activity section SHALL be a single unified timeline that interleaves grant events, transition events, invoice events, and payment events from across the operator panel, ordered by event timestamp (newest first). Each rendered event row SHALL identify the organization (and person, where applicable) and link to the originating organization composite, the originating person detail, or the relevant billing page based on the event type. The section is one timeline because an operator's mental model of "what's been happening" is temporal, not domain-partitioned — an invoice payment that lands near the grant transition that triggered it is more useful adjacent than separated. Money amounts in event rows SHALL render as formatted currency (symbol/code and decimal placement per the console's standard money formatting), never as raw integer minor units. Tier events SHALL render in the vocabulary the organization composite's "Tier changes" list uses (humanized verbs and tier names, joined with a colon and "to": "Started: Basic", "Upgraded: Basic to Pro"), never as raw transition types, integer ranks, or em-dash separators.

The system-status region SHALL be one Integrations card, under a "System" section header whose single action is "Setup", linking to /operator/setup, always. The card SHALL list every registered provider, one row each in registry order, showing the provider's name as a link to its page (its admin page when it declares one; for Stripe, its provider page at /operator/integrations/stripe) and its single Status, the same three states the Integrations table shows (Disabled / Not configured / Configured, integration-settings). Stripe's row SHALL add, after its status, the mode ("Test mode" or "Live mode" from stripe-mode) and the queue ("n queued, n need attention" from the outbox), with alarm styling only when dead-lettered work exists. The region SHALL NOT carry the delivery-queue report, which lives on the Stripe provider page, and its copy SHALL NOT describe any state as "connected" or "read live" — the console performs no connectivity checks.

The landing surface's heading hierarchy SHALL descend without skipping levels (H1, then H2, …), and the surface's root page carries no location trail (chrome-conventions). The operator shell (page title, navbar brand) SHALL render the configured deployment name (deployment-name, defaulting to the generic product name), identically to the member surface, so the application presents one identity across surfaces; with the default value the shell remains deployment-neutral for screenshots and first impressions. The landing surface SHALL use the console's standard bordered-card presentation; it SHALL NOT introduce bespoke elevation or hover-motion card treatments absent from the rest of the console.

Scenario: Navigating to /operator shows the landing surface

  • WHEN an authenticated operator navigates to /operator
  • THEN the panel renders the curated landing surface
  • THEN the landing surface includes the lookup entry point
  • THEN the landing surface includes the at-a-glance region with the People, Team organizations, and Monthly recurring tiles
  • THEN the landing surface includes the System section with its "Setup" action and the Integrations card
  • THEN the landing surface surfaces a single recent-activity section interleaving grants, transitions, invoices, and payments

Scenario: Landing surface is the documented default

  • WHEN an operator follows an internal link to /operator from anywhere in the panel
  • THEN the landing surface renders instead of an arbitrary capability tab

Scenario: Lookup input resolves a unique person match

  • WHEN an operator submits the lookup input with a value that matches exactly one person (by email or name substring) and no key or invoice number
  • THEN the panel issues a 302 redirect to /operator/persons/{personID} for that match

Scenario: Lookup input resolves an organization by name

  • WHEN an operator submits the lookup input with a value that matches no key, invoice number, or person and exactly one organization's name (case-insensitive substring)
  • THEN the panel issues a 302 redirect to /operator/organizations/{orgID} for that organization

Scenario: Lookup input resolves an invoice number

  • WHEN an operator submits the lookup input with a value equal to exactly one invoice's number
  • THEN the panel issues a 302 redirect to /operator/billing/invoices/{invoiceID}
  • WHEN several invoices across billing accounts carry that number
  • THEN the panel renders a disambiguation listing showing each invoice's number, organization, and date, each linking to its detail

Scenario: Ambiguous organization names disambiguate

  • WHEN an operator submits the lookup input with a value that matches no key, invoice number, or person and several organizations' names
  • THEN the panel renders a disambiguation listing of those organizations, each linking to its composite

Scenario: Lookup input renders a disambiguation list on ambiguous match

  • WHEN an operator submits the lookup input with a value that matches more than one person by name substring
  • THEN the panel renders a disambiguation listing showing each candidate person
  • THEN each candidate row links to /operator/persons/{personID} for that person

Scenario: Lookup input reports no match by naming what it searches

  • WHEN an operator submits the lookup input with a value that matches nothing
  • THEN the panel renders the landing surface with a visible no-match message that names every record class the registry resolves
  • THEN no redirect occurs

Scenario: The placeholder names what to type

  • WHEN an operator views the lookup input
  • THEN its placeholder reads "Find anything by its name, key, or number" and lists no record classes

Scenario: Every page-bearing record class has a resolver

  • WHEN the lookup registry test runs against the capture manifest's detail routes
  • THEN each route's record class has a resolver in the registry

Scenario: Recent activity timeline interleaves event types

  • WHEN an operator views /operator and any recent grant, transition, invoice, or payment events exist
  • THEN the "Recent activity" section renders one row per event ordered by event timestamp (newest first), interleaving event types
  • THEN each row identifies the organization (and person, where applicable) and its event type
  • THEN each row links to the originating organization composite, person detail, or relevant billing page based on event type

Scenario: Tier events read as the composite reads them

  • WHEN the timeline renders a transition event
  • THEN the row uses the "Tier changes" vocabulary (for example "Started: Basic"), never a raw transition type, an integer rank, or an em-dash separator

Scenario: Activity money renders as formatted currency

  • WHEN the recent-activity timeline renders an invoice or payment event carrying a money amount
  • THEN the amount renders as formatted currency with its currency named
  • THEN no row renders a raw integer of minor units (e.g. "3600 usd")

Scenario: Organizations headline excludes auto-created personal orgs

  • WHEN an operator views /operator on a deployment holding personal orgs, team orgs, and the reserved system org
  • THEN the organizations tile counts only active organizations that are neither personal-typed nor reserved-typed
  • THEN its caption states the exclusion so the number is self-interpreting

Scenario: People counts real people only

  • WHEN an operator views /operator on a deployment whose only person rows are the reserved system person and two members
  • THEN the People tile reads 2, matching the directory's total

Scenario: People caption carries the 30-day trend

  • WHEN an operator views /operator
  • THEN the People tile's caption states how many persons joined in the last 30 days

Scenario: Headline row carries only the three daily reads

  • WHEN an operator views /operator
  • THEN the at-a-glance region renders the People, Team organizations, and Monthly recurring tiles and no open-invoice, delivery, catalog, or per-feature count
  • THEN those surfaces remain reachable from the sidebar

Scenario: Landing cards use the console's standard idiom

  • WHEN the landing surface renders
  • THEN its cards render with the console's standard bordered-card treatment
  • THEN no card carries a bespoke elevation (shadow) or hover-motion treatment

Scenario: A failed count degrades to an unavailable marker, not a zero

  • WHEN an operator views /operator and one metric's backing query fails
  • THEN that tile renders an em dash with an unavailable caption instead of a value
  • THEN every other tile renders its real value
  • THEN the page returns 200 and the failure is logged

Scenario: The Integrations card lists every provider with one status

  • WHEN an operator views /operator with FedWiki, Discourse, and Stripe registered, Discourse unconfigured, and Stripe configured in test mode with two dead-lettered outbox entries
  • THEN the Integrations card shows three rows: FedWiki with its status, Discourse reading "Not configured", and Stripe reading "Configured", "Test mode", and "2 queued, 2 need attention" with alarm styling
  • THEN each provider's name links to its page, Stripe's to /operator/integrations/stripe
  • THEN the card carries no dead-letter table and no operation identifiers

Scenario: The System header always offers Setup

  • WHEN an operator views /operator, setup complete or not, banner dismissed or not
  • THEN the System section header carries one action, "Setup", linking to /operator/setup

Scenario: Shell renders the configured deployment name

  • WHEN any operator page renders
  • THEN the page title and navbar brand render the configured deployment name, matching the member surface
  • THEN with no name configured, the shell renders the generic product name and stays deployment-neutral

Scenario: Landing heading hierarchy has no skips

  • WHEN an operator views /operator
  • THEN the document outline descends H1 → H2 (→ H3) with no skipped levels

Requirement: People caption counts the same population as the tile

The People tile's 30-day caption SHALL count the same population as its headline: active persons excluding the reserved system person, detected structurally as the owner of the reserved system organization, never by name or email. The exclusion SHALL be a parameter of the count query in the shape the people directory already uses, so the headline, the caption, and the directory total cannot diverge by the system person.

Scenario: Fresh deployment inside the window

  • WHEN an operator views /operator on a deployment created within the last 30 days whose person rows are the reserved system person and thirteen members
  • THEN the People tile reads 13
  • AND its caption states that 13 persons joined in the last 30 days

Scenario: System person outside the window changes nothing

  • WHEN the reserved system person was created more than 30 days ago and two members joined this month
  • THEN the caption states that 2 persons joined in the last 30 days

Requirement: Per-organization composite view is addressable at /operator/organizations/{orgID}

The operator panel SHALL provide a per-organization composite view at the route /operator/organizations/{orgID} that co-locates enrollment state, grant history, billing summary, member roster, and grant actions for one organization. The route SHALL be bookmarkable and reload-stable. The composite view SHALL be reachable from the Runtime group's organization browse surface.

The billing-summary section SHALL show: the current subscription status and plan (if any), the next renewal date (if any), the most recent invoice (date, amount, paid status), and the outstanding balance across unpaid invoices. The section SHALL link out to /operator/billing/accounts for the full billing view.

The members section SHALL list all persons enrolled at the organization, sorted alphabetically by display name. Each row SHALL link to the person's resolved-detail view at /operator/persons/{personID}.

Scenario: Per-org composite view is bookmarkable

  • WHEN an operator loads /operator/organizations/{orgID} directly via URL
  • THEN the page renders the composite view for that organization without requiring prior navigation through the panel

Scenario: Composite view co-locates runtime work

  • WHEN an operator views /operator/organizations/{orgID}
  • THEN the page surfaces the organization's enrollment state, grant history, billing summary, and member roster on the same page
  • THEN grant actions (issue, extend, revoke) are reachable from the same page without navigating to a different top-level capability

Scenario: Composite view exposes a billing summary

  • WHEN an operator views /operator/organizations/{orgID} for an organization with billing activity
  • THEN the page renders a billing-summary section showing current subscription status, next renewal date, most recent invoice (date, amount, paid status), and outstanding balance
  • THEN the section links to the full billing view at /operator/billing/accounts

Scenario: Composite view exposes a member roster

  • WHEN an operator views /operator/organizations/{orgID} for an organization with enrolled members
  • THEN the page renders a members section listing each enrolled person sorted alphabetically by display name
  • THEN each member row links to /operator/persons/{personID} for that person

Requirement: Per-organization composite view is the sole UI entry point for grant actions

The operator panel SHALL expose grant action affordances (issue, extend, revoke) only from the per-organization composite view. The global Grants surface in the Runtime group SHALL be read-only — it presents grants for browse/audit/lookup but exposes no action affordances. Underlying HTTP handlers (e.g. the simple RevokeGrant, the legacy CreateGrant issuance path) MAY remain reachable by direct URL for programmatic callers but SHALL NOT be surfaced as operator UI affordances.

Grant issuance SHALL be exposed as a single product-based form on the per-organization composite view. The form's product picker SHALL list published products available to the organization, including internal wrap products, which SHALL be visually marked as internal in the picker; the picker SHALL NOT be partitioned into intent-labeled variants by catalog shape (no separate "plan" picker and "non-plan" picker). Whether the resulting conferral is position-bearing (occupies a ladder rung) SHALL be derived by the entitlement-conferral capability's confer primitive from the product's catalog shape (billing.product_conferral_shapes) — the operator SHALL NOT select or declare this behavior on the form. The form SHALL carry uniform bounds — an optional quantity and an optional valid_until — available for any product regardless of catalog shape, and a grant_reason select restricted to the fixed domain excluding 'default'. The full semantics of grant issuance (conferral derivation, the uniform-bounds contract, the grant_reason domain, and expiration scheduling) are specified by the plan-enrollment-administration capability's "Operator grant issuance" requirement; this requirement governs only the composite view's role as the sole UI entry point and the single-form shape of that entry point, not conferral behavior itself.

Grant revocation SHALL be enacted by the decree-first, source-resolved revoke behavior owned by the plan-enrollment-administration capability's "Operator grant revocation" requirement: the entry point sets the grant's revoked_* ledger fields and then calls entitlements.end_conferral resolved by the grant's own source, ending only the provision that grant itself delivered. Ending a provision SHALL NOT re-mint the org-type default as a side effect — re-application of the org default, when desired, is a separate ReapplyDefaultsForPool call (owned by auto-provisioning, per plan-transitions). This requirement governs only the composite view's role as the sole UI entry point for revocation, not the revoke semantics themselves; no Revoke affordance SHALL be surfaced outside the per-organization composite view.

Scenario: Per-org view exposes revoke using the source-resolved behavior

  • WHEN an operator views /operator/organizations/{orgID}
  • THEN each active grant for the organization has a Revoke affordance
  • THEN invoking Revoke sets the grant's revoked_* fields and calls end_conferral resolved by the grant's source, ending only that grant's own provision (end_conferral itself never re-applies the org default; the entry point then performs the guarded baseline restoration owned by plan-enrollment-administration, which runs only when the default ladder is left vacant)

Scenario: Per-org view exposes a single issuance form for any published product

  • WHEN an operator views /operator/organizations/{orgID}
  • THEN the page exposes exactly one grant issuance form
  • THEN the form's product picker lists published products available to the organization, with internal wrap products marked as internal
  • THEN no separate plan-typed and non-plan-typed issuance forms exist
  • THEN the form exposes uniform bounds (quantity, valid_until) and a grant_reason select over the fixed domain excluding 'default', regardless of which product is picked

Scenario: Per-org view exposes extension

  • WHEN an operator views /operator/organizations/{orgID}
  • THEN each active grant has an Extend affordance that issues an extend-as-replace with new bounds (a new lineaged grant consumed by confer, per the plan-enrollment-administration capability), never mutating the incumbent grant in place

Scenario: Global Grants surface exposes no action affordances

  • WHEN an operator views the global Grants surface in the Runtime group
  • THEN the grant rows render in a read-only listing
  • THEN no Issue, Extend, or Revoke affordance appears on the surface

Requirement: Every operator page declares its IA position for breadcrumb derivation

Every operator panel page SHALL declare its position in the IA as a positional tuple of the form (group, capability) or (group, capability, instance), where group is one of catalog, runtime, or integration, capability is the capability slug (e.g. products, organizations), and instance is present when the page is scoped to one entity (e.g. an organization's composite view). The visual rendering of breadcrumbs is out of scope for this specification.

Scenario: Capability index page declares its IA position

  • WHEN an operator loads /operator/products
  • THEN the page declares its IA position as (catalog, products)

Scenario: Instance page declares its IA position

  • WHEN an operator loads /operator/organizations/{orgID}
  • THEN the page declares its IA position as (runtime, organizations, {orgID})

Requirement: Browse routes reserve query-parameter real estate for filters

Browse routes under the operator panel (e.g. /operator/organizations, /operator/products, /operator/plan-ladders) SHALL reserve the query parameters ?q=<search> and ?<facet>=<value> (where <facet> is a capability-appropriate field such as org_type, lifecycle_status) as the canonical shape for future filter additions. Browse routes SHALL NOT use these parameter names for unrelated purposes. Implementing visible filter controls is out of scope for this specification; the reservation establishes URL contract only.

Scenario: Reserved parameter names are not reused

  • WHEN a new feature is added to a browse route
  • THEN the feature SHALL NOT use q, org_type, lifecycle_status, or other established facet parameter names for any purpose other than search and filtering

Scenario: Unknown filter params do not error

  • WHEN an operator loads a browse route with a reserved-but-unimplemented filter param (e.g. /operator/organizations?q=acme)
  • THEN the route renders without error
  • THEN the unimplemented filter is ignored rather than rejected

Requirement: Operator panel uses MPA routing with hx-boost for in-panel navigation

The operator panel SHALL serve each top-level capability at its own route under /operator/<capability> and return full HTML responses (not HTMX fragments) from those routes. In-panel navigation SHALL use hx-boost applied at the layout shell so navigations produce real browser history entries and bookmarkable URLs while avoiding full-document reparse. Under htmx 4's explicit inheritance, the shell SHALL mark the boost and the CSRF header for descendants (hx-boost:inherited, hx-headers:inherited on the shell <body>), and links that must not be boosted (the identity-provider handoff, sign-out) override with their own hx-boost="false".

The set of top-level capability routes SHALL include:

  • /operator (curated landing surface — see the landing-surface requirement)
  • /operator/persons (runtime browse directory — see operator-people-directory) and /operator/persons/{personID} (per-person detail — see the person-detail requirement)
  • /operator/organizations (runtime browse) and /operator/organizations/{orgID} (per-org composite — see the composite-view requirement)
  • /operator/grants (runtime browse, read-only — see the grant-action-consolidation requirement)
  • /operator/billing/{accounts,subscriptions,invoices,payments} (runtime read-views) and /operator/billing/invoices/{invoiceID} (per-invoice detail — see operator-billing-views)
  • /operator/org-types, /operator/products, /operator/entitlement-sets, /operator/plan-ladders (catalog)
  • /operator/products/{productID} (catalog, per-product composite — see the addressable-composite requirement)
  • /operator/plan-ladders/{ladderID} (catalog, per-ladder composite — see the addressable-composite requirement)
  • /operator/entitlement-sets/{setID} (catalog, per-entitlement-set composite — see the addressable-composite requirement)
  • /operator/integrations/fedwiki (integration; the legacy /operator/fedwiki-sites path 301-redirects here)

Scenario: Top-level capability route is bookmarkable

  • WHEN an operator loads any top-level capability route (e.g. /operator/products) directly via URL
  • THEN the page renders the full capability surface
  • THEN no prior navigation through /operator is required

Scenario: Unknown operator paths stay in the operator shell

  • WHEN an operator loads a /operator/* path no page claims (e.g. a truncated or mistyped URL)
  • THEN the response SHALL be HTTP 404 rendered inside the operator shell with a link back to the operator overview
  • AND it SHALL NOT fall through to the member-facing dashboard

Scenario: In-panel navigation uses hx-boost

  • WHEN an operator clicks any in-panel navigation link
  • THEN the navigation is intercepted by hx-boost and the response replaces the layout's main content region
  • THEN the browser's URL bar and history reflect the destination route
  • THEN a manual page reload at that URL renders the same page

Scenario: A back navigation restores by re-fetching the full page

  • WHEN an operator navigates or filters (pushing URLs into history) and presses the browser back button
  • THEN htmx re-fetches the previous URL and receives the full page (the panel serves no fragments at pushed URLs)
  • THEN the restored view renders correctly inside the shell with no cached-snapshot artifacts

Scenario: Toast container and modal survive boosted navigation

  • WHEN an operator triggers a HX-Trigger: showSuccessToast response on a POST that redirects to a different capability
  • THEN the success toast renders after the boosted navigation completes
  • THEN the toast container and the confirm-action modal are outside the boost swap target so they are not destroyed by the swap

Requirement: Legacy ?tab= URLs redirect for one release

The operator panel SHALL handle legacy /operator?tab=<tab-id> URLs by issuing a 301 redirect to the equivalent top-level capability route for the duration of one release. After that release the shim SHALL be removed and /operator?tab=<anything> SHALL render the landing surface (the unknown filter param is ignored per the reserved-filter-params requirement).

Scenario: Known ?tab= redirects to capability route

  • WHEN an operator loads /operator?tab=organizations
  • THEN the panel issues a 301 redirect to /operator/organizations

Scenario: ?tab=people redirects to the landing surface

  • WHEN an operator loads /operator?tab=people
  • THEN the panel issues a 301 redirect to /operator (since persons is lookup-only — no top-level browse exists)

Scenario: Unknown ?tab= renders landing

  • WHEN an operator loads /operator?tab=<unknown-id> after the shim is removed
  • THEN the landing surface renders without error
  • THEN the tab parameter is ignored per the reserved-filter-params requirement

Requirement: Operator panel exposes a plan-topology overview under the Catalog group

The plan-topology overview is no longer a separate destination: the Plan Ladders surface at /operator/plan-ladders SHALL open with the topology overview (the cross-ladder grid and structural-health strip, per the operator-topology-overview capability, whose requirements hold unchanged at this home) as its map-first view, with ladder management beneath it. The overview's IA position is the Catalog conceptual group via the Plan Ladders page's declaration. /operator/plan-topology SHALL NOT be a route: with no production deployments there is no old URL to stay compatible with, so no redirect exists and the path receives the operator panel's ordinary 404 handling.

Scenario: Ladders surface opens map-first

  • WHEN an authenticated operator navigates to /operator/plan-ladders
  • THEN the topology grid and health strip render as the surface's opening view
  • AND ladder management (list, create) renders on the same surface beneath or behind the map

Scenario: The old topology URL is gone, not redirected

  • WHEN an operator requests /operator/plan-topology
  • THEN the panel answers with its ordinary 404 handling, not a redirect

Requirement: Provider admin surfaces resolve from the registry, not a hardcoded map

The operator panel SHALL resolve a provider resource's admin surface from the provider registry rather than from a hardcoded resource-key→surface map. Adding or removing a provider SHALL NOT require editing a navigation map in code. Provider surfaces are presented within the dedicated Integrations section (see "Operator panel exposes a dedicated Integrations section"); this requirement governs only how each surface link is resolved (from the registry), not where the section lives.

Scenario: Provider surface is registry-resolved

  • WHEN the operator panel renders the admin-surface link for a provisioning provider
  • THEN the link URL SHALL be obtained from the provider registry
  • AND SHALL NOT be obtained from a hardcoded resource-key→surface map

Scenario: Adding a provider requires no navigation-map edit

  • WHEN a new provisioning provider is registered
  • THEN its admin surface SHALL become reachable without editing any hardcoded resource-key→surface map in code

Scenario: FedWiki surface resolves under its provider identity

  • WHEN an operator opens FedWiki's admin surface
  • THEN it SHALL be resolved via the registry for provider fedwiki (resource fedwiki_sites)
  • AND SHALL NOT be resolved via a literal "sites" map entry

Requirement: Operator panel exposes a dedicated Integrations section

The operator panel SHALL present integrations as a dedicated, registry-driven section, distinct from the catalog and runtime groups. The sidebar SHALL render a single "Integrations" entry that links to a section landing at /operator/integrations; provider surfaces are reached from that landing's rows, not from sidebar children (maintainer decision 2026-08-23), and integration admin pages and settings pages SHALL mark the "Integrations" entry active. The landing SHALL be registry-driven and SHALL list every registered provider of every kind in one table — a payments provider appears alongside provisioning ones, never as a half-present entry — showing each provider's display name as the row's link to the provider's page (its admin surface when it declares one; for Stripe, its provider page at /operator/integrations/stripe, integration-settings), one Status column carrying the provider's single honest state (integration-settings), kind, declared lifecycle operations, owned resource keys, and one Settings action linking to /operator/integrations/{integrationKey}/settings (when the integration declares configuration — sourced from the adapter registry). No "Manage" control renders: navigation is the name link. Integration admin pages SHALL live under the /operator/integrations/ URL namespace, SHALL open with a heading identifying the integration and, when the integration declares a description, that description as the page's lead, and SHALL cross-link to their settings page; settings pages SHALL cross-link back to the admin page. Adding or removing a provider, or an adapter with declared configuration, SHALL change the landing without any template or handler edit. Per-key configuration writes live on the linked settings pages, not on the landing; the landing's only write affordance is the unrecognized-override cleanup (see integration-settings), which SHALL be gated behind the shared confirm-action modal.

Scenario: Sidebar shows the Integrations section home

  • WHEN an operator views any operator page
  • THEN the sidebar SHALL include a single "Integrations" entry linking to /operator/integrations
  • AND no per-provider sidebar entries render
  • AND an integration admin page or settings page marks the "Integrations" entry active

Scenario: Integrations landing lists registered providers from the registry

  • WHEN an operator opens /operator/integrations
  • THEN the page SHALL list each registered provider of every kind with its display name, one Status, kind, declared lifecycle operations, and owned resource keys
  • AND for FedWiki the display name SHALL link to /operator/integrations/fedwiki
  • AND a payments provider (e.g. Stripe) SHALL have a row whose display name links to its provider page
  • AND the listing SHALL be derived from the provider registry, not a hardcoded list

Scenario: Adding a provider populates the section without code edits

  • WHEN a second conforming provisioning provider is registered
  • THEN it SHALL appear on the /operator/integrations landing with its name link and Settings action
  • AND no operator template or page handler SHALL require editing for it to appear

Scenario: Empty state when no provisioning providers are registered

  • WHEN /operator/integrations is opened and no provisioning provider is registered
  • THEN the page SHALL render an empty state rather than an error
  • WHEN an operator opens /operator/integrations
  • THEN every registered integration adapter that declares configuration SHALL have a Settings action linking to /operator/integrations/{integrationKey}/settings on its row, and no other action
  • AND an adapter that is not a provisioning provider (e.g. a payments integration) SHALL still get its link
  • AND the links SHALL be derived from the adapter registry, not a hardcoded list

Scenario: A provider page leads with the integration's description

  • WHEN an integration declares a description and an operator opens its page
  • THEN the page header's lead is that description

Requirement: Per-product, per-plan-ladder, and per-entitlement-set composite views are addressable

The operator panel SHALL provide a per-product composite view at /operator/products/{productID}, a per-plan-ladder composite view at /operator/plan-ladders/{ladderID}, and a per-entitlement-set composite view at /operator/entitlement-sets/{setID}. Each route SHALL render a full operator page (the operator shell), SHALL be bookmarkable and reload-stable, and SHALL be reachable from its capability's browse list via boosted in-panel navigation (a real <a href> under the shell's hx-boost), not via a fragment swap that leaves the URL unchanged. Each instance page SHALL declare its IA position as the (group, capability, instance) tuple — (catalog, products, {productID}), (catalog, plan-ladders, {ladderID}), and (catalog, entitlement-sets, {setID}).

The product composite SHALL co-locate, on the one URL, the product edit form, the product's prices view, and the purchasability/readiness panel including the Sync-to-Stripe action. The plan-ladder composite SHALL co-locate, on the one URL, the ladder edit form, the ladder's tier/rank manager, and the structural-invariant validation surface for that ladder. The entitlement-set composite SHALL co-locate, on the one URL, the entitlement-set edit form and the rules manager (rule list, add-rule form, and per-rule delete).

Scenario: Product composite is bookmarkable

  • WHEN an operator loads /operator/products/{productID} directly via URL
  • THEN the page renders the composite view for that product without requiring prior navigation through the panel

Scenario: Product composite is reachable from the browse list via boosted navigation

  • WHEN an operator activates a product's link on the /operator/products browse list
  • THEN the navigation targets /operator/products/{productID} as a real route under hx-boost
  • THEN the browser URL and history reflect /operator/products/{productID}
  • THEN a manual reload at that URL renders the same composite page

Scenario: Product composite co-locates editing, prices, and readiness

  • WHEN an operator views /operator/products/{productID}
  • THEN the page surfaces the product edit form, the product's prices view, and the purchasability/readiness panel (including the Sync-to-Stripe action when applicable) on the same page

Scenario: Plan-ladder composite is bookmarkable

  • WHEN an operator loads /operator/plan-ladders/{ladderID} directly via URL
  • THEN the page renders the composite view for that ladder without requiring prior navigation through the panel

Scenario: Plan-ladder composite is reachable from the browse list via boosted navigation

  • WHEN an operator activates a ladder's link on the /operator/plan-ladders browse list
  • THEN the navigation targets /operator/plan-ladders/{ladderID} as a real route under hx-boost
  • THEN the browser URL and history reflect /operator/plan-ladders/{ladderID}

Scenario: Plan-ladder composite co-locates editing, tiers, and validation

  • WHEN an operator views /operator/plan-ladders/{ladderID}
  • THEN the page surfaces the ladder edit form, the tier/rank manager, and the structural-invariant validation surface for that ladder on the same page

Scenario: Entitlement-set composite is bookmarkable and co-locates editing and rules

  • WHEN an operator loads /operator/entitlement-sets/{setID} directly via URL
  • THEN the page renders the composite view for that entitlement set without requiring prior navigation through the panel
  • THEN the page surfaces the entitlement-set edit form and the rules manager (rule list, add-rule form, and per-rule delete) on the same page

Scenario: Entitlement-set composite is reachable from the browse list via boosted navigation

  • WHEN an operator activates an entitlement set's link on the /operator/entitlement-sets browse list
  • THEN the navigation targets /operator/entitlement-sets/{setID} as a real route under hx-boost
  • THEN the browser URL and history reflect /operator/entitlement-sets/{setID}

Scenario: Instance pages declare their IA position

  • WHEN an operator loads /operator/products/{productID}
  • THEN the page declares its IA position as (catalog, products, {productID})
  • WHEN an operator loads /operator/plan-ladders/{ladderID}
  • THEN the page declares its IA position as (catalog, plan-ladders, {ladderID})
  • WHEN an operator loads /operator/entitlement-sets/{setID}
  • THEN the page declares its IA position as (catalog, entitlement-sets, {setID})

Requirement: Landing surface carries setup as a banner and a standing action

While any step of the operator setup checklist (see operator-setup-checklist) derives incomplete and the banner has not been dismissed, the curated landing surface at /operator SHALL render the getting-started banner above the lookup, linking to /operator/setup. The banner's copy, its close control, and the deployment-wide dismissal it records are operator-setup-checklist's to define; this surface only places it. Once every step derives complete the banner SHALL NOT render. The checklist SHALL remain reachable whatever the banner does: the System section header SHALL always carry one action, "Setup", linking to /operator/setup. The banner and the header action derive from the same predicates as the checklist, so the landing surface and the checklist never disagree.

Scenario: Fresh instance leads with the banner, not zeros

  • WHEN an operator visits /operator on a deployment where no checklist step is complete and no dismissal is recorded
  • THEN the getting-started banner renders above the lookup, ahead of the metric tiles
  • AND it links to /operator/setup

Scenario: Configured instance shows no banner

  • WHEN an operator visits /operator on a deployment where every checklist step derives complete
  • THEN the landing surface renders without the banner

Scenario: Setup stays reachable from the System header

  • WHEN an operator visits /operator with the banner dismissed or every step complete
  • THEN the System section header still carries its "Setup" action linking to /operator/setup

The curated landing surface SHALL order its regions with the operator's daily reads first: the setup banner while it applies (operator-setup-checklist), then the lookup, then the at-a-glance metric tiles, then the system-status region (the Integrations card under the System header), then recent activity — the counts an operator checks every day outrank the system cards (maintainer decision 2026-08-23), and attention-shaped content is still never the page's last region. Activity-feed entries SHALL link to the subject's surface wherever an addressable one exists (the organization, invoice, or grant the entry describes), so no feed entry is a dead end when a destination exists.

Scenario: Metric tiles render ahead of the system cards

  • WHEN an operator views /operator
  • THEN the at-a-glance tiles render before the system-status region
  • AND the system-status region renders before recent activity, so attention-shaped content (queued work needing attention, unconfigured integrations) is not the last region on the page
  • WHEN the recent-activity feed shows an entry about an entity with an addressable operator surface
  • THEN the entry links to that surface

Requirement: Person detail is addressable and cross-linked

The person-detail view at /operator/persons/{personID} SHALL be bookmarkable (no prior navigation required) and reachable from the People directory's rows, the landing-surface lookup affordance, and organization member tables. It SHALL include an organization-memberships section listing each organization the person belongs to, each row linking to that organization's composite view at /operator/organizations/{orgID}.

Scenario: Person detail is bookmarkable

  • WHEN an operator loads /operator/persons/{personID} directly via URL
  • THEN the page renders the resolved-detail view for that person

Scenario: Person detail is reachable from organization context

  • WHEN an operator views an organization's member listing on /operator/organizations/{orgID}
  • THEN each member row links to /operator/persons/{personID} for that member

Scenario: Person detail exposes organization memberships

  • WHEN an operator views /operator/persons/{personID} for a person who belongs to one or more organizations
  • THEN the page renders an organization-memberships section listing each organization the person belongs to
  • THEN each row links to /operator/organizations/{orgID} for that organization

Requirement: The surface switches name the panels

The operator rail's surface switch SHALL read "Member panel" and link to /; the member rail's surface switch SHALL read "Operator panel" and link to /operator, so the two switches name the two surfaces alike.

Scenario: The switches mirror each other

  • WHEN an operator views the operator rail and the member rail
  • THEN the operator rail's switch reads "Member panel" and the member rail's reads "Operator panel"

Requirement: The landing surface's lookup is a search form

The landing surface's lookup affordance SHALL be a declared form of the search kind, rendered through the form part in the bar family (form-library, design D4, D20). It SHALL submit by GET, carry a native action naming its route, and send no CSRF header, so it keeps a true native fallback: without JavaScript the lookup navigates and resolves exactly as it does with JavaScript. It SHALL NOT be a POST, because a mutation form's request path (the X-CSRF-Token header) is the one the console reserves for writes, and a lookup writes nothing. Its input SHALL carry a label, visually hidden where the row has no room, and autocomplete="off". It SHALL declare Wide, so the bar spans the content column rather than stopping at the width a list's own search takes: it is the landing surface's primary control and its width says so. Its button SHALL render btn-outline-secondary like every other search's, with no exception for this one (maintainer, 2026-09-04). The lookup SHALL take its row in the progressive-enhancement table in docs/design-system.md beside the operator list controls, as the search kind's second entry (form-conventions, "Every mutation form's no-JS behavior is a recorded decision"; finding FA-24, correction 8).

Scenario: The lookup resolves without JavaScript

  • WHEN JavaScript is unavailable and an operator submits the lookup with a value that matches exactly one record
  • THEN the browser performs the form's native GET and lands on that record's page, the same destination the scripted path reaches

Scenario: The lookup is a GET with a native action

  • WHEN an operator views the landing surface
  • THEN the lookup form renders method="get" with an action naming its route, and carries no CSRF header attribute

Scenario: The lookup is a wide bar with an outline button

  • WHEN an operator views the landing surface
  • THEN the lookup renders as one input group spanning the content column, with the magnifier, the control and a btn-outline-secondary "Look up" on one line

Scenario: The lookup's row is in the table

  • WHEN a reader consults the progressive-enhancement table in the design system
  • THEN the lookup appears there as a search-kind form with its native GET fallback recorded, beside the list controls

Requirement: The landing surface is built from the parts

The landing surface's regions (At a glance, System, Recent activity) SHALL be titled through sectionHeader, h2.h5 in normal case with the part's spacing; the System header's one action, "Setup", SHALL be the part's action. The at-a-glance tiles SHALL be readout parts inside linked cards (page-anatomy "A linked card is marked by a glyph and the row's hover"), each card the link to its browse surface, marked by the chevron glyph and the hover shade, the number at the readout's one size. The Integrations card SHALL carry no title of its own; its providers SHALL render as a flush list in the card's body, one row each, the same idiom the setup checklist uses. The recent-activity table SHALL be a boxed ledger like the organization composite's: inside the card's body, a record table with a plain header row, body-size cells, the event's summary as the row's primary cell and first column, and the time last (Summary, Event, Organization, Actor, When), paged through the list scaffold's pager with its own namespaced parameters so the lookup and the pager never disturb each other. The surface SHALL write no uppercase text and no <h2> of its own. Its title stays "Operator overview": a panel's home is titled for the panel, and its rail entry is the short name of the way back (maintainer, 2026-09-06, "Panel homes need no rail").

Scenario: The overview reads like its neighbours

  • WHEN an operator views /operator beside /operator/organizations/{orgID} and the member dashboard
  • THEN the section titles are the same size and case, the sections sit at the same rhythm, and no number on the overview is larger than its page title
  • WHEN an operator views /operator
  • THEN each tile's card is the anchor to its browse surface, carries the chevron glyph, and nothing inside the tile is a link

Scenario: The feed pages like the composite's ledgers

  • WHEN the deployment has more activity than one page of the feed holds
  • THEN the feed shows its first page with the pager's "Showing" line and page controls under the table, and turning the page re-renders only the feed's panel