// dashboard-live-ops.js — extracted verbatim from dashboard.js (classic script,
// shared global scope). Loaded before dashboard.js. Pure move, no logic change.

// ── Live ops panel + System automations collapsible ──────────────────

function buildLiveOpsPanel(opsLive) {
  const customs = opsLive?.customWorkflows || [];
  const loops = opsLive?.longRunningLoops || [];
  const crons = opsLive?.recentCrons || [];
  const total = customs.length + loops.length + crons.length;
  if (total === 0) return null; // hide the panel entirely when nothing is live

  const dot = (color = "#10b981") =>
    el("span", {
      style: `display:inline-block;width:6px;height:6px;border-radius:50%;background:${color};margin-right:6px;vertical-align:middle;`,
    });
  const fmtAge = (sec) => {
    if (sec == null) return "—";
    if (sec < 60) return `${sec}s ago`;
    if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
    return `${Math.round(sec / 3600)}h ago`;
  };
  const fmtSince = (ts) => {
    if (!ts) return "—";
    const ms = Date.now() - new Date(ts).getTime();
    return fmtAge(Math.round(ms / 1000));
  };

  const sectionLabel = (text) =>
    el(
      "div",
      {
        style:
          "font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.06em;margin:8px 0 6px;font-weight:600;",
      },
      text,
    );

  const rowsBox = el("div", { id: "live-ops-rows", style: "display:flex;flex-direction:column;" });

  if (customs.length > 0) {
    rowsBox.appendChild(sectionLabel(`Custom workflows (${customs.length} scheduled)`));
    for (const c of customs) {
      rowsBox.appendChild(
        el(
          "div",
          {
            class: "channel-row",
            style: "padding:6px 0;cursor:pointer;",
            title: `${c.sourcePresetId || ""} · ${c.schedule}`,
            onclick: () => navigate(`automations/view/${c.id}`),
          },
          [
            el("div", { style: "flex:1;min-width:0;" }, [
              el("div", { class: "primary-cell", style: "font-size:12px;" }, [dot(), c.name]),
              el(
                "div",
                { class: "muted small mono", style: "margin-top:2px;" },
                `${c.schedule || "—"} · last run ${fmtSince(c.lastRunAt)} (${c.lastStatus || "—"})`,
              ),
            ]),
          ],
        ),
      );
    }
  }

  if (loops.length > 0) {
    rowsBox.appendChild(sectionLabel(`Long-running loops (${loops.length} active)`));
    // Where each loop type opens when clicked through.
    // campaigns + email_campaigns navigate to the per-row detail page;
    // active_member + auto_responder + forwarder don't have a dashboard
    // detail page yet, so they navigate to the relevant feature route
    // for now (the user can find their config there).
    const detailRoute = (type, id) =>
      ({
        campaign: `campaigns/dm/${id}`,
        email_campaign: `campaigns/email/${id}`,
        active_member: `telegram/activemember`,
        auto_responder: `telegram/autoreply`,
        forwarder: `telegram/forward`,
      })[type] || null;

    for (const l of loops) {
      const typeLabel =
        {
          campaign: "TG campaign",
          email_campaign: "Email campaign",
          active_member: "Group listener",
          auto_responder: "Auto-reply",
          forwarder: "Forwarder",
        }[l.type] || l.type;

      const route = detailRoute(l.type, l.id);

      // Loop control: pause + cancel buttons. resume is implicit — once
      // paused the loop's row leaves Live-now, and the user resumes from
      // its detail page (campaign / config). Cancel is destructive so
      // we confirm.
      const pauseBtn = el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          title: "Pause this loop",
          onclick: async (ev) => {
            ev.stopPropagation();
            await withProgress(ev.currentTarget, async () => {
              const r = await api("ops-loop-action", { type: l.type, id: l.id, action: "pause" });
              if (r?.error) toast(r.error, "err");
              else {
                toast(`Paused ${typeLabel}`, "ok");
                renderAutomations();
              }
            });
          },
        },
        "⏸",
      );

      const cancelBtn = el(
        "button",
        {
          class: "btn btn-tiny btn-danger",
          type: "button",
          title: "Stop and cancel — destructive",
          onclick: async (ev) => {
            ev.stopPropagation();
            if (
              !confirm(
                `Cancel ${typeLabel} "${l.name}"?\n\nThis stops the loop and won't auto-resume. Re-start from its detail page if you want it back.`,
              )
            )
              return;
            await withProgress(ev.currentTarget, async () => {
              const r = await api("ops-loop-action", { type: l.type, id: l.id, action: "cancel" });
              if (r?.error) toast(r.error, "err");
              else {
                toast(`Cancelled ${typeLabel}`, "ok");
                renderAutomations();
              }
            });
          },
        },
        "✕",
      );

      const row = el(
        "div",
        {
          class: "channel-row",
          style: `padding:6px 0;${route ? "cursor:pointer;" : ""}`,
          onclick: route ? () => navigate(route) : null,
          title: route ? `Click to open ${typeLabel} details` : "",
        },
        [
          el("div", { style: "flex:1;min-width:0;" }, [
            el("div", { class: "primary-cell", style: "font-size:12px;" }, [
              dot("#3b82f6"),
              `${typeLabel} · ${l.name}`,
            ]),
            el(
              "div",
              { class: "muted small mono", style: "margin-top:2px;" },
              `running for ${fmtSince(l.since && new Date(l.since).toISOString())}${route ? " · click to view" : ""}`,
            ),
          ]),
          pauseBtn,
          cancelBtn,
        ],
      );

      rowsBox.appendChild(row);
    }
  }

  if (crons.length > 0) {
    rowsBox.appendChild(sectionLabel(`Recent system fires (${crons.length} in last 30 min)`));
    for (const c of crons) {
      const color = c.lastError ? "#ef4444" : c.isRunning ? "#fbbf24" : "#10b981";
      // Trigger button — shown only for whitelisted system crons (matching,
      // cryptorank, warmer, etc.). Backend keeps the whitelist authoritative
      // so non-triggerable jobs return 400 even if someone forges the call.
      const triggerBtn = c.triggerable
        ? el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              title: `Run "${c.name}" now`,
              onclick: async (ev) => {
                ev.stopPropagation();
                await withProgress(ev.currentTarget, async () => {
                  const r = await api("ops-job-trigger", { name: c.name });
                  if (r?.error) toast(r.error, "err");
                  else toast(`▶ ${c.name} started`, "ok", 4000);
                });
              },
            },
            "▶",
          )
        : null;
      rowsBox.appendChild(
        el(
          "div",
          {
            class: "channel-row",
            style: "padding:5px 0;",
            title: c.lastError
              ? `Last error: ${c.lastError}`
              : `${c.schedule || ""} · ${c.category || ""}`,
          },
          [
            el("div", { style: "flex:1;min-width:0;" }, [
              el("div", { class: "primary-cell mono", style: "font-size:11px;" }, [
                dot(color),
                c.name,
              ]),
              el(
                "div",
                { class: "muted small mono", style: "margin-top:2px;" },
                `${c.category || ""} · ${fmtAge(c.ageSec)}${c.isRunning ? " · running" : ""}${c.paused ? " · paused" : ""}`,
              ),
            ]),
            triggerBtn,
          ],
        ),
      );
    }
  }

  return el(
    "div",
    {
      id: "live-ops-panel",
      class: "card",
      style: "padding:14px 16px;margin-bottom:16px;border-left:3px solid #10b981;",
    },
    [
      el("div", { class: "card-title", style: "display:flex;align-items:center;gap:8px;" }, [
        el("span", {}, "● Live now"),
        el(
          "span",
          { class: "muted small", style: "margin-left:auto;font-weight:400;" },
          `auto-refreshes every 15s · ${total} active`,
        ),
      ]),
      rowsBox,
    ],
  );
}

let _liveOpsRefreshTimer = null;
function scheduleLiveOpsRefresh() {
  if (_liveOpsRefreshTimer) clearTimeout(_liveOpsRefreshTimer);
  _liveOpsRefreshTimer = setTimeout(async () => {
    // Only refresh if the panel is still in the DOM AND the user hasn't
    // navigated away from #automations.
    if (!document.getElementById("live-ops-panel")) return;
    if (!location.hash.startsWith("#automations")) return;
    const opsLive = await api("ops-live").catch(() => ({}));
    const newPanel = buildLiveOpsPanel(opsLive);
    const oldPanel = document.getElementById("live-ops-panel");
    if (oldPanel && newPanel) oldPanel.replaceWith(newPanel);
    else if (oldPanel && !newPanel) oldPanel.remove();
    scheduleLiveOpsRefresh();
  }, 15000);
}

function buildSystemAutomationsSection() {
  const wrap = el("div", { style: "margin-top: 32px;" });
  const headerRow = el(
    "div",
    {
      style:
        "display:flex;align-items:center;gap:8px;cursor:pointer;padding:8px 0;border-bottom:1px solid var(--border);",
    },
    [
      el("span", { id: "sysauto-toggle-icon" }, "▸"),
      el("span", { style: "font-weight:600;font-size:13px;" }, "System automations"),
      el(
        "span",
        { class: "muted small" },
        "background jobs you can't tune (yet) — surfaced for visibility",
      ),
    ],
  );
  const body = el("div", { id: "sysauto-body", style: "display:none;padding:8px 0;" }, [
    el("div", { class: "muted small" }, "Loading…"),
  ]);
  let loaded = false;
  headerRow.onclick = async () => {
    const open = body.style.display !== "none";
    body.style.display = open ? "none" : "block";
    document.getElementById("sysauto-toggle-icon").textContent = open ? "▸" : "▾";
    if (!loaded && !open) {
      const data = await api("ops-system-jobs").catch(() => ({}));
      const jobs = data?.jobs || [];
      body.innerHTML = "";
      if (jobs.length === 0) {
        body.appendChild(
          el(
            "div",
            { class: "muted small" },
            "No system jobs registered yet (bot may not have started its scheduler).",
          ),
        );
      } else {
        const table = el("table", { class: "table", style: "font-size:11px;" });
        table.appendChild(
          el("thead", {}, [
            el("tr", {}, [
              el("th", {}, "Job"),
              el("th", {}, "Category"),
              el("th", {}, "Schedule"),
              el("th", {}, "Last fired"),
              el("th", {}, "Runs"),
              el("th", {}, "Status"),
              el("th", {}, ""),
            ]),
          ]),
        );
        const tbody = el("tbody", {});
        for (const j of jobs) {
          const status = j.lastError
            ? el("span", { class: "badge badge-red", title: j.lastError }, "error")
            : j.isRunning
              ? el("span", { class: "badge badge-yellow" }, "running")
              : j.paused
                ? el("span", { class: "badge badge-gray" }, "paused")
                : el("span", { class: "badge badge-green" }, "ok");
          // Trigger button only for whitelisted job names. Same affordance
          // as the Live-now panel — keeps the System list useful as an
          // ops control surface, not just a status board.
          const triggerCell = j.triggerable
            ? el("td", {}, [
                el(
                  "button",
                  {
                    class: "btn btn-tiny btn-secondary",
                    type: "button",
                    title: `Run "${j.name}" now`,
                    onclick: async (ev) => {
                      ev.stopPropagation();
                      await withProgress(ev.currentTarget, async () => {
                        const r = await api("ops-job-trigger", { name: j.name });
                        if (r?.error) toast(r.error, "err");
                        else toast(`▶ ${j.name} started`, "ok", 4000);
                      });
                    },
                  },
                  "▶ Trigger",
                ),
              ])
            : el("td", { class: "muted small" }, "—");
          tbody.appendChild(
            el("tr", {}, [
              el("td", { class: "mono" }, j.name),
              el("td", { class: "muted" }, j.category || "—"),
              el("td", { class: "mono muted small" }, j.schedule || "—"),
              el(
                "td",
                { class: "muted mono small" },
                j.lastRunAt ? new Date(j.lastRunAt).toLocaleString() : "never",
              ),
              el(
                "td",
                { class: "mono" },
                `${j.runCount || 0}${j.errorCount ? ` (${j.errorCount} err)` : ""}`,
              ),
              el("td", {}, [status]),
              triggerCell,
            ]),
          );
        }
        table.appendChild(tbody);
        body.appendChild(table);
      }
      loaded = true;
    }
  };
  wrap.appendChild(headerRow);
  wrap.appendChild(body);
  return wrap;
}

