<h1>
  <%= p.name %>
  <% var isLaravel = p.runtime === 'laravel'; %>
  <span class="chip <%= pm2State === 'online' ? 'live' : (pm2State === 'unknown' ? 'queued' : 'failed') %>"><%= isLaravel ? 'fpm' : 'pm2' %>: <%= pm2State %></span>
  <% if (isLaravel) { %><span class="chip queued" title="Runtime">Laravel · PHP <%= info.PHP_VERSION || '?' %></span><% } %>
  <% if (isLaravel && (info.QUEUE_WORKER || 'off') === 'on') { %>
    <span class="chip <%= queueState === 'online' ? 'live' : 'failed' %>">queue: <%= queueState %></span>
  <% } %>
  <% if (history.length) { %>
    <a class="chip <%= history[0].status %>" href="/deployments/<%= history[0].id %>"><%= h.depLabel(history[0].status) %></a>
  <% } %>
</h1>

<% if (msg) { %><div class="flash"><%= msg %></div><% } %>
<% if (err) { %><div class="flash err"><%= err %></div><% } %>

<div class="card">
  <div class="hero">
    <div class="hero-snap">
      <% if (snapshotMtimeMs) { %>
        <% if (primaryDomain) { %><a href="https://<%= primaryDomain %>/" target="_blank" rel="noopener"><% } %>
          <img class="snapshot" src="/snapshots/<%= p.name %>.png?v=<%= snapshotMtimeMs %>" alt="Homepage snapshot of <%= p.name %>">
        <% if (primaryDomain) { %></a><% } %>
        <div class="muted" style="margin-top:8px">
          snapshot of current deployment · <span title="<%= h.utcMs(snapshotMtimeMs) %>"><%= h.relTimeMs(snapshotMtimeMs) %></span>
        </div>
      <% } else if (primaryDomain) { %>
        <div class="snapshot-placeholder muted">snapshot pending — reload shortly</div>
      <% } else { %>
        <div class="muted">No domain configured — snapshot unavailable.</div>
      <% } %>
    </div>
    <div class="hero-data">
      <h2>Overview</h2>
      <dl class="kv">
        <dt>Repository</dt><dd class="mono"><%= p.repo_url %></dd>
        <dt>Branch</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/branch" id="cc-branch-form"
            title="Branch used by the next deployment and matched against webhook pushes — changing it does not deploy by itself">
            <select name="branch" class="mono" style="max-width:220px" data-current="<%= p.branch %>"
              onchange="if (this.value !== this.dataset.current) this.form.submit()">
              <option value="<%= p.branch %>" selected><%= p.branch %></option>
            </select>
            <span class="muted" id="cc-branch-hint">loading branches…</span>
          </form>
          <script>
            // Fill the dropdown with every remote branch as soon as the page
            // loads (fetching on focus opened the list before it was filled).
            // If listing fails, fall back to a text field + Save so a branch
            // can still be set — it is validated server-side.
            (async function () {
              const form = document.getElementById('cc-branch-form');
              const sel = form.querySelector('select');
              const hint = document.getElementById('cc-branch-hint');
              const cur = sel.dataset.current;
              try {
                const r = await fetch('/projects/<%= p.name %>/branches');
                const names = await r.json();
                if (!Array.isArray(names) || !names.length) throw new Error('empty');
                if (!names.includes(cur)) names.unshift(cur);
                sel.innerHTML = '';
                for (const b of names) {
                  const o = document.createElement('option');
                  o.value = o.textContent = b;
                  if (b === cur) o.selected = true;
                  sel.appendChild(o);
                }
                hint.textContent = names.length + ' branches';
              } catch {
                const input = document.createElement('input');
                input.type = 'text'; input.name = 'branch'; input.className = 'mono';
                input.style.maxWidth = '220px'; input.required = true; input.autocomplete = 'off';
                input.spellcheck = false; input.pattern = '[A-Za-z0-9][A-Za-z0-9._/-]*'; input.value = cur;
                const btn = document.createElement('button');
                btn.type = 'submit'; btn.className = 'btn small hidden'; btn.textContent = 'Save';
                input.addEventListener('input', () => btn.classList.toggle('hidden', input.value.trim() === cur));
                sel.replaceWith(input);
                hint.replaceWith(btn);
                const note = document.createElement('span');
                note.className = 'muted'; note.textContent = ' listing remote branches failed — type a branch name';
                btn.after(note);
              }
            })();
          </script>
        </dd>
        <dt>Auto-deploy</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/autodeploy">
            <input type="hidden" name="auto_deploy" value="">
            <label class="switch" title="Deploy automatically when the repository webhook reports a push to the tracked branch">
              <input type="checkbox" <%= p.auto_deploy ? 'checked' : '' %>
                onchange="this.form.auto_deploy.value = this.checked ? 'on' : 'off'; this.form.submit()">
              <span class="slider"></span>
            </label>
            <span class="muted switch-label"><%= p.auto_deploy ? 'on — pushes to ' + p.branch + ' deploy' : 'off — manual deploys only' %></span>
          </form>
        </dd>
        <dt>Domains</dt>
        <dd>
          <% const domains = String(info.DOMAINS || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean); %>
          <% if (domains.length) { %>
            <% domains.forEach(function (d, i) { %><%= i ? ', ' : '' %><a href="https://<%= d %>" target="_blank" rel="noopener"><%= d %></a><% }) %>
          <% } else { %>—<% } %>
        </dd>
        <dt>Port</dt><dd><%= info.PORT || '—' %></dd>
        <dt>Cache</dt>
        <dd>
          <% if (isLaravel) { %>
            <span class="muted">off — Varnish is not available for Laravel apps (the edge strips cookies; sessions/CSRF would break)</span>
          <% } else { %>
          <form class="inline" method="post" action="/projects/<%= p.name %>/appconf">
            <input type="hidden" name="cache" value="">
            <label class="switch" title="Varnish edge cache">
              <input type="checkbox" <%= (info.CACHE || 'off') === 'on' ? 'checked' : '' %>
                onchange="this.form.cache.value = this.checked ? 'on' : 'off'; this.form.submit()">
              <span class="slider"></span>
            </label>
            <span class="muted switch-label"><%= (info.CACHE || 'off') === 'on' ? 'on (Varnish)' : 'off (direct)' %></span>
          </form>
          <% } %>
        </dd>
        <dt>Noindex</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/appconf">
            <input type="hidden" name="noindex" value="">
            <label class="switch" title="Block search engines">
              <input type="checkbox" <%= (info.NOINDEX || 'off') === 'on' ? 'checked' : '' %>
                onchange="if (!this.checked && !confirm('Allow search engines to index this app?')) { this.checked = true; return; } this.form.noindex.value = this.checked ? 'on' : 'off'; this.form.submit()">
              <span class="slider"></span>
            </label>
            <span class="muted switch-label"><%= (info.NOINDEX || 'off') === 'on' ? 'on (blocked)' : 'off (indexable)' %></span>
          </form>
        </dd>
        <% if (isLaravel) { %>
        <dt>Runtime</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/php" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
            <span>PHP-FPM pool <code>paas-<%= p.name %></code> ·</span>
            <select name="php_version" onchange="if (confirm('Switch this app to PHP ' + this.value + '? The pool moves immediately; caches are rebuilt on the next Restart/deploy.')) this.form.submit(); else this.value = '<%= info.PHP_VERSION || '' %>'">
              <% ((capabilities && capabilities.phpVersions) || []).forEach(function (pv) { %>
                <option value="<%= pv %>" <%= String(info.PHP_VERSION || '') === pv ? 'selected' : '' %>>PHP <%= pv %></option>
              <% }) %>
            </select>
            <span class="muted">health <code><%= info.HEALTH_PATH || '/up' %></code></span>
          </form>
        </dd>
        <% } else { %>
        <dt>Start command</dt><dd class="mono"><%= info.START_CMD || '—' %></dd>
        <% } %>
        <dt>Build command</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/build-cmd" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
            <input type="text" name="build_cmd" class="mono" value="<%= p.build_cmd %>" style="width:100%;max-width:420px" id="bc-input">
            <select onchange="if(this.value){document.getElementById('bc-input').value=this.value;this.selectedIndex=0}" style="max-width:200px">
              <option value="">preset…</option>
              <option value="npm ci && npm run build">Standard (npm ci)</option>
              <option value="npm ci --legacy-peer-deps && npm run build">Compatible (legacy peer deps)</option>
              <option value="rm -rf node_modules package-lock.json && npm install --legacy-peer-deps && npm run build">Clean install (regenerate lockfile)</option>
              <option value="composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader && if [ -f package.json ]; then npm ci && npm run build; fi">Laravel (composer + Vite if package.json)</option>
              <option value="composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader">Laravel (composer only)</option>
            </select>
            <button class="btn small" type="submit">Save</button>
          </form>
        </dd>
        <dt>Output dir</dt><dd class="mono"><%= p.output_dir %></dd>
        <dt>Last deployed</dt>
        <dd>
          <% if (lastLive && lastLive.finished_at) { %>
            <span title="<%= lastLive.finished_at %> UTC"><%= h.relTime(lastLive.finished_at) %></span>
          <% } else { %>never<% } %>
        </dd>
        <dt>Node.js</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/node">
            <select name="node_version" onchange="this.form.submit()">
              <% const curNode = String(info.NODE_VERSION || '').trim() || 'system'; %>
              <% (typeof nodeVersions !== 'undefined' ? nodeVersions : ['system']).forEach(function (nv) { %>
                <option value="<%= nv %>" <%= curNode === nv ? 'selected' : '' %>><%= nv === 'system' ? 'system (22)' : nv %></option>
              <% }) %>
            </select>
          </form>
        </dd>
        <dt>Multi-label</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/multilabel">
            <input type="hidden" name="multi_label" value="">
            <label class="switch" title="Multi-label app (one repo, NUXT_SITE-selected label)">
              <input type="checkbox" <%= p.multi_label ? 'checked' : '' %>
                onchange="this.form.multi_label.value = this.checked ? 'on' : 'off'; this.form.submit()">
              <span class="slider"></span>
            </label>
            <span class="muted switch-label"><%= p.multi_label ? 'on — label helpers active' : 'off' %></span>
          </form>
        </dd>
        <dt>Group</dt>
        <dd>
          <form class="inline" method="post" action="/projects/<%= p.name %>/group">
            <select name="group_name" onchange="this.form.submit()">
              <option value="">— none —</option>
              <% (groups || []).forEach(function (g) { %>
                <option value="<%= g %>" <%= p.group_name === g ? 'selected' : '' %>><%= g %></option>
              <% }) %>
            </select>
          </form>
        </dd>
        <dt>Created</dt><dd><%= h.fmtTime(p.created_at) %></dd>
      </dl>
    </div>
  </div>
