Let a non-blocking checklist item be dismissed with a recorded decision, and run discovery on the mirror once there is something to discover
This commit is contained in:
@@ -40,6 +40,8 @@
|
|||||||
# prevents arrs treating every file as missing after bootstrap
|
# prevents arrs treating every file as missing after bootstrap
|
||||||
# Step 9e: Webhook listener — start listener on mirror (runs continuously, no reboot needed)
|
# Step 9e: Webhook listener — start listener on mirror (runs continuously, no reboot needed)
|
||||||
# Step 10: Conf push — push master.conf + setup state to all listed hosts
|
# Step 10: Conf push — push master.conf + setup state to all listed hosts
|
||||||
|
# Step 11: Service discovery — conf_populate.sh on the mirror, last, once the stacks it
|
||||||
|
# would discover are actually deployed there
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# DESIGN PRINCIPLES
|
# DESIGN PRINCIPLES
|
||||||
@@ -1008,6 +1010,40 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Step 11: Service discovery on the mirror ──────────────────────────────────────────────────
|
||||||
|
# Deliberately last. conf_populate.sh fills host*.conf from what it can actually find running —
|
||||||
|
# arr API keys, container names, URLs — and until Step 3 and Step 5 deployed the auth and arr
|
||||||
|
# stacks there was nothing on the mirror to find. The wizard runs it during first-run setup,
|
||||||
|
# which on a fresh mirror is precisely the moment the machine is still empty, so everything it
|
||||||
|
# could have discovered was discovered as absent.
|
||||||
|
#
|
||||||
|
# No --overwrite: it only fills blanks, so anything the operator set by hand survives. --no-push
|
||||||
|
# because Step 10 above has just pushed conf; letting discovery push again would race it.
|
||||||
|
echo ""
|
||||||
|
echo "━━━ $ICON_GEAR Step 11 — Service Discovery ($MIRROR) ━━━"
|
||||||
|
|
||||||
|
POPULATE_OK=false
|
||||||
|
if [[ "$MIRROR_REACHABLE" != true ]]; then
|
||||||
|
warn "$MIRROR unreachable — skipping discovery, run Deployment/conf_populate.sh there later"
|
||||||
|
POPULATE_OK=skipped
|
||||||
|
elif [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would run conf_populate.sh --no-push on $MIRROR"
|
||||||
|
POPULATE_OK=true
|
||||||
|
else
|
||||||
|
_mirror_sd=$(resolve_remote_scripts_dir "$MIRROR_IP" "$MIRROR_SSH_KEY" "no")
|
||||||
|
_pop_script="${_mirror_sd}/Deployment/conf_populate.sh"
|
||||||
|
if timeout 180 ssh -i "$MIRROR_SSH_KEY" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||||
|
root@"$MIRROR_IP" \
|
||||||
|
"[ -f '$_pop_script' ] || { echo missing; exit 127; }; bash '$_pop_script' --no-push" 2>/dev/null; then
|
||||||
|
echo "Discovery complete on $MIRROR ✅"
|
||||||
|
POPULATE_OK=true
|
||||||
|
else
|
||||||
|
warn "Discovery failed on $MIRROR — run $_pop_script there by hand"
|
||||||
|
fi
|
||||||
|
unset _mirror_sd _pop_script
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1040,6 +1076,7 @@ echo " Step 9d — Media seed: $( [[ "$SKIP_MEDIA_SEED" == true ]] && ech
|
|||||||
|| echo "${MEDIA_SEED_COUNT}/${#DAILY_SYNC_SHARES[@]} shares $(_ok "$MEDIA_SEED_OK")"; } )"
|
|| echo "${MEDIA_SEED_COUNT}/${#DAILY_SYNC_SHARES[@]} shares $(_ok "$MEDIA_SEED_OK")"; } )"
|
||||||
echo " Step 9e — Webhook listener: $(_skip "$SKIP_WEBHOOK_LISTENER" "$WEBHOOK_LISTENER_OK")"
|
echo " Step 9e — Webhook listener: $(_skip "$SKIP_WEBHOOK_LISTENER" "$WEBHOOK_LISTENER_OK")"
|
||||||
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 ""
|
echo ""
|
||||||
|
|
||||||
if [[ "$ONBOARD_OK" == true ]]; then
|
if [[ "$ONBOARD_OK" == true ]]; then
|
||||||
|
|||||||
@@ -232,6 +232,42 @@ if (!empty($host2)) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$allOk = !in_array(false, array_column($items, 'ok'), true);
|
// ── Blocking vs deferrable ────────────────────────────────────────────────────
|
||||||
|
// Setup used to be complete only when every item was green, which made a media-server API key a
|
||||||
|
// gate on finishing onboarding. A mirror that runs no Emby, or runs one the operator has not got
|
||||||
|
// round to configuring, could never reach "complete" — and the checklist is what tells them
|
||||||
|
// whether they are done.
|
||||||
|
//
|
||||||
|
// Blocking items are the ones the mesh genuinely cannot work without: who this host is, its conf,
|
||||||
|
// its SSH key, the owner's master.conf, and the partnership itself. Everything else improves the
|
||||||
|
// install without being load-bearing, and can be dismissed with a recorded decision.
|
||||||
|
//
|
||||||
|
// Deferring is per item and reversible, stored in the setup state so it survives a reload. The
|
||||||
|
// item still shows — amber, "deferred" — rather than disappearing, because a dismissed item is a
|
||||||
|
// decision to revisit, not a thing that stopped being true.
|
||||||
|
$deferrable = ['api_key' => true, 'populated' => true, 'emby_key' => true, 'jellyfin_key' => true];
|
||||||
|
$state = vv_setup_state_read();
|
||||||
|
|
||||||
|
foreach ($items as &$item) {
|
||||||
|
$canDefer = !empty($deferrable[$item['id']]);
|
||||||
|
$item['blocking'] = !$canDefer;
|
||||||
|
$item['deferred'] = $canDefer && !$item['ok']
|
||||||
|
&& !empty($state['DEFER_' . strtoupper($item['id'])]);
|
||||||
|
if ($item['deferred']) {
|
||||||
|
$item['detail'] = ($item['detail'] ?? '') . ' — deferred';
|
||||||
|
$item['action'] = 'undefer';
|
||||||
|
} elseif ($canDefer && !$item['ok']) {
|
||||||
|
// Keep the item's real action as the primary; the UI offers defer alongside it.
|
||||||
|
$item['can_defer'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
// Complete when every blocking item is green and every deferrable one is green or dismissed.
|
||||||
|
$allOk = true;
|
||||||
|
foreach ($items as $i) {
|
||||||
|
if ($i['ok']) continue;
|
||||||
|
if ($i['blocking'] || empty($i['deferred'])) { $allOk = false; break; }
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode(['ok' => true, 'complete' => $allOk, 'host_id' => $hostId, 'items' => $items]);
|
echo json_encode(['ok' => true, 'complete' => $allOk, 'host_id' => $hostId, 'items' => $items]);
|
||||||
|
|||||||
@@ -195,6 +195,39 @@ if ($action === 'populate') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── POST: defer / undefer a non-blocking checklist item ───────────────────────────────────────
|
||||||
|
// Records the operator's "not now" so the checklist can reach complete without the item being
|
||||||
|
// green. Only the ids api/checklist.php marks deferrable are accepted — a blocking item cannot be
|
||||||
|
// dismissed, because dismissing it would report a mesh as ready when it cannot function.
|
||||||
|
if ($action === 'defer' || $action === 'undefer') {
|
||||||
|
// POST only, and not merely by convention: $action is taken from GET too, and Unraid's CSRF
|
||||||
|
// guard checks POST alone. A GET-reachable mutation here would let any page the operator
|
||||||
|
// visits silently dismiss a checklist item.
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$allowed = ['api_key', 'populated', 'emby_key', 'jellyfin_key'];
|
||||||
|
$item = trim($_POST['item'] ?? '');
|
||||||
|
if (!in_array($item, $allowed, true)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Not a deferrable item: ' . $item]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$state = vv_setup_state_read();
|
||||||
|
$key = 'DEFER_' . strtoupper($item);
|
||||||
|
if ($action === 'defer') {
|
||||||
|
$state[$key] = (string)time(); // when, not just whether — a stale decision shows its age
|
||||||
|
} else {
|
||||||
|
unset($state[$key]);
|
||||||
|
}
|
||||||
|
if (!vv_setup_state_write($state)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Failed to write setup state']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode(['ok' => true, 'item' => $item, 'deferred' => $action === 'defer']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -226,10 +226,14 @@ function vv_setup_state_read(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the setup state file (creates or overwrites).
|
// Write the setup state file (creates or overwrites).
|
||||||
function vv_setup_state_write(array $data): void {
|
// Returns whether the write landed. Was void, so a caller had no way to tell a full flash or a
|
||||||
|
// read-only mount from success — and `if (!vv_setup_state_write(...))` on a void function is
|
||||||
|
// always true, which turns "it worked" into a reported failure. Existing void callers are
|
||||||
|
// unaffected by the added return.
|
||||||
|
function vv_setup_state_write(array $data): bool {
|
||||||
$content = '';
|
$content = '';
|
||||||
foreach ($data as $k => $v) $content .= "$k=$v\n";
|
foreach ($data as $k => $v) $content .= "$k=$v\n";
|
||||||
file_put_contents(VV_SETUP_STATE_FILE, $content);
|
return file_put_contents(VV_SETUP_STATE_FILE, $content) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the setup state file to all remote hosts via scp.
|
// Push the setup state file to all remote hosts via scp.
|
||||||
|
|||||||
@@ -446,16 +446,40 @@ const vvActionHref = {
|
|||||||
onboard: '?tab=partnership',
|
onboard: '?tab=partnership',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// URLSearchParams, not FormData — a FormData fetch POST hangs on this platform with no status
|
||||||
|
// and no server-side trace. The fetch shim in Varaverk.page adds the CSRF header either way.
|
||||||
|
function vvDeferItem(btn, id, defer) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = defer ? 'Deferring…' : 'Restoring…';
|
||||||
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({action: defer ? 'defer' : 'undefer', item: id})
|
||||||
|
}).then(r => r.text()).then(text => {
|
||||||
|
if (!text.trim()) throw new Error('Empty response — request rejected before it reached setup.php');
|
||||||
|
const d = JSON.parse(text);
|
||||||
|
if (!d.ok) throw new Error(d.error || 'Unknown error');
|
||||||
|
vvLoadChecklist();
|
||||||
|
}).catch(e => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = defer ? 'Not now' : 'Undo';
|
||||||
|
vvAlert('Could not update: ' + e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function vvLoadChecklist() {
|
function vvLoadChecklist() {
|
||||||
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
||||||
.then(r => r.json()).then(d => {
|
.then(r => r.json()).then(d => {
|
||||||
const el = document.getElementById('vv-checklist');
|
const el = document.getElementById('vv-checklist');
|
||||||
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
||||||
el.innerHTML = d.items.map(item => {
|
el.innerHTML = d.items.map(item => {
|
||||||
const icon = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
|
// Deferred reads as its own state, not as pass and not as fail: the thing is still not
|
||||||
const iclr = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
|
// done, the operator has said not now, and the row has to keep saying both.
|
||||||
|
const icon = item.deferred ? '–' : (item.ok === null ? '○' : (item.ok ? '✓' : '✗'));
|
||||||
|
const iclr = item.deferred ? '#a80' : (item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66'));
|
||||||
let act = '';
|
let act = '';
|
||||||
if (item.action) {
|
if (item.action === 'undefer') {
|
||||||
|
act = `<div class="vv-cl-act"><button onclick="vvDeferItem(this,'${item.id}',false)">Undo</button></div>`;
|
||||||
|
} else if (item.action) {
|
||||||
const lbl = vvActionLabels[item.action] || item.action;
|
const lbl = vvActionLabels[item.action] || item.action;
|
||||||
const href = vvActionHref[item.action];
|
const href = vvActionHref[item.action];
|
||||||
if (href) {
|
if (href) {
|
||||||
@@ -471,6 +495,12 @@ function vvLoadChecklist() {
|
|||||||
+ `<div id="vv-ssh-out" class="vv-cl-err"></div></div>`;
|
+ `<div id="vv-ssh-out" class="vv-cl-err"></div></div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Offered alongside the real action, never instead of it — "not now" is a second choice
|
||||||
|
// on a row that still tells you how to do the thing.
|
||||||
|
if (item.can_defer) {
|
||||||
|
act += `<div class="vv-cl-act"><button onclick="vvDeferItem(this,'${item.id}',true)"
|
||||||
|
style="background:#1a1400;border-color:#3a2e00;color:#a80;">Not now</button></div>`;
|
||||||
|
}
|
||||||
return `<div class="vv-cl-item">
|
return `<div class="vv-cl-item">
|
||||||
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
||||||
<div class="vv-cl-body">
|
<div class="vv-cl-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user