// Clone a preset → custom workflow. Prompts the user for a name (default =
// "<preset name> (copy)"), calls the clone API, and re-renders the page so
// the new workflow appears in the "My workflows" section at the top.
async function cloneAutomation(presetWorkflow) {
  const defaultName = `${presetWorkflow.name} (copy)`;
  const name = window.prompt(`Name for your copy of "${presetWorkflow.name}"?`, defaultName);
  if (name === null) return; // user cancelled
  const trimmed = String(name).trim() || defaultName;
  toast("Cloning…", "info", 1200);
  const r = await api("automation-clone", { body: { presetId: presetWorkflow.id, name: trimmed } });
  if (r?.error) {
    toast(`Clone failed: ${r.error}`, "err");
    return;
  }
  toast(`Created "${trimmed}"`, "ok");
  logTelemetry("automations", "clone", { presetId: presetWorkflow.id });
  // Re-render so the new custom appears at the top
  await renderAutomations();
}

// Archive a custom workflow. Soft-delete (status='archived') so we keep the
// audit trail; the row drops out of the list but stays in the DB.
async function archiveAutomation(customWorkflow) {
  const ok = window.confirm(
    `Archive "${customWorkflow.name}"? You can re-clone the source preset later.`,
  );
  if (!ok) return;
  const r = await api("automation-archive-custom", { customId: customWorkflow.custom_id });
  if (r?.error) {
    toast(`Archive failed: ${r.error}`, "err");
    return;
  }
  toast("Archived", "ok");
  logTelemetry("automations", "archive", { customId: customWorkflow.custom_id });
  await renderAutomations();
}

// Trigger an immediate execution of a custom workflow. Only meaningful when
// the source preset's runner is wired (signal_twitter as of round 11). The
// engine logs a run row that shows up in the Recent runs panel within ~1s.
async function runAutomationNow(customWorkflow) {
  const __pv = await runWithPreview("custom_workflow", customWorkflow.custom_id, {});
  if (!__pv?.confirmed) return;
  toast(`Running "${customWorkflow.name}"…`, "info", 1500);
  const r = await api("automation-run-now", { customId: customWorkflow.custom_id });
  if (r?.error) {
    if (r.wired === false || r.error === "RUNNER_NOT_WIRED") {
      toast(
        `Engine wiring for ${customWorkflow.source_preset_id} is in progress — can't run yet`,
        "err",
        3500,
      );
    } else {
      toast(`Run failed: ${r.error}`, "err");
    }
    return;
  }
  toast("Ran — check Recent runs", "ok", 2000);
  logTelemetry("automations", "run_now", {
    customId: customWorkflow.custom_id,
    presetId: customWorkflow.source_preset_id,
  });
  // If the user is on the viewer for this workflow, refresh it so Recent
  // runs picks up the new row. Otherwise re-render the grid for last_run line.
  if (
    dashboardState.currentRoute === "automations/view" &&
    dashboardState.currentParams?.id === customWorkflow.id
  ) {
    await renderAutomationView({ id: customWorkflow.id });
  } else {
    await renderAutomations();
  }
}

// Flip a custom workflow's run-state. Cycles draft → active → paused → draft.
// "active" customs are picked up by the engine on the next scheduler tick;
// "paused" stops without losing the row; "draft" is the initial state and
// also where the user lands if they pause-then-decide-to-rework.
async function toggleAutomationStatus(customWorkflow) {
  const current = customWorkflow.status || "draft";
  const next = current === "draft" ? "active" : current === "active" ? "paused" : "draft"; // paused → draft (the "I'm working on it again" path)
  const r = await api("automation-update-custom", {
    customId: customWorkflow.custom_id,
    body: { status: next },
  });
  if (r?.error) {
    toast(`Status change failed: ${r.error}`, "err");
    return;
  }
  toast(
    `${next === "active" ? "Activated — engine picks it up next cycle" : next === "paused" ? "Paused" : "Back to draft"}`,
    "ok",
    2000,
  );
  logTelemetry("automations", "status_change", {
    customId: customWorkflow.custom_id,
    from: current,
    to: next,
  });
  await renderAutomations();
}

// Build one purpose section: sticky header + responsive grid of cards.
// Phase C — peer section to buildAutomationSection, but for workflow_compositions.
// Cards click through to the n8n canvas. Kept lightweight (no clone/archive
// actions — those live inside the canvas itself) so the card is a status pill
// + click target. Status counts surface inline.
function buildCompositionsSection(items) {
  const counts = items.reduce((acc, c) => {
    acc[c.status] = (acc[c.status] || 0) + 1;
    return acc;
  }, {});
  const summaryBits = [];
  if (counts.active) summaryBits.push(`${counts.active} active`);
  if (counts.paused) summaryBits.push(`${counts.paused} paused`);
  if (counts.draft) summaryBits.push(`${counts.draft} draft`);
  const summary = summaryBits.length > 0 ? summaryBits.join(" · ") : "—";

  const header = el(
    "div",
    {
      style: [
        "position:sticky",
        "top:0",
        "z-index:1",
        "background:linear-gradient(180deg,var(--bg-1) 70%,rgba(15,15,16,0))",
        "padding:8px 0 14px",
        "margin-bottom:4px",
      ].join(";"),
    },
    [
      el("div", { style: "display:flex;align-items:baseline;gap:10px;" }, [
        el("span", { style: "font-size:18px;line-height:1;" }, icon("layers")),
        el(
          "div",
          { style: "font-size:15px;font-weight:600;color:var(--text-1);letter-spacing:0.01em;" },
          "My workflows",
        ),
        el(
          "div",
          { style: "font-size:11px;font-family:ui-monospace,monospace;color:var(--text-4);" },
          `${items.length}`,
        ),
        el(
          "span",
          {
            style:
              "font-size:10px;font-family:ui-monospace,monospace;letter-spacing:0.06em;color:#5adada;background:rgba(90,218,218,0.12);padding:3px 7px;border-radius:10px;border:1px solid rgba(90,218,218,0.3);",
          },
          "COMPOSER",
        ),
      ]),
      el(
        "div",
        { style: "font-size:12px;color:var(--text-2);margin-top:3px;margin-left:28px;" },
        `Workflows you built in the canvas. ${summary}. Click any to open in canvas.`,
      ),
    ],
  );

  const grid = el("div", {
    style: "display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;",
  });
  for (const c of items) grid.appendChild(buildCompositionCard(c));

  return el("div", {}, [header, grid]);
}

