.md and Manual-.md for the script's own folder. That mirrors // how the docs are actually organised: general behaviour at the top, specifics per folder. // // DESIGN PRINCIPLES // Matches headings by slug, with a first-word fallback. // The script name is normalised — underscores and hyphens to spaces, lowercased — and a // heading matches if it contains the whole slug or, failing that, the first word when // that word is longer than three characters. The length floor is what stops a script // beginning with "arr" or "sync" from matching every section in the file. // // Intro sections are never matched. // The matcher returns false for the intro, because the opening prose of a README // mentions many scripts and would otherwise match nearly all of them. // // Every section carries its source label. // The panel shows where each block came from, so a reader can tell the module manual // from the top-level README rather than seeing one undifferentiated wall of text. // // Works for documents as well as scripts. // A non-.sh id yields no header and only the doc sections, so the same endpoint serves // the docs tab's own entries. // // OPERATIONAL SAFEGUARDS // Read-only. Nothing here writes, executes, or schedules anything. // // Traversal is blocked before any path is composed. // An explicit '..' check plus ^[A-Za-z0-9_./\-]+$. The slash must be permitted because // ids are Category/name.sh, so the '..' test carries the traversal guarantee on its own. // // The filesystem is only touched for ids that name a script. // The header read is guarded by both the .sh suffix test and file_exists(), so a // well-formed id for a file that is not there returns an empty header rather than a // warning that would leak the absolute path into the JSON body. // // Every document is existence-checked before it is searched, and the module-level path is // additionally guarded against a dirname of '.' — an id with no directory component would // otherwise compose README-..md and search a file that cannot exist. // // Missing documentation is a normal outcome. // No header and no matching sections yields ok:true with empty values. A script nobody // has written about yet is not an error, and reporting it as one would put a failure in // the panel for most custom scripts. // // REQUEST // GET ?id= or any documented id // // RESPONSE // {"ok":true,"name":"…","header":"…","sections":[{"source":"…","body":"…"}, …]} // {"ok":false,"error":"Invalid id"} // // DEPENDS ON // include/scheduler.php vv_script_header_clean() // include/docs.php vv_readme_section() (loaded transitively) // SCRIPTS_DIR README.md, Manual.md, README-.md, Manual-.md // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/scheduler.php'; $id = trim($_GET['id'] ?? ''); if (!$id || str_contains($id, '..') || !preg_match('/^[a-zA-Z0-9_.\/\-]+$/', $id)) { echo json_encode(['ok' => false, 'error' => 'Invalid id']); exit; } $isScript = str_ends_with($id, '.sh'); $name = basename($id, $isScript ? '.sh' : ''); $dirName = basename(dirname($id)); // e.g. "Media", "Rsync", "Orchestrators" $slug = strtolower(str_replace(['_', '-'], ' ', $name)); $parts = explode(' ', $slug); $first = $parts[0] ?? ''; // e.g. "radarr" from "radarr cleanup" // Script header (bash scripts only) $path = SCRIPTS_DIR . '/' . $id; $header = ($isScript && file_exists($path)) ? vv_script_header_clean($path) : ''; // Section matcher: heading contains the full slug OR first meaningful word (>3 chars) $matcher = function(string $heading, bool $isIntro) use ($slug, $first): bool { if ($isIntro) return false; $h = strtolower(str_replace(['_', '-'], ' ', $heading)); return str_contains($h, $slug) || (strlen($first) > 3 && str_contains($h, $first)); }; // Search main README/Manual + module-level files for this script's directory $scriptsDir = SCRIPTS_DIR; $searchFiles = []; foreach (['README', 'Manual'] as $docType) { $main = "$scriptsDir/$docType.md"; if (file_exists($main)) $searchFiles[] = [$docType, $main]; $mod = "$scriptsDir/$docType-$dirName.md"; if ($dirName && $dirName !== '.' && file_exists($mod) && $mod !== $main) { $searchFiles[] = ["$docType — $dirName", $mod]; } } $sections = []; foreach ($searchFiles as [$label, $file]) { $body = vv_readme_section($file, $matcher); if ($body) $sections[] = ['source' => $label, 'body' => $body]; } // Any AI enhancement that reads this script's output, and whether it is switched on. Kept out of // $sections because it is not documentation: the sections above describe what the script does, and // this describes something else that watches it. Merging them would put a claim in the doc panel // that the script's own header cannot support. // // require_once here rather than at the top: ai_repair.php pulls in the findings layer, and a // scheduler info request on a host with AI off has no reason to load it. require_once dirname(__DIR__) . '/include/ai_repair.php'; $enhancement = function_exists('vv_ai_script_enhancements') ? vv_ai_script_enhancements($id) : []; echo json_encode([ 'ok' => true, 'name' => $name, 'header' => $header, 'sections' => $sections, 'enhancement' => $enhancement ?: null, ]);