/**
* 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 && (
)}
{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.
)}
);
}