Put the media seed behind MEDIA_SEED_ENABLED so a partner filled by other means never starts a multi-week transfer

Tier 2 beside the per-orchestrator gates. Unset reads as on — the toggle postdates the seed.
This commit is contained in:
Gmer4Lfe
2026-08-17 07:46:51 -04:00
parent ad623353bc
commit 1a47f864f9
7 changed files with 139 additions and 37 deletions
+9
View File
@@ -640,6 +640,14 @@
# WEEKLY_RSYNC_ENABLED=true ← Emby + Critical-Data still sync (NVMe)
# MONTHLY_RSYNC_ENABLED=true ← monthly_maintenance.sh rsync section
# FALLBACK_RSYNC_ENABLED=true ← handback writeback still works
# MEDIA_SEED_ENABLED=false ← the onboard first-fill never starts
#
# MEDIA_SEED_ENABLED is the odd one out: it does not gate a schedule, it gates a single
# multi-week transfer. Rsync/media_seed.sh pushes every DAILY_SYNC_SHARES entry to a newly
# onboarded partner — on HOST1 that is ~28 TB against BW_LIMIT, so weeks. Turn it off when the
# partner is going to be filled some other way (a physically moved disk, an existing library),
# and onboard will finish without ever starting it. Turning it back on does not start anything
# by itself; dispatch it from the Partnership tab or run the script.
# → Run individual: bash Rsync/rsync.sh /mnt/user/Movies
# → When ready: INTERMEDIATE_RSYNC_ENABLED=true DAILY_RSYNC_ENABLED=true
# NOTE ON THE DEFAULT: Tier 1 ships OFF. This template is what a brand-new node is seeded from
@@ -655,6 +663,7 @@
WEEKLY_RSYNC_ENABLED=true # Tier 2 — weekly_sync_maintenance.sh rsync section
MONTHLY_RSYNC_ENABLED=true # Tier 2 — monthly_maintenance.sh rsync section
FALLBACK_RSYNC_ENABLED=true # Tier 2 — fallback.sh writeback jobs on handback
MEDIA_SEED_ENABLED=true # Tier 2 — Rsync/media_seed.sh, the onboard first-fill
# ━━━ Download Webhook ━━━
# Immediate push to remote nodes on every Sonarr/Radarr/Lidarr Download event.
+14 -1
View File
@@ -1089,8 +1089,20 @@ _seed_job="Rsync/media_seed.sh"
_seed_script="$SCRIPTS_ROOT/Rsync/media_seed.sh"
_runner="$SCRIPTS_ROOT/Plugin/$PLATFORM/run_job.sh"
# Read from disk, not from the sourced value: Step 9c rewrote master.conf a few steps ago.
# Unset reads as on — the toggle postdates the seed, so a conf that has not been through a
# conf_upgrade must keep the behaviour it had. media_seed.sh checks this again itself; the
# check here exists so the summary can say "disabled" instead of dispatching a job whose only
# act is to exit.
_seed_gate=$(grep -m1 -E '^[[:space:]]*MEDIA_SEED_ENABLED=' "$SCRIPTS_ROOT/Configurations/master.conf" 2>/dev/null \
| cut -d= -f2- | cut -d'#' -f1 | tr -d '"'"'" | tr -d '[:space:]')
if [[ "$SKIP_MEDIA_SEED" == true ]]; then
warn "Skipping (--skip-media-seed)"
elif [[ -n "$_seed_gate" && "$_seed_gate" != "true" ]]; then
warn "MEDIA_SEED_ENABLED is '$_seed_gate' — not dispatching the seed"
warn "$MIRROR will need its library filled another way, or arm the toggle and start it"
warn "from the Partnership tab"
elif [[ "$ONBOARD_OK" == false ]]; then
warn "Skipping — onboard did not complete"
elif [[ "$DRY_RUN" == true ]]; then
@@ -1158,8 +1170,9 @@ echo " Step 10 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "s
echo " Step 11 — Discovery: $( [[ "$POPULATE_OK" == skipped ]] && echo "skipped (unreachable)" || _ok "$POPULATE_OK" )"
echo " Step 12 — Grouping: $( [[ "$FOLDER_OK" == skipped ]] && echo "skipped" || _ok "$FOLDER_OK" )"
echo " Step 13 — Media seed: $( [[ "$SKIP_MEDIA_SEED" == true ]] && echo "skipped" \
|| { [[ -n "${_seed_gate:-}" && "${_seed_gate:-}" != "true" ]] && echo "off (MEDIA_SEED_ENABLED=$_seed_gate)" \
|| { [[ "$MEDIA_SEED_DISPATCHED" == true ]] && echo "dispatched — runs in background ✅" \
|| echo "not dispatched ❌"; } )"
|| echo "not dispatched ❌"; }; } )"
echo ""
if [[ "$ONBOARD_OK" == true ]]; then
+20 -7
View File
@@ -245,10 +245,22 @@ function vv_pt_remote_system(string $ip, string $sshKey): array {
function vv_pt_media_seed(): array {
$stat = '/var/log/varaverk/Rsync/media_seed.json';
$log = '/var/log/varaverk/Rsync/media_seed.log';
if (!is_file($stat)) return [];
// MEDIA_SEED_ENABLED is reported alongside the record rather than instead of it, because
// the two answer different questions: the toggle says whether a seed may start, the record
// says what the last one did. media_seed.sh exits 0 when the toggle is off — correct, it is
// a decision and not a failure — so a record reading "ok" beside a disabled toggle would
// otherwise render as "Seed complete" over a partner that was never seeded.
//
// Unset reads as enabled, matching media_seed.sh: the toggle postdates the script.
$raw = vv_read_conf_raw('master.conf');
$toggle = vv_arr_scalar($raw, 'MEDIA_SEED_ENABLED');
$enabled = ($toggle === '' || strtolower($toggle) === 'true');
if (!is_file($stat)) return ['enabled' => $enabled];
$j = json_decode((string)file_get_contents($stat), true);
if (!is_array($j)) return [];
if (!is_array($j)) return ['enabled' => $enabled];
$status = (string)($j['status'] ?? '');
$pid = (int)($j['pid'] ?? 0);
@@ -280,11 +292,12 @@ function vv_pt_media_seed(): array {
}
return array_filter([
'status' => $status,
'start' => isset($j['start']) ? (int)$j['start'] : null,
'end' => isset($j['end']) ? (int)$j['end'] : null,
'share' => $share,
'line' => mb_substr($line, 0, 160),
'enabled' => $enabled,
'status' => $status,
'start' => isset($j['start']) ? (int)$j['start'] : null,
'end' => isset($j['end']) ? (int)$j['end'] : null,
'share' => $share,
'line' => mb_substr($line, 0, 160),
], fn($v) => $v !== null && $v !== '');
}
+55 -20
View File
@@ -381,6 +381,29 @@ function vvPtHostChanged(el) {
// Stop the background media seed. Safe to press: rsync.sh runs --inplace --partial, so this
// costs the file in flight and a later start resumes the share rather than restarting it.
// Start the background seed. Nothing here decides whether it may run — MEDIA_SEED_ENABLED is
// checked by the script, and the button is not rendered when the toggle is off.
async function vvPtStartSeed(btn) {
if (!await vvConfirm('Start the media seed?\n\nEvery DAILY_SYNC_SHARES entry is pushed to the partner. A first seed of a full library runs for days.')) return;
btn.disabled = true; btn.textContent = '⟳';
try {
const r = await fetch('/plugins/varaverk/api/media_seed.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
action: 'start'
})
});
const d = await r.json();
if (!d.ok) { vvAlert('Seed did not start: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = '▶ Seed'; return; }
if (typeof vvPtLoad === 'function') vvPtLoad();
} catch (e) {
btn.disabled = false; btn.textContent = '▶ Seed';
vvAlert('Error: ' + e);
}
}
async function vvPtStopSeed(btn) {
if (!await vvConfirm('Stop the media seed?\n\nThe transfer resumes from where it stopped when restarted.')) return;
btn.disabled = true; btn.textContent = '⟳';
@@ -1151,27 +1174,39 @@ function _renderActions(nodes, cfg) {
// Media seed strip. The seed is dispatched detached by onboard Step 13 and runs for
// days, so without this the page would show a finished onboard and no sign that
// terabytes are still moving underneath it.
// The toggle is read first and the record second. media_seed.sh exits 0 when
// MEDIA_SEED_ENABLED is false, so the record can read "ok" for a seed that never
// moved a byte — showing that as "Seed complete" would be exactly the kind of
// outcome-from-a-toggle claim this codebase keeps having to unpick.
const ms = remote.media_seed || {};
if (ms.status) {
const seedRunning = ms.status === 'running';
const seedColor = seedRunning ? '#ff9800'
: ms.status === 'ok' ? '#4caf50'
: ms.status === 'stopped' ? '#666' : '#f44336';
const seedLabel = seedRunning ? 'Seeding'
: ms.status === 'ok' ? 'Seed complete'
: ms.status === 'stopped' ? 'Seed stopped' : 'Seed failed';
html += `<div style="margin-top:9px;padding-top:8px;border-top:1px solid #181818;">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">
<span style="font-size:9px;font-weight:600;color:${seedColor};">${seedLabel}</span>
${ms.share ? `<span style="font-size:9px;color:#444;font-family:monospace;">${vvEscHtml(ms.share)}</span>` : ''}
${seedRunning ? `<button class="vv-pt-action-btn warn" onclick="vvPtStopSeed(this)"
style="font-size:9px;opacity:.55;margin-left:auto;">■ Stop</button>` : ''}
</div>
${ms.line ? `<div style="margin-top:3px;font-size:9px;color:#2e2e2e;font-family:monospace;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
>${vvEscHtml(ms.line)}</div>` : ''}
</div>`;
}
const seedRunning = ms.status === 'running';
let seedColor, seedLabel;
if (ms.enabled === false && !seedRunning) {
seedColor = '#555'; seedLabel = 'Seed off';
} else if (seedRunning) { seedColor = '#ff9800'; seedLabel = 'Seeding'; }
else if (ms.status === 'ok') { seedColor = '#4caf50'; seedLabel = 'Seed complete'; }
else if (ms.status === 'stopped'){ seedColor = '#666'; seedLabel = 'Seed stopped'; }
else if (ms.status) { seedColor = '#f44336'; seedLabel = 'Seed failed'; }
else { seedColor = '#555'; seedLabel = 'Not seeded'; }
const seedOff = ms.enabled === false;
const showLine = seedRunning || (ms.line && !seedOff);
html += `<div style="margin-top:9px;padding-top:8px;border-top:1px solid #181818;">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">
<span style="font-size:9px;font-weight:600;color:${seedColor};">${seedLabel}</span>
${seedOff ? `<span style="font-size:9px;color:#333;">MEDIA_SEED_ENABLED=false</span>` : ''}
${!seedOff && ms.share ? `<span style="font-size:9px;color:#444;font-family:monospace;">${vvEscHtml(ms.share)}</span>` : ''}
${seedRunning
? `<button class="vv-pt-action-btn warn" onclick="vvPtStopSeed(this)"
style="font-size:9px;opacity:.55;margin-left:auto;">■ Stop</button>`
: (seedOff ? '' : `<button class="vv-pt-action-btn info" onclick="vvPtStartSeed(this)"
style="font-size:9px;opacity:.55;margin-left:auto;"
title="Push every DAILY_SYNC_SHARES entry to ${remote.id}">▶ Seed</button>`)}
</div>
${showLine ? `<div style="margin-top:3px;font-size:9px;color:#2e2e2e;font-family:monospace;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
>${vvEscHtml(ms.line || '')}</div>` : ''}
</div>`;
}
// ── Delete Keys panel ──────────────────────────────────────────────────
+1
View File
@@ -1788,6 +1788,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
| `WEEKLY_RSYNC_ENABLED` | a switch | in this section | Tier 2 — weekly_sync_maintenance.sh rsync section |
| `MONTHLY_RSYNC_ENABLED` | a switch | in this section | Tier 2 — monthly_maintenance.sh rsync section |
| `FALLBACK_RSYNC_ENABLED` | a switch | Fallback tab | Tier 2 — fallback.sh writeback jobs on handback |
| `MEDIA_SEED_ENABLED` | a switch | Partnership tab | Tier 2 — Rsync/media_seed.sh, the onboard first-fill of a new partner |
## Rsync Merge Auto-Promote
+5
View File
@@ -111,6 +111,11 @@ HOST1's library is ~28 TB against a 12.5 MB/s `--bwlimit`, which is why onboard
rather than waiting on it, and why the Partnership tab gives it a Stop button — `rsync.sh` runs
`--inplace --partial`, so stopping costs the file in flight, not the share.
It answers to `MEDIA_SEED_ENABLED` in `master.conf` — a Tier 2 toggle beside the per-orchestrator
ones, still under Tier 1 `RSYNC_ENABLED`. Off means onboard finishes without ever dispatching it,
for a partner being filled from a moved disk or one that already holds the library. An unset
`MEDIA_SEED_ENABLED` reads as on, so a conf that predates the toggle keeps its behaviour.
---
## ━━━ HOW THE SCRIPTS RELATE ━━━
+35 -9
View File
@@ -41,12 +41,19 @@
# Interrupting this script costs the current file, not the current share, and a
# re-run picks up where it stopped. Stopping it is cheap; that is deliberate.
#
# Two Gates, And They Mean Different Things
# MEDIA_SEED_ENABLED (master.conf, Tier 2) switches off this transfer and only
# this transfer — for a partner being filled from a physically moved disk, or
# one that already holds the library. Off is a decision, so it exits 0.
# RSYNC_ENABLED (Tier 1) switches off every rsync on the host. Reaching this
# script with Tier 1 closed is a misconfiguration, so it exits 1.
#
# Gate Read From Disk
# RSYNC_ENABLED is read out of master.conf here rather than trusted from the
# sourced environment. Onboard's Step 9c rewrites that file moments before
# dispatching this script, and each rsync.sh below sources it fresh anyway.
# With the gate closed rsync.sh moves nothing and still exits 0, so every share
# would be counted as seeded — refuse once instead of reporting fourteen no-ops.
# Both are read out of master.conf here rather than trusted from the sourced
# environment. Onboard's Step 9c rewrites that file moments before dispatching
# this script, and each rsync.sh below sources it fresh anyway. With Tier 1
# closed rsync.sh moves nothing and still exits 0, so every share would be
# counted as seeded — refuse once instead of reporting fourteen no-ops.
#
# One Failed Share Is Not A Failed Seed
# A share whose backing disk is unmounted on the partner fails its own rsync and
@@ -58,10 +65,16 @@
# ==============================================================================================
#
# acquire_lock "skip" — a second seed cannot run beside the first
# Gate check refuses when RSYNC_ENABLED is not true
# Gate checksMEDIA_SEED_ENABLED then RSYNC_ENABLED, in that order
# Empty list check — refuses when DAILY_SYNC_SHARES is empty
# Per-share accounting — failures are listed by name, not summed away
#
# An absent MEDIA_SEED_ENABLED reads as on, never as off.
# The toggle was added after this script shipped, so a master.conf that has not been
# through a conf_upgrade does not have the key. Defaulting an unset toggle to off would
# silently stop seeding on every node that has not upgraded — a change in behaviour
# delivered by a missing line, which is the hardest kind to notice.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
@@ -94,11 +107,24 @@ echo "━━━ $ICON_SYNC Media Share Seed — $MY_ID ($LOCAL_SERVER_NAME) —
echo ""
# See "Gate Read From Disk" above. The trailing comment is cut before the value is compared:
# master.conf writes this as `RSYNC_ENABLED=true # Tier 1 — global gate, overrides everything
# master.conf writes these as `RSYNC_ENABLED=true # Tier 1 — global gate, overrides everything
# below`, so stopping at `cut -d= -f2` yields "true#Tier1—globalgate,…" and never matches.
_gate=$(grep -m1 -E '^[[:space:]]*RSYNC_ENABLED=' "$SCRIPTS_ROOT/Configurations/master.conf" 2>/dev/null \
| cut -d= -f2- | cut -d'#' -f1 | tr -d '"'"'" | tr -d '[:space:]')
_read_gate() {
grep -m1 -E "^[[:space:]]*$1=" "$SCRIPTS_ROOT/Configurations/master.conf" 2>/dev/null \
| cut -d= -f2- | cut -d'#' -f1 | tr -d '"'"'" | tr -d '[:space:]'
}
# Tier 2 first, because it is the more specific answer and the operator deserves to be told
# which switch stopped this. An unset MEDIA_SEED_ENABLED is treated as on: it was added after
# the seed already existed, so a conf that predates it must keep behaving the way it did.
_seed_gate=$(_read_gate MEDIA_SEED_ENABLED)
if [[ "$DRY_RUN" == false && -n "$_seed_gate" && "$_seed_gate" != "true" ]]; then
warn "MEDIA_SEED_ENABLED is '$_seed_gate' — the media seed is switched off in master.conf"
warn "Set it true there if the partner should be filled by rsync rather than by hand"
exit 0
fi
_gate=$(_read_gate RSYNC_ENABLED)
if [[ "$DRY_RUN" == false && "$_gate" != "true" ]]; then
error "RSYNC_ENABLED is '${_gate:-unset}' — rsync.sh would move nothing"
error "Arm it in master.conf, then re-run this script"