function buildCompositionCard(comp) {
  const accent = "#5adada";
  const accentSoft = "rgba(90,218,218,0.25)";
  const status = comp.status || "draft";
  const statusStyles = {
    active: {
      bg: "rgba(16,185,129,0.18)",
      color: "#a7f3d0",
      border: "rgba(16,185,129,0.5)",
      label: "● ACTIVE",
    },
    paused: {
      bg: "rgba(234,179,8,0.18)",
      color: "#fde68a",
      border: "rgba(234,179,8,0.5)",
      label: "PAUSED",
    },
    draft: {
      bg: "rgba(90,218,218,0.12)",
      color: "#a7e8e8",
      border: "rgba(90,218,218,0.35)",
      label: "DRAFT",
    },
    archived: { bg: "#1f1f22", color: "#888", border: "#2a2a2e", label: "ARCHIVED" },
  };
  const st = statusStyles[status] || statusStyles.draft;

  // Phase G — hover-revealed action row, mirrors the custom_workflow card
  // pattern (Run now / status toggle / Killswitch). Wires to the existing
  // composition-* lifecycle endpoints already used by the Build page.
  // Buttons stopPropagation so card-level click (→ canvas) doesn't fire.
  const refreshActiveCockpit = () => {
    if (typeof renderAutomations === "function") renderAutomations();
  };
  const runNowBtn =
    status === "active" || status === "draft"
      ? el(
          "button",
          {
            type: "button",
            style:
              "padding:4px 10px;background:rgba(59,130,246,0.18);color:#93c5fd;border:1px solid rgba(59,130,246,0.5);border-radius:5px;font-size:11px;cursor:pointer;font-weight:600;",
            onclick: async (e) => {
              e.stopPropagation();
              const __pv = await runWithPreview("composition", comp.id, {});
              if (!__pv?.confirmed) return;
              const r = await api("composition-run", { id: comp.id, body: {} }).catch((err) => ({
                error: err?.message,
              }));
              if (r?.error) toast(`Run failed: ${r.error}`, "err", 4000);
              else toast(`▶ Triggered "${comp.name}"`, "ok", 2500);
            },
            title: "Trigger this composition now",
          },
          "▶ Run now",
        )
      : null;

  const statusActionLabel =
    status === "draft"
      ? "▶ Activate"
      : status === "active"
        ? "⏸ Pause"
        : status === "paused"
          ? "▶ Resume"
          : null;
  const statusToggleBtn = statusActionLabel
    ? el(
        "button",
        {
          type: "button",
          style:
            status === "active"
              ? "padding:4px 10px;background:rgba(234,179,8,0.15);color:#fde68a;border:1px solid rgba(234,179,8,0.4);border-radius:5px;font-size:11px;cursor:pointer;font-weight:500;"
              : "padding:4px 10px;background:rgba(16,185,129,0.15);color:#a7f3d0;border:1px solid rgba(16,185,129,0.4);border-radius:5px;font-size:11px;cursor:pointer;font-weight:500;",
          onclick: async (e) => {
            e.stopPropagation();
            const action = status === "active" ? "composition-pause" : "composition-activate";
            const r = await api(action, { id: comp.id }).catch((err) => ({ error: err?.message }));
            if (r?.error) {
              toast(`Status change failed: ${r.error}`, "err", 4000);
              return;
            }
            toast(status === "active" ? `⏸ Paused` : `▶ Activated`, "ok", 1800);
            refreshActiveCockpit();
          },
          title:
            status === "active"
              ? "Pause — stop running but keep settings"
              : status === "draft"
                ? "Activate — engine picks this up on the next cycle"
                : "Resume — flip back to active",
        },
        statusActionLabel,
      )
    : null;

  const killswitchBtn =
    status === "active" || status === "draft"
      ? el(
          "button",
          {
            type: "button",
            style:
              "padding:4px 10px;background:rgba(239,68,68,0.12);color:var(--red);border:1px solid rgba(239,68,68,0.4);border-radius:5px;font-size:11px;cursor:pointer;font-weight:500;",
            onclick: async (e) => {
              e.stopPropagation();
              if (await _composerKillswitch(comp)) refreshActiveCockpit();
            },
            title: "Killswitch — pause + cancel in-flight runs",
          },
          "🛑",
        )
      : null;

  const actionsRow = el(
    "div",
    {
      class: "composition-card-actions",
      style: "display:flex;gap:6px;opacity:0;transition:opacity 120ms;flex-shrink:0;",
    },
    [runNowBtn, statusToggleBtn, killswitchBtn].filter(Boolean),
  );

  // Step preview — first 4 play types joined
  const preview = (comp.plays || [])
    .map((p) => p.type)
    .filter(Boolean)
    .slice(0, 4)
    .join(" → ");

  // Channels summary
  const channels = (comp.channels || []).map((c) => c.platform).join(" · ");

  // Last-run + run count
  const lastRun = comp.last_run_at ? new Date(comp.last_run_at).toLocaleDateString() : "Never run";
  const runStat = `${comp.total_runs || 0} runs · ${comp.total_replies || 0} replies`;

  return el(
    "div",
    {
      style: [
        "display:flex",
        "background:var(--bg-1)",
        "border:1px solid var(--border)",
        "border-radius:8px",
        "overflow:hidden",
        "cursor:pointer",
        "transition:transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease",
        "min-height:96px",
      ].join(";"),
      onmouseover: (e) => {
        e.currentTarget.style.borderColor = accent;
        e.currentTarget.style.transform = "translateY(-1px)";
        e.currentTarget.style.boxShadow = `0 8px 24px -8px ${accentSoft}`;
        const a = e.currentTarget.querySelector(".composition-card-actions");
        if (a) a.style.opacity = "1";
      },
      onmouseout: (e) => {
        e.currentTarget.style.borderColor = "var(--border)";
        e.currentTarget.style.transform = "translateY(0)";
        e.currentTarget.style.boxShadow = "none";
        const a = e.currentTarget.querySelector(".composition-card-actions");
        if (a) a.style.opacity = "0";
      },
      onclick: () => {
        // Phase C: composer card → n8n canvas (not the legacy viewer)
        dashboardState.composerDraft = comp._raw || comp;
        navigate("workflows/canvas");
      },
    },
    [
      el("div", { style: `width:4px;background:${accent};flex-shrink:0;` }),
      el(
        "div",
        {
          style: "padding:14px 16px;display:flex;flex-direction:column;gap:6px;flex:1;min-width:0;",
        },
        [
          el("div", { style: "display:flex;align-items:flex-start;gap:10px;" }, [
            el(
              "div",
              {
                style: `font-size:18px;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(90,218,218,0.12);border-radius:6px;flex-shrink:0;`,
              },
              "🧩",
            ),
            el("div", { style: "flex:1;min-width:0;" }, [
              el(
                "div",
                {
                  style:
                    "font-size:14px;font-weight:600;color:var(--text-1);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                  title: comp.name,
                },
                comp.name || "(untitled)",
              ),
            ]),
            // Phase G — hover-revealed action row (Run now / Pause / Killswitch)
            actionsRow,
            el(
              "div",
              {
                style: `font-size:10px;font-family:ui-monospace,monospace;letter-spacing:0.06em;color:${st.color};background:${st.bg};padding:3px 7px;border-radius:10px;flex-shrink:0;border:1px solid ${st.border};font-weight:600;`,
              },
              st.label,
            ),
            // Phase H — pin button so users can shortcut compositions to the cockpit
            buildPinButton(`composition:${comp.id}`, comp.name || `Composition #${comp.id}`),
          ]),
          comp.description
            ? el(
                "div",
                {
                  style:
                    "font-size:12px;color:var(--text-2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;",
                },
                comp.description,
              )
            : null,
          preview
            ? el(
                "div",
                { style: "font-size:11px;color:#94a3b8;font-family:ui-monospace,monospace;" },
                preview,
              )
            : null,
          el(
            "div",
            { style: "display:flex;gap:12px;font-size:10px;color:var(--text-4);margin-top:2px;" },
            [
              channels ? el("span", {}, `${channels}`) : null,
              el("span", {}, `${lastRun}`),
              el("span", {}, runStat),
            ].filter(Boolean),
          ),
        ],
      ),
    ],
  );
}

function buildAutomationSection(group, items) {
  const header = el(
    "div",
    {
      style: [
        "position:sticky",
        "top:0",
        "z-index:1",
        "background:linear-gradient(180deg,var(--bg-1) 70%,rgba(15,15,16,0))",
        "padding:8px 0 14px",
        "margin-bottom:4px",
      ].join(";"),
    },
    [
      el("div", { style: "display:flex;align-items:baseline;gap:10px;" }, [
        el("span", { style: "font-size:18px;line-height:1;" }, group.icon),
        el(
          "div",
          { style: "font-size:15px;font-weight:600;color:var(--text-1);letter-spacing:0.01em;" },
          group.label,
        ),
        el(
          "div",
          { style: "font-size:11px;font-family:ui-monospace,monospace;color:var(--text-4);" },
          `${items.length}`,
        ),
      ]),
      group.blurb
        ? el(
            "div",
            { style: "font-size:12px;color:var(--text-2);margin-top:3px;margin-left:28px;" },
            group.blurb,
          )
        : null,
    ],
  );

  const grid = el("div", {
    style: "display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;",
  });
  for (const wf of items) {
    grid.appendChild(buildAutomationCard(wf, group));
  }

  return el("div", {}, [header, grid]);
}

