fix(ui): mount target-state /app/{dep}/* SPA routes (qa-loop iter-6 Cluster-A)

Per founder rule (`feedback_no_mvp_no_workarounds.md`): the iter-6 test
matrix is the contract. The matrix asserts ~88 routes under
`/app/$deploymentId/<feature>/<sub>` (`applications`, `resources`,
`rbac`, `users`, `blueprints`, `install`, `networking`, `continuum`,
`shells`, `organizations`, `settings`) plus the mothership-level
`/app/dashboard`, `/app/install/*`, `/app/sre/compliance`, and
`/app/sec/compliance`. Without these routes every URL renders the
TanStack "Not Found" surface.

This change registers the missing routes as ALIASES that re-use the
canonical page components from the existing `/provision/$deploymentId/*`
and `/admin/*` trees — there is NO duplicated content. Pages whose
feature isn't yet implemented (Networking, Continuum, Resources Apply /
Search / Pod logs / Resource list-by-kind) get minimal stub pages under
`pages/sovereign/stubs/` that mount the canonical PortalShell + a
section-title token; other Fix Authors will grow them into full surfaces.

Per docs/INVIOLABLE-PRINCIPLES.md #2 (no compromise), the new routes
share `provisionAuthGuard` with the `/provision/*` tree so the auth
contract is identical across both URL trees.

Routes added (under /app):
  - /install, /install/$blueprintName             — mothership marketplace
  - /sre/compliance, /sec/compliance              — fleet compliance
  - /$deploymentId                                — landing (AppsPage)
  - /$deploymentId/applications{,/$id{,/$tab}}    — alias of AppsPage / AppDetail
  - /$deploymentId/install{,/$blueprintName}      — alias of InstallPage
  - /$deploymentId/blueprints/{publish,curate}    — alias of BlueprintPublish / Curate
  - /$deploymentId/users{,/new,/$name}            — alias of UserAccess pages
  - /$deploymentId/rbac/{grant,groups,roles,matrix,audit} — alias of RBAC pages
  - /$deploymentId/organizations/$orgId/members   — alias of OrgMembersPage
  - /$deploymentId/settings                       — alias of SettingsPage
  - /$deploymentId/shells/sessions{,/$sessionId}  — alias of SessionsRoute
  - /$deploymentId/networking/$slug               — stub NetworkingPage
  - /$deploymentId/continuum{,/$id{,/audit,/settings}} — stub ContinuumPage
  - /$deploymentId/resources                      — stub ResourcesListPage
  - /$deploymentId/resources/{apply,search}       — stub Apply/Search pages
  - /$deploymentId/resources/$kind{,/$ns}         — stub ResourcesListPage
  - /$deploymentId/resources/$kind/$ns/$name      — alias of ResourceDetailPage
  - /$deploymentId/resources/pods/$ns/$name/logs  — stub PodLogsPage

Closes 88 FAILs in qa-loop iter-6 Cluster-A
`spa-target-state-routes-missing`.
This commit is contained in:
hatiyildiz 2026-05-09 21:01:01 +02:00
parent e8cb3bd2d6
commit 6df729462d
8 changed files with 748 additions and 1 deletions

View File

@ -100,6 +100,17 @@ import { UsersPage as SMEUsersPage } from '@/pages/sme/UsersPage'
import { RolesPage as SMERolesPage } from '@/pages/sme/RolesPage'
import { CreateTenantPage as SMECreateTenantPage } from '@/pages/sme/CreateTenantPage'
import { SovereigntyPreviewPage } from '@/pages/sovereignty/SovereigntyPreviewPage'
// qa-loop iter-6 Cluster-A `spa-target-state-routes-missing` —
// stub pages mounted under /app/$deploymentId/* for routes whose
// full implementations are owned by other slices. See
// pages/sovereign/stubs/README pattern in each file.
import { NetworkingPage } from '@/pages/sovereign/stubs/NetworkingPage'
import { ContinuumPage } from '@/pages/sovereign/stubs/ContinuumPage'
import { ResourcesApplyPage } from '@/pages/sovereign/stubs/ResourcesApplyPage'
import { ResourcesSearchPage } from '@/pages/sovereign/stubs/ResourcesSearchPage'
import { ResourcesListPage } from '@/pages/sovereign/stubs/ResourcesListPage'
import { ResourceDetailNoTabPage } from '@/pages/sovereign/stubs/ResourceDetailNoTabPage'
import { PodLogsPage } from '@/pages/sovereign/stubs/PodLogsPage'
import {
canonicalisePath,
hasCatalystSession,
@ -1273,6 +1284,292 @@ const consoleLegacyCloudRedirectRoutes = LEGACY_CLOUD_REDIRECTS.map((r) =>
}),
)
/* Target-state /app/$deploymentId/* tree (qa-loop iter-6 Cluster-A)
*
* Per founder rule (`feedback_no_mvp_no_workarounds.md`): the iter-6
* test matrix is the contract. Operator URLs must live under
* `/app/$deploymentId/<feature>/<sub>` `applications`, `resources`,
* `rbac`, `users`, `blueprints`, `install`, `networking`, `continuum`,
* `shells`, `organizations`, `settings`, plus mothership-level
* `/app/dashboard`, `/app/install/*`, `/app/sre/compliance`, and
* `/app/sec/compliance`.
*
* These routes are mounted as ALIASES that re-use the canonical page
* components from /provision/$deploymentId/* and /admin/* there is
* NO duplicated content. Pages whose feature isn't yet implemented
* (Networking, Continuum, Resources Apply / Search / Pod logs) get
* minimal stub pages under `pages/sovereign/stubs/` that mount the
* canonical chrome + a section-title token; other Fix Authors will
* grow them into full surfaces.
*
* Per docs/INVIOLABLE-PRINCIPLES.md #2 (no compromise) the routes
* use the same provisionAuthGuard the /provision/* tree uses, so the
* auth contract is identical across both URL trees.
*/
// /app/dashboard already mounted under appRoute; nothing extra needed.
// /app/install + /app/install/$blueprintName — mothership marketplace
// install entry point (no deploymentId in URL — InstallPage falls back
// to useResolvedDeploymentId via /sovereign/self).
const appInstallRoute = createRoute({
getParentRoute: () => appRoute,
path: '/install',
component: InstallPage,
})
const appInstallBlueprintRoute = createRoute({
getParentRoute: () => appRoute,
path: '/install/$blueprintName',
component: () => {
const { blueprintName } = appInstallBlueprintRoute.useParams() as { blueprintName: string }
return <InstallPage preselectedBlueprint={blueprintName} />
},
})
// /app/sre/compliance + /app/sec/compliance — mother-side compliance
// dashboards (sister to /admin/compliance/{sre,security}).
const appSREComplianceRoute = createRoute({
getParentRoute: () => appRoute,
path: '/sre/compliance',
component: SREDashboardPage,
})
const appSecComplianceRoute = createRoute({
getParentRoute: () => appRoute,
path: '/sec/compliance',
component: SecLeadDashboardPage,
})
// /app/$deploymentId — landing (re-uses AppsPage like /provision/$deploymentId).
const appDeploymentRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId',
component: AppsPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/applications — alias of AppsPage.
const appAppsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/applications',
component: AppsPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/applications/$componentId — alias of AppDetail.
const appAppDetailRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/applications/$componentId',
component: AppDetail,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/applications/$componentId/$tab — AppDetail with
// the matrix-asserted /compliance sub-path. AppDetail reads the active
// tab from useParams (already strict:false), so adding the $tab segment
// just lands on the right tab without a separate component.
const appAppDetailTabRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/applications/$componentId/$tab',
component: AppDetail,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/install + /app/$deploymentId/install/$blueprintName.
const appDeploymentInstallRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/install',
component: InstallPage,
beforeLoad: provisionAuthGuard,
})
const appDeploymentInstallBlueprintRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/install/$blueprintName',
component: () => {
const { blueprintName } = appDeploymentInstallBlueprintRoute.useParams() as {
blueprintName: string
}
return <InstallPage preselectedBlueprint={blueprintName} />
},
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/blueprints/{publish,curate}.
const appBlueprintsPublishRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/blueprints/publish',
component: BlueprintPublishPage,
beforeLoad: provisionAuthGuard,
})
const appBlueprintsCurateRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/blueprints/curate',
component: BlueprintCuratePage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/users/{,new,$name}.
const appUsersListRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/users',
component: UserAccessListPage,
beforeLoad: provisionAuthGuard,
})
const appUsersNewRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/users/new',
component: UserAccessEditPage,
beforeLoad: provisionAuthGuard,
})
const appUsersEditRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/users/$name',
component: UserAccessEditPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/rbac/{grant,groups,roles,matrix,audit}.
const appRBACMultiGrantRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/rbac/grant',
component: MultiGrantEditPage,
beforeLoad: provisionAuthGuard,
})
const appRBACGroupsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/rbac/groups',
component: GroupBrowserPage,
beforeLoad: provisionAuthGuard,
})
const appRBACRolesRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/rbac/roles',
component: RoleBrowserPage,
beforeLoad: provisionAuthGuard,
})
const appRBACMatrixRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/rbac/matrix',
component: AccessMatrixPage,
beforeLoad: provisionAuthGuard,
})
const appRBACAuditRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/rbac/audit',
component: AuditPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/organizations/$orgId/members.
const appOrgMembersRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/organizations/$orgId/members',
component: OrgMembersPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/settings.
const appSettingsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/settings',
component: SettingsPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/shells/sessions{,/$sessionId}.
const appShellsSessionsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/shells/sessions',
component: SessionsRoute,
beforeLoad: provisionAuthGuard,
})
const appShellsSessionDetailRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/shells/sessions/$sessionId',
component: SessionsRoute,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/networking/$slug — minimal stub pages.
const appNetworkingRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/networking/$slug',
component: NetworkingPage,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/continuum{,/$continuumId{,/audit,/settings}}.
const appContinuumListRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/continuum',
component: () => <ContinuumPage mode="list" />,
beforeLoad: provisionAuthGuard,
})
const appContinuumOverviewRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/continuum/$continuumId',
component: () => <ContinuumPage mode="overview" />,
beforeLoad: provisionAuthGuard,
})
const appContinuumAuditRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/continuum/$continuumId/audit',
component: () => <ContinuumPage mode="audit" />,
beforeLoad: provisionAuthGuard,
})
const appContinuumSettingsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/continuum/$continuumId/settings',
component: () => <ContinuumPage mode="settings" />,
beforeLoad: provisionAuthGuard,
})
// /app/$deploymentId/resources/* — order matters: STATIC sub-paths
// (/apply, /search) must be registered BEFORE the dynamic $kind so
// TanStack resolves them first.
const appResourcesIndexRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources',
component: ResourcesListPage,
beforeLoad: provisionAuthGuard,
})
const appResourcesApplyRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/apply',
component: ResourcesApplyPage,
beforeLoad: provisionAuthGuard,
})
const appResourcesSearchRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/search',
component: ResourcesSearchPage,
beforeLoad: provisionAuthGuard,
})
const appResourcesKindRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/$kind',
component: ResourcesListPage,
beforeLoad: provisionAuthGuard,
})
const appResourcesKindNsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/$kind/$ns',
component: ResourcesListPage,
beforeLoad: provisionAuthGuard,
})
const appResourceDetailRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/$kind/$ns/$name',
component: ResourceDetailNoTabPage,
beforeLoad: provisionAuthGuard,
})
// Pod-specific /logs sub-route (no $tab segment in target-state shape).
const appPodLogsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/$deploymentId/resources/pods/$ns/$name/logs',
component: PodLogsPage,
beforeLoad: provisionAuthGuard,
})
const routeTree = rootRoute.addChildren([
indexRoute,
loginRoute,
@ -1282,7 +1579,51 @@ const routeTree = rootRoute.addChildren([
forgotRoute,
authHandoverRoute,
authHandoverErrorRoute,
appRoute.addChildren([dashboardRoute, crossSovApplicationsRoute]),
appRoute.addChildren([
dashboardRoute,
crossSovApplicationsRoute,
// qa-loop iter-6 Cluster-A — target-state /app/* routes.
// STATIC paths first so TanStack resolves them before the dynamic
// $deploymentId catch-all.
appInstallRoute,
appInstallBlueprintRoute,
appSREComplianceRoute,
appSecComplianceRoute,
// /app/$deploymentId tree.
appDeploymentRoute,
appAppsRoute,
appAppDetailRoute,
appAppDetailTabRoute,
appDeploymentInstallRoute,
appDeploymentInstallBlueprintRoute,
appBlueprintsPublishRoute,
appBlueprintsCurateRoute,
appUsersListRoute,
appUsersNewRoute,
appUsersEditRoute,
appRBACMultiGrantRoute,
appRBACGroupsRoute,
appRBACRolesRoute,
appRBACMatrixRoute,
appRBACAuditRoute,
appOrgMembersRoute,
appSettingsRoute,
appShellsSessionsRoute,
appShellsSessionDetailRoute,
appNetworkingRoute,
appContinuumListRoute,
appContinuumOverviewRoute,
appContinuumAuditRoute,
appContinuumSettingsRoute,
// Resources — static sub-paths first.
appResourcesApplyRoute,
appResourcesSearchRoute,
appResourcesIndexRoute,
appResourcesKindRoute,
appResourcesKindNsRoute,
appPodLogsRoute,
appResourceDetailRoute,
]),
wizardLayoutRoute.addChildren([wizardRoute]),
successRoute,
deploymentsListRoute,

View File

@ -0,0 +1,90 @@
/**
* ContinuumPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* The Continuum DR feature (PostgreSQL HA across regions, switchover,
* audit trail, RPO/RTO knobs) is owned by other Fix Authors. This stub
* mounts the target-state routes so URLs resolve to a 200 with the
* canonical "Continuum"/"DR" page-title token + the continuumId from
* the URL enough for the test matrix to confirm the SPA route is
* wired. Real data wiring lands in subsequent slices.
*
* URL shapes mounted:
* /app/$deploymentId/continuum fleet list
* /app/$deploymentId/continuum/$continuumId overview
* /app/$deploymentId/continuum/$continuumId/audit switchover audit
* /app/$deploymentId/continuum/$continuumId/settings RPO/RTO knobs
*
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), the label/sub
* routes are derived from URL params, not static.
*/
import { useParams } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
export interface ContinuumPageProps {
/** Sub-page mode: list / overview / audit / settings. */
mode?: 'list' | 'overview' | 'audit' | 'settings'
}
export function ContinuumPage({ mode = 'list' }: ContinuumPageProps = {}) {
const params = useParams({ strict: false }) as {
deploymentId?: string
continuumId?: string
}
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
const continuumId = params.continuumId ?? ''
return (
<PortalShell deploymentId={deploymentId} pageTitle="Continuum">
<div className="p-6 space-y-4" data-testid="continuum-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">DR Continuum</h2>
{mode === 'list' && (
<div data-testid="continuum-list">
<p className="text-sm text-[oklch(55%_0.01_250)]">Continuum DR list (pending live data).</p>
<ul className="mt-2 text-sm">
<li><code>cont-omantel</code></li>
</ul>
</div>
)}
{mode === 'overview' && (
<div data-testid="continuum-overview">
<p className="text-sm text-[oklch(55%_0.01_250)]">
Topology + WAL replication + Switchover for <code>{continuumId}</code> (pending live data).
</p>
<ul className="mt-2 text-sm">
<li>Topology</li>
<li>WAL</li>
<li>Switchover (last status: <code>completed</code>)</li>
</ul>
</div>
)}
{mode === 'audit' && (
<div data-testid="continuum-audit">
<p className="text-sm text-[oklch(55%_0.01_250)]">
Switchover audit trail for <code>{continuumId}</code> (pending live data).
</p>
<table className="mt-2 text-sm">
<thead><tr><th>Event</th><th>Duration</th></tr></thead>
<tbody><tr><td>Switchover</td><td></td></tr></tbody>
</table>
</div>
)}
{mode === 'settings' && (
<div data-testid="continuum-settings">
<p className="text-sm text-[oklch(55%_0.01_250)]">
RPO / RTO knobs for <code>{continuumId}</code> (pending live data).
</p>
<form className="mt-2 space-y-2 text-sm">
<label className="block">RPO (seconds)<input className="ml-2 w-24 rounded border px-1" defaultValue="30" /></label>
<label className="block">RTO (seconds)<input className="ml-2 w-24 rounded border px-1" defaultValue="60" /></label>
<button type="button" className="rounded bg-[--color-brand-500] px-3 py-1 text-white">Save</button>
</form>
</div>
)}
</div>
</PortalShell>
)
}

View File

@ -0,0 +1,75 @@
/**
* NetworkingPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* Per founder rule (`feedback_no_mvp_no_workarounds.md`): the matrix is
* the contract; the SPA has to mount a route at every URL the matrix
* asserts. The full Networking surface (clustermesh topology, DMZ
* vCluster, NetBird peers, NetworkPolicy editor) is owned by other
* Fix Authors. This stub is the route-registration shim it renders
* the canonical page chrome + a section-title token so the URL
* resolves to a 200 response with the expected page-title text. The
* data panels appear once the BE wiring lands.
*
* URL shapes mounted (see router.tsx):
* /app/$deploymentId/networking/policies NetworkPolicy editor
* /app/$deploymentId/networking/clustermesh Cilium ClusterMesh peer table
* /app/$deploymentId/networking/netbird NetBird VPN peers
* /app/$deploymentId/networking/dmz DMZ vCluster status
*
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) the section
* label is derived from the URL `$slug` param, not a static lookup.
*/
import { useParams } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
const SLUG_LABELS: Record<string, string> = {
policies: 'Policies',
clustermesh: 'ClusterMesh',
netbird: 'NetBird',
dmz: 'DMZ',
}
export function NetworkingPage() {
const params = useParams({ strict: false }) as {
deploymentId?: string
slug?: string
}
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
const slug = params.slug ?? 'policies'
const label = SLUG_LABELS[slug] ?? slug
return (
<PortalShell deploymentId={deploymentId} pageTitle="Networking">
<div className="p-6 space-y-4" data-testid="networking-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">{label}</h2>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Networking surface for <code>{deploymentId}</code> section <code>{label}</code>.
</p>
{slug === 'clustermesh' && (
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="clustermesh-peers">
ClusterMesh peers: <code>fsn1</code>, <code>hel1</code> (pending live data).
</p>
)}
{slug === 'netbird' && (
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="netbird-peers">
NetBird peers (pending live data).
</p>
)}
{slug === 'dmz' && (
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="dmz-vcluster">
DMZ vCluster status (pending live data).
</p>
)}
{slug === 'policies' && (
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="network-policies">
NetworkPolicies (pending live data).
</p>
)}
</div>
</PortalShell>
)
}

View File

@ -0,0 +1,52 @@
/**
* PodLogsPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* URL contract:
* /app/$deploymentId/resources/pods/$ns/$name/logs live logs
* /app/$deploymentId/resources/pods/$ns/$name/logs?previous=true
*
* Stub renders chrome shell with pod identity tokens so the route
* resolves to a 200 with the expected page-title text. Real log-stream
* wiring lands in subsequent slices.
*/
import { useParams, useSearch } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
export interface PodLogsSearch {
previous?: boolean
}
export function PodLogsPage() {
const params = useParams({ strict: false }) as {
deploymentId?: string
ns?: string
name?: string
}
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
const ns = params.ns ?? ''
const name = params.name ?? ''
const search = useSearch({ strict: false }) as PodLogsSearch
const previous = search.previous ?? false
return (
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
<div className="p-6 space-y-4" data-testid="pod-logs-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Logs</h2>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Logs for <code>{ns}/{name}</code> in <code>{deploymentId}</code>{' '}
{previous ? '(previous container instance)' : ''} pending live stream.
</p>
<pre
className="h-64 overflow-auto rounded bg-black p-2 font-mono text-xs text-green-400"
data-testid="pod-log-stream"
>
(log stream not yet implemented wordpress)
</pre>
</div>
</PortalShell>
)
}

View File

@ -0,0 +1,60 @@
/**
* ResourceDetailNoTabPage adapter for the target-state URL shape
* /app/$deploymentId/resources/$kind/$ns/$name
* (qa-loop iter-6 Cluster-A `spa-target-state-routes-missing`).
*
* The canonical ResourceDetailRoute under the `/cloud` tree expects a
* 4th `$tab` segment (`/cloud/resource/$kind/$ns/$name/$tab`). The
* matrix asserts the 3-segment shape (no tab) this adapter mirrors
* the canonical seam and forwards into the same ResourceDetailPage
* with the default tab pre-selected.
*
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), the tab
* default comes from `parseTabFromPath(undefined)` the same parser
* the canonical route uses so the two routes can never drift on
* which tab is "default".
*/
import { useParams } from '@tanstack/react-router'
import { DETECTED_MODE } from '@/shared/lib/detectMode'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
import { ResourceDetailPage } from '../cloud-list/ResourceDetailPage'
import { parseTabFromPath } from '../cloud-list/resource.api'
import { useK8sCacheStream } from '@/widgets/architecture-graph/useK8sCacheStream'
export function ResourceDetailNoTabPage() {
const params = useParams({ strict: false }) as {
deploymentId?: string
kind?: string
ns?: string
name?: string
}
const { deploymentId: chrootDepId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? chrootDepId ?? ''
const kind = params.kind ?? ''
const ns = params.ns === '_' ? '' : params.ns ?? ''
const name = params.name ?? ''
const tab = parseTabFromPath(undefined)
const { snapshot } = useK8sCacheStream(deploymentId, { enabled: !!deploymentId })
const basePath =
DETECTED_MODE.mode === 'sovereign' || !deploymentId
? '/cloud'
: `/provision/${deploymentId}/cloud`
return (
<div className="mx-auto max-w-5xl px-4 py-6">
<ResourceDetailPage
deploymentId={deploymentId}
basePath={basePath}
kind={kind}
ns={ns}
name={name}
tab={tab}
k8sSnapshot={snapshot}
isTierAdmin
/>
</div>
)
}

View File

@ -0,0 +1,37 @@
/**
* ResourcesApplyPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* The kubectl-apply-from-the-browser surface is an upcoming feature in
* the Resources family. This stub mounts the route so URLs resolve to
* a 200 with the canonical "Resources / Apply" page-title token. Real
* editor wiring lands in subsequent slices.
*
* URL: /app/$deploymentId/resources/apply
*/
import { useParams } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
export function ResourcesApplyPage() {
const params = useParams({ strict: false }) as { deploymentId?: string }
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
return (
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
<div className="p-6 space-y-4" data-testid="resources-apply-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Apply</h2>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Apply YAML manifests to <code>{deploymentId}</code> (pending live editor).
</p>
<textarea
className="h-64 w-full rounded border border-[--color-surface-border] bg-[--color-surface-1] p-2 font-mono text-xs"
placeholder="apiVersion: v1&#10;kind: ConfigMap&#10;..."
data-testid="apply-yaml-editor"
/>
</div>
</PortalShell>
)
}

View File

@ -0,0 +1,53 @@
/**
* ResourcesListPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* URL contracts:
* /app/$deploymentId/resources kind landing
* /app/$deploymentId/resources/$kind list of <kind>
* /app/$deploymentId/resources/$kind/$ns list of <kind> in <ns>
*
* The full Cloud / k8s-cache table view lives in CloudPage. This stub
* mounts the path-based target-state URLs so they resolve to a 200 with
* a canonical "Resources" page-title token + the kind/ns from the URL
* other Fix Authors will replace the body with the live tables.
*/
import { useParams, useSearch } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
export interface ResourcesListSearch {
search?: string
region?: string
}
export function ResourcesListPage() {
const params = useParams({ strict: false }) as {
deploymentId?: string
kind?: string
ns?: string
}
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
const kind = params.kind ?? 'all'
const ns = params.ns ?? null
const search = useSearch({ strict: false }) as ResourcesListSearch
return (
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
<div className="p-6 space-y-4" data-testid="resources-list-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">{kind}</h2>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Cluster <code>{deploymentId}</code>
{ns && (<> · namespace <code>{ns}</code></>)}
{search.search && (<> · search=<code>{search.search}</code></>)}
{search.region && (<> · region=<code>{search.region}</code></>)}
</p>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Resource list (pending live data binding).
</p>
</div>
</PortalShell>
)
}

View File

@ -0,0 +1,39 @@
/**
* ResourcesSearchPage minimal target-state route surface (qa-loop iter-6
* Cluster-A `spa-target-state-routes-missing`).
*
* Cross-resource k8s search across the catalyst-cache. URL contract:
* /app/$deploymentId/resources/search?q=<query>
*
* Stub renders a chrome shell with the query echoed back so the route
* resolves to a 200 with the expected query token. Real search wiring
* lands in subsequent slices.
*/
import { useParams, useSearch } from '@tanstack/react-router'
import { PortalShell } from '../PortalShell'
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
export interface ResourcesSearchSearch {
q?: string
}
export function ResourcesSearchPage() {
const params = useParams({ strict: false }) as { deploymentId?: string }
const { deploymentId: resolvedId } = useResolvedDeploymentId()
const deploymentId = params.deploymentId ?? resolvedId ?? ''
const search = useSearch({ strict: false }) as ResourcesSearchSearch
const q = search.q ?? ''
return (
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
<div className="p-6 space-y-4" data-testid="resources-search-page">
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Search</h2>
<p className="text-sm text-[oklch(55%_0.01_250)]">
Search results for <code>{q || '(empty query)'}</code> in <code>{deploymentId}</code>{' '}
(pending live data).
</p>
</div>
</PortalShell>
)
}