</div>

<div class="card">
  <h2>Access</h2>
  <% const isEnabled = String(info.ENABLED || 'on') !== 'off'; %>
  <% const baUser = String(info.BASIC_AUTH_USER || '').trim(); %>
  <div class="actions" style="align-items:center">
    <span class="chip <%= isEnabled ? 'live' : 'failed' %>"><%= isEnabled ? 'active' : 'deactivated (503)' %></span>
    <form class="inline" method="post" action="/projects/<%= p.name %>/access"<% if (isEnabled) { %> onsubmit="return confirm('Deactivate this project? All domains answer 503 until re-enabled.')"<% } %>>
      <input type="hidden" name="action" value="<%= isEnabled ? 'disable' : 'enable' %>">
      <button class="btn small <%= isEnabled ? 'danger' : 'primary' %>" type="submit"><%= isEnabled ? 'Deactivate' : 'Activate' %></button>
    </form>
    <span class="chip <%= baUser ? 'live' : 'queued' %>"><%= baUser ? 'protected (user: ' + baUser + ')' : 'public' %></span>
    <% if (baUser) { %>
      <form class="inline" method="post" action="/projects/<%= p.name %>/access">
        <input type="hidden" name="action" value="unprotect">
        <button class="btn small" type="submit">Remove protection</button>
      </form>
    <% } %>
  </div>
  <form method="post" action="/projects/<%= p.name %>/access" style="margin-top:10px">
    <input type="hidden" name="action" value="protect">
    <div class="actions">
      <input type="text" name="ba_user" placeholder="viewer" value="<%= baUser %>" style="width:130px">
      <input type="password" name="ba_password" placeholder="password" style="width:150px">
      <button class="btn small" type="submit"><%= baUser ? 'Change protection' : 'Protect with password' %></button>
      <span class="muted">browser login in front of ALL domains — share credentials to give private access</span>
    </div>
  </form>
</div>

<div class="card-row-2">
<% if ((info.CACHE || 'off') === 'on') { %>
<div class="card">
  <h2>Cache</h2>
  <div class="actions" style="align-items:center">
    <form class="inline" method="post" action="/projects/<%= p.name %>/cache/purge" onsubmit="return confirm('Purge the whole Varnish cache for <%= p.name %>?')">
      <button class="btn" type="submit">Purge cache</button>
    </form>
    <form class="inline" method="post" action="/projects/<%= p.name %>/cache/warm">
      <button class="btn" type="submit">Warm cache</button>
    </form>
    <span class="muted">
      HIT rate today:
      <% const tc = trafficStats.today.hit + trafficStats.today.miss; %>
      <strong><%= tc ? Math.round(100 * trafficStats.today.hit / tc) + '%' : '—' %></strong>
      · 30 days: <strong><%= trafficStats.hitRate30 !== null ? trafficStats.hitRate30 + '%' : '—' %></strong>
    </span>
  </div>
</div>
<% } %>
<div class="card"<%- (info.CACHE || 'off') === 'on' ? '' : ' style="grid-column: 1 / -1"' %>>
  <h2>Actions</h2>
  <div class="actions">
    <form class="inline" method="post" action="/projects/<%= p.name %>/deploy">
      <button class="btn primary" type="submit">Deploy now</button>
    </form>
    <input id="zip-upload-input" type="file" accept=".zip,application/zip" style="display:none"
           onchange="uploadZipDeploy(this, '<%= p.name %>')">
    <button class="btn" type="button" id="zip-upload-btn"
            title="Deploy from a ZIP of the repository — fallback when the git host is down"
            onclick="document.getElementById('zip-upload-input').click()">Deploy from ZIP…</button>
    <progress id="zip-upload-progress" max="100" value="0" style="display:none;width:220px;vertical-align:middle"></progress>
    <script>
      function uploadZipXhr(url, file, onProgress) {
        return new Promise((resolve) => {
          const xhr = new XMLHttpRequest();
          xhr.open('POST', url);
          xhr.setRequestHeader('Content-Type', 'application/zip');
          xhr.upload.onprogress = (e) => {
            if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
          };
          xhr.onload = () => {
            let data = {};
            try { data = JSON.parse(xhr.responseText); } catch { /* non-JSON */ }
            resolve({ status: xhr.status, data });
          };
          xhr.onerror = () => resolve({ status: 0, data: {} });
          xhr.send(file);
        });
      }
      async function uploadZipDeploy(input, name) {
        const file = input.files && input.files[0];
        if (!file) return;
        input.value = '';
        const btn = document.getElementById('zip-upload-btn');
        const bar = document.getElementById('zip-upload-progress');
        btn.disabled = true;
        bar.style.display = 'inline-block';
        bar.value = 0;
        const mb = (file.size / 1048576).toFixed(1);
        btn.textContent = 'Uploading ' + mb + ' MB…';
        const { status, data } = await uploadZipXhr('/projects/' + name + '/upload-zip', file, (pct) => {
          bar.value = pct;
          btn.textContent = 'Uploading ' + mb + ' MB — ' + pct + ' %';
        });
        bar.style.display = 'none';
        if (status >= 200 && status < 300 && data.ok) { location.href = '/deployments/' + data.deployment; return; }
        alert('Upload failed: ' + (data.error || status || 'network error'));
        btn.disabled = false;
        btn.textContent = 'Deploy from ZIP…';
      }
    </script>
    <form class="inline" method="post" action="/projects/<%= p.name %>/restart" onsubmit="return confirm('<%= isLaravel ? 'Restart ' + p.name + '? Rebuilds the Laravel config/route/view caches from the current env and reloads PHP-FPM (graceful).' : 'Restart ' + p.name + ' via PM2 (picks up env changes)?' %>')">
      <button class="btn" type="submit">Restart app</button>
    </form>
    <form class="inline" method="post" action="/projects/<%= p.name %>/rollback" onsubmit="return confirm('Roll back <%= p.name %> to the previous release?')">
      <button class="btn" type="submit">Rollback</button>
    </form>
    <a class="btn" href="/projects/<%= p.name %>/logs">Runtime logs</a>
    <form class="inline" method="post" action="/projects/<%= p.name %>/remove" onsubmit="return confirm('Remove <%= p.name %> completely? This deletes the app, its releases, config and deployment history.')">
      <button class="btn danger" type="submit">Remove</button>
    </form>
  </div>
