{* Full-page group editor. URL: &page=group_edit&id=N  (or no id for "create new")
   Tabs: Actions / General / Hestia config / Standard files.
   Each tab body is loaded lazily via AJAX (renderGroupEditTab) — the initial
   page emits only the shell + JS loader to keep first paint fast. *}

<style>
.puq-grp-hdr { padding:10px 14px; background:#f5f5f5; border:1px solid #ddd; border-radius:3px; margin-bottom:14px; display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
.puq-grp-hdr h3 { margin:0; font-size:18px; }
.puq-grp-hdr .name { color:#337ab7; }
.puq-grp-hdr .actions { margin-left:auto; display:flex; gap:6px; }
.puq-grp-tab-content { border:1px solid #ddd; border-top:none; padding:14px; background:#fafafa; min-height:420px; }
.puq-grp-tab-content table.table { background:#fff; }
.puq-hestia-pills { float:left; width:170px; margin-bottom:8px; }
.puq-hestia-pane { margin-left:185px; min-height:400px; }
.puq-stdfile-row { display:flex; align-items:center; gap:10px; padding:6px 8px; border-bottom:1px solid #eee; }
.puq-stdfile-row:last-child { border-bottom:none; }
.puq-stdfile-row .meta { flex:1; }
.puq-stdfile-row code { font-size:11px; color:#888; }
.puq-stdfile-row .label-text { font-weight:500; }
.puq-stdfile-status { display:inline-block; padding:1px 6px; border-radius:2px; font-size:10px; font-weight:bold; }
.puq-stdfile-status.default { background:#eee; color:#888; }
.puq-stdfile-status.overridden { background:#5cb85c; color:#fff; }
.puq-stdfile-status.disabled { background:#f0ad4e; color:#fff; }
.puq-tab-loading { padding:40px; text-align:center; color:#999; }
.puq-preview-frame { width:100%; height:480px; border:1px solid #ddd; background:#fff; }
</style>

<div class="puq-grp-hdr">
    <a class="btn btn-default btn-sm" href="{$meta.admin_module_url}&page=server_groups">
        <i class="fa fa-arrow-left"></i> Back
    </a>
    <h3>
        <i class="fa fa-layer-group"></i>
        {if $is_new}Create server group{else}<span class="name">{$group.name}</span>{/if}
    </h3>
    <span class="text-muted small" id="puq-grp-summary"></span>
    <div class="actions">
        <button type="button" class="btn btn-primary" id="puq-save-all"><i class="fa fa-save"></i> Save group</button>
    </div>
</div>

<ul class="nav nav-tabs" id="puq-grp-tabs">
    <li data-types="standard"{if $initial_tab eq 'actions'} class="active"{/if}><a href="#tab-actions" data-toggle="tab" data-tab="actions"><i class="fa fa-bolt"></i> Actions</a></li>
    <li data-types="standard vanity"{if $initial_tab eq 'general'} class="active"{/if}><a href="#tab-general" data-toggle="tab" data-tab="general"><i class="fa fa-info-circle"></i> General</a></li>
    <li data-types="standard"{if $initial_tab eq 'hestia'} class="active"{/if}><a href="#tab-hestia" data-toggle="tab" data-tab="hestia"><i class="fa fa-sliders-h"></i> All-servers config</a></li>
    <li data-types="standard"{if $initial_tab eq 'web'} class="active"{/if}><a href="#tab-web" data-toggle="tab" data-tab="web"><i class="fa fa-globe"></i> Web config</a></li>
    <li data-types="standard"{if $initial_tab eq 'mail'} class="active"{/if}><a href="#tab-mail" data-toggle="tab" data-tab="mail"><i class="fa fa-envelope"></i> Mail config</a></li>
    <li data-types="standard"{if $initial_tab eq 'dns_config'} class="active"{/if}><a href="#tab-dns_config" data-toggle="tab" data-tab="dns_config"><i class="fa fa-network-wired"></i> DNS config</a></li>
    <li data-types="standard vanity"{if $initial_tab eq 'variables'} class="active"{/if}><a href="#tab-variables" data-toggle="tab" data-tab="variables"><i class="fa fa-tags"></i> Variables</a></li>
    <li data-types="standard"{if $initial_tab eq 'stdfiles'} class="active"{/if}><a href="#tab-stdfiles" data-toggle="tab" data-tab="stdfiles"><i class="fa fa-file-alt"></i> Standard files</a></li>
    <li data-types="standard vanity"{if $initial_tab eq 'dns'} class="active"{/if}><a href="#tab-dns" data-toggle="tab" data-tab="dns"><i class="fa fa-server"></i> DNS servers</a></li>
    <li data-types="vanity"{if $initial_tab eq 'vanity'} class="active"{/if}><a href="#tab-vanity" data-toggle="tab" data-tab="vanity"><i class="fa fa-at"></i> Vanity domains</a></li>
</ul>

<div class="tab-content puq-grp-tab-content">
    <div class="tab-pane{if $initial_tab eq 'actions'} active{/if}" id="tab-actions"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'general'} active{/if}" id="tab-general"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'hestia'} active{/if}" id="tab-hestia"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'web'} active{/if}" id="tab-web"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'mail'} active{/if}" id="tab-mail"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'dns_config'} active{/if}" id="tab-dns_config"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'variables'} active{/if}" id="tab-variables"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'stdfiles'} active{/if}" id="tab-stdfiles"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'dns'} active{/if}" id="tab-dns"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
    <div class="tab-pane{if $initial_tab eq 'vanity'} active{/if}" id="tab-vanity"><div class="puq-tab-loading"><i class="fa fa-spinner fa-spin"></i> Loading…</div></div>
</div>

{* ============ Edit / Add file modal (Override) ============ *}
<div class="modal fade" id="puq-file-modal" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal">&times;</button>
                <h4 class="modal-title" id="puq-file-modal-title">File</h4>
            </div>
            <div class="modal-body">
                <form id="puq-file-form">
                    <input type="hidden" id="puq-file-id">
                    <input type="hidden" id="puq-file-category">
                    <div class="row">
                        <div class="col-md-9">
                            <div class="form-group">
                                <label>Path</label>
                                <input type="text" id="puq-file-rel-path" class="form-control" readonly>
                            </div>
                        </div>
                        <div class="col-md-3">
                            <div class="form-group">
                                <label style="margin-top:24px;"><input type="checkbox" id="puq-file-enabled" checked> Enabled (push on Apply)</label>
                            </div>
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-md-6">
                            <div class="form-group">
                                <label>Description</label>
                                <input type="text" id="puq-file-description" class="form-control">
                            </div>
                        </div>
                        <div class="col-md-3">
                            <div class="form-group">
                                <label>Mode</label>
                                <input type="text" id="puq-file-mode" class="form-control" value="0644" required>
                            </div>
                        </div>
                        <div class="col-md-3">
                            <div class="form-group">
                                <label>Owner</label>
                                <input type="text" id="puq-file-owner" class="form-control" value="root:root" required>
                            </div>
                        </div>
                    </div>
                    <div class="form-group">
                        <label>Content</label>
                        <ul class="nav nav-tabs" id="puq-file-mode-tabs" style="margin-bottom:6px;">
                            <li class="active"><a href="#" data-mode="source"><i class="fa fa-code"></i> Source</a></li>
                            <li><a href="#" data-mode="rendered"><i class="fa fa-eye"></i> Rendered preview</a></li>
                        </ul>
                        <textarea id="puq-file-content" class="form-control" rows="18"
                            style="font-family:monospace;font-size:12px;white-space:pre;"></textarea>
                        <iframe id="puq-file-iframe" class="puq-preview-frame" sandbox="allow-same-origin" style="display:none;height:380px;"></iframe>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-danger pull-left" id="puq-file-delete" style="display:none;"><i class="fa fa-trash"></i> Delete (stop managing)</button>
                <button type="button" class="btn btn-primary" id="puq-file-save"><i class="fas fa-save"></i> Save file</button>
            </div>
        </div>
    </div>
</div>

{* ============ Pull modal — server picker + HTML preview ============ *}
<div class="modal fade" id="puq-pull-modal" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal">&times;</button>
                <h4 class="modal-title">Pull from server — <span id="puq-pull-title"></span></h4>
            </div>
            <div class="modal-body">
                <div class="row">
                    <div class="col-md-8">
                        <div class="form-group">
                            <label>Choose a server to pull from</label>
                            <select id="puq-pull-server" class="form-control"><option value="">— select —</option></select>
                        </div>
                    </div>
                    <div class="col-md-4" style="padding-top:24px;">
                        <button type="button" class="btn btn-default btn-block" id="puq-pull-load"><i class="fa fa-eye"></i> Load preview</button>
                    </div>
                </div>
                <p class="text-muted small" id="puq-pull-path"></p>
                <div id="puq-pull-preview" style="display:none;">
                    <ul class="nav nav-tabs" id="puq-pull-mode-tabs" style="margin-bottom:6px;">
                        <li class="active"><a href="#" data-mode="rendered">Rendered HTML</a></li>
                        <li><a href="#" data-mode="source">Source</a></li>
                    </ul>
                    <iframe id="puq-pull-iframe" class="puq-preview-frame" sandbox="allow-same-origin"></iframe>
                    <textarea id="puq-pull-source" class="form-control" rows="20" readonly
                        style="display:none;font-family:monospace;font-size:12px;white-space:pre;"></textarea>
                </div>
                <p class="text-muted small" id="puq-pull-hint"></p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-success" id="puq-pull-import" style="display:none;"><i class="fa fa-download"></i> Import this content</button>
            </div>
        </div>
    </div>
</div>

{literal}
<script>
(function(){
    var baseUrl = '{/literal}{$meta.admin_module_url}&ajax={literal}';
    var groupUrl = '{/literal}{$meta.admin_module_url}{literal}';
    var groupId = parseInt('{/literal}{$group_id}{literal}', 10) || 0;
    var isNew = {/literal}{if $is_new}true{else}false{/if}{literal};
    var initialTab = '{/literal}{$initial_tab|escape:javascript}{literal}';
    var groupType = '{/literal}{$group.group_type|default:"standard"|escape:javascript}{literal}';

    // Show only the nav tabs whose data-types includes the group's purpose.
    // Vanity hides the hosting-config tabs; standard hides Vanity domains.
    function applyGroupTabsByType(type) {
        groupType = (type === 'vanity') ? 'vanity' : 'standard';
        var activeHidden = false;
        $('#puq-grp-tabs > li').each(function(){
            var types = (this.getAttribute('data-types') || 'standard vanity').split(/\s+/);
            var show = types.indexOf(groupType) >= 0;
            this.style.display = show ? '' : 'none';
            if (!show && $(this).hasClass('active')) activeHidden = true;
        });
        // If the visible tab set no longer includes the active one, fall back
        // to General so the body isn't left on a hidden pane.
        if (activeHidden) {
            var gen = document.querySelector('#puq-grp-tabs a[data-tab="general"]');
            if (gen && typeof jQuery !== 'undefined') jQuery(gen).tab('show');
        }
    }


    // ============ Lazy tab loader ============
    var tabsLoaded = {};

    function loadTab(tab, cb) {
        if (tabsLoaded[tab]) { if (cb) cb(); return; }
        var $pane = $('#tab-' + tab);
        puqAjax({
            container: $pane,
            loadingCaption: 'Loading…',
            url: baseUrl + 'renderGroupEditTab&tab=' + encodeURIComponent(tab) + '&group_id=' + groupId,
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success' && r.data && r.data.html != null) {
                    $pane.html(r.data.html);
                    tabsLoaded[tab] = true;
                    initTab(tab);
                } else {
                    $pane.html('<div class="alert alert-danger">' + esc(r.msg || 'Failed to load tab') + '</div>');
                }
                if (cb) cb();
            },
            error: function() {
                $pane.html('<div class="alert alert-danger">Request failed.</div>');
                if (cb) cb();
            }
        });
    }

    // Config tabs: name → {target, container, layout (pills|flat)}
    // hestia uses pills (multi-category target=all); the per-type tabs are flat.
    var CFG_TABS = {
        'hestia':     { target: 'all',  container: '#puq-hestia-cat-content',     layout: 'pills' },
        'web':        { target: 'web',  container: '#puq-web-cat-content',        layout: 'flat'  },
        'mail':       { target: 'mail', container: '#puq-mail-cat-content',       layout: 'flat'  },
        'dns_config': { target: 'dns',  container: '#puq-dns_config-cat-content', layout: 'flat'  }
    };

    function initTab(tab) {
        if (tab === 'actions') initActionsTab();
        else if (tab === 'stdfiles') initStdFilesTab();
        else if (tab === 'dns') initDnsTab();
        else if (tab === 'vanity') initVanityTab();
        else if (tab === 'variables') initVariablesTab();
        else if (CFG_TABS[tab]) initCfgTab(tab);
        // 'general' tab needs no JS init — Save reads its inputs directly.
    }

    // ============ Save group ============
    // Collect inputs from every config tab (hestia/web/mail/dns_config) — they
    // all share the same config_hestia JSON storage; the apply-worker filters
    // keys to the right server type at push time.
    function collectHestiaForm() {
        var out = {};
        Object.keys(CFG_TABS).forEach(function(name){
            $(CFG_TABS[name].container + ' [data-hestia-key]').each(function(){
                var key = this.getAttribute('data-hestia-key');
                var val = (this.value || '').toString();
                if (val !== '') out[key] = val;
            });
        });
        return out;
    }

    function saveGroup() {
        var btn = document.getElementById('puq-save-all');
        btnSpinner(btn, 'Saving...');
        var payload = {
            id: $('#puq-grp-id').val() || groupId || 0,
            name: $('#puq-grp-name').val(),
            description: $('#puq-grp-description').val(),
            group_type: ($('input[name="group_type"]:checked').val() || groupType),
            config_hestia: JSON.stringify(collectHestiaForm())
        };
        // Vanity reserved-names — only sent when the field is in the DOM (the
        // General tab is loaded), so a standard-group save never wipes it.
        var $rn = $('#puq-grp-reserved-names');
        if ($rn.length) payload.reserved_names = $rn.val();
        $.ajax({
            url: baseUrl + 'saveServerGroup', type: 'POST', data: payload, dataType: 'json',
            success: function(r) {
                if (r.status === 'success') {
                    showNotification('Saved.', 'success');
                    if (isNew && r.id) {
                        window.location.href = groupUrl + '&page=group_edit&id=' + r.id;
                    }
                } else {
                    showNotification(r.msg || 'Save failed', 'error');
                }
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'save', 'Save group'); }
        });
    }

    // ============ Servers list (shared by Actions tab + Pull picker) ============
    var groupServers = [];
    function fetchGroupServers(cb) {
        if (isNew) { groupServers = []; if (cb) cb(); return; }
        puqAjax({
            container: '#puq-grp-srv-table',
            loadingCaption: 'Loading servers…',
            url: baseUrl + 'listServers&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                groupServers = (r.status === 'success') ? (r.data || []) : [];
                $('#puq-grp-summary').html(
                    '<i class="fa fa-server"></i> ' + groupServers.length + ' server(s) — '
                    + groupServers.filter(function(s){ return s.status_state === 'ok'; }).length + ' online'
                );
                if (cb) cb();
            }
        });
    }

    // ============ Actions tab ============
    function initActionsTab() {
        if (isNew) return;
        fetchGroupServers(function(){
            $('#puq-grp-srv-count').text(groupServers.length);
            var $tbody = $('#puq-grp-srv-table tbody').empty();
            if (!groupServers.length) {
                $tbody.append('<tr><td colspan="5" class="text-muted text-center">No servers in this group yet.</td></tr>');
                return;
            }
            groupServers.forEach(function(s){
                var stateCls = {ok:'success',error:'danger',queued:'warning',collecting:'info',never:'default'}[s.status_state] || 'default';
                // Show every role this server carries (a unified box is web+mail+dns
                // at once), not just the single legacy `type` column.
                var roleBadges = '';
                [['web','caps_web','primary'],['mail','caps_mail','success'],['dns','caps_dns','info']].forEach(function(p){
                    var on = parseInt(s[p[1]] || 0, 10) === 1;
                    roleBadges += '<span class="label label-' + (on ? p[2] : 'default') + '" style="margin-right:3px;opacity:' + (on ? 1 : 0.35) + '">' + p[0] + '</span>';
                });
                // Pre-caps rows (all caps 0) fall back to the legacy type label.
                if (!parseInt(s.caps_web||0,10) && !parseInt(s.caps_mail||0,10) && !parseInt(s.caps_dns||0,10)) {
                    roleBadges = '<span class="label label-default">' + esc(s.type) + '</span>';
                }
                $tbody.append(
                    '<tr>' +
                      '<td><strong>' + esc(s.name) + '</strong><br><small class="text-muted">' + esc(s.hostname) + '</small></td>' +
                      '<td>' + roleBadges + '</td>' +
                      '<td><span class="label label-' + stateCls + '">' + esc((s.status_state || 'never').toUpperCase()) + '</span></td>' +
                      '<td><small>' + esc(s.status_last_collected_at || 'never') + '</small></td>' +
                      '<td><a class="btn btn-xs btn-default" href="' + groupUrl + '&page=server_actions&server_id=' + s.id + '" target="_blank"><i class="fa fa-cog"></i> Open</a></td>' +
                    '</tr>'
                );
            });
        });

        $('#tab-actions').on('click', '#puq-bulk-cfg', function(){ bulkApplyCfg(this, 'all',  'magic',          'Apply Hestia config to <strong>all</strong> servers'); });
        $('#tab-actions').on('click', '#puq-bulk-cfg-web',  function(){ bulkApplyCfg(this, 'web',  'globe',          'Apply config to <strong>web</strong> servers only'); });
        $('#tab-actions').on('click', '#puq-bulk-cfg-mail', function(){ bulkApplyCfg(this, 'mail', 'envelope',       'Apply config to <strong>mail</strong> servers only'); });
        $('#tab-actions').on('click', '#puq-bulk-cfg-dns',  function(){ bulkApplyCfg(this, 'dns',  'network-wired',  'Apply config to <strong>DNS</strong> servers only (Hestia-driver)'); });
        $('#tab-actions').on('click', '#puq-bulk-files', function(){ bulkApply('applyGroupFilesToAllServers', this, 'cloud-upload-alt', 'Apply standard files to web servers'); });
        $('#tab-actions').on('click', '#puq-bulk-refresh', function(){
            var btn = this; btnSpinner(btn, 'Queuing...');
            var n = 0;
            groupServers.forEach(function(s){
                $.ajax({ url: baseUrl + 'refreshServerStatus', type: 'POST', data: { id: s.id }, dataType: 'json' });
                n++;
            });
            showNotification('Queued status refresh for ' + n + ' server(s).', 'success');
            setTimeout(function(){ btnRestore(btn, 'sync', 'Refresh status on all servers'); }, 1500);
        });
    }

    function bulkApply(endpoint, btn, icon, label) {
        btnSpinner(btn, 'Queuing...');
        $.ajax({
            url: baseUrl + endpoint, type: 'POST', data: { group_id: groupId }, dataType: 'json',
            success: function(r) {
                showNotification(r.msg || (r.status === 'success' ? 'Queued.' : 'Failed.'), r.status === 'success' ? 'success' : 'error');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, icon, label); }
        });
    }

    // Targeted variant: passes ?target=all|web|mail|dns so the backend only
    // enqueues for matching server types. 'all' = web+mail+Hestia-DNS.
    function bulkApplyCfg(btn, target, icon, label) {
        btnSpinner(btn, 'Queuing...');
        $.ajax({
            url: baseUrl + 'applyGroupConfigToAllServers', type: 'POST',
            data: { group_id: groupId, target: target }, dataType: 'json',
            success: function(r) {
                showNotification(r.msg || (r.status === 'success' ? 'Queued.' : 'Failed.'), r.status === 'success' ? 'success' : 'error');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, icon, label); }
        });
    }

    // ============ Config tabs (hestia / web / mail / dns_config) ============
    var hestiaConfigSchema = null;

    function initCfgTab(name) {
        var def = CFG_TABS[name];
        if (!def) return;

        // Decode initial values from the hidden input shared per-tab. They all
        // hold the same JSON; reading from one is fine but we read the one
        // belonging to this tab for safety.
        var initialId = '#puq-' + name + '-initial';
        var initial = {};
        try {
            var raw = $(initialId).val() || '';
            if (raw) {
                var dec = $('<textarea>').html(raw).text();
                initial = JSON.parse(dec) || {};
            }
        } catch(e) {}

        if (hestiaConfigSchema) {
            renderCfgForTarget(name, def, hestiaConfigSchema);
            fillCfg(def.container, initial);
            return;
        }
        puqAjax({
            container: def.container,
            loadingCaption: 'Loading config schema…',
            url: baseUrl + 'getHestiaConfigSchema', dataType: 'json',
            success: function(r) {
                if (r.status === 'success' && r.data) {
                    hestiaConfigSchema = r.data;
                    // Render any cfg tab that's already been loaded (mounted)
                    // since the schema is global. The current tab definitely is.
                    renderCfgForTarget(name, def, hestiaConfigSchema);
                    fillCfg(def.container, initial);
                }
            }
        });
    }

    function renderCfgForTarget(name, def, schema) {
        var $body = $(def.container).empty();
        var matchingCats = Object.keys(schema).filter(function(slug){
            return (schema[slug].target || 'all') === def.target;
        });

        if (!matchingCats.length) {
            $body.html('<p class="text-muted text-center" style="padding:60px;">No fields target=' + esc(def.target) + ' in the schema.</p>');
            return;
        }

        if (def.layout === 'pills') {
            // Multi-category layout: nav-pills (vertical) + tab-content. Used
            // for the All-servers tab where target=all spans many categories.
            var $tabs = $('#puq-' + name + '-cat-tabs').empty();
            var first = true;
            matchingCats.forEach(function(slug){
                var cat = schema[slug];
                var target = (cat.target || 'all').toString();
                var badgeCls = target === 'web' ? 'primary' : (target === 'mail' ? 'warning' : (target === 'dns' ? 'success' : 'default'));
                var badge = ' <span class="label label-' + badgeCls + '" style="font-size:9px;font-weight:normal;">' + esc(target) + '</span>';
                $tabs.append('<li' + (first ? ' class="active"' : '') + '><a href="#puq-' + name + '-cat-' + slug + '" data-toggle="pill"><i class="fa ' + esc(cat.icon || 'fa-cog') + '"></i> ' + esc(cat.label) + badge + '</a></li>');
                var fieldsHtml = (cat.fields || []).map(renderHestiaField).join('');
                $body.append('<div class="tab-pane' + (first ? ' active' : '') + '" id="puq-' + name + '-cat-' + slug + '">' + fieldsHtml + '</div>');
                first = false;
            });
            $tabs.find('a').on('click', function(e){ e.preventDefault(); $(this).tab('show'); });
        } else {
            // Flat layout: each category as a panel, fields stacked. Used for
            // the per-type tabs (web/mail/dns_config) where only 1–2 cats apply.
            matchingCats.forEach(function(slug){
                var cat = schema[slug];
                var fieldsHtml = (cat.fields || []).map(renderHestiaField).join('');
                $body.append(
                    '<div class="panel panel-default" style="margin-top:8px;">' +
                      '<div class="panel-heading" style="padding:6px 12px;">' +
                        '<i class="fa ' + esc(cat.icon || 'fa-cog') + '"></i> ' + esc(cat.label) +
                      '</div>' +
                      '<div class="panel-body">' + fieldsHtml + '</div>' +
                    '</div>'
                );
            });
        }
    }

    function renderHestiaField(f) {
        var inputId = 'puq-hkey-' + f.key, input = '';
        if (f.type === 'select') {
            var opts = '<option value="">(not managed by group)</option>';
            Object.keys(f.options || {}).forEach(function(v){ opts += '<option value="' + esc(v) + '">' + esc(f.options[v]) + '</option>'; });
            input = '<select id="' + inputId + '" data-hestia-key="' + esc(f.key) + '" class="form-control input-sm">' + opts + '</select>';
        } else if (f.type === 'textarea') {
            input = '<textarea id="' + inputId + '" data-hestia-key="' + esc(f.key) + '" class="form-control input-sm" rows="3" placeholder="(not managed by group)"></textarea>';
        } else if (f.type === 'number') {
            input = '<input type="number" id="' + inputId + '" data-hestia-key="' + esc(f.key) + '" class="form-control input-sm" placeholder="(not managed)">';
        } else {
            input = '<input type="text" id="' + inputId + '" data-hestia-key="' + esc(f.key) + '" class="form-control input-sm" placeholder="(not managed)">';
        }
        return '<div class="form-group" style="margin-bottom:8px;">'
             + '<label style="font-weight:500;font-size:12px;margin-bottom:2px;" for="' + inputId + '">'
             + esc(f.label) + ' <code style="font-size:10px;color:#888;">' + esc(f.key) + '</code></label>'
             + input
             + (f.help ? '<small class="text-muted" style="font-size:10px;">' + esc(f.help) + '</small>' : '')
             + '</div>';
    }
    function fillCfg(containerSel, values) {
        $(containerSel + ' [data-hestia-key]').each(function(){
            var k = this.getAttribute('data-hestia-key');
            this.value = (values && values[k] != null) ? String(values[k]) : '';
        });
    }

    // ============ Standard files tab ============
    var groupFilesAll = [];
    function loadGroupFiles(cb) {
        if (isNew) { groupFilesAll = []; if (cb) cb(); return; }
        puqAjax({
            container: '#tab-stdfiles',
            loadingCaption: 'Loading standard files…',
            url: baseUrl + 'listGroupFiles&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                groupFilesAll = (r.status === 'success') ? (r.data || []) : [];
                renderStandardFilesStatus();
                if (cb) cb();
            }
        });
    }
    function renderStandardFilesStatus() {
        $('#tab-stdfiles .puq-stdfile-row').each(function(){
            var $row = $(this);
            var cat = $row.data('cat'), rel = $row.data('rel');
            var existing = groupFilesAll.find(function(f){ return f.category === cat && f.rel_path === rel; });
            var $st = $row.find('.puq-stdfile-status');
            if (existing) {
                if (parseInt(existing.enabled,10)) $st.removeClass('default disabled').addClass('overridden').text('overridden');
                else $st.removeClass('default overridden').addClass('disabled').text('disabled');
                $row.attr('data-id', existing.id);
            } else {
                $st.removeClass('overridden disabled').addClass('default').text('default');
                $row.removeAttr('data-id');
            }
        });
    }

    function initStdFilesTab() {
        if (isNew) return;
        // Need both servers list (for Pull picker) and current files (for status).
        fetchGroupServers(function(){ loadGroupFiles(); });

        $('#tab-stdfiles').on('click', '.puq-std-override', function(){
            var $row = $(this).closest('.puq-stdfile-row');
            var cat = $row.data('cat'), rel = $row.data('rel'), mode = String($row.data('mode')) || '0644';
            var id = $row.attr('data-id');
            var btn = this;
            if (id) {
                btnSpinner(btn);
                $.ajax({
                    url: baseUrl + 'getGroupFile&id=' + id, dataType: 'json',
                    success: function(r){
                        if (r.status === 'success') openFileModalEdit(cat, r.data);
                        else showNotification(r.msg || 'Load failed', 'error');
                    },
                    error: function(){ showNotification('Request failed.', 'error'); },
                    complete: function(){ btnRestore(btn, 'pencil-alt', 'Override'); }
                });
            } else {
                // No async work, but flash the spinner briefly so the click feels acknowledged.
                btnSpinner(btn);
                openFileModalNew(cat, rel, mode, '');
                setTimeout(function(){ btnRestore(btn, 'pencil-alt', 'Override'); }, 250);
            }
        });

        $('#tab-stdfiles').on('click', '.puq-std-pull', function(){
            var $row = $(this).closest('.puq-stdfile-row');
            var btn = this;
            btnSpinner(btn);
            openPullModal($row.data('cat'), $row.data('rel'), String($row.data('mode')) || '0644');
            setTimeout(function(){ btnRestore(btn, 'download', 'Pull'); }, 250);
        });
    }

    // ============ Variables tab ============
    // Group-wide {key} placeholders substituted into zone template records.
    // Default fixed slots: ns1, ns2. Anything else is admin-added.
    function initVariablesTab() {
        if (isNew) return;
        loadVariables();
        // Event delegation — the buttons exist after AJAX render.
        $('#tab-variables').off('click.vars').on('click.vars', '#puq-vars-save', saveVariables);
        $('#tab-variables').on('click.vars', '#puq-vars-add', function(){
            $('#puq-vars-grid').append(varFieldHtml('', '', false));
        });
        $('#tab-variables').on('click.vars', '.puq-var-rm', function(){
            $(this).closest('.col-md-6').remove();
        });
    }

    function loadVariables() {
        puqAjax({
            container: '#puq-vars-grid',
            loadingCaption: 'Loading variables…',
            url: baseUrl + 'getGroupVariables&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                var $grid = $('#puq-vars-grid').empty();
                if (r.status !== 'success' || !r.data || !r.data.vars) {
                    $grid.html('<div class="col-md-12 text-danger">Load failed.</div>');
                    return;
                }
                var vars = r.data.vars;
                // Render default slots first (ns1, ns2 — fixed, key read-only).
                ['ns1', 'ns2'].forEach(function(k){
                    $grid.append(varFieldHtml(k, vars[k] || '', true));
                });
                // Any other key admin added via "Add variable".
                Object.keys(vars).forEach(function(k){
                    if (k === 'ns1' || k === 'ns2') return;
                    $grid.append(varFieldHtml(k, vars[k] || '', false));
                });
            },
            error: function() { $('#puq-vars-grid').html('<div class="col-md-12 text-danger">Request failed.</div>'); }
        });
    }

    function varFieldHtml(key, value, fixedKey) {
        var keyInput = fixedKey
            ? '<input type="text" class="form-control input-sm puq-var-key" value="' + esc(key) + '" readonly style="background:#f5f5f5;">'
            : '<input type="text" class="form-control input-sm puq-var-key" value="' + esc(key) + '" placeholder="variable_name">';
        var rm = fixedKey ? '' : ' <button type="button" class="btn btn-xs btn-link text-danger puq-var-rm" style="padding:0 4px;"><i class="fa fa-times"></i></button>';
        return '<div class="col-md-6" style="margin-bottom:6px;">' +
                 '<div class="input-group input-group-sm">' +
                   '<span class="input-group-addon" style="min-width:80px;padding:0;">' + keyInput + '</span>' +
                   '<input type="text" class="form-control input-sm puq-var-val" value="' + esc(value) + '" placeholder="value">' +
                 '</div>' + rm +
               '</div>';
    }

    function saveVariables() {
        var btn = document.getElementById('puq-vars-save');
        if (!btn) return;
        btnSpinner(btn, 'Saving...');
        var map = {};
        $('#puq-vars-grid .col-md-6').each(function(){
            var $row = $(this);
            var k = ($row.find('.puq-var-key').val() || '').trim();
            var v = ($row.find('.puq-var-val').val() || '').trim();
            if (k && v) map[k] = v;
        });
        $.ajax({
            url: baseUrl + 'saveGroupVariables', type: 'POST',
            data: { group_id: groupId, vars: JSON.stringify(map) },
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success') showNotification(r.msg || 'Saved.', 'success');
                else showNotification(r.msg || 'Save failed', 'error');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'save', 'Save variables'); }
        });
    }

    // ============ DNS servers tab ============
    function initDnsTab() {
        if (isNew) return;
        loadDnsTemplate();
        loadDnsServers();
        loadSoa();
        refreshUnifiedReadiness();
        // Default DNS zone template save button.
        $('#tab-dns').off('click.dnstmpl').on('click.dnstmpl', '#puq-dns-tmpl-save', saveDnsTemplate);
        $('#tab-dns').off('click.soa').on('click.soa', '#puq-soa-save', saveSoa);
    }

    function loadSoa() {
        $.ajax({
            url: baseUrl + 'getGroupSoa&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success' || !r.data) return;
                $('#puq-soa-email').val(r.data.soa_email || '');
                $('#puq-soa-ttl').val(r.data.soa_ttl || '');
                $('#puq-soa-refresh').val(r.data.soa_refresh || '');
                $('#puq-soa-retry').val(r.data.soa_retry || '');
                $('#puq-soa-expire').val(r.data.soa_expire || '');
                $('#puq-soa-minimum').val(r.data.soa_minimum || '');
            }
        });
    }

    function saveSoa() {
        var btn = document.getElementById('puq-soa-save');
        if (!btn) return;
        btnSpinner(btn, 'Saving...');
        $.ajax({
            url: baseUrl + 'saveGroupSoa', type: 'POST',
            data: {
                group_id: groupId,
                soa_email:   $('#puq-soa-email').val(),
                soa_ttl:     $('#puq-soa-ttl').val(),
                soa_refresh: $('#puq-soa-refresh').val(),
                soa_retry:   $('#puq-soa-retry').val(),
                soa_expire:  $('#puq-soa-expire').val(),
                soa_minimum: $('#puq-soa-minimum').val()
            },
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success') showNotification(r.msg || 'Saved.', 'success');
                else showNotification(r.msg || 'Save failed', 'error');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'save', 'Save SOA defaults'); }
        });
    }

    // ---- Default DNS zone template dropdown ---------------------------------
    function loadDnsTemplate() {
        // Reuse the variables endpoint — it returns template_id + list too.
        puqAjax({
            container: '#tab-dns',
            loadingCaption: 'Loading DNS template…',
            url: baseUrl + 'getGroupVariables&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success' || !r.data) return;
                var $sel = $('#puq-dns-tmpl-select');
                if (!$sel.length) return;
                $sel.find('option:not([value=""])').remove();
                (r.data.available_templates || []).forEach(function(t){
                    var lbl = t.name + (parseInt(t.is_default, 10) === 1 ? ' (global default)' : '');
                    $sel.append('<option value="' + t.id + '">' + esc(lbl) + '</option>');
                });
                $sel.val(r.data.dns_template_id || '');
            }
        });
    }

    function saveDnsTemplate() {
        var btn = document.getElementById('puq-dns-tmpl-save');
        if (!btn) return;
        btnSpinner(btn, 'Saving...');
        $.ajax({
            url: baseUrl + 'saveGroupDnsTemplate', type: 'POST',
            data: { group_id: groupId, dns_template_id: $('#puq-dns-tmpl-select').val() || '' },
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success') showNotification(r.msg || 'Saved.', 'success');
                else showNotification(r.msg || 'Save failed', 'error');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'save', 'Save default template'); }
        });
    }

    // ---- DNS servers list (junction) -----------------------------------------
    function loadDnsServers() {
        var $tbody = $('#puq-dns-table tbody');
        puqAjax({
            container: '#puq-dns-table',
            loadingCaption: 'Loading DNS servers…',
            url: baseUrl + 'listGroupDnsServers&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success' || !r.data) {
                    $tbody.html('<tr><td colspan="7" class="text-danger text-center">' + esc(r.msg || 'Load failed') + '</td></tr>');
                    return;
                }
                var all = r.data.all || [];
                var attached = {};
                (r.data.attached_ids || []).forEach(function(id){ attached[parseInt(id,10)] = true; });

                if (!all.length) {
                    $tbody.html('<tr><td colspan="7" class="text-muted text-center">No DNS servers configured yet — add one on the <a href="' + groupUrl + '&page=dns_servers" target="_blank">DNS Servers</a> page first.</td></tr>');
                    return;
                }

                $tbody.empty();
                all.forEach(function(s){
                    var sid = parseInt(s.id, 10);
                    var isChecked = !!attached[sid];
                    var stateCls = ({active:'success', inactive:'default', maintenance:'warning'})[s.status] || 'default';
                    var drvCls = (s.driver === 'powerdns') ? 'warning' : 'info';
                    var cap = (s.capacity_used != null && s.capacity_max != null && s.capacity_max > 0)
                        ? esc(s.capacity_used) + ' / ' + esc(s.capacity_max)
                        : (s.capacity_used != null ? esc(s.capacity_used) : '—');
                    // Cap badges — admins picking a unified deployment need
                    // to find servers tagged Web+Mail in addition to DNS.
                    var capsHtml = '';
                    [['web', 'caps_web', 'primary'],
                     ['mail', 'caps_mail', 'success'],
                     ['dns', 'caps_dns', 'info']].forEach(function (pair) {
                        var on = parseInt(s[pair[1]] || 0, 10) === 1;
                        capsHtml += '<span class="label label-' + (on ? pair[2] : 'default')
                            + '" style="margin-right:2px;opacity:' + (on ? 1 : 0.35) + ';">'
                            + pair[0] + '</span>';
                    });
                    $tbody.append(
                        '<tr>' +
                          '<td><input type="checkbox" class="puq-dns-cb" value="' + sid + '"' + (isChecked ? ' checked' : '') + '></td>' +
                          '<td><strong>' + esc(s.name) + '</strong></td>' +
                          '<td><span class="label label-' + drvCls + '">' + esc(s.driver) + '</span></td>' +
                          '<td>' + capsHtml + '</td>' +
                          '<td><code>' + esc(s.hostname) + (s.port && parseInt(s.port,10) !== 22 ? ':' + esc(s.port) : '') + '</code></td>' +
                          '<td><span class="label label-' + stateCls + '">' + esc(s.status) + '</span></td>' +
                          '<td><small>' + cap + '</small></td>' +
                        '</tr>'
                    );
                });
                updateDnsStats();
            },
            error: function() {
                $tbody.html('<tr><td colspan="6" class="text-danger text-center">Request failed.</td></tr>');
            }
        });

        $('#tab-dns').off('change.dnstab').on('change.dnstab', '.puq-dns-cb', updateDnsStats);
        $('#tab-dns').off('click.dnssave').on('click.dnssave', '#puq-dns-save', saveDnsAttachments);
        // Note: Variables block moved to its own tab (#tab-variables). DNS-tab
        // template save button is wired in initDnsTab().
    }

    function updateDnsStats() {
        var total = $('#puq-dns-table .puq-dns-cb').length;
        var sel = $('#puq-dns-table .puq-dns-cb:checked').length;
        $('#puq-dns-stats').text(' — ' + sel + ' of ' + total + ' selected');
    }

    // Aggregate cap counts across this group's WEB / MAIL servers (not DNS-
    // only ones) so admin can see at a glance whether unified-deployment
    // products will be able to provision into this group.
    function refreshUnifiedReadiness() {
        $.ajax({
            url: baseUrl + 'listServers&group_id=' + groupId, dataType: 'json',
            success: function (r) {
                if (!r || r.status !== 'success' || !Array.isArray(r.data)) return;
                var multi = 0;
                r.data.forEach(function (s) {
                    if (parseInt(s.caps_web || 0, 10) === 1 && parseInt(s.caps_mail || 0, 10) === 1) multi++;
                });
                var $b = $('#puq-unified-banner');
                if (!$b.length) return;
                if (multi > 0) {
                    $b.removeClass('alert-warning').addClass('alert-success').html(
                        '<i class="fa fa-check-circle"></i> Unified-deployment products can use this group — '
                        + '<strong>' + multi + '</strong> server(s) carry both Web AND Mail capabilities.'
                    );
                } else {
                    $b.removeClass('alert-success').addClass('alert-warning').html(
                        '<i class="fa fa-exclamation-triangle"></i> No server in this group has Web AND Mail capabilities ticked — '
                        + 'unified-deployment products will fail to provision here. Edit a server under '
                        + '<a href="' + groupUrl + '&page=web_servers" target="_blank">Web servers</a> '
                        + 'and tick both caps, or stick with Split-deployment products.'
                    );
                }
            }
        });
    }

    function saveDnsAttachments() {
        var btn = document.getElementById('puq-dns-save');
        if (!btn) return;
        btnSpinner(btn, 'Saving...');
        var ids = $('#puq-dns-table .puq-dns-cb:checked').map(function(){ return parseInt(this.value, 10); }).get();
        // Send as repeated fields ("dns_server_ids[]") AS WELL as JSON so the
        // backend can fall back to either. WHMCS admin AJAX has bitten us with
        // both formats before; this redundancy avoids debate.
        var formData = { group_id: groupId, dns_server_ids: JSON.stringify(ids) };
        ids.forEach(function(id, i){ formData['dns_server_ids_arr[' + i + ']'] = id; });
        $.ajax({
            url: baseUrl + 'saveGroupDnsServers', type: 'POST',
            data: formData,
            dataType: 'json',
            success: function(r) {
                if (r.status === 'success') {
                    showNotification(r.msg || 'Saved.', 'success');
                    // Re-fetch so the table reflects the persisted state. If
                    // something silently dropped, the checkboxes will visibly
                    // flip back.
                    loadDnsServers();
                } else {
                    showNotification(r.msg || 'Save failed', 'error');
                }
            },
            error: function(xhr) {
                var msg = 'Request failed.';
                try { var j = JSON.parse(xhr.responseText); if (j && j.msg) msg = j.msg; } catch(e) {}
                showNotification(msg, 'error');
            },
            complete: function() { btnRestore(btn, 'save', 'Save DNS selection'); }
        });
    }

    // ============ Vanity domains tab ============
    var vanityMailServers = [];
    var vanityWebServers = [];

    function initVanityTab() {
        loadVanityDomains();
        $('#tab-vanity').off('click.vanity')
            .on('click.vanity', '#puq-vanity-add', function(){ openVanityModal(null); })
            .on('click.vanity', '.puq-vanity-edit', function(){
                var row = $(this).data('row');
                openVanityModal(row);
            })
            .on('click.vanity', '.puq-vanity-del', function(){
                var id = parseInt(this.getAttribute('data-id'), 10);
                var dom = this.getAttribute('data-domain') || '';
                deleteVanityDomainRow(id, dom);
            })
            .on('click.vanity', '#puq-vanity-save', saveVanityDomainRow)
            .on('change.vanity', '#puq-vanity-dns-mode', toggleVanityDnsPush);
    }

    function srvOptionsHtml(list, selectedId) {
        var html = '<option value="">— select —</option>';
        (list || []).forEach(function(s){
            var sel = (parseInt(s.id,10) === parseInt(selectedId||0,10)) ? ' selected' : '';
            html += '<option value="' + parseInt(s.id,10) + '"' + sel + '>'
                + esc(s.name) + ' (' + esc(s.hostname) + ')</option>';
        });
        return html;
    }

    function toggleVanityDnsPush() {
        var mode = $('#puq-vanity-dns-mode').val();
        $('#puq-vanity-dns-push').toggle(mode === 'push');
    }

    function loadVanityDomains() {
        var $tbody = $('#puq-vanity-table tbody');
        puqAjax({
            container: '#puq-vanity-table',
            loadingCaption: 'Loading…',
            url: baseUrl + 'listVanityDomains&group_id=' + groupId, dataType: 'json',
            success: function(r) {
                if (r.status !== 'success' || !r.data) {
                    $tbody.html('<tr><td colspan="7" class="text-danger text-center">' + esc(r.msg || 'Load failed') + '</td></tr>');
                    return;
                }
                vanityMailServers = r.data.mail_servers || [];
                vanityWebServers = r.data.web_servers || [];
                var rows = r.data.rows || [];
                $('#puq-vanity-stats').text(' — ' + rows.length + ' domain(s)');
                if (!rows.length) {
                    $tbody.html('<tr><td colspan="7" class="text-muted text-center">No vanity domains yet. Click <strong>Add domain</strong> to register one you own.</td></tr>');
                    return;
                }
                $tbody.empty();
                rows.forEach(function(v){
                    var statusCls = (v.status === 'active') ? 'success' : 'default';
                    var dnsCell = (v.dns_mode === 'push')
                        ? '<span class="label label-info">push</span> <code>' + esc(v.dns_zone || v.domain) + '</code>'
                        : '<span class="label label-default">external</span>';
                    var mailCell = '<code>' + esc(v.mail_username || '?') + '</code> @ <code>' + esc(v.mail_domain || v.domain) + '</code>';
                    var inUse = parseInt(v.in_use || 0, 10);
                    var delBtn = inUse > 0
                        ? '<button type="button" class="btn btn-xs btn-default" disabled title="In use by ' + inUse + ' service(s)"><i class="fa fa-lock"></i></button>'
                        : '<button type="button" class="btn btn-xs btn-danger puq-vanity-del" data-id="' + parseInt(v.id,10) + '" data-domain="' + esc(v.domain) + '"><i class="fa fa-trash"></i></button>';
                    var $tr = $(
                        '<tr>' +
                          '<td><strong>' + esc(v.domain) + '</strong></td>' +
                          '<td>' + mailCell + '</td>' +
                          '<td>' + dnsCell + '</td>' +
                          '<td>' + parseInt(v.record_ttl || 3600, 10) + '</td>' +
                          '<td><span class="label label-' + statusCls + '">' + esc(v.status) + '</span></td>' +
                          '<td>' + (inUse > 0 ? '<span class="badge">' + inUse + '</span>' : '<span class="text-muted">0</span>') + '</td>' +
                          '<td>' +
                            '<button type="button" class="btn btn-xs btn-default puq-vanity-edit"><i class="fa fa-edit"></i></button> ' +
                            delBtn +
                          '</td>' +
                        '</tr>'
                    );
                    // Stash the full row on the edit button for the modal.
                    $tr.find('.puq-vanity-edit').data('row', v);
                    $tbody.append($tr);
                });
            },
            error: function() {
                $tbody.html('<tr><td colspan="7" class="text-danger text-center">Request failed.</td></tr>');
            }
        });
    }

    function openVanityModal(row) {
        row = row || {};
        $('#puq-vanity-id').val(parseInt(row.id || 0, 10));
        $('#puq-vanity-modal-title').text(row.id ? ('Edit ' + (row.domain || 'vanity domain')) : 'Add vanity domain');
        $('#puq-vanity-domain').val(row.domain || '');
        $('#puq-vanity-mail-server').html(srvOptionsHtml(vanityMailServers, row.mail_server_id, 'mail'));
        $('#puq-vanity-mail-username').val(row.mail_username || '');
        $('#puq-vanity-mail-domain').val(row.mail_domain || '');
        $('#puq-vanity-dns-mode').val(row.dns_mode || 'external');
        $('#puq-vanity-dns-username').val(row.dns_username || '');
        $('#puq-vanity-dns-zone').val(row.dns_zone || '');
        $('#puq-vanity-ttl').val(parseInt(row.record_ttl || 3600, 10));
        $('#puq-vanity-status').val(row.status || 'active');
        $('#puq-vanity-notes').val(row.notes || '');
        renderVanityWebServers(row.web_server_ids || []);
        toggleVanityDnsPush();
        $('#puq-vanity-modal').modal('show');
    }

    function renderVanityWebServers(selectedIds) {
        var sel = {};
        (selectedIds || []).forEach(function(id){ sel[parseInt(id, 10)] = true; });
        var $box = $('#puq-vanity-web-list');
        if (!vanityWebServers.length) {
            $box.html('<div class="text-muted small">No web servers configured. Add one on the <a href="' + groupUrl + '&page=web_servers" target="_blank">Web Servers</a> page first.</div>');
            return;
        }
        var html = '';
        vanityWebServers.forEach(function(s){
            var sid = parseInt(s.id, 10);
            var cap = (s.capacity_used != null && s.capacity_max != null && s.capacity_max > 0)
                ? ' <span class="text-muted small">(' + esc(s.capacity_used) + '/' + esc(s.capacity_max) + ')</span>'
                : (s.capacity_used != null ? ' <span class="text-muted small">(' + esc(s.capacity_used) + ')</span>' : '');
            var off = (s.status && s.status !== 'active') ? ' <span class="label label-default">' + esc(s.status) + '</span>' : '';
            html += '<label style="display:block;font-weight:normal;margin:2px 0;">' +
                '<input type="checkbox" class="puq-vanity-web-cb" value="' + sid + '"' + (sel[sid] ? ' checked' : '') + '> ' +
                esc(s.name) + ' <code>' + esc(s.hostname) + '</code>' + cap + off +
            '</label>';
        });
        $box.html(html);
    }

    function saveVanityDomainRow() {
        var btn = document.getElementById('puq-vanity-save');
        // Light client-side guard — server re-validates everything.
        var domain = ($('#puq-vanity-domain').val() || '').trim().toLowerCase();
        if (!/^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/.test(domain)) {
            showNotification('Enter a valid parent domain (e.g. example.com).', 'error');
            return;
        }
        if (!parseInt($('#puq-vanity-mail-server').val() || 0, 10) || !($('#puq-vanity-mail-username').val() || '').trim()) {
            showNotification('Mail server and provider mail username are required.', 'error');
            return;
        }
        var mode = $('#puq-vanity-dns-mode').val();
        if (mode === 'push' && !($('#puq-vanity-dns-username').val() || '').trim()) {
            showNotification('Push DNS mode needs the provider DNS username (set DNS servers on the DNS servers tab).', 'error');
            return;
        }
        btnSpinner(btn, 'Saving...');
        var webIds = $('#puq-vanity-web-list .puq-vanity-web-cb:checked').map(function(){ return parseInt(this.value, 10); }).get();
        var payload = {
            id: parseInt($('#puq-vanity-id').val() || 0, 10),
            group_id: groupId,
            domain: domain,
            mail_server_id: parseInt($('#puq-vanity-mail-server').val() || 0, 10),
            mail_username: ($('#puq-vanity-mail-username').val() || '').trim(),
            mail_domain: ($('#puq-vanity-mail-domain').val() || '').trim(),
            dns_mode: mode,
            dns_username: ($('#puq-vanity-dns-username').val() || '').trim(),
            dns_zone: ($('#puq-vanity-dns-zone').val() || '').trim(),
            record_ttl: parseInt($('#puq-vanity-ttl').val() || 3600, 10),
            status: $('#puq-vanity-status').val(),
            notes: ($('#puq-vanity-notes').val() || '').trim(),
            // Send as JSON + repeated form-array (same dual-format the backend's
            // decodeIdList accepts) so WHMCS admin AJAX escaping can't drop it.
            web_server_ids: JSON.stringify(webIds)
        };
        webIds.forEach(function(id, i){ payload['web_server_ids_arr[' + i + ']'] = id; });
        $.ajax({
            url: baseUrl + 'saveVanityDomain', type: 'POST', data: payload, dataType: 'json',
            success: function(r) {
                if (r.status === 'success') {
                    showNotification(r.msg || 'Saved.', 'success');
                    $('#puq-vanity-modal').modal('hide');
                    loadVanityDomains();
                } else {
                    showNotification(r.msg || 'Save failed', 'error');
                }
            },
            error: function(xhr) {
                var msg = 'Request failed.';
                try { var j = JSON.parse(xhr.responseText); if (j && j.msg) msg = j.msg; } catch(e) {}
                showNotification(msg, 'error');
            },
            complete: function() { btnRestore(btn, 'save', 'Save'); }
        });
    }

    function deleteVanityDomainRow(id, domain) {
        if (!id) return;
        puqConfirm({
            type: 'warning', icon: 'fa-trash',
            title: 'Delete vanity domain',
            text: 'Delete vanity domain <strong>' + esc(domain) + '</strong>? Existing services keep working; you just stop selling new slots on it.',
            ok: 'Delete', cancel: 'Cancel',
            onConfirm: function(){
                return new Promise(function(resolve, reject){
                    $.ajax({
                        url: baseUrl + 'deleteVanityDomain', type: 'POST', data: { id: id, group_id: groupId }, dataType: 'json',
                        success: function(r){
                            if (r.status === 'success') { showNotification(r.msg || 'Deleted.', 'success'); loadVanityDomains(); resolve(); }
                            else { reject(new Error(r.msg || 'Delete failed')); }
                        },
                        error: function(xhr){
                            var msg = 'Request failed.';
                            try { var j = JSON.parse(xhr.responseText); if (j && j.msg) msg = j.msg; } catch(e) {}
                            reject(new Error(msg));
                        }
                    });
                });
            }
        });
    }

    // ============ File modal (Override editor) ============
    function resetFileModalView() {
        // Reset preview tabs back to Source whenever modal opens.
        $('#puq-file-mode-tabs li').removeClass('active');
        $('#puq-file-mode-tabs li').first().addClass('active');
        $('#puq-file-content').show();
        $('#puq-file-iframe').hide();
        document.getElementById('puq-file-iframe').srcdoc = '';
    }
    function openFileModalNew(cat, rel, mode, prefillContent) {
        $('#puq-file-modal-title').text('Override: ' + rel);
        $('#puq-file-id').val('');
        $('#puq-file-category').val(cat);
        $('#puq-file-rel-path').val(rel);
        $('#puq-file-description').val('');
        $('#puq-file-mode').val(mode || '0644');
        $('#puq-file-owner').val('root:root');
        $('#puq-file-enabled').prop('checked', true);
        $('#puq-file-content').val(prefillContent || '');
        $('#puq-file-delete').hide();
        resetFileModalView();
        $('#puq-file-modal').modal('show');
    }
    function openFileModalEdit(cat, fileRow) {
        $('#puq-file-modal-title').text('Edit override: ' + fileRow.rel_path);
        $('#puq-file-id').val(fileRow.id);
        $('#puq-file-category').val(cat);
        $('#puq-file-rel-path').val(fileRow.rel_path);
        $('#puq-file-description').val(fileRow.description || '');
        $('#puq-file-mode').val(fileRow.mode_octal);
        $('#puq-file-owner').val(fileRow.owner);
        $('#puq-file-enabled').prop('checked', !!parseInt(fileRow.enabled,10));
        $('#puq-file-content').val(fileRow.content || '');
        $('#puq-file-delete').show();
        resetFileModalView();
        $('#puq-file-modal').modal('show');
    }

    // Source / Rendered preview toggle inside the Override editor.
    $(document).on('click', '#puq-file-mode-tabs a', function(e){
        e.preventDefault();
        var mode = $(this).data('mode');
        $('#puq-file-mode-tabs li').removeClass('active');
        $(this).parent().addClass('active');
        if (mode === 'rendered') {
            // Pull the latest textarea content into the iframe so live edits preview.
            document.getElementById('puq-file-iframe').srcdoc = $('#puq-file-content').val() || '';
            $('#puq-file-content').hide();
            $('#puq-file-iframe').show();
        } else {
            $('#puq-file-iframe').hide();
            $('#puq-file-content').show();
        }
    });

    $(document).on('click', '#puq-file-save', function(){
        var btn = this; btnSpinner(btn);
        var data = {
            id: $('#puq-file-id').val(),
            group_id: groupId,
            category: $('#puq-file-category').val(),
            rel_path: $('#puq-file-rel-path').val(),
            description: $('#puq-file-description').val(),
            mode_octal: $('#puq-file-mode').val(),
            owner: $('#puq-file-owner').val(),
            enabled: $('#puq-file-enabled').is(':checked') ? '1' : '0',
            content: $('#puq-file-content').val()
        };
        $.ajax({
            url: baseUrl + 'saveGroupFile', type: 'POST', data: data, dataType: 'json',
            success: function(r) {
                if (r.status === 'success') {
                    showNotification(r.msg || 'Saved', 'success');
                    $('#puq-file-modal').modal('hide');
                    loadGroupFiles();
                } else { showNotification(r.msg || 'Save failed', 'error'); }
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'save', 'Save file'); }
        });
    });
    $(document).on('click', '#puq-file-delete', function(){
        var id = $('#puq-file-id').val();
        if (!id) return;
        puqConfirm({
            type: 'warning', icon: 'fa-trash',
            title: 'Stop managing this file?',
            text: 'The file will no longer be tracked by this group. It will <strong>not</strong> be removed from servers.',
            ok: 'Stop managing', cancel: 'Cancel',
            onConfirm: function(){
                return new Promise(function(resolve, reject){
                    $.ajax({
                        url: baseUrl + 'deleteGroupFile', type: 'POST', data: { id: id }, dataType: 'json',
                        success: function(r){
                            if (r.status === 'success') {
                                showNotification('Removed from group.', 'success');
                                $('#puq-file-modal').modal('hide');
                                loadGroupFiles();
                                resolve();
                            } else { reject(new Error(r.msg || 'Delete failed')); }
                        },
                        error: function(){ reject(new Error('Request failed')); }
                    });
                });
            }
        });
    });

    // ============ Pull modal — server picker + HTML preview ============
    var pullState = { cat: '', rel: '', mode: '0644', content: '' };

    function openPullModal(cat, rel, mode) {
        pullState = { cat: cat, rel: rel, mode: mode || '0644', content: '' };
        $('#puq-pull-title').text(rel);
        $('#puq-pull-path').text('/usr/local/hestia/data/templates/' + cat + '/' + rel);
        $('#puq-pull-preview').hide();
        $('#puq-pull-import').hide();
        // CRITICAL: use the DOM IDL property, NOT jQuery .attr('srcdoc', ...).
        // jQuery's setAttribute path HTML-encodes the value (< → &lt;), so the
        // iframe ends up showing the entity-escaped source as plain text
        // instead of rendering it. The .srcdoc property is plain UTF-16 — no
        // escaping. Same trap caused content to leak into the editor textarea
        // as entities when the user clicked Import.
        document.getElementById('puq-pull-iframe').srcdoc = '';
        $('#puq-pull-source').val('');
        $('#puq-pull-hint').text('');
        // Reset to rendered tab
        $('#puq-pull-mode-tabs li').removeClass('active').first().addClass('active');
        $('#puq-pull-iframe').show();
        $('#puq-pull-source').hide();

        // Populate server picker.
        var $sel = $('#puq-pull-server').empty().append('<option value="">— select —</option>');
        if (!groupServers.length) {
            $('#puq-pull-hint').text('No servers in this group — assign one first.');
        } else {
            groupServers.forEach(function(s){
                var label = s.name + ' (' + s.hostname + ')';
                if (s.status_state && s.status_state !== 'ok' && s.status_state !== 'never') {
                    label += ' [' + s.status_state + ']';
                }
                $sel.append('<option value="' + s.id + '">' + esc(label) + '</option>');
            });
        }
        $('#puq-pull-modal').modal('show');
    }

    $(document).on('click', '#puq-pull-load', function(){
        var sid = parseInt($('#puq-pull-server').val(), 10);
        if (!sid) { showNotification('Choose a server first.', 'error'); return; }
        var btn = this; btnSpinner(btn);
        $.ajax({
            url: baseUrl + 'fetchServerTemplate&server_id=' + sid
                + '&category=' + encodeURIComponent(pullState.cat)
                + '&rel_path=' + encodeURIComponent(pullState.rel),
            dataType: 'json',
            success: function(r) {
                if (r.status !== 'success') {
                    showNotification(r.msg || 'Pull failed', 'error');
                    return;
                }
                pullState.content = r.data.content || '';
                // Render in iframe via srcdoc — set as DOM property, not jQuery
                // attribute, so the value isn't HTML-encoded on its way in.
                document.getElementById('puq-pull-iframe').srcdoc = pullState.content;
                $('#puq-pull-source').val(pullState.content);
                $('#puq-pull-preview').show();
                $('#puq-pull-import').show();
                $('#puq-pull-hint').text('Loaded ' + pullState.content.length + ' bytes.');
            },
            error: function() { showNotification('Request failed.', 'error'); },
            complete: function() { btnRestore(btn, 'eye', 'Load preview'); }
        });
    });

    $(document).on('click', '#puq-pull-mode-tabs a', function(e){
        e.preventDefault();
        var mode = $(this).data('mode');
        $('#puq-pull-mode-tabs li').removeClass('active');
        $(this).parent().addClass('active');
        if (mode === 'source') {
            $('#puq-pull-iframe').hide();
            $('#puq-pull-source').show();
        } else {
            $('#puq-pull-source').hide();
            $('#puq-pull-iframe').show();
        }
    });

    $(document).on('click', '#puq-pull-import', function(){
        $('#puq-pull-modal').modal('hide');
        openFileModalNew(pullState.cat, pullState.rel, pullState.mode, pullState.content);
        $('#puq-file-description').val('Imported from ' + ($('#puq-pull-server option:selected').text() || 'server'));
    });

    // ============ Document ready — wire tabs + initial load ============
    $(document).ready(function(){
        $('#puq-grp-tabs a').on('shown.bs.tab', function(){
            var tab = $(this).data('tab');
            if (tab) loadTab(tab);
        });

        // Hide tabs not relevant to this group's purpose, then load a visible
        // initial tab. Live-toggle when the General-tab radio changes.
        applyGroupTabsByType(groupType);
        $(document).on('change', 'input[name="group_type"]', function(){
            applyGroupTabsByType(this.value);
            // Reserved-names field is vanity-only.
            $('#puq-grp-reserved-wrap').toggle(this.value === 'vanity');
        });
        var startTab = initialTab || 'actions';
        var startLi = document.querySelector('#puq-grp-tabs li[data-types] a[data-tab="' + startTab + '"]');
        if (startLi && startLi.parentNode.style.display === 'none') startTab = 'general';
        loadTab(startTab);

        $('#puq-save-all').on('click', function(){
            // All config tabs + general must be loaded so their inputs exist
            // when we collect form values. Sequential to keep things simple.
            var toLoad = ['general', 'hestia', 'web', 'mail', 'dns_config'].filter(function(t){ return !tabsLoaded[t]; });
            function step() {
                if (!toLoad.length) { saveGroup(); return; }
                loadTab(toLoad.shift(), step);
            }
            step();
        });
    });
})();
</script>
{/literal}
