Make seeding Phase 3 — its own operator-triggered phase, not the tail of an onboard

Onboard now ends at Phase 2: connected, running, and saying so. Phase 3 sets the seeding gate posture and dispatches the seed. With MEDIA_SEED_ENABLED false there is no Phase 3 and the model is two phases.
This commit is contained in:
Gmer4Lfe
2026-08-17 10:23:03 -04:00
parent 427a31cdff
commit 7c957d96be
4 changed files with 207 additions and 139 deletions
+127 -85
View File
@@ -42,9 +42,10 @@
# would discover are actually deployed there # would discover are actually deployed there
# Step 12: Container grouping — file our containers on the mirror under "<OwnerShort>-Fallback", # Step 12: Container grouping — file our containers on the mirror under "<OwnerShort>-Fallback",
# icon resolved here and passed over: the mirror has no Emby key # icon resolved here and passed over: the mirror has no Emby key
# Step 13: Media seed — dispatch Rsync/media_seed.sh detached, after the phase flag #
# is written. It is a multi-week transfer and must never be # Phase 3 (media seed) is NOT part of the above. Onboard ends at Phase 2 — connected, running,
# the thing standing between an onboard and its own summary # and saying so. Seeding is a separate operator-triggered phase; see --phase3-only. With
# MEDIA_SEED_ENABLED=false there is no Phase 3 and the model is two phases.
# #
# ============================================================================================== # ==============================================================================================
# DESIGN PRINCIPLES # DESIGN PRINCIPLES
@@ -159,14 +160,15 @@
# Partnership/partnership_onboard.sh --skip-webhook-setup # Partnership/partnership_onboard.sh --skip-webhook-setup
# Skip webhook registration in arrs (Step 9b) # Skip webhook registration in arrs (Step 9b)
# #
# Partnership/partnership_onboard.sh --phase3-only
# OWNER: media seed only. Refuses unless Phase 2 is done and MEDIA_SEED_ENABLED is true.
# Sets the seeding gate posture (Tier 1 open, every Tier 2 gate closed) and dispatches
# Rsync/media_seed.sh detached. Returns in seconds; the seed runs for weeks.
#
# Partnership/partnership_onboard.sh --skip-appdata-provision # Partnership/partnership_onboard.sh --skip-appdata-provision
# Skip the pre-container auth appdata rsync (Step 1e) # Skip the pre-container auth appdata rsync (Step 1e)
# The mirror's auth containers then start against whatever is already there # The mirror's auth containers then start against whatever is already there
# #
# Partnership/partnership_onboard.sh --skip-media-seed
# Skip dispatching the background media seed (Step 13)
# Use when mirror already has files or you want to seed manually
#
# Partnership/partnership_onboard.sh --skip-webhook-listener # Partnership/partnership_onboard.sh --skip-webhook-listener
# Skip starting webhook listener on mirror (Step 9e) # Skip starting webhook listener on mirror (Step 9e)
# Listener will start automatically on next array restart # Listener will start automatically on next array restart
@@ -203,11 +205,11 @@ SKIP_ARR_STACK=false
SKIP_SERVICES_STACK=false SKIP_SERVICES_STACK=false
SKIP_ARR_SYNC=false SKIP_ARR_SYNC=false
SKIP_WEBHOOK_SETUP=false SKIP_WEBHOOK_SETUP=false
SKIP_MEDIA_SEED=false
SKIP_WEBHOOK_LISTENER=false SKIP_WEBHOOK_LISTENER=false
PHASE1_ONLY=false # OWNER: SSH + conf push only (HOST2 not yet installed) PHASE1_ONLY=false # OWNER: SSH + conf push only (HOST2 not yet installed)
# MIRROR: SSH key install only, no owner notification # MIRROR: SSH key install only, no owner notification
PHASE2_ONLY=false # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards) PHASE2_ONLY=false # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards)
PHASE3_ONLY=false # OWNER: media seed only — a separate decision, see the PHASE 3 block
SKIP_ARM=false # leave the sync gates as they are — see Step 1d SKIP_ARM=false # leave the sync gates as they are — see Step 1d
SKIP_APPDATA_PROVISION=false # skip the pre-container auth appdata sync — see Step 1e SKIP_APPDATA_PROVISION=false # skip the pre-container auth appdata sync — see Step 1e
FILTERED_ARGS=() FILTERED_ARGS=()
@@ -221,11 +223,11 @@ for arg in "$@"; do
--skip-services-stack) SKIP_SERVICES_STACK=true ;; --skip-services-stack) SKIP_SERVICES_STACK=true ;;
--skip-arr-sync) SKIP_ARR_SYNC=true ;; --skip-arr-sync) SKIP_ARR_SYNC=true ;;
--skip-webhook-setup) SKIP_WEBHOOK_SETUP=true ;; --skip-webhook-setup) SKIP_WEBHOOK_SETUP=true ;;
--skip-media-seed) SKIP_MEDIA_SEED=true ;;
--skip-appdata-provision) SKIP_APPDATA_PROVISION=true ;; --skip-appdata-provision) SKIP_APPDATA_PROVISION=true ;;
--skip-webhook-listener) SKIP_WEBHOOK_LISTENER=true ;; --skip-webhook-listener) SKIP_WEBHOOK_LISTENER=true ;;
--phase1-only) PHASE1_ONLY=true ;; --phase1-only) PHASE1_ONLY=true ;;
--phase2-only) PHASE2_ONLY=true; SKIP_SSH=true ;; --phase2-only) PHASE2_ONLY=true; SKIP_SSH=true ;;
--phase3-only) PHASE3_ONLY=true; SKIP_SSH=true ;;
--no-arm) SKIP_ARM=true ;; --no-arm) SKIP_ARM=true ;;
*) FILTERED_ARGS+=("$arg") ;; *) FILTERED_ARGS+=("$arg") ;;
esac esac
@@ -474,8 +476,124 @@ MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
log "Mirror: $MIRROR ($MIRROR_IP)" log "Mirror: $MIRROR ($MIRROR_IP)"
[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)" [[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)"
[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)" [[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)"
[[ "$PHASE3_ONLY" == true ]] && log "Mode: Phase 3 only (media seed)"
echo "" echo ""
# ==============================================================================================
# ── PHASE 3: MEDIA SEED ───────────────────────────────────────────────────────────────────────
#
# The partnership is finished at the end of Phase 2 — connected, running, and saying so. Phase 3
# is not part of it. It is the separate decision to fill the partner's disks by rsync, and it is
# operator-triggered because a first seed of a full library is weeks of transfer and tens of
# terabytes of somebody else's free space.
#
# The seed used to be a step inside Phase 2. Inline it held the onboard open for the entire
# transfer, which meant the phase-2 flag, the conf push, discovery and container grouping all
# waited on it, and both hosts described an unfinished onboard over a partnership that was
# already live. Detaching it fixed the blocking but left the seed starting on its own, which is
# still the wrong default: the machine decided to move 28 TB because an onboard succeeded.
#
# With MEDIA_SEED_ENABLED=false there is no Phase 3 at all and the model is two phases. That is
# the point of the toggle — not a seed that runs and does nothing, an absent phase.
# ==============================================================================================
if [[ "$PHASE3_ONLY" == true ]]; then
echo ""
echo "━━━━━ $ICON_SYNC PHASE 3 — MEDIA SEED — $MY_ID$MIRROR ━━━━━"
echo ""
_master_conf="$SCRIPTS_ROOT/Configurations/master.conf"
_seed_gate=$(grep -m1 -E '^[[:space:]]*MEDIA_SEED_ENABLED=' "$_master_conf" 2>/dev/null \
| cut -d= -f2- | cut -d'#' -f1 | tr -d '"'"'" | tr -d '[:space:]')
if [[ -n "$_seed_gate" && "$_seed_gate" != "true" ]]; then
error "MEDIA_SEED_ENABLED is '$_seed_gate' — there is no Phase 3 on this host"
error " Onboarding is two phases while seeding is off. Arm the toggle in master.conf"
error " if you want the partner's library filled by rsync."
exit 1
fi
_setup_db="$(platform_setup_db_path)"
if ! grep -q "^${MIRROR_ID}_PHASE2_DONE=true" "$_setup_db" 2>/dev/null; then
error "$MIRROR has not completed Phase 2 — seeding a partner that is not onboarded"
error " would push media at a host with no containers to serve it. Finish Phase 2 first."
exit 1
fi
unset _setup_db
# Seeding posture, set before the transfer rather than after it. Tier 1 has to be open or
# rsync.sh refuses every share; every Tier 2 gate is closed so the scheduled orchestrators
# are not competing for the same link and the same disks for the next several weeks. This is
# also the posture the seed leaves behind — global on, per-orchestrator off — so finishing
# Phase 3 does not quietly hand the schedule back.
echo "━━━ Gate posture ━━━"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would set RSYNC_ENABLED true and every Tier 2 rsync gate false"
else
cp -a "$_master_conf" "${_master_conf}.bak-phase3-$(date +%Y%m%d-%H%M%S)"
set_conf_bool RSYNC_ENABLED "true" "$_master_conf" \
|| { error "Could not open Tier 1 — the seed would move nothing"; exit 1; }
for _gate in CRITICAL_RSYNC_ENABLED INTERMEDIATE_RSYNC_ENABLED DAILY_RSYNC_ENABLED \
WEEKLY_RSYNC_ENABLED MONTHLY_RSYNC_ENABLED FALLBACK_RSYNC_ENABLED; do
set_conf_bool "$_gate" "false" "$_master_conf" \
|| warn "Could not close $_gate — a scheduled sync may run beside the seed"
done
unset _gate
echo " Tier 1 open, every Tier 2 gate closed ✅"
fi
unset _master_conf _seed_gate
echo ""
echo "━━━ Dispatch ━━━"
_seed_script="$SCRIPTS_ROOT/Rsync/media_seed.sh"
_runner="$SCRIPTS_ROOT/Plugin/$PLATFORM/run_job.sh"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would dispatch Rsync/media_seed.sh"
exit 0
fi
if [[ ! -f "$_seed_script" || ! -f "$_runner" ]]; then
error "Seed runner not found at $_seed_script"
exit 1
fi
# setsid, not nohup + &: the seed must outlive this script and must not share its process
# group, or stopping Phase 3 takes the transfer down with it.
_seed_flags=()
[[ "$ENABLE_LOGGING" == true ]] && _seed_flags+=(--log)
setsid bash "$_runner" "Rsync/media_seed.sh" "$_seed_script" "${_seed_flags[@]}" \
>/dev/null 2>&1 < /dev/null &
disown 2>/dev/null || true
# Report the record, not the launch. run_job.sh writes its stat file before running the
# script, so a fresh "running" record is the difference between dispatched and attempted.
_seed_stat="/var/log/varaverk/Rsync/media_seed.json"
_dispatched=false
for _i in $(seq 1 10); do
if [[ -f "$_seed_stat" ]] \
&& grep -q '"status":"running"' "$_seed_stat" 2>/dev/null \
&& (( $(date +%s) - $(stat -c %Y "$_seed_stat") < 60 )); then
_dispatched=true
break
fi
sleep 1
done
echo ""
echo "━━━━━ $ICON_SUMMARY PHASE 3 SUMMARY ━━━━━"
if [[ "$_dispatched" == true ]]; then
echo " Seed: running as job Rsync/media_seed.sh ✅"
echo " Shares: ${#DAILY_SYNC_SHARES[@]}"
echo " Gates: Tier 1 open · every Tier 2 closed"
echo ""
echo " A first full seed runs for days. Watch it on the Partnership tab; the"
echo " partnership itself has been live since Phase 2 and does not depend on this."
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
error " Seed did not start — check /var/log/varaverk/Rsync/media_seed.log"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 1
fi
STEP_SSH_OK=false STEP_SSH_OK=false
STEP_NETWORK_OK=false STEP_NETWORK_OK=false
LOCAL_SETUP_OK=true # partnership_manager --local-only; the summary claimed done ✅ regardless LOCAL_SETUP_OK=true # partnership_manager --local-only; the summary claimed done ✅ regardless
@@ -1122,78 +1240,6 @@ fi
# ── Write Phase 2 completion state ──────────────────────────────────────────────────────────── # ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2 [[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2
# ── Step 13: Dispatch the media seed ──────────────────────────────────────────────────────────
# Last, detached, and after the phase flag is already on disk — see the header of
# Rsync/media_seed.sh for why. Briefly: on HOST1 this is thirteen shares and ~28 TB behind a
# 12.5 MB/s bwlimit, so inline it held this script open for weeks. Everything below it waited,
# including write_onboard_phase above, so the owner's card stayed on "Waiting for HOST2 to
# install Varaverk" and the mirror's checklist on "1 required item left: Partnership" for the
# entire transfer, describing a partnership that was already established. The onboard job
# record also stayed "running", which run_job.sh's already-running guard reads as a reason to
# refuse every later onboard.
#
# setsid, not nohup + &: the seed must survive this script exiting and must not share a process
# group with it, or stopping the onboard takes the transfer down with it.
echo ""
echo "━━━ $ICON_SYNC Step 13 — Media Share Seed (dispatch) ━━━"
MEDIA_SEED_DISPATCHED=false
_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 1d 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
warn "DRY RUN — would dispatch $_seed_job"
elif [[ ! -f "$_seed_script" || ! -f "$_runner" ]]; then
warn "Seed runner not found — run manually: bash $_seed_script"
else
_seed_flags=()
[[ "$ENABLE_LOGGING" == true ]] && _seed_flags+=(--log)
setsid bash "$_runner" "$_seed_job" "$_seed_script" "${_seed_flags[@]}" \
>/dev/null 2>&1 < /dev/null &
disown 2>/dev/null || true
# Confirm the runner actually took the job rather than reporting the launch. run_job.sh
# writes its stat file before exec'ing the script, so a fresh "running" record with a live
# pid is the difference between dispatched and merely attempted.
_seed_stat="/var/log/varaverk/Rsync/media_seed.json"
for _i in 1 2 3 4 5 6 7 8 9 10; do
if [[ -f "$_seed_stat" ]] \
&& grep -q '"status":"running"' "$_seed_stat" 2>/dev/null \
&& (( $(date +%s) - $(stat -c %Y "$_seed_stat") < 60 )); then
MEDIA_SEED_DISPATCHED=true
break
fi
sleep 1
done
unset _seed_stat _i _seed_flags
if [[ "$MEDIA_SEED_DISPATCHED" == true ]]; then
echo "Seed running as job $_seed_job"
echo " ${#DAILY_SYNC_SHARES[@]} share(s) — watch it on the Scheduler tab, not here"
echo " A first full seed runs for days. The partnership is live now; this only fills disks."
else
warn "Seed did not start — run manually: bash $_seed_script"
fi
fi
unset _seed_job _seed_script _runner
# ── Summary ─────────────────────────────────────────────────────────────────────────────────── # ── Summary ───────────────────────────────────────────────────────────────────────────────────
END=$(date +%s) END=$(date +%s)
echo "" echo ""
@@ -1223,10 +1269,6 @@ echo " Step 9e — Webhook listener: $(_skip "$SKIP_WEBHOOK_LISTENER" "$WEBHOOK
echo " Step 10 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )" echo " Step 10 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
echo " Step 11 — Discovery: $( [[ "$POPULATE_OK" == skipped ]] && echo "skipped (unreachable)" || _ok "$POPULATE_OK" )" 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 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 "" echo ""
if [[ "$ONBOARD_OK" == true ]]; then if [[ "$ONBOARD_OK" == true ]]; then
+12 -5
View File
@@ -242,7 +242,7 @@ function vv_pt_remote_system(string $ip, string $sshKey): array {
// //
// Returns [] when there has never been a seed. A stale "running" record whose pid is gone is // Returns [] when there has never been a seed. A stale "running" record whose pid is gone is
// reported as stopped rather than running: a record is not a process. // reported as stopped rather than running: a record is not a process.
function vv_pt_media_seed(): array { function vv_pt_media_seed(array $setupDb = [], string $nodeIdUpper = ''): array {
$stat = '/var/log/varaverk/Rsync/media_seed.json'; $stat = '/var/log/varaverk/Rsync/media_seed.json';
$log = '/var/log/varaverk/Rsync/media_seed.log'; $log = '/var/log/varaverk/Rsync/media_seed.log';
@@ -257,10 +257,16 @@ function vv_pt_media_seed(): array {
$toggle = vv_arr_scalar($raw, 'MEDIA_SEED_ENABLED'); $toggle = vv_arr_scalar($raw, 'MEDIA_SEED_ENABLED');
$enabled = ($toggle === '' || strtolower($toggle) === 'true'); $enabled = ($toggle === '' || strtolower($toggle) === 'true');
if (!is_file($stat)) return ['enabled' => $enabled]; // Phase 3 is complete only when every share landed, which media_seed.sh decides and records
// at the end of a run that may have taken weeks. The job record cannot answer this: it goes
// "ok" the moment the process exits, including the exits that seeded nothing.
$phase3Done = $nodeIdUpper !== ''
&& ($setupDb[$nodeIdUpper . '_PHASE3_DONE'] ?? '') === 'true';
if (!is_file($stat)) return ['enabled' => $enabled, 'phase3_done' => $phase3Done];
$j = json_decode((string)file_get_contents($stat), true); $j = json_decode((string)file_get_contents($stat), true);
if (!is_array($j)) return ['enabled' => $enabled]; if (!is_array($j)) return ['enabled' => $enabled, 'phase3_done' => $phase3Done];
$status = (string)($j['status'] ?? ''); $status = (string)($j['status'] ?? '');
$pid = (int)($j['pid'] ?? 0); $pid = (int)($j['pid'] ?? 0);
@@ -292,7 +298,8 @@ function vv_pt_media_seed(): array {
} }
return array_filter([ return array_filter([
'enabled' => $enabled, 'enabled' => $enabled,
'phase3_done' => $phase3Done,
'status' => $status, 'status' => $status,
'start' => isset($j['start']) ? (int)$j['start'] : null, 'start' => isset($j['start']) ? (int)$j['start'] : null,
'end' => isset($j['end']) ? (int)$j['end'] : null, 'end' => isset($j['end']) ? (int)$j['end'] : null,
@@ -433,7 +440,7 @@ function vv_pt_nodes(): array {
'api_key_set' => $apiKeySet, 'api_key_set' => $apiKeySet,
'api_key_preview' => $apiKeyPreview, 'api_key_preview' => $apiKeyPreview,
'metrics' => $metrics, 'metrics' => $metrics,
'media_seed' => $isMe ? [] : vv_pt_media_seed(), 'media_seed' => $isMe ? [] : vv_pt_media_seed($setupDb, $nodeIdUpper),
]; ];
} }
return $nodes; return $nodes;
+50 -48
View File
@@ -381,27 +381,23 @@ function vvPtHostChanged(el) {
// Stop the background media seed. Safe to press: rsync.sh runs --inplace --partial, so this // 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. // 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 // Phase 3 — the media seed, as its own phase rather than the tail of an onboard. Routed through
// checked by the script, and the button is not rendered when the toggle is off. // partnership_onboard.sh --phase3-only rather than straight at media_seed.sh, because the phase
async function vvPtStartSeed(btn) { // is more than the transfer: it refuses when Phase 2 is unfinished, and it sets the gate posture
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; // the seed needs (Tier 1 open so rsync.sh will move anything, every Tier 2 gate closed so the
btn.disabled = true; btn.textContent = '⟳'; // scheduled orchestrators are not fighting it for the link for the next several weeks).
try { async function vvPtPhase3(btn, id) {
const r = await fetch('/plugins/varaverk/api/media_seed.php', { const label = btn.textContent;
method: 'POST', if (!await vvConfirm(`Start Phase 3 — seed media to ${id}?\n\nEvery DAILY_SYNC_SHARES entry is pushed. A first seed of a full library runs for days, and the partnership is already live without it.\n\nTier 1 rsync is opened and every per-orchestrator gate is closed for the duration.`)) return;
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, btn.disabled = true;
body: new URLSearchParams({ btn.textContent = '⟳ Starting…';
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', _vvPtRun('Partnership/partnership_onboard.sh', '--phase3-only')
action: 'start' .then(d => { if (!d.ok) vvAlert('Phase 3 did not start: ' + (d.error ?? 'Unknown error')); })
}) .catch(e => vvAlert('Error: ' + e))
}); .finally(() => setTimeout(() => {
const d = await r.json(); btn.disabled = false; btn.textContent = label;
if (!d.ok) { vvAlert('Seed did not start: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = '▶ Seed'; return; } if (typeof vvPtLoad === 'function') vvPtLoad();
if (typeof vvPtLoad === 'function') vvPtLoad(); }, 3000));
} catch (e) {
btn.disabled = false; btn.textContent = '▶ Seed';
vvAlert('Error: ' + e);
}
} }
async function vvPtStopSeed(btn) { async function vvPtStopSeed(btn) {
@@ -1178,35 +1174,41 @@ function _renderActions(nodes, cfg) {
// MEDIA_SEED_ENABLED is false, so the record can read "ok" for a seed that never // 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 // 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. // outcome-from-a-toggle claim this codebase keeps having to unpick.
// Phase 3 — the media seed. Deliberately not rendered at all when MEDIA_SEED_ENABLED is
// false: with seeding off the model is two phases, and a greyed-out third one would
// invite the question of how to reach it. The toggle removes the phase, it does not
// disable a button.
//
// The toggle is also read before the job record. media_seed.sh exits 0 when seeding is
// off, so the record can read "ok" for a run that moved nothing.
const ms = remote.media_seed || {}; const ms = remote.media_seed || {};
const seedRunning = ms.status === 'running'; if (ms.enabled !== false) {
let seedColor, seedLabel; const seedRunning = ms.status === 'running';
if (ms.enabled === false && !seedRunning) { const seedDone = ms.phase3_done === true;
seedColor = '#555'; seedLabel = 'Seed off'; let seedColor, seedLabel;
} else if (seedRunning) { seedColor = '#ff9800'; seedLabel = 'Seeding'; } if (seedRunning) { seedColor = '#ff9800'; seedLabel = 'Phase 3 — seeding'; }
else if (ms.status === 'ok') { seedColor = '#4caf50'; seedLabel = 'Seed complete'; } else if (seedDone) { seedColor = '#4caf50'; seedLabel = 'Phase 3 complete'; }
else if (ms.status === 'stopped'){ seedColor = '#666'; seedLabel = 'Seed stopped'; } else if (ms.status === 'stopped') { seedColor = '#666'; seedLabel = 'Phase 3 stopped'; }
else if (ms.status) { seedColor = '#f44336'; seedLabel = 'Seed failed'; } else if (ms.status === 'ok') { seedColor = '#ff9800'; seedLabel = 'Phase 3 partial'; }
else { seedColor = '#555'; seedLabel = 'Not seeded'; } else if (ms.status) { seedColor = '#f44336'; seedLabel = 'Phase 3 failed'; }
else { seedColor = '#555'; seedLabel = 'Phase 3 not started'; }
const seedOff = ms.enabled === false; html += `<div style="margin-top:9px;padding-top:8px;border-top:1px solid #181818;">
const showLine = seedRunning || (ms.line && !seedOff); <div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;">
html += `<div style="margin-top:9px;padding-top:8px;border-top:1px solid #181818;"> <span style="font-size:9px;font-weight:600;color:${seedColor};">${seedLabel}</span>
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;"> ${ms.share ? `<span style="font-size:9px;color:#444;font-family:monospace;">${vvEscHtml(ms.share)}</span>` : ''}
<span style="font-size:9px;font-weight:600;color:${seedColor};">${seedLabel}</span> ${seedRunning
${seedOff ? `<span style="font-size:9px;color:#333;">MEDIA_SEED_ENABLED=false</span>` : ''} ? `<button class="vv-pt-action-btn warn" onclick="vvPtStopSeed(this)"
${!seedOff && ms.share ? `<span style="font-size:9px;color:#444;font-family:monospace;">${vvEscHtml(ms.share)}</span>` : ''} style="font-size:9px;opacity:.55;margin-left:auto;">■ Stop</button>`
${seedRunning : `<button class="vv-pt-action-btn info" onclick="vvPtPhase3(this,'${remote.id}')"
? `<button class="vv-pt-action-btn warn" onclick="vvPtStopSeed(this)" style="font-size:9px;opacity:.55;margin-left:auto;"
style="font-size:9px;opacity:.55;margin-left:auto;">■ Stop</button>` title="Seed every DAILY_SYNC_SHARES entry to ${remote.id}">${seedDone ? '↻ Re-seed' : '▶ Phase 3'}</button>`}
: (seedOff ? '' : `<button class="vv-pt-action-btn info" onclick="vvPtStartSeed(this)" </div>
style="font-size:9px;opacity:.55;margin-left:auto;" ${ms.line ? `<div style="margin-top:3px;font-size:9px;color:#2e2e2e;font-family:monospace;
title="Push every DAILY_SYNC_SHARES entry to ${remote.id}">▶ Seed</button>`)} white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
</div> >${vvEscHtml(ms.line)}</div>` : ''}
${showLine ? `<div style="margin-top:3px;font-size:9px;color:#2e2e2e;font-family:monospace; </div>`;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" }
>${vvEscHtml(ms.line || '')}</div>` : ''}
</div>`;
} }
// ── Delete Keys panel ────────────────────────────────────────────────── // ── Delete Keys panel ──────────────────────────────────────────────────
+18 -1
View File
@@ -174,5 +174,22 @@ if [[ "$SEEDED" -eq 0 ]]; then
error "No shares seeded" error "No shares seeded"
exit 1 exit 1
fi fi
[[ "${#FAILED_SHARES[@]}" -gt 0 ]] && exit 1 if [[ "${#FAILED_SHARES[@]}" -gt 0 ]]; then
warn "Phase 3 incomplete — the flag stays unset while any share is unseeded"
exit 1
fi
# Phase 3 is complete only when every share landed. This is what the Partnership tab reads to
# stop offering ▶ Phase 3, so writing it on a partial run would retire the button over a partner
# still missing shares — and a partial seed is the normal outcome of the first attempt, because
# a disk unmounted on the far side fails its share and no other.
#
# Written here rather than by the dispatcher: the dispatcher returns in seconds and knows only
# that the job started. Weeks separate those two facts.
if [[ -n "${REMOTE_ID:-}" ]]; then
set_state_var "$(platform_setup_db_path)" "${REMOTE_ID}_PHASE3_DONE" "true"
platform_push_setup_state >/dev/null 2>&1 \
|| warn "Phase 3 flag written locally but not pushed to the partner"
echo "Phase 3 complete — ${REMOTE_ID}_PHASE3_DONE=true ✅"
fi
exit 0 exit 0