IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

server-info-section.tsx

Buka raw
"use client";

import { SETTINGS_PANEL_INSET } from "@/modules/settings/components/panel/surface";
import { useServerInfo } from "@/modules/system/hooks/useServerInfo";
import { useSystemMetrics } from "@/modules/system/hooks/useSystemMetrics";
import { cn } from "@/lib/utils";
import type { ServerHardwareInfo } from "@/lib/shared/contracts/server-info";
import type { SystemMetricsSnapshot } from "@/lib/shared/contracts/system";
import { useState } from "react";

function formatBytes(bytes: number | null): string {
  if (bytes === null) return "—";
  if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
  if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(0)} MB`;
  if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(0)} KB`;
  return `${bytes} B`;
}

function Row({ label, value, mono = false }: { label: string; value: string | null | undefined; mono?: boolean }) {
  if (!value) return null;
  return (
    <div className="flex items-start justify-between gap-4 py-2">
      <span className="shrink-0 text-xs text-muted-foreground">{label}</span>
      <span className={cn("max-w-[60%] break-all text-right text-xs font-medium text-foreground", mono && "font-mono")}>{value}</span>
    </div>
  );
}

function Rows({ children }: { children: React.ReactNode }) {
  return <div className="divide-y divide-glass-border/40 px-4">{children}</div>;
}

function OsTab({ info }: { info: ServerHardwareInfo }) {
  const { os, system } = info;
  return (
    <Rows>
      <Row label="Hostname" value={os.hostname} />
      <Row label="OS" value={[os.distro, os.release].filter(Boolean).join(" ") || os.platform} />
      <Row label="Codename" value={os.codename} />
      <Row label="Kernel" value={os.kernel} mono />
      <Row label="Architecture" value={os.arch} />
      {system.manufacturer && system.model && (
        <Row label="System" value={[system.manufacturer, system.model, system.version].filter(Boolean).join(" ")} />
      )}
      <Row label="Serial" value={system.serial} mono />
      <Row label="UUID" value={system.uuid} mono />
    </Rows>
  );
}

function CpuTab({ info }: { info: ServerHardwareInfo }) {
  const { cpu } = info;
  const name = [cpu.manufacturer, cpu.brand].filter(Boolean).join(" ") || null;
  const coreInfo = cpu.physicalCores && cpu.cores
    ? `${cpu.physicalCores} physical · ${cpu.cores} logical`
    : cpu.cores ? `${cpu.cores} logical` : null;
  const speedInfo = cpu.maxSpeedGhz
    ? `${cpu.speedGhz ?? "?"} GHz (max ${cpu.maxSpeedGhz} GHz)`
    : cpu.speedGhz ? `${cpu.speedGhz} GHz` : null;

  return (
    <Rows>
      <Row label="Model" value={name} />
      <Row label="Cores" value={coreInfo} />
      {cpu.processors && cpu.processors > 1 && <Row label="Sockets" value={String(cpu.processors)} />}
      <Row label="Speed" value={speedInfo} />
      <Row label="Socket" value={cpu.socket} />
      <Row label="Governor" value={cpu.governor} />
    </Rows>
  );
}

