'UNKNOWN', 'fallback_start' => 0, 'handback_strikes' => 0, 'tier2_started' => false, 'tier3_started' => false, 'tier4_started' => false, 'partnership_suspended' => false, 'partner_lost_at' => 0, ]; foreach (explode("\n", $text) as $line) { $line = trim($line); if (!$line || !str_contains($line, '=')) continue; [$k, $v] = array_pad(explode('=', $line, 2), 2, ''); $k = trim($k); $v = trim($v, '"\''); switch ($k) { case 'state': $out['state'] = $v; break; case 'fallback_start': $out['fallback_start'] = (int)$v; break; case 'handback_strikes': $out['handback_strikes'] = (int)$v; break; case 'tier2_started': $out['tier2_started'] = $v === 'true'; break; case 'tier3_started': $out['tier3_started'] = $v === 'true'; break; case 'tier4_started': $out['tier4_started'] = $v === 'true'; break; case 'partnership_suspended': $out['partnership_suspended'] = $v === 'true'; break; case 'partner_lost_at': $out['partner_lost_at'] = (int)$v; break; } } return $out; } function vv_fb_local_state(): array { $path = STATE_DIR . '/fallback_state.db'; return vv_fb_parse_state(file_exists($path) ? file_get_contents($path) : ''); } function vv_fb_remote_state(string $ip, string $sshKey): array { $out = vv_pt_ssh($ip, $sshKey, vv_remote_state_cmd('fallback_state.db')); return vv_fb_parse_state($out); } // ── Running containers ──────────────────────────────────────────────────────── function vv_fb_local_running(): array { $out = shell_exec("docker ps --format '{{.Names}}' 2>/dev/null") ?: ''; return array_values(array_filter(explode("\n", trim($out)))); } function vv_fb_remote_running(string $ip, string $sshKey): array { $out = vv_pt_ssh($ip, $sshKey, "docker ps --format '{{.Names}}' 2>/dev/null"); return array_values(array_filter(explode("\n", trim($out)))); } // ── Covers — what a node runs for the other when it's down ─────────────────── function vv_fb_covers(string $covering, string $remote, string $coveringRaw, string $remoteRaw): array { $ru = strtoupper($remote); // HOST2 — covered host owns its own tier lists $tiers = []; for ($t = 1; $t <= 4; $t++) { $tiers["tier$t"] = vv_fb_bash_array($remoteRaw, "FALLBACK_{$ru}_TIER{$t}"); } // Delays: how long the remote (covered) host must be down before each tier fires. // Stored in the *remote* host's conf as REMOTE_TIER*_DELAY. $tiers['delays'] = [ 'tier2' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER2_DELAY") ?: 240), 'tier3' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER3_DELAY") ?: 720), 'tier4' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER4_DELAY") ?: 1440), ]; return $tiers; } // ── Known hosts ─────────────────────────────────────────────────────────────── function vv_fb_known_hosts(): array { return vv_known_hosts(); } // ── Main data builder ───────────────────────────────────────────────────────── function vv_fb_all(): array { $currentHost = vv_detect_host(); $hosts = vv_fb_known_hosts(); $tsPeers = vv_pt_ts_peers(); $masterRaw = vv_read_conf_raw('master.conf'); $handbackReq = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_HANDBACK_STRIKES') ?: 3); $fbEnabled = vv_fb_scalar($masterRaw, 'FALLBACK_ENABLED') === 'true'; $ptEnabled = vv_fb_scalar($masterRaw, 'PARTNERSHIP_ENABLED') === 'true'; $rsyncEnabled = vv_fb_scalar($masterRaw, 'FALLBACK_RSYNC_ENABLED') !== 'false'; $checkInterval = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_CHECK_INTERVAL') ?: 30); $suspendAfter = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_PARTNERSHIP_SUSPEND_AFTER') ?: 120); // Read all host conf raws upfront $raws = []; foreach (array_keys($hosts) as $slot) { $raws[$slot] = vv_read_conf_raw($slot . '.conf'); } // SSH key — from local host conf $myId = strtoupper($currentHost); $myRaw = $raws[$currentHost] ?? ''; $mySshKey = vv_fb_scalar($myRaw, $myId . '_SSH_KEY'); $nodes = []; foreach ($hosts as $slot => $hostname) { $isMe = ($slot === $currentHost || $currentHost === 'unknown'); $tsLabel = strtolower($hostname); $ts = $tsPeers[$tsLabel] ?? ['online' => null, 'active' => false, 'ip' => null]; $ip = $ts['ip'] ?? null; // State if ($isMe) { $state = vv_fb_local_state(); } elseif ($ip && $mySshKey) { $state = vv_fb_remote_state($ip, $mySshKey); } else { $state = vv_fb_parse_state(''); $state['state'] = $ts['online'] === false ? 'OFFLINE' : 'UNREACHABLE'; } // Running containers if ($isMe) { $running = vv_fb_local_running(); } elseif ($ip && $mySshKey && $ts['online']) { $running = vv_fb_remote_running($ip, $mySshKey); } else { $running = []; } // Covers: for a 2-node setup, each covers the other // For N nodes this would need a different approach — for now, assume 2-node $covers = null; foreach ($hosts as $otherSlot => $otherHostname) { if ($otherSlot === $slot) continue; $coveringRaw = $raws[$slot] ?? ''; $remoteRaw = $raws[$otherSlot] ?? ''; $covers = [ 'slot' => $otherSlot, 'id' => strtoupper($otherSlot), 'hostname' => $otherHostname, ] + vv_fb_covers($slot, $otherSlot, $coveringRaw, $remoteRaw); break; // 2-node only } $nodes[] = [ 'slot' => $slot, 'id' => strtoupper($slot), 'hostname' => $hostname, 'is_me' => $isMe, 'ts_online' => $ts['online'], 'state' => $state, 'running' => $running, 'covers' => $covers, ]; } return [ 'ts' => time(), 'fb_enabled' => $fbEnabled, 'partnership_enabled' => $ptEnabled, 'fb_rsync_enabled' => $rsyncEnabled, 'handback_req' => $handbackReq, 'check_interval' => $checkInterval, 'suspend_after' => $suspendAfter, 'nodes' => $nodes, ]; }