<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Job log endpoint. Returns the tail of one job's log for the scheduler page's live output
//   view, and — on POST with clear=1 — truncates it.
//
// OPERATIONAL MODEL
//   Read and clear share one URL because they share one identifier and one validation path.
//   The method decides which: GET always reads, POST reads unless clear is set. Splitting
//   them would duplicate the id validation, which is the only part with teeth.
//
//   The response carries the log's mtime as ts, so the page can tell a log that is still
//   growing from one that has stopped without diffing the content it already has.
//
// DESIGN PRINCIPLES
//   Tail, never the whole file.
//       The last 200 lines. Orchestrator logs are appended to indefinitely and this is polled
//       while a job runs, so returning the file would grow the response without bound exactly
//       when the page is fetching it most often.
//
//   A log that does not exist is a successful empty read.
//       ok:true with empty content and ts:0. A job that has never run has no log, and that is
//       a normal state on a fresh install — not a condition the page should report as an
//       error.
//
//   Clearing truncates, never deletes.
//       file_put_contents with an empty string keeps the inode, so a running job's open file
//       handle keeps writing to the same file. Unlinking it would leave the job appending to
//       a file nothing can read.
//
// OPERATIONAL SAFEGUARDS
//   The job id is validated identically on both paths.
//       ^[a-zA-Z0-9_./\-]+\.sh$ plus an explicit '..' check, applied before the method is
//       branched on, so the clear path cannot be reached with an id the read path would have
//       rejected. The slash must be permitted for Category/name.sh ids, which is why
//       traversal gets its own test rather than being implied by the character class.
//
//   The id is never used as a path directly.
//       vv_job_log_path() maps it into LOG_DIR and rewrites the .sh suffix to .log, so the
//       only files this endpoint can name are job logs — the .sh requirement in the pattern
//       is what makes that rewrite total.
//
//   Clearing requires POST and an explicit flag.
//       A GET cannot truncate a log, and a POST without clear=1 reads like any other request.
//       Destroying output needs to be asked for twice, in two different ways.
//
//   Clearing an absent log is success, not an error.
//       file_exists() is checked first, so clearing a job that has never run reports ok
//       rather than failing on a file the caller did not expect to exist anyway.
//
//   The tail read degrades to empty.
//       file() with a ?: [] fallback, so an unreadable or vanishing log yields an empty view
//       instead of a fatal that would blank the page mid-poll.
//
// REQUEST
//   GET  ?id=<Category/name.sh>              last 200 lines
//   POST id=<…>                              same as GET
//   POST id=<…>  clear=1                     truncate the log
//
// RESPONSE
//   {"ok":true,"content":"…","ts":<mtime>}   read — ts is 0 when the log does not exist
//   {"ok":true}                              clear
//   {"ok":false,"error":"Invalid job id"}
//
// DEPENDS ON
//   include/scheduler.php   vv_job_log_path()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';

$id = trim($_SERVER['REQUEST_METHOD'] === 'POST' ? ($_POST['id'] ?? '') : ($_GET['id'] ?? ''));

if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
    echo json_encode(['ok' => false, 'error' => 'Invalid job id']);
    exit;
}

$logFile = vv_job_log_path($id);

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['clear'])) {
    if (file_exists($logFile)) file_put_contents($logFile, '');
    echo json_encode(['ok' => true]);
    exit;
}

if (!file_exists($logFile)) {
    echo json_encode(['ok' => true, 'content' => '', 'ts' => 0]);
    exit;
}

$lines = array_slice(file($logFile) ?: [], -200);
echo json_encode([
    'ok'      => true,
    'content' => implode('', $lines),
    'ts'      => filemtime($logFile),
]);
