{{/* host_detail_body — the shared per-host detail sections, rendered by BOTH the
     standalone /hosts/{id} page (host_detail.html) and the customer page's Host tab
     (customer_unified.html, one instance per host). Data = the map built by
     hostDetailData (web/hosts.go); never carries api_key/escrow/PBS secret values. */}}
{{define "host_detail_body"}}
        <!-- Identity + Status -->
        <section class="card">
            <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
                <h2 style="margin: 0;">{{.HostID}}</h2>
                <span class="status-badge {{.StatusClass}}">{{.StatusLabel}}</span>
            </div>
            <div class="info-grid" style="margin-top: 1rem;">
                <div class="info-item">
                    <span class="label">Host ID</span>
                    <span class="value" style="font-family: var(--font-mono)">{{.HostID}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Customer</span>
                    <span class="value"><a href="/customers/{{.CustomerID}}">{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}}</a></span>
                </div>
                <div class="info-item">
                    <span class="label">Agent Version</span>
                    <span class="value">{{if .AgentVersion}}<code>{{.AgentVersion}}</code>{{else}}—{{end}}</span>
                </div>
                {{if .WrapperDrift}}
                <div class="info-item">
                    <span class="label">PBS wrapper</span>
                    {{if eq .WrapperDrift "ok"}}
                    <span class="value">matches vouched <code>{{slice .ReportedWrapperSHA 0 12}}…</code></span>
                    {{else}}
                    <span class="value" style="color: var(--yellow)">DRIFT — installed <code>{{slice .ReportedWrapperSHA 0 12}}…</code>, vouched <code>{{slice .VouchedWrapperSHA 0 12}}…</code><br>
                        <span style="font-size:.85em">/usr/local/sbin/felhom-pbs-apply differs from the manifest. It is delivered unversioned from <code>main</code> (R-50b), so this host may be running privileged code from a different commit.</span></span>
                    {{end}}
                </div>
                {{end}}
                <div class="info-item">
                    <span class="label">Enrolled</span>
                    <span class="value">{{timeAgo .CreatedAt}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Last Report</span>
                    <span class="value">{{if .HasReport}}{{timeAgoPtr .LastReportAt}}{{else}}waiting for first report{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Desired Generation</span>
                    <span class="value">{{.DesiredGeneration}}</span>
                </div>
                {{if .RecoveryMode}}
                <div class="info-item">
                    <span class="label">Recovery Mode</span>
                    <span class="value" style="color: var(--yellow)">ACTIVE (until {{timeAgoPtr .RecoveryUntil}})</span>
                </div>
                {{end}}
            </div>
        </section>

        {{if .HasReport}}
        <!-- Vitals -->
        <section class="card">
            <h2>Vitals</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">CPU</span>
                    <span class="value">{{formatFloat .Vitals.CPUPercent}}%</span>
                </div>
                <div class="info-item">
                    <span class="label">Memory</span>
                    <span class="value">{{formatFloat .Vitals.MemoryPercent}}%</span>
                </div>
                <div class="info-item">
                    <span class="label">Disk (root fs)</span>
                    <span class="value">{{formatFloat .Vitals.DiskPercent}}%</span>
                </div>
                <div class="info-item">
                    <span class="label">Cloudflared</span>
                    <span class="value">{{if .Vitals.CloudflaredStatus}}{{.Vitals.CloudflaredStatus}}{{else}}—{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Guests</span>
                    <span class="value">{{.GuestRunning}}/{{.GuestTotal}} running</span>
                </div>
            </div>
        </section>
        {{else}}
        <section class="card">
            <div class="empty-state">
                <p>Waiting for first report.</p>
                <p class="hint">Vitals, guests and storage appear once this host's agent sends a host-report.</p>
            </div>
        </section>
        {{end}}

        <!-- Guests -->
        <section class="card" style="padding: 0; overflow: hidden;">
            <h2 style="padding: 1.25rem 1.25rem 0.5rem;">Guests</h2>
            {{if .Guests}}
            <table class="data-table">
                <thead>
                    <tr>
                        <th>VMID</th>
                        <th>Name</th>
                        <th>Status</th>
                        <th>Controller</th>
                        <th>Last Seen</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .Guests}}
                    <tr>
                        <td>{{.VMID}}</td>
                        <td>{{if .DisplayName}}{{.DisplayName}}{{else}}—{{end}}</td>
                        <td>{{if eq .Status "running"}}<span style="color: var(--green)">{{.Status}}</span>{{else if eq .Status "stopped"}}<span style="color: var(--red)">{{.Status}}</span>{{else}}{{.Status}}{{end}}</td>
                        <td>{{if .ControllerVersion}}<code>{{.ControllerVersion}}</code>{{else}}—{{end}}</td>
                        <td>{{timeAgoPtr .LastSeenAt}}</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{else}}
            <div class="empty-state" style="border: none;">
                <p>No guests reported on this host.</p>
            </div>
            {{end}}
        </section>

        <!-- Storage Targets -->
        <section class="card" style="padding: 0; overflow: hidden;">
            <h2 style="padding: 1.25rem 1.25rem 0.5rem;">Storage Targets</h2>
            {{if .StorageTargets}}
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Role</th>
                        <th>Type</th>
                        <th>State</th>
                        <th>Fill</th>
                        <th>Thin Pool</th>
                        <th>SMART</th>
                        <th>Temp</th>
                        <th>Wear</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .StorageTargets}}
                    <tr>
                        <td>{{.Name}}</td>
                        <td>{{if .Role}}{{.Role}}{{else}}—{{end}}</td>
                        <td>{{if .Type}}{{.Type}}{{else}}—{{end}}</td>
                        <td>{{if .State}}{{.State}}{{else}}—{{end}}</td>
                        <td>{{formatFloat .FillPct}}%</td>
                        <td>{{if .HasThin}}{{formatFloat .ThinDataPct}}%{{else}}—{{end}}</td>
                        <td>{{if .SmartHealth}}{{if eq .SmartHealth "PASSED"}}<span style="color: var(--green)">{{.SmartHealth}}</span>{{else if eq .SmartHealth "FAILED"}}<span style="color: var(--red)">{{.SmartHealth}}</span>{{else}}{{.SmartHealth}}{{end}}{{else}}—{{end}}</td>
                        <td>{{if .TempC}}{{.TempC}}°C{{else}}—{{end}}</td>
                        <td>{{if .WearPct}}{{.WearPct}}%{{else}}—{{end}}</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{else}}
            <div class="empty-state" style="border: none;">
                <p>No storage targets reported.</p>
            </div>
            {{end}}
        </section>

        <!-- Capabilities (v0.51.0): the agent's privileged-capability self-check, rendered.
             inactive = disabled by configuration (neutral, DR tier off) — NOT degraded. -->
        <section class="card" style="padding: 0; overflow: hidden;">
            <h2 style="padding: 1.25rem 1.25rem 0.5rem;">Capabilities</h2>
            {{if .NeedsDRMigration}}
            <p class="hint" style="padding: 0 1.25rem; color: var(--text-muted); font-size: 0.85rem;">
                This box predates the uniform DR plumbing (host-install &lt; v1.15.0) — the pbsdr
                wrapper is missing. Migration one-liner set (as root on the host): fetch
                <code>configs/felhom-pbs-apply</code> from the agent repo to
                <code>/usr/local/sbin/felhom-pbs-apply</code> (0755), <code>apt-get install -y age</code>,
                set <code>wg_tunnel.enabled: true</code> in agent.json, restart felhom-agent —
                see documentation/runbooks/day0-install.md (retrofit).
            </p>
            {{end}}
            {{if .Capabilities}}
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Capability</th>
                        <th>Status</th>
                        <th>Feature / reason</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .Capabilities}}
                    <tr>
                        <td>{{.Name}}{{if .Critical}} <span class="badge badge-neutral">critical</span>{{end}}</td>
                        <td><span class="badge {{.Class}}">{{.Status}}</span></td>
                        <td>{{.Feature}}{{if .Reason}} — {{.Reason}}{{end}}</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{else}}
            <div class="empty-state" style="border: none;">
                <p>No capability report yet (waiting for the first host report).</p>
            </div>
            {{end}}
        </section>

        <!-- Diagnostics: component log bundles (v0.46.0) -->
        <section class="card">
            <h2>Diagnostics — Log Bundles</h2>
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
                Pull-based: the box ships its debug ring on its own next cycle — controller &le; one report interval (~15 min),
                agent &asymp; one heartbeat. The pull is recorded in the box's own log (customer-visible). Bundles expire after 72 h.
            </p>
            <div style="display: flex; gap: 0.5rem; margin: 0.75rem 0;">
                <form method="POST" action="/hosts/{{.HostID}}/request-logs" style="display: inline;">
                    <input type="hidden" name="_csrf" value="{{.CSRFToken}}">
                    <input type="hidden" name="component" value="controller">
                    <button type="submit" class="btn btn-sm">Request controller logs</button>
                </form>
                <form method="POST" action="/hosts/{{.HostID}}/request-logs" style="display: inline;">
                    <input type="hidden" name="_csrf" value="{{.CSRFToken}}">
                    <input type="hidden" name="component" value="agent">
                    <button type="submit" class="btn btn-sm">Request agent logs</button>
                </form>
            </div>
            {{if .LogBundles}}
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Component</th>
                        <th>State</th>
                        <th>Collected</th>
                        <th>Received</th>
                        <th>Size</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    {{range .LogBundles}}
                    <tr>
                        <td>{{.Component}}</td>
                        <td>
                            {{if eq .State "pending"}}<span class="badge badge-neutral" title="Waiting for the box's next cycle (requested {{timeAgo .RequestedAt}})">pending</span>
                            {{else if eq .State "blocked"}}<span class="badge badge-error" title="{{.BlockedNote}}">blocked: possible secret</span>
                            {{else}}<span class="badge badge-ok">available</span>{{end}}
                        </td>
                        <td>{{if .CollectedAt.IsZero}}—{{else}}{{timeAgo .CollectedAt}}{{end}}</td>
                        <td>{{if .ReceivedAt.IsZero}}—{{else}}{{timeAgo .ReceivedAt}}{{end}}</td>
                        <td>{{if .SizeBytes}}{{.SizeBytes}} B{{else}}—{{end}}</td>
                        <td>
                            {{if eq .State "available"}}
                            <a href="/hosts/{{$.HostID}}/log-bundles/{{.ID}}" class="btn btn-sm">View</a>
                            <a href="/hosts/{{$.HostID}}/log-bundles/{{.ID}}?download=1" class="btn btn-sm">Download</a>
                            {{end}}
                        </td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{else}}
            <div class="empty-state" style="border: none;">
                <p>No log bundles. Use the request buttons above — the box delivers on its next cycle.</p>
            </div>
            {{end}}
        </section>

        <!-- Network (v0.85.0): where this box actually is. Addresses come from the agent's
             addresses[] (agent >= 0.119.0); the WireGuard row pairs the HUB's allocation with
             whether the box confirms holding it, because an allocation alone cannot tell a live
             tunnel from a peer that was never applied. -->
        <section class="card">
            <h2>Network</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">WireGuard</span>
                    <span class="value">
                        {{if .Network.WGAssignedIP}}<code>{{.Network.WGAssignedIP}}</code>
                            {{if .Network.WGConfirmed}}<span class="badge badge-ok" title="The box reports holding this address">confirmed</span>
                            {{else if .Network.AgentTooOld}}<span class="badge badge-neutral" title="This agent does not report its addresses">allocated</span>
                            {{else}}<span class="badge badge-warn" title="The hub allocated this peer but the box does not report holding the address">not confirmed by the box</span>{{end}}
                        {{else}}<span class="text-muted">no peer allocated</span>{{end}}
                    </span>
                </div>
            </div>
            {{if .Network.AgentTooOld}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
                This host's agent (<code>{{.AgentVersion}}</code>) does not report its addresses &mdash; they are
                <strong>unknown</strong>, not absent. Needs agent <code>0.119.0</code> or newer.
            </p>
            {{else if .Network.Addresses}}
            <table class="data-table" style="margin-top: 0.75rem;">
                <thead>
                    <tr><th>Interface</th><th>Address</th></tr>
                </thead>
                <tbody>
                    {{range .Network.Addresses}}
                    <tr>
                        <td><code>{{.Iface}}</code></td>
                        <td><code>{{.CIDR}}</code></td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
                Every routable address the box holds, as the kernel sees it. Loopback and link-local are
                excluded &mdash; including the <code>169.254.253.1</code> local-API island, which is identical on
                every box. The PVE web console is at <code>https://&lt;the LAN address&gt;:8006</code>.
            </p>
            {{else if .HasReport}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
                The agent reports no routable address on this host.
            </p>
            {{else}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
                Waiting for the first host report.
            </p>
            {{end}}
        </section>

        <!-- Guest network (R-319): the R-54 watchdog's verdict per owned guest, and — the reason this
             card exists — HOW OFTEN IT HAD TO REPAIR EACH ONE. A guest the watchdog keeps fixing is
             healthy every time anyone looks and is nevertheless failing; showing only the state would
             give it a green tick, which is the failed-disk-drawn-as-a-healthy-empty-disk defect.

             An unknown is NEVER drawn as healthy. Three absences, three sentences: an agent too old to
             report it, a capable agent that reported nothing, and a guest whose own state the watchdog
             did not assert. Each branch of this gate has its own render test (the template-gate rule).
             The agent has sent this on every heartbeat since v0.92.0 and nothing read it until now. -->
        <section class="card">
            <h2>Guest network
                {{if .GuestNet.AgentTooOld}}<span class="badge badge-neutral" title="This agent predates the guest-network watchdog">unknown</span>
                {{else if not .GuestNet.Reported}}<span class="badge badge-neutral" title="A capable agent sent no guest-network stanza">unknown</span>
                {{else if .GuestNet.Degraded}}<span class="badge badge-warn" title="A guest is unhealthy, or is being repaired repeatedly">needs attention</span>
                {{else if .GuestNet.UnknownCount}}<span class="badge badge-neutral" title="The watchdog did not assert a state for every guest">partly unknown</span>
                {{else if .GuestNet.Guests}}<span class="badge badge-ok" title="Every owned guest has an address, a default route and a live dhclient">healthy</span>
                {{end}}
            </h2>

            {{if .GuestNet.AgentTooOld}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
                This host's agent (<code>{{.AgentVersion}}</code>) does not run the guest-network watchdog &mdash;
                its guests' networking is <strong>unknown</strong>, not healthy. Needs agent <code>0.92.0</code> or newer.
            </p>
            {{else if not .GuestNet.Reported}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
                This host reported no guest-network state, so it is <strong>unknown</strong> &mdash; not healthy.
                The watchdog is default-on; a silent capable agent means it was switched off
                (<code>guest_net.disable</code>), or this report predates it on this box.
            </p>
            {{else if .GuestNet.Guests}}
            <table class="data-table">
                <thead>
                    <tr><th>Guest</th><th>State</th><th>Address</th><th>Route</th><th>dhclient</th><th>Repairs (1h)</th></tr>
                </thead>
                <tbody>
                    {{range .GuestNet.Guests}}
                    <tr>
                        <td><code>{{.VMID}}</code></td>
                        <td>
                            {{if .Unknown}}<span class="badge badge-neutral" title="{{.Message}}">unknown</span>
                            {{else if eq .State "healthy"}}<span class="badge badge-ok" title="{{.Message}}">healthy</span>
                            {{else if eq .State "static_fault"}}<span class="badge badge-warn" title="{{.Message}}">static fault</span>
                            {{else}}<span class="badge badge-error" title="{{.Message}}">unhealthy</span>{{end}}
                            {{if .HealFailed}}<span class="badge badge-error" title="The watchdog TRIED to repair this guest and did not succeed">repair failed</span>{{end}}
                            {{if .Damped}}<span class="badge badge-neutral" title="Repairs are rate-limited on this guest">damped</span>{{end}}
                        </td>
                        <td>{{if .IP}}<code>{{.IP}}</code> <span class="text-muted">({{.Mode}})</span>{{else}}<span class="text-muted">&mdash;</span>{{end}}</td>
                        <td>{{if .HasRoute}}yes{{else}}<strong>no</strong>{{end}}</td>
                        <td>{{if .DHClientAlive}}yes{{else if eq .Mode "static"}}<span class="text-muted">n/a</span>{{else}}<strong>no</strong>{{end}}</td>
                        <td>
                            {{if .Repairing}}<span class="badge badge-warn" title="The watchdog repaired this guest's network {{.RepairCount}} time(s) in the last hour — last at {{.LastHealAt}}">{{.RepairCount}}</span>
                            {{else}}<span class="text-muted">0</span>{{end}}
                        </td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{if .GuestNet.RepairingCount}}
            <p class="hint" style="font-size: 0.85rem; margin-top: 0.5rem;">
                <strong>{{.GuestNet.RepairingCount}} guest(s) had their networking repaired in the last hour.</strong>
                A guest that keeps being repaired reads healthy between repairs and is not. A killed
                <code>dhclient</code> once took a tunnel down for 1 h 15 m with nobody told; this row is that signal.
            </p>
            {{end}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
                Last swept {{.GuestNet.CheckedAt}}. One row per owned <em>running</em> guest the watchdog has probed.
            </p>
            {{else}}
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
                The watchdog ran and has no guest to report (swept {{.GuestNet.CheckedAt}}). This is
                distinguishable from silence: the stanza arrived, its guest list is empty.
            </p>
            {{end}}
        </section>

        <!-- DR / Backup -->
        <section class="card">
            <h2>DR / Backup</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">DR Recipe</span>
                    <span class="value">{{if .DRPresent}}<span style="color: var(--green)">present</span>{{else}}<span class="text-muted">none</span>{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Key Escrow</span>
                    <span class="value">{{if .EscrowPresent}}<span style="color: var(--green)">present</span>{{else}}<span class="text-muted">none</span>{{end}}{{if gt .SupersededEscrowCount 0}} <span class="text-muted">· {{.SupersededEscrowCount}} superseded escrow blob(s) retained</span>{{end}}</span>
                </div>
            </div>
        </section>

        <!-- Console access (v0.84.0): the break-glass root@pam credential. The page carries
             presence + username + set_at ONLY — the plaintext NEVER enters this document and is
             fetched on demand from POST /hosts/{id}/reveal-recovery-credential. Deliberately NOT
             the customer_unified data-secret widget, which embeds the plaintext on every load. -->
        <section class="card">
            <h2>Console access</h2>
            {{if .RecoveryVaulted}}
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">User</span>
                    <span class="value"><code>{{.RecoveryUsername}}</code></span>
                </div>
                <div class="info-item">
                    <span class="label">Password set</span>
                    <span class="value">{{timeAgo .RecoverySetAt}}</span>
                </div>
            </div>
            <div class="credential-box">
                <code id="console-pw-{{.HostID}}">&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;</code>
                <button type="button" class="copy-btn" id="console-reveal-{{.HostID}}" data-reveal-url="/hosts/{{.HostID}}/reveal-recovery-credential" onclick="revealConsolePassword('{{.HostID}}')">Reveal</button>
                <button type="button" class="copy-btn" id="console-copy-{{.HostID}}" onclick="copyConsolePassword('{{.HostID}}')">Copy</button>
            </div>
            <p class="hint" id="console-hint-{{.HostID}}" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
                Break-glass credential for the PVE web console at https://&lt;host-ip&gt;:8006 (realm: Linux PAM standard authentication). <strong>Copy</strong> puts it straight on the clipboard without showing it; <strong>Reveal</strong> displays it for 60&nbsp;s. Either one is recorded on the customer's event timeline. Last vaulted value — if root@pam was changed on the box without re-vaulting, this is stale.
            </p>
            {{else}}
            <p><span class="badge badge-neutral">not vaulted</span></p>
            <p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
                No console credential is vaulted for this host. Expected for a byo host — the owner manages root@pam. Otherwise the installer's step 4b did not run; re-run felhom-host-install.sh, or set and vault one per the break-glass runbook &sect;5.
            </p>
            {{end}}
        </section>
        {{if .RecoveryVaulted}}
        <script>
        // Fetch-on-demand: the password exists in this document ONLY between a Reveal and the next
        // mask, and only in a local variable — never in localStorage, a data- attribute or the URL.
        // `|| {}` because the customer page renders this sub-template once per host: a plain
        // re-assignment would drop a sibling card's mask timer on the floor.
        var consolePwState = typeof consolePwState !== 'undefined' ? consolePwState : {};
        var consolePwMask = '••••••••••••••••';

        function consoleHint(hostID, msg) {
            var h = document.getElementById('console-hint-' + hostID);
            if (h) { h.textContent = msg; }
        }
        function maskConsolePassword(hostID) {
            var st = consolePwState[hostID];
            if (st && st.timer) { clearTimeout(st.timer); }
            consolePwState[hostID] = null;          // CLEARS the retained plaintext
            var code = document.getElementById('console-pw-' + hostID);
            if (code) { code.textContent = consolePwMask; }
            var reveal = document.getElementById('console-reveal-' + hostID);
            if (reveal) { reveal.textContent = 'Reveal'; }
        }
        // fetchConsolePassword is the ONE retrieval path, shared by Reveal and Copy — so Copy cannot
        // drift onto a different endpoint, and both are audited identically server-side.
        function fetchConsolePassword(hostID, onOK, onErr) {
            var btn = document.getElementById('console-reveal-' + hostID);
            fetch(btn.getAttribute('data-reveal-url'), {
                method: 'POST',
                headers: {'X-CSRF-Token': '{{.CSRFToken}}'}
            }).then(function(r){
                if (!r.ok) { throw new Error('HTTP ' + r.status); }
                return r.json();
            }).then(function(d){ onOK(d.password); }).catch(onErr);
        }
        function showConsolePassword(hostID, pw) {
            document.getElementById('console-pw-' + hostID).textContent = pw;
            consolePwState[hostID] = {pw: pw, timer: setTimeout(function(){ maskConsolePassword(hostID); }, 60000)};
            document.getElementById('console-reveal-' + hostID).textContent = 'Hide';
        }
        function revealConsolePassword(hostID) {
            if (consolePwState[hostID]) { maskConsolePassword(hostID); return; }   // second click hides
            document.getElementById('console-pw-' + hostID).textContent = 'Revealing…';
            fetchConsolePassword(hostID, function(pw){
                showConsolePassword(hostID, pw);
            }, function(e){
                document.getElementById('console-pw-' + hostID).textContent = consolePwMask;
                consoleHint(hostID, 'Could not reveal the credential (' + e.message + '). The global-key curl path in the break-glass runbook §3.1 still works.');
            });
        }
        // Copy works WITHOUT revealing — the safer default, since the secret never renders on screen
        // and so never lands in a screenshot or a shoulder-surf.
        //
        // Every branch below reports its outcome. The version this replaced was disabled until a
        // Reveal and did nothing when clicked, which left the operator's clipboard holding whatever
        // was in it before — in the incident that prompted this, ANOTHER HOST'S console password,
        // pasted into a login that then failed with no clue why. A copy button that silently
        // no-ops is worse than no copy button.
        function copyConsolePassword(hostID) {
            var st = consolePwState[hostID];
            if (st) { writeConsoleClipboard(hostID, st.pw); return; }   // already revealed — reuse it
            consoleHint(hostID, 'Copying…');
            fetchConsolePassword(hostID, function(pw){
                writeConsoleClipboard(hostID, pw);
            }, function(e){
                consoleHint(hostID, 'Could not copy the credential (' + e.message + '). The global-key curl path in the break-glass runbook §3.1 still works.');
            });
        }
        function writeConsoleClipboard(hostID, pw) {
            // No clipboard API at all (an insecure context) — degrade to showing it, and SAY so.
            if (!navigator.clipboard || !navigator.clipboard.writeText) {
                showConsolePassword(hostID, pw);
                consoleHint(hostID, 'This browser will not give the page clipboard access, so the password is shown instead — copy it manually. It hides again in 60 s.');
                return;
            }
            navigator.clipboard.writeText(pw).then(function(){
                maskConsolePassword(hostID);
                // Name the HOST in the confirmation: the clipboard is fleet-wide and every box has a
                // different console password, so "copied" alone does not say copied for WHICH box.
                consoleHint(hostID, '✓ Copied ' + hostID + '\u2019s root@pam password to the clipboard. It stays there until you copy something else.');
            }).catch(function(e){
                // The write was REFUSED (permissions, or not a user gesture). Never claim success.
                showConsolePassword(hostID, pw);
                consoleHint(hostID, 'The clipboard write was refused (' + (e && e.message ? e.message : 'no reason given') + '), so the password is shown instead — copy it manually. It hides again in 60 s.');
            });
        }
        document.addEventListener('visibilitychange', function(){
            if (document.visibilityState === 'hidden') {
                for (var id in consolePwState) { if (consolePwState[id]) { maskConsolePassword(id); } }
            }
        });
        </script>
        {{end}}

        {{if .Deletable}}
        <!-- Danger zone (v0.47.0): rendered ONLY for non-online hosts — deleting a live
             host would brick its heartbeat channel, so the affordance never exists for one.
             Impact + type-to-confirm dialog per the global-floor confirm pattern. -->
        <section class="card" style="border-color: var(--crit);">
            <h2>Danger zone</h2>
            <p class="text-muted" style="font-size: 0.85rem;">
                Removing this host deletes its reports, guests, log bundles and WireGuard peer, and
                permanently invalidates its API key — a still-running agent would receive 401s.
                Re-enrollment requires the Day-0 passphrase flow.
            </p>
            <button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteConfirm('{{.HostID}}')">Remove host&hellip;</button>
            <div id="host-delete-confirm-{{.HostID}}" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); background: var(--crit-dim); border-radius: var(--radius); max-width: 44em;">
                <p id="host-delete-impact-{{.HostID}}" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
                <label id="host-delete-escrow-row-{{.HostID}}" style="display: none; margin: 0 0 0.5rem; font-size: 0.85em;">
                    <input type="checkbox" id="host-delete-escrow-{{.HostID}}">
                    Move key escrow to retained custody (required when escrow present) + remove DR bundle for this host
                </label>
                <p style="margin: 0 0 0.5rem; font-size: 0.85em; color: var(--text-2);">Type the host id to confirm:</p>
                <form method="POST" action="/hosts/{{.HostID}}/delete" id="host-delete-form-{{.HostID}}" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
                    <input type="hidden" name="_csrf" value="{{.CSRFToken}}">
                    <input type="hidden" name="confirm_host_id" id="host-delete-confirm-hidden-{{.HostID}}" value="">
                    <input type="hidden" name="delete_escrow" id="host-delete-escrow-hidden-{{.HostID}}" value="">
                    <input type="text" id="host-delete-confirm-input-{{.HostID}}" placeholder="retype the host id&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
                    <button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteSubmit('{{.HostID}}')">Confirm &amp; remove</button>
                    <button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('host-delete-confirm-{{.HostID}}').style.display='none';">Cancel</button>
                </form>
                <p id="host-delete-err-{{.HostID}}" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
            </div>
        </section>
        <script>
        function hostDeleteConfirm(hostID) {
            var box = document.getElementById('host-delete-confirm-' + hostID);
            var impact = document.getElementById('host-delete-impact-' + hostID);
            document.getElementById('host-delete-confirm-input-' + hostID).value = '';
            document.getElementById('host-delete-err-' + hostID).textContent = '';
            box.style.display = 'block';
            impact.textContent = 'Checking impact…';
            fetch('/hosts/' + encodeURIComponent(hostID) + '/delete-impact')
                .then(function(r){ return r.json(); })
                .then(function(d){
                    var parts = ['Deleting ' + hostID + ' removes ' + d.guests + ' guest row(s), ' +
                        d.reports + ' host report(s), ' + d.log_bundles + ' agent log bundle(s)'];
                    if (d.wg_peer_bound) parts.push('the bound WireGuard peer');
                    if (d.pbs_secret_present) parts.push('the staged PBS secret');
                    if (d.recovery_present) parts.push('the break-glass recovery credential');
                    impact.textContent = parts.join(', ') + '. Host status: ' + d.status + '.' +
                        (d.deletable ? '' : ' Host is ONLINE — deletion will be refused.');
                    document.getElementById('host-delete-escrow-row-' + hostID).style.display =
                        d.escrow_present ? 'block' : 'none';
                })
                .catch(function(){ impact.textContent = 'Could not compute the impact — the server will still enforce every gate.'; });
        }
        function hostDeleteSubmit(hostID) {
            var typed = document.getElementById('host-delete-confirm-input-' + hostID).value.trim();
            var err = document.getElementById('host-delete-err-' + hostID);
            if (typed !== hostID) { err.textContent = 'Confirmation does not match the host id.'; return; }
            var escrowRow = document.getElementById('host-delete-escrow-row-' + hostID);
            var escrowCb = document.getElementById('host-delete-escrow-' + hostID);
            if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
                err.textContent = 'This host has a key escrow — tick the acknowledgement to delete it too.';
                return;
            }
            document.getElementById('host-delete-confirm-hidden-' + hostID).value = typed;
            document.getElementById('host-delete-escrow-hidden-' + hostID).value = escrowCb.checked ? '1' : '';
            document.getElementById('host-delete-form-' + hostID).submit();
        }
        </script>
        {{end}}
{{end}}
