{* Server Actions — per-server admin operations.
   URL: &page=server_actions&server_id=X *}
{include file="$dir/templates/_task_log_modal.tpl"}

<style>
    /* Compact server header — visible on every tab */
    .puq-srv-card { padding: 8px 12px; margin-bottom: 10px; background:#fff; border:1px solid #ddd; border-radius:3px; }
    .puq-srv-bar  { display:flex; justify-content:space-between; align-items:center; gap:10px; }
    .puq-srv-title { font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
    .puq-srv-title .label { font-weight:500; font-size:10px; }
    .puq-srv-meta { font-size:11px; color:#555; display:flex; gap:14px; flex-wrap:wrap; padding:6px 0; border-top:1px solid #eee; border-bottom:1px solid #eee; margin:6px 0; }
    .puq-srv-meta code { background:#f5f5f5; padding:1px 4px; border-radius:2px; font-size:11px; color:#333; }
    .puq-srv-meta .item i { color:#999; margin-right:3px; }
    .puq-srv-stats { display:flex; gap:5px; flex-wrap:wrap; }
    .puq-stat { flex:1 1 100px; min-width:88px; padding:5px 8px; border:1px solid #ececec; border-radius:3px; background:#fafafa; line-height:1.25; }
    .puq-stat-label { font-size:9px; color:#888; text-transform:uppercase; letter-spacing:.4px; }
    .puq-stat-value { font-size:13px; font-weight:600; }
    .puq-stat-sub   { font-size:10px; color:#888; }
    .puq-stat-bar   { height:3px; background:#eee; border-radius:2px; margin-top:3px; overflow:hidden; }
    .puq-stat-bar > div { height:100%; transition:width .3s; }
    .puq-stat-bar > div.success { background:#5cb85c; }
    .puq-stat-bar > div.info    { background:#5bc0de; }
    .puq-stat-bar > div.warning { background:#f0ad4e; }
    .puq-stat-bar > div.danger  { background:#d9534f; }
    .puq-stat-bar > div.default { background:#bbb; }
    .puq-stat.info .puq-stat-value { font-size:11px; font-weight:500; }

    /* Tab content niceties */
    .puq-kv { margin: 0; }
    .puq-kv dt { float: left; width: 160px; clear: left; color: #888; font-weight: normal; padding-right:8px; }
    .puq-kv dd { margin-left: 170px; margin-bottom: 3px; word-break:break-all; }
    pre.puq-raw { background: #f5f5f5; padding: 10px; max-height: 400px; overflow: auto; font-size: 11px; }

    /* System config — clean key/value table */
    table.puq-cfg { font-size:12px; }
    table.puq-cfg td.k { color:#777; font-weight:500; width:240px; padding:4px 8px; border-right:1px solid #eee; }
    table.puq-cfg td.v { padding:4px 8px; word-break:break-word; }
</style>

<div class="puq-srv-card" id="puq-srv-card">
    <div class="puq-srv-bar">
        <div class="puq-srv-title">
            <i class="fa fa-bolt text-primary"></i>
            <span id="puq-hdr-name">Loading…</span>
            <span class="label label-primary" id="puq-hdr-type" style="display:none"></span>
            <span class="label label-default" id="puq-hdr-driver" style="display:none"></span>
            <span id="puq-hdr-status"></span>
            <span id="puq-hdr-health"></span>
        </div>
        <div class="puq-srv-actions">
            <button type="button" class="btn btn-default btn-sm" id="puq-btn-refresh"><i class="fa fa-sync"></i> Refresh status</button>
            <a class="btn btn-default btn-sm puq-nav-link" id="puq-btn-back" href="{$meta.admin_module_url}&amp;page=web_servers">
                <i class="fa fa-arrow-left"></i> Back
            </a>
        </div>
    </div>
    <div class="puq-srv-meta">
        <span class="item"><i class="fa fa-network-wired"></i> <code id="puq-meta-host"><i class="fa fa-spinner fa-spin"></i></code></span>
        <span class="item"><i class="fa fa-ethernet"></i> IP <span id="puq-meta-ip"><i class="fa fa-spinner fa-spin"></i></span></span>
        <span class="item"><i class="fa fa-layer-group"></i> Group <span id="puq-meta-group"><i class="fa fa-spinner fa-spin"></i></span></span>
        <span class="item"><i class="fa fa-clock"></i> <span id="puq-meta-when" title="Last collected"><i class="fa fa-spinner fa-spin"></i></span></span>
    </div>
    <div class="puq-srv-stats">
        {* 4 metrics *}
        <div class="puq-stat metric">
            <div class="puq-stat-label"><i class="fa fa-microchip"></i> CPU</div>
            <div class="puq-stat-value" id="puq-card-cpu"><i class="fa fa-spinner fa-spin text-muted"></i></div>
            <div class="puq-stat-bar"><div id="puq-card-cpu-bar" class="default" style="width:0%"></div></div>
            <div class="puq-stat-sub" id="puq-card-cpu-sub">…</div>
        </div>
        <div class="puq-stat metric">
            <div class="puq-stat-label"><i class="fa fa-memory"></i> Memory</div>
            <div class="puq-stat-value" id="puq-card-mem"><i class="fa fa-spinner fa-spin text-muted"></i></div>
            <div class="puq-stat-bar"><div id="puq-card-mem-bar" class="default" style="width:0%"></div></div>
            <div class="puq-stat-sub" id="puq-card-mem-sub">…</div>
        </div>
        <div class="puq-stat metric">
            <div class="puq-stat-label"><i class="fa fa-hdd"></i> Disk (/)</div>
            <div class="puq-stat-value" id="puq-card-disk"><i class="fa fa-spinner fa-spin text-muted"></i></div>
            <div class="puq-stat-bar"><div id="puq-card-disk-bar" class="default" style="width:0%"></div></div>
            <div class="puq-stat-sub" id="puq-card-disk-sub">…</div>
        </div>
        <div class="puq-stat metric">
            <div class="puq-stat-label"><i class="fa fa-tachometer-alt"></i> Load</div>
            <div class="puq-stat-value" id="puq-card-load"><i class="fa fa-spinner fa-spin text-muted"></i></div>
            <div class="puq-stat-bar"><div id="puq-card-load-bar" class="default" style="width:0%"></div></div>
            <div class="puq-stat-sub" id="puq-card-load-sub">…</div>
        </div>
        {* 4 inventory cells *}
        <div class="puq-stat info"><div class="puq-stat-label">Panel</div><div class="puq-stat-value" id="puq-card-panel"><i class="fa fa-spinner fa-spin text-muted"></i></div></div>
        <div class="puq-stat info"><div class="puq-stat-label">OS</div><div class="puq-stat-value" id="puq-card-os"><i class="fa fa-spinner fa-spin text-muted"></i></div></div>
        <div class="puq-stat info"><div class="puq-stat-label">Uptime</div><div class="puq-stat-value" id="puq-card-uptime"><i class="fa fa-spinner fa-spin text-muted"></i></div></div>
        <div class="puq-stat info"><div class="puq-stat-label">Users / Pkgs</div><div class="puq-stat-value" id="puq-card-users-pkgs"><i class="fa fa-spinner fa-spin text-muted"></i></div></div>
    </div>
    <div id="puq-snapshot-error" style="display:none; margin-top:6px; padding:5px 8px; font-size:11px;" class="alert alert-warning">
        <i class="fa fa-exclamation-triangle"></i>
        <strong>Status snapshot stale or failed.</strong>
        <span id="puq-snapshot-error-msg"></span>
    </div>
</div>

<ul class="nav nav-tabs" id="puq-action-tabs">
    <li class="active"><a href="#puq-tab-config" data-toggle="tab"><i class="fa fa-sliders-h"></i> System config <span class="badge" id="puq-badge-cfg-drift" style="display:none;background:#d9534f;"></span></a></li>
    <li><a href="#puq-tab-group" data-toggle="tab"><i class="fa fa-layer-group"></i> Group config <span class="badge" id="puq-badge-group-drift" style="display:none;background:#d9534f;"></span></a></li>
    <li><a href="#puq-tab-php" data-toggle="tab"><i class="fab fa-php"></i> PHP <span class="badge" id="puq-badge-php"></span></a></li>
    <li><a href="#puq-tab-filemanager" data-toggle="tab"><i class="fa fa-folder-open"></i> File Manager <span class="badge" id="puq-badge-fm"></span></a></li>
    <li><a href="#puq-tab-services" data-toggle="tab"><i class="fa fa-cogs"></i> Services <span class="badge" id="puq-badge-services"></span></a></li>
    <li><a href="#puq-tab-users" data-toggle="tab"><i class="fa fa-users"></i> Users <span class="badge" id="puq-badge-users"></span></a></li>
    <li><a href="#puq-tab-packages" data-toggle="tab"><i class="fa fa-box"></i> Packages <span class="badge" id="puq-badge-packages"></span></a></li>
    <li><a href="#puq-tab-ips" data-toggle="tab"><i class="fa fa-network-wired"></i> IPs <span class="badge" id="puq-badge-ips"></span></a></li>
    <li><a href="#puq-tab-maintenance" data-toggle="tab"><i class="fa fa-wrench"></i> Maintenance</a></li>
    <li><a href="#puq-tab-log" data-toggle="tab"><i class="fa fa-list-alt"></i> Log</a></li>
</ul>

<div class="tab-content" style="padding:12px 0;">

    {* ---------------- SYSTEM CONFIG ---------------- *}
    <div class="tab-pane active" id="puq-tab-config">
        <div class="row">
            <div class="col-md-12">
                <table class="table table-striped table-bordered table-condensed puq-cfg" id="puq-cfg-table">
                    <thead><tr><th style="width:240px;">Key</th><th>Value</th></tr></thead>
                    <tbody><tr><td colspan="2" class="text-center text-muted"><i class="fa fa-spinner fa-spin"></i> Loading…</td></tr></tbody>
                </table>
            </div>
        </div>
        <details>
            <summary style="cursor:pointer;color:#888;">Raw snapshot JSON</summary>
            <pre class="puq-raw" id="puq-snap-raw"></pre>
        </details>
    </div>

    {* ---------------- GROUP CONFIG (Phase B drift) ---------------- *}
    <div class="tab-pane" id="puq-tab-group">
        <div id="puq-group-noassign" class="alert alert-info" style="display:none;">
            <i class="fa fa-info-circle"></i> This server is not assigned to a group. Assign it to a group on the server-edit page to enforce a config template.
        </div>
        <div id="puq-group-empty" class="alert alert-info" style="display:none;">
            <i class="fa fa-info-circle"></i> Group "<span id="puq-group-empty-name"></span>" has no Hestia config defined yet. Edit the group to set keys it should enforce.
        </div>
        <div id="puq-group-drift-ui" style="display:none;">
            <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
                <strong>Group:</strong> <span id="puq-group-name"></span>
                <span class="text-muted">|</span>
                <strong>Last collected:</strong> <span id="puq-group-collected"></span>
                <span class="text-muted">|</span>
                <strong>Drifted keys:</strong> <span id="puq-group-drift-count" class="label label-default">0</span>
                <span style="margin-left:auto;display:flex;gap:8px;align-items:center;">
                    <label class="text-muted small" style="font-weight:normal;margin:0;">
                        <input type="checkbox" id="puq-group-only-drift"> Show only drifted
                    </label>
                    <button type="button" class="btn btn-primary btn-sm" id="puq-apply-group-config">
                        <i class="fa fa-magic"></i> Apply group config
                    </button>
                </span>
            </div>
            <p class="text-muted small">
                Pushes every managed key via <code>v-change-sys-config-value</code> (or specialized
                <code>v-change-sys-*</code> when defined). Keys already in sync are skipped.
                A new <em>collectStatus</em> task is queued automatically afterward to refresh drift state.
            </p>
            <table class="table table-striped table-bordered table-condensed puq-cfg" id="puq-group-table">
                <thead><tr>
                    <th style="width:240px;">Key</th>
                    <th>Group expects</th>
                    <th>Server has</th>
                    <th style="width:110px;">Status</th>
                </tr></thead>
                <tbody></tbody>
            </table>
        </div>
    </div>

    {* ---------------- PHP ---------------- *}
    <div class="tab-pane" id="puq-tab-php">
        {* PPA status banner — shown only when PPA is missing or detected. *}
        <div id="puq-php-ppa-banner" style="display:none;margin-bottom:10px;"></div>

        <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px;">
            <button type="button" class="btn btn-default btn-sm" id="puq-php-refresh"><i class="fa fa-sync"></i> Refresh</button>
            <button type="button" class="btn btn-success btn-sm" id="puq-php-install-all"><i class="fa fa-download"></i> Install all missing</button>
            <button type="button" class="btn btn-primary btn-sm" id="puq-php-install-ioncube"><i class="fa fa-shield-alt"></i> Install / Update IonCube</button>
            <button type="button" class="btn btn-info btn-sm" id="puq-php-matrix-open"><i class="fa fa-table"></i> Modules matrix</button>
            <span class="text-muted small" style="margin-left:auto">Long install/delete operations run in the queue — watch progress on the right.</span>
        </div>

        <div class="row">
            <div class="col-md-8">
                <table class="table table-striped table-bordered table-condensed" id="puq-php-table">
                    <thead><tr>
                        <th style="width:80px;">Version</th>
                        <th style="width:90px;">Installed</th>
                        <th style="width:120px;">FPM service</th>
                        <th>IonCube</th>
                        <th style="width:60px;">Modules</th>
                        <th style="width:230px;">Actions</th>
                    </tr></thead>
                    <tbody><tr><td colspan="6" class="text-center text-muted"><i class="fa fa-spinner fa-spin"></i> Loading PHP snapshot…</td></tr></tbody>
                </table>
                <p class="text-muted small">
                    Available candidates are derived from Hestia's supported list (<code>v-list-sys-php</code> for installed,
                    plus PPA-known versions). Per-version FPM status, IonCube version and loaded modules are probed
                    via SSH ({literal}<code>systemctl is-active</code>, <code>php{ver} -r</code>, <code>php{ver} -m</code>{/literal}).
                </p>
            </div>
            <div class="col-md-4">
                <div class="panel panel-default">
                    <div class="panel-heading"><h4 class="panel-title"><i class="fa fa-tasks"></i> Recent PHP tasks</h4></div>
                    <div class="panel-body" style="padding:8px 12px;max-height:340px;overflow:auto;">
                        <div id="puq-php-tasks">
                            <div class="text-muted small">No tasks yet.</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        {* Modules matrix — hidden by default, opened by the Matrix button. *}
        <div id="puq-php-matrix" style="display:none;margin-top:14px;">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h4 class="panel-title pull-left"><i class="fa fa-table"></i> Modules matrix (version × module)</h4>
                    <button type="button" class="btn btn-default btn-xs pull-right" id="puq-php-matrix-close"><i class="fa fa-times"></i></button>
                    <div class="clearfix"></div>
                </div>
                <div class="panel-body" style="overflow:auto;">
                    <table class="table table-condensed table-bordered" id="puq-php-matrix-table" style="font-size:11px;">
                        <thead><tr><th>Module</th></tr></thead>
                        <tbody><tr><td class="text-muted">Run a refresh first.</td></tr></tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>

    {* ---------------- FILE MANAGERS (FileGator + net2ftp) ---------------- *}
    <div class="tab-pane" id="puq-tab-filemanager">
        <p class="text-muted small">
            Two independent web file managers available on each server. Both go through localhost FTP (<code>127.0.0.1:21</code>) using the client's Hestia credentials, so files end up owned by the correct unix user.
            Install either, both, or none — the client area shows whichever are available.
        </p>

        <div class="row">
            {* ===== FileGator panel ===== *}
            <div class="col-md-6">
                <div style="border:1px solid #ddd;border-radius:4px;padding:14px;background:#fafafa;">
                    <h4 style="margin-top:0;"><i class="fa fa-folder-open"></i> FileGator
                        <small class="text-muted">SPA, drag-and-drop</small></h4>
                    <p class="text-muted small">
                        Vue.js SPA. No chmod UI. <code>/_fm/</code> sub-path.
                    </p>
                    <div id="puq-fm-status" class="puq-loader text-center" style="padding:14px;">
                        <i class="fas fa-circle-notch fa-spin"></i> probing…
                    </div>
                    <div class="form-group" style="margin-top:8px;">
                        <label style="font-size:11px;">Custom download URL <small class="text-muted">(empty = default with SHA-256 verification)</small></label>
                        <input type="text" id="puq-fm-custom-url" class="form-control input-sm" placeholder="">
                    </div>
                    <div id="puq-fm-actions" style="margin-top:8px;"></div>
                </div>
            </div>

            {* ===== net2ftp panel ===== *}
            <div class="col-md-6">
                <div style="border:1px solid #ddd;border-radius:4px;padding:14px;background:#fafafa;">
                    <h4 style="margin-top:0;"><i class="fa fa-terminal"></i> net2ftp
                        <small class="text-muted">classic, with chmod UI</small></h4>
                    <p class="text-muted small">
                        Native PHP FTP client. Has <strong>chmod</strong> file/folder permissions UI. <code>/_n2f/</code> sub-path.
                    </p>
                    <div id="puq-n2f-status" class="puq-loader text-center" style="padding:14px;">
                        <i class="fas fa-circle-notch fa-spin"></i> probing…
                    </div>
                    <div class="form-group" style="margin-top:8px;">
                        <label style="font-size:11px;">Custom download URL <small class="text-muted">(empty = default with SHA-256 verification)</small></label>
                        <input type="text" id="puq-n2f-custom-url" class="form-control input-sm" placeholder="">
                    </div>
                    <div id="puq-n2f-actions" style="margin-top:8px;"></div>
                </div>
            </div>
        </div>

        <hr>
        <h5><i class="fas fa-tasks"></i> Recent file-manager tasks</h5>
        <div id="puq-fm-tasks" style="position:relative;min-height:60px;">
            <table class="table table-striped table-bordered table-condensed" id="puq-fm-tasks-table">
                <thead><tr><th style="width:60px;">ID</th><th>Action</th><th>Status</th><th>Result</th><th style="width:170px;">When</th></tr></thead>
                <tbody><tr><td colspan="5" class="text-muted">No tasks yet.</td></tr></tbody>
            </table>
        </div>
    </div>

    {* ---------------- SERVICES ---------------- *}
    <div class="tab-pane" id="puq-tab-services">
        <p class="text-muted small">Start / stop / restart system services on this Hestia server.</p>
        <table class="table table-striped table-bordered table-condensed" id="puq-services-table">
            <thead><tr><th>Service</th><th>State</th><th>CPU</th><th>Memory</th><th>Uptime</th><th style="width:240px;">Actions</th></tr></thead>
            <tbody><tr><td colspan="6" class="text-center text-muted">No snapshot yet.</td></tr></tbody>
        </table>
    </div>

    {* ---------------- USERS ---------------- *}
    <div class="tab-pane" id="puq-tab-users">
        <p class="text-muted small">All users on this Hestia panel (<code>v-list-users</code>).</p>
        <table class="table table-striped table-bordered table-condensed" id="puq-users-table">
            <thead><tr><th>Username</th><th>Package</th><th>Disk used/quota</th><th>Bandwidth</th><th>Domains</th><th>DBs</th><th>State</th></tr></thead>
            <tbody><tr><td colspan="7" class="text-center text-muted">No snapshot yet.</td></tr></tbody>
        </table>
    </div>

    {* ---------------- PACKAGES ---------------- *}
    <div class="tab-pane" id="puq-tab-packages">
        <p class="text-muted small">Hosting packages (<code>v-list-user-packages</code>).</p>
        <table class="table table-striped table-bordered table-condensed" id="puq-packages-table">
            <thead><tr><th>Name</th><th>Web quota</th><th>Mail quota</th><th>DNS quota</th><th>DBs</th><th>Disk</th><th>BW</th><th>Shell</th></tr></thead>
            <tbody><tr><td colspan="8" class="text-center text-muted">No snapshot yet.</td></tr></tbody>
        </table>
    </div>

    {* ---------------- IPs ---------------- *}
    <div class="tab-pane" id="puq-tab-ips">
        <p class="text-muted small">Network interfaces and IPs allocated on this server.</p>
        <table class="table table-striped table-bordered table-condensed" id="puq-ips-table">
            <thead><tr><th>IP</th><th>Netmask</th><th>Interface</th><th>Owner</th><th>Status</th><th>Used by</th></tr></thead>
            <tbody><tr><td colspan="6" class="text-center text-muted">No snapshot yet.</td></tr></tbody>
        </table>
    </div>

    {* ---------------- MAINTENANCE ---------------- *}
    <div class="tab-pane" id="puq-tab-maintenance">
        <h5><i class="fa fa-sync"></i> Status &amp; connectivity</h5>
        <button type="button" class="btn btn-default btn-sm" id="puq-btn-test"><i class="fa fa-plug"></i> Test connection</button>
        <button type="button" class="btn btn-default btn-sm" id="puq-btn-refresh2"><i class="fa fa-sync"></i> Refresh status now</button>

        <hr>
        <h5><i class="fa fa-wrench"></i> Rebuilds</h5>
        <p class="text-muted small">Run <code>v-rebuild-*</code> — regenerates configs. Safe but slow.</p>
        <button type="button" class="btn btn-warning btn-sm" data-rebuild="rebuildUsers"><i class="fa fa-users"></i> Rebuild users</button>
        <button type="button" class="btn btn-warning btn-sm" data-rebuild="rebuildWeb"><i class="fa fa-globe"></i> Rebuild web</button>
        <button type="button" class="btn btn-warning btn-sm" data-rebuild="rebuildMail"><i class="fa fa-envelope"></i> Rebuild mail</button>
        <button type="button" class="btn btn-warning btn-sm" data-rebuild="rebuildDns"><i class="fa fa-network-wired"></i> Rebuild DNS</button>

        <hr>
        <h5><i class="fa fa-broom"></i> Caches</h5>
        <button type="button" class="btn btn-default btn-sm" id="puq-btn-purge-nginx"><i class="fa fa-trash"></i> Purge Nginx cache</button>

    </div>

    {* ---------------- LOG ---------------- *}
    <div class="tab-pane" id="puq-tab-log">
        <div style="margin-bottom:8px;">
            <select id="puq-log-filter" class="input-sm">
                <option value="">All operations</option>
                <option value="collect_status">collect_status</option>
                <option value="service_action">service_action</option>
                <option value="server_action">server_action</option>
                <option value="mgmt_action">mgmt_action</option>
                <option value="apply_group">apply_group</option>
            </select>
            <button type="button" class="btn btn-default btn-sm" id="puq-log-refresh"><i class="fa fa-sync"></i></button>
        </div>
        <table class="table table-condensed table-bordered" id="puq-log-table">
            <thead><tr><th>When</th><th>Operation</th><th>Step</th><th>Status</th><th>Detail</th></tr></thead>
            <tbody><tr><td colspan="5" class="text-center"><i class="fa fa-spinner fa-spin"></i></td></tr></tbody>
        </table>
    </div>
</div>

{literal}
<script>
(function(){
    var baseUrl = '{/literal}{$meta.admin_module_url}&ajax={literal}';
    var serverId = {/literal}{if isset($smarty.get.server_id)}{$smarty.get.server_id|intval}{else}0{/if}{literal};
    function pctColor(p){
        if (p == null || isNaN(p)) return 'default';
        if (p >= 90) return 'danger';
        if (p >= 75) return 'warning';
        if (p >= 50) return 'info';
        return 'success';
    }
    function fmtUptime(sec){
        if (sec == null) return '—';
        var d = Math.floor(sec / 86400); sec -= d * 86400;
        var h = Math.floor(sec / 3600);  sec -= h * 3600;
        var m = Math.floor(sec / 60);
        var parts = [];
        if (d > 0) parts.push(d + 'd');
        if (h > 0 || d > 0) parts.push(h + 'h');
        parts.push(m + 'm');
        return parts.join(' ');
    }
    var badge = window.statusBadge;
    function pick(obj, keys, fallback){
        if (!obj || typeof obj !== 'object') return fallback;
        for (var i = 0; i < keys.length; i++) {
            var v = obj[keys[i]];
            if (v !== undefined && v !== null && v !== '') return v;
        }
        return fallback;
    }
    function setBar(barSel, pct){
        var p = (pct == null || isNaN(pct)) ? 0 : Math.max(0, Math.min(100, pct));
        var cls = pctColor(pct);
        $(barSel).css('width', p + '%').attr('class', cls);
    }

    function clearLoadingSpinners() {
        // Replace any spinners still spinning in the header card with an em-dash,
        // so a failed request doesn't leave the UI looking like it's still loading.
        $('#puq-srv-card .fa-spinner').each(function(){
            $(this).replaceWith('—');
        });
    }

    // ============== Online-gate ==============
    // Many tab actions (PHP install, services start/stop, rebuilds, group apply)
    // require the server to be reachable. Track that flag from loadStatus()
    // and gate every action-clicking handler through requireOnline().
    //
    // puqStatusLoaded distinguishes "we haven't checked yet" (initial page
    // load) from "checked, server is offline" — banner only shown for the
    // latter. Buttons stay preemptively disabled during the initial check
    // so the user can't click before we know the status.
    var puqServerOnline = false;
    var puqServerStateLabel = 'never';
    var puqStatusLoaded = false;

    // Phase B — group config drift overlay state.
    // Populated from the getServerStatus response on every snapshot load.
    var puqGroupHestiaConfig = {}; // {KEY: expected, ...}
    var puqGroupName         = null;
    var puqGroupId           = null;
    var puqLastCollectedAt   = null;

    function requireOnline() {
        if (puqServerOnline) return true;
        if (!puqStatusLoaded) {
            showNotification('Server status is still loading — please wait a second and try again.', 'info');
            return false;
        }
        showNotification(
            'Operation blocked — server status is "' + puqServerStateLabel + '". '
            + 'Run "Refresh status" or "Test connection" to verify the server is reachable.',
            'error'
        );
        return false;
    }

    // CSS-disable & native-disable every action-changing control on this page
    // when the server is offline. Allowed-always: Refresh, Test, Back, View log.
    function applyOfflineUI() {
        var actionSelectors = [
            '.puq-php-install', '.puq-php-delete',
            '#puq-php-install-all', '#puq-php-install-ioncube', '#puq-php-install-ppa',
            '.puq-svc-start', '.puq-svc-stop', '.puq-svc-restart',
            '[data-rebuild]',
            '#puq-btn-purge-nginx'
        ].join(', ');
        var $els = $(actionSelectors);
        if (puqServerOnline) {
            $els.prop('disabled', false).removeClass('puq-disabled-offline').css('opacity', '');
            $('#puq-offline-banner').hide();
        } else {
            // Always disable buttons while not-online (prevents racing the load).
            $els.prop('disabled', true).addClass('puq-disabled-offline').css('opacity', '0.55');
            // Banner ONLY when we've actually completed a status check.
            // During the initial page load (puqStatusLoaded=false) we don't yet
            // know if the server is offline — showing the scary banner before
            // the first AJAX returns is just noise.
            if (!puqStatusLoaded) {
                $('#puq-offline-banner').hide();
                return;
            }
            var msg = 'Server status is <strong>' + esc(puqServerStateLabel.toUpperCase()) + '</strong>. '
                    + 'All write operations are disabled until a successful snapshot is collected.';
            if ($('#puq-offline-banner').length === 0) {
                $('#puq-srv-card').after(
                    '<div id="puq-offline-banner" class="alert alert-danger" style="margin-bottom:10px;">'
                  + '<i class="fa fa-ban"></i> <span></span></div>'
                );
            }
            $('#puq-offline-banner').html('<i class="fa fa-ban"></i> ' + msg).show();
        }
    }

    function loadServer() {
        puqAjax({
            container: '#puq-srv-card',
            loadingCaption: 'Loading server…',
            url: baseUrl + 'getServer&id=' + serverId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success') {
                    $('#puq-hdr-name').text('Server not found');
                    clearLoadingSpinners();
                    return;
                }
                var s = r.data;
                $('#puq-hdr-name').text(s.name);
                $('#puq-hdr-type').text(s.type).show();
                $('#puq-hdr-driver').text(s.driver).show();
                $('#puq-hdr-status').html(badge(s.status));
                $('#puq-meta-host').text(s.hostname + ':' + s.port);
                $('#puq-meta-ip').text(s.ip || '—');
                $('#puq-meta-group').text(s.group_name ? s.group_name : (s.group_id ? '#' + s.group_id : '—'));
                // Back button — page depends on type.
                var backPage = (s.type === 'mail' ? 'mail_servers' : (s.type === 'dns' ? 'dns_servers' : 'web_servers'));
                var backHref = $('#puq-btn-back').attr('href');
                if (backHref) $('#puq-btn-back').attr('href', backHref.replace(/page=[^&]+/, 'page=' + backPage));
                loadStatus();
            },
            error: function() {
                $('#puq-hdr-name').text('Failed to load server');
                clearLoadingSpinners();
            }
        });
    }

    function loadStatus() {
        puqAjax({
            container: '#puq-srv-card',
            loadingCaption: 'Loading status…',
            url: baseUrl + 'getServerStatus&id=' + serverId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success') {
                    $('#puq-snapshot-error').show().find('#puq-snapshot-error-msg').text(r.msg || '');
                    clearLoadingSpinners();
                    puqServerOnline = false;
                    puqServerStateLabel = 'unreachable';
                    puqStatusLoaded = true;
                    applyOfflineUI();
                    return;
                }
                var st = r.data;
                var snap = st.snapshot || {};

                // Health badge in header
                var stateLbl = (st.state || 'never').toUpperCase();
                var stateCls = {ok:'success',error:'danger',queued:'warning',collecting:'info',never:'default'}[st.state]||'default';
                $('#puq-hdr-health').html('<span class="label label-' + stateCls + '">' + esc(stateLbl) + '</span>');
                $('#puq-meta-when').text(st.last_collected_at || 'never');

                // Update online flag — only "ok" allows write actions.
                puqServerOnline    = (st.state === 'ok');
                puqServerStateLabel = String(st.state || 'never');
                puqStatusLoaded    = true;
                applyOfflineUI();

                if (st.state === 'error' && st.error) {
                    $('#puq-snapshot-error').show().find('#puq-snapshot-error-msg').text(st.error);
                } else {
                    $('#puq-snapshot-error').hide();
                }

                // Phase B: stash group config + last-collected for the Group config tab
                puqGroupHestiaConfig = (st.group_hestia_config && typeof st.group_hestia_config === 'object') ? st.group_hestia_config : {};
                puqGroupName         = st.group_name || null;
                puqGroupId           = st.group_id || null;
                puqLastCollectedAt   = st.last_collected_at || null;

                renderHeader(snap);
                renderConfig(snap.sys_config || {});
                renderGroupDrift(snap.sys_config || {});
                renderServices(snap.sys_services || {});
                renderUsers(snap.users || {});
                renderPackages(snap.packages || {});
                renderIps(snap.sys_ips || {});
                $('#puq-snap-raw').text(JSON.stringify(snap, null, 2));
                clearLoadingSpinners(); // any cell still showing a spinner — replace with —
            },
            error: function() {
                $('#puq-snapshot-error').show().find('#puq-snapshot-error-msg').text('Status request failed');
                clearLoadingSpinners();
                puqServerOnline = false;
                puqServerStateLabel = 'unreachable';
                puqStatusLoaded = true;
                applyOfflineUI();
            }
        });
    }

    function renderHeader(snap) {
        var summary = snap.summary || {};

        // CPU
        $('#puq-card-cpu').text(summary.cpu_percent != null ? summary.cpu_percent + '%' : '—');
        setBar('#puq-card-cpu-bar', summary.cpu_percent);
        $('#puq-card-cpu-sub').text(summary.cpu_cores ? (summary.cpu_cores + ' cores') : '—');

        // Memory
        $('#puq-card-mem').text(summary.mem_percent != null ? summary.mem_percent + '%' : '—');
        setBar('#puq-card-mem-bar', summary.mem_percent);
        $('#puq-card-mem-sub').text(
            (summary.mem_used != null ? fmtBytes(summary.mem_used) : '—') + ' / ' +
            (summary.mem_total != null ? fmtBytes(summary.mem_total) : '—')
        );

        // Disk
        $('#puq-card-disk').text(summary.disk_percent != null ? summary.disk_percent + '%' : '—');
        setBar('#puq-card-disk-bar', summary.disk_percent);
        $('#puq-card-disk-sub').text(
            (summary.disk_used != null ? fmtBytes(summary.disk_used) : '—') + ' / ' +
            (summary.disk_total != null ? fmtBytes(summary.disk_total) : '—') +
            (summary.disk_free != null ? ' · free ' + fmtBytes(summary.disk_free) : '')
        );

        // Load
        $('#puq-card-load').text(summary.load_1 != null ? summary.load_1.toFixed(2) : '—');
        var loadPct = (summary.load_1 != null && summary.cpu_cores) ? (summary.load_1 * 100 / summary.cpu_cores) : null;
        setBar('#puq-card-load-bar', loadPct);
        $('#puq-card-load-sub').text(
            (summary.load_1 != null ? summary.load_1.toFixed(2) : '—') + ' / ' +
            (summary.load_5 != null ? summary.load_5.toFixed(2) : '—') + ' / ' +
            (summary.load_15 != null ? summary.load_15.toFixed(2) : '—')
        );

        // Inventory — Panel cell shows the actual Hestia version, and an
        // upgrade arrow when v-list-sys-hestia-updates returned a non-empty list.
        // The pending-updates list is the tooltip for the icon.
        var $panel = $('#puq-card-panel').empty();
        if (summary.hestia_version) {
            $panel.append(document.createTextNode('v' + summary.hestia_version));
            if (summary.panel_update_available) {
                var pending = (summary.panel_pending_updates && summary.panel_pending_updates.length)
                    ? 'Pending: ' + summary.panel_pending_updates.join(', ')
                    : 'Update available';
                $panel.append(' <i class="fa fa-arrow-up text-warning" title="' + esc(pending) + '" style="margin-left:4px;"></i>');
            }
        } else {
            $panel.text('—');
        }
        $('#puq-card-os').text(summary.os || '—');
        $('#puq-card-uptime').text(fmtUptime(summary.uptime_seconds));
        // Combined Users / Pkgs cell: "1 / 2"
        var u = summary.users_count != null ? summary.users_count : '—';
        var p = summary.packages_count != null ? summary.packages_count : '—';
        $('#puq-card-users-pkgs').text(u + ' / ' + p);

        // Tab badges (Services + IPs tabs still exist)
        $('#puq-badge-services').text(summary.services_count || '');
        $('#puq-badge-users').text(summary.users_count || '');
        $('#puq-badge-packages').text(summary.packages_count || '');
        $('#puq-badge-ips').text(summary.ips_count || '');
    }

    // Hestia returns sys_config keyed by config-name; flatten the first row.
    function unwrapSysConfig(cfg) {
        var firstCfg = null;
        for (var k in cfg) { firstCfg = cfg[k]; break; }
        return (firstCfg && typeof firstCfg === 'object') ? firstCfg : (cfg || {});
    }

    // Normalize values for comparison: cast to string, trim, lowercase a few
    // canonical yes/no synonyms so "yes"/"YES"/"true"/"1" match the same way
    // Hestia treats them in hestia.conf.
    function normHestiaVal(v) {
        if (v === null || v === undefined) return '';
        var s = String(v).trim();
        var l = s.toLowerCase();
        if (l === 'true' || l === 'yes' || l === '1') return 'yes';
        if (l === 'false' || l === 'no' || l === '0') return 'no';
        return s;
    }

    function renderConfig(cfg) {
        var configObj = unwrapSysConfig(cfg);
        var $tbody = $('#puq-cfg-table tbody').empty();
        var keys = Object.keys(configObj || {}).sort();
        if (!keys.length) {
            $tbody.append('<tr><td colspan="2" class="text-center text-muted">No system config collected (snapshot not ready or probe denied).</td></tr>');
            $('#puq-badge-cfg-drift').hide();
            return;
        }
        var driftCount = 0;
        keys.forEach(function(key){
            var val = configObj[key];
            if (typeof val !== 'string' && typeof val !== 'number') return;
            var rowAttr = '', cell = esc(String(val));
            if (puqGroupHestiaConfig && Object.prototype.hasOwnProperty.call(puqGroupHestiaConfig, key)) {
                var expected = puqGroupHestiaConfig[key];
                if (normHestiaVal(val) === normHestiaVal(expected)) {
                    rowAttr = ' class="success"'; // matches group → green
                } else {
                    rowAttr = ' class="danger"'; // drift → red
                    cell = esc(String(val)) + ' <span class="text-muted small" title="Group expects this value">'
                         + '(group: <code>' + esc(String(expected)) + '</code>)</span>';
                    driftCount++;
                }
            }
            $tbody.append(
                '<tr' + rowAttr + '>' +
                  '<td class="k"><code>' + esc(key) + '</code></td>' +
                  '<td class="v">' + cell + '</td>' +
                '</tr>'
            );
        });
        if (driftCount > 0) {
            $('#puq-badge-cfg-drift').text(driftCount).show();
        } else {
            $('#puq-badge-cfg-drift').hide();
        }
    }

    // Phase B — Group config drift table. Compares every key the group manages
    // against the server's current sys_config and renders a per-key status row.
    function renderGroupDrift(cfg) {
        var $body = $('#puq-group-table tbody').empty();
        var $noassign = $('#puq-group-noassign').hide();
        var $empty = $('#puq-group-empty').hide();
        var $ui = $('#puq-group-drift-ui').hide();

        if (!puqGroupId) {
            $noassign.show();
            $('#puq-badge-group-drift').hide();
            return;
        }

        var managed = puqGroupHestiaConfig || {};
        var keys = Object.keys(managed).sort();
        if (!keys.length) {
            $('#puq-group-empty-name').text(puqGroupName || ('#' + puqGroupId));
            $empty.show();
            $('#puq-badge-group-drift').hide();
            return;
        }

        var configObj = unwrapSysConfig(cfg);
        $('#puq-group-name').text(puqGroupName || ('#' + puqGroupId));
        $('#puq-group-collected').text(puqLastCollectedAt || 'never');

        var driftCount = 0, missingCount = 0;
        keys.forEach(function(key){
            var expected = managed[key];
            var actualPresent = Object.prototype.hasOwnProperty.call(configObj, key);
            var actual = actualPresent ? configObj[key] : null;
            var status, badge, rowCls = '';
            if (!actualPresent) {
                status = 'missing'; badge = '<span class="label label-warning">missing</span>';
                rowCls = 'warning'; missingCount++;
            } else if (normHestiaVal(actual) === normHestiaVal(expected)) {
                status = 'match'; badge = '<span class="label label-success">match</span>';
                rowCls = 'success';
            } else {
                status = 'drift'; badge = '<span class="label label-danger">drift</span>';
                rowCls = 'danger'; driftCount++;
            }
            $body.append(
                '<tr class="' + rowCls + '" data-status="' + status + '">' +
                  '<td><code>' + esc(key) + '</code></td>' +
                  '<td>' + esc(String(expected)) + '</td>' +
                  '<td>' + (actualPresent ? esc(String(actual)) : '<span class="text-muted">(not in snapshot)</span>') + '</td>' +
                  '<td>' + badge + '</td>' +
                '</tr>'
            );
        });

        var totalIssues = driftCount + missingCount;
        var $cnt = $('#puq-group-drift-count');
        if (totalIssues === 0) {
            $cnt.removeClass('label-warning label-danger').addClass('label-success').text('0 (all match)');
            $('#puq-badge-group-drift').hide();
        } else {
            $cnt.removeClass('label-success label-default')
                .addClass(driftCount ? 'label-danger' : 'label-warning')
                .text(driftCount + ' drift / ' + missingCount + ' missing');
            $('#puq-badge-group-drift').text(totalIssues).show();
        }

        $ui.show();
        applyOnlyDriftFilter();
    }

    function applyOnlyDriftFilter() {
        var only = $('#puq-group-only-drift').is(':checked');
        $('#puq-group-table tbody tr').each(function(){
            var s = $(this).data('status');
            $(this).toggle(!only || s === 'drift' || s === 'missing');
        });
    }

    function renderServices(services) {
        var $tbody = $('#puq-services-table tbody').empty();
        var keys = Object.keys(services);
        if (!keys.length) { $tbody.append('<tr><td colspan="6" class="text-muted text-center">No services in snapshot.</td></tr>'); return; }
        keys.forEach(function(name) {
            var info = services[name] || {};
            var state = String(pick(info, ['STATE','state','RUNNING','running'], 'unknown')).toLowerCase();
            var cpu = pick(info, ['CPU','cpu','CPU_USAGE'], '');
            var mem = pick(info, ['MEM','mem','MEM_USAGE','MEMORY'], '');
            var uptime = pick(info, ['RTIME','rtime','UPTIME','uptime'], '');
            $tbody.append(
                '<tr data-service="' + esc(name) + '">' +
                '<td><code>' + esc(name) + '</code></td>' +
                '<td>' + badge(state) + '</td>' +
                '<td>' + esc(String(cpu)) + '</td>' +
                '<td>' + esc(String(mem)) + '</td>' +
                '<td>' + esc(String(uptime)) + '</td>' +
                '<td>' +
                  '<button class="btn btn-xs btn-success puq-svc-start"><i class="fa fa-play"></i> Start</button> ' +
                  '<button class="btn btn-xs btn-warning puq-svc-restart"><i class="fa fa-redo"></i> Restart</button> ' +
                  '<button class="btn btn-xs btn-danger puq-svc-stop"><i class="fa fa-stop"></i> Stop</button>' +
                '</td></tr>'
            );
        });
    }

    function renderUsers(users) {
        var $tbody = $('#puq-users-table tbody').empty();
        var keys = Object.keys(users);
        if (!keys.length) { $tbody.append('<tr><td colspan="7" class="text-muted text-center">No users collected.</td></tr>'); return; }
        keys.forEach(function(name) {
            var u = users[name] || {};
            var pkg = pick(u, ['PACKAGE','package'], '—');
            var diskUsed = pick(u, ['U_DISK','disk_used'], '');
            var diskQuota = pick(u, ['DISK_QUOTA','disk_quota'], '');
            var bwUsed = pick(u, ['U_BANDWIDTH','bandwidth_used'], '');
            var bwQuota = pick(u, ['BANDWIDTH','bandwidth'], '');
            var webDom = pick(u, ['U_WEB_DOMAINS','web_domains'], '0');
            var dbs = pick(u, ['U_DATABASES','databases'], '0');
            var suspended = pick(u, ['SUSPENDED','suspended'], 'no');
            $tbody.append(
                '<tr>' +
                '<td><strong>' + esc(name) + '</strong></td>' +
                '<td>' + esc(String(pkg)) + '</td>' +
                '<td class="small">' + esc(String(diskUsed)) + ' / ' + esc(String(diskQuota)) + ' MB</td>' +
                '<td class="small">' + esc(String(bwUsed)) + ' / ' + esc(String(bwQuota)) + ' MB</td>' +
                '<td>' + esc(String(webDom)) + '</td>' +
                '<td>' + esc(String(dbs)) + '</td>' +
                '<td>' + (String(suspended).toLowerCase() === 'yes' ? '<span class="label label-warning">suspended</span>' : '<span class="label label-success">active</span>') + '</td>' +
                '</tr>'
            );
        });
    }

    function renderPackages(packages) {
        var $tbody = $('#puq-packages-table tbody').empty();
        var keys = Object.keys(packages);
        if (!keys.length) { $tbody.append('<tr><td colspan="8" class="text-muted text-center">No packages collected.</td></tr>'); return; }
        keys.forEach(function(name) {
            var p = packages[name] || {};
            $tbody.append(
                '<tr>' +
                '<td><strong>' + esc(name) + '</strong></td>' +
                '<td class="small">' + esc(String(pick(p, ['WEB_DOMAINS','web_domains'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['MAIL_DOMAINS','mail_domains'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['DNS_DOMAINS','dns_domains'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['DATABASES','databases'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['DISK_QUOTA','disk_quota'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['BANDWIDTH','bandwidth'], '—'))) + '</td>' +
                '<td class="small">' + esc(String(pick(p, ['SHELL','shell'], '—'))) + '</td>' +
                '</tr>'
            );
        });
    }

    function renderIps(ips) {
        var $tbody = $('#puq-ips-table tbody').empty();
        var keys = Object.keys(ips);
        if (!keys.length) { $tbody.append('<tr><td colspan="6" class="text-muted text-center">No IPs collected.</td></tr>'); return; }
        keys.forEach(function(addr) {
            var i = ips[addr] || {};
            $tbody.append(
                '<tr>' +
                '<td><code>' + esc(addr) + '</code></td>' +
                '<td>' + esc(String(pick(i, ['NETMASK','netmask'], '—'))) + '</td>' +
                '<td>' + esc(String(pick(i, ['INTERFACE','iface','DEV'], '—'))) + '</td>' +
                '<td>' + esc(String(pick(i, ['OWNER','owner','USER'], '—'))) + '</td>' +
                '<td>' + badge(String(pick(i, ['STATUS','status'], 'shared')).toLowerCase()) + '</td>' +
                '<td>' + esc(String(pick(i, ['U_WEB_DOMAINS','U_HOSTS','used'], '0'))) + '</td>' +
                '</tr>'
            );
        });
    }

    function loadLog() {
        var op = $('#puq-log-filter').val();
        var qs = 'target_type=server&target_id=' + serverId;
        if (op) qs += '&operation=' + encodeURIComponent(op);
        puqAjax({
            container: '#puq-log-table',
            loadingCaption: 'Loading operation log…',
            url: baseUrl + 'operationLog&' + qs, dataType: 'json',
            success: function(r) {
                var $tbody = $('#puq-log-table tbody').empty();
                if (r.status !== 'success' || !r.data.length) {
                    $tbody.append('<tr><td colspan="5" class="text-muted text-center">No log entries.</td></tr>');
                    return;
                }
                r.data.forEach(function(e) {
                    $tbody.append('<tr>' +
                        '<td class="small">' + esc(e.created_at || '') + '</td>' +
                        '<td><code>' + esc(e.operation) + '</code></td>' +
                        '<td>' + esc(e.step) + '</td>' +
                        '<td>' + badge(e.status) + '</td>' +
                        '<td class="small" style="max-width:400px;word-break:break-all;">' +
                            (e.error_detail ? '<span class="text-danger">' + esc(e.error_detail.substring(0,300)) + '</span>' : esc((e.message||'').substring(0,300))) +
                        '</td></tr>');
                });
            }
        });
    }

    function enqueue(action, payload, btn, origIcon, origText) {
        btnSpinner(btn, 'Queuing...');
        $.ajax({
            url: baseUrl + 'enqueueServerAction',
            type: 'POST',
            traditional: false,
            // Send payload as a nested object so $_POST['payload'] arrives as
            // an array on the server (deep-form encoding) — JSON.stringify-as-string
            // was sometimes coming through empty on this WHMCS instance.
            data: { server_id: serverId, action: action, payload: payload || {} },
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success') showNotification(r.msg || 'Queued', 'success');
                else showNotification(r.msg || 'Failed', 'error');
                setTimeout(loadLog, 1200);
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, origIcon || 'play', origText || ''); }
        });
    }

    $(document).ready(function(){
        if (serverId <= 0) {
            $('#puq-srv-card').html('<div class="alert alert-danger">Missing <code>server_id</code> in URL.</div>');
            return;
        }
        // Default to offline until first successful loadStatus — better to
        // disable buttons preemptively than to flash them as enabled.
        applyOfflineUI();
        loadServer();
        loadLog();

        $('#puq-group-only-drift').on('change', applyOnlyDriftFilter);

        $('#puq-services-table').on('click', '.puq-svc-start,.puq-svc-stop,.puq-svc-restart', function() {
            if (!requireOnline()) return;
            var $btn = $(this);
            var svc = $btn.closest('tr').data('service');
            var action, icon;
            if ($btn.hasClass('puq-svc-start'))       { action = 'startService';  icon = 'play'; }
            else if ($btn.hasClass('puq-svc-stop'))   { action = 'stopService';   icon = 'stop'; }
            else                                       { action = 'restartService'; icon = 'redo'; }
            enqueue(action, { service: svc }, this, icon, '');
        });

        $('#puq-btn-refresh, #puq-btn-refresh2').on('click', function() {
            var btn = this;
            btnSpinner(btn, 'Queuing...');
            $.ajax({
                url: baseUrl + 'refreshServerStatus', type: 'POST', data: { id: serverId }, dataType: 'json',
                success: function(r) {
                    if (r.status === 'success') {
                        showNotification(r.msg || 'Queued', 'success');
                        setTimeout(function(){ loadStatus(); loadLog(); }, 2000);
                    } else showNotification(r.msg || 'Failed', 'error');
                },
                error: function() { showNotification('Request failed.', 'error'); },
                complete: function() { btnRestore(btn, 'sync', 'Refresh status'); }
            });
        });

        $('#puq-btn-test').on('click', function() {
            var btn = this;
            btnSpinner(btn, 'Testing...');
            $.ajax({
                url: baseUrl + 'testServerConnection', type: 'POST', data: { id: serverId }, dataType: 'json',
                success: function(r) {
                    if (r.status === 'success') { showNotification('Connection OK', 'success'); setTimeout(loadStatus, 2000); }
                    else showNotification('FAILED: ' + (r.error || r.msg || 'unknown'), 'error');
                },
                error: function() { showNotification('Request failed.', 'error'); },
                complete: function() { btnRestore(btn, 'plug', 'Test connection'); }
            });
        });

        $('[data-rebuild]').on('click', function() {
            if (!requireOnline()) return;
            var action = this.getAttribute('data-rebuild');
            var label = $(this).text().trim();
            enqueue(action, {}, this, 'users', label);
        });

        $('#puq-btn-purge-nginx').on('click', function() {
            if (!requireOnline()) return;
            enqueue('purgeNginxCache', {}, this, 'trash', 'Purge Nginx cache');
        });

        $('#puq-apply-group-config').on('click', function() {
            if (!requireOnline()) return;
            if (!puqGroupId) {
                showNotification('Server is not assigned to a group.', 'error');
                return;
            }
            enqueue('applyGroupConfig', {}, this, 'magic', 'Apply group config');
        });

        $('#puq-log-filter').on('change', loadLog);
        $('#puq-log-refresh').on('click', function() {
            var btn = this; btnSpinner(btn);
            loadLog();
            setTimeout(function(){ btnRestore(btn, 'sync', ''); }, 400);
        });

        // ============== PHP tab ==============
        var phpSnapshot = null;
        var phpPollTimer = null;
        var phpHadActive = false;  // tracks active→idle transition for auto-refresh
        // Tracks queued/running PHP tasks so the table & toolbar can show
        // spinners on the exact rows being installed/removed.
        var phpActiveByVersion = {}; // {version: 'install' | 'delete'}
        var phpIoncubeActive = false;
        var phpPpaActive = false;

        function phpTaskBadge(status) {
            var cls = {queued:'warning', running:'info', success:'success', error:'danger', cancelled:'default'}[status] || 'default';
            return '<span class="label label-' + cls + '">' + esc(status) + '</span>';
        }

        // Override the Actions cell of any row whose version has a queued/running
        // install or delete task so the user can see WHICH version is being worked
        // on. Called after renderPhp() and after every loadPhpTasks() refresh.
        function applyPhpRowSpinners() {
            $('#puq-php-table tbody tr').each(function() {
                var $row = $(this);
                var ver = $row.attr('data-ver');
                if (!ver) return;
                var op = phpActiveByVersion[ver];
                var $cell = $row.find('td').last();
                if (op) {
                    var label = op === 'install' ? 'Installing…' : 'Removing…';
                    $cell.html('<button class="btn btn-xs btn-default" disabled><i class="fa fa-spinner fa-spin"></i> ' + label + '</button>');
                }
                // No "else" — when the task finishes, loadPhp() runs and
                // re-renders the row with the correct Install/Remove button.
            });
        }

        // Disable the toolbar IonCube button + spinner while an installIoncube
        // task is queued/running. Restore from the original HTML cached on the
        // button via a data attribute when the task settles.
        function updateIoncubeButton() {
            var $btn = $('#puq-php-install-ioncube');
            if (!$btn.length) return;
            if (phpIoncubeActive) {
                if ($btn.attr('data-orig-html') == null) $btn.attr('data-orig-html', $btn.html());
                $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Installing IonCube…');
            } else {
                var orig = $btn.attr('data-orig-html');
                if (orig != null) {
                    $btn.prop('disabled', false).html(orig);
                    $btn.removeAttr('data-orig-html');
                }
            }
        }

        // Same pattern for the banner "Install PPA" button. The banner gets
        // rebuilt from scratch on every loadPhp(), so we re-apply state both
        // there and when loadPhpTasks reports an active/idle transition.
        function updatePpaButton() {
            var $btn = $('#puq-php-install-ppa');
            if (!$btn.length) return;
            if (phpPpaActive) {
                if ($btn.attr('data-orig-html') == null) $btn.attr('data-orig-html', $btn.html());
                $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Installing PPA…');
            } else {
                var orig = $btn.attr('data-orig-html');
                if (orig != null) {
                    $btn.prop('disabled', false).html(orig);
                    $btn.removeAttr('data-orig-html');
                }
            }
        }

        function loadPhp() {
            puqAjax({
                container: '#puq-php-table',
                loadingCaption: 'Loading PHP snapshot…',
                url: baseUrl + 'phpSnapshot&server_id=' + serverId, dataType: 'json',
                success: function(r) {
                    var $tbody = $('#puq-php-table tbody').empty();
                    if (r.status !== 'success' || !r.data || !r.data.rows) {
                        $tbody.append('<tr><td colspan="6" class="text-center text-warning">' + esc(r.msg || 'Probe failed') + '</td></tr>');
                        return;
                    }
                    phpSnapshot = r.data;

                    // Render PPA banner — without ondrej/php PPA, every install
                    // will fail because apt has no candidates. The banner makes
                    // this obvious before the user wastes time on Install clicks.
                    var ppa = r.data.ppa || {};
                    var $banner = $('#puq-php-ppa-banner').empty();
                    if (ppa.installed) {
                        var sources = (ppa.files && ppa.files.length ? ppa.files : ppa.policy) || [];
                        $banner.html(
                            '<div class="alert alert-success" style="margin-bottom:0;padding:6px 10px;">' +
                              '<i class="fa fa-check-circle"></i> <strong>ondrej/php PPA installed.</strong> ' +
                              '<small class="text-muted">' + esc(sources.slice(0,2).join(', ')) + '</small>' +
                            '</div>'
                        ).show();
                    } else {
                        $banner.html(
                            '<div class="alert alert-warning" style="margin-bottom:0;padding:8px 12px;">' +
                              '<div style="display:flex;align-items:center;gap:10px;">' +
                                '<i class="fa fa-exclamation-triangle"></i>' +
                                '<div style="flex:1;">' +
                                  '<strong>ondrej/php PPA is NOT installed.</strong> ' +
                                  '<small>v-add-web-php cannot install any PHP version without it. Click to install:</small>' +
                                '</div>' +
                                '<button type="button" class="btn btn-warning btn-sm" id="puq-php-install-ppa">' +
                                  '<i class="fa fa-download"></i> Install PPA' +
                                '</button>' +
                              '</div>' +
                            '</div>'
                        ).show();
                    }

                    var installedCount = 0;
                    r.data.rows.forEach(function(row){
                        if (row.installed) installedCount++;
                        var fpm = row.fpm_active || '—';
                        var fpmCls = fpm === 'active' ? 'success' : (fpm === 'inactive' ? 'default' : (fpm === 'failed' ? 'danger' : 'warning'));
                        var ic = row.ioncube;
                        var icHtml = '—';
                        if (row.installed && ic) {
                            // Build a one-glance tooltip with everything the probe found —
                            // when missing, this is the only way the user can tell why.
                            var tipLines = [];
                            tipLines.push('CLI loaded: ' + (ic.cli_loaded ? 'YES' : 'no'));
                            if (ic.version) tipLines.push('Version: ' + ic.version);
                            if (ic.php_m_match) tipLines.push('php -m: ' + ic.php_m_match);
                            tipLines.push('Ini files: ' + ((ic.ini_files || []).length ? (ic.ini_files || []).join(', ') : 'none'));
                            tipLines.push('.so files: ' + ((ic.so_files || []).length ? (ic.so_files || []).join(', ') : 'none'));
                            if (ic.extension_dir) tipLines.push('extension_dir: ' + ic.extension_dir);
                            var tip = tipLines.join('\n');

                            if (ic.installed) {
                                if (ic.cli_loaded && ic.version) {
                                    icHtml = '<span class="label label-success" title="' + esc(tip) + '">v' + esc(ic.version) + '</span>';
                                } else if (ic.configured) {
                                    icHtml = '<span class="label label-warning" title="' + esc(tip) + '">configured (FPM)</span>';
                                } else {
                                    icHtml = '<span class="label label-info" title="' + esc(tip) + '">detected</span>';
                                }
                            } else {
                                // Definitely missing — but show the diagnostic on hover so the
                                // user can see whether ini files / .so files were even attempted.
                                icHtml = '<span class="label label-default" title="' + esc(tip) + '">missing</span>';
                            }
                        }
                        var modCount = row.modules ? row.modules.length : 0;
                        var actions = '';
                        if (row.installed) {
                            actions += '<button class="btn btn-xs btn-danger puq-php-delete" data-ver="' + esc(row.version) + '"><i class="fa fa-trash"></i> Remove</button>';
                        } else {
                            actions += '<button class="btn btn-xs btn-success puq-php-install" data-ver="' + esc(row.version) + '"><i class="fa fa-download"></i> Install</button>';
                        }
                        $tbody.append(
                            '<tr data-ver="' + esc(row.version) + '">' +
                                '<td><strong>PHP ' + esc(row.version) + '</strong></td>' +
                                '<td>' + (row.installed ? '<span class="label label-success">yes</span>' : '<span class="label label-default">no</span>') + '</td>' +
                                '<td>' + (row.installed ? '<span class="label label-' + fpmCls + '">' + esc(fpm) + '</span>' : '<span class="text-muted">—</span>') + '</td>' +
                                '<td>' + icHtml + '</td>' +
                                '<td>' + (row.installed ? modCount : '—') + '</td>' +
                                '<td>' + actions + '</td>' +
                            '</tr>'
                        );
                    });
                    $('#puq-badge-php').text(installedCount);
                    // Re-apply spinners for any rows whose tasks are still
                    // active. renderPhp rebuilt the table from scratch; without
                    // this, a row whose install just finished one snapshot ago
                    // would briefly flash "Install" before the next poll.
                    applyPhpRowSpinners();
                    // PPA banner was rebuilt — re-apply spinner if installPpa
                    // task is currently queued/running.
                    updatePpaButton();
                    // Re-apply offline-disabled state to the freshly rendered
                    // Install/Remove buttons.
                    applyOfflineUI();
                }
            });
            loadPhpTasks();
        }

        function loadPhpTasks() {
            puqAjax({
                container: '#puq-php-tasks',
                loadingCaption: 'Loading PHP tasks…',
                url: baseUrl + 'serverTasks&server_id=' + serverId + '&action_prefix=server.&limit=50', dataType: 'json',
                error: function(xhr, status, err) {
                    // Surface the real failure instead of leaving the placeholder.
                    $('#puq-php-tasks').html(
                        '<div class="alert alert-danger small">Failed to load tasks: ' + esc(status + ' ' + (err || '')) + '<br>HTTP ' + xhr.status + '</div>'
                    );
                },
                success: function(r) {
                    var $box = $('#puq-php-tasks').empty();
                    if (r.status !== 'success') {
                        $box.html('<div class="alert alert-warning small">' + esc(r.msg || 'serverTasks returned non-success') + '</div>');
                        return;
                    }
                    // Take ALL recent server.* PHP-related tasks. Earlier the regex
                    // forced exact match on (installPhp|deletePhp|installIoncube)$ —
                    // if the action prefix changes (e.g. installPhp- with retries),
                    // tasks would silently disappear from this list. Loose substring
                    // match is safer and still excludes collectStatus/applyGroupTemplate.
                    var phpTasks = (r.data || []).filter(function(t){
                        var a = String(t.action || '');
                        return a.indexOf('server.installPhp') === 0
                            || a.indexOf('server.deletePhp') === 0
                            || a.indexOf('server.installIoncube') === 0
                            || a.indexOf('server.installPpa') === 0;
                    });
                    // Reset trackers before rebuilding from the fresh task list.
                    phpActiveByVersion = {};
                    phpIoncubeActive = false;
                    phpPpaActive = false;

                    if (!phpTasks.length) {
                        // Diagnostic — show how many raw tasks the backend returned
                        // and what their action names were, so we can tell whether
                        // the issue is "no tasks in DB" vs "filter doesn't match".
                        var rawCount = (r.data || []).length;
                        var sample = (r.data || []).slice(0, 5).map(function(t){
                            return (t.action || '?') + ' (' + (t.status || '?') + ')';
                        }).join(', ');
                        var diag = rawCount > 0
                            ? 'No PHP tasks. ' + rawCount + ' other server.* tasks: ' + sample
                            : 'No tasks for this server yet.';
                        $box.append('<div class="text-muted small">' + esc(diag) + '</div>');
                        applyPhpRowSpinners();
                        updateIoncubeButton();
                        updatePpaButton();
                        return;
                    }
                    var hasActive = false;
                    phpTasks.forEach(function(t){
                        var isActive = (t.status === 'queued' || t.status === 'running');
                        if (isActive) {
                            hasActive = true;
                            if (t.action === 'server.installIoncube') {
                                phpIoncubeActive = true;
                            } else if (t.action === 'server.installPpa') {
                                phpPpaActive = true;
                            } else if (t.payload && t.payload.version) {
                                var op = t.action === 'server.installPhp' ? 'install'
                                       : t.action === 'server.deletePhp'  ? 'delete' : null;
                                if (op) phpActiveByVersion[String(t.payload.version)] = op;
                            }
                        }
                        var ver = (t.payload && t.payload.version) ? t.payload.version : '';
                        var act = t.action.replace('server.', '');
                        // Whole row clickable → opens the shared View-log modal.
                        var line = '<div class="puq-php-task-row" data-task-id="' + t.id + '" style="border-bottom:1px solid #eee;padding:5px 0;cursor:pointer;" title="Click to view full log">' +
                            '<div><code>' + esc(act) + (ver ? ' ' + esc(ver) : '') + '</code> ' + phpTaskBadge(t.status) +
                              ' <button type="button" class="btn btn-xs btn-default puq-php-task-log pull-right" title="View log"><i class="fa fa-list-alt"></i></button>' +
                            '</div>' +
                            '<div class="text-muted small">#' + t.id + ' · ' + esc(t.created_at || '') + '</div>';
                        if (t.error) {
                            line += '<div class="small text-danger" style="word-break:break-word;">' + esc((t.error || '').substring(0,200)) + '</div>';
                        }
                        if (t.log_tail && (t.status === 'running' || t.status === 'queued')) {
                            var tail = (t.log_tail || '').split('\n').slice(-3).join('\n');
                            line += '<pre class="small" style="margin:3px 0 0 0;font-size:10px;background:#f8f8f8;padding:4px 6px;max-height:60px;overflow:auto;">' + esc(tail) + '</pre>';
                        }
                        line += '</div>';
                        $box.append(line);
                    });
                    // Click anywhere on the row to view full log.
                    $('#puq-php-tasks').off('click.tasklog').on('click.tasklog', '.puq-php-task-row', function(e) {
                        e.stopPropagation();
                        var id = parseInt($(this).attr('data-task-id'), 10);
                        if (id) window.puqOpenTaskLog(id);
                    });
                    // Push the active-state we just collected into the rows
                    // and toolbars. Rows whose version is being installed/removed
                    // get a spinner; the IonCube and PPA toolbar buttons also.
                    applyPhpRowSpinners();
                    updateIoncubeButton();
                    updatePpaButton();

                    // Detect active → idle transition: a task just finished, so
                    // the snapshot is now stale (new PHP version installed,
                    // IonCube ini written, etc). Refresh once.
                    if (phpHadActive && !hasActive) {
                        phpHadActive = false;
                        if (phpPollTimer) { clearTimeout(phpPollTimer); phpPollTimer = null; }
                        showNotification('PHP task finished — refreshing snapshot', 'success');
                        loadPhp();
                        return;
                    }
                    phpHadActive = hasActive;

                    // Auto-poll while a task is active (queued/running).
                    if (hasActive) {
                        if (phpPollTimer) clearTimeout(phpPollTimer);
                        phpPollTimer = setTimeout(loadPhp, 3000);
                    } else if (phpPollTimer) {
                        clearTimeout(phpPollTimer); phpPollTimer = null;
                    }
                }
            });
        }

        function phpEnqueue(action, payload, btn) {
            // Save the button's original HTML so we can restore the exact label
            // (icon + text) after the request — btnRestore('','') would clear it.
            var $btn = $(btn);
            var origHtml = $btn.html();
            $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Queuing…');
            // Send `payload` as a nested object — jQuery serializes to
            // `payload[version]=8.2` style, so PHP gets $_POST['payload']
            // already as an array. Avoids the JSON.stringify round trip that
            // was sometimes coming through empty.
            $.ajax({
                url: baseUrl + 'enqueueServerAction',
                type: 'POST',
                traditional: false,                  // ensure deep param serialization
                data: { server_id: serverId, action: action, payload: payload || {} },
                dataType: 'json',
                success: function(r) {
                    if (r.status === 'success') {
                        showNotification(r.msg || 'Queued', 'success');
                        setTimeout(loadPhpTasks, 600);
                    } else {
                        showNotification(r.msg || 'Failed', 'error');
                    }
                },
                error: function() { showNotification('Request failed.', 'error'); },
                complete: function() { $btn.prop('disabled', false).html(origHtml); }
            });
        }

        $('#puq-php-refresh').on('click', function(){
            var btn = this; btnSpinner(btn);
            loadPhp();
            setTimeout(function(){ btnRestore(btn, 'sync', 'Refresh'); }, 600);
        });

        $('#puq-php-table').on('click', '.puq-php-install', function(){
            if (!requireOnline()) return;
            // attr() returns the raw string ("8.0"); .data() coerces it to a Number ("8.0" → 8)
            // and strips the precision the worker's regex needs.
            var ver = String($(this).attr('data-ver') || '');
            if (!ver) { showNotification('Version missing on button — refresh the page', 'error'); return; }
            phpEnqueue('installPhp', { version: ver }, this);
        });
        $('#puq-php-table').on('click', '.puq-php-delete', function(){
            if (!requireOnline()) return;
            var ver = String($(this).attr('data-ver') || '');
            if (!ver) { showNotification('Version missing on button — refresh the page', 'error'); return; }
            var btn = this;
            puqConfirm({
                type: 'danger', icon: 'fa-trash',
                title: 'Remove PHP ' + esc(ver) + '?',
                text: 'PHP <code>' + esc(ver) + '</code> will be uninstalled from the server.',
                ok: 'Remove', cancel: 'Cancel',
                onConfirm: function(){ phpEnqueue('deletePhp', { version: ver }, btn); }
            });
        });

        $('#puq-php-install-ioncube').on('click', function(){
            if (!requireOnline()) return;
            var btn = this;
            puqConfirm({
                type: 'info', icon: 'fa-puzzle-piece',
                title: 'Install IonCube Loader?',
                text: 'Installs IonCube Loader for <strong>all installed PHP versions</strong> on this server. Takes ~30–90 seconds.',
                ok: 'Install', cancel: 'Cancel',
                onConfirm: function(){ phpEnqueue('installIoncube', { version: '' }, btn); }
            });
        });

        // PPA install — banner button, delegated since the banner is rebuilt on each loadPhp.
        $('#puq-php-ppa-banner').on('click', '#puq-php-install-ppa', function(){
            if (!requireOnline()) return;
            var btn = this;
            puqConfirm({
                type: 'info', icon: 'fa-download',
                title: 'Install ondrej/php PPA?',
                text: 'Adds the third-party PHP repository so additional PHP versions can be installed. Takes ~60–120 seconds.',
                ok: 'Install PPA', cancel: 'Cancel',
                onConfirm: function(){ phpEnqueue('installPpa', { version: '' }, btn); }
            });
        });

        $('#puq-php-install-all').on('click', function(){
            if (!requireOnline()) return;
            var $btn = $(this);
            if (!phpSnapshot || !phpSnapshot.rows) { showNotification('Refresh first', 'error'); return; }
            var missing = phpSnapshot.rows.filter(function(r){ return !r.installed; });
            if (!missing.length) { showNotification('All candidate versions already installed', 'info'); return; }
            puqConfirm({
                type: 'info', icon: 'fa-download',
                title: 'Install missing PHP versions?',
                text: 'Queue install tasks for <strong>' + missing.length + '</strong> missing PHP version(s).',
                ok: 'Install', cancel: 'Cancel',
                onConfirm: function(){ runInstallAll(); }
            });
        });
        function runInstallAll(){
            var $btn = $('#puq-php-install-all');
            var missing = phpSnapshot.rows.filter(function(r){ return !r.installed; });
            var origHtml = $btn.html();
            $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Queuing ' + missing.length + ' tasks…');
            var done = 0;
            missing.forEach(function(r){
                $.ajax({
                    url: baseUrl + 'enqueueServerAction',
                    type: 'POST',
                    traditional: false,
                    data: { server_id: serverId, action: 'installPhp', payload: { version: String(r.version) } },
                    dataType: 'json',
                    complete: function() {
                        done++;
                        if (done === missing.length) {
                            $btn.prop('disabled', false).html(origHtml);
                            showNotification('Queued ' + missing.length + ' install task(s)', 'success');
                            setTimeout(loadPhpTasks, 600);
                        }
                    }
                });
            });
        }

        $('#puq-php-matrix-open').on('click', function(){
            if (!phpSnapshot || !phpSnapshot.rows) { showNotification('Refresh first', 'error'); return; }
            var installedRows = phpSnapshot.rows.filter(function(r){ return r.installed; });
            if (!installedRows.length) { showNotification('No PHP versions installed yet', 'info'); return; }

            // Union of all modules across all installed versions.
            var allMods = {};
            installedRows.forEach(function(r){ (r.modules || []).forEach(function(m){ allMods[m] = true; }); });
            var moduleNames = Object.keys(allMods).sort();

            var $thead = $('#puq-php-matrix-table thead').empty();
            var $tr = $('<tr><th>Module</th></tr>');
            installedRows.forEach(function(r){ $tr.append('<th>PHP ' + esc(r.version) + '</th>'); });
            $thead.append($tr);

            var $tbody = $('#puq-php-matrix-table tbody').empty();
            if (!moduleNames.length) {
                $tbody.append('<tr><td colspan="' + (installedRows.length+1) + '" class="text-muted">No modules detected.</td></tr>');
            } else {
                moduleNames.forEach(function(mod){
                    var row = '<tr><td><code>' + esc(mod) + '</code></td>';
                    installedRows.forEach(function(r){
                        var has = (r.modules || []).indexOf(mod) !== -1;
                        row += has
                            ? '<td class="text-success" style="text-align:center;background:#dff0d8;"><i class="fa fa-check"></i></td>'
                            : '<td class="text-danger" style="text-align:center;background:#f2dede;font-weight:bold;"><i class="fa fa-times"></i></td>';
                    });
                    row += '</tr>';
                    $tbody.append(row);
                });
            }
            $('#puq-php-matrix').show();
        });
        $('#puq-php-matrix-close').on('click', function(){ $('#puq-php-matrix').hide(); });

        // Lazy-load PHP data when the tab is opened the first time.
        var phpLoaded = false;
        $('a[href="#puq-tab-php"]').on('shown.bs.tab', function(){
            if (!phpLoaded) { phpLoaded = true; loadPhp(); }
            else { loadPhpTasks(); }
        });

        // ===================================================================
        // FILE MANAGERS tab (FileGator + net2ftp, parallel)
        // ===================================================================
        // Both engines share the same state-machine pattern:
        //   1. Pull recent server tasks (with prefix server.) once.
        //   2. For each engine: find any queued/running task of its actions,
        //      render its panel (status + buttons gated on active task).
        //   3. If ANY engine has an active task → poll every 3 s.

        var FM_BTN_REFRESH = '<button type="button" class="btn btn-default btn-sm puq-fm-refresh"><i class="fas fa-redo"></i> Refresh</button>';

        // Per-engine config — DOM selectors + action names.
        var ENGINES = {
            filegator: {
                key:           'filegator',
                label:         'FileGator',
                statusEl:      '#puq-fm-status',
                actionsEl:     '#puq-fm-actions',
                customUrlEl:   '#puq-fm-custom-url',
                actInstall:    'installFileManager',
                actUpdate:     'updateFileManager',
                actUninstall:  'uninstallFileManager'
            },
            net2ftp: {
                key:           'net2ftp',
                label:         'net2ftp',
                statusEl:      '#puq-n2f-status',
                actionsEl:     '#puq-n2f-actions',
                customUrlEl:   '#puq-n2f-custom-url',
                actInstall:    'installNet2Ftp',
                actUpdate:     'updateNet2Ftp',
                actUninstall:  'uninstallNet2Ftp'
            }
        };

        var fmPollTimer = null;
        var fmStatusCache = null;
        var fmInFlight = false;
        var fmHadActive = { filegator: false, net2ftp: false };

        function fmIsEngineTask(action, engine) {
            if (!action) return false;
            var a = String(action);
            return a === 'server.' + engine.actInstall
                || a === 'server.' + engine.actUpdate
                || a === 'server.' + engine.actUninstall;
        }
        function fmIsAnyFmTask(action) {
            return fmIsEngineTask(action, ENGINES.filegator) || fmIsEngineTask(action, ENGINES.net2ftp);
        }

        function fmTaskBadge(status) {
            var color = status === 'success' ? 'success'
                      : status === 'error'   ? 'danger'
                      : status === 'running' ? 'info'
                      : 'warning';
            return '<span class="label label-' + color + '">' + status + '</span>';
        }

        function loadFmStatus() {
            if (fmInFlight) return;
            fmInFlight = true;

            puqAjax({
                container: '#puq-fm-tasks',
                loadingCaption: 'Loading file-manager tasks…',
                url: baseUrl + 'serverTasks&server_id=' + serverId + '&action_prefix=server.&limit=50',
                dataType: 'json',
                error: function() { fmInFlight = false; },
                success: function(rTasks) {
                    var allTasks = (rTasks && rTasks.data) ? rTasks.data : [];
                    var fmTasks = allTasks.filter(function(t){ return fmIsAnyFmTask(t.action); });
                    renderFmTasks(fmTasks);

                    // First active task per engine.
                    var active = { filegator: null, net2ftp: null };
                    fmTasks.forEach(function(t){
                        Object.keys(ENGINES).forEach(function(k){
                            if (!active[k] && fmIsEngineTask(t.action, ENGINES[k])
                                && (t.status === 'queued' || t.status === 'running')) {
                                active[k] = t;
                            }
                        });
                    });
                    var anyActive = !!(active.filegator || active.net2ftp);

                    // Use cached status while ANY task is mid-flight (probe stale).
                    if (anyActive && fmStatusCache) {
                        renderEnginePanel('filegator', fmStatusCache.filegator || {}, active.filegator);
                        renderEnginePanel('net2ftp',   fmStatusCache.net2ftp   || {}, active.net2ftp);
                        fmInFlight = false;
                        scheduleFmPoll(true);
                        return;
                    }
                    $.ajax({
                        url: baseUrl + 'fileManagerStatus&server_id=' + serverId,
                        type: 'GET', dataType: 'json',
                        complete: function(){ fmInFlight = false; },
                        success: function(r){
                            if (r.status === 'success') fmStatusCache = r.data;
                            var src = fmStatusCache || (r.data || {});
                            renderEnginePanel('filegator', src.filegator || {}, active.filegator);
                            renderEnginePanel('net2ftp',   src.net2ftp   || {}, active.net2ftp);

                            // Active → idle transition per engine.
                            ['filegator', 'net2ftp'].forEach(function(k){
                                if (fmHadActive[k] && !active[k]) {
                                    showNotification(ENGINES[k].label + ' task finished — refreshing', 'success');
                                    setTimeout(loadFmStatus, 800);
                                }
                                fmHadActive[k] = !!active[k];
                            });
                            scheduleFmPoll(anyActive);
                        },
                        error: function(x){
                            $('#puq-fm-status, #puq-n2f-status').html(
                                '<div class="alert alert-danger" style="margin:0;">Probe failed: HTTP ' + x.status + '</div>'
                            );
                            $('#puq-fm-actions, #puq-n2f-actions').html(FM_BTN_REFRESH);
                            scheduleFmPoll(anyActive);
                        }
                    });
                }
            });
        }

        function scheduleFmPoll(hasActive) {
            if (fmPollTimer) { clearTimeout(fmPollTimer); fmPollTimer = null; }
            if (hasActive) fmPollTimer = setTimeout(loadFmStatus, 3000);
        }

        // Render one engine's status block + action buttons. `d` is the
        // per-engine payload: { local, live, default_url, default_version, default_sha256 }
        function renderEnginePanel(engineKey, d, activeTask) {
            var engine = ENGINES[engineKey];
            var live = d.live && d.live.data ? d.live.data : null;
            var local = d.local || {};
            var defUrl = d.default_url || '';
            var defVer = d.default_version || '';
            var customUrlInputValue = local.custom_url || '';

            // Status block
            var html = '';
            if (live && live.installed) {
                html += '<div class="alert alert-success" style="margin:0;padding:8px 12px;">'
                      + '<i class="fas fa-check-circle"></i> <strong>Installed</strong>'
                      + (live.version ? ' · v' + live.version : '')
                      + (local.custom_url ? ' · <span class="label label-info">custom URL</span>' : ' · <span class="label label-default">default URL</span>')
                      + ' · <a href="' + (live.url || '#') + '" target="_blank" rel="noopener">' + (live.url || '') + '</a>'
                      + (local.installed_at ? '<br><small class="text-muted">installed ' + local.installed_at + '</small>' : '')
                      + '</div>';
            } else if (live && live.installed === false) {
                html += '<div class="alert alert-info" style="margin:0;padding:8px 12px;">'
                      + '<i class="fas fa-info-circle"></i> <strong>Not installed</strong>'
                      + '<br><small class="text-muted">target: <code>' + (live.install || '') + '</code></small>'
                      + '</div>';
            } else {
                var errMsg = (d.live && d.live.error) ? d.live.error : 'no data';
                html += '<div class="alert alert-danger" style="margin:0;padding:8px 12px;"><i class="fas fa-exclamation-triangle"></i> ' + errMsg + '</div>';
                if (local.installed) {
                    html += '<div class="alert alert-warning" style="margin:6px 0 0;padding:6px 12px;font-size:11px;">Local DB says it WAS installed — server may have changed.</div>';
                }
            }
            $(engine.statusEl).html(html);

            // Custom URL input — placeholder = default URL, value = stored override.
            var $input = $(engine.customUrlEl);
            $input.attr('placeholder', defUrl);
            // Only set value once (on first render) so admin's typing isn't clobbered by polls.
            if ($input.data('initialized') !== '1') {
                $input.val(customUrlInputValue);
                $input.data('initialized', '1');
            }

            // Actions — gated on activeTask
            if (activeTask) {
                var actionName = String(activeTask.action || '').replace('server.', '');
                $(engine.actionsEl).html(
                    '<div class="alert alert-info" style="margin:0;padding:8px 12px;">'
                    + '<i class="fas fa-spinner fa-spin"></i> '
                    + '<strong>' + actionName + '</strong> · task #' + activeTask.id + ' · ' + fmTaskBadge(activeTask.status)
                    + '<br><small class="text-muted">Auto-refresh every 3 s.</small>'
                    + '</div>'
                );
                return;
            }
            var actions = '';
            if (live && live.installed) {
                actions = '<button type="button" class="btn btn-warning btn-sm puq-fm-act" data-engine="' + engineKey + '" data-act="update">'
                        + '<i class="fas fa-sync"></i> Reinstall / update</button> '
                        + '<button type="button" class="btn btn-danger btn-sm puq-fm-act" data-engine="' + engineKey + '" data-act="uninstall">'
                        + '<i class="fas fa-trash"></i> Uninstall</button> '
                        + FM_BTN_REFRESH;
            } else if (live && live.installed === false) {
                actions = '<button type="button" class="btn btn-primary btn-sm puq-fm-act" data-engine="' + engineKey + '" data-act="install">'
                        + '<i class="fas fa-download"></i> Install ' + (defVer ? '(' + defVer + ')' : '')
                        + '</button> ' + FM_BTN_REFRESH;
            } else if (local.installed) {
                actions = '<button type="button" class="btn btn-warning btn-sm puq-fm-act" data-engine="' + engineKey + '" data-act="update">'
                        + '<i class="fas fa-sync"></i> Reinstall</button> '
                        + '<button type="button" class="btn btn-danger btn-sm puq-fm-act" data-engine="' + engineKey + '" data-act="uninstall">'
                        + '<i class="fas fa-trash"></i> Clean state</button> '
                        + FM_BTN_REFRESH;
            } else {
                actions = FM_BTN_REFRESH;
            }
            $(engine.actionsEl).html(actions);
        }

        function renderFmTasks(tasks) {
            var $tb = $('#puq-fm-tasks-table tbody').empty();
            if (!tasks.length) {
                $tb.append('<tr><td colspan="5" class="text-muted">No tasks yet.</td></tr>');
                return;
            }
            tasks.slice(0, 20).forEach(function(t){
                var act = String(t.action || '').replace('server.', '');
                var row = '<tr class="puq-fm-task-row" data-task-id="' + t.id + '" style="cursor:pointer;" title="Click to view full log">'
                        + '<td>#' + t.id + '</td>'
                        + '<td><code>' + act + '</code></td>'
                        + '<td>' + fmTaskBadge(t.status) + '</td>'
                        + '<td><small>' + ((t.error || t.last_log || '') + '').substring(0, 200) + '</small></td>'
                        + '<td><small>' + (t.updated_at || t.created_at || '') + '</small></td>'
                        + '</tr>';
                $tb.append(row);
            });
            $('#puq-fm-tasks-table').off('click.fmlog').on('click.fmlog', '.puq-fm-task-row', function(){
                var id = parseInt($(this).attr('data-task-id'), 10);
                if (id && window.puqOpenTaskLog) window.puqOpenTaskLog(id);
            });
        }

        function fmEnqueue(engineKey, actKind, btn) {
            var engine = ENGINES[engineKey];
            var actionName = engine['act' + actKind.charAt(0).toUpperCase() + actKind.slice(1)];
            var customUrl = ($(engine.customUrlEl).val() || '').trim();
            var meta = ({
                install:   { title: 'Install ' + engine.label + '?',     text: 'Install <strong>' + esc(engine.label) + '</strong> on this server.', icon: 'fa-download',  type: 'info',    ok: 'Install' },
                update:    { title: 'Reinstall ' + engine.label + '?',   text: 'Reinstall <strong>' + esc(engine.label) + '</strong>. Existing files are overwritten.', icon: 'fa-sync',  type: 'warning', ok: 'Reinstall' },
                uninstall: { title: 'Remove ' + engine.label + '?',      text: 'Remove <strong>' + esc(engine.label) + '</strong> from this server. Customer links will stop working.', icon: 'fa-trash', type: 'danger',  ok: 'Remove' }
            })[actKind];

            puqConfirm({
                type: meta.type, icon: meta.icon, title: meta.title, text: meta.text,
                ok: meta.ok, cancel: 'Cancel',
                onConfirm: function(){
                    btn.disabled = true;
                    btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Queuing…';
                    var payload = (actKind === 'uninstall') ? {} : { custom_url: customUrl };
                    return new Promise(function(resolve, reject){
                        $.ajax({
                            url: baseUrl + 'enqueueServerAction',
                            type: 'POST', dataType: 'json',
                            data: { server_id: serverId, action: actionName, payload: payload },
                            success: function(r){
                                if (r.status === 'success') {
                                    showNotification(r.msg || ('Queued ' + actionName), 'success');
                                    fmStatusCache = null; fmInFlight = false;
                                    $(engine.customUrlEl).data('initialized', '0');
                                    setTimeout(loadFmStatus, 200);
                                    resolve();
                                } else {
                                    setTimeout(loadFmStatus, 200);
                                    reject(new Error(r.msg || 'Enqueue failed'));
                                }
                            },
                            error: function(x){
                                setTimeout(loadFmStatus, 200);
                                reject(new Error('Enqueue failed (HTTP ' + x.status + ')'));
                            }
                        });
                    });
                }
            });
        }

        // Delegated handlers
        $(document).on('click', '.puq-fm-act', function(){
            fmEnqueue($(this).data('engine'), $(this).data('act'), this);
        });
        $(document).on('click', '.puq-fm-refresh', function(){
            this.disabled = true;
            this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing…';
            // Reset both inputs so re-rendered values reflect DB truth.
            $('#puq-fm-custom-url, #puq-n2f-custom-url').data('initialized', '0');
            loadFmStatus();
        });

        // Lazy-load on first tab open + poll tasks while open.
        var fmLoaded = false;
        $('a[href="#puq-tab-filemanager"]').on('shown.bs.tab', function(){
            if (!fmLoaded) { fmLoaded = true; loadFmStatus(); }
            else { loadFmStatus(); }
        });
    });
})();
</script>
{/literal}
