{{define "title"}}{{.Title}}{{end}}

{{define "content"}}
{{$page := .Page}}
{{$job := $page.Job}}
{{$host := $page.Host}}
<div class="max-w-[1280px] mx-auto px-8 pt-7 pb-14">

  <div class="crumbs">
    <a href="/">Dashboard</a><span class="sep">/</span>
    <a href="/hosts/{{$host.ID}}">{{$host.Name}}</a><span class="sep">/</span>
    <span class="text-ink-mid">job {{slice $job.ID 0 8}}…{{slice $job.ID (sub (len $job.ID) 4) (len $job.ID)}}</span>
  </div>

  {{/* ---------- header ---------- */}}
  <div class="flex items-start justify-between mt-3.5">
    <div>
      <div class="flex items-center gap-3">
        {{if $page.IsActive}}
          <span class="dot pulse" style="background: var(--accent);"></span>
        {{else if eq $job.Status "succeeded"}}
          <span class="dot dot-online"></span>
        {{else if eq $job.Status "failed"}}
          <span class="dot dot-failed"></span>
        {{else}}
          <span class="dot dot-offline"></span>
        {{end}}
        <h1 class="text-[22px] font-medium tracking-[-0.01em]">
          {{$job.Kind}} <span class="text-ink-fade">·</span>
          <span class="mono text-ink font-medium">{{$host.Name}}</span>
        </h1>
        {{if eq $job.Status "queued"}}
          <span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
                style="background: color-mix(in oklch, var(--ink-mid), transparent 88%); color: var(--ink-mid); border: 1px solid var(--line-soft);">queued</span>
        {{else if eq $job.Status "running"}}
          <span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
                style="background: color-mix(in oklch, var(--accent), transparent 88%); color: var(--accent); border: 1px solid color-mix(in oklch, var(--accent), transparent 70%);">running</span>
        {{else if eq $job.Status "succeeded"}}
          <span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
                style="background: color-mix(in oklch, var(--ok), transparent 88%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%);">succeeded</span>
        {{else if eq $job.Status "failed"}}
          <span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
                style="background: color-mix(in oklch, var(--bad), transparent 88%); color: var(--bad); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%);">failed</span>
        {{else}}
          <span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
                style="background: color-mix(in oklch, var(--warn), transparent 88%); color: var(--warn); border: 1px solid color-mix(in oklch, var(--warn), transparent 70%);">{{$job.Status}}</span>
        {{end}}
      </div>
      <div class="flex items-center gap-3 mt-2.5 text-[12.5px] text-ink-mute">
        <span>job <span class="mono text-ink-mid">{{$job.ID}}</span></span>
        {{if $job.StartedAt}}
          <span class="text-ink-fade">·</span>
          <span>started <span class="mono text-ink-mid">{{relTime $job.StartedAt}}</span></span>
        {{end}}
        {{if $job.FinishedAt}}
          <span class="text-ink-fade">·</span>
          <span>finished <span class="mono text-ink-mid">{{relTime $job.FinishedAt}}</span></span>
        {{end}}
        {{if $job.ExitCode}}
          <span class="text-ink-fade">·</span>
          <span>exit code <span class="mono {{if eq $job.Status "failed"}}text-bad{{else}}text-ink-mid{{end}}">{{derefInt $job.ExitCode}}</span></span>
        {{end}}
      </div>
    </div>
    <div class="flex items-center gap-2">
      <details class="dropdown" id="download-menu">
        <summary>
          Download log
          <span class="chev">▾</span>
        </summary>
        <div class="dropdown-menu">
          <a class="dropdown-item" href="/api/jobs/{{$job.ID}}/log.txt">
            <span class="label">Plain text</span>
            <span class="hint">.txt · for humans / paste into a ticket</span>
          </a>
          <a class="dropdown-item" href="/api/jobs/{{$job.ID}}/log.ndjson">
            <span class="label">JSON Lines</span>
            <span class="hint">.ndjson · pipe into jq / tooling</span>
          </a>
        </div>
      </details>
      {{if $page.IsActive}}
        <button class="btn btn-danger" id="cancel-btn"
                hx-post="/api/jobs/{{$job.ID}}/cancel"
                hx-swap="none">Cancel job</button>
      {{else}}
        <a href="/hosts/{{$host.ID}}" class="btn">Back to host</a>
      {{end}}
    </div>
    <script>
    // Close the download dropdown when clicking outside it.
    (function() {
      var dd = document.getElementById('download-menu');
      if (!dd) return;
      document.addEventListener('click', function(e) {
        if (dd.open && !dd.contains(e.target)) dd.open = false;
      });
    })();
    </script>
  </div>

  {{/* ---------- progress (running only) ---------- */}}
  {{if $page.IsActive}}
    <div class="mt-7 panel rounded-[8px] p-[18px]" id="progress-block">
      <div class="flex items-center justify-between mb-2.5">
        <div class="flex items-center gap-3.5 text-sm">
          <span class="mono text-ink font-medium" id="progress-pct" style="font-size: 18px;">—</span>
          <span class="text-ink-mute" id="progress-bytes"></span>
        </div>
        <div class="text-sm text-ink-mute" id="progress-rate"></div>
      </div>
      <div class="progress-track">
        <div class="progress-fill" id="progress-fill" style="width: 0%;"></div>
      </div>
      {{if eq (printf "%s" $job.Kind) "restore"}}
        <div class="mt-3 text-[12px] text-ink-mute" id="restore-current-block">
          <span class="text-ink-fade uppercase tracking-[0.08em] text-[10.5px]">Current</span>
          <span class="mono text-ink-mid ml-2.5" id="restore-current-file">—</span>
        </div>
      {{end}}
    </div>
  {{end}}

  {{/* ---------- failure summary (failed only) ---------- */}}
  {{if eq $job.Status "failed"}}
    <div class="panel mt-6 p-4 rounded-[7px]" style="border-color: color-mix(in oklch, var(--bad), transparent 60%);">
      <div class="text-[11px] uppercase tracking-[0.08em] font-semibold text-bad mb-1.5">Failure</div>
      {{if $job.Error}}
        <p class="mono text-[13px] text-ink leading-[1.6]">{{deref $job.Error}}</p>
      {{else}}
        <p class="text-ink-mute text-[13px]">No error message captured. Inspect the log below for details.</p>
      {{end}}
    </div>
  {{end}}

  {{/* ---------- log viewer ---------- */}}
  <div class="mt-6">
    <div class="flex items-center justify-between mb-3">
      <div class="flex items-center gap-3">
        <h2 class="text-[13px] font-semibold tracking-[0.01em]">Stream</h2>
        <span class="text-[11.5px] text-ink-fade" id="stream-status">
          {{if $page.IsActive}}
            following · auto-scroll on
          {{else}}
            <span class="text-ink-fade">complete · {{len $page.Logs}} lines</span>
          {{end}}
        </span>
      </div>
      <div class="flex items-center gap-2">
        <button class="btn btn-ghost" id="follow-btn" style="display: none;">⇢ Follow</button>
      </div>
    </div>

    <div class="log" id="log-container">
      <div id="log-stream">
        {{range $page.Logs}}
        <div class="log-line">
          <span class="log-ts">{{.TS.Format "15:04:05.000"}}</span>
          <span class="log-tag">{{if eq .Stream "stdout"}}OUT{{else if eq .Stream "stderr"}}ERR{{else}}EVENT{{end}}</span>
          <span class="log-stream-{{.Stream}}">{{.Payload}}</span>
        </div>
        {{end}}
        {{if and $page.IsActive (eq (len $page.Logs) 0)}}
          <div class="log-line">
            <span class="log-ts" style="color: var(--accent);">···</span>
            <span class="log-tag" style="color: var(--accent);">…</span>
            <span style="color: var(--accent);">awaiting agent output</span>
          </div>
        {{end}}
      </div>
    </div>
  </div>