// Single workflow card — icon stripe, title + status pill, description,
// step preview ("Every 15 min → Search Twitter → Decide..."), node count.
// `wf.kind === "custom"` cards show CUSTOM badge + Archive action.
// Presets show PRESET badge + Clone action (revealed on hover).
function buildAutomationCard(wf, group) {
  const isCustom = wf.kind === "custom";
  const accent = isCustom ? "#a855f7" : "#3b82f6"; // purple for custom, blue for presets
  const accentSoft = isCustom ? "rgba(168,85,247,0.25)" : "rgba(59,130,246,0.25)";

  // Action buttons.
  // Presets: just "+ Clone".
  // Customs: status toggle (Activate / Pause / Resume) + Archive.
  // All hover-revealed so cards stay clean by default.
  const status = isCustom ? wf.status || "draft" : null;
  const statusActionLabel =
    status === "draft" ? "▶ Activate" : status === "active" ? "⏸ Pause" : "↩ Reset to draft";
  const statusActionTitle =
    status === "draft"
      ? "Activate — engine will pick this up on the next cycle"
      : status === "active"
        ? "Pause — stop running but keep settings"
        : "Move back to draft so you can edit before activating again";
  // Wired-runners check is API-driven now (see _wiredRunnersCache populated
  // by renderAutomations from the list response's wired_runners field).
  // Pre-flight here ensures Run now button only renders when it'll work.
  const canRunNow = isCustom && status === "active" && isWiredRunner(wf.source_preset_id);

  const actionsRow = el(
    "div",
    {
      class: "automation-card-actions",
      style: "display:flex;gap:6px;opacity:0;transition:opacity 120ms;flex-shrink:0;",
    },
    isCustom
      ? [
          canRunNow
            ? el(
                "button",
                {
                  type: "button",
                  style:
                    "padding:4px 10px;background:rgba(59,130,246,0.18);color:#93c5fd;border:1px solid rgba(59,130,246,0.5);border-radius:5px;font-size:11px;cursor:pointer;font-weight:600;",
                  onclick: (e) => {
                    e.stopPropagation();
                    runAutomationNow(wf);
                  },
                  title: "Trigger this workflow now (don't wait for the next cron tick)",
                },
                "▶ Run now",
              )
            : null,
          el(
            "button",
            {
              type: "button",
              style:
                status === "active"
                  ? "padding:4px 10px;background:rgba(234,179,8,0.15);color:#fde68a;border:1px solid rgba(234,179,8,0.4);border-radius:5px;font-size:11px;cursor:pointer;font-weight:500;"
                  : status === "draft"
                    ? "padding:4px 10px;background:rgba(16,185,129,0.15);color:#a7f3d0;border:1px solid rgba(16,185,129,0.4);border-radius:5px;font-size:11px;cursor:pointer;font-weight:500;"
                    : "padding:4px 10px;background:var(--bg-3);color:var(--text-1);border:1px solid var(--border-strong);border-radius:5px;font-size:11px;cursor:pointer;",
              onclick: (e) => {
                e.stopPropagation();
                toggleAutomationStatus(wf);
              },
              title: statusActionTitle,
            },
            statusActionLabel,
          ),
          el(
            "button",
            {
              type: "button",
              style:
                "padding:4px 10px;background:var(--bg-3);color:var(--text-1);border:1px solid var(--border-strong);border-radius:5px;font-size:11px;cursor:pointer;",
              onclick: (e) => {
                e.stopPropagation();
                archiveAutomation(wf);
              },
              title: "Archive this custom workflow",
            },
            "Archive",
          ),
        ]
      : [
          el(
            "button",
            {
              type: "button",
              style:
                "padding:4px 10px;background:var(--bg-3);color:var(--text-1);border:1px solid var(--border-strong);border-radius:5px;font-size:11px;cursor:pointer;",
              onclick: (e) => {
                e.stopPropagation();
                cloneAutomation(wf);
              },
              title: "Clone this preset and customize it",
            },
            "+ Clone",
          ),
        ],
  );

  const card = el(
    "div",
    {
      style: [
        "background:var(--bg-2)",
        "border:1px solid var(--border)",
        "border-radius:10px",
        "padding:0",
        "cursor:pointer",
        "transition:border-color 150ms,transform 150ms,box-shadow 150ms",
        "display:flex",
        "overflow:hidden",
        "min-height:148px",
      ].join(";"),
      onmouseover: (e) => {
        e.currentTarget.style.borderColor = accent;
        e.currentTarget.style.transform = "translateY(-1px)";
        e.currentTarget.style.boxShadow = `0 8px 24px -8px ${accentSoft}`;
        const a = e.currentTarget.querySelector(".automation-card-actions");
        if (a) a.style.opacity = "1";
      },
      onmouseout: (e) => {
        e.currentTarget.style.borderColor = "var(--border)";
        e.currentTarget.style.transform = "translateY(0)";
        e.currentTarget.style.boxShadow = "none";
        const a = e.currentTarget.querySelector(".automation-card-actions");
        if (a) a.style.opacity = "0";
      },
      onclick: () => navigate(`automations/view/${wf.id}`),
    },
    [
      // Vertical color stripe (matches purpose group, or accent for custom)
      el("div", {
        style: `width:4px;background:${isCustom ? accent : automationGroupColor(group.key)};flex-shrink:0;`,
      }),
      // Body
      el(
        "div",
        {
          style: "padding:14px 16px;display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;",
        },
        [
          // Top row: icon + title + badge
          el("div", { style: "display:flex;align-items:flex-start;gap:10px;" }, [
            el(
              "div",
              {
                style: `font-size:20px;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:${isCustom ? "#3a1f4f" : automationGroupColorTint(group.key)};border-radius:6px;flex-shrink:0;`,
              },
              isCustom ? "✏️" : group.icon,
            ),
            el("div", { style: "flex:1;min-width:0;" }, [
              el(
                "div",
                {
                  style:
                    "font-size:14px;font-weight:600;color:var(--text-1);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                  title: wf.name,
                },
                wf.name,
              ),
            ]),
            // Badge: status-aware for customs, plain "PRESET" for presets.
            // ACTIVE = green pulse, DRAFT = purple, PAUSED = yellow, default purple.
            (() => {
              if (!isCustom) {
                return el(
                  "div",
                  {
                    style:
                      "font-size:10px;font-family:ui-monospace,monospace;letter-spacing:0.06em;color:var(--text-3);background:var(--bg-3);padding:3px 7px;border-radius:10px;flex-shrink:0;",
                  },
                  "PRESET",
                );
              }
              const s = wf.status || "draft";
              const styles = {
                active: {
                  bg: "rgba(16,185,129,0.18)",
                  color: "#a7f3d0",
                  border: "rgba(16,185,129,0.5)",
                  label: "● ACTIVE",
                },
                paused: {
                  bg: "rgba(234,179,8,0.18)",
                  color: "#fde68a",
                  border: "rgba(234,179,8,0.5)",
                  label: "PAUSED",
                },
                draft: {
                  bg: "#3a1f4f",
                  color: "#ede9fe",
                  border: "rgba(168,85,247,0.4)",
                  label: "DRAFT",
                },
              };
              const st = styles[s] || styles.draft;
              return el(
                "div",
                {
                  style: `font-size:10px;font-family:ui-monospace,monospace;letter-spacing:0.06em;color:${st.color};background:${st.bg};padding:3px 7px;border-radius:10px;flex-shrink:0;border:1px solid ${st.border};font-weight:600;`,
                },
                st.label,
              );
            })(),
            // Pin button — pin id distinguishes preset vs custom so the
            // shortcuts row knows which runner to call.
            buildPinButton(isCustom ? `custom:${wf.id}` : `preset:${wf.id}`, wf.name || wf.id),
          ]),
          // Description (2 lines max)
          wf.description
            ? el(
                "div",
                {
                  style:
                    "font-size:12px;color:var(--text-2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;",
                },
                wf.description,
              )
            : null,
          // Step preview (the killer feature — narrative voice on the card)
          wf.preview
            ? el(
                "div",
                {
                  style: [
                    "font-size:11px",
                    "color:var(--text-1)",
                    "background:var(--bg-1)",
                    "border:1px solid var(--border)",
                    "border-radius:6px",
                    "padding:8px 10px",
                    "line-height:1.5",
                    "font-family:system-ui,-apple-system,sans-serif",
                    "display:-webkit-box",
                    "-webkit-line-clamp:2",
                    "-webkit-box-orient:vertical",
                    "overflow:hidden",
                    "margin-top:auto",
                  ].join(";"),
                },
                wf.preview,
              )
            : null,
          // Last-run line — only on customs that have at least one run logged.
          // Pulls "Last run: 2 min ago · 12/100 kept" from the API's last_run summary.
          isCustom && wf.last_run ? buildLastRunLine(wf.last_run) : null,
          // Schedule-override badge — only when the user changed the cron.
          isCustom && wf.schedule_min
            ? el(
                "div",
                {
                  style:
                    "display:inline-flex;align-items:center;gap:6px;font-size:10px;font-family:ui-monospace,monospace;color:#a78bfa;background:rgba(168,85,247,0.08);border:1px solid rgba(168,85,247,0.25);border-radius:5px;padding:3px 8px;width:fit-content;",
                  title: "Custom schedule — runs at least this often (or less)",
                },
                [el("span", {}, "⏰"), el("span", {}, fmtIntervalMin(wf.schedule_min))],
              )
            : null,
          // Footer: node count + actions
          el(
            "div",
            {
              style:
                "display:flex;gap:8px;font-size:10px;font-family:ui-monospace,monospace;color:#52525b;align-items:center;",
            },
            [
              el("span", {}, `${wf.node_count} steps`),
              !isCustom && wf.engine_runner
                ? el("span", { style: "opacity:0.6;" }, `· ${wf.engine_runner}`)
                : null,
              isCustom && wf.source_preset_id
                ? el("span", { style: "opacity:0.6;" }, `· from ${wf.source_preset_id}`)
                : null,
              el("span", { style: "flex:1;" }), // spacer to push actions right
              actionsRow,
            ],
          ),
        ],
      ),
    ],
  );
  return card;
}

// Top-of-Automations stat strip showing aggregate run health for the last
// 24h. Renders 4 stats (runs / success rate / items kept / errors) plus a
// secondary line summarizing how many workflows have been firing.
function buildAutomationRunHealth(summary) {
  const total = Number(summary.total_runs) || 0;
  const success = Number(summary.success) || 0;
  const errored = Number(summary.error) || 0;
  const itemsKept = Number(summary.items_kept_total) || 0;
  const activeCount = Number(summary.active_workflow_count) || 0;
  const successRate = total > 0 ? Math.round((success / total) * 100) : 0;

  const successColor = successRate >= 80 ? "#10b981" : successRate >= 50 ? "#eab308" : "#ef4444";
  const successText = successRate >= 80 ? "#a7f3d0" : successRate >= 50 ? "#fde68a" : "#f87171";

  function stat(value, label, color) {
    return el("div", { style: "flex:1;min-width:120px;" }, [
      el(
        "div",
        {
          style: `font-size:22px;font-weight:700;line-height:1;color:${color || "#e5e7eb"};font-variant-numeric:tabular-nums;`,
        },
        String(value),
      ),
      el(
        "div",
        {
          style:
            "font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-4);margin-top:6px;font-weight:500;",
        },
        label,
      ),
    ]);
  }

  return el(
    "div",
    {
      style:
        "display:flex;gap:24px;align-items:flex-start;padding:14px 18px;margin:0 0 24px;background:var(--bg-1);border:1px solid var(--border);border-radius:8px;flex-wrap:wrap;",
    },
    [
      stat(total, "runs · last 24h"),
      stat(`${successRate}%`, "success rate", successText),
      stat(itemsKept.toLocaleString(), "items kept"),
      errored > 0 ? stat(errored, "errors", "#fca5a5") : null,
      // Vertical divider then secondary text
      el("div", { style: "border-left:1px solid var(--border);align-self:stretch;" }),
      el(
        "div",
        { style: "flex:2;min-width:200px;font-size:12px;color:var(--text-2);line-height:1.6;" },
        [
          activeCount > 0
            ? el("div", {}, [
                el(
                  "span",
                  {},
                  `${activeCount} workflow${activeCount === 1 ? "" : "s"} firing · last run `,
                ),
                el(
                  "span",
                  { style: "color:var(--text-1);font-family:ui-monospace,monospace;" },
                  summary.last_run_at ? fmtRel(summary.last_run_at) : "—",
                ),
              ])
            : null,
          errored === 0 && successRate >= 80
            ? el("div", { style: "color:var(--text-4);font-size:11px;" }, "All systems healthy.")
            : errored > 0
              ? el(
                  "div",
                  { style: "color:var(--red);font-size:11px;" },
                  `${errored} error${errored === 1 ? "" : "s"} in the last 24h — open an active workflow to inspect.`,
                )
              : null,
        ],
      ),
      // Health dot, far right
      el("div", {
        style: `width:10px;height:10px;border-radius:50%;background:${successColor};margin-top:6px;align-self:flex-start;flex-shrink:0;box-shadow:0 0 0 4px ${successColor}22;`,
        title: `Health: ${successRate}% success rate over ${total} runs`,
      }),
    ],
  );
}

