diff --git a/Plugin/unraid/include/arrs.php b/Plugin/unraid/include/arrs.php index 1da7eb1..e8bddcf 100644 --- a/Plugin/unraid/include/arrs.php +++ b/Plugin/unraid/include/arrs.php @@ -54,6 +54,7 @@ // ═══════════════════════════════════════════════════════════════════════════════════════════════ require_once __DIR__ . '/config.php'; +require_once __DIR__ . '/media.php'; // vv_media_jobs() — the media-side jobs shown on this page // ── Conf helpers ────────────────────────────────────────────────────────────── @@ -450,6 +451,10 @@ function vv_arrs_all(): array { 'nodes' => $result, 'sync' => vv_arr_sync_stats(), 'recovery' => vv_arr_recovery_stats(), + // The three media jobs that operate on the same files the arrs manage and had no tab of + // their own. Cheap — three run records and three log tails — and it rides this payload + // rather than a fourth endpoint because it is shown on this page and cached with it. + 'media_jobs' => function_exists('vv_media_jobs') ? vv_media_jobs() : [], 'host' => $currentHost, 'settings' => [ 'arr_sync_enabled' => ($vars['ARR_SYNC_ENABLED'] ?? 'true') !== 'false', diff --git a/Plugin/unraid/include/media.php b/Plugin/unraid/include/media.php index 3fa4fd4..a09426f 100644 --- a/Plugin/unraid/include/media.php +++ b/Plugin/unraid/include/media.php @@ -257,3 +257,66 @@ function vv_media_sessions(): array { 'server_count' => count($servers), ]; } + +// ── The media jobs that had nowhere to be seen ──────────────────────────────────────────────── +// Three scripts that operate on the media files every day and appeared in no tab: play state sync +// every thirty minutes, permissions and the cleaner nightly. The only way to know whether any of +// them had run was to open the Scheduler and read an orchestrator's log. +// +// Readable now because run_orch_child() writes a run record and a per-script log for its children. +// Before that this function could not have been written: the record did not exist, and the output +// was interleaved into a parent log with forty other scripts behind headings that name no script. +const VV_MEDIA_JOBS = [ + 'Media/play_state_sync' => 'Play state sync', + 'Media/media_shares_permissions' => 'Media permissions', + 'Media/media_cleaner' => 'Media cleaner', +]; + +// The last line the script chose to end on. All three close with a SUMMARY block whose final 🏁 +// line is the sentence a human would read out — "done — 21 shares updated", "clean — nothing to +// remove" — so that line is taken verbatim rather than re-derived from counts this would have to +// parse differently per script. +// +// Decorations are stripped, not matched: the three write 🏁 with varying spacing and only some +// prefix "Status:" or "[OK]". Matching those shapes would break the first time one was reworded, +// and the words after them are the part with the meaning. +function vv_media_job_summary(string $id): string { + $path = LOG_DIR . '/' . $id . '.log'; + $raw = @file_get_contents($path); + if ($raw === false) return ''; + + $lines = array_slice(explode("\n", rtrim($raw)), -60); + $out = ''; + foreach ($lines as $line) { + if (!str_contains($line, '🏁')) continue; + $t = trim(str_replace('🏁', '', $line)); + $t = preg_replace('/^\[(OK|WARN|INFO|ERROR)\]\s*/u', '', trim($t)); + $t = preg_replace('/^Status:\s*/u', '', $t); + $t = trim($t); + // "Done ✅" carries nothing the status field does not already say; keep looking for a line + // that does. If nothing better appears, the earlier one already captured is kept. + if ($t === '' || preg_match('/^done\s*✅?$/iu', $t)) continue; + $out = $t; + } + return mb_substr($out, 0, 160); +} + +function vv_media_jobs(): array { + $out = []; + foreach (VV_MEDIA_JOBS as $id => $label) { + $rec = @json_decode((string)@file_get_contents(LOG_DIR . '/' . $id . '.json'), true); + $rec = is_array($rec) ? $rec : []; + $start = isset($rec['start']) ? (int)$rec['start'] : 0; + $end = isset($rec['end']) ? (int)$rec['end'] : 0; + $out[] = [ + 'id' => $id . '.sh', + 'label' => $label, + 'last_run' => $start ?: null, + 'status' => $rec['status'] ?? null, + 'exit' => isset($rec['exit']) ? (int)$rec['exit'] : null, + 'duration' => ($start && $end) ? $end - $start : null, + 'summary' => vv_media_job_summary($id), + ]; + } + return $out; +} diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index ff63c3a..aeb8695 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -374,6 +374,40 @@ function _syncSection(sync, recovery, settings) { `; } +// ── Media jobs ──────────────────────────────────────────────────────────────── +// Play state sync, permissions and the cleaner. They work on the same files the arrs manage and +// appeared in no tab at all — the only way to know whether one had run was to open the Scheduler +// and read an orchestrator's log. Readable at all only because run_orch_child() now writes a run +// record and a per-script log for its children. +// +// Each shows the sentence the script itself ended on rather than a count re-derived here. The +// three word their outcome differently — "done — 21 shares updated", "clean — nothing to remove" — +// and the wording is the part worth reading. +function _mediaJobsSection(jobs) { + if (!jobs || !jobs.length) return ''; + const cards = jobs.map(j => { + const st = j.status || null; + const col = !st ? '#444' : st === 'ok' ? '#4caf50' : st === 'running' ? '#4a9eff' + : st === 'warn' ? '#ffb74d' : '#ef5350'; + const pill = !st ? 'never run' : st; + const dur = j.duration != null ? _dur(j.duration) : ''; + return `