IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

service.ts

Buka raw
import "server-only";

import { LruCache } from "@/lib/server/cache/lru";
import { memoizeAsync } from "@/lib/server/cache/memoize-async";
import { serverEnv } from "@/lib/server/env";
import { logServerAction } from "@/lib/server/logging/logger";
import type { SmartDiskInfo, StorageMetrics, SystemMetricsSnapshot } from "@/lib/shared/contracts/system";
import os from "node:os";
import { statfs } from "node:fs/promises";
import path from "node:path";
import si from "systeminformation";
import { isContainerRuntime } from "@/lib/server/modules/system/update-service";
import {
  createDiskRateTracker,
  readDiskCounters,
  totalMegabytesPerSecond,
} from "@/lib/server/modules/system/disk-activity";
import { createCpuLoadTracker, readCpuTimes } from "@/lib/server/modules/system/cpu-activity";
import {
  createNetworkRateTracker,
  readDefaultRouteInterface,
  readInterfaceAddresses,
  readInterfaceCounters,
} from "@/lib/server/modules/system/network-activity";
import {
  getNetworkStatusFromHelper,
  isNetworkHelperUnavailableError,
} from "@/lib/server/modules/network/helper-client";

// CPU load, disk throughput and network throughput are rates between two
// kernel counter readings, one per collected snapshot.
const snapshotCpuLoad = createCpuLoadTracker();
const snapshotDiskRates = createDiskRateTracker();
const snapshotNetworkRates = createNetworkRateTracker();

const metricsCache = new LruCache<SystemMetricsSnapshot>(
  8,
  serverEnv.METRICS_CACHE_TTL_MS,
);
const HELPER_STATUS_CACHE_TTL_MS = 10_000;
const HELPER_STATUS_UNAVAILABLE_BACKOFF_MS = 15_000;
const HELPER_STATUS_ERROR_LOG_COOLDOWN_MS = 30_000;
const STORAGE_DETAILS_CACHE_TTL_MS = 60_000;
const WIFI_NETWORKS_CACHE_TTL_MS = 30_000;
const CPU_TEMPERATURE_CACHE_TTL_MS = 10_000;
const WIFI_CONNECTIONS_CACHE_TTL_MS = 10_000;

// Temperature and the Wi-Fi link change slowly, and systeminformation spawns
// commands to read each of them: read them every 10s, not on every snapshot.
const getCpuTemperature = memoizeAsync(
  () => withFallback("system.metrics.cpuTemperature", () => si.cpuTemperature(), null),
  CPU_TEMPERATURE_CACHE_TTL_MS,
);
const getWifiConnections = memoizeAsync(
  () => withFallback("system.metrics.wifiConnections", () => si.wifiConnections(), []),
  WIFI_CONNECTIONS_CACHE_TTL_MS,
);

/** Filesystem types that are virtual/pseudo and should not appear as real disk volumes. */
const VIRTUAL_FS_TYPES = new Set([
  "overlay",
  "tmpfs",
  "squashfs",
  "devtmpfs",
  "sysfs",
  "proc",
  "cgroup",
  "cgroup2",
  "nsfs",
  "fusectl",
  "hugetlbfs",
  "mqueue",
  "debugfs",
  "tracefs",
  "securityfs",
  "configfs",
  "pstore",
  "autofs",
  "efivarfs",
  "bpf",
]);

type CachedStorageDetails = Pick<StorageMetrics, "volumes" | "raid" | "smart">;

let helperStatusCache:
  | {
      value: Awaited<ReturnType<typeof getNetworkStatusFromHelper>>;
      expiresAt: number;
    }
  | null = null;
let wifiNetworksCache: {
  value: Awaited<ReturnType<typeof si.wifiNetworks>>;
  expiresAt: number;
} | null = null;
let wifiNetworksRefreshing = false;
let storageDetailsCache:
  | {
      value: CachedStorageDetails;
      expiresAt: number;
    }
  | null = null;