function MemoryTab({ info }: { info: ServerHardwareInfo }) {
  const { memory } = info;
  const filledSlots = memory.slots.filter((s) => s.sizeBytes && s.sizeBytes > 0);

  return (
    <div>
      <Rows>
        <Row label="Total" value={formatBytes(memory.totalBytes)} />
        {filledSlots.length > 0 && <Row label="Modules" value={`${filledSlots.length} installed`} />}
      </Rows>

      {filledSlots.length > 0 && (
        <>
          <div className="border-t border-glass-border/40 px-4 pb-1 pt-3">
            <span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50">Slots</span>
          </div>
          <div className="divide-y divide-glass-border/30 px-4">
            {filledSlots.map((slot, i) => (
              <div key={i} className="flex items-start justify-between gap-4 py-2.5">
                <span className="shrink-0 font-mono text-xs text-muted-foreground">{slot.bank ?? `Slot ${i + 1}`}</span>
                <div className="text-right">
                  <div className="text-xs font-medium text-foreground">{formatBytes(slot.sizeBytes)}</div>
                  {slot.manufacturer && <div className="text-[11px] text-muted-foreground/60">{slot.manufacturer}</div>}
                  <div className="text-[11px] text-muted-foreground/50">
                    {[slot.type, slot.formFactor, slot.clockSpeedMhz ? `${slot.clockSpeedMhz} MHz` : null].filter(Boolean).join(" · ")}
                  </div>
                </div>
              </div>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

function GpuTab({ info }: { info: ServerHardwareInfo }) {
  const { controllers } = info.gpu;
  if (controllers.length === 0) {
    return <div className="px-4 py-3 text-xs text-muted-foreground">No GPU detected.</div>;
  }
  return (
    <div className="divide-y divide-glass-border/40 px-4">
      {controllers.map((ctrl, i) => {
        const name = [ctrl.vendor, ctrl.model].filter(Boolean).join(" ") || `GPU ${i + 1}`;
        return (
          <div key={i} className="py-3">
            <div className="mb-1 text-xs font-medium text-foreground">{name}</div>
            <div className="flex flex-wrap gap-x-4 gap-y-0.5">
              {ctrl.bus && <span className="text-[11px] text-muted-foreground/70">{ctrl.bus}</span>}
              {ctrl.vramBytes && <span className="text-[11px] text-muted-foreground/70">VRAM {formatBytes(ctrl.vramBytes)}</span>}
              {ctrl.subVendor && <span className="text-[11px] text-muted-foreground/70">{ctrl.subVendor}</span>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function ThermalTab({ metrics }: { metrics: SystemMetricsSnapshot | undefined }) {
  const temp = metrics?.temperature;

  if (!metrics) {
    return <div className="px-4 py-3 text-xs text-muted-foreground">Loading temperature data…</div>;
  }

  const main = temp?.mainCelsius;
  const max = temp?.maxCelsius;
  const cores = temp?.coresCelsius ?? [];

  if (main === null && cores.length === 0) {
    return <div className="px-4 py-3 text-xs text-muted-foreground">Temperature sensors not available on this host.</div>;
  }

  function tempColor(c: number) {
    if (c >= 85) return "text-status-red";
    if (c >= 70) return "text-status-amber";
    return "text-status-green";
  }

  function tempBarColor(c: number) {
    if (c >= 85) return "bg-status-red";
    if (c >= 70) return "bg-status-amber";
    return "bg-status-green";
  }

  return (
    <div>
      {/* Summary row */}
      <div className="flex gap-6 border-b border-glass-border/40 px-4 py-3">
        {main != null && (
          <div>
            <div className="text-[10px] uppercase tracking-[0.14em] text-muted-foreground/60">CPU</div>
            <div className={cn("mt-0.5 text-lg font-semibold tabular-nums", tempColor(main))}>{main}°C</div>
          </div>
        )}
        {max != null && (
          <div>
            <div className="text-[10px] uppercase tracking-[0.14em] text-muted-foreground/60">Max</div>
            <div className={cn("mt-0.5 text-lg font-semibold tabular-nums", tempColor(max))}>{max}°C</div>
          </div>
        )}
      </div>

      {/* Per-core grid */}
      {cores.length > 0 && (
        <div className="px-4 pb-3 pt-3">
          <div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50">
            Cores
          </div>
          <div className="grid grid-cols-2 gap-x-6 gap-y-2.5 sm:grid-cols-3">
            {cores.map((c, i) => (
              <div key={i}>
                <div className="mb-1 flex items-center justify-between">
                  <span className="text-[11px] text-muted-foreground/70">Core {i}</span>
                  <span className={cn("text-[11px] font-medium tabular-nums", tempColor(c))}>{c}°C</span>
                </div>
                <div className="h-1 overflow-hidden rounded-full bg-background/50">
                  <div
                    className={cn("h-full rounded-full transition-all", tempBarColor(c))}
                    style={{ width: `${Math.min((c / 100) * 100, 100)}%` }}
                  />
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function NetworkTab({ info }: { info: ServerHardwareInfo }) {
  const { interfaces } = info.network;

  if (interfaces.length === 0) {
    return <div className="px-4 py-3 text-xs text-muted-foreground">No network interfaces found.</div>;
  }

  return (
    <div className="divide-y divide-glass-border/40 px-4">
      {interfaces.map((iface, i) => (
        <div key={i} className="py-3">
          {/* Header row */}
          <div className="mb-2 flex items-center gap-2">
            <span className="font-mono text-xs font-semibold text-foreground">{iface.iface}</span>
            {iface.type && (
              <span className="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide bg-background/60 text-muted-foreground/70">
                {iface.type}
              </span>
            )}
            {iface.isDefault && (
              <span className="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide bg-primary/15 text-primary">
                default
              </span>
            )}
            <div className="ml-auto flex items-center gap-1.5">
              <div className={cn(
                "size-1.5 rounded-full",
                iface.operstate === "up" ? "bg-status-green" : iface.operstate === "down" ? "bg-status-red" : "bg-muted-foreground/30",
              )} />
              <span className="text-[11px] text-muted-foreground/60">{iface.operstate ?? "unknown"}</span>
            </div>
          </div>

          {/* Details */}
          <div className="space-y-0.5">
            {iface.ip4 && (
              <div className="flex justify-between gap-4">
                <span className="text-[11px] text-muted-foreground/60">IPv4</span>
                <span className="font-mono text-[11px] text-foreground/80">
                  {iface.ip4}{iface.ip4subnet ? `/${iface.ip4subnet}` : ""}
                </span>
              </div>
            )}
            {iface.ip6 && (
              <div className="flex justify-between gap-4">
                <span className="shrink-0 text-[11px] text-muted-foreground/60">IPv6</span>
                <span className="max-w-[70%] break-all font-mono text-[11px] text-foreground/80">{iface.ip6}</span>
              </div>
            )}
            {iface.mac && (
              <div className="flex justify-between gap-4">
                <span className="text-[11px] text-muted-foreground/60">MAC</span>
                <span className="font-mono text-[11px] text-foreground/80">{iface.mac}</span>
              </div>
            )}
            <div className="flex gap-4">
              {iface.speedMbps && (
                <span className="text-[11px] text-muted-foreground/60">{iface.speedMbps} Mbps</span>
              )}
              {iface.duplex && (
                <span className="text-[11px] text-muted-foreground/60">{iface.duplex} duplex</span>
              )}
              {iface.dhcp && (
                <span className="text-[11px] text-muted-foreground/60">DHCP</span>
              )}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

function FirmwareTab({ info }: { info: ServerHardwareInfo }) {
  const { bios, baseboard } = info;
  return (
    <Rows>
      {baseboard.manufacturer && baseboard.model && (
        <Row label="Motherboard" value={[baseboard.manufacturer, baseboard.model, baseboard.version].filter(Boolean).join(" ")} />
      )}
      <Row label="BIOS Vendor" value={bios.vendor} />
      <Row label="BIOS Version" value={bios.version} mono />
      <Row label="BIOS Date" value={bios.releaseDate} />
    </Rows>
  );
}

function BatteryTab({ info }: { info: ServerHardwareInfo }) {
  const { battery } = info;
  return (
    <Rows>
      <Row label="Manufacturer" value={battery.manufacturer} />
      <Row label="Designed Capacity" value={battery.designedCapacityWh ? `${battery.designedCapacityWh} Wh` : null} />
      <Row label="Max Capacity" value={battery.maxCapacityWh ? `${battery.maxCapacityWh} Wh` : null} />
      <Row label="Cycle Count" value={battery.cycleCount != null ? String(battery.cycleCount) : null} />
    </Rows>
  );
}

function SkeletonTab() {
  return (
    <div className="space-y-2.5 px-4 py-3">
      {[80, 60, 90, 50].map((w, i) => (
        <div key={i} className="flex justify-between">
          <div className="h-3 w-20 animate-pulse rounded bg-muted-foreground/15" />
          <div className="h-3 animate-pulse rounded bg-muted-foreground/10" style={{ width: `${w}px` }} />
        </div>
      ))}
    </div>
  );
}

type TabId = "os" | "cpu" | "memory" | "gpu" | "thermal" | "network" | "battery" | "firmware";

export function ServerInfoSection() {
  const { data: info, isLoading, isError } = useServerInfo();
  const { data: metrics } = useSystemMetrics();
  const [activeTab, setActiveTab] = useState<TabId>("os");

  const tabs: { id: TabId; label: string; hidden?: boolean }[] = [
    { id: "os",       label: "OS" },
    { id: "cpu",      label: "CPU" },
    { id: "memory",   label: "Memory" },
    { id: "gpu",      label: "GPU" },
    { id: "thermal",  label: "Thermal" },
    { id: "network",  label: "Network" },
    { id: "battery",  label: "Battery",  hidden: !info?.battery.hasBattery },
    { id: "firmware", label: "Firmware", hidden: !info?.bios.vendor && !info?.baseboard.manufacturer },
  ];

  const visibleTabs = tabs.filter((t) => !t.hidden);
  const safeTab = visibleTabs.find((t) => t.id === activeTab) ? activeTab : (visibleTabs[0]?.id ?? "os");

  return (
    <div className={cn(SETTINGS_PANEL_INSET, "overflow-hidden")}>
      {/* Tab bar */}
      <div className="flex flex-wrap items-center gap-1 border-b border-glass-border/50 px-2 py-2">
        {visibleTabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            className={cn(
              "rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
              safeTab === tab.id
                ? "bg-primary/15 text-primary"
                : "text-muted-foreground hover:bg-background/60 hover:text-foreground",
            )}
          >
            {tab.label}
          </button>
        ))}
        {info && (
          <span className="ml-auto text-[10px] text-muted-foreground/40">
            {new Date(info.fetchedAt).toLocaleTimeString()}
          </span>
        )}
      </div>

      {/* Tab content */}
      <div className="py-1">
        {isLoading && <SkeletonTab />}
        {isError && (
          <div className="px-4 py-3 text-xs text-muted-foreground">Failed to load hardware information.</div>
        )}
        {info && safeTab === "os"       && <OsTab info={info} />}
        {info && safeTab === "cpu"      && <CpuTab info={info} />}
        {info && safeTab === "memory"   && <MemoryTab info={info} />}
        {info && safeTab === "gpu"      && <GpuTab info={info} />}
        {         safeTab === "thermal" && <ThermalTab metrics={metrics} />}
        {info && safeTab === "network"  && <NetworkTab info={info} />}
        {info && safeTab === "battery"  && <BatteryTab info={info} />}
        {info && safeTab === "firmware" && <FirmwareTab info={info} />}
      </div>
    </div>
  );
}
byte
15.468
baris
382
Jenis
tsx
Commit terakhir
16 Oktober 2026