// Build the "Recent runs" panel shown above the canvas in the viewer.
// Each run gets a small status pill with timestamp + counts; click to expand
// the stats blob (keywords scanned, threshold used, etc).
// Renders the "Recent runs" panel above the canvas in the workflow viewer.
// New shape (round 13):
//   [summary line] 12 of last 10 · 80% success · 47 items kept
//   [filter pills] All · Success · Skipped · Errors        [▶ Run now]
//   [pill strip]   ●now ●1m ●3m ●5m ●1h …
//   [expanded]     stats grid for the clicked pill (toggles)
//
// Local state lives on the wrap element via dataset so re-rendering on
// filter change doesn't lose context.
function buildRecentRunsPanel(runs, opts = {}) {
  const wrap = el("div", {
    style:
      "margin:8px 0 16px;padding:12px 14px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;",
  });
  // Local state: which pill is expanded (null = none) + filter status
  const state = { expandedId: null, filter: "all" };

  // Summary line
  const total = runs.length;
  const successCount = runs.filter((r) => r.status === "success").length;
  const skippedCount = runs.filter((r) => r.status === "skipped").length;
  const errorCount = runs.filter((r) => r.status === "error").length;
  const itemsKeptTotal = runs.reduce((s, r) => s + (Number(r.items_kept) || 0), 0);
  const successRate = total > 0 ? Math.round((successCount / total) * 100) : 0;

  function makeFilterPill(label, key, count) {
    const isActive = state.filter === key;
    return el(
      "button",
      {
        type: "button",
        "data-filter": key,
        style: `padding:3px 10px;background:${isActive ? "#27272a" : "transparent"};color:${isActive ? "#e5e7eb" : "#9ca3af"};border:1px solid ${isActive ? "#3f3f46" : "#1f1f22"};border-radius:12px;font-size:11px;cursor:pointer;font-family:system-ui,sans-serif;font-weight:${isActive ? 600 : 400};`,
        onclick: () => {
          state.filter = key;
          rerender();
        },
      },
      count != null ? `${label} (${count})` : label,
    );
  }

  function rerender() {
    wrap.innerHTML = "";

    // Top row: title + summary + run-now button
    const top = el(
      "div",
      {
        style:
          "display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.06em;font-weight:600;margin-bottom:8px;",
      },
      [
        el("span", {}, "Recent runs"),
        total > 0
          ? el(
              "span",
              {
                style:
                  "color:#52525b;font-weight:400;text-transform:none;letter-spacing:0;font-family:ui-monospace,monospace;",
              },
              `${total} of last 10`,
            )
          : null,
        total > 0
          ? el(
              "span",
              {
                style: "color:#52525b;font-weight:400;text-transform:none;letter-spacing:0;",
              },
              "·",
            )
          : null,
        total > 0
          ? el(
              "span",
              {
                style: `font-weight:500;text-transform:none;letter-spacing:0;color:${successRate >= 80 ? "#a7f3d0" : successRate >= 50 ? "#fde68a" : "var(--red)"};`,
              },
              `${successRate}% success`,
            )
          : null,
        itemsKeptTotal > 0
          ? el(
              "span",
              {
                style: "color:#52525b;font-weight:400;text-transform:none;letter-spacing:0;",
              },
              "·",
            )
          : null,
        itemsKeptTotal > 0
          ? el(
              "span",
              {
                style: "color:var(--text-1);font-weight:500;text-transform:none;letter-spacing:0;",
              },
              `${itemsKeptTotal} items kept`,
            )
          : null,
        el("span", { style: "flex:1;" }),
        opts.canRunNow && opts.onRunNow
          ? el(
              "button",
              {
                type: "button",
                style:
                  "padding:4px 12px;background:rgba(59,130,246,0.18);color:#93c5fd;border:1px solid rgba(59,130,246,0.5);border-radius:5px;font-size:11px;cursor:pointer;font-weight:600;text-transform:none;letter-spacing:0;font-family:system-ui,sans-serif;",
                title: "Trigger this workflow immediately",
                onclick: () => opts.onRunNow(),
              },
              "▶ Run now",
            )
          : null,
      ],
    );
    wrap.appendChild(top);

    // Empty state
    if (total === 0) {
      wrap.appendChild(
        el(
          "div",
          {
            style: "font-size:12px;color:var(--text-4);padding:6px 0;",
          },
          "No runs yet — the engine will pick this up on the next scheduled cycle.",
        ),
      );
      return;
    }

    // Filter pill row — only show when there's variety to filter
    if (total > 1) {
      const filters = el(
        "div",
        { style: "display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;" },
        [
          makeFilterPill("All", "all", total),
          successCount > 0 ? makeFilterPill("Success", "success", successCount) : null,
          skippedCount > 0 ? makeFilterPill("Skipped", "skipped", skippedCount) : null,
          errorCount > 0 ? makeFilterPill("Errors", "error", errorCount) : null,
        ],
      );
      wrap.appendChild(filters);
    }

    const filtered = state.filter === "all" ? runs : runs.filter((r) => r.status === state.filter);

    // Horizontal pill strip — clicking toggles inline expansion below
    const strip = el("div", { style: "display:flex;flex-wrap:wrap;gap:6px;" });
    const palette = {
      success: {
        bg: "rgba(16,185,129,0.12)",
        border: "rgba(16,185,129,0.4)",
        text: "#a7f3d0",
        dot: "#10b981",
      },
      skipped: {
        bg: "rgba(148,163,184,0.12)",
        border: "rgba(148,163,184,0.4)",
        text: "#cbd5e1",
        dot: "#94a3b8",
      },
      error: {
        bg: "rgba(239,68,68,0.12)",
        border: "rgba(239,68,68,0.4)",
        text: "#fca5a5",
        dot: "#ef4444",
      },
      running: {
        bg: "rgba(59,130,246,0.12)",
        border: "rgba(59,130,246,0.4)",
        text: "#93c5fd",
        dot: "#3b82f6",
      },
    };
    for (const r of filtered) {
      const p = palette[r.status] || palette.skipped;
      const when = fmtRel(r.finished_at || r.started_at);
      let label = when;
      if (r.status === "success") label += ` · ${r.items_kept ?? 0}/${r.items_processed ?? 0}`;
      else if (r.status === "skipped") label += " · skipped";
      else if (r.status === "error") label += " · failed";

      const isExpanded = state.expandedId === r.id;
      // Manual runs (Run-now) get a small ▶ indicator; scheduled runs use ⏰.
      // null falls back to '⏰' (pre-migration rows).
      const isManual = r.triggered_by === "manual";
      const triggerIcon = isManual ? "▶" : "⏰";
      const triggerTitle = isManual ? "Manual (Run now)" : "Scheduled (cron)";
      const pill = el(
        "button",
        {
          type: "button",
          style: `display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:${p.bg};border:1px solid ${isExpanded ? p.dot : p.border};border-radius:14px;font-size:11px;color:${p.text};cursor:pointer;font-family:ui-monospace,monospace;${isExpanded ? "box-shadow:0 0 0 2px " + p.bg + ";" : ""}`,
          title: `${triggerTitle} — click to expand stats`,
          onclick: () => {
            state.expandedId = isExpanded ? null : r.id;
            rerender();
          },
        },
        [
          el("span", {
            style: `display:inline-block;width:6px;height:6px;border-radius:50%;background:${p.dot};`,
          }),
          el("span", { style: "opacity:0.7;font-size:10px;" }, triggerIcon),
          el("span", {}, label),
          isExpanded ? el("span", { style: "opacity:0.5;" }, "▾") : null,
        ],
      );
      strip.appendChild(pill);
    }
    wrap.appendChild(strip);

    // Expanded detail panel — appears below the strip when a pill is selected.
    if (state.expandedId != null) {
      const r = runs.find((x) => x.id === state.expandedId);
      if (r) wrap.appendChild(buildRunDetailPanel(r, palette[r.status] || palette.skipped));
    }
  }

  rerender();
  return wrap;
}

