<h1>Pipeline</h1>
<div class="stages" id="stages"></div>

<h2 id="activity">Activity</h2>
<div id="jobstate" aria-live="polite">idle</div>
<div id="joblog" aria-label="stage output">(stage output appears here)</div>
<script>
"use strict";
let P = null;
let running = false;

async function runStage(stage, body) {
  const res = await apiPost(`/api/run/${stage}`, body);
  if (res) refresh().catch(() => {});  // the poll self-heals a hiccup
}

function stageCard(num, name, facts, actions) {
  return `<section class="stage">
    <div class="top"><span class="num">${num}</span><span class="name">${name}</span></div>
    <div class="facts">${facts}</div>
    <div class="act">${actions}</div>
  </section>`;
}

function runBtn(stage, label) {
  return `<button class="primary" data-run="${stage}" ${running ? "disabled" : ""}>${esc(label ?? "Run")}</button>`;
}

function render() {
  const m = P.matches, log = P.upload_log;
  running = P.job.status === "running";  // buttons below depend on this

  const banners = [];
  if (P.stub_data) banners.push(
    `<div class="banner warn">Stub fixtures active: resolved ids are placeholders and the
     real upload stays locked until real BGG data replaces them.</div>`);
  const missing = Object.entries(P.env).filter(([, ok]) => !ok).map(([k]) => k);
  if (missing.length) banners.push(
    `<div class="banner warn">Credentials not loaded in this server's environment:
     ${missing.map(esc).join(", ")} — run <code>bggpipe init</code> or load .env, then restart.</div>`);
  showBanner(banners.join(""));

  const uploadFacts = `<b>${P.to_add}</b> to add · <b>${P.to_update}</b> version updates
    ${log.added || log.added_no_version ? `· <b>${(log.added ?? 0) + (log.added_no_version ?? 0)}</b> added` : ""}
    ${log.failed ? `· <b>${log.failed}</b> failed` : ""}
    · <a href="/queue">inspect the queue</a>`;

  document.getElementById("stages").innerHTML = [
    stageCard(1, "extract", `read titles off <b>${P.photos}</b> <a href="/photos">photo(s)</a> — <b>${P.titles}</b> so far`
      + (P.titles > 0
          ? P.shaky_reads > 0
            ? ` · <b>${P.shaky_reads}</b> shaky read(s) worth a <a href="/titles">proofread</a> before resolving`
            : ` · <a href="/titles">proofread the titles</a> before resolving`
          : ""), runBtn("extract")),
    stageCard(2, "resolve", `match titles to BGG ids — <b>${(m.auto ?? 0)}</b> auto · <b>${m.ambiguous ?? 0}</b> ambiguous · <b>${m.unmatched ?? 0}</b> unmatched`, runBtn("resolve")),
    stageCard(3, "review", `<b>${P.pending_review}</b> item(s) waiting for your call`, `<a class="linkbtn" href="/review">Open review</a>`),
    stageCard(4, "diff", `compare against your BGG collection`, runBtn("diff")),
    stageCard(5, "upload", uploadFacts,
      `${runBtn("upload", "Dry run")}
       <button class="danger" data-upload-real ${running || P.stub_data ? "disabled" : ""}>Upload</button>
       <label>limit <input type="number" id="uplimit" min="1" placeholder="all"></label>`),
    stageCard(6, "enrich", `<b>${P.games}</b> game(s) in the <a href="/library">library</a>`, runBtn("enrich")),
  ].join("");

  document.querySelectorAll("[data-run]").forEach(b =>
    b.addEventListener("click", () => runStage(b.dataset.run,
      b.dataset.run === "upload" ? {dry_run: true} : {})));
  const real = document.querySelector("[data-upload-real]");
  if (real) real.addEventListener("click", () => {
    const limit = Number(document.getElementById("uplimit").value) || null;
    if (!confirm(`Really add games to your live BGG collection${limit ? ` (limit ${limit})` : ""}? A browser window will open.`)) return;
    runStage("upload", {dry_run: false, limit});
  });

  const job = P.job;
  const state = document.getElementById("jobstate");
  if (job.status === "idle") state.textContent = "idle";
  else state.innerHTML = `<span class="${esc(job.status)}">${esc(job.stage)}: ${esc(job.status)}</span>` +
    (job.error ? ` — ${esc(job.error)}` : "");
  const logEl = document.getElementById("joblog");
  const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 4;
  logEl.textContent = job.log.length ? job.log.join("\n") : "(stage output appears here)";
  if (atBottom) logEl.scrollTop = logEl.scrollHeight;
}

let LAST = null;
async function refresh() {
  const p = await fetchJSON("/api/pipeline");
  const payload = JSON.stringify(p);
  if (payload === LAST) return;
  LAST = payload;
  P = p;
  render();
}

refresh().catch(err => errorBanner(err.message || err));
pollLoop(refresh, 2000, () => render());
</script>
