Partnership page: Varaverk assistant profile, row-2 Actions, and the two container list cards
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// The two container lists the Partnership page edits:
|
||||
//
|
||||
// cover FALLBACK_<me>_TIER1-4 in this host's own conf — which of this host's containers
|
||||
// the partner starts when this host goes dark, and in which delay tier.
|
||||
// services HOST<n>_PARTNERSHIP_SERVICES_STACK — XML templates pushed to the mirror during
|
||||
// onboard, for services that are neither auth nor arr.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// GET returns this host's docker containers plus the current membership of both lists.
|
||||
// POST action=cover tiers=<json {container: tier}> rewrites all four tier arrays
|
||||
// POST action=services stack=<json [container, …]> rewrites the services stack
|
||||
//
|
||||
// Both write through vv_conf_edit(), the same locked read-modify-write every other conf
|
||||
// endpoint uses, and both push the result — a partner holding the old list is a partner that
|
||||
// will act on the old list.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Edit the array fallback.sh actually reads, not a parallel one.
|
||||
// The coverage tiers already exist and already carry the timing. A second "what to fail
|
||||
// over" list would be a second answer to the same question, and the two would drift.
|
||||
//
|
||||
// The services stack stores XML template filenames, not container names.
|
||||
// That is what onboard pushes. The picker speaks container names because that is what the
|
||||
// operator recognises, and the mapping to my-<Name>.xml happens here, once.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only for writes, so Unraid's CSRF guard applies. See README-unraid.md.
|
||||
//
|
||||
// Names are validated against the containers this host actually runs. A tier or stack entry
|
||||
// for a container that does not exist here is a line fallback.sh will fail on during an
|
||||
// outage, which is the worst possible time to discover a typo.
|
||||
//
|
||||
// A tier value outside 1-4 is rejected rather than clamped — silently moving a container from
|
||||
// tier 9 to tier 4 would give it a 24-hour delay nobody asked for.
|
||||
//
|
||||
// Writing an empty list is allowed. "Cover nothing" is a legitimate choice and the only way to
|
||||
// express it.
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/confform.php vv_conf_edit()
|
||||
// include/docker.php vv_docker_containers()
|
||||
// include/config.php vv_detect_host(), vv_read_conf_raw(), vv_push_master_conf()
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/confform.php';
|
||||
require_once dirname(__DIR__) . '/include/docker.php';
|
||||
|
||||
$hostId = vv_detect_host(); // host1 / host2
|
||||
$hostUp = strtoupper($hostId); // HOST1
|
||||
$myConf = $hostId . '.conf';
|
||||
|
||||
$TIERS = [1, 2, 3, 4];
|
||||
$tierVar = fn(int $t) => "FALLBACK_{$hostUp}_TIER{$t}";
|
||||
$svcVar = "{$hostUp}_PARTNERSHIP_SERVICES_STACK";
|
||||
|
||||
// Container name ⇄ template filename. Unraid writes my-<Name>.xml; the list stores that.
|
||||
$toXml = fn(string $c) => 'my-' . $c . '.xml';
|
||||
$fromXml = fn(string $x) => preg_replace('/^my-|\.xml$/', '', trim($x));
|
||||
|
||||
// ── Read ─────────────────────────────────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$raw = vv_read_conf_raw($myConf);
|
||||
$mRaw = vv_read_conf_raw('master.conf');
|
||||
|
||||
$cover = [];
|
||||
foreach ($TIERS as $t) {
|
||||
// Tiers live in host*.conf on this host; fall back to master.conf for installs that
|
||||
// kept them there. Reading both costs nothing and avoids an empty card on either shape.
|
||||
$vals = vv_parse_conf_list($raw, $tierVar($t)) ?: vv_parse_conf_list($mRaw, $tierVar($t));
|
||||
foreach ($vals as $c) { $c = trim($c); if ($c !== '') $cover[$c] = $t; }
|
||||
}
|
||||
|
||||
$services = [];
|
||||
foreach (vv_parse_conf_list($raw, $svcVar) as $x) {
|
||||
$n = $fromXml($x);
|
||||
if ($n !== '') $services[] = $n;
|
||||
}
|
||||
|
||||
$containers = [];
|
||||
foreach (vv_docker_containers() as $c) {
|
||||
$n = is_array($c) ? ($c['name'] ?? '') : (string)$c;
|
||||
if ($n !== '') $containers[] = $n;
|
||||
}
|
||||
sort($containers, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
// Templates that exist, so the services picker can say which choices onboard could actually
|
||||
// push. A container with no XML cannot be deployed to the partner however it is ticked.
|
||||
$haveXml = [];
|
||||
foreach (glob('/boot/config/plugins/dockerMan/templates-user/my-*.xml') ?: [] as $p) {
|
||||
$haveXml[] = $fromXml(basename($p));
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'host' => $hostUp,
|
||||
'containers' => $containers,
|
||||
'have_xml' => $haveXml,
|
||||
'cover' => (object)$cover,
|
||||
'services' => $services,
|
||||
'tier_vars' => array_map($tierVar, $TIERS),
|
||||
'svc_var' => $svcVar,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Write ────────────────────────────────────────────────────────────────────────────────────
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
$known = [];
|
||||
foreach (vv_docker_containers() as $c) {
|
||||
$n = is_array($c) ? ($c['name'] ?? '') : (string)$c;
|
||||
if ($n !== '') $known[strtolower($n)] = $n;
|
||||
}
|
||||
|
||||
// Rewrites one `NAME=(` … `)` block in place, preserving the leading indent the conf uses.
|
||||
$rewrite = function (string $cur, string $var, array $items): ?string {
|
||||
$body = '';
|
||||
foreach ($items as $i) $body .= " \"" . $i . "\"\n";
|
||||
$pattern = '/^([ \t]*)' . preg_quote($var, '/') . '=\((?:[^)]*)\)/m';
|
||||
if (preg_match($pattern, $cur)) {
|
||||
return preg_replace_callback($pattern,
|
||||
fn($m) => $m[1] . $var . "=(\n" . $body . $m[1] . ")", $cur, 1);
|
||||
}
|
||||
return null; // absent: refuse rather than append into an unknown section
|
||||
};
|
||||
|
||||
if ($action === 'cover') {
|
||||
$map = json_decode((string)($_POST['tiers'] ?? ''), true);
|
||||
if (!is_array($map)) { echo json_encode(['ok' => false, 'error' => 'tiers must be an object']); exit; }
|
||||
|
||||
$byTier = array_fill_keys($TIERS, []);
|
||||
foreach ($map as $name => $tier) {
|
||||
$t = (int)$tier;
|
||||
if (!in_array($t, $TIERS, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => "Tier $tier is not 1-4 (for $name)"]); exit;
|
||||
}
|
||||
if (!isset($known[strtolower((string)$name)])) {
|
||||
echo json_encode(['ok' => false, 'error' => "No container named $name on this host"]); exit;
|
||||
}
|
||||
$byTier[$t][] = $known[strtolower((string)$name)];
|
||||
}
|
||||
|
||||
$ok = vv_conf_edit($myConf, function (string $cur) use ($byTier, $TIERS, $tierVar, $rewrite): ?string {
|
||||
foreach ($TIERS as $t) {
|
||||
$next = $rewrite($cur, $tierVar($t), $byTier[$t]);
|
||||
if ($next === null) return null;
|
||||
$cur = $next;
|
||||
}
|
||||
return $cur;
|
||||
}, [], array_map($tierVar, $TIERS));
|
||||
|
||||
if (!$ok) { echo json_encode(['ok' => false, 'error' => vv_conf_last_error() ?: 'Write failed']); exit; }
|
||||
vv_push_master_conf();
|
||||
echo json_encode(['ok' => true, 'counts' => array_map('count', $byTier)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'services') {
|
||||
$list = json_decode((string)($_POST['stack'] ?? ''), true);
|
||||
if (!is_array($list)) { echo json_encode(['ok' => false, 'error' => 'stack must be an array']); exit; }
|
||||
|
||||
$xml = [];
|
||||
foreach ($list as $name) {
|
||||
if (!isset($known[strtolower((string)$name)])) {
|
||||
echo json_encode(['ok' => false, 'error' => "No container named $name on this host"]); exit;
|
||||
}
|
||||
$real = $known[strtolower((string)$name)];
|
||||
$path = '/boot/config/plugins/dockerMan/templates-user/' . $toXml($real);
|
||||
if (!is_file($path)) {
|
||||
echo json_encode(['ok' => false, 'error' => "No template " . $toXml($real) . " — the partner could not deploy it"]); exit;
|
||||
}
|
||||
$xml[] = $toXml($real);
|
||||
}
|
||||
|
||||
$ok = vv_conf_edit($myConf, fn(string $cur): ?string => $rewrite($cur, $svcVar, $xml), [], [$svcVar]);
|
||||
if (!$ok) { echo json_encode(['ok' => false, 'error' => vv_conf_last_error() ?: 'Write failed']); exit; }
|
||||
vv_push_master_conf();
|
||||
echo json_encode(['ok' => true, 'count' => count($xml)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown action']);
|
||||
@@ -120,21 +120,47 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
|
||||
<div style="color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
|
||||
</div>
|
||||
|
||||
<!-- Data moved between the servers -->
|
||||
<div class="vv-card" id="vv-pt-xfer-card" style="margin-bottom:12px;">
|
||||
<!-- Row 2 — what has moved, and what you can do about it -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:12px;align-items:stretch;flex-wrap:wrap;">
|
||||
<div class="vv-card" id="vv-pt-xfer-card" style="flex:1 1 260px;min-width:0;display:flex;flex-direction:column;">
|
||||
<h3>Data Transferred</h3>
|
||||
<div id="vv-pt-xfer-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||
<div id="vv-pt-xfer-body" style="flex:1;color:#444;font-size:12px;">Loading…</div>
|
||||
</div>
|
||||
<div class="vv-card" id="vv-pt-actions-card" style="flex:2 1 340px;min-width:0;">
|
||||
<h3>Actions</h3>
|
||||
<div id="vv-pt-actions-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mirror sync + Actions side by side -->
|
||||
<!-- Row 3 — sync state, and the two lists that decide what the partner carries -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:12px;align-items:stretch;flex-wrap:wrap;">
|
||||
<div class="vv-card" id="vv-pt-sync-card" style="flex:1 1 220px;min-width:0;display:flex;flex-direction:column;">
|
||||
<h3>Mirror Sync</h3>
|
||||
<div id="vv-pt-sync-body" style="flex:1;color:#444;font-size:12px;">Loading…</div>
|
||||
</div>
|
||||
<div class="vv-card" id="vv-pt-actions-card" style="flex:2 1 300px;min-width:0;">
|
||||
<h3>Actions</h3>
|
||||
<div id="vv-pt-actions-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||
|
||||
<!-- Failover coverage: which of THIS host's containers the partner starts when this host is
|
||||
down, and after how long. Backed by FALLBACK_<me>_TIER1-4 in this host's own conf, which
|
||||
is what fallback.sh on the partner actually reads — so the picker edits the real list
|
||||
rather than a second one that could disagree with it. -->
|
||||
<div class="vv-card" id="vv-pt-cover-card" style="flex:1 1 300px;min-width:0;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 style="margin:0;">Failover Coverage</h3>
|
||||
<button id="vv-pt-cover-save" class="vv-pt-action-btn run"
|
||||
onclick="vvPtSaveCover(this)" style="display:none;font-size:11px;">Save</button>
|
||||
</div>
|
||||
<div id="vv-pt-cover-body" style="flex:1;color:#444;font-size:12px;margin-top:8px;">Loading…</div>
|
||||
</div>
|
||||
|
||||
<!-- Services deployed to the partner that are neither auth nor arr. These are pushed as XML
|
||||
templates during onboard, so they run there rather than only starting during an outage. -->
|
||||
<div class="vv-card" id="vv-pt-svc-card" style="flex:1 1 300px;min-width:0;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 style="margin:0;">Shared Services</h3>
|
||||
<button id="vv-pt-svc-save" class="vv-pt-action-btn run"
|
||||
onclick="vvPtSaveSvc(this)" style="display:none;font-size:11px;">Save</button>
|
||||
</div>
|
||||
<div id="vv-pt-svc-body" style="flex:1;color:#444;font-size:12px;margin-top:8px;">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +168,7 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
|
||||
<?php if (vv_ai_ui_on()): ?>
|
||||
<div class="vv-card" id="vv-pt-ai-card" style="margin-bottom:12px;">
|
||||
<?php vv_ai_chat_markup('vv-pt-ai', [
|
||||
'profile' => 'chat',
|
||||
'profile' => 'varaverk',
|
||||
'compact' => true,
|
||||
'title' => 'Assistant',
|
||||
'height' => '300px',
|
||||
@@ -1459,8 +1485,10 @@ setInterval(vvPtLoad, 10000);
|
||||
if (typeof VvAiChat === 'function' && document.getElementById('vv-pt-ai-chat')) {
|
||||
VvAiChat({
|
||||
prefix: 'vv-pt-ai',
|
||||
profile: 'chat',
|
||||
resumeProfile: 'chat',
|
||||
// The Varaverk assistant, not general chat: every question asked on this page is about this
|
||||
// installation, and the strict profile is the one that can answer from it.
|
||||
profile: 'varaverk',
|
||||
resumeProfile: 'varaverk',
|
||||
empty: 'Ask about the partnership — sync state, fallback tiers, what the mirror is running, '
|
||||
+ 'why a transfer moved nothing.',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user