Scheduler: inline cron beside name, dynamic description from script headers

Cron input now sits right of the script label in the same row.
Below each row: one-line description pulled live from the script header
(PURPOSE block or first meaningful comment line) — updates whenever
scripts are edited, no plugin changes needed.
This commit is contained in:
Gmer4Lfe
2026-05-23 18:11:12 -04:00
parent fc01268c12
commit 065b29fcc8
3 changed files with 56 additions and 9 deletions
@@ -37,9 +37,14 @@
.vv-sched-card { margin-bottom: 10px; }
.vv-script { background: #161616; border: 1px solid #333; border-radius: 4px;
padding: 6px 10px; margin: 4px 0; margin-left: 20px; }
.vv-job-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vv-job-label { flex: 1; font-size: 14px; min-width: 120px; }
.vv-cron { width: 140px; background: #111; border: 1px solid #444; color: #ddd;
.vv-job-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vv-name-cron { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.vv-job-label { flex: 1; font-size: 14px; min-width: 80px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vv-job-desc { font-size: 12px; color: #555; margin: 2px 0 6px 44px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
cursor: default; }
.vv-cron { width: 140px; flex-shrink: 0; background: #111; border: 1px solid #444; color: #ddd;
padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 13px; }
.vv-children { padding-top: 8px; border-top: 1px solid #333; margin-top: 8px; }
.vv-advanced-toggle { background: none; border: 1px solid #555; color: #aaa;
@@ -59,6 +59,36 @@ function vv_cron_rebuild(array $schedule): bool {
return file_put_contents(CRON_FILE, implode("\n", $lines)) !== false;
}
// Extract a one-line description from a bash script header.
// Supports two patterns:
// 1. # PURPOSE (or # DESCRIPTION) block — returns first non-separator line after it
// 2. First meaningful comment line after the banner
function vv_script_description(string $path): string {
if (!file_exists($path)) return '';
$lines = array_slice(file($path) ?: [], 0, 50);
$purposeNext = false;
$first = '';
foreach ($lines as $raw) {
$raw = rtrim($raw);
if (str_starts_with($raw, '#!')) continue; // shebang
if (!str_starts_with($raw, '#')) continue; // non-comment
$inner = ltrim(substr($raw, 1)); // strip leading #
if (preg_match('/^[\s=\-─━\*]+$/', $inner) || $inner === '') { // separator / blank
if ($purposeNext) continue;
continue;
}
if (preg_match('/^\s*(PURPOSE|DESCRIPTION|Description)\s*$/i', $inner)) {
$purposeNext = true;
continue;
}
if ($purposeNext) {
return mb_substr(trim($inner), 0, 200);
}
if (!$first) $first = mb_substr(trim($inner), 0, 200);
}
return $first;
}
// Walk the scripts repo and return the job tree:
// orchestrators as top-level, individual scripts as children
function vv_job_tree(): array {
@@ -76,6 +106,7 @@ function vv_job_tree(): array {
$orchs[] = [
'id' => $id,
'label' => basename($path, '.sh'),
'desc' => vv_script_description($path),
'type' => 'orchestrator',
'enabled' => (bool)($entry['enabled'] ?? false),
'cron' => $entry['cron'] ?? '',
@@ -120,6 +151,7 @@ function vv_script_children(string $orchPath, array $schedule): array {
$children[] = [
'id' => $rel,
'label' => basename($rel, '.sh'),
'desc' => vv_script_description("$scriptsDir/$rel"),
'type' => 'script',
'enabled' => (bool)($entry['enabled'] ?? false),
'cron' => $entry['cron'] ?? '',
@@ -23,9 +23,11 @@ $tree = vv_job_tree();
onchange="vvSaveJob(this)">
<span class="vv-slider"></span>
</label>
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
placeholder="cron expression">
<div class="vv-name-cron">
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
placeholder="cron expression">
</div>
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
<button class="vv-btn-sm" onclick="vvSelectLog(this)">Log</button>
@@ -35,6 +37,9 @@ $tree = vv_job_tree();
<button class="vv-advanced-toggle" onclick="vvToggleAdvanced(this)">Advanced ▸</button>
<?php endif; ?>
</div>
<?php if (!empty($orch['desc'])): ?>
<div class="vv-job-desc" title="<?= htmlspecialchars($orch['desc']) ?>"><?= htmlspecialchars($orch['desc']) ?></div>
<?php endif; ?>
<?php if (!empty($orch['children'])): ?>
<div class="vv-children" style="display:none;">
@@ -47,15 +52,20 @@ $tree = vv_job_tree();
onchange="vvSaveJob(this)">
<span class="vv-slider"></span>
</label>
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
placeholder="cron expression">
<div class="vv-name-cron">
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
placeholder="cron expression">
</div>
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
<button class="vv-btn-sm" onclick="vvSelectLog(this)">Log</button>
<span class="vv-job-dot"></span>
<span class="vv-job-status"></span>
</div>
<?php if (!empty($child['desc'])): ?>
<div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>