console: one word for the platform-sudo gate — SuperAdmin
The gate had three names for one concept: the component was OperatorAccessRequired, the headline said 'Operator access required', the body said 'an admin role', and the predicate underneath was useIsSuperAdmin. Lux carried a fourth copy — its own hardcoded 'Operator access required' whose body told a signed-in operator to 'sign in with an operator account', which this repo's own P1 rule forbids: a 403 is signed-in-but-not-authorized, never a sign-in prompt. The structure was already one-way — one component, one predicate across 27 call sites. Only the naming forked. So: SuperAdminRequired, and the headline is a single exported SUPERADMIN_REQUIRED that Lux's error card now shares, so the string cannot drift again. The body states what the predicate actually tests — membership of the reserved admin org. Also adds an e2e for the research dashboard, written so it can fail. The console is a SPA behind a catch-all: every path returns 200, so a status-code test passes after the route is deleted. And /research is behind AuthGate, so an anonymous visitor sees neither the dashboard nor the gate — measured, the body reads 'Sign in to your account'. The two tests that run therefore prove the gate holds and that a nonsense path renders no dashboard; each anchors on the shell having rendered first, because an absence assertion is otherwise satisfied by a dead host. Verified: green against cloud.hanzo.ai, both red against an unreachable one. Proving the dashboard paints needs a SuperAdmin session, so that test is staged behind HANZO_PASSWORD rather than faked green.
This commit is contained in:
@@ -149,7 +149,7 @@ test.describe('(A) fixture render — model mix (fable-5 75%) + 62% margin + hon
|
||||
|
||||
// Page rendered (not the operator gate).
|
||||
await expect(page.getByText('AI Economics').first()).toBeVisible()
|
||||
await expect(page.locator('text=/Operator access required|not authorized|access denied/i')).toHaveCount(0)
|
||||
await expect(page.locator('text=/SuperAdmin access required|not authorized|access denied/i')).toHaveCount(0)
|
||||
|
||||
// (a) model mix — the mocked rows WITH request-share %.
|
||||
const modelMix = page.getByTestId('model-mix')
|
||||
|
||||
@@ -120,7 +120,7 @@ async function auditSurface(page: Page, slug: string, name: string, marker: RegE
|
||||
// Honest states that count as a truthful render for ANY surface (real content is added
|
||||
// per-surface). Kept in ONE place so every marker is consistent.
|
||||
const HONEST =
|
||||
'Add credits|Your session expired|Access required|Not enabled|Not available on this deployment|initializing|runtime|managed by Hanzo|Connected|Operator access|No .* yet|not connected|not configured|Sign in'
|
||||
'Add credits|Your session expired|Access required|Not enabled|Not available on this deployment|initializing|runtime|managed by Hanzo|Connected|SuperAdmin access|No .* yet|not connected|not configured|Sign in'
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════════
|
||||
// A. UNAUTHENTICATED fail-closed proof — ALWAYS runs (no credentials required).
|
||||
@@ -314,9 +314,9 @@ test.describe.serial('Billing / Settings / Usage / o11y render smoke (authentica
|
||||
test('Alerts renders (alerting rules or honest state)', async () => {
|
||||
await auditSurface(page, 'alerts', 'Alerts', new RegExp(`Alert|rule|notification|${HONEST}`, 'i'))
|
||||
})
|
||||
test('Fleet Observability renders (global-admin board or honest operator-access state)', async () => {
|
||||
// For a non-global-admin this is honestly `OperatorAccessRequired` — that IS a pass.
|
||||
await auditSurface(page, 'fleet-o11y', 'Fleet Observability', new RegExp(`Fleet Observability|Requests|Tokens|Latency|Top organizations|Operator access|${HONEST}`, 'i'))
|
||||
test('Fleet Observability renders (global-admin board or honest superadmin-access state)', async () => {
|
||||
// For a non-global-admin this is honestly `SuperAdminRequired` — that IS a pass.
|
||||
await auditSurface(page, 'fleet-o11y', 'Fleet Observability', new RegExp(`Fleet Observability|Requests|Tokens|Latency|Top organizations|SuperAdmin access|${HONEST}`, 'i'))
|
||||
})
|
||||
|
||||
// ── AGGREGATE — the dead-card audit. FLAGS every surface that showed a dead
|
||||
|
||||
@@ -183,7 +183,7 @@ test.describe('Insights renders on admin.hanzo.ai for the SuperAdmin (authentica
|
||||
{ id: 'service-map', label: 'Service Map', expect: /Service Map|Rate|Errors|Duration|p99|dependency|Observability|no telemetry|not enabled|initializing/i },
|
||||
{ id: 'logs', label: 'Logs', expect: /Logs|Application logs|Request activity|Severity|Message|no application logs|Observability|initializing/i },
|
||||
{ id: 'o11y', label: 'Traces', expect: /Traces|Trace|Latency|Tokens|Cost|Observability|No traces|initializing|not enabled/i },
|
||||
{ id: 'fleet-o11y', label: 'Fleet Observability', expect: /Fleet Observability|Requests|Tokens|Latency|Top organizations|operator access|not authorized/i },
|
||||
{ id: 'fleet-o11y', label: 'Fleet Observability', expect: /Fleet Observability|Requests|Tokens|Latency|Top organizations|superadmin access|not authorized/i },
|
||||
]
|
||||
for (const m of modules) {
|
||||
await page.goto(`${surface}/${m.id}`, { waitUntil: 'domcontentloaded' })
|
||||
|
||||
@@ -188,7 +188,7 @@ test.describe('(B) LIVE — admin gate + real DO data', () => {
|
||||
await expect(page).not.toHaveURL(/\/signin/, { timeout: 15_000 })
|
||||
// The board (not the operator-access gate) rendered for the SuperAdmin.
|
||||
await expect(page.locator('text=/Provider credit|Credit vs paid/i').first()).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.locator('text=/Operator access required/i')).toHaveCount(0)
|
||||
await expect(page.locator('text=/SuperAdmin access required/i')).toHaveCount(0)
|
||||
// Real DO data: the $26k grant / a do-ai card / glm-5.2 usage.
|
||||
await expect(page.locator('text=/do-ai|digitalocean/i').first()).toBeVisible({ timeout: 30_000 })
|
||||
await page.screenshot({ path: join(SHOTS, 'provider-billing-live-do.png'), fullPage: true })
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* The research dashboard — every benchmark run and its verdict.
|
||||
*
|
||||
* What this file can and cannot prove, stated plainly, because the difference is the
|
||||
* whole value:
|
||||
*
|
||||
* The console is a SPA behind a catch-all, so EVERY path returns HTTP 200 — including
|
||||
* `/definitely-not-a-page`. A test asserting "200" or "the page loaded" passes even when
|
||||
* the route is deleted. And `/research` is behind `AuthGate`, so an anonymous visitor is
|
||||
* redirected to sign-in and sees NEITHER the dashboard NOR the SuperAdmin gate. Measured,
|
||||
* not assumed: the body reads "Sign in to your account".
|
||||
*
|
||||
* So the two tests that RUN here prove security and routing, not rendering:
|
||||
* - anonymous callers see no corpus data (the gate genuinely holds)
|
||||
* - a nonsense path renders no dashboard (the assertion above can fail)
|
||||
* Proving the dashboard PAINTS needs a SuperAdmin session, so that test is staged behind
|
||||
* HANZO_PASSWORD rather than faked — the same staging `insights-o11y.spec.ts` uses.
|
||||
*
|
||||
* Run: BASE_URL=https://cloud.hanzo.ai npx playwright test research-dashboard
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.BASE_URL ?? 'https://cloud.hanzo.ai'
|
||||
|
||||
/** Copy rendered ONLY by ResearchModule. */
|
||||
const DASHBOARD = /Falsifiable R&D experiments/i
|
||||
/** Corpus numbers. Never visible to a caller who is not a SuperAdmin. */
|
||||
const CORPUS = [/\bProven\b/, /\bRefuted\b/, /\bAttempts\b/]
|
||||
|
||||
async function settle(page: import('@playwright/test').Page) {
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {})
|
||||
}
|
||||
|
||||
test.describe('research dashboard', () => {
|
||||
test('an anonymous caller sees no corpus data', async ({ page }) => {
|
||||
// The security assertion. The module gates on useIsSuperAdmin and the `research`
|
||||
// head is org-scoped server-side by the Bearer owner — so an unauthenticated
|
||||
// visitor must reach sign-in with zero experiment counts painted. A client-only
|
||||
// gate that rendered the KPI band behind a card would fail here.
|
||||
await page.goto(`${BASE_URL}/research`)
|
||||
await settle(page)
|
||||
const body = page.locator('body')
|
||||
|
||||
// ANCHOR FIRST. Every assertion below is an absence, and an absence is satisfied by
|
||||
// a blank page, a 502, or a dead host — so prove the app actually rendered before
|
||||
// claiming the gate held. Without this the test passes while the site is down.
|
||||
await expect(
|
||||
body.getByText(/Sign in|Log in/i).first(),
|
||||
'the console shell did not render — the absence assertions below would be vacuous',
|
||||
).toBeVisible({ timeout: 15_000 })
|
||||
|
||||
for (const label of CORPUS) {
|
||||
await expect(
|
||||
body.getByText(label),
|
||||
`"${label}" rendered to an anonymous caller — corpus data leaked past the gate`,
|
||||
).toHaveCount(0)
|
||||
}
|
||||
await expect(
|
||||
body.getByText(DASHBOARD),
|
||||
'the dashboard rendered to an anonymous caller — the auth gate is not holding',
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('is not a catch-all — a nonsense path renders no dashboard', async ({ page }) => {
|
||||
// The control that gives the authenticated test (below) its meaning: the SPA
|
||||
// answers 200 here too, so if this path could show the research surface, matching
|
||||
// on that copy would prove nothing about routing.
|
||||
await page.goto(`${BASE_URL}/definitely-not-a-page-9137`)
|
||||
await settle(page)
|
||||
|
||||
// Same anchor: the app must have rendered for "no dashboard here" to mean anything.
|
||||
await expect(
|
||||
page.locator('body').getByText(/Sign in|Log in/i).first(),
|
||||
'the console shell did not render — the absence assertion below would be vacuous',
|
||||
).toBeVisible({ timeout: 15_000 })
|
||||
|
||||
await expect(
|
||||
page.locator('body').getByText(DASHBOARD),
|
||||
'a nonsense path rendered the research dashboard — matching on that copy proves nothing',
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('renders the corpus for a SuperAdmin', async ({ page }) => {
|
||||
// STAGED, not skipped-and-forgotten: needs the reserved-admin SuperAdmin
|
||||
// credential, which lives in KMS and not on a dev host. With it, this is the
|
||||
// assertion that actually proves the dashboard paints real evidence.
|
||||
const password = process.env.HANZO_PASSWORD
|
||||
test.skip(!password, 'set HANZO_PASSWORD (reserved-admin SuperAdmin) to run the render proof')
|
||||
|
||||
await page.goto(`${BASE_URL}/signin`)
|
||||
await settle(page)
|
||||
await page.getByLabel(/email|username/i).first().fill(process.env.HANZO_USER ?? 'z@hanzo.ai')
|
||||
await page.getByLabel(/password/i).first().fill(password!)
|
||||
await page.getByRole('button', { name: /sign in|log in/i }).first().click()
|
||||
await page.waitForURL((u) => !u.pathname.startsWith('/signin'), { timeout: 30_000 })
|
||||
|
||||
await page.goto(`${BASE_URL}/research`)
|
||||
await settle(page)
|
||||
|
||||
await expect(
|
||||
page.locator('body').getByText(DASHBOARD),
|
||||
'a SuperAdmin did not get the research dashboard',
|
||||
).toHaveCount(1)
|
||||
await expect(page.locator('body').getByText(/\bExperiments\b/)).not.toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -26,7 +26,7 @@ import { currentOrg } from '~/lib/org-scope'
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { FieldRow, FieldText } from '~/components/ui/Field'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
|
||||
/** IAM-specific guidance for the honest 404 / unauthorized states. */
|
||||
const IAM_COPY: HonestCopy = {
|
||||
@@ -234,7 +234,7 @@ function UsersAdminView({ owner }: { owner: string }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={run} copy={IAM_COPY} />
|
||||
)
|
||||
@@ -350,7 +350,7 @@ function RolesAdminView({ owner }: { owner: string }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={run} copy={IAM_COPY} />
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* request BEFORE any cross-tenant row is read).
|
||||
*
|
||||
* Gated twice: the registry entry is `admin: true` (hidden from every customer's
|
||||
* nav/palette) and this module renders `OperatorAccessRequired` for a
|
||||
* nav/palette) and this module renders `SuperAdminRequired` for a
|
||||
* non-global-admin client. Honest by construction — every KPI is a real aggregate
|
||||
* or an em-dash, near-empty signals (o11y AI generations) render honest-empty,
|
||||
* and a failed fetch shows the honest error/access state, never a fabricated fleet.
|
||||
@@ -22,7 +22,7 @@ import { AdminO11yApi, type FleetO11y, type O11yRange } from '~/lib/api/admin-o1
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { LineChart, type ChartPoint } from '~/components/ui/Charts'
|
||||
import { asApiError, ErrorState, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { useIsSuperAdmin } from '~/lib/auth/admin'
|
||||
import { formatMetric } from '~/components/products/overview/living/logic'
|
||||
|
||||
@@ -81,7 +81,7 @@ export function AdminO11yModule() {
|
||||
|
||||
// Client gate — a non-global-admin never sees the cross-tenant board (the server
|
||||
// gate is the authoritative one; this is the honest UI twin).
|
||||
if (!isAdmin) return <OperatorAccessRequired />
|
||||
if (!isAdmin) return <SuperAdminRequired />
|
||||
|
||||
const d = st.data
|
||||
const t = d?.totals
|
||||
|
||||
@@ -20,7 +20,7 @@ import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { FieldRow, FieldSelect, FieldText } from '~/components/ui/Field'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { ApiError } from '~/lib/api'
|
||||
import { dollarsToCents, percentToBps, ratePct, shortDate, statusLabel, statusColor, usd } from './affiliates/logic'
|
||||
import { toneColor } from '~/components/ui/tone'
|
||||
@@ -168,7 +168,7 @@ export function AffiliatesAdminModule() {
|
||||
</XStack>
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState
|
||||
err={state.err}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { FieldRow, FieldSelect, FieldText } from '~/components/ui/Field'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { ApiError } from '~/lib/api'
|
||||
import { dollarsToCents, sharePct, shortDate, statusLabel, statusColor, usd } from './authors/logic'
|
||||
import { toneColor } from '~/components/ui/tone'
|
||||
@@ -161,7 +161,7 @@ export function AuthorsAdminModule() {
|
||||
</XStack>
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState
|
||||
err={state.err}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* Every call goes through the console's OWN same-origin `/v1/catalog/*` user-bearer
|
||||
* proxy → commerce, whose `requireSuperAdmin` (owner=="admin") is the AUTHORITATIVE
|
||||
* gate: a signed-in non-admin gets an honest 403 (the OperatorAccessRequired panel),
|
||||
* gate: a signed-in non-admin gets an honest 403 (the SuperAdminRequired panel),
|
||||
* never catalog write access. The browser holds no credential.
|
||||
*
|
||||
* The structured `Metadata` spec is edited as a type-preserving key/value list
|
||||
@@ -33,7 +33,7 @@ import { FieldRow, FieldSelect, FieldSwitch, FieldText, FieldTextArea } from '~/
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { SlideOver } from '~/components/ui/SlideOver'
|
||||
import { ConfirmDelete } from '~/components/ui/ConfirmDelete'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { MetadataEditor } from './pricing/MetadataEditor'
|
||||
import {
|
||||
centsToInput,
|
||||
@@ -227,7 +227,7 @@ export function CatalogModule(_props: { params: Record<string, string> }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={load} copy={CATALOG_COPY} />
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
*
|
||||
* HONEST BY CONSTRUCTION: renders an honest empty state until the compute-events
|
||||
* emitter + aggregate land — never a fabricated fleet. States: loading,
|
||||
* operator-access-required (403), not-routed (404), error, empty.
|
||||
* superadmin-required (403), not-routed (404), error, empty.
|
||||
*/
|
||||
import { type ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { Button, Text, XStack, YStack } from '@hanzo/gui'
|
||||
@@ -33,7 +33,7 @@ import { AdminComputeApi, type ComputeFleets, type ComputeKind, type Rollup, typ
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
|
||||
type Range = '24h' | '7d' | '30d'
|
||||
const RANGES: { key: Range; label: string }[] = [
|
||||
@@ -268,7 +268,7 @@ function ComputeBoard({ kind }: { kind: ComputeKind }) {
|
||||
</XStack>
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={() => load(range)} copy={copyFor(kind)} />
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { FieldSwitch } from '~/components/ui/Field'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { ApiError } from '~/lib/api'
|
||||
|
||||
type Async<T> = { phase: 'loading' } | { phase: 'error'; err: ApiError } | { phase: 'ready'; data: T }
|
||||
@@ -101,7 +101,7 @@ export function FeatureGateModule() {
|
||||
)
|
||||
}
|
||||
if (state.phase === 'error') {
|
||||
return isForbidden(state.err) ? <OperatorAccessRequired /> : <ErrorState err={state.err} onRetry={load} />
|
||||
return isForbidden(state.err) ? <SuperAdminRequired /> : <ErrorState err={state.err} onRetry={load} />
|
||||
}
|
||||
|
||||
const { services, pending } = state.data
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* entirely through the server-gated `/admin/kms` proxy (KmsAdminApi), which
|
||||
* enforces the brand-admin gate and forwards as the user, scoped to the active
|
||||
* org. Values are revealed one at a time, shown once, never listed or stored.
|
||||
* States are honest: loading, operator-access-required (403), listing-
|
||||
* States are honest: loading, superadmin-required (403), listing-
|
||||
* unavailable (404), error, and empty.
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
@@ -23,7 +23,7 @@ import { config } from '~/config'
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { FieldRow, FieldText } from '~/components/ui/Field'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
|
||||
/** KMS-specific guidance for the honest 404 / unauthorized states. */
|
||||
const KMS_COPY: HonestCopy = {
|
||||
@@ -216,7 +216,7 @@ export function KmsModule(_props: { params: Record<string, string> }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={run} copy={KMS_COPY} />
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* Gated twice, like the fleet o11y board: the catalog entry is `admin: true` +
|
||||
* `brands: ['lux']` (hidden from every customer and every non-Lux console), and this
|
||||
* module renders OperatorAccessRequired for a non-SuperAdmin client. The cloud VM
|
||||
* module renders SuperAdminRequired for a non-SuperAdmin client. The cloud VM
|
||||
* proxy is the authoritative server gate (admin(c) + the fixed allowlist).
|
||||
*
|
||||
* Honest uptime: there is NO uptime metric in the hub, and the luxd on-chain uptime
|
||||
@@ -33,7 +33,7 @@ import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard, UtilBar, utilColor } from '~/components/ui/Metric'
|
||||
import { Panel } from '~/components/ui/Panel'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { SuperAdminRequired, SUPERADMIN_REQUIRED } from '~/components/ui/States'
|
||||
|
||||
// Lux primary-network staking facts (mainnet): 5 validators, each bonds 500M LUX →
|
||||
// 2.5B LUX staked. Displayed as a caption on the primary panel (investor context).
|
||||
@@ -73,7 +73,7 @@ export function LuxNetworkModule(_props: { params: Record<string, string> }) {
|
||||
|
||||
// Client gate — the authoritative one is the cloud VM proxy (admin(c)); this is the
|
||||
// honest UI twin, so a non-SuperAdmin never sees a broken board.
|
||||
if (!isSuperAdmin) return <OperatorAccessRequired />
|
||||
if (!isSuperAdmin) return <SuperAdminRequired />
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -401,13 +401,13 @@ function LuxError({ status, message, onRetry }: { status: number; message: strin
|
||||
status === 501
|
||||
? 'Telemetry not configured'
|
||||
: status === 401 || status === 403
|
||||
? 'Operator access required'
|
||||
? SUPERADMIN_REQUIRED
|
||||
: 'Could not reach the telemetry store'
|
||||
const body =
|
||||
status === 501
|
||||
? 'This console reads Lux telemetry from the telemetry store, but its URL is not set on this deployment yet. Once it is, live validator and infrastructure metrics appear here — no fabricated data is shown.'
|
||||
: status === 401 || status === 403
|
||||
? 'The Lux Network board is a platform-operator surface. Sign in with an operator account to view it.'
|
||||
? 'The Lux Network board is a platform surface, restricted to SuperAdmins — members of the reserved admin org.'
|
||||
: message
|
||||
return (
|
||||
<Card borderWidth={1} borderColor="$borderColor" p="$4" gap="$2" maxWidth={640}>
|
||||
|
||||
@@ -31,7 +31,7 @@ import { FieldRow, FieldSelect, FieldSwitch, FieldText, FieldTextArea } from '~/
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { SlideOver } from '~/components/ui/SlideOver'
|
||||
import { ConfirmDelete } from '~/components/ui/ConfirmDelete'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { MetadataEditor } from './pricing/MetadataEditor'
|
||||
import { centsToInput, distinctCategories, formatUsd, inputToCents, metadataToRows, type MetadataRow, rowsToMetadata } from './catalog/logic'
|
||||
import { annualDisplay, PLAN_CATEGORIES, priceDisplay } from './plans/logic'
|
||||
@@ -212,7 +212,7 @@ export function PlansCatalogModule(_props: { params: Record<string, string> }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={load} copy={PLANS_COPY} />
|
||||
)
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
* not a live probe: disabled ⇒ Off; enabled + keyPresent ⇒ Ready; enabled + no key
|
||||
* ⇒ No key. No fabricated green.
|
||||
*
|
||||
* States are honest end to end: loading (Spinner), operator-access-required (403 →
|
||||
* the shared `OperatorAccessRequired` panel — a signed-in non-admin reads "admin
|
||||
* States are honest end to end: loading (Spinner), superadmin-required (403 →
|
||||
* the shared `SuperAdminRequired` panel — a signed-in non-admin reads "admin
|
||||
* only", NEVER "sign in"), listing-unavailable (404), error (retry), empty.
|
||||
*
|
||||
* All calls go through the origin-pinned admin client → the global-admin-gated
|
||||
@@ -39,7 +39,7 @@ import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { FieldSwitch } from '~/components/ui/Field'
|
||||
import { ProviderLogo } from '~/components/ui/ProviderLogo'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired, type HonestCopy } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired, type HonestCopy } from '~/components/ui/States'
|
||||
|
||||
/** Provider-board guidance for the honest 404 / unauthorized states. */
|
||||
const PROVIDERS_COPY: HonestCopy = {
|
||||
@@ -245,7 +245,7 @@ export function ProviderAdminModule(_props: { params: Record<string, string> })
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={run} copy={PROVIDERS_COPY} />
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AdminReferralsApi, type AdminReferralsView, type SweepResult } from '~/
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { ApiError } from '~/lib/api'
|
||||
import { shortDate, statusLabel, statusColor, usd } from './referrals/logic'
|
||||
import { toneColor } from '~/components/ui/tone'
|
||||
@@ -88,7 +88,7 @@ export function ReferralsAdminModule() {
|
||||
</XStack>
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState
|
||||
err={state.err}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* Gated twice, exactly like the Fleet Observability board: the registry entry is
|
||||
* `admin: true` (hidden from every customer's nav/palette) and this module
|
||||
* renders `OperatorAccessRequired` for a non-super-admin client. The read is
|
||||
* renders `SuperAdminRequired` for a non-super-admin client. The read is
|
||||
* org-scoped server-side (the `research` head resolves the org from the Bearer owner),
|
||||
* so a customer only ever reaches their OWN corpus — never Hanzo's platform R&D.
|
||||
*
|
||||
@@ -24,7 +24,7 @@ import { FlaskConical, CheckCircle2, CircleSlash, FolderGit2, ListChecks, Coins,
|
||||
import { ResearchApi, type Experiment, type Totals, type Verdict } from '~/lib/api/research'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { asApiError, ErrorState, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { useIsSuperAdmin } from '~/lib/auth/admin'
|
||||
import { fmtValue, fmtDate, rowKeyOf } from './research-fmt'
|
||||
import { toneVar } from '~/components/ui/tone'
|
||||
@@ -106,7 +106,7 @@ export function ResearchModule() {
|
||||
|
||||
// Client gate — the authoritative gate is server-side (the `research` head is
|
||||
// org-scoped by the Bearer owner); this is the honest matching UI gate.
|
||||
if (!isAdmin) return <OperatorAccessRequired />
|
||||
if (!isAdmin) return <SuperAdminRequired />
|
||||
|
||||
return (
|
||||
<YStack gap="$4" p="$4" maxW={1200} self="center" width="100%">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* an honest partial rather than blanking the board.
|
||||
*
|
||||
* Gated twice: the registry entry is `admin: true` (hidden from every customer's
|
||||
* nav/palette) and this module renders `OperatorAccessRequired` for a
|
||||
* nav/palette) and this module renders `SuperAdminRequired` for a
|
||||
* non-global-admin client. Honest by construction — every figure is a real aggregate
|
||||
* or an em-dash; not-instrumented signals (upgrades/downgrades, per-model latency)
|
||||
* render honestly, never fabricated.
|
||||
@@ -29,7 +29,7 @@ import { SaasApi, type SaaSMetrics, type SaasWindow } from '~/lib/api/saas'
|
||||
import { AdminO11yApi, type FleetO11y, type O11yRange } from '~/lib/api/admin-o11y'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { asApiError, ErrorState, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { useIsSuperAdmin } from '~/lib/auth/admin'
|
||||
import { formatMetric } from '~/components/products/overview/living/logic'
|
||||
|
||||
@@ -134,7 +134,7 @@ export function SaasModule() {
|
||||
}
|
||||
}, [isAdmin, range])
|
||||
|
||||
if (!isAdmin) return <OperatorAccessRequired />
|
||||
if (!isAdmin) return <SuperAdminRequired />
|
||||
|
||||
const d = m.data
|
||||
const rev = d?.revenue
|
||||
|
||||
@@ -33,7 +33,7 @@ import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { FieldText } from '~/components/ui/Field'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { ApiError } from '~/lib/api'
|
||||
import { toneColor } from '~/components/ui/tone'
|
||||
|
||||
@@ -126,7 +126,7 @@ export function TreasuryAdminModule() {
|
||||
</XStack>
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState
|
||||
err={state.err}
|
||||
|
||||
@@ -75,7 +75,7 @@ import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { Donut } from '~/components/ui/Charts'
|
||||
import { RAMP, OTHER } from '~/lib/theme/ramp'
|
||||
import { RangeTabs } from '~/components/products/billing/RangeTabs'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { toneColor, toneVar } from '~/components/ui/tone'
|
||||
|
||||
// ── small presentational helpers ──────────────────────────────────────────────
|
||||
@@ -393,7 +393,7 @@ export function AiEconomicsModule() {
|
||||
return (
|
||||
<YStack p="$4" gap="$4">
|
||||
<PageHeader title="AI Economics" />
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AdminCockpitApi, type AnalyticsData } from '~/lib/api/admin-cockpit'
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { LineChart, BarChart, type ChartPoint } from '~/components/ui/Charts'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { toneVar } from '~/components/ui/tone'
|
||||
|
||||
type Range = '7d' | '30d' | '90d' | 'all'
|
||||
@@ -95,7 +95,7 @@ export function AnalyticsModule() {
|
||||
|
||||
useEffect(() => { void load() }, [load])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Analytics" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
const c = data?.computed ?? {}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
|
||||
const usd = (cents: number): string => '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
const shortDate = (s: string): string => (s ? (s.split('T')[0] ?? s) : '—')
|
||||
@@ -189,7 +189,7 @@ export function CustomersModule() {
|
||||
|
||||
if (selected) return <CustomerDetailView org={selected} onBack={() => { setSelected(null); void load() }} />
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Customers" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
const totalBal = rows.reduce((s, r) => s + r.balanceCents, 0)
|
||||
@@ -326,7 +326,7 @@ function CustomerDetailView({ org, onBack }: { org: string; onBack: () => void }
|
||||
|
||||
const back = <Button size="$3" chromeless icon={<ArrowLeft size={16} />} onPress={onBack}>Customers</Button>
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4">{back}<ErrorState err={err} onRetry={load} /></YStack>
|
||||
if (loading || !detail) return <YStack p="$4" gap="$4">{back}<PageHeader title={org} /><Text color="$color10">Loading…</Text></YStack>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ApiError } from '~/lib/api'
|
||||
import { EnablementApi, type AdminEnablementItem, type EnablementState } from '~/lib/api/admin-cockpit'
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
|
||||
const STATES: EnablementState[] = ['off', 'beta', 'ga']
|
||||
const KINDS = ['model', 'provider', 'feature'] as const
|
||||
@@ -69,7 +69,7 @@ export function EnablementModule() {
|
||||
setNewId('')
|
||||
}, [newKind, newId, setState])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Enablement" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AdminGrantsApi, type AdminGrant, type GrantSource } from '~/lib/api/adm
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
|
||||
const usd = (cents: number): string => '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
const shortDate = (s: string): string => (s ? (s.split('T')[0] ?? s) : '—')
|
||||
@@ -129,7 +129,7 @@ export function GrantsModule() {
|
||||
}
|
||||
}, [load])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Grants" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
const total = rows.reduce((s, r) => s + r.amountCents, 0)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* GLOBAL-ADMIN ONLY. The catalog entry is `admin: true` (hidden from every customer's
|
||||
* nav/palette) and this module additionally gates on `useIsSuperAdmin()` — the
|
||||
* matching UI gate over the authoritative server-side SuperAdmin gate (a non-admin sees
|
||||
* the honest OperatorAccessRequired panel, never a 403 crash). Every read is REAL over
|
||||
* the honest SuperAdminRequired panel, never a 403 crash). Every read is REAL over
|
||||
* the `/v1` user-bearer BFF; honest loading/empty/error states throughout, no fabrication.
|
||||
*
|
||||
* SCOPE: the Live-state section reads the admin org's OWN `/v1/guide/profile` (Hanzo's
|
||||
@@ -66,7 +66,7 @@ import { Panel } from '~/components/ui/Panel'
|
||||
import { Donut, type Slice } from '~/components/ui/Charts'
|
||||
import { RAMP } from '~/lib/theme/ramp'
|
||||
import { asColor } from '~/components/ui/color'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { BackendStateCard, classifyRead } from '~/components/ui/BackendState'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { FieldRow, FieldText, FieldTextArea } from '~/components/ui/Field'
|
||||
@@ -373,7 +373,7 @@ function BlueprintPanel({
|
||||
}
|
||||
}, [showHistory, versions])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) {
|
||||
const state = classifyRead(err)
|
||||
return state ? <BackendStateCard state={state} onRetry={reload} hint="GET /v1/guide/blueprint" /> : <ErrorState err={err} onRetry={reload} />
|
||||
@@ -630,7 +630,7 @@ function CorpusPanel({ nonce }: { nonce: number }) {
|
||||
return q ? base.filter((s) => `${s.action} ${s.principle} ${s.tags.join(' ')} ${s.source}`.toLowerCase().includes(q)) : base
|
||||
}, [rows, search])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) {
|
||||
const onRetry = () => setRetry((r) => r + 1)
|
||||
const state = classifyRead(err)
|
||||
@@ -783,7 +783,7 @@ function LivePanel({ nonce }: { nonce: number }) {
|
||||
return () => { live = false }
|
||||
}, [nonce, retry])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err && !profile) {
|
||||
const onRetry = () => setRetry((r) => r + 1)
|
||||
const state = classifyRead(err)
|
||||
@@ -947,7 +947,7 @@ export function GrowthModule() {
|
||||
return (
|
||||
<YStack p="$5" gap="$4">
|
||||
<PageHeader title="Growth" subtitle="The Zen-of-Hanzo Guide engine — operator cockpit." />
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AdminProjectsApi, groupByOrg, type ProjectRow } from '~/lib/api/admin-p
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
|
||||
const shortDate = (s: string): string => (s ? (s.split('T')[0] ?? s) : '—')
|
||||
|
||||
@@ -72,7 +72,7 @@ export function ProjectsModule() {
|
||||
|
||||
const orgCount = useMemo(() => groupByOrg(rows).length, [rows])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Projects" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
const columns: Column<ProjectRow>[] = [
|
||||
|
||||
@@ -43,7 +43,7 @@ import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { Donut } from '~/components/ui/Charts'
|
||||
import { RangeTabs } from '~/components/products/billing/RangeTabs'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { toneColor, toneVar } from '~/components/ui/tone'
|
||||
|
||||
// ── small presentational helpers ──────────────────────────────────────────────
|
||||
@@ -210,7 +210,7 @@ export function ProvidersBillingModule() {
|
||||
return (
|
||||
<YStack p="$4" gap="$4">
|
||||
<PageHeader title="Provider billing" />
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { DataTable, type Column } from '~/components/ui/DataTable'
|
||||
import { LineChart } from '~/components/ui/Charts'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { toneVar } from '~/components/ui/tone'
|
||||
|
||||
const usd = (cents: number): string => '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
@@ -34,7 +34,7 @@ export function RevenueModule() {
|
||||
|
||||
useEffect(() => { void load() }, [load])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err) return <YStack p="$4" gap="$4"><PageHeader title="Revenue" /><ErrorState err={err} onRetry={load} /></YStack>
|
||||
|
||||
const cols: Column<RevenueCustomer>[] = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* Every edit is a write to the OrgSettings row via OrgSettingsApi (read-modify-write,
|
||||
* so sibling routing-policy fields are never clobbered). GLOBAL-ADMIN ONLY: the
|
||||
* endpoints are RequireSuperAdmin upstream, so a non-admin who reaches this sees the
|
||||
* real OperatorAccessRequired panel — never a faked success. Honest states, no
|
||||
* real SuperAdminRequired panel — never a faked success. Honest states, no
|
||||
* fabricated rows.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@@ -37,7 +37,7 @@ import { getBrand } from '~/lib/branding/brands'
|
||||
import { PageHeader } from '~/components/ui/PageHeader'
|
||||
import { MetricCard } from '~/components/ui/Metric'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { ErrorState, asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { ErrorState, asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
|
||||
const STATES: RoutingState[] = ['inherit', 'enabled', 'disabled']
|
||||
const LABEL: Record<RoutingState, string> = { inherit: 'Inherit', enabled: 'Enabled', disabled: 'Disabled' }
|
||||
@@ -170,7 +170,7 @@ export function RoutingModule() {
|
||||
}
|
||||
}, [newOrg])
|
||||
|
||||
if (err && isForbidden(err)) return <OperatorAccessRequired />
|
||||
if (err && isForbidden(err)) return <SuperAdminRequired />
|
||||
if (err)
|
||||
return (
|
||||
<YStack p="$4" gap="$4">
|
||||
|
||||
@@ -49,7 +49,7 @@ import { MetricCard } from '~/components/ui/Metric'
|
||||
import { Panel } from '~/components/ui/Panel'
|
||||
import { PrimaryButton } from '~/components/ui/PrimaryButton'
|
||||
import { FieldRow, FieldSelect, FieldSwitch, FieldText, FieldSlider } from '~/components/ui/Field'
|
||||
import { asApiError, ErrorState, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, ErrorState, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import {
|
||||
capVerdict,
|
||||
deriveBudgetsSummary,
|
||||
@@ -189,7 +189,7 @@ function PromoTab() {
|
||||
|
||||
if (state.phase === 'loading') return <Loading label="Loading the platform promo…" />
|
||||
if (state.phase === 'error')
|
||||
return isForbidden(state.err) ? <OperatorAccessRequired /> : <ErrorState err={state.err} onRetry={load} />
|
||||
return isForbidden(state.err) ? <SuperAdminRequired /> : <ErrorState err={state.err} onRetry={load} />
|
||||
if (!form) return null
|
||||
|
||||
const live = state.data
|
||||
@@ -594,7 +594,7 @@ function CapsTab() {
|
||||
<Loading label={`Loading caps for ${org}…`} />
|
||||
) : state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={reload} />
|
||||
)
|
||||
@@ -655,7 +655,7 @@ export function UsageCapsPromoModule() {
|
||||
const isAdmin = useIsSuperAdmin()
|
||||
const [tab, setTab] = useState<Tab>('promo')
|
||||
|
||||
if (!isAdmin) return <OperatorAccessRequired />
|
||||
if (!isAdmin) return <SuperAdminRequired />
|
||||
|
||||
return (
|
||||
<YStack p="$4" gap="$5">
|
||||
|
||||
@@ -48,7 +48,7 @@ import { StatusTag } from '~/components/ui/StatusTag'
|
||||
import { EmptyState } from '~/components/ui/EmptyState'
|
||||
import { FieldSwitch } from '~/components/ui/Field'
|
||||
import { useToast } from '~/components/ui/Toast'
|
||||
import { asApiError, isForbidden, OperatorAccessRequired } from '~/components/ui/States'
|
||||
import { asApiError, isForbidden, SuperAdminRequired } from '~/components/ui/States'
|
||||
import { fmtAgo, fmtBytes } from '~/components/products/observability/apm-format'
|
||||
import { StorageFleetApi, type StorageVolume } from '~/lib/api/storage-fleet'
|
||||
import {
|
||||
@@ -150,7 +150,7 @@ export function InfraModule({ params }: { params: Record<string, string> }) {
|
||||
const data = snap ?? EMPTY
|
||||
|
||||
const body = (() => {
|
||||
if (err) return isForbidden(err) ? <OperatorAccessRequired /> : <BackendStateCard state={classifyBackend(err)} onRetry={() => void load()} hint="GET /v1/admin/infra" />
|
||||
if (err) return isForbidden(err) ? <SuperAdminRequired /> : <BackendStateCard state={classifyBackend(err)} onRetry={() => void load()} hint="GET /v1/admin/infra" />
|
||||
if (tab === 'clusters') return <ClustersTab data={data} loading={loading} />
|
||||
if (tab === 'nodes') return <NodesTab data={data} loading={loading} reload={() => void load(true)} toast={toast} />
|
||||
if (tab === 'volumes') return <VolumesTab data={data} loading={loading} reload={() => void load(true)} toast={toast} />
|
||||
|
||||
@@ -34,7 +34,7 @@ import { Loader } from '~/components/ui/Loader'
|
||||
import { SlideOver } from '~/components/ui/SlideOver'
|
||||
import { FieldText } from '~/components/ui/Field'
|
||||
import { useToast } from '~/components/ui/Toast'
|
||||
import { OperatorAccessRequired, ErrorState, asApiError, isForbidden, type HonestCopy } from '~/components/ui/States'
|
||||
import { SuperAdminRequired, ErrorState, asApiError, isForbidden, type HonestCopy } from '~/components/ui/States'
|
||||
import { usePoll, useReducedMotion } from '../overview/living/hooks'
|
||||
import { renderServiceIcon } from '../platform-apps/icons'
|
||||
import { LazyProjectCanvas, CanvasFrame } from './canvas-lazy'
|
||||
@@ -228,7 +228,7 @@ export function GitOpsModule({ params }: { params: Record<string, string> }) {
|
||||
|
||||
{state.phase === 'error' ? (
|
||||
isForbidden(state.err) ? (
|
||||
<OperatorAccessRequired />
|
||||
<SuperAdminRequired />
|
||||
) : (
|
||||
<ErrorState err={state.err} onRetry={refresh} copy={COPY} />
|
||||
)
|
||||
|
||||
@@ -31,15 +31,21 @@ export type { HonestCopy }
|
||||
export const asApiError = (e: unknown): ApiError =>
|
||||
e instanceof ApiError ? e : new ApiError(e instanceof Error ? e.message : String(e))
|
||||
|
||||
/** True for the gate's `ApiError('forbidden', 403)` — the operator-access panel. */
|
||||
/** True for the gate's `ApiError('forbidden', 403)` — the SuperAdmin-required panel. */
|
||||
export const isForbidden = (err: ApiError): boolean => err.status === 403
|
||||
|
||||
/**
|
||||
* The operator-access-required panel — the honest UX on top of the authoritative
|
||||
* The ONE gate headline. Exported so a surface that renders its own error card
|
||||
* (rather than this panel) says the same words — one predicate, one string.
|
||||
*/
|
||||
export const SUPERADMIN_REQUIRED = 'SuperAdmin access required'
|
||||
|
||||
/**
|
||||
* The SuperAdmin-required panel — the honest UX on top of the authoritative
|
||||
* server-side admin gate. Shown when the IAM/KMS gated proxies return 403: the
|
||||
* caller is signed in but not authorized for THIS brand's admin console.
|
||||
*/
|
||||
export function OperatorAccessRequired() {
|
||||
export function SuperAdminRequired() {
|
||||
const { account } = useSession()
|
||||
const brand = getBrand()
|
||||
const who = account?.email || account?.name || 'This account'
|
||||
@@ -48,12 +54,12 @@ export function OperatorAccessRequired() {
|
||||
<XStack gap="$2" items="center">
|
||||
<Lock size={16} />
|
||||
<Text fontSize="$4" fontWeight="700">
|
||||
Operator access required
|
||||
{SUPERADMIN_REQUIRED}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$3" color="$color11">
|
||||
{who} is not authorized for the {brand.brandName} admin console. This console requires an
|
||||
@{brand.adminDomain} account with an admin role.
|
||||
{who} is not authorized for the {brand.brandName} admin console. This console requires a
|
||||
SuperAdmin — a member of the reserved admin org, on an @{brand.adminDomain} account.
|
||||
</Text>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* Reads/writes ride originGet/originPut/originDelete — the console's OWN origin, dispatched by
|
||||
* next.config to the /ai user-bearer proxy (session cookie → short-lived minted
|
||||
* bearer → the hanzoai/ai gateway). The super-admin gate is enforced UPSTREAM; a
|
||||
* non-admin gets a real 403 the module renders honestly (OperatorAccessRequired).
|
||||
* non-admin gets a real 403 the module renders honestly (SuperAdminRequired).
|
||||
*/
|
||||
import { originGet, originPut, originDelete } from './client'
|
||||
|
||||
|
||||
@@ -1177,7 +1177,7 @@ export const catalog: CatalogEntry[] = [
|
||||
// admin toggle — set one org to Enabled for the org-first rollout, then flip the
|
||||
// global default. GLOBAL-ADMIN ONLY (`admin: true` hides it from every customer;
|
||||
// the endpoints are super-admin gated server-side, so a non-admin sees the honest
|
||||
// OperatorAccessRequired panel). Runtime policy lives as editable rows, never env.
|
||||
// SuperAdminRequired panel). Runtime policy lives as editable rows, never env.
|
||||
id: 'routing',
|
||||
label: 'Routing',
|
||||
icon: Route,
|
||||
@@ -2836,7 +2836,7 @@ export const catalog: CatalogEntry[] = [
|
||||
// pod memory pressure, and the named-service status grid. In-console `module`
|
||||
// (owns its route), reading the SuperAdmin-gated, allowlisted cloud VM proxy
|
||||
// (lib/api/lux-infra.ts → /v1/o11y/vm/query). SUPERADMIN + LUX ONLY: `admin: true`
|
||||
// hides it from every customer and the module renders OperatorAccessRequired for a
|
||||
// hides it from every customer and the module renders SuperAdminRequired for a
|
||||
// non-SuperAdmin; `brands: ['lux']` keeps it OFF every non-Lux console (zero
|
||||
// cross-brand leak). The cloud proxy (admin(c) + fixed allowlist) is the
|
||||
// authoritative server gate. Honest by construction — every value folds real
|
||||
|
||||
Reference in New Issue
Block a user