/** Last successfully collected storage details — persists beyond cache TTL for stale serving. */
let storageDetailsLastKnown: CachedStorageDetails | null = null;
/** True while a background (or first-ever) storage detail collection is in flight. */
let storageDetailsRefreshing = false;
let helperStatusUnavailableUntil = 0;
let lastHelperStatusErrorLogAt = 0;
/**
 * In-flight snapshot collection, shared by all concurrent callers.
 * collectSnapshot shells out to `sensors`, `nmcli` and `ip`; on a slow box it can
 * outlast METRICS_PUBLISH_INTERVAL_MS, and without this guard every SSE tick
 * started another one on top of the last until the subprocesses ate the CPU.
 */
let snapshotInFlight: Promise<SystemMetricsSnapshot> | null = null;

function clampPercent(value: number) {
  if (!Number.isFinite(value)) return 0;
  return Math.max(0, Math.min(Number(value.toFixed(2)), 100));
}

function toNumber(value: number | bigint) {
  if (typeof value === "bigint") {
    return Number(value);
  }
  return value;
}

function toFiniteNumber(value: unknown) {
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
    return null;
  }
  return value;
}

function baseDeviceFromPartition(device: string) {
  return device.replace(/p?\d+$/, "");
}

function deriveVolumeLabel(input: {
  device: string;
  mountPath: string;
  mediaType: string | null;
}) {
  const mountName =
    input.mountPath === "/"
      ? "System"
      : input.mountPath.startsWith("/DATA")
        ? "Data"
        : path.basename(input.mountPath) || "Volume";
  const mediaSuffix = input.mediaType ? ` (${input.mediaType})` : "";
  return `${input.device} - ${mountName}${mediaSuffix}`;
}

function inferRaidFromVolumes(
  volumes: NonNullable<StorageMetrics["volumes"]>,
): StorageMetrics["raid"] {
  const zfsVolumes = volumes.filter((volume) =>
    volume.filesystem?.toLowerCase().includes("zfs"),
  );
  if (zfsVolumes.length > 0) {
    return {
      name: "zpool",
      type: "ZFS",
      totalBytes: zfsVolumes.reduce((sum, volume) => sum + volume.totalBytes, 0),
      redundancy: "Managed by ZFS",
      status: "healthy",
    };
  }

  const mdVolumes = volumes.filter((volume) => volume.device.startsWith("/dev/md"));
  if (mdVolumes.length > 0) {
    return {
      name: path.basename(mdVolumes[0].device),
      type: "Linux RAID",
      totalBytes: mdVolumes.reduce((sum, volume) => sum + volume.totalBytes, 0),
      redundancy: "Managed by mdadm",
      status: "healthy",
    };
  }

  return null;
}

function summarizeSmartHealth(
  diskLayout: Awaited<ReturnType<typeof si.diskLayout>>,
): StorageMetrics["smart"] {
  if (diskLayout.length === 0) {
    return null;
  }

  let healthyDisks = 0;
  let failingDisks = 0;
  const disks: SmartDiskInfo[] = [];

  for (const disk of diskLayout) {
    if (typeof disk.device !== "string" || disk.device.trim().length === 0) continue;

    const rawStatus =
      typeof disk.smartStatus === "string"
        ? disk.smartStatus.trim().toLowerCase()
        : "";

    let diskStatus: SmartDiskInfo["status"];
    if (
      rawStatus.includes("fail") ||
      rawStatus.includes("error") ||
      rawStatus.includes("pred") ||
      rawStatus.includes("bad")
    ) {
      diskStatus = "degraded";
      failingDisks += 1;
    } else if (
      rawStatus.includes("ok") ||
      rawStatus.includes("good") ||
      rawStatus.includes("pass")
    ) {
      diskStatus = "healthy";
      healthyDisks += 1;
    } else {
      diskStatus = "unknown";
    }

    const temperatureCelsius =
      disk.temperature !== null &&
      typeof disk.temperature === "number" &&
      Number.isFinite(disk.temperature) &&
      disk.temperature > 0
        ? Math.round(disk.temperature)
        : null;

    const smartDataHours = disk.smartData?.power_on_time?.hours;
    const powerOnHours =
      typeof smartDataHours === "number" &&
      Number.isFinite(smartDataHours) &&
      smartDataHours >= 0
        ? Math.round(smartDataHours)
        : null;

    const sizeBytes =
      typeof disk.size === "number" && Number.isFinite(disk.size) && disk.size > 0
        ? disk.size
        : null;

    disks.push({
      device: disk.device.trim(),
      name:
        typeof disk.name === "string" && disk.name.trim().length > 0
          ? disk.name.trim()
          : null,
      vendor:
        typeof disk.vendor === "string" && disk.vendor.trim().length > 0
          ? disk.vendor.trim()
          : null,
      type:
        typeof disk.type === "string" && disk.type.trim().length > 0
          ? disk.type.trim().toUpperCase()
          : null,
      sizeBytes,
      status: diskStatus,
      smartStatus:
        typeof disk.smartStatus === "string" && disk.smartStatus.trim().length > 0
          ? disk.smartStatus.trim()
          : "Unknown",
      temperatureCelsius,
      powerOnHours,
    });
  }

  if (disks.length === 0) {
    return null;
  }

  const status =
    failingDisks > 0 ? "degraded" : healthyDisks > 0 ? "healthy" : "unknown";

  const message =
    status === "degraded"
      ? `${failingDisks} drive(s) reported potential SMART issues.`
      : status === "healthy"
        ? `All ${healthyDisks} SMART-capable drive(s) report healthy status.`
        : "SMART status unavailable for detected drives.";

  return {
    status,
    healthyDisks,
    failingDisks,
    checkedAt: new Date().toISOString(),
    message,
    disks,
  };
}

