Declared in PHP rather than in each bash header, against scriptinfo's usual rule that the header next to the code is authoritative: it is authoritative about what the script does, and an enhancement is something else reading its output, gated by a flag the script has never heard of. A description of PHP inside a file that cannot enforce it would drift the first time either changed. Shown with its switch, so "there is an enhancement" and "it is running" are never the same claim. Discovery is listed even though nothing acts on its output — it takes the first accessible root folder with no regard for content, and the classification scan is what notices that night. That relationship explains where misfiled series come from and was written down nowhere.
133 lines
6.6 KiB
PHP
133 lines
6.6 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Contextual help for one script. Returns its header block plus any documentation sections
|
|
// about it found in the README and Manual files — the info panel on the scheduler page.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Documentation is assembled at request time, not indexed. Two sources are combined: the
|
|
// script's own header comment, which is authoritative because it lives next to the code,
|
|
// and matching sections from the markdown docs, which give the surrounding context the
|
|
// header deliberately leaves out.
|
|
//
|
|
// Four documents are searched per request — the top-level README.md and Manual.md, plus the
|
|
// module-level README-<Dir>.md and Manual-<Dir>.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=<Category/name.sh> 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-<Dir>.md, Manual-<Dir>.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,
|
|
]);
|