fix(provision-monitor): chroot-correct paths in Sidebar / JobsTable / FlowPage (#983 follow-up) (#998)

While the operator monitors an in-flight Sovereign from the mothership
wizard surface (`console.openova.io/sovereign/provision/$deploymentId/...`),
every internal link MUST stay scoped under that prefix. Today, three
places escape the chroot to clean root paths intended for the
Sovereign's adult hostname:

1. Sidebar.tsx (mother-monitor sidebar): FLAT_NAV[*].to and SETTINGS_ITEM.to
   were hardcoded to clean roots like '/jobs', '/cloud' — clicking a nav
   item bounced the operator out of /provision/<id>/* to /sovereign/jobs
   (which is either Sovereign-Console route on contabo's mothership view
   = 404, or the Sovereign-on-clean-root on adult view = wrong context).
   Restore the canonical /provision/$deploymentId/<page> TanStack template;
   the params={{ deploymentId }} prop already feeds the substitution.

2. JobsTable.tsx (job row + parent-chip Links): `to=`/jobs/$jobId`` is
   valid on the Sovereign adult surface but escapes the chroot on the
   mother monitor view. Add a useJobLinkBuilder hook that returns
   /provision/<id>/jobs/<jobId> on Catalyst-Zero hostnames and
   /jobs/<jobId> on Sovereign hostnames.

3. FlowPage.tsx (canvas leaf-job click navigate): same chroot escape.
   Same mode-aware target construction.

The chroot rule (founder framing): the operator CANNOT distinguish
'I'm monitoring my child being born under /provision/<id>/' from
'I'm at home on the adult Sovereign console' visually — every page,
sidebar, link, and chip must look identical (#983 pixel-byte-byte
contract). This commit closes the navigation half of that contract
on the mother side; PR #983 already covered the data-fetch half.

Closes the bug surfaced live on otech118 mid-provision: clicking Jobs
in the sidebar from /sovereign/provision/571a382deb47e50a/dashboard
sent the operator to /sovereign/jobs (404 / wrong scope), and a row
click sent them to /sovereign/jobs/571a382...:install-valkey instead
of /sovereign/provision/<id>/jobs/<id>:install-valkey.

Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
e3mrah 2026-05-05 23:25:02 +04:00 committed by GitHub
parent 643f9df9dd
commit 11dd19e519
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 16 deletions

View File

@ -41,6 +41,7 @@ import {
} from 'react'
import { Link, useNavigate, useParams, useSearch } from '@tanstack/react-router'
import { useWizardStore } from '@/entities/deployment/store'
import { DETECTED_MODE } from '@/shared/lib/detectMode'
import { PortalShell } from './PortalShell'
import { resolveApplications, type ApplicationDescriptor } from './applicationCatalog'
import { useDeploymentEvents } from './useDeploymentEvents'
@ -443,11 +444,17 @@ export function FlowPage({
toggleFold(jobId)
return
}
// Leaf: navigate to its own home.
navigate({
to: '/jobs/$jobId' as never,
params: { deploymentId, jobId } as never,
})
// Leaf: navigate to its own home. Chroot-aware target: when the
// operator is on the mother's monitoring surface (deploymentId
// present in URL params), stay scoped under
// /provision/<id>/jobs/<jobId>; on the Sovereign's adult
// hostname the deploymentId is implicit so the clean root form
// /jobs/<jobId> is correct.
const target =
deploymentId && DETECTED_MODE.mode !== 'sovereign'
? `/provision/${deploymentId}/jobs/${jobId}`
: `/jobs/${jobId}`
navigate({ to: target as never })
},
[navigate, deploymentId, cancelPendingClick, allJobs, toggleFold],
)

View File

@ -26,8 +26,9 @@
*/
import { useMemo, useState } from 'react'
import { Link } from '@tanstack/react-router'
import { Link, useParams } from '@tanstack/react-router'
import type { Job, JobStatus } from '@/lib/jobs.types'
import { DETECTED_MODE } from '@/shared/lib/detectMode'
/*
* Pure helpers (exported for unit tests)
@ -345,8 +346,34 @@ interface JobRowProps {
parentLabel: string
}
/**
* useJobLinkBuilder returns a function that builds the chroot-correct
* Link `to` for a job id.
*
* On the mother's monitoring surface (Catalyst-Zero hostname,
* `/sovereign/provision/$deploymentId/...`) every link MUST stay scoped
* under `/provision/$deploymentId/jobs/$jobId` escaping to clean
* `/jobs/$jobId` would route the operator to either the mother's own
* Sovereign-Console route (404 here) or the legacy /sovereign/jobs
* surface (which renders disjointed data).
*
* On the Sovereign's adult surface (hostname = `console.<sov-fqdn>`)
* the deploymentId is implicit from the hostname, so the link uses the
* clean root form `/jobs/$jobId`.
*/
function useJobLinkBuilder(): (jobId: string) => string {
const params = useParams({ strict: false }) as { deploymentId?: string }
const isSovereign = DETECTED_MODE.mode === 'sovereign'
const depId = params.deploymentId ?? ''
return (jobId: string) =>
isSovereign || !depId
? `/jobs/${jobId}`
: `/provision/${depId}/jobs/${jobId}`
}
function JobRow({ job, parentLabel }: JobRowProps) {
const started = formatRelative(job.startedAt)
const jobLink = useJobLinkBuilder()
return (
<tr
className="jobs-row"
@ -355,8 +382,7 @@ function JobRow({ job, parentLabel }: JobRowProps) {
>
<td className="jobs-cell jobs-cell-name">
<Link
to={`/jobs/$jobId` as never}
params={{ jobId: job.id } as never}
to={jobLink(job.id) as never}
className="jobs-row-link"
data-testid={`jobs-row-link-${job.id}`}
>
@ -387,8 +413,7 @@ function JobRow({ job, parentLabel }: JobRowProps) {
group as its host job (issue #351). */}
{job.parentId ? (
<Link
to={`/jobs/$jobId` as never}
params={{ jobId: job.parentId } as never}
to={jobLink(job.parentId) as never}
className="jobs-chip jobs-chip-parent jobs-chip-link"
data-testid={`jobs-cell-parent-${job.id}`}
title={parentLabel}

View File

@ -75,31 +75,31 @@ const FLAT_NAV: FlatNavItem[] = [
{
id: 'apps',
label: 'Apps',
to: '/dashboard' as never,
to: '/provision/$deploymentId',
icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z',
},
{
id: 'jobs',
label: 'Jobs',
to: '/jobs' as never,
to: '/provision/$deploymentId/jobs',
icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',
},
{
id: 'dashboard',
label: 'Dashboard',
to: '/dashboard' as never,
to: '/provision/$deploymentId/dashboard',
icon: 'M3 3h7v9H3V3zm11 0h7v5h-7V3zM14 10h7v11h-7V10zM3 14h7v7H3v-7z',
},
{
id: 'cloud',
label: 'Cloud',
to: '/cloud' as never,
to: '/provision/$deploymentId/cloud',
icon: CLOUD_ICON,
},
{
id: 'users',
label: 'Users',
to: '/users' as never,
to: '/provision/$deploymentId/users',
// Tabler IconUsers — verbatim path data, viewBox 24x24.
icon: 'M9 7a4 4 0 100 8 4 4 0 000-8zM3 21v-2a4 4 0 014-4h4a4 4 0 014 4v2M16 3.13a4 4 0 010 7.75M21 21v-2a4 4 0 00-3-3.87',
},
@ -108,7 +108,7 @@ const FLAT_NAV: FlatNavItem[] = [
const SETTINGS_ITEM: FlatNavItem = {
id: 'settings',
label: 'Settings',
to: '/settings' as never,
to: '/provision/$deploymentId/settings',
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
}