fix(console): platform.hanzo.ai boots into the fleet control plane, not the per-org deploy home

The platform face was a single-product shell (rootId/home = 'platform' → the
per-org PaaS deploy home, PlatformModule). Per the CTO call — and the FOLLOW-UP the
OSS App Store commit explicitly flagged ('upgrade the platform face from
single-product to a MULTI-product nav … a separate CTO call') — platform.<brand> is
the fleet CONTROL PLANE: it must show every deployment of every app + manage all
services.

Retarget the platform shell to a full-catalog control-plane shape:
  rootId: 'platform' → null   (full admin nav — a global admin sees Deploy,
                                Applications, Clusters, Kubernetes, Tenants, Storage,
                                … every service; not a single-product scope)
  home:   'platform' → 'gitops' (boots into the Deploy fleet map — every operator
                                App CR's declared/running/latest/drift + builds/logs/
                                rollback), not the customer home.

Additive to the concurrent OSS-store work: the per-org deploy home (the 'platform'
product / PlatformHome) stays reachable as one nav item among many; only the LANDING
moves. isProductShell stays false for platform (a home alone never scopes the nav —
only a rootId does), so nav rendering is the full catalog exactly like console.
Auth unchanged (brand cloud app; a global admin resolves owner==='admin' on this
go:embed host exactly as on cloud.<brand>, so the admin fleet board works).

tsc clean; shell.test + config/index.test green (56); build:embed ✓ (go:embed gate).
This commit is contained in:
Hanzo AI
2026-07-25 06:48:18 -07:00
parent 1c0fd191cc
commit 445b9d4008
2 changed files with 30 additions and 7 deletions
+14 -3
View File
@@ -19,8 +19,8 @@ import {
// env restored), so no window mocking.
const ALL: ShellId[] = ['console', 'billing', 'marketing', 'ads', 'social', 'sentry', 'dns', 'platform']
/** The single-product FACES (everything but the full console). */
const FACES: ShellId[] = ['billing', 'marketing', 'ads', 'social', 'sentry', 'dns', 'platform']
/** The single-product FACES (everything but the full-catalog console + platform). */
const FACES: ShellId[] = ['billing', 'marketing', 'ads', 'social', 'sentry', 'dns']
afterEach(() => {
delete process.env.NEXT_PUBLIC_PRODUCT_SHELL
@@ -56,6 +56,14 @@ describe('product-shell descriptor', () => {
}
})
it('platform is the CONTROL-PLANE shell — full catalog (rootId null, not a product shell), boots into the gitops fleet map', () => {
const p = shellFor('platform')
expect(p.rootId).toBeNull() // full catalog nav — a global admin sees every surface
expect(isProductShell('platform')).toBe(false) // NOT a single-product scope
expect(p.home).toBe('gitops') // …but boots into the Deploy fleet map, not the per-org home
expect(p.indexLabel).toBe('Overview')
})
it('wordmarks: billing keeps mark-only; the newer faces wear their product wordmark', () => {
expect(shellFor('billing').wordmark).toBe('') // legacy look, unchanged
expect(shellFor('marketing').wordmark).toBe('Marketing')
@@ -69,7 +77,7 @@ describe('product-shell descriptor', () => {
it('sentry boots into Issues, dns into Zones; every other face indexes on Overview', () => {
expect(shellFor('sentry').indexLabel).toBe('Issues')
expect(shellFor('dns').indexLabel).toBe('Zones')
for (const id of ['billing', 'marketing', 'ads', 'social', 'platform'] as ShellId[]) {
for (const id of ['billing', 'marketing', 'ads', 'social'] as ShellId[]) {
expect(shellFor(id).indexLabel).toBe('Overview')
}
})
@@ -118,6 +126,9 @@ describe('shellFromHost — ONE resolver, five faces', () => {
process.env.NEXT_PUBLIC_PRODUCT_SHELL = id
expect(shellFromHost('cloud.hanzo.ai')).toBe(id)
}
// the control-plane shell is overridable too (preview any host as platform)
process.env.NEXT_PUBLIC_PRODUCT_SHELL = 'platform'
expect(shellFromHost('cloud.hanzo.ai')).toBe('platform')
process.env.NEXT_PUBLIC_PRODUCT_SHELL = 'console'
expect(shellFromHost('sentry.hanzo.ai')).toBe('console')
})
+16 -4
View File
@@ -39,6 +39,8 @@ export type ProductShell = {
* look); the newer faces wear their product wordmark next to the Hanzo brand mark.
* The single-screen faces (marketing/ads/social — one route, no sub-pages) surface a
* lone "Overview" nav item; billing/sentry surface their root module's sub-pages.
* `platform` is the ONE exception — a full-catalog control-plane shell (rootId null,
* like console) that merely boots into the fleet map (see its descriptor below).
*/
export const PRODUCT_SHELLS: Record<ShellId, ProductShell> = {
console: { id: 'console', rootId: null, indexLabel: 'Overview', wordmark: '', home: '' },
@@ -48,15 +50,25 @@ export const PRODUCT_SHELLS: Record<ShellId, ProductShell> = {
social: { id: 'social', rootId: 'social', indexLabel: 'Overview', wordmark: 'Publish', home: 'social' },
sentry: { id: 'sentry', rootId: 'sentry', indexLabel: 'Issues', wordmark: 'Sentry', home: 'sentry' },
dns: { id: 'dns', rootId: 'dns', indexLabel: 'Zones', wordmark: 'DNS', home: 'dns' },
platform: { id: 'platform', rootId: 'platform', indexLabel: 'Overview', wordmark: 'Platform', home: 'platform' },
// The CONTROL-PLANE shell (platform.<brand>) — the CTO-called upgrade of the initial
// single-product platform face. A third shape: like `console` it keeps the FULL catalog
// nav (rootId null → NOT a single-product scope, so a global admin sees every platform/
// admin surface — Deploy, Applications, Clusters, Kubernetes, Tenants, Storage, …), but
// it BOOTS into the Deploy fleet map (`gitops` — the operator App-CR drift board: every
// app's declared/running/latest/drift + builds/logs/rollback) instead of the customer
// home. This is what makes platform.hanzo.ai the fleet control plane. The per-org deploy
// home (the `platform` product) stays reachable as one nav item among many.
platform: { id: 'platform', rootId: null, indexLabel: 'Overview', wordmark: 'Platform', home: 'gitops' },
}
/** The descriptor for a shell id. */
export const shellFor = (id: ShellId): ProductShell => PRODUCT_SHELLS[id]
/**
* True when a shell scopes the console to a SINGLE product face (billing/sentry) —
* i.e. it has a root module and a home to boot into. The full `console` is false.
* ONE predicate the nav + home + catalog gates share.
* True when a shell scopes the console nav to a SINGLE product face (billing/sentry) —
* i.e. it has a ROOT module. The full-catalog shells are false: `console` (no home) and
* `platform` (the control plane — full nav, but boots into `gitops` via `home`, not a
* scope). A `home` alone never scopes the nav; only a `rootId` does. ONE predicate the
* nav + home + catalog gates share.
*/
export const isProductShell = (id: ShellId): boolean => shellFor(id).rootId !== null