</div>
</div>

<% if (isLaravel) { %>
<div class="card">
  <h2>Laravel</h2>
  <dl class="kv">
    <dt>Migrate on deploy</dt>
    <dd>
      <form class="inline" method="post" action="/projects/<%= p.name %>/laravel">
        <input type="hidden" name="migrate" value="">
        <label class="switch" title="php artisan migrate --force after the health-gate, before the release goes live">
          <input type="checkbox" <%= (info.MIGRATE || 'on') === 'on' ? 'checked' : '' %>
            onchange="this.form.migrate.value = this.checked ? 'on' : 'off'; this.form.submit()">
          <span class="slider"></span>
        </label>
        <span class="muted switch-label"><%= (info.MIGRATE || 'on') === 'on' ? 'on — migrate --force runs after the health-gate; a failing migration fails the deploy (schema changes are not rolled back)' : 'off — run migrations yourself' %></span>
      </form>
    </dd>
    <dt>Scheduler</dt>
    <dd>
      <form class="inline" method="post" action="/projects/<%= p.name %>/laravel">
        <input type="hidden" name="scheduler" value="">
        <label class="switch" title="php artisan schedule:run every minute (systemd timer)">
          <input type="checkbox" <%= (info.SCHEDULER || 'off') === 'on' ? 'checked' : '' %>
            onchange="this.form.scheduler.value = this.checked ? 'on' : 'off'; this.form.submit()">
          <span class="slider"></span>
        </label>
        <span class="muted switch-label"><%= (info.SCHEDULER || 'off') === 'on' ? 'on — schedule:run every minute (log: storage/logs/scheduler.log)' : 'off' %></span>
      </form>
    </dd>
    <dt>Queue worker</dt>
    <dd>
      <form class="inline" method="post" action="/projects/<%= p.name %>/laravel">
        <input type="hidden" name="queue" value="">
        <label class="switch" title="php artisan queue:work under PM2, restarted gracefully on every deploy">
          <input type="checkbox" <%= (info.QUEUE_WORKER || 'off') === 'on' ? 'checked' : '' %>
            onchange="this.form.queue.value = this.checked ? 'on' : 'off'; this.form.submit()">
          <span class="slider"></span>
        </label>
        <span class="muted switch-label"><%= (info.QUEUE_WORKER || 'off') === 'on' ? 'on — PM2 process ' + p.name + '-queue (' + (queueState || 'absent') + '); deploys send queue:restart' : 'off' %></span>
      </form>
      <div class="hint mono" style="margin-top:4px"><%= info.QUEUE_CMD || 'php artisan queue:work --sleep=3 --tries=3 --max-time=3600' %></div>
    </dd>
    <dt>FPM max children</dt>
    <dd>
      <form class="inline" method="post" action="/projects/<%= p.name %>/laravel" style="display:flex;gap:6px;align-items:center">
        <input type="number" name="max_children" min="1" max="200" value="<%= info.FPM_MAX_CHILDREN || 8 %>" style="width:80px">
        <button class="btn small" type="submit">Save</button>
        <span class="muted">concurrent PHP workers for this app (ondemand pool, ~40–80 MB each)</span>
      </form>
    </dd>
  </dl>
  <div class="hint" style="margin-top:8px">
    Releases: the whole built checkout; <code>storage/</code> and <code>.env</code> are shared across releases; <code>config/route/view/event:cache</code> are built per release, then the release is health-gated through the real PHP-FPM pool before <code>current</code> flips. No PHP-FPM reload per deploy (opcache is keyed per release path).
  </div>
</div>
<% } %>

<% if (capabilities && capabilities.db) { %>
<div class="card">
  <h2>Database <span class="muted" style="font-weight:normal;font-size:13px">— MariaDB on this server, loopback only</span></h2>
  <% if (dbInfo) { %>
    <dl class="kv">
      <dt>Database</dt><dd class="mono"><%= dbInfo.database %> <% if (!dbInfo.managed) { %><span class="muted">(external — not provisioned here)</span><% } %></dd>
      <dt>Host</dt><dd class="mono"><%= dbInfo.host %>:<%= dbInfo.port %></dd>
      <dt>User</dt><dd class="mono"><%= dbInfo.username %></dd>
      <dt>Password</dt>
      <dd>
        <span class="mono" id="db-pw" data-pw="<%= dbInfo.password %>">••••••••••••</span>
        <button type="button" class="btn small" onclick="var e=document.getElementById('db-pw'); e.textContent = e.textContent.indexOf('•')===0 ? e.getAttribute('data-pw') : '••••••••••••'">Reveal</button>
        <button type="button" class="btn small" onclick="navigator.clipboard.writeText(document.getElementById('db-pw').getAttribute('data-pw'))">Copy</button>
      </dd>
      <dt>URL</dt>
      <dd>
        <span class="mono" id="db-url" data-url="<%= dbInfo.url %>">mysql://<%= dbInfo.username %>:••••@<%= dbInfo.host %>:<%= dbInfo.port %>/<%= dbInfo.database %></span>
        <button type="button" class="btn small" onclick="navigator.clipboard.writeText(document.getElementById('db-url').getAttribute('data-url'))">Copy</button>
      </dd>
    </dl>
    <div class="hint" style="margin:8px 0">
      From your workstation: <code>ssh -L 3306:127.0.0.1:3306 deploy@&lt;this server&gt;</code>, then connect any client to <code>localhost:3306</code> with these credentials. Nightly dumps in <code>/srv/paas/backups/db</code> (last 7 kept).
    </div>
    <div class="actions" style="align-items:center">
      <form class="inline" method="post" action="/projects/<%= p.name %>/db">
        <input type="hidden" name="action" value="backup">
        <button class="btn" type="submit">Backup now</button>
      </form>
      <% if (dbInfo.managed) { %>
      <form class="inline" method="post" action="/projects/<%= p.name %>/db" onsubmit="var c = prompt('This DROPS database <%= dbInfo.database %> and its user (a final dump is kept). Type the project name to confirm:'); if (c === null) return false; this.confirm.value = c; return true;">
        <input type="hidden" name="action" value="drop">
        <input type="hidden" name="confirm" value="">
        <button class="btn danger" type="submit">Drop database</button>
      </form>
      <% } %>
    </div>
  <% } else { %>
    <p class="muted">No database yet. Creates <code><%= p.name.replace(/-/g, '_') %></code> with a dedicated user and writes <code>DB_CONNECTION/DB_HOST/DB_PORT/DB_DATABASE/DB_USERNAME/DB_PASSWORD</code><%= isLaravel ? '' : ' and DATABASE_URL' %> into the environment.</p>
    <form class="inline" method="post" action="/projects/<%= p.name %>/db">
      <input type="hidden" name="action" value="create">
      <label class="muted" style="display:block;margin-bottom:6px"><input type="checkbox" name="force_env" value="1"> overwrite existing <code>DB_*</code> values in the env if present</label>
      <button class="btn primary" type="submit">Create database</button>
    </form>
  <% } %>
</div>
<% } %>