// Detail card shown beneath the pill strip when a run is selected. Renders
// stats as a compact grid + duration + error if present.
function buildRunDetailPanel(run, palette) {
  const card = el("div", {
    style: `margin-top:10px;padding:10px 12px;background:${palette.bg};border:1px solid ${palette.border};border-radius:6px;`,
  });

  const triggeredBy = run.triggered_by === "manual" ? "▶ Manual" : "⏰ Scheduled";
  // Verification badge: a green ✓ "Verified working" or red ✗ "Marked broken"
  // chip if the user has stamped this run after checking side effects.
  const verifyBadge =
    run.verified_status === "working"
      ? el(
          "span",
          {
            style:
              "color:#a7f3d0;background:rgba(16,185,129,0.18);border:1px solid rgba(16,185,129,0.5);padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;",
          },
          "Verified",
        )
      : run.verified_status === "broken"
        ? el(
            "span",
            {
              style:
                "color:var(--red);background:rgba(239,68,68,0.18);border:1px solid rgba(239,68,68,0.5);padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;",
              title: run.verified_notes || "",
            },
            "Broken",
          )
        : null;
  const meta = el(
    "div",
    {
      style:
        "display:flex;gap:14px;font-size:11px;color:var(--text-2);font-family:ui-monospace,monospace;margin-bottom:8px;flex-wrap:wrap;align-items:center;",
    },
    [
      el(
        "span",
        {
          style: `color:${palette.text};font-weight:600;text-transform:uppercase;letter-spacing:0.05em;`,
        },
        run.status,
      ),
      el("span", { style: "color:var(--text-4);" }, triggeredBy),
      el("span", {}, `started ${fmtRel(run.started_at)}`),
      run.finished_at ? el("span", {}, `· ${fmtDurationMs(run)}`) : null,
      run.items_processed != null ? el("span", {}, `· processed ${run.items_processed}`) : null,
      run.items_kept != null && run.status === "success"
        ? el("span", {}, `· kept ${run.items_kept}`)
        : null,
      verifyBadge,
    ],
  );
  card.appendChild(meta);

  if (run.error) {
    card.appendChild(
      el(
        "div",
        {
          style:
            "font-size:12px;color:var(--red);background:rgba(239,68,68,0.06);border:1px solid rgba(239,68,68,0.2);border-radius:4px;padding:8px 10px;font-family:ui-monospace,monospace;line-height:1.5;",
        },
        run.error,
      ),
    );
  }

  if (run.stats && Object.keys(run.stats).length > 0) {
    const grid = el("div", {
      style:
        "display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:12px;font-family:ui-monospace,monospace;",
    });
    for (const [k, v] of Object.entries(run.stats)) {
      grid.appendChild(el("div", { style: "color:var(--text-4);" }, k));
      if (Array.isArray(v)) {
        // Array-valued stat (e.g. error_samples) — render each entry on its
        // own line so the operator sees WHAT failed, not just a count.
        grid.appendChild(
          el(
            "div",
            {
              style: "display:flex;flex-direction:column;gap:3px;",
            },
            v.length === 0
              ? [el("span", { style: "color:var(--text-4);font-style:italic;" }, "(none)")]
              : v.map((item) =>
                  el(
                    "div",
                    {
                      style: "color:var(--red);word-break:break-word;line-height:1.45;",
                    },
                    String(item),
                  ),
                ),
          ),
        );
      } else {
        grid.appendChild(
          el("div", { style: "color:var(--text-1);word-break:break-word;" }, String(v)),
        );
      }
    }
    card.appendChild(grid);
  } else if (!run.error) {
    card.appendChild(
      el(
        "div",
        { style: "font-size:11px;color:var(--text-4);font-style:italic;" },
        "No stats recorded for this run.",
      ),
    );
  }

  // Verification row — user stamps the run as working or broken after
  // checking the side effects (signal table populated, DM landed, etc.).
  // Notes prompt fires on ✗ so the user records WHY it's broken; ✓ is
  // one-click. Re-clicking the same status clears the mark.
  const currentStatus = run.verified_status;
  const verifyRow = el(
    "div",
    {
      style:
        "margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.05);display:flex;align-items:center;gap:8px;",
    },
    [
      el(
        "span",
        {
          style:
            "font-size:11px;color:var(--text-4);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;",
        },
        "Verify",
      ),
      el(
        "button",
        {
          type: "button",
          title:
            currentStatus === "working" ? "Click to clear" : "Mark this run as verified-working",
          style: `padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;font-family:system-ui,sans-serif;border:1px solid ${currentStatus === "working" ? "rgba(16,185,129,0.6)" : "rgba(16,185,129,0.3)"};background:${currentStatus === "working" ? "rgba(16,185,129,0.18)" : "transparent"};color:#a7f3d0;font-weight:${currentStatus === "working" ? 600 : 400};`,
          onclick: async (ev) => {
            const next = currentStatus === "working" ? null : "working";
            await withProgress(ev.currentTarget, async () => {
              const r = await api("ops-run-verify", { runId: run.id, body: { status: next } });
              if (r?.error) toast(r.error, "err");
              else {
                toast(next ? "Marked working" : "Cleared", "ok", 2000);
                // Re-render the parent automation view so the badge updates
                // and coverage stats refresh.
                if (
                  typeof renderAutomations === "function" &&
                  location.hash.startsWith("#automations/view")
                ) {
                  const parts = location.hash.replace(/^#/, "").split("/");
                  if (parts[0] === "automations" && parts[1] === "view" && parts[2]) {
                    renderAutomationView({ id: parts[2] });
                    return;
                  }
                }
              }
            });
          },
        },
        "Working",
      ),
      el(
        "button",
        {
          type: "button",
          title:
            currentStatus === "broken"
              ? "Click to clear"
              : "Mark this run as broken (you'll be prompted for notes)",
          style: `padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;font-family:system-ui,sans-serif;border:1px solid ${currentStatus === "broken" ? "rgba(239,68,68,0.6)" : "rgba(239,68,68,0.3)"};background:${currentStatus === "broken" ? "rgba(239,68,68,0.18)" : "transparent"};color:var(--red);font-weight:${currentStatus === "broken" ? 600 : 400};`,
          onclick: async (ev) => {
            const next = currentStatus === "broken" ? null : "broken";
            let notes = run.verified_notes || "";
            if (next === "broken") {
              const input = prompt("Why is this broken? (one line, optional)", notes);
              if (input == null) return; // user cancelled
              notes = input;
            }
            await withProgress(ev.currentTarget, async () => {
              const r = await api("ops-run-verify", {
                runId: run.id,
                body: { status: next, notes: next ? notes : null },
              });
              if (r?.error) toast(r.error, "err");
              else {
                toast(next ? "Marked broken" : "Cleared", "ok", 2000);
                if (
                  typeof renderAutomations === "function" &&
                  location.hash.startsWith("#automations/view")
                ) {
                  const parts = location.hash.replace(/^#/, "").split("/");
                  if (parts[0] === "automations" && parts[1] === "view" && parts[2]) {
                    renderAutomationView({ id: parts[2] });
                    return;
                  }
                }
              }
            });
          },
        },
        "Broken",
      ),
      run.verified_notes
        ? el(
            "span",
            { class: "muted small", style: "font-style:italic;flex:1;text-align:right;" },
            `note: ${run.verified_notes}`,
          )
        : null,
    ],
  );
  card.appendChild(verifyRow);

  return card;
}

// Format a run's duration (started_at → finished_at). Returns "1.2s" style.
function fmtDurationMs(run) {
  if (!run.started_at || !run.finished_at) return "";
  const start = new Date(run.started_at).getTime();
  const end = new Date(run.finished_at).getTime();
  const ms = end - start;
  if (!Number.isFinite(ms) || ms < 0) return "";
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
  return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}

// Compact "Last run" line for an automation card. Shows status + relative
// time + items_kept/items_processed counts. Color-codes by run status so
// errors stand out without ceremony.
function buildLastRunLine(lastRun) {
  if (!lastRun) return null;
  const status = lastRun.status || "unknown";
  const palette = {
    success: { dot: "#10b981", text: "#a7f3d0" },
    skipped: { dot: "#94a3b8", text: "#cbd5e1" },
    error: { dot: "#ef4444", text: "#fca5a5" },
    running: { dot: "#3b82f6", text: "#93c5fd" },
  };
  const c = palette[status] || palette.skipped;
  const when = fmtRel(lastRun.finished_at || lastRun.started_at);
  let count = "";
  if (status === "success") {
    const kept = lastRun.items_kept ?? 0;
    const processed = lastRun.items_processed ?? 0;
    count = ` · ${kept}/${processed} kept`;
  } else if (status === "skipped") {
    count = " · skipped";
  } else if (status === "error") {
    count = " · failed";
  } else if (status === "running") {
    count = " · running…";
  }
  const triggerIcon = lastRun.triggered_by === "manual" ? "▶" : "⏰";
  return el(
    "div",
    {
      style:
        "display:flex;gap:8px;font-size:11px;font-family:system-ui,sans-serif;align-items:center;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;padding:6px 10px;",
      title: `Last run: ${status} ${when} (${lastRun.triggered_by === "manual" ? "manual" : "scheduled"})`,
    },
    [
      el("span", {
        style: `display:inline-block;width:7px;height:7px;border-radius:50%;background:${c.dot};flex-shrink:0;`,
      }),
      el("span", { style: "color:var(--text-4);font-size:10px;" }, triggerIcon),
      el("span", { style: `color:${c.text};font-weight:500;` }, `Last run ${when}`),
      el("span", { style: "color:var(--text-4);" }, count),
    ],
  );
}

// Section showing wizard-based operation templates that don't have a visual
// preset equivalent. Each card opens the existing /new-workflow/:type wizard.
// Round 17 merge: Automations is now the single primary surface, so the old
// Operations Catalog content shows up here instead of as a separate sidebar
// entry. /operations route still works for direct deep-links + ⌘K.
// Platform actions section — surfaces in-browser content-script actions
// (LinkedIn / Twitter / Telegram floating launcher buttons) on the
// Automations page. Source: chrome-extension/platform-actions.js
// (window.PLATFORM_ACTIONS).
function buildPlatformActionsSection(actions, opts = {}) {
  const platforms = {
    linkedin: { icon: "💼", label: "LinkedIn", color: "#0a66c2" },
    twitter: { icon: "𝕏", label: "Twitter / X", color: "#1da1f2" },
    telegram: { icon: "✈️", label: "Telegram", color: "#0088cc" },
    email: { icon: "📧", label: "Email", color: "#ea4335" },
  };
  // Group by platform preserving catalog order
  const byPlatform = new Map();
  for (const a of actions) {
    if (!byPlatform.has(a.platform)) byPlatform.set(a.platform, []);
    byPlatform.get(a.platform).push(a);
  }

  const wrap = el("div", {});
  // Header — skipped when rendered inside a per-account sub-group.
  if (!opts.hideHeader)
    wrap.appendChild(
      el(
        "div",
        {
          style: [
            "position:sticky",
            "top:0",
            "z-index:1",
            "background:linear-gradient(180deg,var(--bg-1) 70%,rgba(15,15,16,0))",
            "padding:8px 0 14px",
            "margin-bottom:4px",
          ].join(";"),
        },
        [
          el("div", { style: "display:flex;align-items:baseline;gap:10px;" }, [
            el("span", { style: "font-size:18px;line-height:1;" }, icon("globe")),
            el(
              "div",
              {
                style: "font-size:15px;font-weight:600;color:var(--text-1);letter-spacing:0.01em;",
              },
              "Platform actions",
            ),
            el(
              "div",
              { style: "font-size:11px;font-family:ui-monospace,monospace;color:var(--text-4);" },
              `${actions.length}`,
            ),
          ]),
          el(
            "div",
            { style: "font-size:12px;color:var(--text-2);margin-top:3px;margin-left:28px;" },
            "In-browser actions you trigger from the floating launcher on each platform. Click any to view its module map.",
          ),
        ],
      ),
    );

  // Per-platform sub-section
  for (const [platformKey, items] of byPlatform) {
    const meta = platforms[platformKey] || { icon: "·", label: platformKey, color: "#94a3b8" };
    wrap.appendChild(
      el(
        "div",
        {
          style:
            "display:flex;align-items:baseline;gap:8px;margin:12px 0 8px;padding-left:6px;border-left:3px solid " +
            meta.color +
            ";padding-left:10px;",
        },
        [
          el("span", { style: "font-size:14px;line-height:1;" }, meta.icon),
          el("div", { style: "font-size:13px;font-weight:600;color:var(--text-1);" }, meta.label),
          el(
            "div",
            { style: "font-size:10px;font-family:ui-monospace,monospace;color:var(--text-4);" },
            `${items.length} action${items.length === 1 ? "" : "s"}`,
          ),
        ],
      ),
    );

    const grid = el("div", {
      style:
        "display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;margin-bottom:6px;",
    });
    for (const a of items) grid.appendChild(buildPlatformActionCard(a, meta));
    wrap.appendChild(grid);
  }

  return wrap;
}

function buildPlatformActionCard(action, platformMeta) {
  const hasMap = !!action.moduleMap;
  const card = el(
    "div",
    {
      class: "card",
      style: "padding:12px 14px;display:flex;flex-direction:column;gap:8px;",
    },
    [
      // Title row: name + in-browser badge + pin
      el("div", { style: "display:flex;align-items:baseline;gap:8px;" }, [
        el(
          "div",
          { style: "font-size:13px;font-weight:600;color:var(--text-1);flex:1;" },
          action.label,
        ),
        el(
          "span",
          {
            style:
              "font-size:9px;color:" +
              platformMeta.color +
              ";background:rgba(255,255,255,0.04);border:1px solid " +
              platformMeta.color +
              "55;padding:2px 6px;border-radius:9px;letter-spacing:0.04em;text-transform:uppercase;font-weight:600;",
            title:
              "Runs in your browser via the floating launcher panel on this platform — not as a backend cron job.",
          },
          "In-browser",
        ),
        buildPinButton(`platform:${action.id}`, action.label),
      ]),
      // Description
      el("div", { class: "muted small", style: "line-height:1.4;" }, action.description),
      // Action row: View map (if has one) + Open platform
      el("div", { style: "display:flex;gap:6px;margin-top:auto;padding-top:6px;" }, [
        hasMap
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "View the conceptual module map (DAG) for this action",
                onclick: () => openPlatformActionDrawer(action),
              },
              "Module map",
            )
          : el(
              "span",
              {
                class: "muted small",
                style: "padding:4px 8px;font-style:italic;",
                title:
                  "A visual module map for this action hasn't been authored yet. The action still works on the platform.",
              },
              "Module map: TBD",
            ),
        el("span", { style: "flex:1;" }),
        el(
          "button",
          {
            class: "btn btn-tiny btn-primary",
            type: "button",
            title: `Open ${platformMeta.label} in a new tab where you can click this action from the floating launcher`,
            onclick: () => {
              try {
                chrome.tabs.create({ url: action.launchUrl });
              } catch {
                window.open(action.launchUrl, "_blank");
              }
            },
          },
          `Open in ${platformMeta.label}`,
        ),
      ]),
    ],
  );
  return card;
}