function readStorageDetailsCache() {
  if (!storageDetailsCache) return null;
  if (storageDetailsCache.expiresAt <= Date.now()) {
    storageDetailsCache = null;
    return null;
  }
  return storageDetailsCache.value;
}

/**
 * Execute the expensive storage detail collection (fsSize + diskLayout probes).
 * Updates both the TTL cache and the persistent lastKnown value.
 */
async function executeStorageDetailCollection(): Promise<CachedStorageDetails> {
  const [fsSizes, diskLayout] = await Promise.all([
    withFallback("system.metrics.storage.fsSize", () => si.fsSize(), []),
    withFallback("system.metrics.storage.diskLayout", () => si.diskLayout(), []),
  ]);

  const mediaTypeByDevice = new Map<string, string>();
  for (const disk of diskLayout) {
    if (typeof disk.device !== "string" || disk.device.trim().length === 0) continue;
    const mediaType =
      typeof disk.type === "string" && disk.type.trim().length > 0
        ? disk.type.trim().toUpperCase()
        : null;
    if (!mediaType) continue;
    mediaTypeByDevice.set(baseDeviceFromPartition(disk.device), mediaType);
  }

  // Track seen devices so we only include one entry per physical device
  // (e.g. /dev/nvme0n1p2 mounted at both / and /boot should show only one).
  const seenDevices = new Set<string>();

  const volumes = fsSizes
    .map((fsSize) => {
      const totalBytesRaw = toFiniteNumber(fsSize.size);
      if (totalBytesRaw === null || totalBytesRaw <= 0) {
        return null;
      }

      const filesystem =
        typeof fsSize.type === "string" && fsSize.type.trim().length > 0
          ? fsSize.type.trim()
          : null;

      // Skip virtual / pseudo filesystems (Docker overlay layers, tmpfs, snap, etc.)
      if (filesystem && VIRTUAL_FS_TYPES.has(filesystem.toLowerCase())) {
        return null;
      }

      const totalBytes = Math.max(Math.round(totalBytesRaw), 0);
      const usedBytesRaw = toFiniteNumber(fsSize.used);
      const usedPercentRaw = toFiniteNumber(fsSize.use);
      const inferredUsedBytes =
        usedPercentRaw !== null
          ? Math.round((totalBytes * clampPercent(usedPercentRaw)) / 100)
          : 0;
      const usedBytes =
        usedBytesRaw !== null
          ? Math.max(Math.round(usedBytesRaw), 0)
          : Math.max(inferredUsedBytes, 0);
      const usedPercent =
        usedPercentRaw !== null
          ? clampPercent(usedPercentRaw)
          : totalBytes > 0
            ? clampPercent((usedBytes / totalBytes) * 100)
            : 0;

      const mountPath =
        typeof fsSize.mount === "string" && fsSize.mount.trim().length > 0
          ? fsSize.mount
          : "/";
      const device =
        typeof fsSize.fs === "string" && fsSize.fs.trim().length > 0
          ? fsSize.fs
          : mountPath;

      // Deduplicate real block devices — keep only the first (most significant) mount point
      const baseDevice = baseDeviceFromPartition(device);
      if (baseDevice.startsWith("/dev/") && seenDevices.has(device)) {
        return null;
      }
      if (baseDevice.startsWith("/dev/")) {
        seenDevices.add(device);
      }

      const mediaType = mediaTypeByDevice.get(baseDevice) ?? null;

      return {
        id: `${device}:${mountPath}`,
        label: deriveVolumeLabel({ device, mountPath, mediaType }),
        mountPath,
        device,
        filesystem,
        mediaType,
        totalBytes,
        usedBytes: Math.min(usedBytes, totalBytes),
        usedPercent,
      };
    })
    .filter((volume): volume is NonNullable<typeof volume> => volume !== null)
    .sort((left, right) => right.usedBytes - left.usedBytes)
    .slice(0, 8);

  const details: CachedStorageDetails = {
    volumes,
    raid: inferRaidFromVolumes(volumes),
    smart: summarizeSmartHealth(diskLayout),
  };

  storageDetailsCache = {
    value: details,
    expiresAt: Date.now() + STORAGE_DETAILS_CACHE_TTL_MS,
  };
  storageDetailsLastKnown = details;

  return details;
}

