Fixes system map handles.

This commit is contained in:
2026-07-12 14:07:57 -07:00
parent 2967d1fa5e
commit 9af37541fb

View File

@@ -198,10 +198,13 @@ function MapNode({ data }: NodeProps) {
</div> </div>
{d.sub && <div className="truncate text-[10px] text-[var(--muted)]">{d.sub}</div>} {d.sub && <div className="truncate text-[10px] text-[var(--muted)]">{d.sub}</div>}
</div> </div>
{/* Right handle: drag OUT to connect — to a hub (expose) or another node {/* Right handles: `rs` (source) is what you drag OUT to connect — to a hub
(declare "requires"). Right target lands incoming dependency lines. */} (expose) or another node (declare "requires"). `rt` (target) only anchors
incoming dependency lines, so it's nudged below `rs`: stacked on top it
would swallow the pointer-down (you can't START a drag from a target
handle in strict mode) and dragging a fresh line would do nothing. */}
<Handle id="rs" type="source" position={Position.Right} style={{ ...HANDLE, opacity: 0.9, background: color }} /> <Handle id="rs" type="source" position={Position.Right} style={{ ...HANDLE, opacity: 0.9, background: color }} />
<Handle id="rt" type="target" position={Position.Right} style={{ ...HANDLE, opacity: 0 }} /> <Handle id="rt" type="target" position={Position.Right} style={{ ...HANDLE, opacity: 0, top: "78%" }} />
<Handle id="lt" type="target" position={Position.Left} isConnectable={false} style={{ ...HANDLE, opacity: 0 }} /> <Handle id="lt" type="target" position={Position.Left} isConnectable={false} style={{ ...HANDLE, opacity: 0 }} />
<Handle id="ls" type="source" position={Position.Left} isConnectable={false} style={{ ...HANDLE, opacity: 0 }} /> <Handle id="ls" type="source" position={Position.Left} isConnectable={false} style={{ ...HANDLE, opacity: 0 }} />
</div> </div>
@@ -474,9 +477,13 @@ export function SystemMapPage() {
// fallback, but there's no ProgramSpec, so no grip/link. // fallback, but there's no ProgramSpec, so no grip/link.
const catalog = new Set((programs ?? []).map((p) => p.id)) const catalog = new Set((programs ?? []).map((p) => p.id))
const routes = gateway?.routes ?? [] const routes = gateway?.routes ?? []
const exposed = new Set(routes.map((r) => r.name).filter(Boolean) as string[]) // The gateway's authoritative public (tunnel) URL per exposed name, so a public
const publicSet = new Set( // service's launch link reads as its internet address, not the internal one.
routes.filter((r) => r.public_url).map((r) => r.name).filter(Boolean) as string[], // (Only the *address* comes from the gateway; exposure state is `n.reach`.)
const publicUrlOf = new Map(
routes
.filter((r) => r.public_url && r.name)
.map((r) => [r.name as string, r.public_url as string]),
) )
// Per-node lookups from the graph itself (authoritative for sockets/reach now). // Per-node lookups from the graph itself (authoritative for sockets/reach now).
@@ -487,11 +494,15 @@ export function SystemMapPage() {
// http-exposed service served at <name>.<domain>. TCP/tools/jobs aren't. // http-exposed service served at <name>.<domain>. TCP/tools/jobs aren't.
const domain = gateway?.domain ?? null const domain = gateway?.domain ?? null
const launchOf = (n: (typeof graph.nodes)[number]): string | undefined => { const launchOf = (n: (typeof graph.nodes)[number]): string | undefined => {
if (!domain || !n.reach || n.reach === "off") return undefined if (!n.reach || n.reach === "off") return undefined
if (n.kind === "static") return `https://${n.name}.${domain}` const isHttp =
if (n.kind === "service" && n.endpoints.some((e) => e.protocol === "http")) n.kind === "static" ||
return `https://${n.name}.${domain}` (n.kind === "service" && n.endpoints.some((e) => e.protocol === "http"))
return undefined if (!isHttp) return undefined
// Public services live at both URLs; prefer the internet one so the link
// matches the green Internet edge instead of reading as internal-only.
if (n.reach === "public" && publicUrlOf.has(n.name)) return publicUrlOf.get(n.name)
return domain ? `https://${n.name}.${domain}` : undefined
} }
// Remote launch: <subdomain>.<node-domain>, using the node's own acme domain // Remote launch: <subdomain>.<node-domain>, using the node's own acme domain
@@ -745,17 +756,19 @@ export function SystemMapPage() {
} }
} }
// Reach is modal: one line per exposed node. public → a single green line to // Reach is modal: one line per exposed node, driven by the node's own `reach`
// Internet; internal → a single blue line to Gateway. Dragging a new line to // (the single source of truth). public → a single green line to Internet;
// the other target switches the mode; deleting the line removes exposure. // internal → a single blue line to Gateway. Dragging a new line to the other
// target switches the mode; deleting the line removes exposure.
const reachOf: Record<string, "internal" | "public"> = {} const reachOf: Record<string, "internal" | "public"> = {}
for (const name of exposed) { for (const n of graph.nodes) {
if (!present.has(name)) continue if (!present.has(n.name)) continue
const isPub = publicSet.has(name) if (n.reach !== "internal" && n.reach !== "public") continue
reachOf[name] = isPub ? "public" : "internal" const isPub = n.reach === "public"
reachOf[n.name] = isPub ? "public" : "internal"
edges.push({ edges.push({
id: `exp:${name}`, id: `exp:${n.name}`,
source: name, source: n.name,
target: isPub ? "__internet__" : "__gateway__", target: isPub ? "__internet__" : "__gateway__",
sourceHandle: "rs", sourceHandle: "rs",
targetHandle: "lt", targetHandle: "lt",