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.
83 lines
4.1 KiB
PHP
83 lines
4.1 KiB
PHP
<?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';
|
|
|
|
$logDir = LOG_DIR;
|
|
$runs = [];
|
|
|
|
try {
|
|
$ri = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($logDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($ri as $file) {
|
|
if ($file->getExtension() !== 'json') continue;
|
|
$d = @json_decode(@file_get_contents($file->getPathname()), true);
|
|
if (!is_array($d) || empty($d['start']) || empty($d['status'])) continue;
|
|
if ($d['status'] === 'running') continue;
|
|
$id = (string)($d['id'] ?? '');
|
|
$runs[] = [
|
|
'id' => $id,
|
|
'label' => basename(str_replace('.sh', '', $id)),
|
|
'status' => $d['status'],
|
|
'start' => (int)$d['start'],
|
|
'dur' => isset($d['end']) ? max(0, (int)$d['end'] - (int)$d['start']) : 0,
|
|
];
|
|
}
|
|
} catch (Exception $e) {}
|
|
|
|
usort($runs, fn($a, $b) => $b['start'] - $a['start']);
|
|
echo json_encode(['ok' => true, 'runs' => array_slice($runs, 0, 24)]);
|