Files
Gmer4Lfe 987313e7dc Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it
didn't obvious — shell arguments reaching a crontab or a bash -c
unescaped, master.conf written without tmp+rename, and conf edits that
could be saved without ever being parsed.
2026-08-02 10:11:39 -04:00

116 lines
5.8 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Header snapshot. The small always-visible summary carried across every tab — CPU, RAM,
// fallback state, partner peers, and active stream and transcode counts.
//
// OPERATIONAL MODEL
// This is the most frequently requested endpoint in the plugin: it is polled from whichever
// tab happens to be open, continuously, for as long as the page is open. Everything about
// it is shaped by that. Each field is the cheapest available answer to its question, not
// the most complete one — monitor.php exists for the complete one.
//
// DESIGN PRINCIPLES
// Shares the CPU baseline rather than sampling its own.
// vv_cpu_per_core() keeps its counters in /tmp/vv_cpu_stat.json, and reading through it
// means the header and the monitor page report the same number. Two independent
// samplers would drift apart and produce a visible disagreement between the header and
// the page directly below it.
//
// Fallback state is read from the file, never derived.
// A direct read of fallback_state.db with no exec and no SSH. The authoritative answer
// is the one fallback.sh wrote; recomputing it here would be both slower and capable of
// disagreeing with the process that actually controls failover.
//
// Media counts are cached 30 seconds, everything else is live.
// Streams are the only field that costs HTTP round trips to another service. Caching
// just that keeps the poll cheap without making CPU or RAM stale, and 30s is well under
// the time it takes anyone to notice a stream started.
//
// Peers exclude self.
// is_me is filtered out here rather than in the UI, so every consumer of this payload
// gets the same definition of "peers" and none of them can forget to apply it.
//
// OPERATIONAL SAFEGUARDS
// Read-only. Nothing here starts, stops, or changes anything.
//
// The fallback read degrades to a named unknown.
// @file_get_contents with a ?: '' fallback, and the parsed state defaults to 'UNKNOWN'.
// A missing or unreadable state file shows UNKNOWN in the header rather than NORMAL —
// reporting healthy for a fallback process that is not running is the one error this
// field must never make.
//
// Division is guarded.
// ram_total_mb > 0 is checked before the percentage, so a failed meminfo read yields 0
// rather than a division by zero that would fatal on every tab at once.
//
// Every count is cast on the way out.
// (int) on the cached stream and transcode counts with ?? 0 defaults, so a cache file
// written by an older schema cannot put a null or a string into the header payload.
//
// Media failures are already contained upstream — vv_media_sessions() time-boxes each
// request at 3s and returns an empty session list on any failure, so an unreachable Emby
// costs this poll nothing beyond that timeout, once every 30 seconds.
//
// REQUEST
// GET, no parameters
//
// RESPONSE
// {"cpu_pct","ram_pct","ram_used_mb","ram_total_mb","fallback","partner_enabled",
// "peers":[…],"stream_count","transcode_count"}
//
// DEPENDS ON
// include/common.php vv_cpu_per_core(), vv_system_resources()
// (loaded transitively through include/monitor.php)
// include/monitor.php vv_partner_state()
// include/config.php vv_parse_kv_db(), vv_cache_read(), vv_cache_write(), STATE_DIR
// include/media.php vv_media_sessions()
// STATE_DIR fallback_state.db — written by Fallback/fallback.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/media.php';
// CPU% — shares /tmp/vv_cpu_stat.json with vv_cpu_per_core() so both read the same baseline
$cpuPct = vv_cpu_per_core()['overall'] ?? 0;
// RAM%
$res = vv_system_resources();
$ramTotalMb = $res['ram_total_mb'];
$ramUsedMb = $ramTotalMb - $res['ram_free_mb'];
$ramPct = $ramTotalMb > 0 ? (int)round($ramUsedMb / $ramTotalMb * 100) : 0;
// Fallback state (fast file read, no exec)
$fbRaw = @file_get_contents(STATE_DIR . '/fallback_state.db') ?: '';
$fbData = vv_parse_kv_db($fbRaw);
$fallbackState = $fbData['state'] ?? 'UNKNOWN';
// Partner
$partner = vv_partner_state();
$peers = array_values(array_filter($partner['hosts'], fn($h) => !$h['is_me']));
// Media sessions — cached 30s so the HTTP calls don't hold up every snapshot poll
$mediaCache = vv_cache_read('snap_media', 30);
if (!$mediaCache) {
$media = vv_media_sessions();
$mediaCache = [
'stream_count' => count($media['sessions']),
'transcode_count' => count(array_filter($media['sessions'], fn($s) => !empty($s['is_tc']))),
];
vv_cache_write('snap_media', $mediaCache);
}
$streamCount = (int)($mediaCache['stream_count'] ?? 0);
$transcodeCount = (int)($mediaCache['transcode_count'] ?? 0);
echo json_encode([
'cpu_pct' => $cpuPct,
'ram_pct' => $ramPct,
'ram_used_mb' => $ramUsedMb,
'ram_total_mb' => $ramTotalMb,
'fallback' => $fallbackState,
'partner_enabled' => $partner['enabled'],
'peers' => $peers,
'stream_count' => $streamCount,
'transcode_count' => $transcodeCount,
]);