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:
@@ -42,9 +42,10 @@
|
||||
# would discover are actually deployed there
|
||||
# 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
|
||||
# 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
|
||||
# the thing standing between an onboard and its own summary
|
||||
#
|
||||
# Phase 3 (media seed) is NOT part of the above. Onboard ends at Phase 2 — connected, running,
|
||||
# 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
|
||||
@@ -159,14 +160,15 @@
|
||||
# Partnership/partnership_onboard.sh --skip-webhook-setup
|
||||
# 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
|
||||
# Skip the pre-container auth appdata rsync (Step 1e)
|
||||
# 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
|
||||
# Skip starting webhook listener on mirror (Step 9e)
|
||||
# Listener will start automatically on next array restart
|
||||
@@ -203,11 +205,11 @@ SKIP_ARR_STACK=false
|
||||
SKIP_SERVICES_STACK=false
|
||||
SKIP_ARR_SYNC=false
|
||||
SKIP_WEBHOOK_SETUP=false
|
||||
SKIP_MEDIA_SEED=false
|
||||
SKIP_WEBHOOK_LISTENER=false
|
||||
PHASE1_ONLY=false # OWNER: SSH + conf push only (HOST2 not yet installed)
|
||||
# MIRROR: SSH key install only, no owner notification
|
||||
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_APPDATA_PROVISION=false # skip the pre-container auth appdata sync — see Step 1e
|
||||
FILTERED_ARGS=()
|
||||
@@ -221,11 +223,11 @@ for arg in "$@"; do
|
||||
--skip-services-stack) SKIP_SERVICES_STACK=true ;;
|
||||
--skip-arr-sync) SKIP_ARR_SYNC=true ;;
|
||||
--skip-webhook-setup) SKIP_WEBHOOK_SETUP=true ;;
|
||||
--skip-media-seed) SKIP_MEDIA_SEED=true ;;
|
||||
--skip-appdata-provision) SKIP_APPDATA_PROVISION=true ;;
|
||||
--skip-webhook-listener) SKIP_WEBHOOK_LISTENER=true ;;
|
||||
--phase1-only) PHASE1_ONLY=true ;;
|
||||
--phase2-only) PHASE2_ONLY=true; SKIP_SSH=true ;;
|
||||
--phase3-only) PHASE3_ONLY=true; SKIP_SSH=true ;;
|
||||
--no-arm) SKIP_ARM=true ;;
|
||||
*) FILTERED_ARGS+=("$arg") ;;
|
||||
esac
|
||||
@@ -474,8 +476,124 @@ MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
|
||||
log "Mirror: $MIRROR ($MIRROR_IP)"
|
||||
[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)"
|
||||
[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)"
|
||||
[[ "$PHASE3_ONLY" == true ]] && log "Mode: Phase 3 only (media seed)"
|
||||
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_NETWORK_OK=false
|
||||
LOCAL_SETUP_OK=true # partnership_manager --local-only; the summary claimed done ✅ regardless
|
||||
@@ -1122,78 +1240,6 @@ fi
|
||||
# ── Write Phase 2 completion state ────────────────────────────────────────────────────────────
|
||||
[[ "$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 ───────────────────────────────────────────────────────────────────────────────────
|
||||
END=$(date +%s)
|
||||
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 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 ""
|
||||
|
||||
if [[ "$ONBOARD_OK" == true ]]; then
|
||||
|
||||
@@ -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
|
||||
// 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';
|
||||
$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');
|
||||
$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);
|
||||
if (!is_array($j)) return ['enabled' => $enabled];
|
||||
if (!is_array($j)) return ['enabled' => $enabled, 'phase3_done' => $phase3Done];
|
||||
|
||||
$status = (string)($j['status'] ?? '');
|
||||
$pid = (int)($j['pid'] ?? 0);
|
||||
@@ -293,6 +299,7 @@ function vv_pt_media_seed(): array {
|
||||
|
||||
return array_filter([
|
||||
'enabled' => $enabled,
|
||||
'phase3_done' => $phase3Done,
|
||||
'status' => $status,
|
||||
'start' => isset($j['start']) ? (int)$j['start'] : null,
|
||||
'end' => isset($j['end']) ? (int)$j['end'] : null,
|
||||
@@ -433,7 +440,7 @@ function vv_pt_nodes(): array {
|
||||
'api_key_set' => $apiKeySet,
|
||||
'api_key_preview' => $apiKeyPreview,
|
||||
'metrics' => $metrics,
|
||||
'media_seed' => $isMe ? [] : vv_pt_media_seed(),
|
||||
'media_seed' => $isMe ? [] : vv_pt_media_seed($setupDb, $nodeIdUpper),
|
||||
];
|
||||
}
|
||||
return $nodes;
|
||||
|
||||
@@ -381,27 +381,23 @@ 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; }
|
||||
// Phase 3 — the media seed, as its own phase rather than the tail of an onboard. Routed through
|
||||
// partnership_onboard.sh --phase3-only rather than straight at media_seed.sh, because the phase
|
||||
// is more than the transfer: it refuses when Phase 2 is unfinished, and it sets the gate posture
|
||||
// the seed needs (Tier 1 open so rsync.sh will move anything, every Tier 2 gate closed so the
|
||||
// scheduled orchestrators are not fighting it for the link for the next several weeks).
|
||||
async function vvPtPhase3(btn, id) {
|
||||
const label = btn.textContent;
|
||||
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;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Starting…';
|
||||
_vvPtRun('Partnership/partnership_onboard.sh', '--phase3-only')
|
||||
.then(d => { if (!d.ok) vvAlert('Phase 3 did not start: ' + (d.error ?? 'Unknown error')); })
|
||||
.catch(e => vvAlert('Error: ' + e))
|
||||
.finally(() => setTimeout(() => {
|
||||
btn.disabled = false; btn.textContent = label;
|
||||
if (typeof vvPtLoad === 'function') vvPtLoad();
|
||||
} catch (e) {
|
||||
btn.disabled = false; btn.textContent = '▶ Seed';
|
||||
vvAlert('Error: ' + e);
|
||||
}
|
||||
}, 3000));
|
||||
}
|
||||
|
||||
async function vvPtStopSeed(btn) {
|
||||
@@ -1178,36 +1174,42 @@ function _renderActions(nodes, cfg) {
|
||||
// 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.
|
||||
// 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 || {};
|
||||
if (ms.enabled !== false) {
|
||||
const seedRunning = ms.status === 'running';
|
||||
const seedDone = ms.phase3_done === true;
|
||||
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'; }
|
||||
if (seedRunning) { seedColor = '#ff9800'; seedLabel = 'Phase 3 — seeding'; }
|
||||
else if (seedDone) { seedColor = '#4caf50'; seedLabel = 'Phase 3 complete'; }
|
||||
else if (ms.status === 'stopped') { seedColor = '#666'; seedLabel = 'Phase 3 stopped'; }
|
||||
else if (ms.status === 'ok') { seedColor = '#ff9800'; seedLabel = 'Phase 3 partial'; }
|
||||
else if (ms.status) { seedColor = '#f44336'; seedLabel = 'Phase 3 failed'; }
|
||||
else { seedColor = '#555'; seedLabel = 'Phase 3 not started'; }
|
||||
|
||||
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>` : ''}
|
||||
${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)"
|
||||
: `<button class="vv-pt-action-btn info" onclick="vvPtPhase3(this,'${remote.id}')"
|
||||
style="font-size:9px;opacity:.55;margin-left:auto;"
|
||||
title="Push every DAILY_SYNC_SHARES entry to ${remote.id}">▶ Seed</button>`)}
|
||||
title="Seed every DAILY_SYNC_SHARES entry to ${remote.id}">${seedDone ? '↻ Re-seed' : '▶ Phase 3'}</button>`}
|
||||
</div>
|
||||
${showLine ? `<div style="margin-top:3px;font-size:9px;color:#2e2e2e;font-family:monospace;
|
||||
${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>` : ''}
|
||||
>${vvEscHtml(ms.line)}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete Keys panel ──────────────────────────────────────────────────
|
||||
if (isDeleting) {
|
||||
|
||||
+18
-1
@@ -174,5 +174,22 @@ if [[ "$SEEDED" -eq 0 ]]; then
|
||||
error "No shares seeded"
|
||||
exit 1
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user