<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   High-frequency monitor poll. CPU, memory and network only, cheap enough to fetch every
//   second — the live-updating subset of what monitor.php returns in full.
//
// OPERATIONAL MODEL
//   Split from monitor.php on refresh rate. The fields here move second to second and are
//   readable from /proc alone. Everything that needs `docker stats`, a GraphQL call, or an
//   SSH round trip stays in the full payload, because those cannot be sampled at this rate.
//
//   The expensive memory breakdown is borrowed rather than recomputed. Docker and VM memory,
//   top processes and swap come from the last full cache — a 600s window, wide enough that
//   the card stays populated even when the background writer falls behind. Stale is the right
//   trade here: per-container memory does not move meaningfully between seconds, and paying
//   for it would defeat the point of this endpoint.
//
// DESIGN PRINCIPLES
//   Reads /proc directly, no shell.
//       /proc/meminfo and the ZFS arcstats file are parsed inline. At this poll rate a single
//       fork per request would dominate the cost of the endpoint.
//
//   Shares the CPU baseline with everything else.
//       vv_cpu_per_core() keeps its counters in a shared state file, so this endpoint, the
//       header snapshot, and the full monitor payload all report the same number rather than
//       three independent samples that visibly disagree.
//
//   Free means available, not unused.
//       MemAvailable, not MemFree — reclaimable page cache is not memory pressure, and
//       reporting MemFree would show a healthy machine as nearly full.
//
//   ZFS ARC is broken out of system memory.
//       On a host with ZFS cache pools the ARC is most of the "used" figure and is fully
//       reclaimable. Folding it into system memory would make every reading alarming and
//       none of them actionable.
//
// OPERATIONAL SAFEGUARDS
//   Read-only.
//
//   Every source read has a fallback.
//       file() with a ?: [] fallback, @file() for the arcstats path that does not exist on a
//       host without ZFS, and ?? 0 on every extracted key. A missing subsystem contributes
//       zero rather than a warning or a fatal — and at one request per second, a fatal here
//       would be a page that never stops erroring.
//
//   The borrowed fields degrade to zero independently.
//       Each ?? default is applied per field, so an absent or expired full cache costs those
//       four values and leaves CPU, memory and network — the reason to call this endpoint —
//       intact.
//
//   Swap has its own fallback path.
//       When the cached figures are absent, swap is recomputed from /proc/meminfo rather than
//       reported as zero, because zero swap used and unknown swap used look identical in the
//       UI and mean very different things.
//
//   Derived values are clamped.
//       max(0, …) on used and system memory, so the subtraction cannot go negative when the
//       cached docker and VM figures were sampled against a different total.
//
// REQUEST
//   GET, no parameters
//
// RESPONSE
//   {"cpu":{…},"mem":{total_kb,free_kb,used_kb,arc_kb,docker_kb,vm_kb,system_kb,
//    swap_total_kb,swap_used_kb,top_procs},"net":{…},"ts":epoch}
//
// DEPENDS ON
//   include/config.php   vv_cache_read()
//   include/common.php   vv_cpu_per_core(), vv_network_stats()
//   /proc/meminfo, /proc/spl/kstat/zfs/arcstats
//   monitor cache        written by Tools/api_cache_writer.sh — source of the borrowed fields
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/common.php';

// Pull slow fields (docker/vm/top_procs/swap) from last full cache — stale is fine,
// these don't change on a per-second basis. 600s window so the card stays populated
// even if the full cache writer is temporarily behind.
$_full      = vv_cache_read('monitor', 600);
$_dockerKb  = $_full['mem']['docker_kb']      ?? 0;
$_vmKb      = $_full['mem']['vm_kb']          ?? 0;
$_topProcs  = $_full['mem']['top_procs']      ?? [];
$_swapTotal = $_full['mem']['swap_total_kb']  ?? 0;
$_swapUsed  = $_full['mem']['swap_used_kb']   ?? 0;

// Fast memory: /proc/meminfo + ZFS ARC (no docker stats, no GQL)
$_memRaw = [];
foreach (file('/proc/meminfo') ?: [] as $_l) {
    if (preg_match('/^(\w+):\s+(\d+)/', $_l, $_m)) $_memRaw[$_m[1]] = (int)$_m[2];
}
$_totalKb = $_memRaw['MemTotal']    ?? 0;
$_freeKb  = $_memRaw['MemAvailable'] ?? 0;
$_arcKb   = 0;
foreach (@file('/proc/spl/kstat/zfs/arcstats') ?: [] as $_l) {
    if (preg_match('/^size\s+\d+\s+(\d+)/', $_l, $_m)) { $_arcKb = (int)($_m[1] / 1024); break; }
}
if (!$_swapTotal) {
    $_swapTotal = $_memRaw['SwapTotal'] ?? 0;
    $_swapUsed  = max(0, ($_memRaw['SwapTotal'] ?? 0) - ($_memRaw['SwapFree'] ?? 0));
}

echo json_encode([
    'cpu' => vv_cpu_per_core(),
    'mem' => [
        'total_kb'      => $_totalKb,
        'free_kb'       => $_freeKb,
        'used_kb'       => max(0, $_totalKb - $_freeKb),
        'arc_kb'        => $_arcKb,
        'docker_kb'     => $_dockerKb,
        'vm_kb'         => $_vmKb,
        'system_kb'     => max(0, $_totalKb - $_freeKb - $_arcKb - $_dockerKb - $_vmKb),
        'swap_total_kb' => $_swapTotal,
        'swap_used_kb'  => $_swapUsed,
        'top_procs'     => $_topProcs,
    ],
    'net' => vv_network_stats(),
    'ts'  => time(),
]);
