<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}} — Felhom Hub</title>
    <link rel="stylesheet" href="/style.css?v={{hubVersion}}">
    <meta name="csrf-token" content="{{.CSRFToken}}">
    <script>function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}</script>
    {{template "inline_confirm_js"}}
</head>
<body>
    {{template "icon_sprite"}}
    <div class="container">
        <header>
            <nav class="nav-links" style="margin-bottom: 0.5rem;">
                <a href="/" class="nav-link">Dashboard</a>
                <a href="/configs" class="nav-link active">Customers</a>
                <a href="/apps" class="nav-link">Apps</a>
                <a href="/hosts" class="nav-link">Hosts</a>
                <a href="/offsite" class="nav-link">Offsite</a>
                <a href="/configuration" class="nav-link">Configuration</a>
            </nav>
            <a href="/configs" class="back-link">&larr; All Customers</a>
            <h1>
                <span class="status-dot status-dot-{{statusColor .OverallStatus}}"></span>
                {{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}}
                {{if .HostCause}}<span class="status-badge status-badge-warn">{{.HostCause}}</span>{{end}}
            </h1>
            {{if .HasReports}}
            <p class="subtitle">
                Last report: {{timeAgo .Customer.ReceivedAt}} &middot; Controller {{.Customer.ControllerVersion}}
                <label class="auto-refresh-toggle" title="Auto-refresh every 60s">
                    <input type="checkbox" id="autoRefreshToggle">
                    <span class="toggle-slider"></span>
                    <span class="toggle-label">Auto-refresh</span>
                    <span id="refresh-paused-hint" class="toggle-label" style="display:none">(paused)</span>
                </label>
            </p>
            {{else}}
            <p class="subtitle">No reports received yet</p>
            {{end}}
        </header>

        {{if .Flash}}
        <div class="flash flash-success">
            {{if eq .Flash "created"}}Configuration created successfully.
            {{else if eq .Flash "updated"}}Configuration updated.
            {{else if eq .Flash "password_regenerated"}}Retrieval password regenerated.
            {{else if eq .Flash "offsite_reissued"}}Offsite credentials re-issued — a fresh one-time password is staged; the controller picks it up on its next config refresh.
            {{else if eq .Flash "pbsdr_reissued"}}PBS DR credentials re-issued — a fresh one-time token secret is staged for the agent. Confirm it actually landed: the host's <code>pvesm status</code> must show the PBS entry <strong>active</strong>. A converged agent can report <code>applied</code> while the storage still authenticates 401 (R-39).
            {{else if eq .Flash "offsite_frozen"}}Offsite storage FROZEN (read-only) — new backups and prune will fail until unfrozen.
            {{else if eq .Flash "offsite_unfrozen"}}Offsite storage unfrozen — read-write restored.
            {{else if eq .Flash "blocked"}}Customer blocked — hidden from Dashboard.
            {{else if eq .Flash "unblocked"}}Customer unblocked — visible on Dashboard again.
            {{else if eq .Flash "log_tail_requested"}}Log tail requested — the controller delivers it on its next report cycle (a few minutes). A customer-visible event line was recorded.
            {{else if eq .Flash "claim-resent"}}Code re-sent to the registered address. A kód a doboz következő jelentésekor (~15 percen belül) aktiválódik.
            {{else if eq .Flash "claim-resend-failed"}}Claim code resend FAILED — check the hub log (email delivery / send error).
            {{else if eq .Flash "selfbind-sent"}}Self-bind link sent to the registered address — valid for 7 days. The customer enters the box's console pairing code + their retrieval passphrase; no operator bind needed.
            {{else if eq .Flash "selfbind-no-email"}}Self-bind link NOT sent — this customer has no registered email address. Set one first, or bind the appliance manually from the Hosts page.
            {{else if eq .Flash "selfbind-send-failed"}}Self-bind link send FAILED — the link was invalidated (not left live). Check the hub log (email delivery / send error).
            {{else if eq .Flash "reset_done"}}Customer RESET complete — every operational trace was destroyed (offsite repo, PBS namespace, DR recipe, claim state, retained escrow custody). Identity and basic config survive; the audit event stream records it.
            {{end}}
        </div>
        {{end}}

        {{if .IsBlocked}}
        <div class="flash flash-blocked">
            This customer is blocked — reports are accepted but not shown on the Dashboard.
        </div>
        {{end}}

        {{if .StaleSinceReset}}
        <div class="flash flash-warn">
            <strong>No data since the RESET</strong> ({{.ResetAt}}) — „RESET óta nincs adat".
            Every health figure below predates it and describes a lifecycle that no longer exists.
            The box repopulates this page on its first report after re-onboarding.
        </div>
        {{end}}

        {{if .OffsiteUnprovisioned}}
        <div class="flash flash-warn">
            <strong>Offsite is enabled but was never provisioned</strong> — no descriptor exists for
            this customer, so nothing is being backed up offsite. Provisioning is <em>Save</em>-triggered:
            open the Edit tab and press Save once to provision it, then verify on the Offsite page.
            Re-enrollment will not fix this on its own — the auto-re-issue deliberately skips an
            unprovisioned target. (R-36)
        </div>
        {{end}}

        <!-- Sticky summary strip: identity + liveness at a glance while any tab is scrolled.
             Values mirror fields already rendered in the panels — no extra handler data. -->
        <div class="summary-strip">
            <span class="strip-name">{{if .CustomerName}}{{.CustomerName}}{{else}}{{.CustomerID}}{{end}}</span>
            <span class="strip-item"><span class="status-dot status-dot-{{statusColor .OverallStatus}}"></span> {{.OverallStatus}}</span>
            {{if .HostCause}}<span class="strip-item"><span class="status-badge status-badge-warn">{{.HostCause}}</span></span>{{end}}
            {{if .HasReports}}
            <span class="strip-item">Controller <code>{{.Customer.ControllerVersion}}</code></span>
            <span class="strip-item">Last report {{timeAgo .Customer.ReceivedAt}}</span>
            <span class="strip-item">Containers {{.Customer.ContainerRunning}}/{{.Customer.ContainerTotal}}</span>
            {{end}}
        </div>

        {{if not .HasReports}}
        {{if .HasConfig}}
        <!-- Always above the tab bar — visible whichever tab is active. -->
        <section class="card">
            <h2>Waiting for First Report</h2>
            <p class="text-muted">This customer has been configured but no controller report has been received yet.</p>
            <p class="text-muted" style="margin-top: 0.5rem;">Use one of the setup commands below to deploy the controller on the customer node.</p>
        </section>
        {{end}}
        {{end}}

        <!-- Tab nav: hash-based (#tab=<name>); with JS off it is plain anchors and every panel
             below stays visible. data-live-tabs = the tabs whose content changes with incoming
             reports; the 60s auto-refresh fires ONLY while one of them is active (v0.48.0). -->
        <nav class="tab-nav" id="tab-nav" data-live-tabs="overview,applications,events,host">
            <a href="#tab=overview" data-tab="overview" class="active">Overview</a>
            <a href="#tab=applications" data-tab="applications">Applications</a>
            <a href="#tab=setup" data-tab="setup">Setup</a>
            <a href="#tab=edit" data-tab="edit">Edit</a>
            <a href="#tab=backup" data-tab="backup">Backup &amp; DR</a>
            <a href="#tab=events" data-tab="events">Events{{with mapGet .EventCounts "error"}}<span class="tab-badge">{{.}}</span>{{end}}</a>
            <a href="#tab=notifications" data-tab="notifications">Notifications</a>
            <a href="#tab=host" data-tab="host">Host</a>
        </nav>

        <!-- ═══ Overview ═══ -->
        <div class="tab-panel tab-panel-active" data-tab="overview">

        <!-- Customer Info -->
        <section class="card">
            <div style="display: flex; justify-content: space-between; align-items: flex-start;">
                <h2>Customer Info</h2>
                <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
                    {{/* v0.48.0 edit-a: Edit/Block/Delete moved to the Edit tab (form + danger
                         zone); only the config-less bootstrap action stays in the header. */}}
                    {{if not .HasConfig}}
                    <form method="POST" action="/customers/{{.CustomerID}}/create-config" style="display:inline">
                        {{.CSRFField}}
                        <button type="submit" class="btn btn-sm">Create Config</button>
                    </form>
                    {{end}}
                </div>
            </div>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Customer ID</span>
                    <span class="value"><code>{{.CustomerID}}</code></span>
                </div>
                <div class="info-item">
                    <span class="label">Name</span>
                    <span class="value">{{if .CustomerName}}{{.CustomerName}}{{else}}—{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Domain</span>
                    <span class="value">{{if .Domain}}{{.Domain}}{{else}}—{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Email</span>
                    <span class="value">{{if .Email}}{{.Email}}{{else}}—{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Config</span>
                    <span class="value">
                        {{if .HasConfig}}
                        <span class="config-badge config-badge-managed">MANAGED</span>
                        {{else}}
                        <span class="config-badge config-badge-manual">MANUAL</span>
                        {{end}}
                        {{if .IsBlocked}}<span class="config-badge config-badge-blocked">BLOCKED</span>{{end}}
                    </span>
                </div>
                {{if .HasConfig}}
                <div class="info-item">
                    <span class="label">Config Created</span>
                    <span class="value">{{timeAgo .Config.CreatedAt}}</span>
                </div>
                {{end}}
            </div>
        </section>

        {{if .HasReports}}
        <!-- Health -->
        <section class="card">
            <h2>Health</h2>
            {{if eq .OverallStatus "disabled"}}
            <p class="health-status health-status-disabled">Reporting has been disabled on this node</p>
            <p class="hint">Enable it in the controller's <code>controller.yaml</code>: <code>hub.enabled: true</code></p>
            {{else if eq .OverallStatus "blocked"}}
            <p class="health-status health-status-disabled">Customer is blocked</p>
            {{else}}
            {{with .Report.health}}
            <p class="health-status health-status-{{index . "status"}}">
                Status: {{index . "status"}}
            </p>
            {{$issues := index . "issues"}}
            {{if $issues}}
            <h3>Issues</h3>
            <ul class="issue-list">
                {{range $issues}}
                <li class="issue">{{.}}</li>
                {{end}}
            </ul>
            {{end}}
            {{$warnings := index . "warnings"}}
            {{if $warnings}}
            <h3>Warnings</h3>
            <ul class="warning-list">
                {{range $warnings}}
                <li class="warning">{{.}}</li>
                {{end}}
            </ul>
            {{end}}
            {{end}}
            {{end}}
        </section>

        <!-- System Info -->
        <section class="card">
            <h2>System</h2>
            <div class="info-grid">
                {{with .Report.system}}
                <div class="info-item">
                    <span class="label">Hostname</span>
                    <span class="value">{{index . "hostname"}}</span>
                </div>
                <div class="info-item">
                    <span class="label">OS</span>
                    <span class="value">{{index . "os"}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Kernel</span>
                    <span class="value">{{index . "kernel"}}</span>
                </div>
                <div class="info-item">
                    <span class="label">CPU</span>
                    <span class="value">{{index . "cpu_model"}} ({{index . "cpu_cores"}} cores)</span>
                </div>
                {{end}}
            </div>
            <div class="metrics-grid">
                <div class="metric">
                    <span class="metric-label">CPU</span>
                    <span class="metric-value">{{formatFloat .Customer.CPUPercent}}%</span>
                    <div class="bar"><div class="bar-fill" style="width: {{formatFloat .Customer.CPUPercent}}%"></div></div>
                </div>
                <div class="metric">
                    <span class="metric-label">Memory</span>
                    <span class="metric-value">{{formatFloat .Customer.MemoryPercent}}%</span>
                    <div class="bar"><div class="bar-fill" style="width: {{formatFloat .Customer.MemoryPercent}}%"></div></div>
                </div>
            </div>
        </section>

        <!-- Storage -->
        <section class="card">
            <h2>Storage</h2>
            {{with .Report.storage}}
            <div class="metrics-grid">
                {{range .}}
                <div class="metric">
                    <span class="metric-label">{{with index . "label"}}{{.}}{{else}}{{index . "mount"}}{{end}}</span>
                    <span class="metric-value">{{printf "%.0f" (index . "percent")}}%</span>
                    <div class="bar"><div class="bar-fill" style="width: {{printf "%.0f" (index . "percent")}}%"></div></div>
                    <span class="metric-detail">{{printf "%.1f" (index . "used_gb")}} / {{printf "%.1f" (index . "total_gb")}} GB</span>
                </div>
                {{end}}
            </div>
            {{end}}
        </section>

        <!-- Backup -->
        <section class="card">
            <h2>Backup</h2>
            {{with .Report.backup}}
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Enabled</span>
                    <span class="value">{{if index . "enabled"}}Yes{{else}}No{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Snapshots</span>
                    <span class="value">{{index . "snapshot_count"}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Repo Size</span>
                    <span class="value">{{index . "repo_size_mb"}} MB</span>
                </div>
                <div class="info-item">
                    <span class="label">Integrity</span>
                    <span class="value">{{if index . "integrity_ok"}}OK{{else}}Unknown{{end}}</span>
                </div>
            </div>
            {{end}}
        </section>
        {{end}}

        </div>

        <!-- ═══ Applications ═══ -->
        <div class="tab-panel" data-tab="applications">

        {{if .HasReports}}
        <!-- Containers -->
        <section class="card">
            <h2>Containers ({{.Customer.ContainerRunning}}/{{.Customer.ContainerTotal}})</h2>
            {{with .Report.containers}}
            {{$list := index . "list"}}
            {{if $list}}
            <table class="container-table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>State</th>
                        <th>CPU</th>
                        <th>Memory</th>
                    </tr>
                </thead>
                <tbody>
                    {{range $list}}
                    <tr>
                        <td>{{index . "name"}}</td>
                        <td><span class="container-state container-state-{{index . "state"}}">{{index . "state"}}</span></td>
                        <td>{{printf "%.1f" (index . "cpu_percent")}}%</td>
                        <td>{{printf "%.0f" (index . "memory_mb")}} MB</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{end}}
            {{end}}
        </section>

        <!-- App telemetry -->
        {{if .HasAppTelemetry}}
        <section class="card">
            <h2>App Telemetry <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(last 7 days)</span></h2>
            <table class="data-table">
                <thead>
                    <tr>
                        <th>App</th>
                        <th>Memory (current)</th>
                        <th>Memory (avg 7d)</th>
                        <th>Memory (peak 7d)</th>
                        <th>Catalog Limit</th>
                        <th>Errors</th>
                        <th>Warnings</th>
                        <th>Logs</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .AppTelemetry}}
                    <tr>
                        <td><a href="/apps/{{.AppName}}?customer={{$.CustomerID}}" title="Known issues filtered to this customer">{{if .DisplayName}}{{.DisplayName}}{{else}}{{.AppName}}{{end}}</a></td>
                        <td class="{{memoryColor .MemoryCurrentMB .CatalogLimit}}">{{formatFloat .MemoryCurrentMB}} MB</td>
                        <td>{{formatFloat .MemoryAvgMB}} MB</td>
                        <td>{{formatFloat .MemoryPeakMB}} MB</td>
                        <td>{{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}}</td>
                        <td>{{if gt .LogErrors 0}}<span class="badge badge-error">{{.LogErrors}}</span>{{else}}0{{end}}</td>
                        <td>{{if gt .LogWarnings 0}}<span class="badge badge-warn">{{.LogWarnings}}</span>{{else}}0{{end}}</td>
                        <td>
                            {{if index $.PendingTails .AppName}}
                            <span class="badge badge-neutral" title="The controller delivers the tail on its next report cycle">tail pending</span>
                            {{else}}
                            <form method="POST" action="/customers/{{$.CustomerID}}/request-log-tail" style="display: inline;">
                                <input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
                                <input type="hidden" name="app" value="{{.AppName}}">
                                <button type="submit" class="btn btn-sm" title="Pull-based: the controller ships the last 200 log lines on its next report; a customer-visible event line is recorded">Request log tail</button>
                            </form>
                            {{end}}
                        </td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
        </section>
        {{end}}

        <!-- Received log tails (on-demand, transient — last 2 per app) -->
        {{if .HasLogTails}}
        <section class="card">
            <h2>App Log Tails <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(on-demand, last 2 per app kept)</span></h2>
            <table class="data-table">
                <thead>
                    <tr>
                        <th>App</th>
                        <th>Collected</th>
                        <th>Received</th>
                        <th>Lines</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    {{range .LogTails}}
                    <tr>
                        <td style="font-family: var(--font-mono);">{{.AppName}}</td>
                        <td>{{.CollectedAt.Format "2006-01-02 15:04:05"}} ({{timeAgo .CollectedAt}})</td>
                        <td>{{timeAgo .ReceivedAt}}</td>
                        <td>{{len .Lines}}</td>
                        <td style="white-space: nowrap;">
                            <a href="/customers/{{$.CustomerID}}/log-tail/{{.ID}}" class="btn btn-sm">View</a>
                            <a href="/customers/{{$.CustomerID}}/log-tail/{{.ID}}?download=1" class="btn btn-sm">Download .log</a>
                        </td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
        </section>
        {{end}}
        {{else}}
        <section class="card"><p class="text-muted">Container and app data appear once the first report arrives.</p></section>
        {{end}}

        </div>

        <!-- ═══ Setup ═══ -->
        <div class="tab-panel" data-tab="setup">

        {{if .HasConfig}}
        <section class="card">
            <h2>Credentials</h2>
            <div class="credential-row">
                <div>
                    <span class="label">Retrieval Password</span>
                    <div class="credential-box">
                        <code id="retrieval-pw" data-secret="{{.Config.RetrievalPassword}}">&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;</code>
                        <button type="button" class="copy-btn" id="reveal-retrieval-pw" onclick="toggleSecret('retrieval-pw')" title="Reveal / hide">Reveal</button>
                        <button type="button" class="copy-btn" onclick="copySecret('retrieval-pw')" title="Copy">&#x2398;</button>
                    </div>
                    <span class="form-hint">The per-customer secret that fetches the whole config — masked by default; never place it on a command line (the installer reads it at a no-echo prompt).</span>
                </div>
                <form method="POST" action="/configs/{{.CustomerID}}/regen-password" style="margin-top: 0.5rem;">
                    {{.CSRFField}}
                    <button type="submit" class="btn btn-outline btn-sm" data-confirm="Regenerate retrieval password? The old password will stop working immediately.">Regenerate</button>
                </form>
            </div>
            <div class="credential-row" style="margin-top: 1rem;">
                <div>
                    <span class="label">API Key</span>
                    <div class="credential-box">
                        <code id="api-key">{{.Config.APIKey}}</code>
                        <button type="button" class="copy-btn" onclick="copyText('api-key')" title="Copy">&#x2398;</button>
                    </div>
                </div>
                <span class="form-hint">Used by the controller for ongoing hub communication (reports, notifications, backups)</span>
            </div>
        </section>

        <section class="card">
            <h2>Dashboard access — customer claim</h2>
            <div class="credential-row">
                <div>
                    <span class="label">Claim status</span>
                    <div style="margin: 0.3rem 0;">
                        {{if .Claim}}
                            {{if .Claim.ClaimedAt}}
                                <span class="badge badge-ok">Claimed {{timeAgoPtr .Claim.ClaimedAt}}</span>
                            {{else if .Claim.EmailedAt}}
                                <span class="badge badge-warn">Nyitott — kód kiküldve {{timeAgoPtr .Claim.EmailedAt}}</span>
                            {{else}}
                                <span class="badge badge-error">Nyitott — a kód e-mail NEM ment ki (resend!)</span>
                            {{end}}
                            <span class="form-hint" style="margin-left: 0.5rem;">generation {{.Claim.Generation}} · issued {{timeAgo .Claim.IssuedAt}}</span>
                        {{else}}
                            <span class="badge badge-neutral">no code issued yet</span>
                            <span class="form-hint" style="margin-left: 0.5rem;">issued automatically at the first config pull or report</span>
                        {{end}}
                    </div>
                    <span class="form-hint">The customer sets + owns the dashboard password (claim code → own password). The code goes ONLY to the registered address ({{.Email}}); the hub stores a hash — no plaintext code exists to display.</span>
                </div>
                {{if .Claim}}
                <form method="POST" action="/configs/{{.CustomerID}}/claim-resend" style="margin-top: 0.5rem;">
                    {{.CSRFField}}
                    <button type="submit" class="btn btn-outline btn-sm" data-confirm="Send a fresh code to the registered address? The previous code stops working immediately (the box activates it on its next report, ~15 min).">{{if .Claim.ClaimedAt}}Beállító kód küldése{{else}}Kód újraküldése{{end}}</button>
                </form>
                {{end}}

                <div class="form-group" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">
                    <label class="form-label">Customer self-bind (R-27)</label>
                    <span class="form-hint">Let the customer bind their own freshly-installed appliance — no operator bind needed. Sends a 7-day capability link to the registered address ({{.Email}}); the customer opens it and enters the box's <strong>console pairing code</strong> + their <strong>retrieval passphrase</strong>. Wrong entries lock the link after 5 attempts. If the link expires, bind the appliance manually from the Hosts page.</span>
                    <form method="POST" action="/customers/{{.CustomerID}}/selfbind-link" style="margin-top: 0.5rem;">
                        {{.CSRFField}}
                        <button type="submit" class="btn btn-outline btn-sm" data-confirm="Email a self-bind link to the registered address? Any previous self-bind link for this customer stops working immediately.">Send self-bind link</button>
                    </form>
                </div>
            </div>
        </section>

        <section class="card">
            <h2>Setup Command</h2>
            <p class="text-muted" style="margin-bottom: 1rem; font-size: 0.85rem;">
                Day-0 host bootstrap. The command always fetches the <strong>current</strong>
                felhom-host-install.sh from felhom.eu &mdash; there is no version to pick here. Run it
                on a freshly-PVE-installed Proxmox <strong>host</strong> as root (create the customer in
                the hub first). It enrolls the host, installs + verifies the agent, and provisions the guest;
                the in-guest controller then pulls its own <code>controller.yaml</code>. The retrieval
                passphrase is entered at the no-echo prompt — never on the command line.
            </p>

            <!-- Generator controls (client-side only; nothing is submitted) -->
            <div class="gen-controls" id="gen-controls" data-customer-id="{{.CustomerID}}">
                <div class="form-group">
                    <label>Mode <span style="color:var(--red,#E5534B)">*</span></label>
                    <div class="gen-radios">
                        <label class="gen-radio"><input type="radio" name="gen-mode" value="appliance" onchange="genUpdate()"> appliance <span class="form-hint">(Felhom-owned box)</span></label>
                        <label class="gen-radio"><input type="radio" name="gen-mode" value="byo" onchange="genUpdate()"> byo <span class="form-hint">(a host you do not own)</span></label>
                    </div>
                </div>
                <div class="form-grid" style="margin-top:0.75rem;">
                    <div class="form-group" id="gen-cores-grp">
                        <label>Cores <span class="gen-req" style="display:none;color:var(--red,#E5534B)">*</span></label>
                        <input type="number" id="gen-cores" min="1" placeholder="e.g. 12" oninput="genUpdate()">
                        <span class="form-hint">byo: required — a conservative slice of the host</span>
                    </div>
                    <div class="form-group" id="gen-memory-grp">
                        <label>Memory (MiB) <span class="gen-req" style="display:none;color:var(--red,#E5534B)">*</span></label>
                        <input type="number" id="gen-memory" min="256" placeholder="e.g. 32768 (= 32 GB)" oninput="genUpdate()">
                        <span class="form-hint">byo: required — MiB (32 GB → 32768)</span>
                    </div>
                    <div class="form-group">
                        <label>VMID <span class="form-hint">(optional)</span></label>
                        <input type="number" id="gen-vmid" min="100" placeholder="e.g. 9201" oninput="genUpdate()">
                        <span class="form-hint">pick from <code>pct list</code>+<code>qm list</code>; blank = default/auto</span>
                    </div>
                    <div class="form-group">
                        <label>Node <span class="form-hint">(optional)</span></label>
                        <input type="text" id="gen-node" placeholder="e.g. pve1" oninput="genUpdate()">
                        <span class="form-hint">required only on a multi-node cluster</span>
                    </div>
                    <div class="form-group">
                        <label>ACL storages <span class="form-hint">(optional)</span></label>
                        <input type="text" id="gen-acl" placeholder='e.g. local local-lvm' oninput="genUpdate()">
                        <span class="form-hint">grant the token write access on exactly these storages</span>
                    </div>
                    <div class="form-group">
                        <label>Operator pubkey file <span class="form-hint">(optional)</span></label>
                        <input type="text" id="gen-pubkey" placeholder="/path/to/operator-keys" oninput="genUpdate()">
                        <span class="form-hint">arm self-update from day-0</span>
                    </div>
                    <div class="form-group">
                        <label>Preserve state from <span class="form-hint">(optional)</span></label>
                        <input type="text" id="gen-preserve" placeholder="/var/lib/felhom-agent.old" oninput="genUpdate()">
                        <span class="form-hint">reinstall keeping the leaf pin stable</span>
                    </div>
                </div>
                <div class="gen-checks">
                    <label class="gen-check"><input type="checkbox" id="gen-dry" onchange="genUpdate()"> <code>--dry-run</code> <span class="form-hint">print, don't execute</span></label>
                    <label class="gen-check"><input type="checkbox" id="gen-preflight" onchange="genUpdate()"> <code>--preflight-only</code> <span class="form-hint">checks + verdict, no install</span></label>
                    <label class="gen-check"><input type="checkbox" id="gen-skip" onchange="genUpdate()"> <code>--skip-provision</code> <span class="form-hint">agent only, no guest</span></label>
                    <label class="gen-check"><input type="checkbox" id="gen-leaf" onchange="genUpdate()"> <code>--allow-new-leaf</code> <span class="form-hint" id="gen-leaf-warn"></span></label>
                </div>
            </div>

            <p class="gen-msg" id="gen-msg" style="display:none;"></p>

            <h3 style="margin-top:1rem;">Option 1: Online install (recommended)</h3>
            <p class="text-muted" style="margin: 0 0 0.4rem; font-size: 0.8rem;">
                Download-then-run (not <code>curl | sudo bash</code>) so you can inspect the script first —
                the right default for a sovereignty product. The passphrase is entered at the no-echo prompt.
            </p>
            <div class="credential-box">
                <code id="cmd-online">curl -fsSL https://felhom.eu/scripts/felhom-host-install.sh -o felhom-host-install.sh \
  &amp;&amp; sudo bash felhom-host-install.sh --customer-id {{.CustomerID}} --mode &lt;appliance|byo&gt;</code>
                <button type="button" class="copy-btn" onclick="copyText('cmd-online')" title="Copy">&#x2398;</button>
            </div>

            <h3 style="margin-top: 1rem;">Option 2: Local install (script already on host)</h3>
            <div class="credential-box">
                <code id="cmd-setup">sudo ./felhom-host-install.sh --customer-id {{.CustomerID}} --mode &lt;appliance|byo&gt;</code>
                <button type="button" class="copy-btn" onclick="copyText('cmd-setup')" title="Copy">&#x2398;</button>
            </div>

            <h3 style="margin-top: 1rem;">Option 3: Manual config fetch (debug only)</h3>
            <p class="text-muted" style="margin: 0 0 0.4rem; font-size: 0.8rem;">The same payload the controller pulls itself — for inspection, not normal provisioning. Replace the placeholder with the Retrieval Password above (Reveal to see it) — it is intentionally NOT baked into this command.</p>
            <div class="credential-box">
                <code id="cmd-curl">curl -fsSL https://hub.felhom.eu/api/v1/config/{{.CustomerID}} -H "X-Retrieval-Password: &lt;YOUR-RETRIEVAL-PASSWORD&gt;" -o controller.yaml</code>
                <button type="button" class="copy-btn" onclick="copyText('cmd-curl')" title="Copy">&#x2398;</button>
            </div>
        </section>

        <section class="card">
            <h2>YAML Preview</h2>
            <div id="yaml-preview" class="yaml-preview">
                <p class="text-muted">Loading preview...</p>
            </div>
        </section>
        {{else}}
        <section class="card"><p class="text-muted">No managed config yet — create one from the Overview tab to get setup commands.</p></section>
        {{end}}

        </div>

        <!-- ═══ Edit ═══ (v0.48.0 edit-a: the standalone config form embedded via the shared
             config_form_body sub-template, plus the relocated Block/Delete danger zone. The
             Controller Update / Geo / Danger zone cards are SIBLINGS after </form> — nesting
             them inside the config <form> is invalid HTML and breaks the formaction sub-buttons. -->
        <div class="tab-panel" data-tab="edit">

        {{if .HasConfig}}
        {{template "config_form_body" .ConfigForm}}
        {{else}}
        <section class="card"><p class="text-muted">No managed config yet — create one with the Create Config button above.</p></section>
        {{end}}

        {{if .HasReports}}
        <!-- Controller Update -->
        <section class="card">
            <h2>Controller Update</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Controller version</span>
                    <span class="value">{{.Customer.ControllerVersion}}</span>
                </div>
                {{if .LatestVersion}}
                <div class="info-item">
                    <span class="label">Registry latest</span>
                    <span class="value">
                        v{{.LatestVersion}}
                        {{if .UpdateAvailable}}
                        <span style="color: var(--blue-bright); margin-left: 0.3em;">● update available</span>
                        {{else}}
                        <span style="color: var(--text-2); margin-left: 0.3em;">— up to date</span>
                        {{end}}
                    </span>
                </div>
                {{end}}
                {{if .ControllerURL}}
                <div class="info-item">
                    <span class="label">Controller URL</span>
                    <span class="value"><a href="{{.ControllerURL}}" target="_blank" style="color: var(--blue-bright);">{{.ControllerURL}}</a></span>
                </div>
                {{end}}
            </div>
            <!-- Phase 2 managed-update floor (operator-enforced minimum) -->
            <div class="info-grid" style="margin-top: 0.75rem; border-top: 1px solid var(--line); padding-top: 0.75rem;">
                <div class="info-item">
                    <span class="label">Effective floor (min. version)</span>
                    <span class="value">
                        {{if .EffectiveFloor}}v{{.EffectiveFloor}}
                            {{if .BelowFloor}}<span style="color: #f59e0b; margin-left: 0.3em;">● below floor — will auto-update</span>
                            {{else}}<span style="color: var(--text-2); margin-left: 0.3em;">— at/above floor</span>{{end}}
                        {{else}}<span style="color: var(--text-2);">none (Phase 2 inert)</span>{{end}}
                    </span>
                </div>
                <div class="info-item">
                    <span class="label">Global floor</span>
                    <span class="value">{{if .GlobalFloor}}v{{.GlobalFloor}}{{else}}<span style="color: var(--text-2);">unset</span>{{end}}</span>
                </div>
            </div>
            <form method="POST" action="/customers/{{.CustomerID}}/floor" style="margin-top: 0.5rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
                {{.CSRFField}}
                <label style="font-size: 0.85em; color: #cbd5e1;">Per-customer override</label>
                <input type="text" name="min_controller_version" value="{{.FloorOverride}}" placeholder="e.g. 0.87.0 (blank = use global)" style="padding: 0.3em 0.5em; font-size: 0.85em; width: 14em;">
                <button class="btn btn-outline btn-sm" type="submit">Save floor</button>
                <span style="font-size: 0.8em; color: var(--text-2);">Boxes below the effective floor auto-update on their next report. Blank clears the override.</span>
            </form>
            <p class="text-muted" style="margin-top: 0.75em; font-size: 0.8em;">
                Controller updates are agent-driven (the version floor above) and config is delivered by the
                box pulling it on a config change — the hub never connects into the box. Edit the config in
                the form above; the controller re-pulls and restarts on its next report.
            </p>
        </section>

        <!-- Geo-restriction -->
        {{with .Report.geo_restriction}}
        <section class="card">
            <h2>Geo-korlátozás</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Állapot</span>
                    <span class="value">
                        {{if index . "enabled"}}
                            <span class="severity-badge severity-critical">Aktív</span>
                        {{else}}
                            <span class="severity-badge severity-ok">Inaktív</span>
                        {{end}}
                    </span>
                </div>
                {{if index . "enabled"}}
                <div class="info-item">
                    <span class="label">Engedélyezett országok</span>
                    <span class="value">
                        {{$countries := index . "allowed_countries"}}
                        {{if $countries}}
                            {{range $i, $c := $countries}}{{if $i}}, {{end}}{{$c}}{{end}}
                        {{else}}
                            —
                        {{end}}
                    </span>
                </div>
                {{end}}
                {{if index . "last_sync"}}
                <div class="info-item">
                    <span class="label">Utolsó szinkron</span>
                    <span class="value">{{index . "last_sync"}}</span>
                </div>
                {{end}}
                {{if index . "last_sync_error"}}
                <div class="info-item">
                    <span class="label">Szinkron hiba</span>
                    <span class="value" style="color: var(--crit)">{{index . "last_sync_error"}}</span>
                </div>
                {{end}}
            </div>
            {{$overrides := index . "app_overrides"}}
            {{if $overrides}}
            <h3 style="margin-top: 1rem; font-size: 0.95rem;">Alkalmazás felülírások</h3>
            <table class="data-table" style="margin-top: 0.5rem;">
                <thead><tr><th>Alkalmazás</th><th>Engedélyezett országok</th></tr></thead>
                <tbody>
                {{range $app, $override := $overrides}}
                <tr>
                    <td>{{$app}}</td>
                    <td>
                        {{$ac := index $override "allowed_countries"}}
                        {{if $ac}}{{range $i, $c := $ac}}{{if $i}}, {{end}}{{$c}}{{end}}{{else}}—{{end}}
                    </td>
                </tr>
                {{end}}
                </tbody>
            </table>
            {{end}}
            {{if index . "enabled"}}
            <div style="margin-top: 1rem;">
                <button class="btn btn-danger" id="btn-geo-disable" onclick="disableGeo('{{$.Customer.CustomerID}}')">Összes geo-korlátozás eltávolítása</button>
                <span id="geo-msg" style="display:none; margin-left: 0.75rem;"></span>
            </div>
            {{end}}
        </section>
        {{end}}
        {{else}}
        <section class="card"><p class="text-muted">Controller update and geo-restriction settings appear once the first report arrives.</p></section>
        {{end}}

        {{if .HasConfig}}
        <!-- Reset customer (v0.61.0): the MIDDLE lifecycle tier — host delete < RESET < customer Delete.
             One action returns the customer to pre-first-install: every OPERATIONAL trace dies (offsite
             repo, PBS namespace, DR recipe, one-time secret, claim state, retained escrow custody), while
             IDENTITY and the basic config SURVIVE. Amber (--warn), deliberately distinct from the red
             Danger-zone Delete below it. Refuses while any host row exists (delete hosts first). -->
        <section class="card" style="border-color: var(--warn);">
            <h2>Ügyfél-visszaállítás <span class="text-muted" style="font-size: 0.8em; font-weight: normal;">(RESET — pre-első-telepítés)</span></h2>
            <p class="text-muted">Egyetlen művelettel visszaállítja az ügyfelet az első telepítés előtti állapotba: <strong>minden működési állapot törlődik</strong> (offsite tároló, PBS névtér, DR-recept, egyszeri jelszó, azonosítási állapot). Az <strong>azonosság és az alapkonfiguráció megmarad</strong> (ügyfélrekord, előzmények, események). Ez NEM törli a hostokat — ha még van host, előbb azt kell törölni. Kevesebb, mint a Danger zone Delete: az ügyfél megmarad, csak a működési nyomok tűnnek el.</p>
            <button type="button" class="btn btn-sm" style="border-color: var(--warn); color: var(--warn);" onclick="customerResetConfirm('{{.CustomerID}}')">Ügyfél visszaállítása&hellip;</button>
            <div id="cust-reset-confirm-{{.CustomerID}}" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--warn); background: var(--warn-dim); border-radius: var(--radius); max-width: 46em;">
                <p id="cust-reset-inv-{{.CustomerID}}" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
                <label id="cust-reset-escrow-row-{{.CustomerID}}" style="display: none; margin: 0 0 0.6rem; font-size: 0.85em; color: var(--crit);">
                    <input type="checkbox" id="cust-reset-escrow-{{.CustomerID}}">
                    <strong>Megőrzött helyreállítási-kulcs letét megsemmisítése</strong> — külön megerősítés (ez visszafordíthatatlanul törli a megőrzött escrow blobokat).
                </label>
                <p style="margin: 0 0 0.4rem; font-size: 0.85em; color: var(--text-2);">Írd be az ügyfél azonosítóját a megerősítéshez:</p>
                <form method="POST" action="/configs/{{.CustomerID}}/reset" id="cust-reset-form-{{.CustomerID}}" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
                    {{.CSRFField}}
                    <input type="hidden" name="confirm_id" id="cust-reset-confirm-hidden-{{.CustomerID}}" value="">
                    <input type="hidden" name="escrow_ack" id="cust-reset-escrow-hidden-{{.CustomerID}}" value="">
                    <input type="text" id="cust-reset-input-{{.CustomerID}}" placeholder="ügyfél-azonosító&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
                    <button type="button" class="btn btn-sm" id="cust-reset-go-{{.CustomerID}}" style="border-color: var(--warn); color: var(--warn);" onclick="customerResetSubmit('{{.CustomerID}}')">Megerősítés &amp; visszaállítás</button>
                    <button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('cust-reset-confirm-{{.CustomerID}}').style.display='none';">Mégse</button>
                </form>
                <p id="cust-reset-err-{{.CustomerID}}" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
            </div>
        </section>
        <script>
        function customerResetConfirm(cid) {
            var box = document.getElementById('cust-reset-confirm-' + cid);
            var inv = document.getElementById('cust-reset-inv-' + cid);
            var go = document.getElementById('cust-reset-go-' + cid);
            document.getElementById('cust-reset-input-' + cid).value = '';
            document.getElementById('cust-reset-err-' + cid).textContent = '';
            box.style.display = 'block';
            inv.textContent = 'Leltár lekérése…';
            go.disabled = false;
            fetch('/configs/' + encodeURIComponent(cid) + '/reset')
                .then(function(r){ return r.json(); })
                .then(function(d){
                    if (d.refused) {
                        inv.innerHTML = '<strong style="color: var(--crit)">Elutasítva:</strong> ehhez az ügyfélhez még ' + d.host_count +
                            ' host tartozik. A RESET soha nem töröl hostot — előbb töröld a host(oka)t.';
                        go.disabled = true;
                        document.getElementById('cust-reset-escrow-row-' + cid).style.display = 'none';
                        return;
                    }
                    var dies = [];
                    if (d.offsite_enabled) dies.push('offsite tároló' + (d.offsite_identifier ? ' (' + d.offsite_identifier + ')' : ''));
                    if (d.pbs_tenancy_configured) dies.push('PBS névtér + mentések');
                    if (d.dr_recipe_present) dies.push('DR-recept');
                    if (d.one_time_secret) dies.push('egyszeri jelszó');
                    if (d.claim_present) dies.push('azonosítási állapot (friss kód a következő onboardingnál)');
                    if (d.superseded_blobs > 0) dies.push(d.superseded_blobs + ' megőrzött escrow blob');
                    inv.innerHTML = '<strong>Törlődik:</strong> ' + (dies.length ? dies.join(', ') : 'nincs működési állapot') +
                        '. <strong>Megmarad:</strong> ügyfélrekord, alapkonfiguráció, előzmények, események.';
                    var escrowRow = document.getElementById('cust-reset-escrow-row-' + cid);
                    escrowRow.style.display = d.escrow_ack_required ? 'block' : 'none';
                    document.getElementById('cust-reset-escrow-' + cid).checked = false;
                })
                .catch(function(){ inv.textContent = 'A leltár nem kérhető le — a szerver minden feltételt így is kikényszerít.'; });
        }
        function customerResetSubmit(cid) {
            var typed = document.getElementById('cust-reset-input-' + cid).value.trim();
            var err = document.getElementById('cust-reset-err-' + cid);
            if (typed !== cid) { err.textContent = 'A beírt azonosító nem egyezik.'; return; }
            var escrowRow = document.getElementById('cust-reset-escrow-row-' + cid);
            var escrowCb = document.getElementById('cust-reset-escrow-' + cid);
            if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
                err.textContent = 'A megőrzött kulcs-letét megsemmisítéséhez pipáld be a külön megerősítést.';
                return;
            }
            document.getElementById('cust-reset-confirm-hidden-' + cid).value = typed;
            document.getElementById('cust-reset-escrow-hidden-' + cid).value = escrowCb.checked ? '1' : '';
            document.getElementById('cust-reset-form-' + cid).submit();
        }
        </script>
        {{end}}

        <!-- Danger zone (v0.48.0 edit-a): the Block/Delete forms relocated verbatim from the
             Customer Info header — endpoints and confirm() handlers unchanged.
             v0.70.1: gated on .Deletable, NOT .HasConfig — a GHOST customer (config gone, hosts or
             report/telemetry residue remain) must render the Delete button, or the whole v0.70.0
             ghost-delete path is dead UI (found live on demo-vm-felhom). RESET stays HasConfig-gated
             above (identity-preserving re-onboarding — a ghost has no identity to preserve). -->
        {{if .Deletable}}
        <section class="card">
            <h2>Danger zone</h2>
            <p class="text-muted">{{if not .HasConfig}}<strong style="color: var(--warn);">Ghost customer</strong> — the configuration record is already gone; Delete is the applicable action. {{end}}Blocking hides the customer from the Dashboard (reports are still accepted). <strong>Delete customer</strong> is the full offboarding teardown (v0.69.0): it deletes the host(s), then RESETs the customer (offsite repository destroyed, PBS credentials revoked, tunnel and zone removed), then purges the customer record and all escrow ciphertext — including the <strong>retained recovery-key custody</strong> for this customer's hosts. This is the one true purge point; host deletion only demotes custody, never destroys it. Three acknowledgements and the typed customer-id are required. For identity-preserving re-onboarding use <em>Ügyfél-visszaállítás (RESET)</em> above instead.</p>
            <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
                {{if .HasConfig}}{{/* v0.70.1: blocking gates dashboard visibility of a CONFIGURED customer — meaningless for a ghost */}}
                {{if .IsBlocked}}
                <form method="POST" action="/customers/{{.CustomerID}}/unblock" style="display:inline">
                    {{.CSRFField}}
                    <button type="submit" class="btn btn-sm">Unblock</button>
                </form>
                {{else}}
                <form method="POST" action="/customers/{{.CustomerID}}/block" style="display:inline">
                    {{.CSRFField}}
                    <button type="submit" class="btn btn-outline btn-sm" data-confirm="Block this customer? They will be hidden from the Dashboard.">Block</button>
                </form>
                {{end}}
                {{end}}
                <button type="button" class="btn btn-danger btn-sm" onclick="customerDeleteOpen('{{.CustomerID}}')">Delete customer&hellip;</button>
            </div>

            <!-- Guided full-teardown cascade (v0.69.0, R-25b). The inventory panel is fetched from
                 GET /configs/{id}/delete; the three acknowledgements + typed customer-id are ALSO
                 enforced server-side (this is convenience, never the gate). -->
            <div id="cust-del-box-{{.CustomerID}}" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); border-radius: var(--radius); max-width: 52em;">
                <p id="cust-del-inv-{{.CustomerID}}" style="margin: 0 0 0.75rem; font-size: 0.9em;">&hellip;</p>
                <div id="cust-del-journal-{{.CustomerID}}" style="display: none; margin: 0 0 0.75rem; padding: 0.5rem; border: 1px solid var(--warn); font-size: 0.85em;"></div>
                <label style="display: block; margin: 0 0 0.5rem; font-size: 0.85em;">
                    <input type="checkbox" id="cust-del-ack1-{{.CustomerID}}">
                    <strong>1.</strong> <span id="cust-del-ack1-text-{{.CustomerID}}">The host(s) will be deleted</span> — recovery-key custody is <strong>demoted</strong> to retained custody, not destroyed.
                </label>
                <label style="display: block; margin: 0 0 0.5rem; font-size: 0.85em;">
                    <input type="checkbox" id="cust-del-ack2-{{.CustomerID}}">
                    <strong>2.</strong> The customer will be <strong>RESET</strong> — the offsite repository is <strong>DESTROYED</strong>, PBS credentials are revoked, tunnel and zone are removed.
                </label>
                <label style="display: block; margin: 0 0 0.75rem; font-size: 0.85em; color: var(--crit);">
                    <input type="checkbox" id="cust-del-ack3-{{.CustomerID}}">
                    <strong>3.</strong> The customer record and <strong>ALL escrow ciphertext</strong> are <strong>PURGED</strong> — unrecoverable.
                </label>
                <p style="margin: 0 0 0.4rem; font-size: 0.85em; color: var(--text-2);">Type the customer-id to confirm:</p>
                <form method="POST" action="/configs/{{.CustomerID}}/delete" id="cust-del-form-{{.CustomerID}}" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
                    {{.CSRFField}}
                    <input type="hidden" name="confirm_id" id="cust-del-cid-{{.CustomerID}}" value="">
                    <input type="hidden" name="ack_hosts" id="cust-del-h1-{{.CustomerID}}" value="">
                    <input type="hidden" name="ack_reset" id="cust-del-h2-{{.CustomerID}}" value="">
                    <input type="hidden" name="ack_purge" id="cust-del-h3-{{.CustomerID}}" value="">
                    <input type="hidden" name="expect_hosts" id="cust-del-exp-{{.CustomerID}}" value="">
                    <input type="text" id="cust-del-input-{{.CustomerID}}" placeholder="customer-id&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
                    <button type="button" class="btn btn-danger btn-sm" id="cust-del-go-{{.CustomerID}}" onclick="customerDeleteSubmit('{{.CustomerID}}')">Confirm &amp; delete everything</button>
                    <button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('cust-del-box-{{.CustomerID}}').style.display='none';">Cancel</button>
                </form>
                <p id="cust-del-err-{{.CustomerID}}" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
            </div>
        </section>
        <script>
        function customerDeleteOpen(cid) {
            var box = document.getElementById('cust-del-box-' + cid);
            var inv = document.getElementById('cust-del-inv-' + cid);
            var go = document.getElementById('cust-del-go-' + cid);
            var jr = document.getElementById('cust-del-journal-' + cid);
            box.style.display = 'block';
            inv.textContent = 'Loading inventory…';
            jr.style.display = 'none';
            document.getElementById('cust-del-input-' + cid).value = '';
            document.getElementById('cust-del-err-' + cid).textContent = '';
            document.getElementById('cust-del-exp-' + cid).value = '';
            ['ack1', 'ack2', 'ack3'].forEach(function(a) { document.getElementById('cust-del-' + a + '-' + cid).checked = false; });
            go.disabled = false;
            fetch('/configs/' + encodeURIComponent(cid) + '/delete')
                .then(function(r){ return r.json(); })
                .then(function(d){
                    document.getElementById('cust-del-exp-' + cid).value = String(d.host_count);
                    var hostNames = (d.hosts || []).map(function(h){ return h.host_id + ' (' + h.status + ')'; });
                    document.getElementById('cust-del-ack1-text-' + cid).textContent =
                        d.host_count + ' host(s) will be deleted' + (hostNames.length ? ': ' + hostNames.join(', ') : '');
                    var dies = [];
                    if (d.host_count) dies.push(d.host_count + ' host row(s)');
                    if (d.offsite_enabled) dies.push('offsite repository' + (d.offsite_identifier ? ' (' + d.offsite_identifier + ')' : ''));
                    if (d.pbs_tenancy_configured) dies.push('PBS namespace + backups + token');
                    if (d.dr_recipe_present) dies.push('DR recipe');
                    if (d.one_time_secret) dies.push('one-time password');
                    if (d.claim_present) dies.push('claim state');
                    if (d.residue && d.residue.reports) dies.push(d.residue.reports + ' report row(s)');
                    if (d.residue && d.residue.app_telemetry) dies.push(d.residue.app_telemetry + ' app-telemetry row(s)');
                    if (d.residue && d.residue.appliance_registrations) dies.push('appliance registration (token)');
                    if (d.residue && d.residue.selfbind_tokens) dies.push('self-bind token(s)');
                    if (d.residue && d.residue.notification_prefs) dies.push('notification preferences');
                    dies.push('customer record');
                    var custody = d.superseded_blobs > 0
                        ? d.superseded_blobs + ' retained escrow blob(s) + every current host escrow'
                        : 'every current host escrow';
                    inv.innerHTML = '<strong>Will be destroyed:</strong> ' + dies.join(', ') +
                        '. <strong>Custody:</strong> ' + custody + ' (purged in the final leg). ' +
                        '<strong>Survives:</strong> the audit event stream, the notification log and the deletion provenance.';
                    if (d.has_config === false) {
                        inv.innerHTML = '<strong style="color: var(--warn)">Ghost customer:</strong> the configuration record ' +
                            'is already gone, but ' + d.residue_total + ' row(s) of report/telemetry state keep it on the ' +
                            'Customers list and keep it raising staleness alerts. This clears it.<br>' + inv.innerHTML;
                    }
                    if (d.online_host_present) {
                        inv.innerHTML += '<br><strong style="color: var(--crit)">Refused:</strong> a host is ONLINE. ' +
                            'Decommission the box first — the cascade never deletes a live host.';
                        go.disabled = true;
                    }
                    if (d.pending_journal) {
                        var legs = d.pending_journal.legs || {};
                        var parts = Object.keys(legs).map(function(k){ return k + '=' + legs[k]; });
                        jr.innerHTML = '<strong>An earlier cascade stopped mid-way</strong> (journal #' + d.pending_journal.id +
                            ', started ' + d.pending_journal.started_at + '). Legs: ' + (parts.length ? parts.join(', ') : 'none recorded') +
                            '. Confirming again RESUMES it — completed legs are no-ops.';
                        jr.style.display = 'block';
                        document.getElementById('cust-del-go-' + cid).textContent = 'Confirm & resume teardown';
                    }
                })
                .catch(function(){ inv.textContent = 'Inventory unavailable — the server enforces every gate regardless.'; });
        }
        function customerDeleteSubmit(cid) {
            var err = document.getElementById('cust-del-err-' + cid);
            var a1 = document.getElementById('cust-del-ack1-' + cid).checked;
            var a2 = document.getElementById('cust-del-ack2-' + cid).checked;
            var a3 = document.getElementById('cust-del-ack3-' + cid).checked;
            if (!a1 || !a2 || !a3) { err.textContent = 'All three acknowledgements are required.'; return; }
            var typed = document.getElementById('cust-del-input-' + cid).value.trim();
            if (typed !== cid) { err.textContent = 'The typed customer-id does not match.'; return; }
            document.getElementById('cust-del-cid-' + cid).value = typed;
            document.getElementById('cust-del-h1-' + cid).value = '1';
            document.getElementById('cust-del-h2-' + cid).value = '1';
            document.getElementById('cust-del-h3-' + cid).value = '1';
            document.getElementById('cust-del-form-' + cid).submit();
        }
        </script>
        {{end}}

        </div>

        <!-- ═══ Backup & DR ═══ -->
        <div class="tab-panel" data-tab="backup">

        {{if .HasReports}}
        {{if .HasDRRecipe}}
        <!-- DR recipe (secret-free reconstruction recipe) -->
        <section class="card">
            <h2>DR Recipe <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(secret-free reconstruction plan)</span></h2>
            <p class="text-muted" style="margin-top: 0;">
                The non-secret re-provision plan — guest sizing, drive inventory (durable-id → role → mount → intent),
                PVE storage defs, PBS coordinates, and app inventory + storage bindings. It complements escrow (keys)
                and PBS/restic (bytes): <strong>it contains no key, password, or token</strong>. Use it to rebuild the
                host/guest/storage scaffolding before the PBS bytes land.
            </p>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Storage / guest / PBS half (agent)</span>
                    <span class="value">{{if .DRRecipeHasHost}}<span class="badge badge-ok">present</span>{{else}}<span class="badge badge-warn">awaiting host-report</span>{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Customer / apps half (controller)</span>
                    <span class="value">{{if .DRRecipeHasApps}}<span class="badge badge-ok">present</span>{{else}}<span class="badge badge-warn">awaiting controller report</span>{{end}}</span>
                </div>
                <div class="info-item">
                    <span class="label">Last updated</span>
                    <span class="value">{{if .DRRecipeUpdatedAt}}{{.DRRecipeUpdatedAt}}{{else}}—{{end}}</span>
                </div>
            </div>
            <div style="margin-top: 1rem;">
                <a href="/customers/{{.CustomerID}}/dr-recipe.json" class="btn" download>Download recipe (JSON)</a>
            </div>
        </section>
        {{else}}
        <section class="card"><p class="text-muted">No DR recipe yet — it assembles from the host-report and controller report.</p></section>
        {{end}}
        {{else}}
        <section class="card"><p class="text-muted">Backup and DR data appear once the first report arrives.</p></section>
        {{end}}

        </div>

        <!-- ═══ Events ═══ -->
        <div class="tab-panel" data-tab="events">

        {{if .HasReports}}
        <!-- Events -->
        <section class="card">
            <h2>Events
                {{if .EventCounts}}
                    {{with mapGet .EventCounts "critical"}}<span class="severity-badge severity-critical">{{.}} critical</span>{{end}}
                    {{with mapGet .EventCounts "error"}}<span class="severity-badge severity-error">{{.}} error{{if gt . 1}}s{{end}}</span>{{end}}
                    {{with mapGet .EventCounts "warning"}}<span class="severity-badge severity-warning">{{.}} warning{{if gt . 1}}s{{end}}</span>{{end}}
                {{end}}
                <span class="text-muted" style="font-size: 0.7em; font-weight: normal;"> (last 24h)</span>
            </h2>
            {{if .Events}}
            <div style="margin-bottom: 0.5rem;">
                <button class="btn btn-sm btn-outline event-filter active" data-filter="all">All</button>
                <button class="btn btn-sm btn-outline event-filter" data-filter="error">Errors</button>
                <button class="btn btn-sm btn-outline event-filter" data-filter="warning">Warnings</button>
                <button class="btn btn-sm btn-outline event-filter" data-filter="info">Info</button>
            </div>
            <table class="history-table" id="events-table">
                <thead>
                    <tr>
                        <th>Time</th>
                        <th>Severity</th>
                        <th>Type</th>
                        <th>Message</th>
                        <th>Source</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .Events}}
                    <tr data-severity="{{.Severity}}">
                        <td title="{{.CreatedAt.Format "2006-01-02 15:04:05"}}">{{.CreatedAt.Format "Jan 02 15:04"}}</td>
                        <td><span class="severity-badge severity-{{.Severity}}">{{.Severity}}</span></td>
                        <td><code>{{.EventType}}</code></td>
                        <td>{{.Message}}</td>
                        <td>{{.Source}}</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            <script>
            document.querySelectorAll('.event-filter').forEach(btn => {
                btn.addEventListener('click', function() {
                    document.querySelectorAll('.event-filter').forEach(b => b.classList.remove('active'));
                    this.classList.add('active');
                    const filter = this.dataset.filter;
                    document.querySelectorAll('#events-table tbody tr').forEach(row => {
                        row.style.display = (filter === 'all' || row.dataset.severity === filter) ? '' : 'none';
                    });
                });
            });
            </script>
            {{else}}
            <p class="text-muted">No events recorded yet.</p>
            {{end}}
        </section>

        <!-- Report History -->
        {{if .History}}
        <section class="card">
            <h2>Report History (last 24h)</h2>
            <details>
                <summary>{{len .History}} reports</summary>
                <table class="history-table">
                    <thead>
                        <tr>
                            <th>Time</th>
                            <th>Status</th>
                            <th>CPU</th>
                            <th>Memory</th>
                        </tr>
                    </thead>
                    <tbody>
                        {{range .History}}
                        <tr>
                            <td>{{.ReceivedAt.Format "Jan 02 15:04"}}</td>
                            <td><span class="status-badge status-badge-{{.HealthStatus}}">{{.HealthStatus}}</span></td>
                            <td>{{formatFloat .CPUPercent}}%</td>
                            <td>{{formatFloat .MemoryPercent}}%</td>
                        </tr>
                        {{end}}
                    </tbody>
                </table>
            </details>
        </section>
        {{end}}
        {{else}}
        <section class="card"><p class="text-muted">Events appear once the first report arrives.</p></section>
        {{end}}

        </div>

        <!-- ═══ Notifications ═══ -->
        <div class="tab-panel" data-tab="notifications">

        {{if .HasReports}}
        <!-- Notifications -->
        <section class="card">
            <h2>Notifications</h2>
            <div class="info-grid">
                <div class="info-item">
                    <span class="label">Email</span>
                    <span class="value">{{if .NotifPrefs}}{{if .NotifPrefs.Email}}{{.NotifPrefs.Email}}{{else}}Not set{{end}}{{else}}Not configured{{end}}</span>
                </div>
                {{if .NotifPrefs}}
                <div class="info-item">
                    <span class="label">Events</span>
                    <span class="value">{{if .NotifPrefs.EnabledEvents}}{{joinStrings .NotifPrefs.EnabledEvents ", "}}{{else}}None{{end}}</span>
                </div>
                {{end}}
            </div>
            {{if .RecentNotifications}}
            <h3>Recent (last 10)</h3>
            <table class="history-table">
                <thead>
                    <tr>
                        <th>Time</th>
                        <th>Channel</th>
                        <th>Event</th>
                        <th>Status</th>
                        <th>Message</th>
                    </tr>
                </thead>
                <tbody>
                    {{range .RecentNotifications}}
                    <tr>
                        <td>{{.CreatedAt.Format "Jan 02 15:04"}}</td>
                        <td><span class="status-badge status-badge-{{.Channel}}">{{.Channel}}</span></td>
                        <td>{{.EventType}}</td>
                        <td><span class="status-badge status-badge-{{.Status}}">{{.Status}}</span></td>
                        <td>{{.Message}}</td>
                    </tr>
                    {{end}}
                </tbody>
            </table>
            {{end}}
        </section>
        {{else}}
        <section class="card"><p class="text-muted">Notification data appears once the first report arrives.</p></section>
        {{end}}

        </div>

        <!-- ═══ Host ═══ (a LIST by design — 1 host today, N for a later HA cluster) -->
        <div class="tab-panel" data-tab="host">
        {{if .Hosts}}
        {{range .Hosts}}
        <p style="margin: 0 0 0.5rem;"><a href="/hosts/{{.HostID}}" class="back-link">Open host page: {{.HostID}} &rarr;</a></p>
        {{template "host_detail_body" .}}
        {{end}}
        {{else}}
        <section class="card">
            <div class="empty-state" style="border: none;">
                <p>No host enrolled yet.</p>
                <p class="hint">A host appears here once it enrolls via the Day-0 bootstrap.</p>
            </div>
        </section>
        {{end}}
        </div>

        <footer>
            {{if .HasReports}}<p>Auto-refreshes every 60 seconds &middot; {{end}}<a href="/">Felhom Hub</a> {{hubVersion}}{{if .HasReports}}</p>{{end}}
        </footer>
    </div>

    <script>
    function copyText(elementId) {
        var el = document.getElementById(elementId);
        var text = el.textContent || el.innerText;
        navigator.clipboard.writeText(text.trim()).then(function() {
            var btn = el.parentElement.querySelector('.copy-btn');
            var orig = btn.innerHTML;
            btn.innerHTML = '&#x2713;';
            setTimeout(function() { btn.innerHTML = orig; }, 1500);
        });
    }

    // Masked-secret controls (retrieval password): the value lives in data-secret and is masked in
    // the visible node by default; Reveal toggles it, Copy copies the real value. It is never baked
    // into a copyable command (see the Option-3 placeholder).
    var _secretMask = '•'.repeat(16);
    function toggleSecret(elementId) {
        var el = document.getElementById(elementId);
        var btn = document.getElementById('reveal-' + elementId);
        if (el.dataset.revealed === '1') {
            el.textContent = _secretMask; el.dataset.revealed = '0';
            if (btn) btn.textContent = 'Reveal';
        } else {
            el.textContent = el.getAttribute('data-secret') || ''; el.dataset.revealed = '1';
            if (btn) btn.textContent = 'Hide';
        }
    }
    function copySecret(elementId) {
        var el = document.getElementById(elementId);
        var val = (el.getAttribute('data-secret') || '').trim();
        navigator.clipboard.writeText(val).then(function() {
            var btn = el.parentElement.querySelectorAll('.copy-btn');
            var b = btn[btn.length - 1]; var orig = b.innerHTML;
            b.innerHTML = '&#x2713;';
            setTimeout(function() { b.innerHTML = orig; }, 1500);
        });
    }

    // ── Install-command generator (GL-7) — client-side only; assembles a command from the controls
    // and only ever emits real host-install flags. Nothing is submitted.
    function genFlags() {
        // returns {flags: "<assembled>", err: "<message or empty>"}
        var mode = (document.querySelector('input[name="gen-mode"]:checked') || {}).value || '';
        if (!mode) return { flags: '', err: 'Pick a mode (appliance or byo) to generate the command.' };
        var f = ['--mode ' + mode];
        var cores = (document.getElementById('gen-cores').value || '').trim();
        var memory = (document.getElementById('gen-memory').value || '').trim();
        var isInt = function (v) { return v !== '' && /^[0-9]+$/.test(v) && parseInt(v, 10) > 0; };
        if (mode === 'byo') {
            if (!isInt(cores) || !isInt(memory)) {
                return { flags: '', err: 'byo mode requires --cores and --memory (noisy-neighbor caps on a host you do not own).' };
            }
            f.push('--cores ' + cores, '--memory ' + memory);
        } else {
            // appliance: caps optional; emit only if both are valid positive integers
            if (isInt(cores)) f.push('--cores ' + cores);
            if (isInt(memory)) f.push('--memory ' + memory);
        }
        var vmid = (document.getElementById('gen-vmid').value || '').trim();
        if (vmid !== '' && isInt(vmid)) f.push('--vmid ' + vmid);
        var node = (document.getElementById('gen-node').value || '').trim();
        if (node) f.push('--node ' + node);
        var acl = (document.getElementById('gen-acl').value || '').trim().replace(/\s+/g, ' ');
        if (acl) f.push('--acl-storages "' + acl + '"');
        var pub = (document.getElementById('gen-pubkey').value || '').trim();
        if (pub) f.push('--operator-pubkey-file ' + pub);
        var pre = (document.getElementById('gen-preserve').value || '').trim();
        if (pre) f.push('--preserve-state-from ' + pre);
        if (document.getElementById('gen-skip').checked) f.push('--skip-provision');
        if (document.getElementById('gen-dry').checked) f.push('--dry-run');
        if (document.getElementById('gen-preflight').checked) f.push('--preflight-only');
        if (document.getElementById('gen-leaf').checked) f.push('--allow-new-leaf');
        return { flags: f.join(' '), err: '' };
    }

    function genUpdate() {
        var ctrl = document.getElementById('gen-controls');
        if (!ctrl) return;
        var cid = ctrl.getAttribute('data-customer-id') || '';
        var mode = (document.querySelector('input[name="gen-mode"]:checked') || {}).value || '';
        // byo requires caps → show the required markers only in byo
        var reqOn = (mode === 'byo');
        document.querySelectorAll('.gen-req').forEach(function (e) { e.style.display = reqOn ? 'inline' : 'none'; });
        // allow-new-leaf inline warning
        var lw = document.getElementById('gen-leaf-warn');
        lw.textContent = document.getElementById('gen-leaf').checked
            ? '⚠ regenerates the agent leaf — every provisioned guest must then be re-bootstrapped' : '';

        var r = genFlags();
        var msg = document.getElementById('gen-msg');
        var online = document.getElementById('cmd-online');
        var local = document.getElementById('cmd-setup');
        var base = 'felhom-host-install.sh --customer-id ' + cid;
        if (r.err) {
            msg.textContent = r.err; msg.style.display = 'block';
            // Do NOT emit a runnable command — show the incomplete shape with a placeholder.
            online.textContent = 'curl -fsSL https://felhom.eu/scripts/felhom-host-install.sh -o felhom-host-install.sh \\\n  && sudo bash ' + base + ' --mode <appliance|byo>';
            local.textContent = 'sudo ./' + base + ' --mode <appliance|byo>';
            return;
        }
        msg.style.display = 'none';
        online.textContent = 'curl -fsSL https://felhom.eu/scripts/felhom-host-install.sh -o felhom-host-install.sh \\\n  && sudo bash ' + base + ' ' + r.flags;
        local.textContent = 'sudo ./' + base + ' ' + r.flags;
    }
    // initialize on load (also gives JS-enabled users the "pick a mode" prompt state)
    if (document.getElementById('gen-controls')) { genUpdate(); }

    function disableGeo(customerID) {
        var geoBtn = document.getElementById('btn-geo-disable');
        felhomConfirm(geoBtn, 'Összes geo-korlátozás eltávolítása? Ez közvetlenül törli a Cloudflare WAF szabályokat és értesíti a controllert.', function () {
        var btn = document.getElementById('btn-geo-disable');
        var msg = document.getElementById('geo-msg');
        btn.disabled = true;
        btn.textContent = 'Eltávolítás...';
        msg.style.display = 'none';
        fetch('/customers/' + customerID + '/geo/disable', {method: 'POST', headers: csrfHeaders()})
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (data.ok) {
                msg.textContent = data.message || 'Geo-korlátozás eltávolítva';
                msg.style.display = 'inline';
                msg.style.color = '#2EA8F5';
                setTimeout(function() { location.reload(); }, 2000);
            } else {
                msg.textContent = data.error || 'Hiba történt';
                msg.style.display = 'inline';
                msg.style.color = '#E5534B';
                btn.disabled = false;
                btn.textContent = 'Összes geo-korlátozás eltávolítása';
            }
        })
        .catch(function() {
            msg.textContent = 'Kapcsolódási hiba';
            msg.style.display = 'inline';
            msg.style.color = '#E5534B';
            btn.disabled = false;
            btn.textContent = 'Összes geo-korlátozás eltávolítása';
        });
        });
    }
    {{if .HasConfig}}
    // Load YAML preview
    fetch('/configs/{{.CustomerID}}/preview')
        .then(function(r) { return r.text(); })
        .then(function(yaml) {
            document.getElementById('yaml-preview').innerHTML = '<pre>' + yaml.replace(/&/g,'&amp;').replace(/</g,'&lt;') + '</pre>';
        })
        .catch(function() {
            document.getElementById('yaml-preview').innerHTML = '<p class="text-muted">Failed to load preview.</p>';
        });
    {{end}}
    </script>

    <script>
    // Client-side hash tabs (v0.47.0). Without JS this never runs — body never gets
    // .js-tabs, so every panel stays visible and the page reads top-to-bottom.
    // The 60s auto-refresh (location.reload) preserves the hash, so the active tab
    // survives a refresh; an unknown/absent hash falls back to Overview.
    (function() {
        var panels = document.querySelectorAll('.tab-panel');
        var links = document.querySelectorAll('#tab-nav a');
        if (!panels.length || !links.length) return;
        document.body.classList.add('js-tabs');
        var known = {};
        panels.forEach(function(p) { known[p.getAttribute('data-tab')] = true; });
        function currentTab() {
            var m = (location.hash || '').match(/^#tab=([a-z-]+)$/);
            var t = m ? m[1] : '';
            if (t === 'settings') t = 'edit'; // legacy alias: the Settings tab became Edit (v0.48.0)
            return known[t] ? t : 'overview';
        }
        function activate() {
            var tab = currentTab();
            panels.forEach(function(p) { p.classList.toggle('tab-panel-active', p.getAttribute('data-tab') === tab); });
            links.forEach(function(a) { a.classList.toggle('active', a.getAttribute('data-tab') === tab); });
        }
        window.addEventListener('hashchange', activate);
        activate();
    })();
    </script>

    {{if .HasReports}}
    <style>
    .auto-refresh-toggle {
        display: inline-flex;
        align-items: center;
        gap: 0.4rem;
        margin-left: 1rem;
        cursor: pointer;
        vertical-align: middle;
        font-size: 0.8rem;
        user-select: none;
    }
    .auto-refresh-toggle input { display: none; }
    .toggle-slider {
        position: relative;
        width: 32px;
        height: 18px;
        background: var(--bg-2);
        border-radius: 9px;
        transition: background 0.2s;
    }
    .toggle-slider::after {
        content: '';
        position: absolute;
        top: 2px;
        left: 2px;
        width: 14px;
        height: 14px;
        background: var(--text-2);
        border-radius: 50%;
        transition: transform 0.2s, background 0.2s;
    }
    .auto-refresh-toggle input:checked + .toggle-slider {
        background: #22c55e;
    }
    .auto-refresh-toggle input:checked + .toggle-slider::after {
        transform: translateX(14px);
        background: #fff;
    }
    .toggle-label { color: var(--text-2); }
    </style>
    <script>
    // Scoped auto-refresh (v0.48.0): the 60s reload fires ONLY while a live tab (the nav's
    // data-live-tabs list) is active AND no form on the page is dirty. Dirty = any input/change
    // anywhere (delegated listener, never reset — a reload resets it naturally). A skipped tick
    // reschedules, so switching back to a live tab resumes on the next tick. The toggle +
    // localStorage key + 60s cadence + default-on behavior are unchanged.
    (function() {
        var toggle = document.getElementById('autoRefreshToggle');
        if (!toggle) return;
        var key = 'hub_auto_refresh';
        var enabled = localStorage.getItem(key) !== 'off';
        var timer = null;
        var dirty = false;
        var nav = document.getElementById('tab-nav');
        var liveTabs = ((nav && nav.getAttribute('data-live-tabs')) || '').split(',');
        var hint = document.getElementById('refresh-paused-hint');

        toggle.checked = enabled;

        function activeTab() {
            var m = (location.hash || '').match(/^#tab=([a-z-]+)$/);
            var t = m ? m[1] : 'overview';
            if (t === 'settings') t = 'edit'; // legacy alias, same as the tabs script
            return t;
        }
        function onLiveTab() { return liveTabs.indexOf(activeTab()) !== -1; }
        function updateHint() {
            if (hint) hint.style.display = (!onLiveTab() || dirty) ? 'inline' : 'none';
        }
        function tick() {
            if (toggle.checked && onLiveTab() && !dirty) { location.reload(); return; }
            schedule(); // skipped (non-live tab or dirty form) — try again next tick
        }
        function schedule() {
            clearTimeout(timer);
            if (toggle.checked) timer = setTimeout(tick, 60000);
        }
        function markDirty(e) {
            if (e.target === toggle) return; // the auto-refresh toggle itself is not form input
            dirty = true;
            updateHint();
        }
        document.addEventListener('input', markDirty);
        document.addEventListener('change', markDirty);
        window.addEventListener('hashchange', updateHint);

        toggle.addEventListener('change', function() {
            localStorage.setItem(key, this.checked ? 'on' : 'off');
            updateHint();
            schedule();
        });

        updateHint();
        schedule();
    })();
    </script>
    {{end}}
</body>
</html>
