Fleet: node hostname/IP on register + node terminal in the infra computer
Hostname/IP: - Daemon reports the machine's hostname (sysinfo) + primary outbound IPv4 on each heartbeat. migrations/0021 adds nodes.hostname/local_ip; cm-db heartbeat stores them; node JSON exposes them. Cards now title on the real hostname (falling back to name) + show the IP, instead of the "New node" placeholder. `name` stays user-overridable (rename). Terminal moved into the pull-out computer (no more per-card modal): - New infra computer app NodeTerminalApp (computer/apps/infra) — xterm bridged to a node's host shell over the node control channel, filling the app window (mirrors the agent Terminal's layout + ResizeObserver). Added "terminal" to the INFRA_CATALOG grid; a ?node= panel param targets a specific node (picker when unset). Clicking Terminal on a node card now opens the infra computer to that node's shell instead of a separate full-screen window. Deleted NodeTerminal.tsx. Rebuilt + re-hosted both daemon binaries (hostname change). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cfa16751c5
commit
cf6c331b02
@@ -5,11 +5,12 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Cpu, HardDrive, MemoryStick, Network, Plus, Server, ShieldCheck, Terminal, Trash2 } from "lucide-react";
|
||||
import { useQueryStates } from "nuqs";
|
||||
|
||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||
import { panelParsers } from "@/lib/url/panel-params";
|
||||
|
||||
import { ConnectHostWizard } from "../ConnectHostWizard";
|
||||
import { NodeTerminal } from "./NodeTerminal";
|
||||
|
||||
const mono = "'Geist Mono', ui-monospace, monospace";
|
||||
|
||||
@@ -30,6 +31,8 @@ export interface NodeHealth {
|
||||
export interface FleetNode {
|
||||
id: string;
|
||||
name: string;
|
||||
hostname: string | null;
|
||||
localIp: string | null;
|
||||
status: "pending" | "online" | "offline" | "draining";
|
||||
agentVersion: string | null;
|
||||
tailscaleIp: string | null;
|
||||
@@ -78,7 +81,7 @@ function Bar({ label, pct, detail, color }: { label: string; pct: number; detail
|
||||
|
||||
export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () => void }) {
|
||||
const h = node.health;
|
||||
const [term, setTerm] = useState(false);
|
||||
const [, setParams] = useQueryStates(panelParsers, { shallow: true });
|
||||
const [check, setCheck] = useState<{ ok: boolean; output: string } | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const runCheck = useCallback(() => {
|
||||
@@ -102,17 +105,18 @@ export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () =
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<span style={{ width: 36, height: 36, borderRadius: 9, background: "rgba(94,200,216,.1)", border: "1px solid rgba(94,200,216,.25)", display: "flex", alignItems: "center", justifyContent: "center", color: "#5ec8d8" }}><Server size={18} /></span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14.5, fontWeight: 700, color: "#f3f3f5", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{node.name}</div>
|
||||
<div style={{ fontSize: 14.5, fontWeight: 700, color: "#f3f3f5", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{node.hostname ?? node.name}</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 10, color: "#7a7a82", marginTop: 2 }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: STATUS_COLOR[node.status] }} />
|
||||
{node.status.toUpperCase()}
|
||||
{node.localIp ? <span>· {node.localIp}</span> : null}
|
||||
{node.agentVersion ? <span>· v{node.agentVersion}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
{node.status === "online" ? (
|
||||
<>
|
||||
<button type="button" onClick={runCheck} disabled={checking} title="Run sandbox readiness check" aria-label="Sandbox check" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(95,208,138,.3)", background: "rgba(95,208,138,.08)", color: "#5fd08a", cursor: checking ? "default" : "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><ShieldCheck size={14} /></button>
|
||||
<button type="button" onClick={() => setTerm(true)} title="Open terminal" aria-label="Open terminal" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(94,200,216,.3)", background: "rgba(94,200,216,.08)", color: "#5ec8d8", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Terminal size={14} /></button>
|
||||
<button type="button" onClick={() => setParams({ app: "terminal", node: node.id, device: "phone" })} title="Open terminal in the computer" aria-label="Open terminal" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(94,200,216,.3)", background: "rgba(94,200,216,.08)", color: "#5ec8d8", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Terminal size={14} /></button>
|
||||
</>
|
||||
) : null}
|
||||
<button type="button" onClick={remove} title="Remove node" aria-label="Remove node" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(255,255,255,.1)", background: "transparent", color: "#7a7a82", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Trash2 size={14} /></button>
|
||||
@@ -148,7 +152,6 @@ export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () =
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{term ? <NodeTerminal nodeId={node.id} nodeName={node.name} onClose={() => setTerm(false)} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user