/**
 * Returns storage details using a stale-while-revalidate strategy:
 *  - Fresh cache hit  → return immediately (no I/O).
 *  - Stale but data exists → return last-known value instantly; kick off a
 *    background refresh so the *next* call gets fresh data.
 *  - First call ever → must await the probe (no prior data to serve).
 *
 * This prevents the 1-3 s fsSize/diskLayout probes from blocking every SSE push
 * after the 60-second TTL expires.
 */
async function collectStorageDetailMetrics(): Promise<CachedStorageDetails> {
  // 1) Fresh cache hit — no I/O needed.
  const cached = readStorageDetailsCache();
  if (cached) return cached;

  // 2) We have a previously collected value. Serve it immediately and refresh
  //    in the background so the next cache miss gets fresh data.
  if (storageDetailsLastKnown !== null) {
    if (!storageDetailsRefreshing) {
      storageDetailsRefreshing = true;
      void executeStorageDetailCollection()
        .catch((error) => {
          logServerAction({
            level: "warn",
            layer: "service",
            action: "system.metrics.storage.background-refresh",
            status: "error",
            message: "Background storage detail refresh failed",
            error,
          });
        })
        .finally(() => {
          storageDetailsRefreshing = false;
        });
    }
    return storageDetailsLastKnown;
  }

  // 3) First call ever — must await synchronously (no stale value to serve).
  storageDetailsRefreshing = true;
  try {
    return await executeStorageDetailCollection();
  } finally {
    storageDetailsRefreshing = false;
  }
}

async function collectStorageMetrics() {
  try {
    const configuredRoot = serverEnv.FILES_ROOT;
    const targetPath = path.isAbsolute(configuredRoot)
      ? configuredRoot
      : path.resolve(process.cwd(), configuredRoot);
    const filesystemStats = await statfs(targetPath);

    const blockSize = toNumber(filesystemStats.bsize);
    const totalBlocks = toNumber(filesystemStats.blocks);
    const availableBlocks = toNumber(filesystemStats.bavail);
    const totalBytes = Math.max(Math.round(blockSize * totalBlocks), 0);
    const availableBytes = Math.max(Math.round(blockSize * availableBlocks), 0);
    const usedBytes = Math.max(totalBytes - availableBytes, 0);
    const usedPercent = totalBytes > 0 ? (usedBytes / totalBytes) * 100 : 0;
    const details = await collectStorageDetailMetrics();

    return {
      mountPath: targetPath,
      totalBytes,
      availableBytes,
      usedBytes,
      usedPercent: clampPercent(usedPercent),
      ...details,
    };
  } catch (error) {
    logServerAction({
      level: "warn",
      layer: "service",
      action: "system.metrics.storage",
      status: "error",
      message: "Unable to collect storage metrics",
      error,
    });
    return null;
  }
}