<div class="card">
  <h2>Domains</h2>
  <% const sysDomain = String(info.SYSTEM_DOMAIN || '').trim(); %>
  <% const customDomains = String(info.DOMAINS || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean); %>
  <% const redirDomains = String(info.REDIRECT_DOMAINS || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean); %>
  <% const allServing = customDomains.concat(sysDomain ? [sysDomain] : []); %>
  <% const declaredMain = String(info.MAIN_DOMAIN || '').trim(); %>
  <% const mainDomain = allServing.indexOf(declaredMain) >= 0 ? declaredMain : allServing[0]; %>
  <table>
    <thead><tr><th>Domain</th><th>Type</th><th title="used as the monitoring host name in Checkmk (uptime + cert); other domains get cert checks">Main</th><th></th></tr></thead>
    <tbody>
      <% allServing.forEach(function (d) { %>
        <% const isSys = d === sysDomain; %>
        <tr>
          <td><a href="https://<%= d %>/" target="_blank" rel="noopener"><%= d %></a></td>
          <td><span class="chip <%= isSys ? 'queued' : 'live' %>"><%= isSys ? 'system · noindex' : 'domain' %></span></td>
          <td>
            <form class="inline" method="post" action="/projects/<%= p.name %>/domains">
              <input type="hidden" name="action" value="set-main"><input type="hidden" name="domain" value="<%= d %>">
              <input type="radio" name="main-radio" <%= d === mainDomain ? 'checked' : '' %> onchange="this.form.submit()" title="make <%= d %> the main domain">
            </form>
          </td>
          <td>
            <form class="inline" method="post" action="/projects/<%= p.name %>/ssl-retry">
              <input type="hidden" name="domain" value="<%= d %>">
              <button class="btn small" type="submit" title="retry certificate issuance for <%= d %> only">SSL</button>
            </form>
            <% if (isSys) { %>
              <span class="muted">always available, cannot be removed</span>
            <% } else { %>
              <form class="inline" method="post" action="/projects/<%= p.name %>/domains" onsubmit="return confirm('Remove domain <%= d %>?')">
                <input type="hidden" name="action" value="remove"><input type="hidden" name="domain" value="<%= d %>">
                <button class="btn small" type="submit">Remove</button>
              </form>
            <% } %>
          </td>
        </tr>
      <% }) %>
      <% redirDomains.forEach(function (d) { %>
        <tr>
          <td><%= d %></td>
          <td><span class="chip queued">308 → <%= primaryDomain %></span></td>
          <td>
            <form class="inline" method="post" action="/projects/<%= p.name %>/domains" onsubmit="return confirm('Remove redirect <%= d %>?')">
              <input type="hidden" name="action" value="remove-redirect"><input type="hidden" name="domain" value="<%= d %>">
              <button class="btn small" type="submit">Remove</button>
            </form>
          </td>
        </tr>
      <% }) %>
    </tbody>
  </table>
  <div class="actions" style="margin-top:10px">
    <form class="inline" method="post" action="/projects/<%= p.name %>/domains">
      <input type="hidden" name="action" value="add">
      <input type="text" name="domain" placeholder="www.example.com" required pattern="[a-z0-9.-]+">
      <button class="btn small" type="submit">Add domain</button>
    </form>
    <form class="inline" method="post" action="/projects/<%= p.name %>/domains">
      <input type="hidden" name="action" value="add-redirect">
      <input type="text" name="domain" placeholder="example.com (redirects to primary)" required pattern="[a-z0-9.-]+">
      <button class="btn small" type="submit">Add redirect</button>
    </form>
  </div>
  <div class="actions" style="margin-top:8px">
    <form class="inline" method="post" action="/projects/<%= p.name %>/ssl-retry">
      <button class="btn small" type="submit">Retry SSL</button>
    </form>
    <span class="muted">re-attempts certificate issuance for domains whose DNS was fixed after creation</span>
  </div>
  <div class="hint">Point DNS at <code>188.40.238.163</code> before adding — certificates are issued automatically.</div>
</div>

