# 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, 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. Rendered group headings in the sidebar are NOT required by this specification; implementations MAY render a flat link list as long as the within-group ordering is preserved. The sidebar render order SHALL be Runtime → Catalog → Integration (frequency-weighted; runtime is the hot path). #### Scenario: Catalog capabilities are grouped together - **WHEN** an operator views the operator panel navigation - **THEN** `org_types`, `products`, `entitlement_sets`, and `plan_ladders` appear in the Catalog group - **THEN** none of them appear outside the Catalog group #### Scenario: Runtime capabilities are grouped together - **WHEN** an operator views the operator panel navigation - **THEN** organizations, grants, and billing read-views (accounts, subscriptions, invoices, payments) appear in the Runtime group - **THEN** none of them appear outside the Runtime group #### Scenario: Integration capabilities are grouped together - **WHEN** an operator views the operator panel navigation - **THEN** FedWiki sites appears in the Integration group - **THEN** future integrations attach to the Integration group rather than a new top-level group #### Scenario: Sidebar renders runtime above catalog - **WHEN** an operator views the operator panel sidebar - **THEN** Runtime links render above Catalog links - **THEN** Catalog links render above Integration links #### Scenario: Group headings are optional - **WHEN** an implementation renders the sidebar as a flat link list without visible group headings - **THEN** the spec is still satisfied, provided the runtime → catalog → integration render order is preserved - **THEN** machine-readable IA-position declarations (positional tuples per the breadcrumb requirement) carry the grouping contract ### 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: a lookup affordance, an at-a-glance overview region, a recent-activity timeline, and a system-status region. The landing-surface lookup affordance SHALL be a text input that accepts a person name (substring), an email address (exact), or an organization slug (exact). Submitting the input SHALL resolve the entry to one of: `/operator/persons/{personID}` (when a person match is unique), `/operator/organizations/{orgID}` (when an org slug matches), or an inline disambiguation listing (when multiple persons match a name substring). Resolution SHALL be performed server-side; no client-side autocomplete is required. The at-a-glance region SHALL present headline metrics spanning the deployment's membership, organizations, catalog, billing, and entitlement-delivery state, each sourced from its owning module's query layer. The organizations headline 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. Billing coverage SHALL include both commitment and receivables. Commitment SHALL render as monthly-normalized recurring money across active subscriptions: monthly prices at face value, yearly prices at one-twelfth, each times item quantity; trialing subscriptions SHALL be excluded from the money and stated in the caption. Receivables SHALL render as a count of invoices in `open` status (issued and awaiting payment) with the outstanding balance in the caption. Money values SHALL be summed per currency with the headline showing the largest currency bucket; when other currency buckets exist the caption SHALL acknowledge them, and cross-currency conversion is explicitly out of scope for this surface. The headline row is reserved for core capability state: per-feature counts (such as domain claims) SHALL NOT render as headline tiles — feature moderation surfaces are reached from the sidebar, not the landing row. Where a count's owning capability has a browse surface, its tile SHALL link there. 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. Any count presented as "currently delivering" SHALL derive from `pool_provisions.status` (delivery state); it SHALL NOT be computed from `grants.status` alone, which is an issuance ledger, not current state. 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. The system-status region SHALL list every registered integration provider (registry-driven, not a hardcoded subset). For each provider it SHALL show two honestly-labeled facts: the **registry status** (the operator-managed lifecycle value, labeled so it cannot read as a health-check result) and a **configuration-readiness signal** derived from the same required-key resolution the settings pages perform (a provider with any required key unresolved is visibly not configured). The region's copy SHALL NOT describe this state as "connected" or as being "read live" — the console performs no connectivity checks and the status is a stored value. The region SHALL report integration outbox depth split into pending, retrying, and dead-letter buckets, and its caption SHALL name which integrations' queued work the outbox actually covers, so an all-quiet outbox cannot imply health for integrations that do not enqueue through it. Only the dead-letter bucket — work that has exhausted retries — SHALL receive alarm styling; pending and retrying are normal draining states. The landing surface's heading hierarchy SHALL descend without skipping levels (H1, then H2, …). 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 an organization lookup entry point - **THEN** the landing surface includes an at-a-glance region of headline metrics - **THEN** the landing surface surfaces a single recent-activity section interleaving grants, transitions, invoices, and payments - **THEN** the landing surface includes a system-status region #### 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) - **THEN** the panel issues a 302 redirect to `/operator/persons/{personID}` for that match #### Scenario: Lookup input resolves an organization slug - **WHEN** an operator submits the lookup input with a value that matches an organization slug exactly - **THEN** the panel issues a 302 redirect to `/operator/organizations/{orgID}` for that organization #### 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 - **WHEN** an operator submits the lookup input with a value that matches no person or organization - **THEN** the panel renders the landing surface with a visible no-match message - **THEN** no redirect occurs #### 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: 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: Billing commitment renders as monthly-normalized recurring money - **WHEN** an operator views `/operator` with active subscriptions on monthly and yearly prices - **THEN** the recurring-money headline sums monthly prices at face value and yearly prices at one-twelfth, times item quantity, over active subscriptions only - **THEN** trialing subscriptions appear in the caption count but contribute nothing to the money - **WHEN** the active subscriptions bill in more than one currency - **THEN** the headline shows the largest currency bucket and the caption acknowledges the other buckets #### Scenario: Billing coverage includes a receivables headline - **WHEN** an operator views `/operator` - **THEN** the at-a-glance region includes a tile counting invoices in `open` status - **THEN** the tile links to `/operator/billing/invoices` - **THEN** its caption states the outstanding balance and what was counted (issued and awaiting payment) #### 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: Delivering-grants headline counts deliberate grants - **WHEN** an operator views `/operator` - **THEN** the delivering-grants headline counts deliberately issued (non-default) delivering grants — the set an operator acts on - **THEN** its caption names how many delivering signup defaults were excluded, so the full delivering picture stays one glance away #### Scenario: Headline row carries no per-feature counts - **WHEN** an operator views `/operator` - **THEN** the at-a-glance region renders no domain-claims tile or other per-feature count - **THEN** domain moderation remains reachable from the sidebar's Domains entry #### 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: Delivering count reflects provisions, not the grant ledger - **WHEN** a grant row is `active` in `grants.status` but has no active row in `pool_provisions` - **THEN** the "delivering" headline count does not include it #### Scenario: System region lists all registered providers with honest state - **WHEN** an operator views `/operator` with multiple integration providers registered - **THEN** the system-status region lists every registered provider with its registry status under a label that cannot read as a health check - **THEN** a provider with an unresolved required key shows a visible not-configured signal - **THEN** the region's copy makes no "connected" or "read live" claim #### Scenario: Outbox caption names its coverage - **WHEN** an operator views the system-status region's outbox report - **THEN** the caption names which integrations' queued work the outbox covers - **THEN** an empty outbox does not present itself as evidence that uncovered integrations are healthy #### Scenario: Only dead-letter outbox work raises alarm styling - **WHEN** the integration outbox contains pending and retrying entries but no dead-letter entries - **THEN** the outbox report renders without alarm styling - **WHEN** the integration outbox contains at least one dead-letter entry - **THEN** the dead-letter bucket renders with alarm styling #### 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: 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. The set of top-level capability routes SHALL include: - `/operator` (curated landing surface — see the landing-surface 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) - `/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: 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: Operator-facing persons surface is lookup-only The operator panel SHALL NOT expose a top-level persons browse route (no `/operator/persons` listing). The only operator-facing persons surface SHALL be `/operator/persons/{personID}` — a resolved-detail view for one person, reachable from the landing-surface lookup affordance, from organization member tables, and by direct URL. The person-detail view SHALL include an organization-memberships section listing each organization the person belongs to. Each membership row SHALL link to that organization's composite view at `/operator/organizations/{orgID}`. #### Scenario: No top-level persons browse exists - **WHEN** an operator views the operator panel navigation - **THEN** no sidebar link labeled "People" or "Persons" leads to a flat browse listing of all persons - **THEN** the visible "People" affordance, if present, is a lookup entry point on the landing surface, not a browse link #### 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 - **THEN** no prior navigation through a persons browse is required #### 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: 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 operator panel SHALL expose a read-only plan-topology overview addressable at `/operator/plan-topology`, grouped within the **Catalog** information-architecture group alongside `plan_ladders` (configuration entities edited rarely with wide blast radius). It SHALL be reachable from operator navigation and SHALL NOT appear at the operator panel's top level outside the Catalog group. As a full operator page it SHALL participate in `hx-boost` in-panel navigation and declare its IA position for breadcrumb derivation. #### Scenario: Topology overview is grouped under Catalog - **WHEN** an operator views the operator panel navigation - **THEN** the plan-topology overview appears within the Catalog group - **THEN** it does not appear outside the Catalog group #### Scenario: Topology overview is addressable and boosted - **WHEN** an authenticated operator navigates to `/operator/plan-topology` - **THEN** the panel renders the topology overview as a full operator page - **THEN** the page declares its IA position for breadcrumb derivation ### 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 labeled "Integrations" entry that links to a section landing at `/operator/integrations`, with the registered provisioning providers listed beneath it; integration admin pages and settings pages SHALL mark the parent "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, status, kind, declared lifecycle operations, owned resource keys, a Manage link to its admin surface (when the provider declares one), and a Settings link to `/operator/integrations/{slug}/settings` (when the integration declares configuration — sourced from the adapter registry). Integration admin pages SHALL live under the `/operator/integrations/` URL namespace, SHALL open with a heading identifying the integration, 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 section and 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 an "Integrations" entry linking to `/operator/integrations` - **AND** each registered provisioning provider with an admin surface SHALL appear as a link beneath it #### 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, status, kind, declared lifecycle operations, and owned resource keys - **AND** for FedWiki the row SHALL link to `/operator/integrations/fedwiki` - **AND** a payments provider (e.g. Stripe) SHALL have a row even though it declares no admin surface - **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 in the sidebar Integrations section and on the `/operator/integrations` landing - **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 link to `/operator/integrations/{slug}/settings` on its row - **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 ### 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 foregrounds setup progress while setup is incomplete While any **required** step of the operator setup checklist (see `operator-setup-checklist`) derives incomplete, the curated landing surface at `/operator` SHALL render a setup-progress region ahead of the metric tiles, summarizing which steps are complete and linking each incomplete step to the surface that completes it (or linking the checklist surface as a whole). Once every required step derives complete, the region SHALL NOT render, even when conditional steps remain. The region derives from the same predicates as the checklist, so the landing surface and the checklist never disagree. #### Scenario: Fresh instance leads with setup, not zeros - **WHEN** an operator visits `/operator` on a deployment where no checklist step is complete - **THEN** the setup-progress region renders ahead of the metric tiles - **AND** it links the first incomplete steps to their completing surfaces #### Scenario: Configured instance shows no setup region - **WHEN** an operator visits `/operator` on a deployment where every required checklist step derives complete - **THEN** the landing surface renders without the setup-progress region