Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-FE-MULTI-TENANT-COCKPIT-AGGREGATE-AND-DEEPLINK — aggregate cost panel + tenant deep-link URLs#208

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-MULTI-TENANT-COCKPIT-AGGREGATE-AND-DEEPLINK-MEGABUCKET
May 24, 2026
Merged

feat(kora): KR-FE-MULTI-TENANT-COCKPIT-AGGREGATE-AND-DEEPLINK — aggregate cost panel + tenant deep-link URLs#208
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-MULTI-TENANT-COCKPIT-AGGREGATE-AND-DEEPLINK-MEGABUCKET

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Two follow-on deliverables from the #207 CC#2 hand-off — both small, parallel, ship without waiting on other lanes.

Deliverable A — "All tenants" aggregate cost panel. When the operator picks the aggregate pseudo-option in the tenant picker, /cost-state now renders side-by-side per-tenant cost cards (one card per tenant) sourced from the snapshot v6 cost_ladder_by_tenant block. Aggregate footer surfaces total spent + combined credit pool. DashboardPage's small Cost card shows a compact one-line aggregate summary in the same mode, linking through to /cost-state for the breakdown.

Deliverable B — Deep-link URLs + active-tenant header badge. ?tenant=marvin (and ?tenant=all for aggregate) deep-links work on every tenant-scoped page. Every audit + cost + promotion page header gets an <ActiveTenantBadge> that surfaces the current tenant + opens the sidebar picker on click + copies a share-URL via the Clipboard API (with prompt() fallback for blocked contexts). Hidden on single-tenant deployments.

What changed

web/src/hooks/useActiveTenant.ts

  • ALL_TENANTS_URL_ALIAS = \"all\" — operator-readable URL form for the aggregate sentinel. Internal sentinel stays \"__all__\" so it can't collide with a real tenant_id named "all". useResolvedTenant translates the alias on read.
  • tenantToUrlValue() helper round-trips sentinel → alias for share-URL construction.
  • OPEN_TENANT_PICKER_EVENT custom-event name + requestOpenTenantPicker() helper — page-header badges dispatch this to ask the sidebar picker to open. Decouples badge (per-page) from picker (sidebar) without prop-drilling.

web/src/components/TenantPicker.tsx

  • Listens for OPEN_TENANT_PICKER_EVENT and sets open=true when fired.

web/src/components/ActiveTenantBadge.tsx (new)

  • Compact Viewing: <tenant> chip + Copy-share-URL button.
  • Hidden when loadingTenants || !isMultiTenant (B.2 single-tenant degradation).
  • Share-URL uses navigator.clipboard.writeText in secure contexts; falls back to window.prompt() for blocked clipboard / non-HTTPS.
  • Re-exports ACTIVE_TENANT_BADGE_USES_SENTINEL = ALL_TENANTS_SENTINEL so the drift-guard test pins the badge ↔ hook constant tie.

web/src/components/AggregateCostCards.tsx (new)

  • Fetches /api/snapshot once (re-fetches on parent-supplied reloadKey bump).
  • Renders one <TenantCostCard> per tenant from availableTenants (default-first, then alphabetical). Empty-state card for tenants listed but absent from cost_ladder_by_tenant.
  • Per-card: tenant name + canonical-tenant chip for default + tier badge + spent / pool + pct-used progress bar with tone tracking the rung.
  • Aggregate footer (when ≥2 summable tenants): total spent across all tenants + combined credit pool + skipped-count for tenants with no numeric data yet.
  • Italic foot-note explaining what's not in the aggregate view (per-tenant deferred tickets / reconciliation history / rate-limit pulse live on the single-tenant view — picker swap to a specific tenant for those).

web/src/pages/CostStatePage.tsx

web/src/pages/DashboardPage.tsx

  • Captures snap.cost_ladder_by_tenant into local state during loadInitial.
  • Cost DashboardCard title becomes \"Cost (all tenants)\" in aggregate mode; body switches to <AggregateCostCardBody> showing \"$N / $M, K tenants · click for breakdown\". Falls back to the single-tenant body when the snapshot's by-tenant block isn't populated (single-tenant deployments).

Audit + promotion page headers

ProbeInvestigations, AlertInvestigations, EmailIntentLog, OutboundEmailLog, Autofix, KoraActions, PromotionReview — each H2 now sits inside a <div className=\"flex items-center gap-2 flex-wrap\"> alongside <ActiveTenantBadge />. URL ?tenant= deep-link already works (the #207 hook resolves URL → localStorage → default); the badge surfaces which tenant is active in the page chrome.

tests/test_tenants_endpoint.py

  • New test_fe_aggregate_and_deeplink_pins pinning:
    • ALL_TENANTS_URL_ALIAS = \"all\" (URL form)
    • ALL_TENANTS_SENTINEL = \"__all__\" (internal form)
    • OPEN_TENANT_PICKER_EVENT = \"kora:open-tenant-picker\" name
    • ACTIVE_TENANT_BADGE_USES_SENTINEL = ALL_TENANTS_SENTINEL re-export in badge
    • cost_ladder_by_tenant literal grep in AggregateCostCards (asserts canonical data-source choice)