// Drawer that renders the action's hand-authored module map via the
// existing workflow-viewer (window.WorkflowViewer.render). Uses a local
// minimal registry built from the workflow nodes registry so node-type
// labels render correctly. Read-only.
async function openPlatformActionDrawer(action) {
  // Lazy-fetch the workflow registry once. Cached on window so subsequent
  // opens don't refetch. The action name is automations-registry per the
  // background.js dispatch map.
  if (!window._workflowRegistryCache) {
    try {
      const r = await api("automations-registry");
      window._workflowRegistryCache = r?.registry || { nodes: [] };
    } catch {
      window._workflowRegistryCache = { nodes: [] };
    }
  }
  const registry = window._workflowRegistryCache;

  // openDrawer takes (title, contentNode) — not an options object.
  const canvasId = `platform-action-canvas-${action.id}`;
  const body = el(
    "div",
    { style: "display:flex;flex-direction:column;gap:14px;height:100%;min-height:520px;" },
    [
      el("div", { class: "muted", style: "font-size:13px;line-height:1.5;" }, action.description),
      el("div", { style: "display:flex;gap:8px;align-items:center;" }, [
        el(
          "button",
          {
            class: "btn btn-primary btn-tiny",
            type: "button",
            onclick: () => {
              try {
                chrome.tabs.create({ url: action.launchUrl });
              } catch {
                window.open(action.launchUrl, "_blank");
              }
            },
          },
          "Launch on platform",
        ),
        el("span", { class: "muted small" }, `Source: ${action.platform} content script`),
      ]),
      el("div", {
        id: canvasId,
        style:
          "flex:1;min-height:400px;border:1px solid var(--border);border-radius:6px;background:var(--bg-1);position:relative;",
      }),
    ],
  );
  openDrawer(action.label, body);

  // Render the DAG into the canvas div. The viewer needs an `id` + `name`
  // on the workflow object — synthesize from the catalog entry.
  setTimeout(async () => {
    const canvas = document.getElementById(canvasId);
    if (!canvas || !window.WorkflowViewer?.render) return;
    const workflow = {
      id: action.id,
      name: action.label,
      description: action.description,
      preset: true,
      nodes: action.moduleMap.nodes,
      connections: action.moduleMap.connections,
    };
    try {
      await window.WorkflowViewer.render(canvas, workflow, registry, { editable: false });
    } catch (err) {
      canvas.innerHTML = `<div style="padding:14px;color:var(--red);">Failed to render module map: ${esc(err.message || err)}</div>`;
    }
  }, 50);
}

function buildOperationTemplatesSection(ops) {
  const header = el(
    "div",
    {
      style: [
        "position:sticky",
        "top:0",
        "z-index:1",
        "background:linear-gradient(180deg,var(--bg-1) 70%,rgba(15,15,16,0))",
        "padding:8px 0 14px",
        "margin-bottom:4px",
      ].join(";"),
    },
    [
      el("div", { style: "display:flex;align-items:baseline;gap:10px;" }, [
        el("span", { style: "font-size:18px;line-height:1;" }, icon("settings")),
        el(
          "div",
          { style: "font-size:15px;font-weight:600;color:var(--text-1);letter-spacing:0.01em;" },
          "Operation templates",
        ),
        el(
          "div",
          { style: "font-size:11px;font-family:ui-monospace,monospace;color:var(--text-4);" },
          `${ops.length}`,
        ),
      ]),
      el(
        "div",
        { style: "font-size:12px;color:var(--text-2);margin-top:3px;margin-left:28px;" },
        "Wizard-based ops without a visual preset yet — click to configure and launch.",
      ),
    ],
  );

  const grid = el("div", {
    style: "display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;",
  });
  for (const op of ops) {
    grid.appendChild(buildOperationTemplateCard(op));
  }

  return el("div", {}, [header, grid]);
}

function buildOperationTemplateCard(op) {
  const platformLabel = (op.platforms || [])
    .map(
      (p) =>
        ({
          twitter: "X",
          linkedin: "LI",
          telegram: "TG",
          email: "Email",
        })[p] || p,
    )
    .join(" · ");

  return el(
    "div",
    {
      style: [
        "background:var(--bg-2)",
        "border:1px solid var(--border)",
        "border-radius:10px",
        "padding:0",
        "cursor:pointer",
        "transition:border-color 150ms,transform 150ms,box-shadow 150ms",
        "display:flex",
        "overflow:hidden",
        "min-height:120px",
      ].join(";"),
      onmouseover: (e) => {
        e.currentTarget.style.borderColor = "#fb923c";
        e.currentTarget.style.transform = "translateY(-1px)";
        e.currentTarget.style.boxShadow = "0 8px 24px -8px rgba(251,146,60,0.25)";
      },
      onmouseout: (e) => {
        e.currentTarget.style.borderColor = "var(--border)";
        e.currentTarget.style.transform = "translateY(0)";
        e.currentTarget.style.boxShadow = "none";
      },
      onclick: () => navigate(`new-workflow/${op.templateKey}`),
    },
    [
      el("div", { style: "width:4px;background:#fb923c;flex-shrink:0;" }),
      el(
        "div",
        {
          style: "padding:14px 16px;display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;",
        },
        [
          el("div", { style: "display:flex;align-items:flex-start;gap:10px;" }, [
            el(
              "div",
              {
                style:
                  "font-size:20px;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:#3a2410;border-radius:6px;flex-shrink:0;",
              },
              op.icon || "🛠",
            ),
            el("div", { style: "flex:1;min-width:0;" }, [
              el(
                "div",
                {
                  style:
                    "font-size:14px;font-weight:600;color:var(--text-1);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                  title: op.label,
                },
                op.label,
              ),
            ]),
            el(
              "div",
              {
                style:
                  "font-size:10px;font-family:ui-monospace,monospace;letter-spacing:0.06em;color:#fed7aa;background:#3a2410;padding:3px 7px;border-radius:10px;flex-shrink:0;border:1px solid rgba(251,146,60,0.4);font-weight:600;",
              },
              "TEMPLATE",
            ),
            buildPinButton(`operation:${op.id}`, op.label),
          ]),
          op.description
            ? el(
                "div",
                {
                  style:
                    "font-size:12px;color:var(--text-2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;",
                },
                op.description,
              )
            : null,
          el(
            "div",
            {
              style:
                "display:flex;gap:8px;font-size:10px;font-family:ui-monospace,monospace;color:#52525b;align-items:center;margin-top:auto;",
            },
            [
              platformLabel ? el("span", {}, platformLabel) : null,
              op.accountMode
                ? el(
                    "span",
                    { style: "opacity:0.6;" },
                    `· ${op.accountMode === "multi" ? "multi-account" : op.accountMode}`,
                  )
                : null,
              el("span", { style: "flex:1;" }),
              el("span", { style: "color:#fed7aa;font-weight:500;" }, "Configure →"),
            ],
          ),
        ],
      ),
    ],
  );
}

function automationGroupColor(key) {
  return (
    {
      discover: "#3b82f6",
      outreach: "#ec4899",
      enrichment: "#a855f7",
      reports: "#10b981",
      cross_account: "#ec4899", // matches outreach hue — campaigns are outreach
      background: "#64748b", // neutral grey — passive system
      custom: "#a855f7",
      other: "#64748b",
    }[key] || "#64748b"
  );
}
function automationGroupColorTint(key) {
  return (
    {
      discover: "#1e3a5f",
      outreach: "#4f1f3a",
      enrichment: "#3a1f4f",
      reports: "#1e4438",
      cross_account: "#4f1f3a",
      background: "#26323e",
      custom: "#3a1f4f",
      other: "#26323e",
    }[key] || "#26323e"
  );
}

// Cache the registry across viewer mounts (same set for the whole session).
let _automationRegistryCache = null;

// Cache the wired-runners set, populated from the most recent /api/workflows
// list response. Avoids hardcoding the set in two places (was prone to drift).
// Falls back to a known-good seed if we render before the API ever loaded
// (rare — the grid hits the API first).
let _wiredRunnersCache = new Set([
  "signal_twitter",
  "signal_linkedin",
  "signal_reddit",
  "signal_hn",
  "outreach_orchestrator",
  "daily_briefing",
  "comment_campaign",
  "group_amplification",
  "event_promotion",
  "cross_account_react",
  "tg_campaign",
  "account_warmer",
  "active_member_listener",
  "feed_monitor",
  "group_join",
  "email_sequence",
  "enrichment_pipeline",
  "email_campaign",
  "top_tokens_ingest",
  "new_token_outreach",
  "coin_mm_outreach",
  "coin_mm_group_activation",
  "proposal_outreach",
  "opportunity_defillama",
  "opportunity_rootdata",
  "opportunity_jobs",
]);
function isWiredRunner(presetId) {
  return _wiredRunnersCache.has(presetId);
}

