diff --git a/Plugin/unraid/include/fallback.php b/Plugin/unraid/include/fallback.php
index db7a612..a592946 100644
--- a/Plugin/unraid/include/fallback.php
+++ b/Plugin/unraid/include/fallback.php
@@ -356,6 +356,15 @@ function vv_fb_all(): array {
'handback_req' => $handbackReq,
'check_interval' => $checkInterval,
'suspend_after' => $suspendAfter,
+ // Tier delays are per-host and live in this host's own conf, not master.conf — the save
+ // path has to name the right file per field or the write is refused as an absent key.
+ 'my_conf' => vv_detect_host() . '.conf',
+ 'tier_delays' => [
+ 'tier2' => (int)(vv_fb_scalar($myRaw, strtoupper($currentHost) . '_TIER2_DELAY') ?: 240),
+ 'tier3' => (int)(vv_fb_scalar($myRaw, strtoupper($currentHost) . '_TIER3_DELAY') ?: 720),
+ 'tier4' => (int)(vv_fb_scalar($myRaw, strtoupper($currentHost) . '_TIER4_DELAY') ?: 1440),
+ ],
+ 'external_ip' => vv_fb_scalar($masterRaw, 'EXTERNAL_IP'),
'nodes' => $nodes,
];
}
diff --git a/Plugin/unraid/pages/fallback.php b/Plugin/unraid/pages/fallback.php
index 2a4afef..769d0f2 100644
--- a/Plugin/unraid/pages/fallback.php
+++ b/Plugin/unraid/pages/fallback.php
@@ -205,26 +205,6 @@ if (vv_ai_ui_on()) vv_ai_chat_assets();
Loading…
-
-
-
- 'varaverk',
- 'compact' => true,
- // Fallback is the mesh's business by definition — a failover is two hosts agreeing about
- // each other — so the mesh side leads here, as it does on Partnership.
- 'mesh' => true,
- 'meshDefault' => true,
- 'title' => 'Assistant',
- 'height' => '300px',
- 'tall' => '500px',
- 'tallLarge' => '750px',
- 'empty' => 'Ask about fallback — why a tier has not fired, what the partner would '
- . 'start if this host went dark, whether a stale state file matters.',
- ]); ?>
-
-
-
+
+
Tier 2 delay
+
How long this host must be dark before the partner starts its Tier 2 containers.
+
+
+ minutes
+
+
+
+
+
Tier 3 delay
+
Same for Tier 3 — secondary services that can wait out a short outage.
+
+
+ minutes
+
+
+
+
+
Tier 4 delay
+
Same for Tier 4 — the last to move, usually arrs and downloaders.
+
+
+ minutes
+
+
+
+
+
+
Internet probe target
+
Pinged to tell "the partner is down" apart from "our own line is down". Must be reachable from both hosts.
+
+
+
+
+
+
+
+ 'varaverk',
+ 'compact' => true,
+ // Fallback is the mesh's business by definition — a failover is two hosts agreeing about
+ // each other — so the mesh side leads here, as it does on Partnership.
+ 'mesh' => true,
+ 'meshDefault' => true,
+ 'title' => 'Assistant',
+ 'height' => '300px',
+ 'tall' => '500px',
+ 'tallLarge' => '750px',
+ 'empty' => 'Ask about fallback — why a tier has not fired, what the partner would '
+ . 'start if this host went dark, whether a stale state file matters.',
+ ]); ?>
+
+
+
{
const el = document.getElementById(id);
if (el && el !== document.activeElement) el.value = val;
@@ -734,28 +783,45 @@ window.vvFbToggle = function(track, key) {
.catch(() => track.classList.toggle('on', !on));
};
+// Which conf each field belongs to. Tier delays are per-host and live in host.conf; the rest
+// are mesh-wide in master.conf. confform refuses a key it cannot find in the named file rather
+// than appending it into an unknown section, so getting this wrong fails the save outright —
+// which is the right failure, but only if the mapping is stated rather than assumed.
+let _vvFbMyConf = 'host1.conf';
+
window.vvFbSaveSettings = function() {
- const ivEl = document.getElementById('vv-fb-interval');
- const stEl = document.getElementById('vv-fb-strikes');
- const suEl = document.getElementById('vv-fb-suspend');
- const fb = document.getElementById('vv-fb-set-fb');
- const btn = document.getElementById('vv-fb-save-btn');
+ const num = id => parseInt(document.getElementById(id).value, 10);
+ const fb = document.getElementById('vv-fb-set-fb');
+ const btn = document.getElementById('vv-fb-save-btn');
- const interval = parseInt(ivEl.value, 10);
- const strikes = parseInt(stEl.value, 10);
- const suspend = parseInt(suEl.value, 10);
+ const interval = num('vv-fb-interval'), strikes = num('vv-fb-strikes'), suspend = num('vv-fb-suspend');
+ const t2 = num('vv-fb-t2'), t3 = num('vv-fb-t3'), t4 = num('vv-fb-t4');
+ const extip = (document.getElementById('vv-fb-extip').value || '').trim();
- if ([interval, strikes, suspend].some(n => isNaN(n) || n < 0)) {
+ if ([interval, strikes, suspend, t2, t3, t4].some(n => isNaN(n) || n < 0)) {
fb.style.color = '#ef5350'; fb.textContent = 'Invalid values'; return;
}
+ // Tiers fire in order, and a later tier that fires sooner than an earlier one is a config that
+ // cannot do what it reads as. Caught here rather than at 3am during an outage.
+ if (!(t2 <= t3 && t3 <= t4)) {
+ fb.style.color = '#ef5350'; fb.textContent = 'Tier delays must increase: T2 ≤ T3 ≤ T4'; return;
+ }
+ if (extip === '') {
+ fb.style.color = '#ef5350'; fb.textContent = 'Probe target cannot be empty'; return;
+ }
btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = '';
+ const H = _vvFbMyConf.replace('.conf', '').toUpperCase();
const fd = new URLSearchParams();
fd.append('id', 'fallback');
fd.append('changes', JSON.stringify([
- { file: 'master.conf', key: 'FALLBACK_CHECK_INTERVAL', value: String(interval), type: 'scalar' },
- { file: 'master.conf', key: 'FALLBACK_HANDBACK_STRIKES', value: String(strikes), type: 'scalar' },
- { file: 'master.conf', key: 'FALLBACK_PARTNERSHIP_SUSPEND_AFTER', value: String(suspend), type: 'scalar' },
+ { file: 'master.conf', key: 'FALLBACK_CHECK_INTERVAL', value: String(interval), type: 'scalar' },
+ { file: 'master.conf', key: 'FALLBACK_HANDBACK_STRIKES', value: String(strikes), type: 'scalar' },
+ { file: 'master.conf', key: 'FALLBACK_PARTNERSHIP_SUSPEND_AFTER', value: String(suspend), type: 'scalar' },
+ { file: 'master.conf', key: 'EXTERNAL_IP', value: extip, type: 'scalar' },
+ { file: _vvFbMyConf, key: H + '_TIER2_DELAY', value: String(t2), type: 'scalar' },
+ { file: _vvFbMyConf, key: H + '_TIER3_DELAY', value: String(t3), type: 'scalar' },
+ { file: _vvFbMyConf, key: H + '_TIER4_DELAY', value: String(t4), type: 'scalar' },
]));
fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -763,7 +829,7 @@ window.vvFbSaveSettings = function() {
btn.disabled = false; btn.textContent = 'Save';
fb.style.color = d.ok ? '#4caf50' : '#ef5350';
fb.textContent = d.ok ? 'Saved ✓' : (d.error || 'Failed');
- if (d.ok) setTimeout(() => { fb.textContent = ''; }, 3000);
+ if (d.ok) { setTimeout(() => { fb.textContent = ''; }, 3000); vvFbLoad(); }
})
.catch(() => {
btn.disabled = false; btn.textContent = 'Save';
@@ -819,7 +885,11 @@ function _vvFbCovRender() {
`;
}).join('');
- body.innerHTML = `
${rows}
`;
+ // Three columns. One column of forty-nine rows is a scroll, not a list — you cannot see what
+ // is covered without dragging, which is the one thing this card exists to show.
+ body.innerHTML = '
'
+ + rows + '
';
}
// Tier 0 means "not covered" — the picker's way of removing something, since an array the