function toNullableMetric(
  value: number | undefined,
  options?: {
    precision?: number;
    allowZero?: boolean;
  },
) {
  if (typeof value !== "number" || Number.isNaN(value)) return null;

  const allowZero = options?.allowZero ?? false;
  if (!allowZero && value <= 0) return null;

  const precision = options?.precision ?? 1;
  return Number(value.toFixed(precision));
}

function toNullablePercent(value: number | undefined) {
  if (typeof value !== "number" || Number.isNaN(value)) return null;
  return Math.max(0, Math.min(Math.round(value), 100));
}

function toNullableText(value: string | undefined | null) {
  if (typeof value !== "string") return null;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : null;
}

function toDesignToMaxCapacityPercent(
  designedCapacityWh: number | null,
  maxCapacityWh: number | null,
) {
  if (designedCapacityWh === null || maxCapacityWh === null) return null;
  if (maxCapacityWh <= 0) return null;

  return Number(((maxCapacityWh / designedCapacityWh) * 100).toFixed(1));
}

/**
 * Returns cached WiFi networks immediately (or [] on first call) and
 * triggers a background scan so the next call gets fresh data.
 * This prevents the 5-8s WiFi scan from blocking the metrics snapshot.
 */
function getWifiNetworksStale(): Awaited<ReturnType<typeof si.wifiNetworks>> {
  const now = Date.now();

  if (wifiNetworksCache && wifiNetworksCache.expiresAt > now) {
    return wifiNetworksCache.value;
  }

  if (!wifiNetworksRefreshing) {
    wifiNetworksRefreshing = true;
    void withFallback("system.metrics.wifiNetworks", () => si.wifiNetworks(), [])
      .then((networks) => {
        wifiNetworksCache = { value: networks, expiresAt: Date.now() + WIFI_NETWORKS_CACHE_TTL_MS };
      })
      .finally(() => {
        wifiNetworksRefreshing = false;
      });
  }

  return wifiNetworksCache?.value ?? [];
}

/** Download and upload rate of an interface since the previous snapshot; null on the first one. */
async function readNetworkRate(iface: string | null) {
  if (!iface) return null;
  const counters = await readInterfaceCounters(iface);
  return counters ? snapshotNetworkRates(iface, counters, Date.now()) : null;
}

async function withFallback<T>(
  action: string,
  run: () => Promise<T>,
  fallback: T,
) {
  try {
    return await run();
  } catch (error) {
    logServerAction({
      level: "warn",
      layer: "service",
      action,
      status: "error",
      error,
      message: "systeminformation probe failed; using fallback",
    });
    return fallback;
  }
}

