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.
This commit is contained in:
Gmer4Lfe
2026-08-02 10:11:39 -04:00
parent 6a959fb5e4
commit 987313e7dc
55 changed files with 3972 additions and 95 deletions
+53
View File
@@ -1,4 +1,57 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Recent run history. The last 24 finished job runs across every orchestrator and script,
// newest first, with status and duration — the activity feed on the scheduler page.
//
// OPERATIONAL MODEL
// There is no run database. Each job writes a .json stat file beside its log, and this
// endpoint reconstructs history by walking LOG_DIR recursively and sorting what it finds by
// start time. That keeps the stat file the single source of truth for a job's outcome —
// the same file status.php reads for live state — instead of maintaining a second record
// that could disagree with it.
//
// DESIGN PRINCIPLES
// Finished runs only.
// status === 'running' is skipped, because a run in progress has no duration and
// belongs to status.php's live indicators, not to history. The two endpoints partition
// the same files rather than overlapping.
//
// Capped at 24 after sorting, not before.
// Every stat file is read and ranked before the slice, so the newest 24 are genuinely
// the newest — filesystem iteration order says nothing about run time.
//
// Duration is derived, never stored.
// end - start is computed here, so a stat file that recorded a start but never got to
// write an end still yields a usable row rather than being discarded.
//
// OPERATIONAL SAFEGUARDS
// Read-only. Nothing here deletes a stat file, truncates a log, or re-runs a job.
//
// The whole walk is wrapped in a try/catch.
// RecursiveDirectoryIterator throws when LOG_DIR does not exist or a subdirectory is
// unreadable — on a fresh install that is the normal state, not an error. The catch
// yields an empty run list so the page renders "no runs yet" instead of a 500.
//
// Every individual file read is independently suppressed and validated.
// @file_get_contents, @json_decode, then an is_array plus required-key check. A stat
// file caught mid-write, truncated, or left over from an older schema is skipped —
// one bad file cannot cost the other 23 rows.
//
// Negative durations are clamped.
// max(0, end - start) — a stat file whose clock went backwards across an NTP step
// reports 0s rather than a negative duration the UI would have to special-case.
//
// REQUEST
// GET, no parameters
//
// RESPONSE
// {"ok":true,"runs":[{"id","label","status","start","dur"}, …]} newest first, max 24
//
// DEPENDS ON
// include/config.php LOG_DIR
// LOG_DIR/**/*.json stat files written by run_job.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';