</div>

{{if $page.IsActive}}
<script>
(function() {
  const jobID = '{{$job.ID}}';
  const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
  const ws = new WebSocket(`${wsProto}://${location.host}/api/jobs/${jobID}/stream`);
  const stream = document.getElementById('log-stream');
  const container = document.getElementById('log-container');
  const fill = document.getElementById('progress-fill');
  const pct = document.getElementById('progress-pct');
  const bytes = document.getElementById('progress-bytes');
  const rate = document.getElementById('progress-rate');
  const status = document.getElementById('stream-status');
  const followBtn = document.getElementById('follow-btn');

  let autoScroll = true;
  // If the user scrolls up, stop auto-scrolling and surface a "Follow" button.
  container.addEventListener('scroll', () => {
    const atBottom = container.scrollHeight - container.clientHeight - container.scrollTop < 8;
    if (!atBottom && autoScroll) {
      autoScroll = false;
      followBtn.style.display = '';
      status.textContent = 'paused · scroll to follow';
    } else if (atBottom && !autoScroll) {
      autoScroll = true;
      followBtn.style.display = 'none';
      status.textContent = 'following · auto-scroll on';
    }
  });
  followBtn.addEventListener('click', () => {
    container.scrollTop = container.scrollHeight;
  });

  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, c => ({
      '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
    }[c]));
  }
  function fmtTs(iso) {
    // Show HH:MM:SS.mmm in local time. Falls back to iso if parsing fails.
    const d = new Date(iso);
    if (isNaN(d)) return iso;
    const pad = (n, w) => String(n).padStart(w, '0');
    return `${pad(d.getHours(),2)}:${pad(d.getMinutes(),2)}:${pad(d.getSeconds(),2)}.${pad(d.getMilliseconds(),3)}`;
  }
  function fmtBytes(n) {
    if (!n) return '0 B';
    const u = ['B','kB','MB','GB','TB'];
    let i = 0;
    while (n >= 1000 && i < u.length-1) { n /= 1000; i++; }
    return (i === 0 ? n.toFixed(0) : n.toFixed(1)) + ' ' + u[i];
  }

  const currentFileEl = document.getElementById('restore-current-file');
  function maybeUpdateCurrent(p) {
    // Restore-specific: surface the most recent stdout path in the
    // "Current" slot. Restic restore --json prints per-file lines on
    // stdout (no JSON wrapper) so any line starting with "/" is a
    // good candidate.
    if (!currentFileEl || p.stream !== 'stdout') return;
    const v = (p.payload || '').trim();
    if (v.startsWith('/') && v.length < 400) {
      currentFileEl.textContent = v;
    }
  }
  function appendLine(p) {
    // Drop the "awaiting" placeholder once real lines arrive.
    if (stream.children.length === 1 && stream.firstElementChild.textContent.includes('awaiting agent')) {
      stream.firstElementChild.remove();
    }
    const tag = p.stream === 'stdout' ? 'OUT' : p.stream === 'stderr' ? 'ERR' : 'EVENT';
    const line = document.createElement('div');
    line.className = 'log-line';
    line.innerHTML =
      `<span class="log-ts">${fmtTs(p.ts)}</span>` +
      `<span class="log-tag">${tag}</span>` +
      `<span class="log-stream-${p.stream}">${escapeHtml(p.payload)}</span>`;
    stream.appendChild(line);
    if (autoScroll) container.scrollTop = container.scrollHeight;
    maybeUpdateCurrent(p);
  }

  ws.onmessage = (ev) => {
    let env;
    try { env = JSON.parse(ev.data); } catch { return; }
    let p;
    try { p = env.payload ? JSON.parse(JSON.stringify(env.payload)) : {}; } catch { p = {}; }
    // The server sends payload as a JSON object literal, but Go wire
    // wraps it as raw JSON in the envelope. After JSON.parse(ev.data)
    // payload is already an object — no second parse needed.
    p = env.payload || {};
    switch (env.type) {
      case 'log.stream':
        appendLine(p);
        break;
      case 'job.progress': {
        const percent = Math.round((p.percent_done || 0) * 100);
        fill.style.width = percent + '%';
        pct.textContent = percent + '%';
        const totals = [];
        if (p.bytes_done && p.total_bytes) totals.push(`${fmtBytes(p.bytes_done)} of ${fmtBytes(p.total_bytes)}`);
        if (p.files_done && p.total_files) totals.push(`${p.files_done.toLocaleString()} of ${p.total_files.toLocaleString()} files`);
        bytes.textContent = totals.length ? '· ' + totals.join(' · ') : '';
        const parts = [];
        if (p.throughput_bps) parts.push(`${fmtBytes(p.throughput_bps)}/s`);
        if (p.eta_seconds) parts.push(`ETA ${Math.floor(p.eta_seconds/60)}m ${p.eta_seconds%60}s`);
        rate.textContent = parts.join(' · ');
        break;
      }
      case 'job.started':
        // Already shown in the header on a fresh page load; if we
        // arrive before the job moves to "running" the next progress
        // tick will paint it.
        break;
      case 'job.finished':
        // Reload to render the final-state header (stats / exit code
        // / end-of-stream markers come from the server).
        setTimeout(() => location.reload(), 600);
        break;
    }
  };
  ws.onerror = () => { status.textContent = 'connection error · reload to retry'; };
  ws.onclose = () => {
    if (status.textContent.startsWith('following')) {
      status.textContent = 'stream closed';
    }
  };
})();
</script>
{{end}}

{{end}}
