# Operator Panel — Information Architecture > **Status:** Decision record for M7 phase 7b (in progress). > **Companion artifacts:** OpenSpec change [`operator-ia`](../openspec/changes/operator-ia/) (proposal, design, specs delta, tasks). > **Builds on:** [`docs/operator-ux-research.md`](operator-ux-research.md) — the M7a research synthesis whose findings this document commits to as a contract. ## Purpose The operator panel grew organically into a flat 12-tab SPA. M7a's research named the seam — *catalog vs. runtime vs. integration* — and surfaced the hot paths (per-org lookup, grant adjustments) and the silent-correctness risks (two non-equivalent grant-revoke paths). This document captures the information-architecture decisions that subsequent M7 phases (7c design system, 7d SPA→MPA conversion, 7e form & action patterns) and the member-side milestones (M8 plan catalog, M9 usage views) inherit. It is the contract those phases must honor. The contract is documented twice: - **Here** (`docs/operator-ia.md`): narrative rationale, links to research, deferred items, future direction. - **In the spec** (`openspec/specs/operator-panel-navigation/spec.md` after archive): testable requirements with scenarios. When the two diverge, the spec wins. This document explains *why*; the spec defines *what*. ## The three-layer model The operator panel's top level has exactly three groups. The grouping is not aesthetic — it follows the dependency direction the data model already encodes ([`operator-ux-research.md`, "The hidden IA"](operator-ux-research.md#the-hidden-ia-catalog-vs-runtime)). ``` ┌─────────────────────────────────────────────────┐ │ CATALOG (rare edits, blast radius wide) │ │ org_types ─▶ products ─▶ entitlement_sets │ │ └─▶ plan_ladders │ └────────────────────────┬────────────────────────┘ │ one-way cascade ▼ ┌─────────────────────────────────────────────────┐ │ RUNTIME (per-org churn, per-ticket work) │ │ organizations · grants · billing read-views │ └─────────────────────────────────────────────────┘ ─ ─ ─ orthogonal axis (not part of the cascade) ─ ─ ─ INTEGRATION FedWiki sites today; NextCloud, Discourse, others later ``` ### Catalog Configuration entities edited rarely, with effects that cascade across all organizations. Mistakes here have wide blast radius (a product edit can change every grant referencing it). Members: `org_types`, `products`, `entitlement_sets`, `plan_ladders` (and their nested `tiers` / `rules`). ### Runtime Per-organization operational work. The dominant workflow ("what's the state of org X?") lives here, alongside grant adjustments and read-only billing views (Stripe is the source of truth — the operator panel surfaces but does not mutate billing state directly). Members: organizations (browse + per-org composite view), global grants (read-only browse — see §"Grant revocation" below), billing accounts, billing subscriptions, billing invoices, billing payments. ### Integration System-specific surfaces that don't fit the catalog/runtime cascade. Today this group is FedWiki-only; the axis exists so future integrations (NextCloud, Discourse, etc.) attach here rather than as new top-level groups. The pervasive FedWiki-only assumption in the codebase ([open issue: "FedWiki-only integration assumption pervades UI and data patterns"](../status/issues.md)) is tracked separately and is **not** addressed by this change — M7b reserves the slot; deeper rework arrives with the second integration. Members: FedWiki sites. ## Route hierarchy Top-level routes mirror the three-layer model. The IA contract is the route *shape*; the implementation rewrite that retires `?tab=` and turns these into real HTTP routes lives in M7d (see [§"SPA → MPA transition"](#spa--mpa-transition) below). ``` /operator curated landing surface /operator/organizations runtime · browse /operator/organizations/{orgID} runtime · per-org composite view ◀── hot path /operator/grants runtime · global grants browse (read-only) /operator/billing/accounts runtime · billing read-views /operator/billing/subscriptions /operator/billing/invoices /operator/billing/payments /operator/org-types catalog /operator/products catalog /operator/entitlement-sets catalog /operator/plan-ladders catalog /operator/fedwiki-sites integration ``` These names are the canonical route slugs. The current `?tab=` IDs (`tab-1`, `users`, etc.) are SPA-era artifacts and are not part of this contract; 7d's MPA conversion replaces them. ## The curated landing surface `/operator` renders a curated landing page, not whichever tab happens to load first. The page surfaces hot-path entry points so the dominant workflows reach their target screen in ≤ 2 navigation steps from the landing. Required content categories: - **Organization lookup** — search/jump-to-org entry for hot path #1. - **Recent runtime activity** — recent grant transitions, subscription state changes. - **Recent billing activity** — recent invoices, payment events. Visual treatment and component choice are 7c's call. The IA requirement is only that these categories appear and that the landing is the default destination for `/operator`. ## The per-organization composite view `/operator/organizations/{orgID}` is the addressable home for runtime work on one organization. It co-locates: - Enrollment state (active grants, current org-type defaults). - Grant history (issued, transitioned, revoked). - Billing summary (current subscription, recent invoices). - Grant actions (issue, extend, revoke — see [§"Grant revocation"](#grant-revocation) below). The route is bookmarkable and reload-stable. Sub-page splits (vertical sections, accordion panels, sub-routes) are 7d's implementation call; the IA contract is satisfied as long as the URL exists and co-locates this work. ## Grant actions Today both grant *issuance* and grant *revocation* split across global and per-org surfaces, but the splits have different root causes: - **Issuance** ([`status/issues.md` open issue "Operator grant-issuance: two non-overlapping surfaces…"](../status/issues.md)). Global Grants tab `CreateGrant` lets the operator pick any product or entitlement set; per-org Enrollment `IssueGrant` restricts the picker to plan-typed products attached to the org's ladder. Neither surface cross-links to the other, and the operator must already know the distinction to choose correctly. The two code paths exist for a real reason: per `membcons-db` Doc 35, plan products are identified *structurally* by `plan_ladder_tiers` membership, while non-plan products (`addon`, `usage`, `one_time`) carry a labeled `product_type`. `IssueGrant` is the ladder-routing path for plans; `CreateGrant` is the legacy path that handles the three labeled kinds. - **Revocation** ([`status/issues.md` open issue "Two grant-revoke paths…"](../status/issues.md)). Global Grants tab `RevokeGrant` does a simple entitlement recalculation; per-org Enrollment `RevokeGrantAndTransition` is composite — the pool returns to the org-type default *and* a transition is recorded. Unlike issuance, the two paths are non-equivalent in effect; choosing the wrong one is silent-correctness risk. - **Extension.** Single path, not contested. The UI hides both distinctions today. Operators have to internalize the difference between the surfaces every time, with no in-product affordance to guide the choice. **Decision:** all grant action affordances — issue, extend, revoke — live exclusively on the per-organization composite view at `/operator/organizations/{orgID}`. The global Grants surface becomes read-only: it remains in the Runtime group for cross-org browse/audit/lookup, but exposes no Issue, Extend, or Revoke affordance. The three actions consolidate differently: - **Issuance** — both code paths persist. The per-org view exposes two distinct forms, labeled by intent: - *Grant a plan* — uses `IssueGrant`; product picker restricted to plan-typed products on the org's ladder. - *Grant a non-plan product* — uses `CreateGrant`; product picker restricted to products with `product_type ∈ {addon, usage, one_time}`. Labeling the forms by *what kind is being granted* rather than *which surface they live on* lets the operator choose by intent instead of by tab. The two code paths align with the M6a product-type narrowing; consolidating them is out of scope here (see [§"Related: product kind taxonomy"](#related-product-kind-taxonomy) below). - **Revocation** — consolidates to `RevokeGrantAndTransition` (composite). The simple `RevokeGrant` HTTP handler may stay reachable by direct URL for programmatic callers; it is no longer a UI affordance. - **Extension** — co-located on the per-org view alongside issuance and revocation. Single path, no consolidation needed. This is a contract decision. 7d implements the affordance removals from the global Grants surface and the dual-form issuance UI as part of the MPA rewrite. ### Related: product kind taxonomy The two issuance code paths and the structural-vs-labeled product kind distinction are explained in upstream `membcons-db` Doc 35 (*Product Kind Taxonomy: Why `product_type` and `product_kinds` Coexist*). The IA decision here only governs UI surface; the underlying read-path discipline (when to read `billing.product_kinds` view vs. `products.product_type` column) is tracked separately as a member-console tech-debt item in [`status/issues.md`](../status/issues.md). M7b does not adjudicate that question — it just acknowledges that the two issuance forms reflect a real schema-level distinction, not redundancy. ## Breadcrumb positional contract Every operator page declares its position in the IA as a tuple: ``` (group, capability) for capability index pages (group, capability, instance) for entity-scoped pages ``` `group ∈ { catalog, runtime, integration }`. `capability` is the route slug (`products`, `organizations`, etc.). `instance` is present when the URL is scoped to one entity (e.g. the per-org composite view). Examples: | Route | Position | |---|---| | `/operator/products` | `(catalog, products)` | | `/operator/plan-ladders/{ladderID}` | `(catalog, plan-ladders, {ladderID})` | | `/operator/organizations` | `(runtime, organizations)` | | `/operator/organizations/{orgID}` | `(runtime, organizations, {orgID})` | | `/operator/fedwiki-sites` | `(integration, fedwiki-sites)` | The *visual* breadcrumb rendering (separator character, truncation rules, current-page styling) is 7c's call. M7b defines the positional vocabulary, not the render. Pages declare their position; the render layer consumes it. ## Reserved query-parameter real estate Browse routes reserve canonical filter parameter names so future filter UX can attach without URL-breaking changes or spec rewrites: | Parameter | Purpose | Status | |---|---|---| | `?q=` | Free-text search across the route's primary entity | Reserved; UX deferred | | `?org_type=` | Filter by org type (where applicable) | Reserved; UX deferred | | `?lifecycle_status=` | Filter by lifecycle status (products, etc.) | Reserved; UX deferred | Reservation rules: 1. Browse routes MUST NOT use these parameter names for unrelated purposes. 2. Routes MUST accept the reserved params without error even when no filter UI exists yet — unimplemented filters are silently ignored, not rejected. 3. Adding visible filter controls is a future change (likely 7d or a follow-up); the URL contract is locked in now. Search/filter UX itself is deferred — [`operator-ux-research.md` "Open threads"](operator-ux-research.md#open-threads-handed-to-7b7e) handed only the URL real estate to 7b; the visual surface is for a later milestone. ## SPA → MPA transition Until 7d ships, `openspec/specs/operator-panel-navigation/spec.md` contains two competing contracts: - The legacy SPA contract (tab state in `?tab=`, lazy first-reveal partial loads, cross-tab `HX-Trigger` refresh mesh) — unchanged by this change. - The new IA contract from this change (three-layer grouping, landing surface, per-org composite route, breadcrumb positions, reserved filter params, grant-revoke single entry point) — added by this change. This dual-contract state is intentional and short-lived. 7d's BREAKING change removes the SPA contract; the IA contract is what survives. New code written between archive of this change and ship of 7d should target the IA contract; the SPA contract is *running* behavior, not *target* behavior. ## What this change does *not* do The contract is deliberately narrow. The following are explicitly out of scope for M7b: - **Component primitives, typography, spacing tokens, accessibility patterns.** 7c. - **Route rewiring, `?tab=` removal, partial-loader retirement, MPA conversion.** 7d. - **Form patterns, confirmation modals, destructive-action guards, inline validation, success/failure feedback.** 7e. - **A11y audit, keyboard-nav pass, convention-drift guard.** 7f. - **Action-counter telemetry, audit log surfacing, frequency validation.** M11 owns this work; M7b explicitly stays on M7a's estimated frequencies. Decision: no telemetry spike in M7. - **FedWiki-sites internals or generalizing the integration model.** The axis is reserved; the rework arrives with the second integration. - **Search/filter visible UX.** Only URL real estate is reserved. ## Deferred decisions and open threads - **Sub-page structure under `/operator/organizations/{orgID}`** — single page vs. vertical sections vs. sub-routes is 7d's call. The IA contract is the URL and the co-located content; the page shape is implementation. - **Catalog-side landing summaries** — whether `/operator/products`, `/operator/plan-ladders`, etc. need their own most-edited / recently-changed summaries is a 7c/7d decision. Not a contract concern. - **Integration-axis future shape** — the second integration's arrival will force a deeper rethink. Tracked under the [open issue "FedWiki-only integration assumption pervades UI and data patterns"](../status/issues.md). - **Search/filter UX** — beyond the URL reservation here, the visible surface is deferred. ## Future direction The IA is a decision record, not an immutable schema. If a future capability genuinely does not fit catalog, runtime, or integration, the right move is a follow-up `operator-ia` change adding a fourth group — not stretching one of the existing groups to absorb it. The contract is the *layering principle* (one-way cascade between configuration and operations, integrations orthogonal), not the specific count of groups. Member-side UX (M8 plan catalog, M9 usage views) reuses the layering vocabulary and the design system from 7c. The catalog/runtime split applies cleanly: M8's plan catalog is a member-facing read of the catalog layer; the upgrade/downgrade flows live in the runtime layer of the member surface. This is the reuse the M7 foundation was set up to enable.