Deep-link examples

  • /probe-investigations?tenant=marvin — investigations scoped to marvin (badge: Viewing marvin)
  • /cost-state?tenant=all — aggregate cost cards (badge: Viewing All tenants)
  • /kora-actions?tenant=default — explicit default view (badge: Viewing default, only if multi-tenant)

URL precedence (from #207): URL ?tenant= → localStorage → "default". Picker selections write localStorage but do NOT mutate the URL — a shared deep-link continues to anchor the page even after the operator clicks a different tenant.

Build

  • tsc -b && vite build ✓ clean (one pre-existing chunk-size warning; not introduced by this bucket).
  • python3 -m py_compile tests/test_tenants_endpoint.py ✓.
  • Pytest runtime not available in CC#2 worktree (no .venv); CI runs it on PR.

Test plan

  • ?tenant=marvin direct link → useActiveTenant returns \"marvin\"; picker shows marvin selected; audit endpoints get ?tenant_id=marvin
  • ?tenant=all direct link → isAllTenants=true; badge shows "All tenants"; cost-state renders aggregate cards
  • Direct link without ?tenant= → falls back to localStorage → "default"
  • Single-tenant deployment: badge hidden everywhere (mirrors picker auto-hide)
  • 2+ tenant deployment + All tenants pick:
    • CostStatePage shows N cards, default-first
    • aggregate footer surfaces $X across N tenants this period · $Y combined credit pool
    • DashboardPage Cost card shows compact $X / $Y, N tenants · click for breakdown
  • Mobile viewport (< md breakpoint) → aggregate cards stack 1-up
  • Copy-share-URL: badge button copies <current URL>?tenant=<active> to clipboard; non-secure context falls back to prompt()
  • Click badge → sidebar picker opens
  • pytest tests/test_tenants_endpoint.py — 6 tests pass (3 endpoint + 3 drift-guard, +1 new aggregate/deeplink pin)

Recommendation for next CC#2 dispatch

Per-tenant audit BE filter verification (depends on CC#1 NousResearch#447 landing). Once CC#1's per-tenant audit JSONL paths + ?tenant_id= BE filter ship, add a small verification bucket: spin up two tenants, fire seam events for each, assert each FE audit page (with the existing ?tenant_id= forwarding from #207) renders only the matching subset. ~½ day. Tightens the contract that's currently "BE may ignore the param" into "BE filters correctly."

Lighter follow-on if NousResearch#447 hasn't landed yet: TenantPicker keyboard nav + URL-write toggle. Currently the picker is mouse-only and selections never mutate the URL. Add (a) ↑/↓/Enter/Esc keyboard nav and (b) an opt-in "also update URL" toggle in the picker so deep-link-anchored pages can decouple if the operator wants. ~3 hours.

🤖 Generated with Claude Code

…gate cost panel + tenant deep-link URLs

Deliverable A — "All tenants" aggregate cost panel:
  * AggregateCostCards component renders one card per tenant
    from snapshot v6 cost_ladder_by_tenant block (#206 sibling)
  * default-first then alphabetical render order; calm empty
    card for tenants observed via /api/tenants/list but absent
    from the snapshot (no holder activity yet)
  * aggregate footer: total spent + combined credit pool +
    skipped-count when some tenants have no numeric data
  * responsive 1/2/3-up grid (mobile / tablet / desktop)
  * CostStatePage early-exits the per-tenant /api/cost-state
    fetch when isAllTenants — no misleading default-tenant
    payload pinned to page state
  * DashboardPage Cost card shows compact one-line aggregate
    summary ("$N / $M, K tenants · click for breakdown") when
    isAllTenants; full per-tenant grid lives on /cost-state

Deliverable B — Deep-link URLs + active-tenant header badge:
  * ?tenant=all URL alias resolves to ALL_TENANTS_SENTINEL
    (operator-readable in shared URLs; internal sentinel still
    `__all__` to avoid collision with real tenant_id "all")
  * tenantToUrlValue() helper round-trips sentinel → alias for
    share-URL construction
  * ActiveTenantBadge component in every audit + cost +
    promotion page header — clickable opens sidebar picker via
    OPEN_TENANT_PICKER_EVENT custom event; Copy-share-URL
    button with prompt() fallback for blocked clipboard API
  * Hidden on single-tenant deployments (isMultiTenant gate
    mirrors the picker's auto-hide behavior)

Drift-guard extension (tests/test_tenants_endpoint.py):
  * ALL_TENANTS_URL_ALIAS = "all" pin (URL form)
  * ALL_TENANTS_SENTINEL = "__all__" pin (internal form)
  * OPEN_TENANT_PICKER_EVENT name pin (badge ↔ picker contract)
  * ACTIVE_TENANT_BADGE_USES_SENTINEL re-export pin (badge
    participates in the same constant set as the hook)
  * AggregateCostCards reads cost_ladder_by_tenant block pin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant