# 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=` and `?=` (where `` 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/` 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 ``), 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=` 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=` 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=` 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 #### Scenario: Settings links cover every configuring adapter - **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 `` 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` ### Requirement: Landing surface orders attention before totals and links its content 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 #### Scenario: Feed entries link to their subjects - **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 `

` 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 #### Scenario: A tile is one link, marked - **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