Single-source the scheduler help from markdown the AI index can read

This commit is contained in:
Gmer4Lfe
2026-08-04 19:30:38 -04:00
parent e1bbecad38
commit 3ea80ef944
7 changed files with 363 additions and 46 deletions
+30
View File
@@ -503,6 +503,36 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
.vv-info-cols li strong { color: #ccc; }
.vv-info-cols code { background: #1a1a1a; padding: 0 4px; border-radius: 2px;
font-size: 11px; color: #9ab; border: 1px solid #333; }
/* ── Rendered page docs (include/docs.php → pages/readme/*.md) ─────────────────────────────── */
/* Two columns so the reference tables and the task sections sit side by side rather than making
the panel a long scroll. Headings span both, which is what keeps a task and its steps together
instead of splitting across the gap. */
.vv-doc { columns: 320px 2; column-gap: 26px; font-size: 12px; color: #aaa; line-height: 1.55; }
.vv-doc > h1 { display: none; } /* the panel already has its own title */
.vv-doc h2 { column-span: all; margin: 12px -4px 7px; padding: 4px 8px; background: #161a1d;
border-left: 2px solid #1e6fa5; font-size: 10px; font-weight: bold;
text-transform: uppercase; letter-spacing: .08em; color: #4a8ab5; }
.vv-doc h2:first-child { margin-top: 2px; }
.vv-doc p { margin: 0 0 7px; break-inside: avoid; }
.vv-doc ul, .vv-doc ol { margin: 0 0 8px; padding-left: 16px; }
.vv-doc li { margin-bottom: 4px; break-inside: avoid; }
.vv-doc strong { color: #ccc; }
.vv-doc em { color: #9a9a9a; font-style: italic; }
.vv-doc hr { display: none; } /* h2 bars already separate the sections */
.vv-doc code { background: #1a1a1a; padding: 0 4px; border-radius: 2px; font-size: 11px;
color: #9ab; border: 1px solid #333; }
.vv-doc blockquote { margin: 0 0 8px; padding: 6px 9px; background: #16130d;
border-left: 2px solid #7a5a2a; color: #b39a72; break-inside: avoid; }
.vv-doc .vv-live-var { color: #8fc98f; border-color: #2e4a2e; }
.vv-doc .vv-unknown-var { color: #e57; border-color: #5a2a2a; }
.vv-doc-table { width: 100%; border-collapse: collapse; margin: 0 0 9px; break-inside: avoid; }
.vv-doc-table th { text-align: left; font-size: 9px; text-transform: uppercase; letter-spacing: .07em;
color: #4a4a4a; border-bottom: 1px solid #2a2a2a; padding: 3px 6px 3px 0; }
.vv-doc-table td { font-size: 11px; color: #999; border-bottom: 1px solid #1a1a1a;
padding: 4px 6px 4px 0; vertical-align: top; }
.vv-doc-code { background: #0d0d0d; border: 1px solid #222; border-radius: 3px; padding: 7px 9px;
font-size: 11px; color: #9ab; overflow-x: auto; break-inside: avoid; }
.vv-info-divider { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: #444;
padding: 10px 12px 4px; border-top: 1px solid #2a2a2a; margin-top: 2px; }
+136 -12
View File
@@ -83,17 +83,26 @@ function vv_docs_render(string $rel, array $vars): string {
if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'md') return '<p>File not found.</p>';
if (!is_file($path)) return '<p>File not found.</p>';
$md = file_get_contents($path);
return vv_docs_markdown(file_get_contents($path), $vars);
}
// Substitute `$VAR_NAME` markers with live conf values
$md = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function($m) use ($vars) {
$key = $m[1];
return isset($vars[$key])
? '<code class="vv-live-var">' . htmlspecialchars($vars[$key]) . '</code>'
: '<code class="vv-unknown-var">$' . htmlspecialchars($key) . '</code>';
}, $md);
// Render markdown
// Markdown → HTML for the subset these docs actually use: headings, paragraphs, bullet and
// ordered lists, tables, blockquotes, fenced code, horizontal rules, and inline bold / italic /
// code / links.
//
// Hand-rolled rather than vendored. Parsedown was the original plan and PARSEDOWN_PATH is still
// honoured below if the file ever appears, but it has never been present on this system, so the
// only path this function ever took was a <pre> dump of raw markdown — which is not a document,
// it is the source of one. A renderer for a subset we control is a few dozen lines and adds
// nothing to a repo that gets pushed.
//
// Escape first, then format. Every line is passed through htmlspecialchars() before any tag is
// introduced, so the only HTML in the output is HTML this function put there. That is what makes
// safe mode unnecessary rather than merely configured — and it is why the $VAR substitution runs
// here, after escaping. Injecting <code> into the markdown before rendering, as this file used to
// do, meant both Parsedown's safe mode and the <pre> fallback escaped the tags and printed them
// as literal text. The feature never worked; nothing called it, so nothing reported it.
function vv_docs_markdown(string $md, array $vars): string {
if (file_exists(PARSEDOWN_PATH)) {
require_once PARSEDOWN_PATH;
$pd = new Parsedown();
@@ -101,6 +110,121 @@ function vv_docs_render(string $rel, array $vars): string {
return $pd->text($md);
}
// Fallback: plain preformatted text
return '<pre>' . htmlspecialchars($md) . '</pre>';
$inline = function (string $s) use ($vars): string {
$s = htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
// `$VAR` → the live conf value. Unresolved names render in their own class rather than
// silently reading as prose, so a stale reference in a doc is visible as a defect.
$s = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function ($m) use ($vars) {
return isset($vars[$m[1]])
? '<code class="vv-live-var">' . htmlspecialchars($vars[$m[1]]) . '</code>'
: '<code class="vv-unknown-var">$' . $m[1] . '</code>';
}, $s);
$s = preg_replace('/`([^`]+)`/', '<code>$1</code>', $s);
$s = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $s);
$s = preg_replace('/(?<![\w*])\*([^*]+)\*(?![\w*])/', '<em>$1</em>', $s);
// Links are restricted to http/https and relative paths — a doc is trusted, but these
// files sync between hosts, so javascript: must not be reachable through one.
$s = preg_replace('/\[([^\]]+)\]\((https?:\/\/[^\s)]+|[^\s):]+)\)/', '<a href="$2">$1</a>', $s);
return $s;
};
$out = '';
$list = null; // 'ul' | 'ol' | null
$inTable = false;
$inCode = false;
$para = [];
$quote = [];
$flushPara = function () use (&$para, &$out, $inline) {
if (!$para) return;
$out .= '<p>' . $inline(implode(' ', $para)) . "</p>\n";
$para = [];
};
$flushQuote = function () use (&$quote, &$out, $inline) {
if (!$quote) return;
$out .= '<blockquote>' . $inline(implode(' ', $quote)) . "</blockquote>\n";
$quote = [];
};
$closeList = function () use (&$list, &$out) {
if ($list) { $out .= "</$list>\n"; $list = null; }
};
$closeTable = function () use (&$inTable, &$out) {
if ($inTable) { $out .= "</tbody></table>\n"; $inTable = false; }
};
foreach (explode("\n", str_replace("\r\n", "\n", $md)) as $line) {
if (preg_match('/^```/', $line)) {
$flushPara(); $closeList(); $closeTable();
$out .= $inCode ? "</code></pre>\n" : '<pre class="vv-doc-code"><code>';
$inCode = !$inCode;
continue;
}
if ($inCode) { $out .= htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . "\n"; continue; }
$t = trim($line);
// One guard rather than a flush in every branch: the quote ends the moment a line is
// not a quote line, whatever that next line turns out to be.
if ($quote && !str_starts_with($t, '>')) $flushQuote();
if ($t === '') { $flushPara(); $closeList(); $closeTable(); continue; }
if (preg_match('/^(---+|\*\*\*+)$/', $t)) { $flushPara(); $closeList(); $closeTable(); $out .= "<hr>\n"; continue; }
if (preg_match('/^(#{1,6})\s+(.*)$/', $t, $m)) {
$flushPara(); $closeList(); $closeTable();
$n = strlen($m[1]);
$out .= "<h$n>" . $inline($m[2]) . "</h$n>\n";
continue;
}
// Consecutive > lines are one quote, not one per line — same wrapping convention as
// paragraphs, which is how they are written.
if (preg_match('/^>\s?(.*)$/', $t, $m)) {
$closeList(); $closeTable();
if (!$quote) $flushPara();
$quote[] = $m[1];
continue;
}
// Tables: a header row, a separator of dashes, then body rows. The separator is what
// identifies the block — a lone pipe in prose is not a table.
if (strpos($t, '|') !== false && preg_match('/^\|?[\s:-]*-[\s|:-]*\|/', $t)) {
continue; // separator consumed by the header below
}
if (strpos($t, '|') !== false && substr_count($t, '|') >= 2) {
$cells = array_map('trim', explode('|', trim($t, '| ')));
if (!$inTable) {
$flushPara(); $closeList();
$out .= '<table class="vv-doc-table"><thead><tr>';
foreach ($cells as $c) $out .= '<th>' . $inline($c) . '</th>';
$out .= "</tr></thead><tbody>\n";
$inTable = true;
} else {
$out .= '<tr>';
foreach ($cells as $c) $out .= '<td>' . $inline($c) . '</td>';
$out .= "</tr>\n";
}
continue;
}
$closeTable();
if (preg_match('/^[-*]\s+(.*)$/', $t, $m)) {
$flushPara();
if ($list !== 'ul') { $closeList(); $out .= "<ul>\n"; $list = 'ul'; }
$out .= '<li>' . $inline($m[1]) . "</li>\n";
continue;
}
if (preg_match('/^\d+\.\s+(.*)$/', $t, $m)) {
$flushPara();
if ($list !== 'ol') { $closeList(); $out .= "<ol>\n"; $list = 'ol'; }
$out .= '<li>' . $inline($m[1]) . "</li>\n";
continue;
}
$para[] = $t;
}
$flushQuote(); $flushPara(); $closeList(); $closeTable();
if ($inCode) $out .= "</code></pre>\n";
return $out;
}
+1
View File
@@ -307,6 +307,7 @@ if (is_dir('/var/log/varaverk')) {
<div>
<select id="vv-ai-kind" title="Restrict retrieval to one kind of source">
<option value="">All sources</option>
<option value="ui">WebUI — how the pages work</option>
<option value="readme">README — what things are</option>
<option value="manual">Manual — how to do things</option>
<option value="header">Script headers</option>
@@ -0,0 +1,171 @@
# Scheduler
Everything that runs on a timer is decided here. The page is the front end for two files:
`schedule.json` holds when things fire, and `master.conf` holds which steps an orchestrator
actually calls. Every control below writes to one of those two — nothing on this page needs a
conf file opened by hand.
**An orchestrator is a job that calls other scripts in order.** The orchestrator has the cron
entry; its steps do not. That one fact explains most of the behaviour below.
---
## Stopping one step without disabling the whole job
The common case: a nightly job is fine, but one script inside it should stop running.
1. Find the orchestrator's card — for example **Daily Sync Maintenance**.
2. Click **Steps ▸** on the right of its row. The list of scripts it calls expands.
3. Toggle off the one you want stopped.
The orchestrator keeps running on its schedule and simply skips that step. Under the hood the
toggle comments the script out of that orchestrator's array in `master.conf`, which is the same
edit you would make by hand — the toggle is just the safe way to make it.
Turn it back on with the same toggle. The commented line is left in place, so the step keeps its
position in the sequence rather than being appended to the end.
> The button is called **Steps**, not Advanced. There is a separate **Advanced** button at the
> top right of the panel that does something quite different — see *Advanced mode* below.
## Turning an orchestrator off entirely
Toggle the orchestrator itself off. Its cron entry stops firing and **every step inside it is
suppressed** — an enabled step under a disabled orchestrator does not run, because the
orchestrator is what calls it.
If you still want one of those scripts running while its orchestrator is off, give that step its
own cron expression. With the orchestrator off, a step's cron field becomes live and it fires
standalone.
## Running something right now
**Run** fires the script immediately and resets its cron timer, so the next scheduled fire is a
full interval later rather than a few minutes away.
**Dry Run** passes `--dry-run`. Nothing is written to disk, and the full output is shown rather
than a summary — with the cleanup scripts, reading the list of what *would* be removed is the
entire point. Anything destructive offers both, and Run asks for confirmation first.
## Changing when something runs
Type into the cron field. It saves when the field loses focus, shows the schedule back to you in
plain English, and turns red if the expression is invalid.
Two shortcuts worth knowing:
- **Suggested cron** — in the Orch tree on the right, each script shows a suggested cron badge.
Click it to apply and save that schedule instantly.
- **Cron Calculator** — accepts an expression or plain English, shows a description plus the next
five fire times, and **Apply** pushes it into the cron field you last used.
For array events, put `array_start` or `array_stop` in the cron field instead of an expression.
A badge appears and the script fires on Unraid's array start or stop. This works for any
orchestrator or custom script.
## Finding out why something failed
**Log** opens that script's log in the right-hand panel. It auto-scrolls to the newest line,
pauses when you scroll up, and resumes when you return to the bottom. **Invert** puts newest
lines at the top. The **Search** box in the toolbar filters and highlights matches while dimming
everything else.
Two blocks on the Scheduler Info panel get you there faster:
- **Recent Activity** — the last 24 runs. Click any row to open that log. Expands itself when
there are errors.
- **Recent Errors** — the most recent error per script over 7 days, so a failure that happened
once at 3am is still visible days later.
The status dot in the Orch tree is green, orange or red for the last run's outcome, dim if the
script has never run. Hover for detail.
## Stopping a script that is stuck
**Stop** sends SIGTERM, waits 3 seconds, then SIGKILL, and clears any lock files the script left
behind.
Locks are never cleared automatically anywhere in Varaverk, and that is deliberate — a lock that
looks stale may belong to a job that is still working. Stop is the explicit way to say you have
decided otherwise.
## Moving a script between orchestrators
Use **Arrange** to drag scripts between orchestrators. Dropping one onto the right-hand panel
removes it from its orchestrator. Nothing is written until you click **Save Arrangement**, which
commits the new order to `master.conf`.
Order matters — an orchestrator runs its steps in the sequence shown.
## Adding your own script
Custom scripts live in `$CUSTOM_SCRIPTS_DIR`, outside the git repo. Any `*.sh` placed there is
detected and listed automatically; it does not have to be registered anywhere. Being outside the
repo means a `git pull` never touches your scripts and they are never committed by accident.
**+ Folder** creates a collapsible subfolder in Custom Scripts that you can drag scripts into.
While editing, **Cancel** discards your changes and returns to the Scheduler Info panel. The file
on disk is never touched until you press Save.
---
## Reference — the controls on a job row
| Control | What it does |
|---|---|
| **Toggle** | Enable/disable. Saves immediately to `schedule.json` |
| **Cron field** | Schedule. Auto-saves on focus-out; plain-English hint; red border means invalid |
| **⚙ Cog** | Settings panel — the script's own header, matching README/Manual sections, and its config fields |
| **Script name** | Jumps to that script's entry in the Orch tree on the right |
| **Run** | Execute now; resets the cron timer |
| **Dry Run** | Execute with `--dry-run`; writes nothing |
| **Log** | Open this script's log in the right panel |
| **Verbose** | Appends `--log` for per-item detail in the output |
| **Steps ▸** | Expand the scripts this orchestrator calls |
## Reference — orchestrator and step behaviour
| State | Behaviour |
|---|---|
| Orchestrator **ON** | Sole trigger. Cron fires the orchestrator, which calls its steps in sequence |
| Orchestrator **OFF** | Never runs automatically. Steps are suppressed; each can take its own cron |
| Step, orchestrator ON | Toggle comments/uncomments the script in `master.conf` |
| Step, orchestrator ON, no conf array | The orchestrator hardcodes the call — the toggle is display-only |
| Step, orchestrator OFF | Enter a cron to run it standalone |
| Rsync badge | Toggle writes `TIER_RSYNC_ENABLED` straight to `master.conf` |
| Rsync, orchestrator OFF | Needs both a location and a standalone cron before it will fire independently |
## Reference — the Scheduler Info panel
The right-hand panel is the default view and everything returns to it. **← Scheduler Info** comes
back here from any log, editor, conf form or script view.
- **Notification board** — how many jobs are scheduled out of the total, and what is running now.
- **Next Runs** — what fires next, and when.
- **Recent Activity** / **Recent Errors** — described above.
- **Cron Calculator** — described above.
## Reference — Advanced mode
The **Advanced** button at the top right turns blue when active. It is a display and editing
mode, unrelated to the **Steps ▸** expander on each orchestrator:
- The ⚙ cog switches to an enriched view — full script source, documentation and config together.
- Raw conf editing unlocks, including direct buttons for each conf file.
- **Git Pull** appears on the notification board, which pulls the latest scripts and runs a conf
upgrade.
Raw conf editing writes the file directly. The toggles elsewhere on this page are surgical — they
preserve the comment blocks that document every threshold — so prefer them where one exists.
## Reference — what this page writes
| File | Written by |
|---|---|
| `schedule.json` | Every toggle and cron field |
| `master.conf` | Step toggles, Save Arrangement, rsync tier flags, raw conf editing |
| `CUSTOM_SCRIPTS_DIR` | The custom script editor |
Cron changes take effect on the next cron rebuild. Nothing here edits the running crontab
directly — `varaverk.cron` is regenerated from `schedule.json`.
+16 -31
View File
@@ -59,6 +59,15 @@
// api/savefolders.php folder grouping api/reorderarray.php
// api/rsync_standalone.php
require_once dirname(__DIR__) . '/include/scheduler.php';
require_once dirname(__DIR__) . '/include/docs.php';
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
// path constants — those are not conf keys, but they are exactly what a reader needs resolved
// rather than described.
$_vv_doc_vars = array_merge(vv_conf_vars(), [
'CUSTOM_SCRIPTS_DIR' => CUSTOM_SCRIPTS_DIR,
'SCRIPTS_DIR' => SCRIPTS_DIR,
]);
// Setup mode — auto-open a conf file and force the editing sequence
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
@@ -495,38 +504,14 @@ $runningScripts = array_unique($runningScripts);
<span class="vv-sug-chevron">▾</span>
<span class="vv-sug-title">How do I use this</span>
</div>
<!-- Rendered from pages/readme/scheduler-readme.md, not maintained here. That file is
also what the AI tab retrieves, so the panel you read and the answer the
assistant gives are the same text and cannot drift — the same argument that
makes this page parse script PURPOSE blocks instead of restating them. -->
<div class="vv-sug-body vv-info-body">
<ul class="vv-info-cols">
<li><strong>Toggle</strong> — saves immediately to schedule.json</li>
<li><strong>Cron field</strong> — auto-saved on focus-out; hint shows plain English; red border = invalid</li>
<li><strong>⚙ Cog</strong> — opens settings panel: script header + matching README/Manual sections + config fields</li>
<li><strong>Script name</strong> — click navigates to the matching entry in the Orch tree (right panel)</li>
<li><strong>Run</strong> — fires script immediately; resets the cron timer so next fire is one full interval later</li>
<li><strong>Dry Run</strong> — same as Run but passes <code>--dry-run</code>; no changes written to disk</li>
<li><strong>Log</strong> — opens script log here; auto-scrolls; use Search box to filter/highlight lines</li>
<li><strong>Stop</strong> — SIGTERM → 3 s → SIGKILL; clears stuck lock files</li>
<li><strong>Verbose</strong> — appends <code>--log</code> flag for per-item detail output</li>
<li><strong>Cancel</strong> — discards changes and returns here; file is never touched until Save</li>
<li><strong>⚡ Array events</strong> — set any cron field to <code>array_start</code> or <code>array_stop</code>; badge appears; fires on Unraid array start/stop; works on any orch or custom script</li>
<li><strong>Arrange</strong> — drag scripts between orchs; drop to right panel removes from orch; Save Arrangement commits to master.conf</li>
<li><strong>+ Folder</strong> — collapsible subfolder in Custom Scripts; drag scripts in</li>
<li><strong>Suggested cron</strong> — click the cron code badge in the Orch tree to apply and save instantly</li>
<li><strong>Status dot (tree)</strong> — green/orange/red ● = last run ok/warn/error; dim = never run; hover for detail</li>
<li><strong>Cron Calculator</strong> — type an expression or plain English; shows description + next 5 fires; Apply pushes to last-focused cron field</li>
<li><strong>Recent Activity</strong> — last 24 runs; click a row to open that log; auto-expands on errors</li>
<li><strong>Recent Errors</strong> — last error per script (7 days); click to open log; auto-expands on new errors</li>
<li><strong>Advanced</strong> — top-right button, turns blue; ⚙ shows enriched view (header + docs + config); raw conf editing unlocked</li>
<li><strong>Log search</strong> — filter box in toolbar when log is open; highlights matches, dims others</li>
<li><strong>← Scheduler Info</strong> — returns here from any log, editor, conf, or script view</li>
<li class="vv-info-sep">Orchestrator &amp; Child Behaviour</li>
<li><strong>Orch ON</strong> — sole trigger; cron fires the orch; it calls children in sequence</li>
<li><strong>Orch OFF</strong> — never runs automatically; children suppressed; each can get its own standalone cron</li>
<li><strong>Child (orch ON)</strong> — toggle comments/uncomments the script in master.conf</li>
<li><strong>Child (orch ON, no array)</strong> — orch hardcodes the call; toggle is display-only</li>
<li><strong>Child (orch OFF)</strong> — enter a cron to run it standalone</li>
<li><strong>Rsync badge</strong> — toggle writes TIER_RSYNC_ENABLED directly to master.conf</li>
<li><strong>Rsync (orch OFF)</strong> — fill location + standalone cron + Save; both required for independent firing</li>
</ul>
<div class="vv-doc">
<?= vv_docs_render('Plugin/unraid/pages/readme/scheduler-readme.md', $_vv_doc_vars) ?>
</div>
</div>
</div>