<% if (customDomains.length) { %>
<div class="card">
  <h2>Domain rules</h2>
  <div class="hint">Restrict a specific domain to only serve certain paths (everything else 404s on that host) and/or keep it out of search results — useful for a secondary marketing/campaign domain that should only expose a handful of landing pages instead of the whole site.</div>
  <div id="domrule-rules"></div>
  <div class="geo-editor">
    <div class="geo-row">
      <select id="domrule-domain"><% customDomains.forEach(function (d) { %><option value="<%= d %>"><%= d %></option><% }) %></select>
      <input type="text" id="domrule-path-input" placeholder="/tiger-gold, /gold-strike* — Enter to add (comma-separated for multiple)" style="flex:1">
      <button class="btn small" type="button" onclick="domRuleAddPathChip()">Add path</button>
    </div>
    <div id="domrule-path-chips" class="geo-chips"></div>
    <label><input type="checkbox" id="domrule-noindex"> Keep this domain out of search results (X-Robots-Tag: noindex)</label>
    <div class="actions" style="margin-top:8px">
      <button class="btn small" type="button" id="domrule-commit-btn" onclick="domRuleCommitRule()">Add rule</button>
      <button class="btn small" type="button" id="domrule-cancel-btn" onclick="domRuleResetEditor()" hidden>Cancel edit</button>
    </div>
  </div>
  <form method="post" action="/projects/<%= p.name %>/domain-rules" id="domrule-form" style="margin-top:10px">
    <input type="hidden" name="rules_json" id="domrule-rules-json">
    <button class="btn primary" type="submit" onclick="domRuleSerialize()">Save domain rules</button>
    <span class="muted">enforced in Caddy before the cache, same as geo-blocking · one rule per domain, any number of paths</span>
  </form>
</div>

<script>
var DOMRULE_RULES = <%- JSON.stringify(domainRules || []) %>;
var DOMRULE_PATH_RE = /^\/[A-Za-z0-9_\-./*]*$/;
var domrulePaths = [];
var domruleEditIndex = -1;

function domRuleRenderRules() {
  document.getElementById('domrule-rules').innerHTML = DOMRULE_RULES.length === 0
    ? '<div class="muted">No domain rules — every domain serves the whole app.</div>'
    : '<table><thead><tr><th>Domain</th><th>Paths</th><th>Noindex</th><th></th></tr></thead><tbody>' + DOMRULE_RULES.map(function (r, i) {
        return '<tr><td class="mono">' + r.domain + '</td>'
          + '<td class="mono">' + (r.paths && r.paths.length ? r.paths.join(', ') : '(whole site)') + '</td>'
          + '<td>' + (r.noindex ? 'yes' : '—') + '</td>'
          + '<td><button class="btn small" type="button" onclick="domRuleEditRule(' + i + ')" title="load this rule into the editor to change its paths or noindex flag">Edit</button> '
          + '<button class="btn small" type="button" onclick="domRuleCloneRule(' + i + ')" title="load this rule into the editor to reuse its paths/noindex for another domain">Clone</button> '
          + '<button class="btn small" type="button" onclick="domRuleRemoveRule(' + i + ')">Remove</button></td></tr>';
      }).join('') + '</tbody></table>';
}

function domRuleRenderPathChips() {
  document.getElementById('domrule-path-chips').innerHTML = domrulePaths.length
    ? domrulePaths.map(function (p, idx) { return '<span class="chip live" onclick="domRuleRemovePathChip(' + idx + ')" title="remove">' + p + ' ✕</span>'; }).join(' ')
    : '<span class="muted">no paths added — empty means the whole site</span>';
}

function domRuleAddPathChip() {
  var input = document.getElementById('domrule-path-input');
  var parts = input.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
  if (!parts.length) return;
  var invalid = parts.filter(function (p) { return !DOMRULE_PATH_RE.test(p); });
  if (invalid.length) { alert('Invalid path(s): ' + invalid.join(', ') + ' — must start with / and use only letters, numbers, _-./*'); return; }
  parts.forEach(function (p) { if (domrulePaths.indexOf(p) === -1) domrulePaths.push(p); });
  input.value = '';
  domRuleRenderPathChips();
  input.focus();
}

function domRuleRemovePathChip(idx) {
  domrulePaths.splice(idx, 1);
  domRuleRenderPathChips();
}

function domRuleResetEditor() {
  domrulePaths = [];
  domruleEditIndex = -1;
  document.getElementById('domrule-noindex').checked = false;
  document.getElementById('domrule-path-input').value = '';
  document.getElementById('domrule-commit-btn').textContent = 'Add rule';
  document.getElementById('domrule-cancel-btn').hidden = true;
  domRuleRenderPathChips();
}

function domRuleEditRule(i) {
  var r = DOMRULE_RULES[i];
  if (!r) return;
  domruleEditIndex = i;
  domrulePaths = (r.paths || []).slice();
  document.getElementById('domrule-domain').value = r.domain;
  document.getElementById('domrule-noindex').checked = !!r.noindex;
  document.getElementById('domrule-commit-btn').textContent = 'Save changes';
  document.getElementById('domrule-cancel-btn').hidden = false;
  domRuleRenderPathChips();
  document.getElementById('domrule-path-input').focus();
}

function domRuleCloneRule(i) {
  var r = DOMRULE_RULES[i];
  if (!r) return;
  // domain is intentionally left for the user to pick — a domain can only
  // carry one rule, so cloning is for reusing paths/noindex on ANOTHER domain
  domruleEditIndex = -1;
  domrulePaths = (r.paths || []).slice();
  document.getElementById('domrule-noindex').checked = !!r.noindex;
  document.getElementById('domrule-commit-btn').textContent = 'Add rule';
  document.getElementById('domrule-cancel-btn').hidden = true;
  domRuleRenderPathChips();
  document.getElementById('domrule-path-input').focus();
}

function domRuleRemoveRule(i) {
  DOMRULE_RULES.splice(i, 1);
  if (domruleEditIndex === i) domRuleResetEditor();
  else if (domruleEditIndex > i) domruleEditIndex--;
  domRuleRenderRules();
}

function domRuleCommitRule() {
  var domain = document.getElementById('domrule-domain').value;
  if (!domain) { alert('Pick a domain.'); return; }
  if (DOMRULE_RULES.some(function (r, idx) { return r.domain === domain && idx !== domruleEditIndex; })) {
    alert('That domain already has a rule — edit or clone it instead.'); return;
  }
  var noindex = document.getElementById('domrule-noindex').checked;
  if (domruleEditIndex === -1) {
    DOMRULE_RULES.push({ domain: domain, paths: domrulePaths.slice(), noindex: noindex });
  } else {
    DOMRULE_RULES[domruleEditIndex] = { domain: domain, paths: domrulePaths.slice(), noindex: noindex };
  }
  domRuleResetEditor();
  domRuleRenderRules();
}

function domRuleSerialize() { document.getElementById('domrule-rules-json').value = JSON.stringify(DOMRULE_RULES); }

document.getElementById('domrule-path-input').addEventListener('keydown', function (ev) {
  if (ev.key === 'Enter') { ev.preventDefault(); domRuleAddPathChip(); }
});

domRuleRenderPathChips();
domRuleRenderRules();
</script>
<% } %>

<div class="card">
  <h2>Geo-blocking</h2>
  <form class="inline" method="post" action="/projects/<%= p.name %>/appconf" style="margin-bottom:10px">
    <input type="hidden" name="geo_exempt_google" value="">
    <label class="switch" title="Exempt Google's published crawler/AdsBot IP ranges from geo-blocking">
      <input type="checkbox" <%= (info.GEO_EXEMPT_GOOGLE || 'on') !== 'off' ? 'checked' : '' %>
        onchange="this.form.geo_exempt_google.value = this.checked ? 'on' : 'off'; this.form.submit()">
      <span class="slider"></span>
    </label>
    <span class="muted switch-label"><%= (info.GEO_EXEMPT_GOOGLE || 'on') !== 'off' ? "Exempt Google's AdsBot/Googlebot IP ranges from these rules" : "Google's AdsBot/Googlebot are subject to these rules like any other visitor" %></span>
  </form>
  <div id="geo-rules"></div>
  <div class="geo-editor">
    <div class="geo-row">
      <input type="text" id="geo-paths" placeholder="paths, e.g. /api/*, /premium* (empty = whole site)" style="flex:1">
      <select id="geo-mode">
        <option value="block">Block selected countries</option>
        <option value="allow-only">Only allow selected countries</option>
      </select>
    </div>
    <div class="geo-dd" id="geo-dd">
      <div class="geo-dd-toggle" onclick="geoDdToggle(event)">
        <span id="geo-picked" class="geo-chips"><span class="muted">Select countries…</span></span>
        <span class="geo-caret">▾</span>
      </div>
      <div class="geo-dd-panel" id="geo-dd-panel" hidden>
        <input type="text" id="geo-search" placeholder="Search countries…" autocomplete="off">
        <div id="geo-list" class="geo-list"></div>
      </div>
    </div>
    <div class="actions" style="margin-top:8px">
      <button class="btn small" type="button" onclick="geoAddRule()">Add rule</button>
    </div>
  </div>
  <form method="post" action="/projects/<%= p.name %>/geo" id="geo-form" style="margin-top:10px">
    <input type="hidden" name="rules_json" id="geo-rules-json">
    <button class="btn primary" type="submit" onclick="geoSerialize()">Save geo rules</button>
    <span class="muted">rules apply in front of the cache; internal checks and warming are never blocked</span>
  </form>
</div>

<script>
var GEO_RULES = <%- JSON.stringify(geoRules || []) %>;
var GEO_CODES = "AD AE AF AG AI AL AM AO AR AT AU AW AZ BA BB BD BE BF BG BH BI BJ BM BN BO BR BS BT BW BY BZ CA CD CF CG CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DO DZ EC EE EG ER ES ET FI FJ FM FR GA GB GD GE GH GI GL GM GN GQ GR GT GW GY HK HN HR HT HU ID IE IL IN IQ IR IS IT JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MG MH MK ML MM MN MO MR MT MU MV MW MX MY MZ NA NE NG NI NL NO NP NR NZ OM PA PE PG PH PK PL PS PT PW PY QA RO RS RU RW SA SB SC SD SE SG SI SK SL SM SN SO SR SS ST SV SY SZ TD TG TH TJ TL TM TN TO TR TT TV TW TZ UA UG US UY UZ VC VE VN VU WS YE ZA ZM ZW".split(" ");
var geoNames; try { geoNames = new Intl.DisplayNames(['en'], { type: 'region' }); } catch (e) { geoNames = { of: function (c) { return c; } }; }
function geoFlag(c) { return String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65, 0x1f1e6 + c.charCodeAt(1) - 65); }
var geoSel = {};
function geoDdToggle(ev) {
  if (ev && ev.target.closest('.chip')) return; // chip clicks remove, not toggle
  var panel = document.getElementById('geo-dd-panel');
  panel.hidden = !panel.hidden;
  if (!panel.hidden) { geoRenderList(document.getElementById('geo-search').value); document.getElementById('geo-search').focus(); }
}
document.addEventListener('click', function (ev) {
  if (!ev.target.closest('#geo-dd')) document.getElementById('geo-dd-panel').hidden = true;
});
function geoRenderList(filter) {
  var q = (filter || '').toLowerCase();
  document.getElementById('geo-list').innerHTML = GEO_CODES
    .map(function (c) { return { c: c, n: geoNames.of(c) || c }; })
    .filter(function (x) { return !q || x.n.toLowerCase().indexOf(q) >= 0 || x.c.toLowerCase().indexOf(q) >= 0; })
    .map(function (x) {
      return '<label class="geo-item' + (geoSel[x.c] ? ' sel' : '') + '" onclick="geoToggle(\'' + x.c + '\');event.preventDefault();event.stopPropagation()">' + geoFlag(x.c) + ' ' + x.n + '</label>';
    }).join('');
  var picked = Object.keys(geoSel).sort();
  document.getElementById('geo-picked').innerHTML = picked.length
    ? picked.map(function (c) { return '<span class="chip live" onclick="geoToggle(\'' + c + '\')" title="remove">' + geoFlag(c) + ' ' + c + ' ✕</span>'; }).join(' ')
    : '<span class="muted">Select countries…</span>';
}
function geoToggle(c) { if (geoSel[c]) delete geoSel[c]; else geoSel[c] = 1; geoRenderList(document.getElementById('geo-search').value); }
function geoRenderRules() {
  document.getElementById('geo-rules').innerHTML = GEO_RULES.length === 0
    ? '<div class="muted">No geo rules — the site is reachable from everywhere.</div>'
    : '<table><thead><tr><th>Paths</th><th>Mode</th><th>Countries</th><th></th></tr></thead><tbody>' + GEO_RULES.map(function (r, i) {
        return '<tr><td class="mono">' + (r.paths && r.paths.length ? r.paths.join(', ') : '(whole site)') + '</td>'
          + '<td>' + (r.mode === 'allow-only' ? 'only allow' : 'block') + '</td>'
          + '<td>' + r.countries.map(function (c) { return geoFlag(c) + ' ' + c; }).join(' ') + '</td>'
          + '<td><button class="btn small" type="button" onclick="geoCloneRule(' + i + ')" title="load this rule into the editor to tweak (e.g. a different path) and save as a new rule">Clone</button> '
          + '<button class="btn small" type="button" onclick="GEO_RULES.splice(' + i + ',1);geoRenderRules()">Remove</button></td></tr>';
      }).join('') + '</tbody></table>';
}
function geoCloneRule(i) {
  var r = GEO_RULES[i];
  if (!r) return;
  geoSel = {};
  (r.countries || []).forEach(function (c) { geoSel[c] = 1; });
  document.getElementById('geo-paths').value = (r.paths || []).join(', ');
  document.getElementById('geo-mode').value = r.mode === 'allow-only' ? 'allow-only' : 'block';
  geoRenderList(document.getElementById('geo-search').value);
  document.getElementById('geo-paths').focus();
}
function geoAddRule() {
  var countries = Object.keys(geoSel).sort();
  if (countries.length === 0) { alert('Select at least one country.'); return; }
  var paths = document.getElementById('geo-paths').value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
  GEO_RULES.push({ paths: paths, mode: document.getElementById('geo-mode').value, countries: countries });
  geoSel = {}; document.getElementById('geo-paths').value = ''; document.getElementById('geo-search').value = '';
  document.getElementById('geo-dd-panel').hidden = true;
  geoRenderList(''); geoRenderRules();
}
function geoSerialize() { document.getElementById('geo-rules-json').value = JSON.stringify(GEO_RULES); }
document.getElementById('geo-search').addEventListener('input', function () { geoRenderList(this.value); });
geoRenderList(''); geoRenderRules();
</script>

<div class="card">
  <h2>Traffic — last 30 days</h2>
  <% if (traffic.some(function (t) { return t.requests > 0; })) { %>
    <div class="traffic-chart" role="img" aria-label="Daily bandwidth over the last 30 days">
      <% traffic.forEach(function (t) { %>
        <% const cacheable = t.hit + t.miss; const split = (t.bytes_cache || 0) + (t.bytes_origin || 0); %>
        <div class="tc-col" title="<%= t.day %> — <%= h.fmtBytes(t.bytes) %> · <%= h.fmtInt(t.requests) %> requests<%= split ? ' · cache ' + h.fmtBytes(t.bytes_cache) + ' / server ' + h.fmtBytes(t.bytes_origin) : '' %><%= cacheable ? ' · ' + Math.round(100 * t.hit / cacheable) + '% HIT' : '' %><%= t.s5xx ? ' · ' + t.s5xx + '× 5xx' : '' %>">
          <div class="tc-bar<%= split ? ' tc-split' : '' %>" style="height:<%= t.requests ? Math.max(t.pct, 2) : 0 %>%"><% if (split) { %><div class="tc-seg-origin" style="height:<%= Math.min(100, Math.round(100 * t.bytes_origin / split)) %>%"></div><div class="tc-seg-cache"></div><% } %></div>
        </div>
      <% }) %>
    </div>
    <div class="tc-axis muted"><span><%= traffic[0].day %></span><span><%= traffic[traffic.length - 1].day %></span></div>
    <div class="tc-legend muted"><span class="tc-chip" style="background:var(--accent)"></span>served from cache (Varnish)<span class="tc-chip" style="background:var(--chart2)"></span>served by server (app/static)</div>
    <div class="tc-stats">
      <div class="tc-stat"><div class="v"><%= h.fmtBytes(trafficStats.today.bytes) %> · <%= h.fmtInt(trafficStats.today.requests) %> req</div><div class="l">today</div></div>
      <div class="tc-stat"><div class="v"><%= h.fmtBytes(trafficStats.yesterday.bytes) %> · <%= h.fmtInt(trafficStats.yesterday.requests) %> req</div><div class="l">yesterday</div></div>
      <div class="tc-stat"><div class="v"><%= h.fmtBytes(trafficStats.bytes7) %> · <%= h.fmtInt(trafficStats.req7) %> req</div><div class="l">7 days</div></div>
      <div class="tc-stat"><div class="v"><%= h.fmtBytes(trafficStats.bytes30) %> · <%= h.fmtInt(trafficStats.req30) %> req</div><div class="l">30 days</div></div>
      <% if (trafficStats.hitRate30 !== null) { %>
        <div class="tc-stat"><div class="v"><%= trafficStats.hitRate30 %>%</div><div class="l">cache hit (30 d)</div></div>
      <% } %>
      <% if (trafficStats.cacheShare30 !== null) { %>
        <div class="tc-stat"><div class="v"><%= h.fmtBytes(trafficStats.cacheBytes30) %> · <%= trafficStats.cacheShare30 %>%</div><div class="l">from cache (30 d)</div></div>
      <% } %>
    </div>
    <h3 class="tc-h3">Monthly totals</h3>
    <div class="tc-months">
      <% months.forEach(function (m) { %>
        <% const mc = m.hit + m.miss; const msplit = (m.bytes_cache || 0) + (m.bytes_origin || 0); %>
        <div class="tc-month" title="<%= m.month %><%= m.final ? '' : ' (running)' %> — <%= h.fmtBytes(m.bytes) %> · <%= h.fmtInt(m.requests) %> requests<%= msplit ? ' · cache ' + h.fmtBytes(m.bytes_cache) + ' / server ' + h.fmtBytes(m.bytes_origin) : '' %><%= mc ? ' · ' + Math.round(100 * m.hit / mc) + '% HIT' : '' %>">
          <div class="tc-mbar-wrap"><div class="tc-bar<%= m.final ? '' : ' tc-bar-running' %><%= msplit ? ' tc-split' : '' %>" style="height:<%= m.requests ? Math.max(m.pct, 2) : 0 %>%"><% if (msplit) { %><div class="tc-seg-origin" style="height:<%= Math.min(100, Math.round(100 * m.bytes_origin / msplit)) %>%"></div><div class="tc-seg-cache"></div><% } %></div></div>
          <div class="tc-mlabel muted"><%= m.month.slice(2) %><%= m.final ? '' : ' *' %></div>
          <div class="tc-mval"><%= h.fmtBytes(m.bytes) %></div>
          <div class="tc-mreq muted"><%= h.fmtInt(m.requests) %> req</div>
        </div>
      <% }) %>
    </div>
    <div class="muted" style="margin-top:6px">* running month — archived automatically once the month completes<%= monthsAll.length > months.length ? ' · chart shows the last 24 months' : '' %></div>
    <% if (monthsAll.length > 1) { %>
    <details class="tc-table">
      <summary class="muted">all months</summary>
      <table>
        <thead><tr><th>Month</th><th>Requests</th><th>Bandwidth</th><th>Cache</th><th>Server</th><th>HIT</th><th>MISS</th><th>5xx</th><th></th></tr></thead>
        <tbody>
          <% monthsAll.slice().reverse().forEach(function (m) { %>
            <% const ms = (m.bytes_cache || 0) + (m.bytes_origin || 0); %>
            <tr><td><%= m.month %></td><td><%= h.fmtInt(m.requests) %></td><td><%= h.fmtBytes(m.bytes) %></td><td><%= ms ? h.fmtBytes(m.bytes_cache) : '—' %></td><td><%= ms ? h.fmtBytes(m.bytes_origin) : '—' %></td><td><%= h.fmtInt(m.hit) %></td><td><%= h.fmtInt(m.miss) %></td><td><%= h.fmtInt(m.s5xx) %></td><td class="muted"><%= m.final ? 'archived' : 'running' %></td></tr>
          <% }) %>
        </tbody>
      </table>
    </details>
    <% } %>
    <details class="tc-table">
      <summary class="muted">daily table</summary>
      <table>
        <thead><tr><th>Day</th><th>Requests</th><th>Bandwidth</th><th>Cache</th><th>Server</th><th>HIT</th><th>MISS</th><th>2xx</th><th>4xx</th><th>5xx</th></tr></thead>
        <tbody>
          <% traffic.slice().reverse().forEach(function (t) { %><% if (t.requests) { %>
            <% const ts = (t.bytes_cache || 0) + (t.bytes_origin || 0); %>
            <tr><td><%= t.day %></td><td><%= h.fmtInt(t.requests) %></td><td><%= h.fmtBytes(t.bytes) %></td><td><%= ts ? h.fmtBytes(t.bytes_cache) : '—' %></td><td><%= ts ? h.fmtBytes(t.bytes_origin) : '—' %></td><td><%= h.fmtInt(t.hit) %></td><td><%= h.fmtInt(t.miss) %></td><td><%= h.fmtInt(t.s2xx) %></td><td><%= h.fmtInt(t.s4xx) %></td><td><%= h.fmtInt(t.s5xx) %></td></tr>
          <% } %><% }) %>
        </tbody>
      </table>
    </details>
  <% } else { %>
    <div class="muted">No traffic data yet — collection runs every 5 minutes; first numbers appear shortly after the next visits.</div>
  <% } %>
</div>

<div class="card">
  <h2>Repository</h2>
  <% if (p.git_account_id && p.repo_full) { %>
  <div class="actions">
    <span class="chip <%= gitAccount ? 'live' : 'failed' %>"><%= gitAccount ? gitAccount.provider + ' · ' + gitAccount.label : 'account removed' %></span>
    <span class="mono"><%= p.repo_full %></span>
    <% if (gitAccount) { %>
    <form class="inline" method="post" action="/projects/<%= p.name %>/git-verify">
      <button class="btn small" type="submit">Verify key + webhook</button>
    </form>
    <% } %>
  </div>
  <div class="hint">Re-checks (and re-adds if missing) the read-only deploy key and the push webhook on the repository.</div>
  <% } else if (p.repo_url) { %>
  <div class="mono"><%= p.repo_url %></div>
  <div class="hint">Manual repository — add the webhook and deploy key below to the repository yourself.</div>
  <% } else { %>
  <div class="muted">No repository connected yet — this project currently deploys from ZIP uploads only.</div>
  <% } %>

  <details style="margin-top:14px">
    <summary style="cursor:pointer">Connect / change repository</summary>
    <form method="post" action="/projects/<%= p.name %>/repo" id="repo-form" style="margin-top:12px">
      <label for="r-gitacc">Import from</label>
      <select id="r-gitacc" name="git_account_id" onchange="repoAccountChanged()">
        <option value="">Manual — enter repository URL below</option>
        <% (gitAccounts || []).forEach(function (a) { %>
          <option value="<%= a.id %>" <%= p.git_account_id === a.id ? 'selected' : '' %>><%= a.provider %> · <%= a.label %></option>
        <% }) %>
      </select>
      <div class="hint">Connect accounts under <a href="/settings/git">Git accounts</a> to browse repositories. With an account selected, the deploy key and push webhook are set up automatically.</div>

      <div id="r-repo-picker" style="display:none">
        <label>Repository</label>
        <div class="geo-dd" id="r-repo-dd">
          <div class="geo-dd-toggle" onclick="rRepoDdToggle()">
            <span id="r-repo-picked" class="muted">Select repository…</span>
            <span class="geo-caret">▾</span>
          </div>
          <div class="geo-dd-panel" id="r-repo-dd-panel" hidden>
            <input type="text" id="r-repo-search" placeholder="Search repositories…" autocomplete="off">
            <div id="r-repo-list" class="geo-list" style="flex-direction:column;flex-wrap:nowrap"></div>
          </div>
        </div>
      </div>
      <input type="hidden" name="repo_full" id="r-repo-full" value="<%= p.repo_full || '' %>">

      <label for="r-repo-url">Repository URL (manual)</label>
      <input id="r-repo-url" type="text" name="repo_url" class="mono" value="<%= p.repo_url || '' %>" placeholder="git@github.com:org/repo.git">
      <div class="hint">Only used when no account is selected above.</div>

      <div style="margin-top:14px">
        <button class="btn primary" type="submit">Save repository</button>
      </div>
    </form>
  </details>
</div>

<script>
var rRepoCache = [];
var rRepoError = null;
function rEscHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; }); }
function repoAccountChanged() {
  var id = document.getElementById('r-gitacc').value;
  var picker = document.getElementById('r-repo-picker');
  var urlField = document.getElementById('r-repo-url');
  rRepoCache = [];
  document.getElementById('r-repo-picked').textContent = 'Select repository…';
  document.getElementById('r-repo-full').value = '';
  if (!id) { picker.style.display = 'none'; urlField.disabled = false; return; }
  picker.style.display = '';
  urlField.disabled = true;
  document.getElementById('r-repo-list').innerHTML = '<div class="muted">loading repositories…</div>';
  fetch('/api/git/' + id + '/repos').then(function (r) { return r.json(); }).then(function (d) {
    rRepoCache = d.repos || [];
    rRepoError = d.error || null;
    rRenderRepoList('');
  }).catch(function () {
    rRepoError = 'failed to reach the panel — check your connection and retry';
    rRepoCache = [];
    rRenderRepoList('');
  });
}
function rRenderRepoList(q) {
  if (rRepoError) {
    document.getElementById('r-repo-list').innerHTML = '<div class="muted" style="color:#c0392b">' + rEscHtml(rRepoError) + '</div>';
    return;
  }
  q = (q || '').toLowerCase();
  var items = rRepoCache.filter(function (r) { return !q || r.full.toLowerCase().indexOf(q) >= 0; }).slice(0, 60);
  document.getElementById('r-repo-list').innerHTML = items.map(function (r) {
    return '<div class="geo-item" style="width:100%" data-repo="' + rEscHtml(r.full) + '" onclick="rPickRepo(this.getAttribute(\'data-repo\'))">' + (r.private ? '🔒 ' : '') + rEscHtml(r.full) + '</div>';
  }).join('') || '<div class="muted">no matches</div>';
}
function rPickRepo(full) {
  var r = null;
  for (var i = 0; i < rRepoCache.length; i++) if (rRepoCache[i].full === full) r = rRepoCache[i];
  if (!r) return;
  document.getElementById('r-repo-full').value = r.full;
  document.getElementById('r-repo-picked').textContent = (r.private ? '🔒 ' : '') + r.full;
  document.getElementById('r-repo-dd-panel').hidden = true;
}
function rRepoDdToggle() {
  var p = document.getElementById('r-repo-dd-panel');
  p.hidden = !p.hidden;
  if (!p.hidden) document.getElementById('r-repo-search').focus();
}
document.getElementById('r-repo-search').addEventListener('input', function () { rRenderRepoList(this.value); });
document.addEventListener('click', function (ev) {
  if (!ev.target.closest('#r-repo-dd')) { var p = document.getElementById('r-repo-dd-panel'); if (p) p.hidden = true; }
});
</script>

<div class="card">
  <h2>Webhook</h2>
  <div class="copyrow">
    <code><%= webhookUrl %></code>
    <button class="btn small" type="button" data-copy="<%= webhookUrl %>" onclick="copyText(this)">Copy</button>
  </div>
  <ul class="hints">
    <li><strong>GitHub:</strong> Settings &rarr; Webhooks &rarr; add the URL, content type <code>application/json</code>, and set the webhook <em>Secret</em> to <code><%= p.webhook_secret %></code> (verified via <code>X-Hub-Signature-256</code>).</li>
    <li><strong>GitLab:</strong> Settings &rarr; Webhooks &rarr; add the URL and put the same secret into <em>Secret token</em> (sent as <code>X-Gitlab-Token</code>).</li>
    <li><strong>Gitea:</strong> add the URL and set the webhook secret (HMAC verified via <code>X-Gitea-Signature</code>).</li>
    <li><strong>Generic / Bitbucket:</strong> the secret embedded in the URL is enough — keep the URL private.</li>
    <li>Only pushes to branch <code><%= p.branch %></code> trigger a deployment<% if (!p.auto_deploy) { %> — <strong>currently ignored</strong>, Auto-deploy is off (Overview card)<% } %>. Change the branch in the Overview card; the change applies to the next deployment, it does not deploy by itself.</li>
  </ul>
</div>

<div class="card">
  <h2>Deploy key</h2>
  <div class="muted" style="margin-bottom:8px">Add this public key to the repository (read-only deploy key) if it is private.</div>
  <% if (deployKey) { %>
  <div class="copyrow">
    <code><%= deployKey %></code>
    <button class="btn small" type="button" data-copy="<%= deployKey %>" onclick="copyText(this)">Copy</button>
  </div>
  <% } else { %>
  <div class="muted">No deploy key found at the configured path.</div>
  <% } %>
</div>

<div class="card">
  <h2>Environment variables</h2>
  <% if (p.multi_label) { %>
  <div class="hint" style="margin-bottom:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
    <span>Label preset — fills <code>NUXT_SITE=<%= p.name %></code>, a fresh purge secret and this app's domain;</span>
    <label>copy admin secrets from
      <select id="env-from">
        <option value="">— none —</option>
        <% (typeof otherProjects !== 'undefined' ? otherProjects : []).forEach(function (n) { %>
          <option value="<%= n %>"><%= n %></option>
        <% }) %>
      </select>
    </label>
    <button type="button" class="btn small" onclick="ccFillEnv('<%= p.name %>')">Fill template</button>
  </div>
  <div id="env-webhook-hint" class="hint" style="display:none;margin-bottom:8px"></div>
  <% } %>
  <form method="post" action="/projects/<%= p.name %>/env">
    <textarea name="env" id="env-textarea" rows="10" spellcheck="false" placeholder="KEY=value"><%= envContent %></textarea>
    <% if (isLaravel) { %>
    <div class="hint">Written to <code><%= envPath %></code> (the release's <code>.env</code> symlinks to it). Laravel reads it through its <strong>config cache</strong> — after saving, use "Restart app" (rebuilds the cache) or deploy. <code>env()</code> outside <code>config/</code> returns null under a cached config, as usual.</div>
    <% } else { %>
    <div class="hint">Written to <code><%= envPath %></code>. Takes effect on the next deploy — or use "Restart app". <code>NUXT_SITE</code> is baked at BUILD time — a label change always needs a deploy.</div>
    <% } %>
    <div style="margin-top:10px">
      <button class="btn" type="submit">Save env</button>
    </div>
  </form>
  <script>
    async function ccFillEnv(project) {
      const ta = document.getElementById('env-textarea');
      if (ta.value.trim() && !confirm('Replace the current env content with the label template?')) return;
      const from = document.getElementById('env-from').value;
      try {
        const r = await fetch('/projects/' + project + '/env-template' + (from ? '?from=' + encodeURIComponent(from) : ''));
        const t = await r.json();
        if (!t || !t.env) return;
        ta.value = t.env;
        const hint = document.getElementById('env-webhook-hint');
        hint.style.display = '';
        hint.innerHTML = 'After saving: 1) deploy the app (NUXT_SITE is build-time), 2) register the Strapi purge webhook: <code>' +
          (t.webhookUrl || '(set a domain first)') + '</code> with header <code>Authorization: Bearer ' + t.purgeSecret + '</code>.';
      } catch { /* leave editor untouched */ }
    }
  </script>
