<!DOCTYPE html>
<?php
/* Owner-only Updates page.
   Vars: $baseUrl, $assetsUrl, $user, $codeVersion, $installedVersion,
         $installed, $preflight, $restorePoints */
if (!isset($baseUrl)) $baseUrl = '';
if (!isset($assetsUrl)) $assetsUrl = rtrim($baseUrl, '/') . '/assets';
$b = rtrim($baseUrl, '/');
$token = \PpPhp\Core\Security::CSRF()->Token();
?>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> · Updates</title>
    <?php include constant('BASE_PATH') . '/core/views/favicon.php'; ?>
    <link href="<?= $assetsUrl ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="<?= $assetsUrl ?>/icons/bootstrap-icons.css" rel="stylesheet">
    <?php include constant('BASE_PATH') . '/core/views/theme-head.php'; ?>
    <link href="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.css" rel="stylesheet">
    <script src="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.js"></script>
    <style>
        body { background: var(--pp-app-bg, #f5f6fa); color: var(--pp-text, #172033); }
        .ppu-topbar { background: var(--pp-surface, #fff); border-bottom: 1px solid var(--pp-border, #e6e9f0);
            position: sticky; top: 0; z-index: 1030; }
        .ppu-wrap { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
        .ppu-card { background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0);
                    border-radius: 14px; padding: 1.25rem 1.35rem; margin-bottom: 1.1rem; }
        .ppu-card h2 { font-size: 1.02rem; font-weight: 800; margin: 0 0 .2rem; }
        .ppu-sub { color: var(--pp-text-muted, #64748b); font-size: .88rem; margin: 0 0 1rem; }
        .ppu-ver { font: 700 1.6rem/1 ui-monospace, Menlo, monospace; }
        .ppu-chk { display: flex; align-items: center; gap: .55rem; font-size: .86rem; padding: .18rem 0; }
        .ppu-chk .bi-check-circle-fill { color: #16a34a; } .ppu-chk .bi-x-circle-fill { color: #dc2626; }
        .ppu-chk small { color: var(--pp-text-muted, #64748b); margin-left: auto; }
        .ppu-drop { border: 2px dashed var(--pp-border, #cbd5e1); border-radius: 12px; padding: 1.6rem;
                    text-align: center; cursor: pointer; transition: border-color .15s, background .15s; }
        .ppu-drop:hover, .ppu-drop.drag { border-color: var(--pp-accent, #4f46e5);
                    background: color-mix(in srgb, var(--pp-accent, #4f46e5) 6%, transparent); }
        .ppu-safe { background: color-mix(in srgb, #16a34a 8%, transparent); border-radius: 10px;
                    padding: .8rem 1rem; font-size: .86rem; }
        .ppu-safe li { margin: .12rem 0; }
        .ppu-plan { font-size: .9rem; }
        .ppu-plan b { font-variant-numeric: tabular-nums; }
        .ppu-rp { display: flex; align-items: center; gap: .6rem; font-size: .85rem;
                  padding: .5rem 0; border-top: 1px solid var(--pp-border, #eef1f6); }
        .ppu-rp code { font-size: .78rem; }
        /* ── hero + decorated Check button ── */
        .ppu-hero { position: relative; overflow: hidden; }
        .ppu-hero::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
                            background: linear-gradient(180deg, var(--pp-accent, #4f46e5), color-mix(in srgb, var(--pp-accent, #4f46e5) 45%, transparent)); }
        .ppu-hero-badge { width: 46px; height: 46px; border-radius: 12px; flex: none; display: grid; place-items: center;
                          font-size: 1.35rem; color: var(--pp-accent, #4f46e5);
                          background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, transparent); }
        .ppu-check-btn { display: inline-flex; align-items: center; gap: .5rem; border: 0; cursor: pointer;
                         padding: .6rem 1.15rem; border-radius: 11px; font-weight: 700; font-size: .9rem; color: #fff;
                         background: linear-gradient(135deg, var(--pp-accent, #4f46e5), color-mix(in srgb, var(--pp-accent, #4f46e5) 62%, #000));
                         box-shadow: 0 6px 18px color-mix(in srgb, var(--pp-accent, #4f46e5) 34%, transparent);
                         transition: transform .12s ease, box-shadow .12s ease, filter .12s ease; }
        .ppu-check-btn:hover { transform: translateY(-1px); filter: brightness(1.05);
                         box-shadow: 0 10px 26px color-mix(in srgb, var(--pp-accent, #4f46e5) 46%, transparent); }
        .ppu-check-btn:active { transform: translateY(0); box-shadow: 0 4px 12px color-mix(in srgb, var(--pp-accent, #4f46e5) 34%, transparent); }
        .ppu-check-btn .bi { font-size: 1rem; }
        .ppu-check-btn.busy { pointer-events: none; opacity: .8; }
        .ppu-check-btn.busy .bi { animation: ppu-spin .9s linear infinite; }
        .ppu-feed-hint { display: inline-flex; align-items: center; gap: .35rem; }
        .ppu-dot { width: 7px; height: 7px; border-radius: 50%; background: #16a34a; flex: none;
                   box-shadow: 0 0 0 0 color-mix(in srgb, #16a34a 60%, transparent); animation: ppu-pulse 2.2s ease-out infinite; }
        @keyframes ppu-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, #16a34a 55%, transparent); }
                               70% { box-shadow: 0 0 0 6px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
        /* ── foldable sections ── */
        .ppu-fold { padding: 0; overflow: hidden; }
        .ppu-fold-head { width: 100%; display: flex; align-items: center; gap: .6rem; text-align: left;
                         background: none; border: 0; padding: .95rem 1.35rem; cursor: pointer; color: var(--pp-text, #172033);
                         font-size: 1.02rem; font-weight: 800; transition: background .12s ease; }
        .ppu-fold-head:hover { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 5%, transparent); }
        .ppu-fold-head .lead-ic { color: var(--pp-accent, #4f46e5); }
        .ppu-fold-meta { margin-left: auto; font-size: .74rem; font-weight: 700; letter-spacing: .01em;
                         padding: .16rem .5rem; border-radius: 999px; white-space: nowrap;
                         color: var(--pp-text-muted, #64748b); background: color-mix(in srgb, var(--pp-text-muted, #64748b) 12%, transparent); }
        .ppu-fold-meta.ok { color: #16a34a; background: color-mix(in srgb, #16a34a 13%, transparent); }
        .ppu-fold-meta.warn { color: #d97706; background: color-mix(in srgb, #d97706 15%, transparent); }
        .ppu-chev { color: var(--pp-text-muted, #94a3b8); font-size: .85rem; transition: transform .26s ease; flex: none; }
        .ppu-fold.open .ppu-chev { transform: rotate(180deg); }
        .ppu-fold-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .28s ease; }
        .ppu-fold.open .ppu-fold-body { grid-template-rows: 1fr; }
        .ppu-fold-inner { overflow: hidden; min-height: 0; padding: 0 1.35rem; }
        .ppu-fold.open .ppu-fold-inner { padding-bottom: 1.25rem; }
        .ppu-fold-inner > :first-child { margin-top: 0; }
        /* ── "what happens" flow strip ── */
        .ppu-flow { display: flex; align-items: stretch; gap: .4rem; flex-wrap: wrap; }
        .ppu-fstep { flex: 1 1 170px; min-width: 155px; border: 1px solid var(--pp-border, #e6e9f0); border-radius: 12px;
            padding: .7rem .8rem; background: var(--pp-surface-soft, #f8fafc); }
        .ppu-fstep .n { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;
            background: var(--pp-accent, #4f46e5); color: var(--pp-accent-contrast, #fff);
            font-size: .7rem; font-weight: 800; margin-bottom: .35rem; }
        .ppu-fstep strong { display: block; font-size: .86rem; margin-bottom: .15rem; }
        .ppu-fstep small { font-size: .76rem; line-height: 1.45; color: var(--pp-text-muted, #64748b); }
        .ppu-farrow { display: flex; align-items: center; color: var(--pp-text-muted, #94a3b8); flex: 0 0 auto; }
        @media (max-width: 780px) { .ppu-farrow { display: none; } }
        .ppu-note-online { display: flex; gap: .6rem; align-items: flex-start; margin-top: .9rem; padding: .7rem .85rem;
            border-radius: 11px; font-size: .82rem; line-height: 1.5;
            background: color-mix(in srgb, #0ea5e9 12%, var(--pp-surface, #fff));
            border: 1px solid color-mix(in srgb, #0ea5e9 34%, var(--pp-border, #e6e9f0)); }
        .ppu-note-online i { color: #0284c7; font-size: 1.05rem; margin-top: 1px; }

        /* ── phased progress overlay ── */
        .ppu-ov { position: fixed; inset: 0; z-index: 9999; display: none; align-items: center; justify-content: center;
                  background: color-mix(in srgb, var(--pp-app-bg,#0b1220) 74%, transparent); backdrop-filter: blur(2px); }
        .ppu-ov.on { display: flex; }
        .ppu-ov-card { background: var(--pp-surface,#fff); border: 1px solid var(--pp-border,#e6e9f0); border-radius: 16px;
                       padding: 1.6rem 1.8rem; max-width: 460px; width: 92%; box-shadow: 0 24px 70px rgba(0,0,0,.4); }
        .ppu-ov-h { display: flex; align-items: center; gap: .6rem; font-weight: 800; font-size: 1.05rem; margin-bottom: 1rem; color: var(--pp-text,#172033); }
        .ppu-steps { list-style: none; margin: 0; padding: 0; }
        .ppu-step { display: flex; align-items: center; gap: .6rem; font-size: .9rem; padding: .32rem 0; color: var(--pp-text-muted,#64748b); }
        .ppu-step .ic { width: 20px; height: 20px; flex: none; display: grid; place-items: center; }
        .ppu-step.active, .ppu-step.done { color: var(--pp-text,#172033); }
        .ppu-step.active { font-weight: 600; }
        .ppu-step.done .ic { color: #16a34a; }
        .ppu-step.err { color: #dc2626; } .ppu-step.err .ic { color: #dc2626; }
        .ppu-spin { width: 15px; height: 15px; border-radius: 50%; border: 3px solid color-mix(in srgb,var(--pp-accent,#4f46e5) 25%,transparent);
                    border-top-color: var(--pp-accent,#4f46e5); animation: ppu-spin .8s linear infinite; }
        @keyframes ppu-spin { to { transform: rotate(360deg); } }
        .ppu-ov-msg { font-size: .82rem; color: var(--pp-text-muted,#64748b); margin-top: .95rem; min-height: 1.1em; }
        /* Generic "working…" lock: reuse the full-page overlay but drop the
           OTA-specific step list and show a single spinner + message. Used while
           a bundle is being inspected or installed, so the page is visibly locked
           and un-clickable rather than looking idle. */
        .ppu-ov.busy .ppu-steps { display: none; }
        .ppu-busy-row { display: flex; align-items: center; gap: .75rem; font-size: .95rem; color: var(--pp-text,#172033); }
        .ppu-busy-row .ppu-spin { width: 22px; height: 22px; border-width: 3px; flex: none; }
        .ppu-busy-sub { font-size: .8rem; color: var(--pp-text-muted,#64748b); margin-top: .7rem; }
    </style>
</head>
<body>
<div class="ppu-ov" id="ppuOv" role="alertdialog" aria-live="assertive" aria-label="Update progress">
    <div class="ppu-ov-card">
        <div class="ppu-ov-h"><i class="bi bi-arrow-repeat"></i> <span id="ppuOvTitle">Checking for updates…</span></div>
        <ul class="ppu-steps" id="ppuSteps">
            <li class="ppu-step" data-step="license"><span class="ic"></span> Verifying this is a licensed copy</li>
            <li class="ppu-step" data-step="host"><span class="ic"></span> Reading the signed update host</li>
            <li class="ppu-step" data-step="pick"><span class="ic"></span> Finding the right update for this version</li>
            <li class="ppu-step" data-step="download"><span class="ic"></span> Downloading &amp; verifying</li>
            <li class="ppu-step" data-step="apply"><span class="ic"></span> Applying &amp; patching (restore point first)</li>
        </ul>
        <div class="ppu-ov-msg" id="ppuOvMsg"></div>
    </div>
</div>
<div class="ppu-topbar py-2 px-3 d-flex align-items-center gap-2">
    <a href="<?= $b ?>/projects" class="btn btn-sm btn-link text-decoration-none"><i class="bi bi-arrow-left"></i> Projects</a>
    <strong class="ms-1">Platform Updates</strong>
    <div class="ms-auto d-flex align-items-center gap-2">
        <?php include constant('BASE_PATH') . '/core/views/theme-toggle.php'; ?>
        <span class="small text-muted"><i class="bi bi-person-circle"></i> <?= htmlspecialchars($user['username'] ?? '') ?></span>
    </div>
</div>

<div class="ppu-wrap">

    <div class="ppu-card ppu-hero d-flex align-items-center gap-3">
        <div class="ppu-hero-badge"><i class="bi bi-rocket-takeoff"></i></div>
        <div>
            <div class="ppu-sub mb-1">Installed version</div>
            <div class="ppu-ver"><?= htmlspecialchars($installedVersion) ?></div>
            <?php if (!empty($installed['codename'])): ?>
                <div class="small text-muted"><?= htmlspecialchars((string) $installed['codename']) ?></div>
            <?php endif; ?>
        </div>
        <div class="ms-auto text-end">
            <?php if (!($updatesLocked ?? false)): ?>
            <button class="ppu-check-btn" id="btnCheck">
                <i class="bi bi-cloud-arrow-down"></i> Check for Updates
            </button>
            <div class="small text-muted mt-2 ppu-feed-hint justify-content-end">
                <span class="ppu-dot"></span> Checks the signed release feed online
            </div>
            <?php else: ?>
            <span class="ppu-check-btn" style="opacity:.55;cursor:not-allowed" title="Activate a licence to enable updates"><i class="bi bi-lock-fill"></i> Locked — not activated</span>
            <?php endif; ?>
            <?php if (\PpPhp\Core\Security::IsOwner() && !\PpPhp\Core\Licensing\LicenseGate::isClientEdition()): ?>
                <div class="small mt-1"><a href="<?= $b ?>/platform/app-bundle" class="text-decoration-none"><i class="bi bi-box-seam"></i> App Bundle (owner)</a></div>
            <?php endif; ?>
        </div>
    </div>

    <?php if ($updatesLocked ?? false): ?>
    <div class="ppu-card" style="border-color:#f59e0b66;background:color-mix(in srgb,#f59e0b 8%,var(--pp-surface,#fff))">
        <div class="ppu-sub mb-1" style="color:#b45309;font-weight:800;font-size:1rem"><i class="bi bi-shield-lock-fill me-1"></i> This copy is not yet activated</div>
        <p class="mb-2">Updates are unavailable until this installation has a valid licence. Open
            <a href="<?= $b ?>/licensing">Licence &amp; activation</a> and enter the activation code from your provider —
            once activated, <strong>Check for Updates</strong> is enabled.</p>
        <?php if (!empty($supportContact)): ?>
        <p class="small text-muted mb-0">Need a code? Contact <strong><?= htmlspecialchars((string) $supportContact) ?></strong>.</p>
        <?php endif; ?>
    </div>
    <?php else: ?>
    <?php /* A newer release the background notifier already spotted. Prominent,
             dismiss-free (it disappears once the copy is updated), and its CTA
             just runs the same Check-for-Updates the button below does. */ ?>
    <?php if (!empty($updateAvailable)): ?>
    <div class="ppu-avail" role="status">
        <div class="ppu-avail-ic"><i class="bi bi-rocket-takeoff-fill"></i></div>
        <div class="ppu-avail-body">
            <strong>Update available — version <?= htmlspecialchars((string) $updateAvailable) ?></strong>
            <span>A newer platform release is ready. You're on <?= htmlspecialchars((string) $installedVersion) ?>. Review and install it below — nothing changes until you confirm.</span>
        </div>
        <button type="button" class="ppu-avail-cta" onclick="var b=document.getElementById('btnCheck');if(b){b.scrollIntoView({behavior:'smooth',block:'center'});b.click();}">
            <i class="bi bi-cloud-arrow-down"></i> Check now
        </button>
    </div>
    <style>
        .ppu-avail { display:flex; align-items:center; gap:14px; padding:16px 18px; margin-bottom:18px;
            border-radius:16px; border:1px solid color-mix(in srgb, var(--pp-accent,#4f46e5) 38%, transparent);
            background:linear-gradient(120deg, color-mix(in srgb, var(--pp-accent,#4f46e5) 14%, var(--pp-surface,#fff)),
                color-mix(in srgb, var(--pp-accent,#4f46e5) 5%, var(--pp-surface,#fff)));
            box-shadow:0 10px 30px color-mix(in srgb, var(--pp-accent,#4f46e5) 16%, transparent); }
        .ppu-avail-ic { flex:0 0 44px; width:44px; height:44px; border-radius:12px; display:flex; align-items:center;
            justify-content:center; font-size:20px; color:#fff;
            background:linear-gradient(135deg, var(--pp-accent,#4f46e5), color-mix(in srgb, var(--pp-accent,#4f46e5) 60%, #000));
            animation: ppu-avail-float 2.6s ease-in-out infinite; }
        @keyframes ppu-avail-float { 0%,100%{ transform:translateY(0);} 50%{ transform:translateY(-3px);} }
        .ppu-avail-body { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
        .ppu-avail-body strong { font-size:1rem; color:var(--pp-text,#1e293b); }
        .ppu-avail-body span { font-size:.83rem; color:var(--pp-text-muted,#64748b); line-height:1.5; }
        .ppu-avail-cta { flex:0 0 auto; border:0; cursor:pointer; border-radius:10px; padding:.6rem 1rem;
            font-weight:800; font-size:.85rem; color:#fff; display:inline-flex; align-items:center; gap:.45rem;
            background:linear-gradient(135deg, var(--pp-accent,#4f46e5), color-mix(in srgb, var(--pp-accent,#4f46e5) 62%, #000));
            box-shadow:0 6px 16px color-mix(in srgb, var(--pp-accent,#4f46e5) 30%, transparent); transition:transform .15s, filter .15s; }
        .ppu-avail-cta:hover { transform:translateY(-1px); filter:brightness(1.06); }
        @media (max-width:640px){ .ppu-avail{ flex-wrap:wrap; } .ppu-avail-cta{ width:100%; justify-content:center; } }
        @media (prefers-reduced-motion: reduce){ .ppu-avail-ic{ animation:none; } }
    </style>
    <?php endif; ?>
    <!-- Four steps, at a glance. Updating is the one operation people hesitate
         over, so the whole sequence is shown before they start rather than only
         while it runs. -->
    <div class="ppu-card">
        <div class="ppu-sub mb-2"><i class="bi bi-signpost-split"></i> What happens when you update</div>
        <div class="ppu-flow">
            <div class="ppu-fstep"><span class="n">1</span><strong>Check</strong><small>We read the signed release feed and find the right update for your version.</small></div>
            <div class="ppu-farrow"><i class="bi bi-chevron-right"></i></div>
            <div class="ppu-fstep"><span class="n">2</span><strong>Verify</strong><small>The download is checked against its signature before a single file is touched.</small></div>
            <div class="ppu-farrow"><i class="bi bi-chevron-right"></i></div>
            <div class="ppu-fstep"><span class="n">3</span><strong>Restore point</strong><small>A rollback snapshot is taken automatically, so you can undo it.</small></div>
            <div class="ppu-farrow"><i class="bi bi-chevron-right"></i></div>
            <div class="ppu-fstep"><span class="n">4</span><strong>Apply</strong><small>Only changed platform files are replaced. Takes a few seconds.</small></div>
        </div>
        <?php if (class_exists(\PpPhp\Core\Licensing\ProtectedCode::class)
                  && \PpPhp\Core\Licensing\ProtectedCode::isProtectedBundle()): ?>
            <div class="ppu-note-online">
                <i class="bi bi-wifi"></i>
                <div><strong>Stay online for the first build after updating.</strong>
                    This copy is licence-protected, so after an update it collects a fresh key from the licence
                    server before you can build again. It happens automatically on your next check-in — just make
                    sure this server can reach the internet once.</div>
            </div>
        <?php endif; ?>
    </div>

    <?php /* Auto-install critical security patches — opt-out (default on). Only
             ever applies SECURITY-flagged, signed releases your provider pushes;
             ordinary updates still wait for a human. */ ?>
    <?php if ($canLocalUpdate ?? false): ?>
    <div class="ppu-card ppu-secauto">
        <div class="ppu-secauto-ic"><i class="bi bi-shield-check"></i></div>
        <div class="ppu-secauto-body">
            <strong>Auto-install critical security patches</strong>
            <span>When your provider pushes a signed <em>critical</em> security fix, this copy installs it automatically — signature-verified, with a restore point taken first. Ordinary updates still wait for you.<?php if (empty($isClientEdition)): ?> <a href="<?= $b ?>/help/advisories" target="_blank" rel="noopener">How pushing to clients works →</a><?php endif; ?></span>
        </div>
        <label class="ppu-switch" title="Toggle automatic critical security patches">
            <input type="checkbox" id="secAuto" <?= ($autoSecurity ?? true) ? 'checked' : '' ?>>
            <span class="ppu-switch-track"></span>
        </label>
    </div>
    <style>
        .ppu-secauto { display:flex; align-items:center; gap:14px; }
        .ppu-secauto-ic { flex:0 0 42px; width:42px; height:42px; border-radius:11px; display:flex; align-items:center;
            justify-content:center; font-size:19px; color:#fff;
            background:linear-gradient(135deg,#10b981,#059669); }
        .ppu-secauto-body { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
        .ppu-secauto-body strong { font-size:.95rem; }
        .ppu-secauto-body span { font-size:.82rem; color:var(--pp-text-muted,#64748b); line-height:1.5; }
        .ppu-switch { position:relative; flex:0 0 auto; width:46px; height:26px; cursor:pointer; }
        .ppu-switch input { position:absolute; opacity:0; width:0; height:0; }
        .ppu-switch-track { position:absolute; inset:0; border-radius:999px; background:var(--pp-border-strong,#cbd5e1); transition:background .18s; }
        .ppu-switch-track::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%;
            background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.28); transition:transform .18s; }
        .ppu-switch input:checked + .ppu-switch-track { background:#10b981; }
        .ppu-switch input:checked + .ppu-switch-track::after { transform:translateX(20px); }
        .ppu-switch input:focus-visible + .ppu-switch-track { outline:2px solid var(--pp-accent,#4f46e5); outline-offset:2px; }
    </style>
    <script>
    (function () {
        var el = document.getElementById('secAuto'); if (!el) return;
        el.addEventListener('change', function () {
            fetch(<?= json_encode($b) ?> + '/platform/updates/security-auto', {
                method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin',
                body: JSON.stringify({ enabled: el.checked, _token: <?= json_encode($token) ?> })
            }).then(function (r) { return r.json(); }).then(function (j) {
                if (typeof Swal !== 'undefined') {
                    Swal.fire({ toast: true, position: 'top-end', icon: (j && j.success) ? 'success' : 'error',
                        title: (j && j.success) ? (el.checked ? 'Critical patches will auto-install' : 'Auto-install turned off')
                                                : 'Could not save', showConfirmButton: false, timer: 2000, timerProgressBar: true });
                }
                if (!j || !j.success) el.checked = !el.checked; // revert on failure
            }).catch(function () { el.checked = !el.checked; });
        });
    }());
    </script>
    <?php endif; ?>

    <?php /* Update Locally — available to any ADMIN of a licensed install, so a
             customer can update their own copy even when OTA can't reach the host. */ ?>
    <?php if ($canLocalUpdate ?? ($isOwner ?? true)): ?>
    <div class="ppu-card ppu-fold" data-fold="local">
        <button type="button" class="ppu-fold-head">
            <i class="bi bi-box-arrow-in-down lead-ic"></i> <span>Update Locally</span>
            <span class="ppu-fold-meta">Upload a .zip</span>
            <i class="bi bi-chevron-down ppu-chev"></i>
        </button>
        <div class="ppu-fold-body"><div class="ppu-fold-inner">
        <p class="ppu-sub">Upload a release bundle (<code>.zip</code>). You'll see exactly what will change before anything is applied.
            <span class="text-muted">Max upload here: <strong><?= htmlspecialchars($uploadCeilingHuman ?? 'unknown') ?></strong>.</span></p>

        <form id="frmUp" enctype="multipart/form-data">
            <input type="hidden" name="_token" value="<?= htmlspecialchars($token) ?>">
            <div class="ppu-drop" id="drop">
                <i class="bi bi-file-earmark-zip fs-3 d-block mb-2"></i>
                <strong id="dropLabel">Choose a release bundle, or drop it here</strong>
                <div class="small text-muted mt-1">Nothing is installed until you confirm</div>
                <input type="file" name="bundle" id="bundle" accept=".zip" hidden>
            </div>
        </form>

        <div id="report" class="mt-3 d-none">
            <div class="ppu-plan mb-2" id="planText"></div>
            <div id="planWarn"></div>
            <button class="btn btn-primary btn-sm" id="btnApply"><i class="bi bi-check-lg"></i> Install this update</button>
            <button class="btn btn-link btn-sm text-muted" id="btnCancel">Cancel</button>
        </div>
        </div></div>
    </div>
    <?php endif; ?>

    <?php /* The remaining advanced surfaces (restore points, full-bundle fetch,
             auto-update) stay OWNER-only on a distributed client. */ ?>
    <?php if ($isOwner ?? true): ?>
    <div class="ppu-card ppu-fold" data-fold="safety">
        <button type="button" class="ppu-fold-head">
            <i class="bi bi-shield-check lead-ic"></i> <span>Your data is never touched</span>
            <span class="ppu-fold-meta ok">Protected</span>
            <i class="bi bi-chevron-down ppu-chev"></i>
        </button>
        <div class="ppu-fold-body"><div class="ppu-fold-inner">
            <p class="ppu-sub">An update replaces platform code only. These are refused even if a bundle contains them:</p>
            <div class="ppu-safe">
                <ul class="mb-0">
                    <li><code>projects/</code> — every user's tables, pages, designs, events and custom files</li>
                    <li><code>output/</code> — generated applications</li>
                    <li><code>storage/</code> — logs, backups and restore points</li>
                    <li><code>config/database.php</code> — database credentials</li>
                </ul>
            </div>
            <p class="ppu-sub mt-3 mb-0">Accounts, permissions and settings live in the database and are not affected.
               New tables and columns are added automatically on first use; nothing is dropped.</p>
        </div></div>
    </div>

    <?php $failCount = 0; foreach ($preflight['checks'] as $c) { if (empty($c['ok'])) $failCount++; } ?>
    <div class="ppu-card ppu-fold" data-fold="readiness">
        <button type="button" class="ppu-fold-head">
            <i class="bi bi-clipboard-check lead-ic"></i> <span>Server readiness</span>
            <?php if ($failCount === 0): ?>
                <span class="ppu-fold-meta ok"><i class="bi bi-check-lg"></i> All checks passed</span>
            <?php else: ?>
                <span class="ppu-fold-meta warn"><i class="bi bi-exclamation-triangle"></i> <?= $failCount ?> to fix</span>
            <?php endif; ?>
            <i class="bi bi-chevron-down ppu-chev"></i>
        </button>
        <div class="ppu-fold-body"><div class="ppu-fold-inner">
            <p class="ppu-sub">Checked when this page loaded.</p>
            <?php foreach ($preflight['checks'] as $c): ?>
                <div class="ppu-chk">
                    <i class="bi <?= $c['ok'] ? 'bi-check-circle-fill' : 'bi-x-circle-fill' ?>"></i>
                    <span><?= htmlspecialchars($c['label']) ?></span>
                    <small><?= htmlspecialchars($c['detail']) ?></small>
                </div>
            <?php endforeach; ?>
            <?php if (!$preflight['ok']): ?>
                <div class="alert alert-warning mt-3 mb-0 py-2 small">
                    Fix the items marked in red before updating — usually a folder permission on the host.
                </div>
            <?php endif; ?>
        </div></div>
    </div>

    <!-- Large-bundle paths: fetch the full server-side, or FTP-drop it manually. -->
    <div class="ppu-card ppu-fold" data-fold="ftp">
        <button type="button" class="ppu-fold-head">
            <i class="bi bi-hdd-network lead-ic"></i> <span>Full update (large bundles)</span>
            <span class="ppu-fold-meta">No upload limit</span>
            <i class="bi bi-chevron-down ppu-chev"></i>
        </button>
        <div class="ppu-fold-body"><div class="ppu-fold-inner">
        <p class="ppu-sub mb-2"><strong>Fetch the full bundle from your update host</strong> — the server pulls it directly (no browser
            upload limit) and shows the plan before anything changes. Use this when an over-the-air delta won't apply
            (e.g. “a file differs on this server”) or the bundle is larger than the upload limit above.</p>
        <button class="btn btn-primary btn-sm mb-3" id="btnFetchFull"><i class="bi bi-cloud-arrow-down"></i> Fetch full &amp; install</button>

        <p class="ppu-sub">
            <span class="text-muted">Or, manually:</span> drop the <code>.zip</code> straight into
            <code><?= htmlspecialchars($stageDirRel ?? 'storage/updates/incoming') ?></code> with your FTP client, then click <em>Refresh</em>.
            It installs from there with no upload size limit.
        </p>
        <button class="btn btn-outline-secondary btn-sm mb-2" id="btnRescan"><i class="bi bi-arrow-clockwise"></i> Refresh</button>
        <div id="stagedList">
            <?php if (empty($stagedBundles)): ?>
                <div class="text-muted small">No bundles found in that folder yet.</div>
            <?php else: foreach ($stagedBundles as $sb): ?>
                <div class="ppu-rp">
                    <i class="bi bi-file-earmark-zip"></i>
                    <code><?= htmlspecialchars($sb['name']) ?></code>
                    <span class="text-muted small"><?= number_format($sb['size'] / 1048576, 1) ?> MB</span>
                    <button class="btn btn-outline-primary btn-sm ms-auto btn-staged" data-bundle="<?= htmlspecialchars($sb['name']) ?>">
                        <i class="bi bi-search"></i> Inspect &amp; install
                    </button>
                </div>
            <?php endforeach; endif; ?>
        </div>
        </div></div>
    </div>

    <?php endif; /* $isOwner — owner-only advanced cards */ ?>

    <?php /* Restore points / rollback are SELF-RECOVERY: shown to any admin who can
             update this licensed install (not just the vendor owner), so a customer
             can undo a bad update themselves rather than needing FTP or the vendor. */ ?>
    <?php if (($canLocalUpdate ?? false) && $restorePoints): ?>
    <div class="ppu-card ppu-fold" data-fold="restore">
        <button type="button" class="ppu-fold-head">
            <i class="bi bi-arrow-counterclockwise lead-ic"></i> <span>Restore points</span>
            <span class="ppu-fold-meta"><?= count($restorePoints) ?> saved</span>
            <i class="bi bi-chevron-down ppu-chev"></i>
        </button>
        <div class="ppu-fold-body"><div class="ppu-fold-inner">
        <p class="ppu-sub">Every update snapshots the files it changes, so you can go straight back.</p>
        <?php foreach (array_slice($restorePoints, 0, 8) as $rp): ?>
            <div class="ppu-rp">
                <code><?= htmlspecialchars($rp['id']) ?></code>
                <span class="text-muted">
                    <?= htmlspecialchars((string) ($rp['info']['fromVersion'] ?? '?')) ?>
                    → <?= htmlspecialchars((string) ($rp['info']['toVersion'] ?? '?')) ?>
                </span>
                <button class="btn btn-outline-secondary btn-sm ms-auto btn-roll"
                        data-id="<?= htmlspecialchars($rp['id']) ?>">Roll back to this</button>
                <button class="btn btn-outline-danger btn-sm btn-rp-del"
                        data-id="<?= htmlspecialchars($rp['id']) ?>" title="Delete this restore point and its files">Delete</button>
            </div>
        <?php endforeach; ?>
        </div></div>
    </div>
    <?php endif; ?>
    <?php endif; /* !$updatesLocked — the licensed update body */ ?>
</div>

<script>
(function () {
    const B = <?= json_encode($b) ?>;
    let TOKEN = <?= json_encode($token) ?>;
    // Vendor's friendly name for the update service — shown in failure messages so
    // a real host/path is never exposed to the customer.
    const UPDATE_SVC = <?= json_encode($updateServiceName ?? 'the update service') ?>;

    // ── Foldable sections: folded on first load, choice remembered per browser ──
    const FOLD_KEY = 'ppu.folds.v1';
    const foldState = (() => { try { return JSON.parse(localStorage.getItem(FOLD_KEY) || '{}') || {}; } catch (e) { return {}; } })();
    const saveFolds = () => { try { localStorage.setItem(FOLD_KEY, JSON.stringify(foldState)); } catch (e) {} };
    document.querySelectorAll('.ppu-fold').forEach(card => {
        const key = card.dataset.fold, head = card.querySelector('.ppu-fold-head');
        if (foldState[key] === true) card.classList.add('open');         // default = folded
        head.setAttribute('aria-expanded', card.classList.contains('open') ? 'true' : 'false');
        head.addEventListener('click', () => {
            const open = card.classList.toggle('open');
            head.setAttribute('aria-expanded', open ? 'true' : 'false');
            foldState[key] = open; saveFolds();
        });
    });

    const drop = document.getElementById('drop'), input = document.getElementById('bundle');

    // A long-open page can outlive its CSRF token (a re-login rotates it). Rather
    // than fail an upload with "invalid security token", pull a fresh token and
    // retry once. Same-origin only, so it stays safe.
    async function freshToken() {
        try {
            const r = await fetch(B + '/platform/updates/token', { credentials: 'same-origin' });
            const j = await r.json();
            if (j && j.token) {
                TOKEN = j.token;
                const hid = document.querySelector('#frmUp [name="_token"]');
                if (hid) hid.value = TOKEN;
                return true;
            }
        } catch (e) {}
        return false;
    }
    const isTokenError = (r, j) => r && r.status === 400 && j && /security token/i.test(j.message || '');

    // POST JSON with automatic one-shot token refresh on a stale token.
    async function jsonPost(path, payload, failMsg) {
        const call = () => fetch(B + path, {
            method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(Object.assign({}, payload, { _token: TOKEN }))
        });
        let r = await call();
        let j = await r.json().catch(() => ({ success: false, message: failMsg }));
        if (isTokenError(r, j) && await freshToken()) {
            r = await call();
            j = await r.json().catch(() => ({ success: false, message: failMsg }));
        }
        return j;
    }
    const report = document.getElementById('report'), planText = document.getElementById('planText');
    const planWarn = document.getElementById('planWarn'), dropLabel = document.getElementById('dropLabel');
    let staged = null;

    // The "Update Locally" drop-zone is OWNER-only, so #drop / #bundle are absent on
    // a client edition. Guard every binding — an unguarded drop.addEventListener here
    // would throw and kill the whole script, taking "Check for Updates" down with it.
    if (drop && input) {
        drop.addEventListener('click', () => input.click());
        ['dragenter','dragover'].forEach(e => drop.addEventListener(e, ev => { ev.preventDefault(); drop.classList.add('drag'); }));
        ['dragleave','drop'].forEach(e => drop.addEventListener(e, ev => { ev.preventDefault(); drop.classList.remove('drag'); }));
        drop.addEventListener('drop', ev => { if (ev.dataTransfer.files[0]) { input.files = ev.dataTransfer.files; send(); } });
        input.addEventListener('change', () => input.files[0] && send());
    }

    async function uploadOnce() {
        const fd = new FormData(document.getElementById('frmUp'));   // picks up the current hidden _token
        const r = await fetch(B + '/platform/updates/upload', { method:'POST', body: fd, credentials:'same-origin' });
        const j = await r.json().catch(() => ({ success:false, message:'Upload failed.' }));
        return { r, j };
    }

    // Show / hide the "here's the plan" panel — shared by upload and FTP-drop.
    function showPlan(j) {
        staged = j.bundle;
        var sigBadge = j.signed
            ? '<span class="badge bg-success-subtle text-success-emphasis"><i class="bi bi-patch-check-fill"></i> Verified publisher signature</span>'
            : '<span class="badge bg-secondary-subtle text-secondary-emphasis" title="This bundle carries no publisher signature — apply only if you obtained it directly from your provider."><i class="bi bi-shield-exclamation"></i> Unsigned bundle</span>';
        planText.innerHTML = 'Version <b>' + j.current + '</b> → <b>' + j.version + '</b>' +
            (j.codename ? ' <span class="text-muted">(' + j.codename + ')</span>' : '') +
            ' ' + sigBadge + '<br>' +
            '<b>' + j.summary.update + '</b> files updated · <b>' + j.summary.add + '</b> added · ' +
            '<b>' + j.summary.remove + '</b> removed · <b>' + j.summary.unchanged + '</b> unchanged';
        planWarn.innerHTML = (j.warnings || []).map(w =>
            '<div class="alert alert-warning py-2 px-3 small mb-2">' + w + '</div>').join('');
        report.classList.remove('d-none');
        report.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
    function bundleRejected(j) {
        report.classList.add('d-none');
        Swal.fire({ icon:'error', title:'That bundle was not accepted',
                    html: '<div style="text-align:left;font-size:.9rem">' +
                          ((j.errors && j.errors.length ? j.errors : [j.message]).map(e => '• ' + e).join('<br>')) + '</div>' });
    }

    async function send() {
        if (ppuBusy) return;
        busyShow('Uploading & inspecting…', 'Checking ' + esc2(input.files[0].name) + '…');
        dropLabel.textContent = 'Checking ' + input.files[0].name + '…';
        try {
            let { r, j } = await uploadOnce();
            if (isTokenError(r, j) && await freshToken()) { ({ r, j } = await uploadOnce()); }
            busyHide();
            dropLabel.textContent = 'Choose a release bundle, or drop it here';
            if (!j.success || !j.ok) { bundleRejected(j); return; }
            dropLabel.textContent = input.files[0].name;
            showPlan(j);
        } catch (e) {
            busyHide();
            dropLabel.textContent = 'Choose a release bundle, or drop it here';
            Swal.fire({ icon:'error', title:'Upload failed', text:'Please try again.' });
        }
    }

    // FTP-drop: inspect a bundle already in storage/updates/incoming, then reuse the same plan/apply UI.
    document.querySelectorAll('.btn-staged').forEach(btn => btn.addEventListener('click', async () => {
        if (ppuBusy) return;                                  // ignore repeat clicks while working
        busyShow('Inspecting bundle…', 'Reading and verifying ' + esc2(btn.dataset.bundle) + '…');
        try {
            const j = await jsonPost('/platform/updates/inspect-staged', { bundle: btn.dataset.bundle }, 'Could not read that bundle.');
            busyHide();
            if (!j.success || !j.ok) { bundleRejected(j); return; }
            showPlan(j);
        } catch (e) { busyHide(); Swal.fire({ icon:'error', title:'Could not read that bundle', text:'Please try again.' }); }
    }));
    document.getElementById('btnRescan')?.addEventListener('click', () => location.reload());

    // Fetch the FULL bundle from the update host, server-side (no upload cap; skips
    // the delta so a drifted install still updates), then reuse the plan/apply UI.
    document.getElementById('btnFetchFull')?.addEventListener('click', async () => {
        if (ppuBusy) return;
        busyShow('Fetching the full bundle…', 'Downloading the full release from your update host and verifying it. This can take a minute.');
        try {
            const j = await jsonPost('/platform/updates/fetch-full', {}, 'Could not fetch the full bundle.');
            busyHide();
            if (!j.success) { Swal.fire({ icon:'error', title:'Could not fetch the full bundle', text: j.message || 'Please try again.' }); return; }
            if (!j.ok) { bundleRejected(j); return; }
            showPlan(j);
        } catch (e) { busyHide(); Swal.fire({ icon:'error', title:'Could not fetch the full bundle', text:'Please try again.' }); }
    });

    document.getElementById('btnCancel')?.addEventListener('click', () => {
        report.classList.add('d-none'); staged = null; input.value = '';
        dropLabel.textContent = 'Choose a release bundle, or drop it here';
    });

    document.getElementById('btnApply')?.addEventListener('click', async () => {
        const ok = await Swal.fire({ icon:'question', title:'Install this update?',
            text:'The site goes into maintenance for a few seconds. Your projects and data are not touched.',
            showCancelButton:true, confirmButtonText:'Install now' });
        if (!ok.isConfirmed) return;
        Swal.fire({ title:'Installing…', allowOutsideClick:false, didOpen: () => Swal.showLoading() });
        const j = await jsonPost('/platform/updates/apply', { bundle: staged }, 'The update could not be applied.');
        if (j.success) {
            await Swal.fire({ icon:'success', title:'Updated to ' + j.version,
                html: j.written + ' files written, ' + j.removed + ' removed.<br><small>Restore point ' + j.restoreId + '</small>' });
            location.reload();
        } else {
            Swal.fire({ icon:'error', title:'Update stopped', text: j.message });
        }
    });

    document.querySelectorAll('.btn-roll').forEach(btn => btn.addEventListener('click', async () => {
        const ok = await Swal.fire({ icon:'warning', title:'Roll back?',
            text:'Platform files return to the state captured in this restore point.',
            showCancelButton:true, confirmButtonText:'Roll back' });
        if (!ok.isConfirmed) return;
        const j = await jsonPost('/platform/updates/rollback', { id: btn.dataset.id }, 'Rollback failed.');
        await Swal.fire({ icon: j.success ? 'success' : 'error', title: j.success ? 'Rolled back' : 'Failed', text: j.message });
        if (j.success) location.reload();
    }));

    document.querySelectorAll('.btn-rp-del').forEach(btn => btn.addEventListener('click', async () => {
        const ok = await Swal.fire({ icon:'warning', title:'Delete this restore point?',
            text:'The snapshot files are permanently removed from the server, and you will no longer be able to roll back to this point.',
            showCancelButton:true, confirmButtonText:'Delete', confirmButtonColor:'#dc2626' });
        if (!ok.isConfirmed) return;
        const j = await jsonPost('/platform/updates/restore-delete', { id: btn.dataset.id }, 'Delete failed.');
        await Swal.fire({ icon: j.success ? 'success' : 'error', title: j.success ? 'Deleted' : 'Failed', text: j.message });
        if (j.success) location.reload();
    }));

    const humanKB = b => b >= 1048576 ? (b/1048576).toFixed(1)+' MB' : b >= 1024 ? Math.round(b/1024)+' KB' : b+' B';
    // ── Phased, dynamic Check for Updates ──
    const ovEl = document.getElementById('ppuOv');
    const esc2 = s => String(s == null ? '' : s).replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
    function stepIcon(st) { return st==='done'?'<i class="bi bi-check-circle-fill"></i>':st==='active'?'<span class="ppu-spin"></span>':st==='err'?'<i class="bi bi-x-circle-fill"></i>':'<i class="bi bi-circle" style="opacity:.3"></i>'; }
    function setStep(name, st) { const li = document.querySelector('#ppuSteps [data-step="'+name+'"]'); if (!li) return; li.className = 'ppu-step ' + (st==='pending'?'':st); li.querySelector('.ic').innerHTML = stepIcon(st); }
    function ovShow(title) { document.getElementById('ppuOvTitle').textContent = title || 'Working…'; ['license','host','pick','download','apply'].forEach(s => setStep(s,'pending')); document.getElementById('ppuOvMsg').innerHTML = ''; ovEl.classList.add('on'); }
    function ovMsg(m) { document.getElementById('ppuOvMsg').innerHTML = m; }
    function ovHide() { ovEl.classList.remove('on'); }

    // ── Generic page lock for inspect / install work ──
    // Shows the full-page overlay in a simple spinner mode so a slow backend
    // action (reading/hashing a 28 MB bundle) never looks idle, and blocks the
    // stray clicks / refreshes / navigations that would interrupt it.
    let ppuBusy = false;
    function busyShow(title, sub) {
        ppuBusy = true;
        document.getElementById('ppuOvTitle').textContent = title || 'Working…';
        ovEl.classList.add('busy');
        document.getElementById('ppuOvMsg').innerHTML =
            '<div class="ppu-busy-row"><span class="ppu-spin"></span><span>' + esc2(sub || 'This can take a moment.') + '</span></div>' +
            '<div class="ppu-busy-sub">Please keep this page open — it will update on its own when finished.</div>';
        ovEl.classList.add('on');
    }
    function busyHide() { ppuBusy = false; ovEl.classList.remove('on', 'busy'); }
    // Last-ditch guard: warn before leaving while an action is mid-flight.
    window.addEventListener('beforeunload', e => { if (ppuBusy) { e.preventDefault(); e.returnValue = ''; } });

    const wait = ms => new Promise(r => setTimeout(r, ms));

    const btnCheck = document.getElementById('btnCheck');
    btnCheck.addEventListener('click', async () => {
        btnCheck.classList.add('busy');
        ovShow('Checking for updates…');
        // Steps 1–3 are performed inside one signed check() call; reveal them as it runs.
        setStep('license','active'); ovMsg('Confirming this installation holds a genuine licence…');
        const checkP = fetch(B + '/platform/updates/check', { credentials:'same-origin' })
            .then(r => r.json())
            .catch(() => ({ success: false, ok: false, available: false, message: 'We couldn’t reach ' + UPDATE_SVC + ' just now. Please try again later.' }));
        await wait(600);
        setStep('license','done'); setStep('host','active'); ovMsg('Reading the signed index on the update host…');
        await wait(600);
        setStep('host','done'); setStep('pick','active'); ovMsg('Matching your version against the available deltas…');
        const j = await checkP;
        setStep('pick','done');
        btnCheck.classList.remove('busy');   // initial check resolved; install phase (if any) uses the overlay

        if (!j.available) {
            ovHide();
            // Two very different outcomes share this branch: the service answered
            // "you're current", or the check couldn't complete. Never render a raw
            // "Version ?" — pick a clean, context-aware title. Failure text already
            // uses the display name (UPDATE_SVC), never a host/path.
            const failed = j.success === false || j.ok === false;
            Swal.fire({
                icon:  failed ? 'info' : 'success',
                title: failed ? 'Couldn’t check for updates' : 'You’re up to date',
                text:  j.message || (failed
                    ? 'We couldn’t reach ' + UPDATE_SVC + ' just now. Please try again later.'
                    : 'You’re running the latest version.')
            });
            return;
        }
        ovHide();
        const sizeTxt = j.size ? ' · ' + humanKB(j.size) + (j.kind === 'delta' ? ' patch' : ' full') : '';
        const ok = await Swal.fire({
            icon:'success', title:'Update ' + j.version + ' available',
            html: '<div style="text-align:left;font-size:.9rem">Installed <b>' + (j.installed||'?') + '</b> → <b>' + j.version + '</b>' + sizeTxt +
                  (j.notes ? '<div class="text-muted mt-2">' + esc2(j.notes) + '</div>' : '') + '</div>',
            showCancelButton:true, confirmButtonText:'Download & install'
        });
        if (!ok.isConfirmed) return;

        // ── Install phase ──
        ovShow('Installing update ' + j.version + '…');
        setStep('license','done'); setStep('host','done'); setStep('pick','done');
        setStep('download','active'); ovMsg('Downloading the ' + (j.kind==='delta'?'delta patch':'bundle') + ' and verifying its signature &amp; hash…');
        const d = await jsonPost('/platform/updates/download', {}, 'Download failed.');
        if (!d || !d.success || !d.ok) { setStep('download','err'); ovMsg('❌ ' + esc2((d && (d.message||d.error)) || 'Download failed.')); await wait(2400); ovHide(); bundleRejected(d || {}); return; }
        setStep('download','done'); setStep('apply','active');
        ovMsg('Creating a restore point, then applying &amp; patching' + (d.changed != null ? (' (' + d.changed + ' files)') : '') + '…');
        const a = await jsonPost('/platform/updates/apply', { bundle: d.bundle }, 'The update could not be applied.');
        if (!a || !a.success) { setStep('apply','err'); ovMsg('❌ ' + esc2((a && a.message) || 'Apply failed.')); await wait(2800); ovHide(); Swal.fire({ icon:'error', title:'Not applied', text: (a && a.message) || '' }); return; }
        setStep('apply','done'); ovMsg('✅ ' + esc2(a.message || 'Updated.'));
        await wait(1300);
        ovHide();
        Swal.fire({ icon:'success', title:'Updated to ' + (a.version || j.version), text:'A restore point was created. Reloading…', timer:2200, showConfirmButton:false }).then(() => location.reload());
    });
})();
</script>
</body>
</html>