async function collectSnapshot(): Promise<SystemMetricsSnapshot> {
  const [oneMinute, fiveMinute, fifteenMinute] = os.loadavg();
  const cpuCores = os.cpus().length || 1;
  const totalBytes = os.totalmem();
  const freeBytes = os.freemem();
  const usedBytes = Math.max(totalBytes - freeBytes, 0);
  const defaultCpuPercent = Math.min((oneMinute / cpuCores) * 100, 100);

  const [
    cpuTemperature,
    batteryData,
    wifiConnections,
    wifiNetworks,
    defaultRouteIface,
    storageMetrics,
    diskCounters,
  ] = await Promise.all([
    getCpuTemperature(),
    withFallback("system.metrics.battery", () => si.battery(), null),
    getWifiConnections(),
    Promise.resolve(getWifiNetworksStale()),
    readDefaultRouteInterface(),
    collectStorageMetrics(),
    readDiskCounters(),
  ]);
  const cpuLoadPercent = snapshotCpuLoad(readCpuTimes());
  const diskIo = totalMegabytesPerSecond(
    diskCounters ? snapshotDiskRates(diskCounters, Date.now()) : new Map(),
  );

  const mainTemperature = toNullableMetric(cpuTemperature?.main);
  const normalizedCoreTemps = (cpuTemperature?.cores ?? [])
    .map((coreTemp) => toNullableMetric(coreTemp))
    .filter((coreTemp): coreTemp is number => coreTemp !== null);
  const maxTemperature = toNullableMetric(cpuTemperature?.max);

  // Try to get WiFi status from NetworkManager D-Bus helper first (more reliable),
  // but cache it to avoid querying DBus for every metrics stream frame.
  const helperStatus = await resolveHelperStatusForMetrics();

  const primaryWifiConnection =
    wifiConnections.find(
      (connection) =>
        connection.iface.length > 0 || connection.ssid.trim().length > 0,
    ) ?? null;
  // The Wi-Fi interface when there is a Wi-Fi link, else the one carrying the default route.
  const primaryIface = primaryWifiConnection?.iface || defaultRouteIface;
  const primaryAddresses = primaryIface
    ? readInterfaceAddresses(primaryIface)
    : { ipv4: null, ipv6: null };

  // Prefer D-Bus helper data for WiFi status (more reliable with NetworkManager)
  const connectedSsid = helperStatus?.ssid?.trim() ?? primaryWifiConnection?.ssid.trim() ?? "";
  const isWifiConnected = connectedSsid.length > 0;
  // Ethernet: no SSID but the primary interface has an IPv4 address
  const isEthernetConnected = !isWifiConnected && primaryAddresses.ipv4 !== null;
  const preferredIface = helperStatus?.iface ?? primaryIface ?? null;
  const networkRate = await readNetworkRate(preferredIface);
  const designedCapacityWh = batteryData?.hasBattery
    ? toNullableMetric(batteryData.designedCapacity, {
        precision: 0,
      })
    : null;
  const maxCapacityWh = batteryData?.hasBattery
    ? toNullableMetric(batteryData.maxCapacity, {
        precision: 0,
      })
    : null;
  const availableNetworks = wifiNetworks
    .filter((network) => network.ssid.trim().length > 0)
    .sort((left, right) => (right.quality ?? 0) - (left.quality ?? 0))
    .slice(0, 6)
    .map((network) => ({
      ssid: network.ssid.trim(),
      channel: Number.isFinite(network.channel) ? network.channel : null,
      qualityPercent: toNullablePercent(network.quality),
      security:
        network.security.length > 0
          ? network.security.join(", ")
          : network.rsnFlags.length > 0
            ? network.rsnFlags.join(", ")
            : null,
    }));
  return {
    timestamp: new Date().toISOString(),
    hostname: os.hostname(),
    platform: `${os.platform()} ${os.release()}`,
    architecture: os.arch(),
    uptimeSeconds: os.uptime(),
    cpu: {
      oneMinute,
      fiveMinute,
      fifteenMinute,
      normalizedPercent: clampPercent(cpuLoadPercent ?? defaultCpuPercent),
    },
    memory: {
      totalBytes,
      freeBytes,
      usedBytes,
      usedPercent: totalBytes > 0 ? (usedBytes / totalBytes) * 100 : 0,
    },
    temperature: {
      mainCelsius: mainTemperature,
      maxCelsius: maxTemperature,
      coresCelsius: normalizedCoreTemps,
    },
    diskIo,
    battery: {
      hasBattery: Boolean(batteryData?.hasBattery),
      isCharging: Boolean(batteryData?.isCharging),
      percent: batteryData?.hasBattery
        ? toNullablePercent(batteryData.percent)
        : null,
      timeRemainingMinutes:
        batteryData?.hasBattery &&
        typeof batteryData.timeRemaining === "number" &&
        batteryData.timeRemaining >= 0
          ? Math.round(batteryData.timeRemaining)
          : null,
      acConnected:
        typeof batteryData?.acConnected === "boolean"
          ? batteryData.acConnected
          : null,
      manufacturer: batteryData?.hasBattery
        ? toNullableText(batteryData?.manufacturer)
        : null,
      cycleCount:
        batteryData?.hasBattery &&
        typeof batteryData.cycleCount === "number" &&
        Number.isFinite(batteryData.cycleCount) &&
        batteryData.cycleCount >= 0
          ? Math.round(batteryData.cycleCount)
          : null,
      designedCapacityWh,
      maxCapacityWh,
      designToMaxCapacityPercent: toDesignToMaxCapacityPercent(
        designedCapacityWh,
        maxCapacityWh,
      ),
    },
    storage: storageMetrics ?? undefined,
    wifi: {
      connected: helperStatus?.connected ?? (isWifiConnected || isEthernetConnected),
      iface:
        helperStatus?.iface ?? (primaryIface || null),
      ssid: helperStatus?.ssid ?? (isWifiConnected ? connectedSsid : null),
      bssid: primaryWifiConnection?.bssid || null,
      signalPercent: helperStatus?.signalPercent ?? toNullablePercent(primaryWifiConnection?.quality),
      txRateMbps: toNullableMetric(primaryWifiConnection?.txRate, {
        precision: 1,
        allowZero: true,
      }),
      downloadMbps: toNullableMetric(
        networkRate ? (networkRate.rxBytesPerSec * 8) / 1_000_000 : undefined,
        {
          precision: 2,
          allowZero: true,
        },
      ),
      uploadMbps: toNullableMetric(
        networkRate ? (networkRate.txBytesPerSec * 8) / 1_000_000 : undefined,
        {
          precision: 2,
          allowZero: true,
        },
      ),
      ipv4: helperStatus?.ipv4 ?? primaryAddresses.ipv4,
      ipv6: primaryAddresses.ipv6,
      availableNetworks,
    },
    process: {
      pid: process.pid,
      uptimeSeconds: process.uptime(),
      nodeVersion: process.version,
      runtime: isContainerRuntime() ? "docker" : "host",
    },
  };
}