</div>

<div class="card">
  <h2>Deployments</h2>
  <% if (!history.length) { %>
    <div class="muted">No deployments yet.</div>
  <% } else { %>
  <table>
    <thead>
      <tr><th>#</th><th>Status</th><th>Source</th><th>Commit</th><th>Started</th><th>Duration</th><th></th></tr>
    </thead>
    <tbody id="dep-rows">
    <% for (const d of history) { %>
      <tr>
        <td><a href="/deployments/<%= d.id %>">#<%= d.id %></a></td>
        <td>
          <span class="chip <%= d.status %>"><%= h.depLabel(d.status) %></span>
          <% if (d.step && d.status === 'failed') { %><div class="muted"><%= d.step %></div><% } %>
        </td>
        <td><%= d.source %></td>
        <td class="mono">
          <%= h.shortSha(d.commit_sha) %>
          <% if (d.commit_msg) { %>
            <div class="muted" style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="<%= d.commit_msg %>"><%= d.commit_msg %></div>
          <% } %>
        </td>
        <td><%= h.fmtTime(d.started_at) %></td>
        <td><%= h.duration(d.started_at, d.finished_at) %></td>
        <td><a href="/deployments/<%= d.id %>">log</a></td>
      </tr>
    <% } %>
    </tbody>
  </table>
  <% if (historyHasMore) { %>
  <div id="dep-more-wrap" style="margin-top:10px;text-align:center">
    <button class="btn small" type="button" id="dep-more" onclick="depLoadMore()">Load more</button>
  </div>
  <script>
  var depCursor = <%= history[history.length - 1].id %>;
  function depLoadMore() {
    var btn = document.getElementById('dep-more');
    btn.disabled = true;
    btn.textContent = 'Loading…';
    fetch('/api/projects/<%= p.name %>/deployments?before=' + depCursor)
      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function (d) {
        var tbody = document.getElementById('dep-rows');
        d.rows.forEach(function (x) { tbody.appendChild(depRow(x)); depCursor = x.id; });
        if (d.hasMore) { btn.disabled = false; btn.textContent = 'Load more'; }
        else document.getElementById('dep-more-wrap').remove();
      })
      .catch(function () { btn.disabled = false; btn.textContent = 'Load more'; });
  }
  // Build rows with textContent (never innerHTML) — commit messages are untrusted repo data.
  function depRow(x) {
    var tr = document.createElement('tr');
    function td(cls) { var c = document.createElement('td'); if (cls) c.className = cls; tr.appendChild(c); return c; }
    function log(parent, text) { var a = document.createElement('a'); a.href = '/deployments/' + x.id; a.textContent = text; parent.appendChild(a); }
    log(td(), '#' + x.id);
    var st = td();
    var chip = document.createElement('span');
    chip.className = 'chip ' + x.status;
    chip.textContent = x.label;
    st.appendChild(chip);
    if (x.step) { var sd = document.createElement('div'); sd.className = 'muted'; sd.textContent = x.step; st.appendChild(sd); }
    td().textContent = x.source;
    var sha = td('mono');
    sha.appendChild(document.createTextNode(x.sha));
    if (x.msg) {
      var m = document.createElement('div');
      m.className = 'muted';
      m.style.cssText = 'max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
      m.title = x.msg;
      m.textContent = x.msg;
      sha.appendChild(m);
    }
    td().textContent = x.started;
    td().textContent = x.duration;
    log(td(), 'log');
    return tr;
  }
  </script>
  <% } %>
  <% } %>
</div>