async function renderAutomationView(params) {
  const id = params?.id;
  if (!id) return navigate("automations");

  // Header skeleton + loading canvas placeholder
  const canvasHost = el(
    "div",
    {
      id: "wf-canvas-host",
      style:
        "position:relative;width:100%;height:calc(100vh - 200px);min-height:520px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--bg-1);",
    },
    [el("div", { style: "padding:40px;color:var(--text-2);" }, "Loading workflow…")],
  );

  const headerNode = el("div", { id: "wf-header", class: "page-header" }, [
    el("div", {}, [
      el(
        "button",
        { class: "btn btn-secondary btn-tiny", onclick: () => navigate("automations") },
        "« All automations",
      ),
      el("div", { class: "page-title", style: "margin-top:8px;" }, "Loading…"),
      el("div", { class: "page-subtitle", id: "wf-subtitle" }, ""),
    ]),
  ]);

  setContentNode(el("div", {}, [headerNode, canvasHost]));

  // Custom workflow IDs are prefixed `custom_<dbId>`. Dispatch to the
  // user-scoped detail endpoint instead of the preset endpoint.
  const isCustom = String(id).startsWith("custom_");
  const fetchWorkflow = isCustom
    ? api("automation-detail-custom", { customId: String(id).slice("custom_".length) })
    : api("automation-detail", { id });

  // Fetch workflow + registry in parallel (registry cached per session)
  const [wfResp, regResp] = await Promise.all([
    fetchWorkflow.catch((err) => ({ error: err?.message || "fetch failed" })),
    _automationRegistryCache
      ? Promise.resolve({ registry: _automationRegistryCache })
      : api("automations-registry").catch((err) => ({ error: err?.message || "fetch failed" })),
  ]);

  if (wfResp?.error) {
    canvasHost.innerHTML = `<div style="padding:40px;color:#ef4444">Could not load workflow: ${escapeHtml(wfResp.error)}</div>`;
    return;
  }
  if (regResp?.error) {
    canvasHost.innerHTML = `<div style="padding:40px;color:#ef4444">Could not load node registry: ${escapeHtml(regResp.error)}</div>`;
    return;
  }
  if (regResp?.registry) _automationRegistryCache = regResp.registry;
  // Refresh wired-runners cache from the registry response too — covers the
  // case where a user navigates directly to /automations/view/<id> without
  // hitting the grid first.
  if (Array.isArray(regResp?.wired_runners)) {
    _wiredRunnersCache = new Set(regResp.wired_runners);
  }

  const wf = wfResp.workflow;
  const recentRuns = wfResp.recent_runs || [];

  // Custom workflows carry parameter_overrides — merge them into each node's
  // parameters BEFORE the viewer renders, and stash the originals on the
  // node so the inspector can show "modified from default" indicators.
  if (isCustom && wf.nodes) {
    const overrides = wf.parameter_overrides || {};
    for (const node of wf.nodes) {
      // Snapshot the pre-override values so the inspector can compute deltas
      node._originalParameters = { ...(node.parameters || {}) };
      const nodeOverrides = overrides[node.id];
      if (nodeOverrides && typeof nodeOverrides === "object") {
        node.parameters = { ...(node.parameters || {}), ...nodeOverrides };
        node._overriddenParameters = new Set(Object.keys(nodeOverrides));
      } else {
        node._overriddenParameters = new Set();
      }
    }
  }

  // Count how many parameters are overridden, total. Drives the pill in the
  // header so users see at a glance how customized the workflow is.
  let overrideCount = 0;
  if (isCustom) {
    const overrides = wf.parameter_overrides || {};
    for (const params of Object.values(overrides)) {
      overrideCount += Object.keys(params || {}).length;
    }
  }

  // Update header now that we have the data. For custom workflows, the title
  // is click-to-rename and the pill shows the override count.
  const titleEl = headerNode.querySelector(".page-title");
  const subEl = headerNode.querySelector("#wf-subtitle");
  if (subEl) subEl.textContent = wf.description || "";
  if (titleEl) {
    titleEl.textContent = "";
    if (isCustom) {
      // Editable title element. Click → contenteditable, Enter or blur → save.
      const titleSpan = el(
        "span",
        {
          contenteditable: "true",
          spellcheck: "false",
          title: "Click to rename",
          style:
            "outline:none;border-bottom:1px dashed transparent;padding:1px 2px;border-radius:3px;transition:background 120ms,border-color 120ms;",
          onfocus: (e) => {
            e.currentTarget.style.background = "rgba(168,85,247,0.08)";
            e.currentTarget.style.borderBottomColor = "rgba(168,85,247,0.4)";
          },
          onblur: async (e) => {
            e.currentTarget.style.background = "";
            e.currentTarget.style.borderBottomColor = "transparent";
            const newName = e.currentTarget.textContent.trim();
            if (!newName || newName === wf.name) {
              e.currentTarget.textContent = wf.name; // revert empty edits
              return;
            }
            const customId = String(id).slice("custom_".length);
            const r = await api("automation-update-custom", { customId, body: { name: newName } });
            if (r?.error) {
              toast(`Rename failed: ${r.error}`, "err");
              e.currentTarget.textContent = wf.name;
            } else {
              wf.name = newName;
              toast("Renamed", "ok", 1200);
              logTelemetry("automations", "rename", { customId });
            }
          },
          onkeydown: (e) => {
            if (e.key === "Enter") {
              e.preventDefault();
              e.currentTarget.blur();
            }
            if (e.key === "Escape") {
              e.preventDefault();
              e.currentTarget.textContent = wf.name;
              e.currentTarget.blur();
            }
          },
        },
        wf.name,
      );
      titleEl.appendChild(titleSpan);

      if (overrideCount > 0) {
        titleEl.appendChild(
          el(
            "span",
            {
              style:
                "margin-left:10px;font-size:11px;font-family:ui-monospace,monospace;background:rgba(168,85,247,0.18);color:#d8b4fe;padding:3px 9px;border-radius:10px;letter-spacing:0.04em;font-weight:500;vertical-align:middle;",
              title: "Number of parameters overridden from the source preset's default",
            },
            `${overrideCount} override${overrideCount === 1 ? "" : "s"}`,
          ),
        );
      }
    } else {
      titleEl.textContent = wf.name;
    }
  }

  // Status-aware banner for custom workflows. Engine wiring is now in for
  // signal_twitter (round 9). Banner copy reflects the actual run state:
  //   draft   — yellow, "won't run until activated"
  //   active  — green, "engine picks it up next cycle"
  //   paused  — gray, "running stopped, click to resume"
  if (isCustom) {
    const status = wf.status || "draft";
    const isWired = isWiredRunner(wf.source_preset_id);
    let bannerStyle = "rgba(234,179,8,0.08)";
    let borderColor = "rgba(234,179,8,0.3)";
    let textColor = "#fde68a";
    let icon = "ℹ️";
    let bold = "Configuration only. ";
    let body = "";
    if (status === "active") {
      bannerStyle = "rgba(16,185,129,0.08)";
      borderColor = "rgba(16,185,129,0.3)";
      textColor = "#a7f3d0";
      icon = "●";
      bold = "Active. ";
      body = isWired
        ? "The engine picks up your overrides on the next scheduled cycle."
        : "Engine wiring for this preset is still in progress — your status is tracked but the runner isn't reading overrides yet.";
    } else if (status === "paused") {
      bannerStyle = "rgba(148,163,184,0.08)";
      borderColor = "rgba(148,163,184,0.3)";
      textColor = "#cbd5e1";
      icon = "⏸";
      bold = "Paused. ";
      body = "Settings are preserved. Click ▶ Activate on the card to resume.";
    } else {
      // draft
      body = isWired
        ? "Activate from the card or use ▶ Activate after editing — the engine reads your overrides once the workflow is active."
        : "Engine wiring for this preset is in progress — edits save, but won't run until the runner ships.";
    }
    const banner = el(
      "div",
      {
        style: `margin:8px 0 16px;padding:10px 14px;background:${bannerStyle};border:1px solid ${borderColor};border-radius:6px;font-size:12px;color:${textColor};display:flex;align-items:center;gap:10px;`,
      },
      [
        el("span", { style: "font-size:16px;" }, icon),
        el("span", { style: "flex:1;line-height:1.5;" }, [
          el("span", { style: "font-weight:600;" }, bold),
          body,
        ]),
      ],
    );
    headerNode.parentNode.insertBefore(banner, canvasHost);

    // Recent runs panel — only shown for customs that have run at least once.
    // Compact horizontal strip of the last ~10 cycles with click-to-expand
    // stats. Empty state appears for active customs that haven't fired yet.
    if (recentRuns.length > 0 || status === "active") {
      const canRunNowHere = status === "active" && isWiredRunner(wf.source_preset_id);
      const runsPanel = buildRecentRunsPanel(recentRuns, {
        canRunNow: canRunNowHere,
        onRunNow: canRunNowHere
          ? () =>
              runAutomationNow({
                custom_id: wf.custom_id,
                name: wf.name,
                source_preset_id: wf.source_preset_id,
                id: `custom_${wf.custom_id}`,
              })
          : null,
      });
      headerNode.parentNode.insertBefore(runsPanel, canvasHost);
    }
  }

  if (!window.WorkflowViewer) {
    canvasHost.innerHTML = `<div style="padding:40px;color:#ef4444">workflow-viewer.js failed to load. Check the dashboard.html script tags.</div>`;
    return;
  }

  // Edit handler — only meaningful for custom workflows. Receives:
  //   adds:   { nodeId: { paramKey: newValue, ... }, ... }
  //   resets: ["nodeId|paramKey", ...]   keys to remove from existing overrides
  // Builds the new full parameter_overrides map, PATCHes, re-renders.
  const onParameterSave = isCustom
    ? async ({ adds, resets }) => {
        const merged = JSON.parse(JSON.stringify(wf.parameter_overrides || {}));
        // Apply additions / updates
        for (const [nodeId, params] of Object.entries(adds || {})) {
          merged[nodeId] = { ...(merged[nodeId] || {}), ...params };
        }
        // Apply resets — remove the key entirely. Also drop the per-node bucket
        // if it becomes empty so the saved JSON stays clean.
        for (const k of resets || []) {
          const [nodeId, paramKey] = k.split("|");
          if (merged[nodeId]) {
            delete merged[nodeId][paramKey];
            if (Object.keys(merged[nodeId]).length === 0) delete merged[nodeId];
          }
        }
        const customId = String(id).slice("custom_".length);
        const r = await api("automation-update-custom", {
          customId,
          body: { parameterOverrides: merged },
        });
        if (r?.error) {
          toast(`Save failed: ${r.error}`, "err");
          throw new Error(r.error);
        }
        const addCount = Object.values(adds || {}).reduce((s, p) => s + Object.keys(p).length, 0);
        const resetCount = (resets || []).length;
        toast(
          resetCount > 0
            ? `Saved (${addCount} edit${addCount === 1 ? "" : "s"}, ${resetCount} reset${resetCount === 1 ? "" : "s"})`
            : "Saved",
          "ok",
          1500,
        );
        logTelemetry("automations", "param_save", { customId, addCount, resetCount });
        // Re-render — pulls fresh overrides from server, refreshes canvas + story
        await renderAutomationView(params);
      }
    : null;

  await window.WorkflowViewer.render(canvasHost, wf, _automationRegistryCache, {
    editable: isCustom,
    onSave: onParameterSave,
    // Activity view (replaces the legacy Diagram) reads recent_runs to
    // show what this workflow has actually produced cycle-by-cycle.
    recentRuns,
    // Per-run drill-down callback. Click an Activity row → fetches the
    // signals + outreach events that this specific run produced (via
    // produced_by_run_id for signals, time-window correlation for events).
    fetchRunOutputs: (runId) => api("automation-run-outputs", { runId }),
  });
}
