/** * ComputeResourcesPanel — the "Resources" tab. * * One glance at everything that can run a request: * • This PC — live CPU / RAM / per-GPU VRAM meters (multi-GPU aware). * • Remote sources — each with its devices, status badges (Online / Busy / * Offline / Ephemeral), GPU/VRAM, model count and heartbeat age, plus * per-source actions (Test · Sync · Disconnect). * • A global "Use remote resources" default that never overrides an explicit * per-model route. */ import React, { useCallback, useEffect, useState } from "react"; import { Cloud, Cpu, Loader2, MemoryStick, Plug, RefreshCw, Trash2, Wifi, WifiOff, Zap, } from "lucide-react"; import type { ComputeDevice, ComputeSource, ComputeStatus, LocalResources, ModelManifest, } from "@homepilot/types"; import { computeClient } from "../../api"; function heartbeat(d: ComputeDevice): string { if (d.heartbeatAgeS == null) return "—"; const s = Math.max(0, Math.round(d.heartbeatAgeS)); return s < 60 ? `${s}s ago` : `${Math.round(s / 60)}m ago`; } function deviceBadge(d: ComputeDevice): { label: string; cls: string } { const online = d.onlineEffective ?? d.online; if (!online) return { label: "Offline", cls: "bg-white/5 border-white/10 text-white/40" }; if (d.capacity > 0 && d.activeJobs >= d.capacity) return { label: "Busy", cls: "bg-amber-500/10 border-amber-500/20 text-amber-300" }; return { label: "Online", cls: "bg-emerald-500/10 border-emerald-500/20 text-emerald-300" }; } function Meter({ label, percent, tone = "cyan" }: { label: string; percent?: number | null; tone?: string }) { const p = Math.max(0, Math.min(100, Math.round(percent ?? 0))); const bar = tone === "violet" ? "bg-violet-400/70" : tone === "emerald" ? "bg-emerald-400/70" : "bg-cyan-400/70"; return (
{label} {percent == null ? "—" : `${p}%`}
); } export default function ComputeResourcesPanel({ onConnectSource, }: { onConnectSource?: () => void; }) { const [status, setStatus] = useState(null); const [local, setLocal] = useState(null); const [sources, setSources] = useState([]); const [devices, setDevices] = useState([]); const [manifests, setManifests] = useState([]); const [remoteDefault, setRemoteDefault] = useState(false); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(null); const [tested, setTested] = useState>({}); const [error, setError] = useState(null); const [syncing, setSyncing] = useState(false); const [syncMsg, setSyncMsg] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const [st, res, srcs, devs, mans, settings] = await Promise.all([ computeClient.getComputeStatus().catch(() => null), computeClient.getLocalResources().catch(() => null), computeClient.listComputeSources(), computeClient.listComputeDevices(), computeClient.listModelManifests().catch(() => []), computeClient.getComputeSettings().catch(() => null), ]); setStatus(st); setLocal(res); setSources(srcs); setDevices(devs); setManifests(mans); setRemoteDefault(settings?.computeMode === "auto" || settings?.computeMode === "ollabridge_cloud"); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load resources"); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const modelsFor = (deviceId: string) => manifests.filter((m) => m.deviceIds.includes(deviceId)).length; const toggleRemoteDefault = useCallback(async () => { const next = !remoteDefault; setRemoteDefault(next); try { await computeClient.putComputeSettings({ computeMode: next ? "auto" : "local" }); } catch { setRemoteDefault(!next); // revert on failure } }, [remoteDefault]); const testSource = useCallback(async (id: string) => { setBusyId(id); try { const r = await computeClient.testComputeSource(id); setTested((t) => ({ ...t, [id]: r.ok ? "✓ Reachable" : r.reason || "Unreachable" })); } catch (e) { setTested((t) => ({ ...t, [id]: e instanceof Error ? e.message : "Test failed" })); } finally { setBusyId(null); } }, []); const syncFromCloud = useCallback(async () => { setSyncing(true); setSyncMsg(null); try { const r = await computeClient.syncFromCloud(); if (!r.linked) { setSyncMsg(r.reason || "Link your OllaBridge account to sync devices."); } else { setSyncMsg(`Synced ${r.devices} device${r.devices === 1 ? "" : "s"} · ${r.models} model${r.models === 1 ? "" : "s"} from OllaBridge Cloud.`); await load(); } } catch (e) { setSyncMsg(e instanceof Error ? e.message : "Sync failed"); } finally { setSyncing(false); } }, [load]); const disconnect = useCallback(async (id: string) => { setBusyId(id); try { await computeClient.deleteComputeSource(id); await load(); } finally { setBusyId(null); } }, [load]); const gpus = local?.gpus?.length ? local.gpus : local?.gpu?.available ? [local.gpu] : []; const localOnline = status?.localGpuAvailable ?? (gpus.length > 0); return (

Resources

Live capacity across this PC and connected sources.

{onConnectSource && ( )}
{status && (
{status.message}
)} {error && (
{error}
)} {syncMsg && (
{syncMsg}
)} {/* This PC */}
This PC
{localOnline ? "Online" : "Offline"}
{local?.cpu?.name && (
{local.cpu.name} {local.cpu.logicalCores ? · {local.cpu.logicalCores} threads : null}
{local.ram?.percent != null && }
)} {gpus.map((g, i) => (
{g.name || `GPU ${g.index ?? i}`} {g.vramTotalMb ? · {Math.round(g.vramTotalMb / 1024)} GB : null}
{g.utilizationPercent != null && }
))} {gpus.length === 0 && (
No local GPU detected — Ollama/ComfyUI may run on CPU.
)}
{/* Global default */} {/* Remote sources */} {sources.filter((s) => s.kind !== "local").map((s) => { const devs = devices.filter((d) => d.sourceId === s.id); return (
{s.name}
{s.kind}{s.baseUrl ? ` · ${s.baseUrl}` : ""}
{tested[s.id] &&
{tested[s.id]}
} {devs.length === 0 ? (
No devices reported yet.
) : (
    {devs.map((d) => { const badge = deviceBadge(d); return (
  • {d.name || d.id} {d.ephemeral && ( Ephemeral )} {d.gpuName && {d.gpuName}} {d.vramMb != null && {Math.round(d.vramMb / 1024)} GB} {modelsFor(d.id)} models {heartbeat(d)} {(d.onlineEffective ?? d.online) ? ( ) : ( )} {badge.label}
  • ); })}
)}
); })} {!loading && sources.filter((s) => s.kind !== "local").length === 0 && (
No remote compute sources connected yet.
)}
); }