<?php
/** @var string $baseUrl @var string $assetsUrl @var array $inv @var array $dest @var string $csrf */
use PpPhp\Core\Brand;
use PpPhp\Core\Migrate\MigrationInventory;
$h = static fn ($s) => htmlspecialchars((string) $s, ENT_QUOTES);
$hb = static fn (int $b) => MigrationInventory::human($b);
$totalBytes = ($inv['platform']['bytes'] ?? 0)
    + array_sum(array_map(static fn ($p) => $p['bytes'], $inv['projects']))
    + array_sum(array_map(static fn ($f) => $f['bytes'], $inv['fileSets']))
    + array_sum(array_map(static fn ($d) => $d['bytes'], $inv['managed']));
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Migrate server · <?= $h(Brand::name()) ?></title>
    <?php include constant('BASE_PATH') . '/core/views/favicon.php'; ?>
    <link href="<?= $h($assetsUrl) ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="<?= $h($assetsUrl) ?>/icons/bootstrap-icons.css" rel="stylesheet">
    <?php include constant('BASE_PATH') . '/core/views/theme-head.php'; ?>
    <style>
        body { background: var(--pp-app-bg, #0b1120); color: var(--pp-text, #e5e7eb); padding-top: 56px; }
        .mig-top { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 1rem; padding: .7rem 1.1rem; background: var(--pp-surface, #141824); border-bottom: 1px solid var(--pp-border, #242a3a); }
        .mig-top .sp { margin-left: auto; }
        .mig-back { display: inline-flex; align-items: center; gap: .35rem; text-decoration: none; color: var(--pp-text); border: 1px solid var(--pp-border, #242a3a); border-radius: 9px; padding: .35rem .7rem; font-size: .85rem; }
        .mig-back:hover { border-color: var(--pp-accent, #818cf8); }
        .mig-wrap { max-width: 1080px; margin: 0 auto; padding: 1.4rem clamp(1rem, 3vw, 2rem) 4rem; }
        .mig-h1 { font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 850; letter-spacing: -.02em; margin: .2rem 0 .2rem; }
        .mig-sub { color: var(--pp-text-muted, #9aa3b2); font-size: .92rem; margin: 0 0 1.3rem; max-width: 70ch; }
        .mig-grid { display: grid; grid-template-columns: 1fr; gap: 1.1rem; }
        @media (min-width: 940px) { .mig-grid { grid-template-columns: 1.15fr .85fr; align-items: start; } }
        .mig-card { background: var(--pp-surface, #141824); border: 1px solid var(--pp-border, #242a3a); border-radius: 16px; padding: 1.15rem 1.25rem; }
        .mig-card h2 { font-size: 1.02rem; font-weight: 800; margin: 0 0 .2rem; display: flex; align-items: center; gap: .55rem; }
        .mig-card h2 .bi { color: var(--pp-accent, #818cf8); }
        .mig-note { color: var(--pp-text-muted, #9aa3b2); font-size: .84rem; margin: .1rem 0 1rem; }
        .mig-item { display: flex; align-items: center; gap: .7rem; padding: .55rem .1rem; border-top: 1px solid var(--pp-border, #242a3a); }
        .mig-item:first-of-type { border-top: 0; }
        .mig-item input[type=checkbox] { width: 17px; height: 17px; flex: 0 0 auto; accent-color: var(--pp-accent, #6366f1); }
        .mig-item .nm { flex: 1 1 auto; min-width: 0; }
        .mig-item .nm b { font-weight: 600; font-size: .9rem; }
        .mig-item .nm small { display: block; color: var(--pp-text-muted, #9aa3b2); font-size: .74rem; }
        .mig-item .sz { flex: 0 0 auto; font-size: .8rem; color: var(--pp-text-muted, #9aa3b2); font-variant-numeric: tabular-nums; }
        .mig-grp { font-size: .66rem; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--pp-text-muted, #9aa3b2); margin: 1rem 0 .2rem; }
        .mig-total { display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; padding-top: .8rem; border-top: 1px dashed var(--pp-border, #242a3a); font-size: .9rem; }
        .mig-total b { font-size: 1.05rem; font-variant-numeric: tabular-nums; }
        .mig-field { margin-bottom: .8rem; }
        .mig-field label { display: block; font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--pp-text-muted, #9aa3b2); margin-bottom: .3rem; }
        .mig-input, .mig-select { width: 100%; height: 40px; padding: 0 .7rem; border: 1px solid var(--pp-border, #242a3a); border-radius: 9px; background: var(--pp-input-bg, #0b1120); color: var(--pp-text, #e5e7eb); font: inherit; font-size: .88rem; }
        .mig-input:focus, .mig-select:focus { outline: none; border-color: var(--pp-accent, #6366f1); box-shadow: 0 0 0 3px var(--pp-accent-soft, rgba(99,102,241,.25)); }
        .mig-row { display: grid; grid-template-columns: 1fr 90px; gap: .6rem; }
        .mig-btn { display: inline-flex; align-items: center; justify-content: center; gap: .4rem; height: 42px; padding: 0 1.1rem; border-radius: 10px; border: 1px solid transparent; font: inherit; font-size: .88rem; font-weight: 700; cursor: pointer; text-decoration: none; }
        .mig-btn-primary { background: linear-gradient(135deg, var(--pp-accent, #4f46e5), #8b5cf6); color: #fff; }
        .mig-btn-ghost { background: transparent; border-color: var(--pp-border, #242a3a); color: var(--pp-text); }
        .mig-btn-ghost:hover { border-color: var(--pp-accent, #818cf8); }
        .mig-btn-danger { background: transparent; border-color: color-mix(in srgb, #ef4444 55%, transparent); color: #ef4444; }
        .mig-btn-danger:hover { background: color-mix(in srgb, #ef4444 12%, var(--pp-surface)); }
        .mig-btn:disabled { opacity: .5; cursor: not-allowed; }
        .mig-target { margin: .2rem 0 .4rem; border: 1px solid var(--pp-border, #242a3a); border-radius: 10px; padding: .55rem .8rem; }
        .mig-target > summary { cursor: pointer; font-size: .82rem; font-weight: 700; color: var(--pp-text); }
        .mig-target > summary span { font-weight: 400; color: var(--pp-text-muted, #9aa3b2); }
        .mig-target[open] > summary { margin-bottom: .4rem; }
        .mig-actions { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1rem; }
        .mig-steps { margin-top: 1rem; display: none; }
        .mig-steps.show { display: block; }
        .mig-step { display: flex; align-items: flex-start; gap: .55rem; padding: .4rem 0; font-size: .84rem; border-top: 1px solid var(--pp-border, #242a3a); }
        .mig-step:first-child { border-top: 0; }
        .mig-step .ic { flex: 0 0 auto; margin-top: .1rem; }
        .mig-step .ic.ok { color: #10b981; } .mig-step .ic.bad { color: #ef4444; } .mig-step .ic.info { color: var(--pp-text-muted, #9aa3b2); }
        .mig-verdict { padding: .7rem .85rem; border-radius: 10px; font-size: .86rem; margin-top: 1rem; display: none; }
        .mig-verdict.show { display: block; }
        .mig-verdict.ok { background: color-mix(in srgb, #10b981 14%, var(--pp-surface)); border: 1px solid color-mix(in srgb, #10b981 40%, transparent); }
        .mig-verdict.bad { background: color-mix(in srgb, #ef4444 12%, var(--pp-surface)); border: 1px solid color-mix(in srgb, #ef4444 40%, transparent); }
        .mig-phase { display: inline-flex; align-items: center; gap: .4rem; font-size: .72rem; font-weight: 700; padding: .2rem .6rem; border-radius: 999px; background: var(--pp-accent-soft, rgba(99,102,241,.18)); color: var(--pp-accent, #818cf8); }
        .mig-plan { margin-top: 1.1rem; }
        .mig-plan ol { margin: .5rem 0 0; padding-left: 1.1rem; color: var(--pp-text-muted, #9aa3b2); font-size: .84rem; }
        .mig-plan li { margin: .25rem 0; }
        .mig-plan li b { color: var(--pp-text); font-weight: 600; }
        .mig-run-scope { font-size: .74rem; font-weight: 600; color: var(--pp-text-muted, #9aa3b2); }
        .mig-bar { height: 9px; border-radius: 999px; background: var(--pp-input-bg, #0b1120); border: 1px solid var(--pp-border, #242a3a); overflow: hidden; margin: .7rem 0 1rem; }
        .mig-bar span { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--pp-accent, #4f46e5), #8b5cf6); transition: width .4s ease; }
        .mig-phases { display: grid; gap: .35rem; margin-bottom: 1rem; }
        .mig-ph { display: flex; align-items: center; gap: .6rem; font-size: .85rem; padding: .35rem .1rem; }
        .mig-ph .dot { flex: 0 0 auto; width: 20px; text-align: center; }
        .mig-ph .lbl { flex: 1 1 auto; }
        .mig-ph .det { color: var(--pp-text-muted, #9aa3b2); font-size: .78rem; }
        .mig-ph.done .lbl { color: var(--pp-text); } .mig-ph.done .dot { color: #10b981; }
        .mig-ph.running .dot { color: var(--pp-accent, #818cf8); } .mig-ph.running .lbl { font-weight: 700; }
        .mig-ph.failed .dot { color: #ef4444; } .mig-ph.failed .det { color: #ef4444; }
        .mig-ph.staged { opacity: .55; } .mig-ph.staged .dot { color: var(--pp-text-muted, #9aa3b2); }
        .mig-ph.pending .dot { color: var(--pp-text-muted, #9aa3b2); }
        .mig-log { max-height: 230px; overflow: auto; margin: 0; padding: .75rem .85rem; border-radius: 10px; background: var(--pp-input-bg, #0b1120); border: 1px solid var(--pp-border, #242a3a); color: var(--pp-text-muted, #cbd5e1); font-size: .78rem; line-height: 1.5; white-space: pre-wrap; }
        .mig-help { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin-left: .35rem; border-radius: 50%; background: var(--pp-accent-soft, rgba(99,102,241,.18)); color: var(--pp-accent, #818cf8); font-size: .68rem; font-weight: 800; cursor: help; vertical-align: middle; }
        .mig-lblhelp { font-weight: 800; margin-left: .3rem; color: var(--pp-accent, #818cf8); cursor: help; }
        .mig-dbrow { border-top: 1px solid var(--pp-border, #242a3a); padding: .7rem 0; }
        .mig-dbhead { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
        .mig-dbhead b { font-size: .9rem; } .mig-dbhead small { color: var(--pp-text-muted, #9aa3b2); font-size: .76rem; }
        .mig-dbstatus { margin-left: auto; font-size: .78rem; } .mig-dbstatus.ok { color: #10b981; } .mig-dbstatus.bad { color: #ef4444; }
        .mig-dbform { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: .5rem; margin-top: .5rem; }
        @media (max-width: 720px) { .mig-dbform { grid-template-columns: 1fr 1fr; } }
        .mig-input.mig-mini { height: 36px; font-size: .82rem; }
        .mig-mini-btn { height: 36px; padding: 0 .8rem; font-size: .8rem; white-space: nowrap; }
        .mig-inline { display: flex; gap: .5rem; align-items: stretch; }
        .mig-inline .mig-input { flex: 1 1 auto; }
        .mig-testrow { display: flex; align-items: center; gap: .6rem; margin-top: -.2rem; margin-bottom: .8rem; flex-wrap: wrap; }
        .mig-testmsg { font-size: .8rem; } .mig-testmsg.ok { color: #10b981; } .mig-testmsg.bad { color: #ef4444; } .mig-testmsg.busy { color: var(--pp-text-muted, #9aa3b2); }
        /* Folder picker modal */
        .mig-modal { position: fixed; inset: 0; z-index: 1200; display: grid; place-items: center; background: rgba(2,6,23,.6); backdrop-filter: blur(2px); padding: 1rem; }
        .mig-modal[hidden] { display: none; }
        .mig-modal-card { width: min(560px, 96vw); max-height: 82vh; display: flex; flex-direction: column; background: var(--pp-surface, #141824); border: 1px solid var(--pp-border, #242a3a); border-radius: 16px; overflow: hidden; box-shadow: 0 24px 60px rgba(0,0,0,.4); }
        .mig-modal-head { display: flex; align-items: center; gap: .6rem; padding: .9rem 1.1rem; border-bottom: 1px solid var(--pp-border, #242a3a); }
        .mig-modal-head h3 { margin: 0; font-size: 1rem; font-weight: 800; flex: 1; display: flex; align-items: center; gap: .5rem; }
        .mig-modal-head h3 .bi { color: var(--pp-accent, #818cf8); }
        .mig-modal-x { background: none; border: 0; color: var(--pp-text-muted, #9aa3b2); font-size: 1.5rem; line-height: 1; cursor: pointer; }
        .mig-fp-path { padding: .55rem 1.1rem; font-family: ui-monospace, monospace; font-size: .82rem; color: var(--pp-accent, #818cf8); background: var(--pp-input-bg, #0b1120); border-bottom: 1px solid var(--pp-border, #242a3a); word-break: break-all; }
        .mig-fp-list { overflow: auto; padding: .35rem; flex: 1; min-height: 120px; }
        .mig-fp-item { display: flex; align-items: center; gap: .6rem; width: 100%; text-align: left; padding: .5rem .7rem; border: 0; background: none; color: var(--pp-text); font: inherit; font-size: .86rem; border-radius: 8px; cursor: pointer; }
        .mig-fp-item:hover { background: var(--pp-accent-soft, rgba(99,102,241,.14)); }
        .mig-fp-item .bi { color: var(--pp-accent, #818cf8); }
        .mig-fp-item.up .bi { color: var(--pp-text-muted, #9aa3b2); }
        .mig-fp-empty { padding: 1.2rem; text-align: center; color: var(--pp-text-muted, #9aa3b2); font-size: .84rem; }
        .mig-modal-foot { display: flex; align-items: center; gap: .5rem; padding: .8rem 1.1rem; border-top: 1px solid var(--pp-border, #242a3a); }
    </style>
</head>
<body>
<?php
    $user = $user ?? \PpPhp\Core\Security::CurrentUserData();
    $navBrandLabel = 'Server Migration';
    $navBrandIcon  = 'hdd-network';
    $navBack = ['url' => $baseUrl . '/settings', 'label' => 'Settings', 'icon' => 'arrow-left'];
    $navExtra = '<a class="btn btn-sm btn-outline-light" href="#" data-guide-open="migGuide" data-tip="Open the full Server Migration guide"><i class="bi bi-question-circle"></i> <span class="d-none d-md-inline">Guide</span></a>'
        . '<a class="btn btn-sm btn-outline-light" href="' . $h($baseUrl) . '/help/server_migration" data-tip="Open the Help article for Server Migration" target="_blank" rel="noopener"><i class="bi bi-book"></i> <span class="d-none d-md-inline">Help</span></a>';
    include constant('BASE_PATH') . '/core/views/console-navbar.php';
?>

    <div class="mig-wrap">
        <h1 class="mig-h1">Move this platform to another server</h1>
        <p class="mig-sub">Clone the whole platform — the database, your projects, every user's managed database, and files — to a new server over SFTP/FTP, provision its databases via the host panel (cPanel / DirectAdmin), verify it, then flip over. Pick exactly what to move, then run a <b>preflight</b> to check the destination before anything is transferred. Your current server stays untouched until you choose to flip.</p>

        <div class="mig-grid">
            <!-- LEFT: what will move -->
            <div class="mig-card">
                <h2><i class="bi bi-box-seam"></i> What will move
                    <span class="mig-help" data-tip="Choose exactly what this migration carries. The running total below updates as you tick items. Nothing here changes your current server.">?</span>
                </h2>
                <p class="mig-note">Everything is selected by default. Untick anything you don't want to transfer this time.</p>

                <div class="mig-grp">Platform <span class="mig-help" data-tip="The central database that runs the platform itself — users, projects metadata, settings. Almost always keep this ticked.">?</span></div>
                <label class="mig-item">
                    <input type="checkbox" checked data-size="<?= (int) ($inv['platform']['bytes'] ?? 0) ?>" data-scope="platformdb" value="1">
                    <span class="nm"><b>Platform database</b><small><?= $h($inv['platform']['name'] ?: 'central database') ?> · <?= (int) ($inv['platform']['tables'] ?? 0) ?> tables</small></span>
                    <span class="sz"><?= $hb((int) ($inv['platform']['bytes'] ?? 0)) ?></span>
                </label>

                <?php if (!empty($inv['projects'])): ?>
                <div class="mig-grp">Projects (<?= count($inv['projects']) ?>) <span class="mig-help" data-tip="Each app you've built — its design, generated code and custom files. Ticked projects are zipped into the bundle and unpacked on the new server.">?</span></div>
                <?php foreach ($inv['projects'] as $p): ?>
                    <label class="mig-item">
                        <input type="checkbox" checked data-size="<?= (int) $p['bytes'] ?>" data-scope="projects" value="<?= $h($p['id']) ?>">
                        <span class="nm"><b><?= $h($p['slug']) ?></b><small><?= $h($p['owner']) ?></small></span>
                        <span class="sz"><?= $hb((int) $p['bytes']) ?></span>
                    </label>
                <?php endforeach; ?>
                <?php endif; ?>

                <?php if (!empty($inv['managed'])): ?>
                <div class="mig-grp">Managed user databases (<?= count($inv['managed']) ?>) <span class="mig-help" data-tip="Databases you've provisioned for your users. Their data is dumped into the bundle; on the new server each is restored individually in the Cut over step (each may need its own target credentials).">?</span></div>
                <?php foreach ($inv['managed'] as $d): ?>
                    <label class="mig-item">
                        <input type="checkbox" checked data-size="<?= (int) $d['bytes'] ?>" data-scope="db" value="<?= (int) $d['id'] ?>">
                        <span class="nm"><b><?= $h($d['name']) ?></b><small><?= $h($d['assigned'] ?: 'unassigned') ?><?= $d['server'] ? ' · ' . $h($d['server']) : '' ?></small></span>
                        <span class="sz"><?= $d['bytes'] ? $hb((int) $d['bytes']) : '—' ?></span>
                    </label>
                <?php endforeach; ?>
                <?php endif; ?>

                <?php if (!empty($inv['fileSets'])): ?>
                <div class="mig-grp">Files <span class="mig-help" data-tip="On-disk content that isn't in a database: uploaded files, runtime storage and generated app builds. Ticked sets are zipped and unpacked on the new server.">?</span></div>
                <?php foreach ($inv['fileSets'] as $f): ?>
                    <label class="mig-item">
                        <input type="checkbox" <?= $f['key'] === 'output' ? '' : 'checked' ?> data-size="<?= (int) $f['bytes'] ?>" data-scope="files" value="<?= $h($f['key']) ?>">
                        <span class="nm"><b><?= $h($f['label']) ?></b><small><?= $h($f['path']) ?></small></span>
                        <span class="sz"><?= $hb((int) $f['bytes']) ?></span>
                    </label>
                <?php endforeach; ?>
                <?php endif; ?>

                <div class="mig-total"><span>Selected to transfer</span><b id="migTotal"><?= $hb($totalBytes) ?></b></div>
            </div>

            <!-- RIGHT: destination + preflight -->
            <div>
                <div class="mig-card">
                    <h2><i class="bi bi-hdd-network"></i> Destination server
                        <span class="mig-help" data-tip="Where this platform is moving to. Fill this in, run Preflight to check the connection, then Start (rehearsal) or Commit (real cutover).">?</span>
                    </h2>
                    <p class="mig-note">The new server's SFTP/FTP access. The password is encrypted at rest.</p>
                    <form id="migForm">
                        <input type="hidden" name="_token" value="<?= $h($csrf) ?>">
                        <div class="mig-field">
                            <label>Protocol <span class="mig-lblhelp" data-tip="How files reach the new server. SFTP (over SSH) is the most reliable and secure — pick it whenever the host offers SSH. FTPS is FTP encrypted with TLS; plain FTP is a last resort.">?</span></label>
                            <select class="mig-select" name="protocol">
                                <option value="sftp" <?= $dest['protocol'] === 'sftp' ? 'selected' : '' ?>>SFTP (SSH) — recommended</option>
                                <option value="ftps" <?= $dest['protocol'] === 'ftps' ? 'selected' : '' ?>>FTPS (FTP over TLS)</option>
                                <option value="ftp" <?= $dest['protocol'] === 'ftp' ? 'selected' : '' ?>>FTP (plain)</option>
                            </select>
                        </div>
                        <div class="mig-row">
                            <div class="mig-field"><label>Host <span class="mig-lblhelp" data-tip="The new server's address for SFTP/FTP — a hostname (new-server.example.com) or IP. Your host provides this; it is NOT your website URL.">?</span></label><input class="mig-input" name="host" value="<?= $h($dest['host']) ?>" placeholder="new-server.example.com"></div>
                            <div class="mig-field"><label>Port <span class="mig-lblhelp" data-tip="Connection port. Leave blank to use the default: 22 for SFTP, 21 for FTP/FTPS.">?</span></label><input class="mig-input" name="port" type="number" value="<?= $dest['port'] ?: '' ?>" placeholder="22"></div>
                        </div>
                        <div class="mig-field"><label>Username <span class="mig-lblhelp" data-tip="The SFTP/FTP login for the new server (often your cPanel/DirectAdmin username or an SSH user).">?</span></label><input class="mig-input" name="user" value="<?= $h($dest['user']) ?>" autocomplete="off"></div>
                        <div class="mig-field"><label>Password <span class="mig-lblhelp" data-tip="The SFTP/FTP password. Stored encrypted at rest on this server. Leave blank to reuse the saved one.">?</span> <?= $dest['hasPass'] ? '<span style="text-transform:none;font-weight:500;opacity:.7">(saved — leave blank to reuse)</span>' : '' ?></label><input class="mig-input" name="pass" type="password" autocomplete="new-password" placeholder="<?= $dest['hasPass'] ? '••••••••' : '' ?>"></div>
                        <div class="mig-field"><label>Remote base path <span class="mig-lblhelp" data-tip="The absolute folder on the new server where the site lives, e.g. /home/acme/public_html. The bundle is uploaded into an _ppmigrate/ subfolder here.">?</span></label>
                            <div class="mig-inline">
                                <input class="mig-input" name="basePath" id="migBasePath" value="<?= $h($dest['basePath']) ?>" placeholder="/home/acme/public_html">
                                <button type="button" class="mig-btn mig-btn-ghost mig-mini-btn" id="migBrowse" data-tip="Log in and browse the new server's folders — pick the path instead of typing it."><i class="bi bi-folder2-open"></i> Browse</button>
                            </div>
                        </div>
                        <div class="mig-testrow">
                            <button type="button" class="mig-btn mig-btn-ghost mig-mini-btn" id="migTestConn" data-tip="Quickly check the SFTP/FTP login and that the folder is writable."><i class="bi bi-plug"></i> Test connection</button>
                            <span class="mig-testmsg" id="migTestConnMsg"></span>
                        </div>
                        <div class="mig-row">
                            <div class="mig-field"><label>Host panel <span class="mig-lblhelp" data-tip="The control panel on the new server. Used as a hint for database provisioning. Choose None/manual for a VPS or if you'll create the database yourself.">?</span></label>
                                <select class="mig-select" name="panel">
                                    <option value="" <?= $dest['panel'] === '' ? 'selected' : '' ?>>None / manual</option>
                                    <option value="cpanel" <?= $dest['panel'] === 'cpanel' ? 'selected' : '' ?>>cPanel</option>
                                    <option value="directadmin" <?= $dest['panel'] === 'directadmin' ? 'selected' : '' ?>>DirectAdmin</option>
                                </select>
                            </div>
                            <div class="mig-field" style="align-self:end"><label style="visibility:hidden">save</label>
                                <label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;text-transform:none;color:var(--pp-text)" data-tip="Remember this destination (password encrypted) so you don't retype it next time."><input type="checkbox" name="save" value="1" checked> Save</label>
                            </div>
                        </div>
                        <div class="mig-field"><label>New site URL (agent runs here) <span class="mig-lblhelp" data-tip="The web address the new server answers on (https://new.example.com). The migration agent is reached over HTTP here to verify, provision and restore. During testing this can be a temporary URL or one pointed via your hosts file.">?</span></label><input class="mig-input" name="newBaseUrl" value="<?= $h($dest['newBaseUrl']) ?>" placeholder="https://new.example.com"></div>

                        <details class="mig-target">
                            <summary>Target database credentials <span>(only needed to commit the cutover)</span></summary>
                            <p class="mig-note" style="margin-top:.6rem">The new server's MySQL, as seen from that server (usually <code>localhost</code>). Used by the agent to create/restore the platform database. Leave blank to run a safe rehearsal only.</p>
                            <div class="mig-row">
                                <div class="mig-field"><label>DB host <span class="mig-lblhelp" data-tip="The MySQL host as seen from the NEW server — almost always 'localhost' on shared hosting.">?</span></label><input class="mig-input" name="targetDbHost" value="localhost"></div>
                                <div class="mig-field"><label>DB port <span class="mig-lblhelp" data-tip="MySQL port on the new server. Usually 3306.">?</span></label><input class="mig-input" name="targetDbPort" type="number" value="3306"></div>
                            </div>
                            <div class="mig-field"><label>Database name <span class="mig-lblhelp" data-tip="The database on the new server to load the platform data into. On cPanel/DirectAdmin create it first (it'll be prefixed, e.g. acme_ppphp) and enter that full name.">?</span></label><input class="mig-input" name="targetDbName" placeholder="<?= $h($inv['platform']['name'] ?: 'ppphp_core') ?>"></div>
                            <div class="mig-row">
                                <div class="mig-field"><label>DB user <span class="mig-lblhelp" data-tip="A MySQL user on the new server with full rights on that database.">?</span></label><input class="mig-input" name="targetDbUser" autocomplete="off"></div>
                                <div class="mig-field"><label>DB password <span class="mig-lblhelp" data-tip="Password for that MySQL user. It is sent to the agent on the new server, never stored here.">?</span></label><input class="mig-input" name="targetDbPass" type="password" autocomplete="new-password"></div>
                            </div>
                            <label style="display:flex;align-items:center;gap:.4rem;font-size:.82rem;color:var(--pp-text)" data-tip="On a VPS where the DB user can CREATE databases, tick this to have the agent create the database automatically. On shared hosting leave it off and create the DB in the panel first."><input type="checkbox" name="targetDbCreate" value="1"> Create the database if it doesn't exist (VPS with CREATE privilege)</label>
                            <div class="mig-testrow" style="margin-top:.6rem">
                                <button type="button" class="mig-btn mig-btn-ghost mig-mini-btn" id="migTestDb" data-tip="Briefly deploys the agent to the new server and asks it to connect to this database, then removes it. Needs the destination host + new site URL set."><i class="bi bi-database-check"></i> Test database</button>
                                <span class="mig-testmsg" id="migTestDbMsg"></span>
                            </div>
                        </details>

                        <div class="mig-actions">
                            <button type="submit" class="mig-btn mig-btn-primary" id="migPreflight"><i class="bi bi-shield-check"></i> Run preflight</button>
                            <button type="button" class="mig-btn mig-btn-ghost" id="migStart" data-tip="Packages a verified snapshot; with a destination set it also transports & rehearses — no changes to the target"><i class="bi bi-rocket-takeoff"></i> Start migration</button>
                            <button type="button" class="mig-btn mig-btn-danger" id="migCommit" data-tip="Real cutover: provisions, restores the platform DB, unpacks files & rewrites config on the destination"><i class="bi bi-hdd-network-fill"></i> Commit cutover</button>
                        </div>
                    </form>

                    <div class="mig-verdict" id="migVerdict"></div>
                    <div class="mig-steps" id="migSteps"></div>
                </div>

                <div class="mig-card mig-plan" style="margin-top:1.1rem">
                    <h2><i class="bi bi-diagram-3"></i> How the migration runs</h2>
                    <ol>
                        <li><b>Preflight</b> — reach the destination, check it's writable, PHP/DB ready.</li>
                        <li><b>Snapshot</b> — package the selected DBs + files (encrypted).</li>
                        <li><b>Transport</b> — stream the bundle over <?= strtoupper($h($dest['protocol'] ?: 'sftp')) ?>, resumable &amp; checksummed; a self-contained agent lands with it.</li>
                        <li><b>Verify</b> — the agent re-hashes every file against the manifest on the destination.</li>
                        <li><b>Provision</b> — the agent creates / connects the target database (works on cPanel, DirectAdmin or a VPS).</li>
                        <li><b>Restore</b> — the agent loads the database, unpacks the files and rewrites the config.</li>
                        <li><b>Flip</b> — verify the new site, then point DNS over; the old server can show a "migrated" notice.</li>
                    </ol>
                </div>
            </div>
        </div>

        <!-- Live migration progress -->
        <div class="mig-card mig-run" id="migRun" style="margin-top:1.1rem;display:none">
            <h2><i class="bi bi-activity"></i> Migration progress <span class="mig-run-scope" id="migRunScope"></span></h2>
            <div class="mig-bar"><span id="migBar"></span></div>
            <div class="mig-phases" id="migPhases"></div>
            <pre class="mig-log" id="migLog" aria-live="polite"></pre>
        </div>

        <?php $lastJobId = $recent[0]['id'] ?? ''; ?>
        <!-- Cut over: managed-DB restore + flip -->
        <div class="mig-card" id="migCut" style="margin-top:1.1rem">
            <h2><i class="bi bi-signpost-2"></i> Cut over
                <span class="mig-help" data-tip="The final step. Restore each user database on the new server, then flip this old server to a 'moved' notice. Fully reversible.">?</span>
            </h2>
            <p class="mig-note">Do this only after a commit has restored the platform database and you've checked the new site works.</p>

            <?php if (!empty($inv['managed'])): ?>
            <div class="mig-grp">Restore user databases
                <span class="mig-help" data-tip="Each managed user database is restored individually because each may target its own database name and credentials on the new server. Uses the latest transported bundle.">?</span>
            </div>
            <?php foreach ($inv['managed'] as $d): ?>
            <div class="mig-dbrow" data-dbid="<?= (int) $d['id'] ?>">
                <div class="mig-dbhead">
                    <b><?= $h($d['name']) ?></b>
                    <small><?= $h($d['assigned'] ?: 'unassigned') ?><?= $d['bytes'] ? ' · ' . $hb((int) $d['bytes']) : '' ?></small>
                    <span class="mig-dbstatus" data-role="status"></span>
                </div>
                <div class="mig-dbform">
                    <input class="mig-input mig-mini" data-f="dbName" placeholder="target db name" value="<?= $h($d['name']) ?>" data-tip="The database name on the NEW server to load this data into.">
                    <input class="mig-input mig-mini" data-f="dbUser" placeholder="db user" autocomplete="off" data-tip="A MySQL user on the new server that can write to that database.">
                    <input class="mig-input mig-mini" data-f="dbPass" type="password" placeholder="db password" autocomplete="new-password" data-tip="Password for that MySQL user (stays on the new server via the agent).">
                    <button type="button" class="mig-btn mig-btn-ghost mig-mini-btn" data-role="restore" data-tip="Provision (if needed) and restore this database on the new server."><i class="bi bi-database-down"></i> Restore</button>
                </div>
            </div>
            <?php endforeach; ?>
            <?php endif; ?>

            <div class="mig-grp" style="margin-top:1.1rem">Retire this server
                <span class="mig-help" data-tip="Shows visitors a 'this site has moved' page pointing at the new URL. The owner and logged-in staff can still use the console. One click to undo.">?</span>
            </div>
            <?php if ($flip): ?>
            <div class="mig-verdict show bad" style="display:block">
                <i class="bi bi-exclamation-triangle-fill"></i>
                This server is showing a <b>“moved”</b> notice → <a href="<?= $h($flip['newUrl']) ?>" style="color:inherit"><?= $h($flip['newUrl']) ?></a>
                <small style="display:block;opacity:.8">Engaged <?= $h($flip['at'] ?? '') ?></small>
            </div>
            <div class="mig-actions">
                <button type="button" class="mig-btn mig-btn-primary" id="migUnflip" data-tip="Remove the notice and bring this server back into normal service."><i class="bi bi-arrow-counterclockwise"></i> Bring back online</button>
            </div>
            <?php else: ?>
            <p class="mig-note">Once the new site is confirmed working, flip this server so visitors are guided to it.</p>
            <div class="mig-actions">
                <button type="button" class="mig-btn mig-btn-danger" id="migFlip" data-tip="Retire this server: show visitors a moved notice pointing at the new site URL from the destination form."><i class="bi bi-signpost-split"></i> Flip to new server</button>
            </div>
            <?php endif; ?>
        </div>

        <?php if (!empty($recent)): ?>
        <div class="mig-card" style="margin-top:1.1rem">
            <h2><i class="bi bi-clock-history"></i> Recent migrations</h2>
            <p class="mig-note">The last few snapshots packaged on this server. Bundles live under <code>storage/migrations/</code>.</p>
            <?php foreach ($recent as $j):
                $st = (string) ($j['status'] ?? '');
                $stCls = $st === 'done' ? 'ok' : ($st === 'failed' ? 'bad' : 'info');
            ?>
            <div class="mig-item" style="gap:.9rem">
                <i class="bi bi-<?= $st === 'done' ? 'check-circle-fill' : ($st === 'failed' ? 'x-circle-fill' : 'hourglass-split') ?> ic <?= $stCls ?>" style="font-size:1.05rem"></i>
                <span class="nm"><b><?= $h($j['scope'] ?? '') ?></b><small><?= $h(gmdate('Y-m-d H:i', (int) ($j['created'] ?? 0))) ?> UTC · <?= $h($j['id'] ?? '') ?></small></span>
                <span class="sz"><?= $hb((int) ($j['bundle']['bytes'] ?? 0)) ?> · <?= (int) ($j['progress'] ?? 0) ?>%</span>
            </div>
            <?php endforeach; ?>
        </div>
        <?php endif; ?>
    </div>

    <!-- Remote folder picker -->
    <div class="mig-modal" id="migFp" hidden>
        <div class="mig-modal-card">
            <div class="mig-modal-head">
                <h3><i class="bi bi-folder2-open"></i> Choose the destination folder</h3>
                <button type="button" class="mig-modal-x" id="migFpClose" data-tip="Close">&times;</button>
            </div>
            <div class="mig-fp-path" id="migFpPath">/</div>
            <div class="mig-fp-list" id="migFpList"></div>
            <div class="mig-modal-foot">
                <span class="mig-testmsg" id="migFpMsg"></span>
                <span style="flex:1"></span>
                <button type="button" class="mig-btn mig-btn-ghost mig-mini-btn" id="migFpCancel">Cancel</button>
                <button type="button" class="mig-btn mig-btn-primary mig-mini-btn" id="migFpPick" data-tip="Use the folder shown above as the remote base path."><i class="bi bi-check2"></i> Select this folder</button>
            </div>
        </div>
    </div>

    <script src="<?= $h($assetsUrl) ?>/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script>
    (function () {
        var base = <?= json_encode($baseUrl) ?>;
        var form = document.getElementById('migForm');
        var totalEl = document.getElementById('migTotal');
        var verdict = document.getElementById('migVerdict');
        var stepsEl = document.getElementById('migSteps');
        var startBtn = document.getElementById('migStart');

        function human(b) { if (b <= 0) return '0 B'; var u = ['B','KB','MB','GB','TB'], i = Math.min(u.length - 1, Math.floor(Math.log(b) / Math.log(1024))); return (b / Math.pow(1024, i)).toFixed(i ? 1 : 0) + ' ' + u[i]; }
        function recalc() {
            var t = 0;
            document.querySelectorAll('.mig-item input[type=checkbox]:checked').forEach(function (c) { t += parseInt(c.dataset.size || '0', 10); });
            totalEl.textContent = human(t);
        }
        document.querySelectorAll('.mig-item input[type=checkbox]').forEach(function (c) { c.addEventListener('change', recalc); });
        recalc();

        form.addEventListener('submit', function (e) {
            e.preventDefault();
            var btn = document.getElementById('migPreflight');
            btn.disabled = true; btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Checking…';
            verdict.className = 'mig-verdict'; stepsEl.className = 'mig-steps'; stepsEl.innerHTML = '';
            fetch(base + '/settings/migrate/preflight', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(new FormData(form)) })
                .then(function (r) { return r.json(); })
                .then(function (d) {
                    verdict.className = 'mig-verdict show ' + (d.ok ? 'ok' : 'bad');
                    verdict.innerHTML = '<i class="bi bi-' + (d.ok ? 'check-circle-fill' : 'exclamation-triangle-fill') + '"></i> ' + (d.message || '');
                    if (Array.isArray(d.steps) && d.steps.length) {
                        stepsEl.className = 'mig-steps show';
                        stepsEl.innerHTML = d.steps.map(function (s) {
                            var ok = s.ok === true || s.success === true || s.status === 'ok';
                            var bad = s.ok === false || s.success === false || s.status === 'fail' || s.status === 'error';
                            var cls = ok ? 'ok' : (bad ? 'bad' : 'info');
                            var ic = ok ? 'check-circle-fill' : (bad ? 'x-circle-fill' : 'dash-circle');
                            var label = s.label || s.step || s.name || '';
                            var msg = s.message || s.detail || '';
                            return '<div class="mig-step"><i class="bi bi-' + ic + ' ic ' + cls + '"></i><div><b>' + label + '</b>' + (msg ? ' — ' + msg : '') + '</div></div>';
                        }).join('');
                    }
                    startBtn.disabled = !d.ok;
                })
                .catch(function () { verdict.className = 'mig-verdict show bad'; verdict.textContent = 'Preflight failed to run.'; })
                .finally(function () { btn.disabled = false; btn.innerHTML = '<i class="bi bi-shield-check"></i> Run preflight'; });
        });

        // ── Start migration (runs preflight + snapshot; polls for live state) ──
        var runEl = document.getElementById('migRun');
        var barEl = document.getElementById('migBar');
        var phasesEl = document.getElementById('migPhases');
        var logEl = document.getElementById('migLog');
        var scopeEl = document.getElementById('migRunScope');
        var pollTimer = null;

        function scopeParams(extra) {
            var body = new URLSearchParams(new FormData(form));
            document.querySelectorAll('.mig-item input[data-scope]:checked').forEach(function (c) {
                body.append(c.dataset.scope + '[]', c.value);
            });
            if (extra) { Object.keys(extra).forEach(function (k) { body.append(k, extra[k]); }); }
            return body;
        }
        function renderJob(job) {
            if (!job) return;
            runEl.style.display = 'block';
            barEl.style.width = (job.progress || 0) + '%';
            scopeEl.textContent = '· ' + (job.scope || '');
            var order = ['preflight','snapshot','transport','provision','restore','verify','flip'];
            phasesEl.innerHTML = order.map(function (k) {
                var p = (job.phases || {})[k]; if (!p) return '';
                var icon = { done:'check-circle-fill', running:'arrow-repeat', failed:'x-circle-fill', staged:'hourglass', pending:'circle' }[p.status] || 'circle';
                return '<div class="mig-ph ' + p.status + '"><span class="dot"><i class="bi bi-' + icon + '"></i></span>'
                     + '<span class="lbl">' + p.label + (p.detail ? ' <span class="det">— ' + p.detail + '</span>' : '') + '</span></div>';
            }).join('');
            if (Array.isArray(job.log)) { logEl.textContent = job.log.join('\n'); logEl.scrollTop = logEl.scrollHeight; }
        }
        function poll(id) {
            fetch(base + '/settings/migrate/status?id=' + encodeURIComponent(id), { credentials: 'same-origin' })
                .then(function (r) { return r.json(); })
                .then(function (d) {
                    if (!d.ok) return;
                    renderJob(d.job);
                    if (d.job && (d.job.status === 'done' || d.job.status === 'failed')) {
                        clearInterval(pollTimer);
                        startBtn.disabled = false;
                        startBtn.innerHTML = '<i class="bi bi-rocket-takeoff"></i> Start migration';
                    }
                });
        }
        function resetBtns() {
            startBtn.disabled = false; startBtn.innerHTML = '<i class="bi bi-rocket-takeoff"></i> Start migration';
            commitBtn.disabled = false; commitBtn.innerHTML = '<i class="bi bi-hdd-network-fill"></i> Commit cutover';
        }
        function launch(endpoint, extra, busyBtn, busyLabel) {
            startBtn.disabled = true; commitBtn.disabled = true;
            busyBtn.innerHTML = '<i class="bi bi-arrow-repeat"></i> ' + busyLabel;
            runEl.style.display = 'block';
            logEl.textContent = 'Starting…';
            fetch(base + endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: scopeParams(extra) })
                .then(function (r) { return r.json(); })
                .then(function (d) {
                    if (!d.ok) { logEl.textContent = d.message || 'Could not start.'; resetBtns(); return; }
                    renderJob(d.job);
                    if (d.job && (d.job.status === 'done' || d.job.status === 'failed')) { resetBtns(); }
                    else { pollTimer = setInterval(function () { poll(d.id); }, 1500); }
                })
                .catch(function () { logEl.textContent = 'Migration failed to start.'; resetBtns(); });
        }
        var commitBtn = document.getElementById('migCommit');
        startBtn.addEventListener('click', function () {
            var hasDest = (form.host.value || '').trim() !== '';
            var msg = hasDest
                ? 'This packages a <b>verified snapshot</b>, transports it to the destination, and runs a full <b>rehearsal</b> (integrity check + restore plan). <b>Nothing on the destination is changed.</b>'
                : 'This packages a <b>verified snapshot</b> into a bundle on this server. Connect a destination to transport &amp; cut over.';
            var run = function () { launch('/settings/migrate/start', null, startBtn, hasDest ? 'Transporting…' : 'Packaging…'); };
            if (window.Swal) {
                Swal.fire({ title: hasDest ? 'Start migration (rehearsal)?' : 'Package snapshot?', html: msg, icon: 'question', showCancelButton: true, confirmButtonText: hasDest ? 'Start' : 'Package', focusCancel: false })
                    .then(function (res) { if (res.isConfirmed) run(); });
            } else if (confirm('Proceed?')) { run(); }
        });
        commitBtn.addEventListener('click', function () {
            if ((form.newBaseUrl.value || '').trim() === '' || (form.host.value || '').trim() === '') {
                if (window.Swal) { Swal.fire({ icon: 'warning', title: 'Destination incomplete', text: 'A destination host and new site URL are required to commit.' }); }
                else { alert('A destination host and new site URL are required to commit.'); }
                return;
            }
            var run = function () { launch('/settings/migrate/commit', { confirm: '1' }, commitBtn, 'Committing…'); };
            if (window.Swal) {
                Swal.fire({
                    title: 'Commit the cutover?',
                    html: 'This performs the <b>real migration</b> on the destination: provision the target database, restore the platform database into it, unpack the files and rewrite the config. This changes the destination server.',
                    icon: 'warning', showCancelButton: true, confirmButtonText: 'Yes, commit', confirmButtonColor: '#ef4444', focusCancel: true
                }).then(function (res) { if (res.isConfirmed) run(); });
            } else if (confirm('Commit the real cutover to the destination?')) { run(); }
        });

        // ── Cut over: per-managed-DB restore + flip ───────────────────
        var csrf = form._token ? form._token.value : '';
        var lastJobId = <?= json_encode($lastJobId) ?>;

        document.querySelectorAll('.mig-dbrow [data-role="restore"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var row = btn.closest('.mig-dbrow');
                var statusEl = row.querySelector('[data-role="status"]');
                if (!lastJobId) {
                    statusEl.className = 'mig-dbstatus bad'; statusEl.textContent = 'Run a commit first (no bundle on the destination yet).';
                    return;
                }
                var body = new URLSearchParams();
                body.append('_token', csrf); body.append('id', lastJobId); body.append('dbId', row.dataset.dbid);
                body.append('dbName', row.querySelector('[data-f="dbName"]').value);
                body.append('dbUser', row.querySelector('[data-f="dbUser"]').value);
                body.append('dbPass', row.querySelector('[data-f="dbPass"]').value);
                btn.disabled = true; btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Restoring…';
                statusEl.className = 'mig-dbstatus'; statusEl.textContent = '';
                fetch(base + '/settings/migrate/restore-db', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body })
                    .then(function (r) { return r.json(); })
                    .then(function (d) {
                        statusEl.className = 'mig-dbstatus ' + (d.ok ? 'ok' : 'bad');
                        statusEl.textContent = d.message || (d.ok ? 'Restored.' : 'Failed.');
                    })
                    .catch(function () { statusEl.className = 'mig-dbstatus bad'; statusEl.textContent = 'Request failed.'; })
                    .finally(function () { btn.disabled = false; btn.innerHTML = '<i class="bi bi-database-down"></i> Restore'; });
            });
        });

        var flipBtn = document.getElementById('migFlip');
        if (flipBtn) flipBtn.addEventListener('click', function () {
            if ((form.newBaseUrl.value || '').trim() === '') {
                if (window.Swal) { Swal.fire({ icon: 'warning', title: 'New site URL required', text: 'Enter the new site URL in the destination form first.' }); }
                else { alert('Enter the new site URL first.'); }
                return;
            }
            var go = function () {
                var body = new URLSearchParams();
                body.append('_token', csrf); body.append('confirm', '1');
                body.append('newBaseUrl', form.newBaseUrl.value); body.append('id', lastJobId || '');
                fetch(base + '/settings/migrate/flip', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body })
                    .then(function (r) { return r.json(); }).then(function (d) {
                        if (window.Swal) { Swal.fire({ icon: d.ok ? 'success' : 'error', title: d.ok ? 'Flipped' : 'Could not flip', text: d.message || '' }).then(function () { if (d.ok) location.reload(); }); }
                        else { alert(d.message || ''); if (d.ok) location.reload(); }
                    });
            };
            if (window.Swal) {
                Swal.fire({ title: 'Flip to the new server?', html: 'Visitors will see a <b>“this site has moved”</b> notice pointing at <b>' + (form.newBaseUrl.value) + '</b>. You (the owner) keep full access, and this is reversible.', icon: 'warning', showCancelButton: true, confirmButtonText: 'Flip now', confirmButtonColor: '#ef4444' }).then(function (res) { if (res.isConfirmed) go(); });
            } else if (confirm('Show a moved notice to visitors?')) { go(); }
        });

        var unflipBtn = document.getElementById('migUnflip');
        if (unflipBtn) unflipBtn.addEventListener('click', function () {
            var go = function () {
                var body = new URLSearchParams(); body.append('_token', csrf);
                fetch(base + '/settings/migrate/unflip', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body })
                    .then(function (r) { return r.json(); }).then(function (d) {
                        if (window.Swal) { Swal.fire({ icon: d.ok ? 'success' : 'error', title: d.ok ? 'Back online' : 'Failed', text: d.message || '' }).then(function () { if (d.ok) location.reload(); }); }
                        else { alert(d.message || ''); if (d.ok) location.reload(); }
                    });
            };
            if (window.Swal) { Swal.fire({ title: 'Bring this server back online?', text: 'The moved notice will be removed and visitors can use this server again.', icon: 'question', showCancelButton: true, confirmButtonText: 'Bring back online' }).then(function (res) { if (res.isConfirmed) go(); }); }
            else if (confirm('Bring this server back online?')) { go(); }
        });

        // ── Compact connection tests ──────────────────────────────────
        function needHostUser() {
            if ((form.host.value || '').trim() === '' || (form.user.value || '').trim() === '') { return 'Enter the host and username first.'; }
            return '';
        }
        var testConnBtn = document.getElementById('migTestConn');
        var testConnMsg = document.getElementById('migTestConnMsg');
        testConnBtn.addEventListener('click', function () {
            var miss = needHostUser();
            if (miss) { testConnMsg.className = 'mig-testmsg bad'; testConnMsg.textContent = miss; return; }
            testConnBtn.disabled = true; testConnMsg.className = 'mig-testmsg busy'; testConnMsg.innerHTML = '<i class="bi bi-arrow-repeat"></i> Testing…';
            fetch(base + '/settings/migrate/preflight', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(new FormData(form)) })
                .then(function (r) { return r.json(); }).then(function (d) {
                    testConnMsg.className = 'mig-testmsg ' + (d.ok ? 'ok' : 'bad');
                    testConnMsg.innerHTML = '<i class="bi bi-' + (d.ok ? 'check-circle-fill' : 'x-circle-fill') + '"></i> ' + (d.ok ? 'Connected & writable.' : (d.message || 'Failed.'));
                })
                .catch(function () { testConnMsg.className = 'mig-testmsg bad'; testConnMsg.textContent = 'Test failed to run.'; })
                .finally(function () { testConnBtn.disabled = false; });
        });

        var testDbBtn = document.getElementById('migTestDb');
        var testDbMsg = document.getElementById('migTestDbMsg');
        testDbBtn.addEventListener('click', function () {
            if ((form.newBaseUrl.value || '').trim() === '' || (form.host.value || '').trim() === '') {
                testDbMsg.className = 'mig-testmsg bad'; testDbMsg.textContent = 'Set the destination host and new site URL first.'; return;
            }
            if ((form.targetDbUser.value || '').trim() === '' || (form.targetDbName.value || '').trim() === '') {
                testDbMsg.className = 'mig-testmsg bad'; testDbMsg.textContent = 'Enter the target DB name and user.'; return;
            }
            testDbBtn.disabled = true; testDbMsg.className = 'mig-testmsg busy'; testDbMsg.innerHTML = '<i class="bi bi-arrow-repeat"></i> Deploying probe & testing…';
            fetch(base + '/settings/migrate/test-db', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(new FormData(form)) })
                .then(function (r) { return r.json(); }).then(function (d) {
                    testDbMsg.className = 'mig-testmsg ' + (d.ok ? 'ok' : 'bad');
                    testDbMsg.innerHTML = '<i class="bi bi-' + (d.ok ? 'check-circle-fill' : 'x-circle-fill') + '"></i> ' + (d.message || (d.ok ? 'OK.' : 'Failed.'));
                })
                .catch(function () { testDbMsg.className = 'mig-testmsg bad'; testDbMsg.textContent = 'Test failed to run.'; })
                .finally(function () { testDbBtn.disabled = false; });
        });

        // ── Remote folder picker ──────────────────────────────────────
        var fp = document.getElementById('migFp');
        var fpPathEl = document.getElementById('migFpPath');
        var fpListEl = document.getElementById('migFpList');
        var fpMsg = document.getElementById('migFpMsg');
        var fpCurrent = '/';
        function fpOpen() {
            var miss = needHostUser();
            if (miss) { testConnMsg.className = 'mig-testmsg bad'; testConnMsg.textContent = miss; return; }
            fp.hidden = false;
            fpLoad((document.getElementById('migBasePath').value || '').trim() || '/');
        }
        function fpClose() { fp.hidden = true; }
        function fpLoad(path) {
            fpMsg.className = 'mig-testmsg busy'; fpMsg.innerHTML = '<i class="bi bi-arrow-repeat"></i> Loading…';
            fpListEl.innerHTML = '';
            var body = new URLSearchParams(new FormData(form)); body.set('path', path);
            fetch(base + '/settings/migrate/browse', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body })
                .then(function (r) { return r.json(); }).then(function (d) {
                    if (!d.ok) { fpMsg.className = 'mig-testmsg bad'; fpMsg.textContent = d.message || 'Could not list folder.'; return; }
                    fpMsg.className = 'mig-testmsg'; fpMsg.textContent = '';
                    fpCurrent = d.path; fpPathEl.textContent = d.path;
                    var html = '';
                    if (d.parent !== null && d.parent !== undefined) {
                        html += '<button type="button" class="mig-fp-item up" data-path="' + d.parent.replace(/"/g, '&quot;') + '"><i class="bi bi-arrow-90deg-up"></i> ..</button>';
                    }
                    (d.dirs || []).forEach(function (dir) {
                        html += '<button type="button" class="mig-fp-item" data-path="' + dir.path.replace(/"/g, '&quot;') + '"><i class="bi bi-folder-fill"></i> ' + dir.name.replace(/</g, '&lt;') + '</button>';
                    });
                    if (!html) { html = '<div class="mig-fp-empty">No sub-folders here. You can still select this folder.</div>'; }
                    fpListEl.innerHTML = html;
                    fpListEl.querySelectorAll('.mig-fp-item').forEach(function (b) {
                        b.addEventListener('click', function () { fpLoad(b.dataset.path); });
                    });
                })
                .catch(function () { fpMsg.className = 'mig-testmsg bad'; fpMsg.textContent = 'Could not reach the server.'; });
        }
        document.getElementById('migBrowse').addEventListener('click', fpOpen);
        document.getElementById('migFpClose').addEventListener('click', fpClose);
        document.getElementById('migFpCancel').addEventListener('click', fpClose);
        fp.addEventListener('click', function (e) { if (e.target === fp) fpClose(); });
        document.getElementById('migFpPick').addEventListener('click', function () {
            document.getElementById('migBasePath').value = fpCurrent;
            fpClose();
        });
    })();
    </script>
    <script src="<?= $h($assetsUrl) ?>/sweetalert2/sweetalert2.min.js"></script>
    <?php
    $ownerGuide = [
        'id' => 'migGuide',
        'eyebrow' => 'Owner guide',
        'title' => 'Server Migration / Transfer',
        'subtitle' => 'Move this whole platform — database, projects, user data and files — to a new server, safely and reversibly.',
        'icon' => 'hdd-network-fill',
        'topics' => [
            ['id' => 'what', 'label' => 'What it does', 'icon' => 'stars',
             'keywords' => 'overview clone copy move transfer hosting cpanel directadmin vps',
             'body' => '<p class="og-lead">This tool clones your entire platform to another server and cuts over to it — with no command line, on any host (cPanel, DirectAdmin, Plesk or a plain VPS).</p>'
                . '<p>It packages a verified <b>bundle</b> (database dumps + zipped files + a self-contained agent), transports it over SFTP/FTP, then a small agent <b>on the new server</b> verifies, provisions and restores everything. Your current server is never touched until you choose to flip.</p>'
                . '<div class="og-tip">Every step is checksummed. A safe <b>rehearsal</b> runs the whole plan without changing the destination, so you can prove it works before committing.</div>'],
            ['id' => 'prep', 'label' => 'Before you start', 'icon' => 'clipboard-check',
             'keywords' => 'requirements prepare ssh sftp credentials database create',
             'body' => '<h3>You will need</h3><ul class="og-list">'
                . '<li>SFTP/FTP access to the new server (host, username, password).</li>'
                . '<li>The absolute path where the site lives there (e.g. <code>/home/acme/public_html</code>).</li>'
                . '<li>The new site\'s URL, reachable during migration (a temporary URL or a hosts-file entry is fine).</li>'
                . '<li>A MySQL database + user on the new server (create it in the panel first on shared hosting), or a VPS DB user that can create databases.</li>'
                . '</ul>'],
            ['id' => 'scope', 'label' => 'Choose what moves', 'icon' => 'box-seam',
             'keywords' => 'scope select projects databases files platform',
             'body' => '<p>In <b>What will move</b>, tick the platform database, the projects, each managed user database, and the file sets you want. The running total shows the transfer size. Leave items unticked to migrate only part of your estate.</p>'],
            ['id' => 'preflight', 'label' => 'Preflight & rehearsal', 'icon' => 'shield-check',
             'keywords' => 'preflight test rehearsal dry run check connection browse folder test database',
             'body' => '<h3>1 · Preflight</h3><p>Enter the destination and click <b>Run preflight</b>. It connects, checks the folder is writable and confirms PHP/DB readiness — changing nothing.</p>'
                . '<div class="og-tip">Quick checks live right in the destination form: <b>Browse</b> logs in and lets you pick the remote folder (no typing paths); <b>Test connection</b> verifies the SFTP/FTP login; and <b>Test database</b> briefly deploys the agent to confirm your target DB credentials on the new server.</div>'
                . '<h3>2 · Start (rehearsal)</h3><p>With a destination set, <b>Start migration</b> packages the bundle, transports it, and the agent verifies every file and validates the restore plan. <b>Nothing on the destination is changed.</b></p>'
                . '<div class="og-tip">Run the rehearsal until it is clean. It is safe to repeat — transport is resumable, so re-runs only send what is missing.</div>'],
            ['id' => 'commit', 'label' => 'Commit the cutover', 'icon' => 'hdd-network-fill',
             'keywords' => 'commit restore provision platform database credentials',
             'body' => '<p>Open <b>Target database credentials</b> and enter the new server\'s DB host (usually <code>localhost</code>), database name, user and password. Then click <b>Commit cutover</b>.</p>'
                . '<p>The agent provisions/connects the database, restores the platform database into it, unpacks your files and rewrites the config on the new server.</p>'
                . '<div class="og-warn">Commit changes the destination server. Your current (source) server is still untouched — visitors keep using it until you flip.</div>'],
            ['id' => 'userdbs', 'label' => 'User databases', 'icon' => 'database-down',
             'keywords' => 'managed user databases restore per database credentials',
             'body' => '<p>In <b>Cut over → Restore user databases</b>, each managed database has its own target name/user/password (they may live in separate databases on the new server). Fill each and click <b>Restore</b>. Status shows per row.</p>'],
            ['id' => 'flip', 'label' => 'Flip & go live', 'icon' => 'signpost-split',
             'keywords' => 'flip cutover dns moved notice retire reverse',
             'body' => '<h3>Verify first</h3><p>Open the new site URL and confirm it works and your data is present.</p>'
                . '<h3>Flip</h3><p><b>Flip to new server</b> makes this old server show a “this site has moved” notice pointing at the new URL. You (the owner) and logged-in staff keep full access.</p>'
                . '<h3>Point DNS</h3><p>Update your domain\'s DNS/records to the new server. Once traffic flows there, you can retire the old box.</p>'
                . '<div class="og-tip">Changed your mind? <b>Bring back online</b> removes the notice instantly — the flip is fully reversible.</div>'],
            ['id' => 'safety', 'label' => 'Safety & security', 'icon' => 'shield-lock',
             'keywords' => 'security safety token encryption checksum rollback owner only',
             'body' => '<ul class="og-list">'
                . '<li>Owner-only: no other role can see or run migrations.</li>'
                . '<li>The destination password is encrypted at rest; the agent token is never placed in the transported bundle.</li>'
                . '<li>Every file is SHA-256 checksummed and re-verified on the destination.</li>'
                . '<li>Destructive actions require explicit confirmation and support a dry run first.</li>'
                . '<li>The source server is read-only throughout — nothing is deleted here.</li>'
                . '<li>Delete the <code>_ppmigrate/</code> folder on the new server when done (the agent\'s cleanup does this).</li>'
                . '</ul>'],
        ],
    ];
    include constant('BASE_PATH') . '/core/views/owner-ux.php';
    ?>
</body>
</html>