export async function getSystemMetricsSnapshot(options?: {
  bypassCache?: boolean;
}) {
  const startedAt = performance.now();

  if (!options?.bypassCache) {
    const cached = metricsCache.get("latest");
    if (cached) {
      logServerAction({
        level: "debug",
        layer: "service",
        action: "system.metrics.snapshot",
        status: "success",
        durationMs: Number((performance.now() - startedAt).toFixed(2)),
        meta: {
          cache: "hit",
        },
      });
      return cached;
    }
  }

  const joinedInFlight = !options?.bypassCache && snapshotInFlight !== null;
  let pending = snapshotInFlight;
  if (!joinedInFlight || pending === null) {
    const collection: Promise<SystemMetricsSnapshot> = collectSnapshot().finally(() => {
      if (snapshotInFlight === collection) {
        snapshotInFlight = null;
      }
    });
    snapshotInFlight = collection;
    pending = collection;
  }

  const snapshot = await pending;
  metricsCache.set("latest", snapshot);

  logServerAction({
    level: "debug",
    layer: "service",
    action: "system.metrics.snapshot",
    status: "success",
    durationMs: Number((performance.now() - startedAt).toFixed(2)),
    meta: {
      cache: joinedInFlight ? "in-flight" : "miss",
    },
  });

  return snapshot;
}

function readHelperStatusCache() {
  if (!helperStatusCache) return null;
  if (helperStatusCache.expiresAt <= Date.now()) {
    helperStatusCache = null;
    return null;
  }

  return helperStatusCache.value;
}

async function resolveHelperStatusForMetrics() {
  const cached = readHelperStatusCache();
  if (cached) {
    return cached;
  }

  if (Date.now() < helperStatusUnavailableUntil) {
    return null;
  }

  try {
    const status = await getNetworkStatusFromHelper();
    helperStatusCache = {
      value: status,
      expiresAt: Date.now() + HELPER_STATUS_CACHE_TTL_MS,
    };
    helperStatusUnavailableUntil = 0;
    return status;
  } catch (error) {
    if (isNetworkHelperUnavailableError(error)) {
      helperStatusUnavailableUntil = Date.now() + HELPER_STATUS_UNAVAILABLE_BACKOFF_MS;
      return null;
    }

    const now = Date.now();
    if (now - lastHelperStatusErrorLogAt >= HELPER_STATUS_ERROR_LOG_COOLDOWN_MS) {
      lastHelperStatusErrorLogAt = now;
      logServerAction({
        level: "warn",
        layer: "service",
        action: "system.metrics.network.helper",
        status: "error",
        message: "Failed to get network status from D-Bus helper",
        error,
      });
    }

    return null;
  }
}
byte
26.991
baris
868
Jenis
ts
Commit terakhir
16 Oktober 2026