// dashboard-always-on-services-panel.js — extracted verbatim from dashboard.js (pure move).

// ── Always-on services panel ─────────────────────────────────────────
//
// Surfaces the 20+ cron-driven engines that run perpetually for users
// regardless of explicit activation. Phase 1 is read-only — shows
// state (running / idle / paused / errored), schedule, last fire,
// errors, and a manage-link to the underlying gate.
//
// Why this matters: today most of these services have no user-level
// off-switch. The user has to delete the prerequisite (account /
// keyword / session) to stop them. This panel makes that REALITY
// visible — what's running and why — without yet adding new toggles.
//
// Phase 2 (next) will add a user_service_toggles table + per-service
// hard off switch. For now: visibility = trust.
//
// The catalog is curated, not auto-generated. Pure system jobs
// (heartbeat, archival, cleanup) are intentionally excluded —
// they're plumbing, not user-actionable.

// Each entry's `toggleKey` (when present) maps to a SERVICE_KEYS entry on
// the backend (nexus-bot/src/db/service-toggles.js). Entries without a
// toggleKey can't be turned off per-user — they're either global infra
// or services where a per-user toggle doesn't make sense (e.g. shared
// scrapers that produce data ALL users consume).
const ALWAYS_ON_SERVICES = [
  // ── Discovery (find new things in the world) ─────────────────────
  {
    jobName: "luma-discovery",
    toggleKey: "luma",
    label: "Luma discovery",
    icon: "calendar",
    category: "discovery",
    description: "Twice-daily check of your Luma feeds + auto-RSVP to matching events.",
    manageRoute: "integrations/luma",
    manageLabel: "Manage subscriptions",
  },
  {
    jobName: "luma-approvals",
    toggleKey: "luma",
    label: "Luma approval check",
    icon: "calendar",
    category: "discovery",
    description: "Daily check on pending RSVP approvals + post-approval enrichment.",
    manageRoute: "integrations/luma",
  },
  {
    jobName: "signal-twitter",
    toggleKey: "signal_twitter",
    label: "Twitter signals",
    icon: "𝕏",
    category: "discovery",
    description: "Scans X for buying signals matching your keywords.",
    manageRoute: "signals",
    manageLabel: "Manage keywords",
  },
  {
    jobName: "signal-linkedin",
    toggleKey: "signal_linkedin",
    label: "LinkedIn signals",
    icon: "briefcase",
    category: "discovery",
    description: "Scans LinkedIn posts for buying signals.",
    manageRoute: "signals",
  },
  {
    jobName: "signal-reddit",
    toggleKey: "signal_reddit",
    label: "Reddit signals",
    icon: "🔻",
    category: "discovery",
    description: "Scans Reddit threads for buying signals.",
    manageRoute: "signals",
  },
  {
    jobName: "signal-hn",
    toggleKey: "signal_hn",
    label: "Hacker News signals",
    icon: "🔶",
    category: "discovery",
    description: "Hourly HN front-page scan for relevant posts.",
    manageRoute: "signals",
  },
  {
    jobName: "defillama",
    label: "DeFi fundraises",
    icon: "money",
    category: "discovery",
    description:
      "Every 6h — scrapes DeFiLlama for new fundraise rounds. Global — shared across all users.",
  },
  {
    jobName: "rootdata",
    label: "RootData fundraises",
    icon: "money",
    category: "discovery",
    description: "3× daily — scrapes RootData for fundraise activity. Global.",
  },
  {
    jobName: "cryptorank",
    label: "CryptoRank fundraises",
    icon: "money",
    category: "discovery",
    description: "Every 8h — scrapes CryptoRank fundraise listings. Global.",
  },
  {
    jobName: "jobs",
    label: "Hiring opportunities",
    icon: "briefcase",
    category: "discovery",
    description: "Every 12h — scrapes job boards for hiring waves. Global.",
  },
  {
    jobName: "grants",
    label: "Grant programs",
    icon: "book",
    category: "discovery",
    description: "Weekly — scrapes new grant programs. Global.",
  },
  {
    jobName: "member-scrape",
    label: "Group member scraping",
    icon: "users",
    category: "discovery",
    description: "Daily — scrapes Telegram group memberships you've enabled.",
  },

  // ── Enrichment (improve data quality) ────────────────────────────
  {
    jobName: "enrichment",
    label: "Company enrichment",
    icon: "flask",
    category: "enrichment",
    description:
      "Daily catchup — fills website / Twitter / LinkedIn / employees for unenriched companies. Global pool — runs regardless.",
  },
  {
    jobName: "icp-signal-enrich",
    toggleKey: "enrichment_signals",
    label: "Signal enrichment",
    icon: "flask",
    category: "enrichment",
    description: "Every 15min — enriches discovered signals with ICP scoring + filter evaluation.",
  },

  // ── Engagement (system-driven outreach + warming) ────────────────
  {
    jobName: "warmer",
    toggleKey: "account_warmer",
    label: "Account warmer",
    icon: "flame",
    category: "engagement",
    description: "Every 30min — keeps warmer-conversations active to avoid stale-account flags.",
    manageRoute: "accounts",
    manageLabel: "Manage accounts",
  },
  {
    jobName: "active-member-health",
    toggleKey: "active_member",
    label: "Active member listener",
    icon: "users",
    category: "engagement",
    description: "Listens to Telegram groups + responds to keyword matches.",
    manageRoute: "telegram/activemember",
    manageLabel: "Manage listeners",
  },
  {
    jobName: "tg-promo-posting",
    label: "TG promo posting",
    icon: "megaphone",
    category: "engagement",
    description: "Every 8h — posts your active member promos.",
    manageRoute: "telegram/activemember",
  },
  {
    jobName: "cross-reactor",
    label: "Cross-account reactor",
    icon: "refresh",
    category: "engagement",
    description: "Every 5min — orchestrates likes + reactions across your accounts.",
  },
  {
    jobName: "story-post",
    toggleKey: "story_poster",
    label: "Story poster",
    icon: "camera",
    category: "engagement",
    description: "Hourly — auto-posts stories at scheduled hours.",
  },
  {
    jobName: "li-connection-poll",
    toggleKey: "linkedin_connections",
    label: "LinkedIn connection poll",
    icon: "briefcase",
    category: "engagement",
    description: "Every 6h — accepts incoming LinkedIn connection requests.",
  },
  {
    jobName: "group-engage",
    label: "Group amplification",
    icon: "megaphone",
    category: "engagement",
    description: "Every 10min — engages with active group campaigns.",
    manageRoute: "operations/op/group_amplification",
    manageLabel: "Manage campaigns",
  },
  {
    jobName: "group-join",
    label: "Group amplification join",
    icon: "megaphone",
    category: "engagement",
    description: "Every 15min — joins target groups for active campaigns.",
    manageRoute: "operations/op/group_amplification",
  },

  // ── Intel (analytics + reports) ──────────────────────────────────
  {
    jobName: "smart-alerts",
    toggleKey: "smart_alerts",
    label: "Smart alerts",
    icon: "bell",
    category: "intel",
    description: "Every 30min — surfaces behavioral intel + opportunities to your inbox.",
  },
  {
    jobName: "matching",
    toggleKey: "daily_matching",
    label: "Daily matching",
    icon: "users",
    category: "intel",
    description: "Daily 8am — finds person-to-person matches across the network.",
  },
  {
    jobName: "icp-scoring",
    toggleKey: "icp_scoring",
    label: "ICP scoring",
    icon: "target",
    category: "intel",
    description: "Daily 7am — computes lookalike scores for your contacts.",
  },
  {
    jobName: "digest",
    toggleKey: "weekly_digest",
    label: "Weekly digest",
    icon: "chart",
    category: "intel",
    description: "Weekly Monday push — summary of activity, matches, signals.",
  },
  {
    jobName: "gate-reminders",
    toggleKey: "gate_reminders",
    label: "Gate reminders",
    icon: "external",
    category: "intel",
    description: "Every 6h — pings users with pending verification steps.",
  },
];

const SERVICE_CATEGORY_META = {
  discovery: {
    icon: "search",
    label: "Discovery",
    blurb: "Find new prospects, signals, opportunities",
  },
  enrichment: {
    icon: "flask",
    label: "Enrichment",
    blurb: "Improve data quality on what you already have",
  },
  engagement: {
    icon: "message",
    label: "Engagement",
    blurb: "Keep accounts warm + run system-driven outreach",
  },
  intel: { icon: "chart", label: "Intel", blurb: "Analytics, matches, reports" },
};

const SERVICES_PANEL_KEY = "automations:servicesExpanded";
function getServicesExpanded() {
  try {
    return localStorage.getItem(SERVICES_PANEL_KEY) === "1";
  } catch {
    return false;
  }
}
function setServicesExpanded(v) {
  try {
    localStorage.setItem(SERVICES_PANEL_KEY, v ? "1" : "0");
  } catch {}
}

// Compute the running state of a service from its system-jobs row.
// Returns { state, color, label, icon }
function classifyServiceState(jobRow) {
  if (!jobRow) {
    return { state: "unknown", color: "#71717a", icon: "·", label: "Unknown" };
  }
  if (jobRow.paused) {
    return { state: "paused", color: "#fbbf24", icon: "pause", label: "Paused" };
  }
  if (jobRow.lastError) {
    return { state: "errored", color: "#ef4444", icon: "alert", label: "Last run errored" };
  }
  if (jobRow.isRunning) {
    return { state: "running", color: "#22c55e", icon: "play", label: "Running now" };
  }
  if (!jobRow.lastRunAt) {
    return { state: "scheduled", color: "#9ca3af", icon: "·", label: "Scheduled, not yet fired" };
  }
  // Fired recently? Within 2× the schedule's interval is "healthy."
  const ageSec = jobRow.ageSec || 0;
  if (ageSec < 60 * 60 * 2) {
    // last 2 hours
    return { state: "on", color: "#22c55e", icon: "●", label: "On" };
  }
  if (ageSec < 60 * 60 * 24) {
    // last 24h
    return { state: "stale", color: "#fbbf24", icon: "·", label: "Stale" };
  }
  return { state: "idle", color: "#9ca3af", icon: "·", label: "Idle" };
}

function buildAlwaysOnServicesPanel(systemJobsResp, togglesResp) {
  const jobs = Array.isArray(systemJobsResp?.jobs) ? systemJobsResp.jobs : [];
  // Cache on window so the per-service drawer can read job state
  // without a re-fetch on every drawer open.
  window.__systemJobsCache = systemJobsResp || null;
  // Index by name for O(1) lookup as we render.
  const byName = new Map(jobs.map((j) => [j.name, j]));
  // toggles map (default-on if missing). Mutate-in-place when user flips
  // a switch so subsequent re-renders during the same view see the new
  // state without a full refetch (the PATCH response is idempotent).
  const toggles = (togglesResp && togglesResp.toggles) || {};

  // Resolve each catalog entry's state by matching on jobName.
  const enriched = ALWAYS_ON_SERVICES.map((s) => {
    const jobRow = byName.get(s.jobName) || null;
    const userOff = !!s.toggleKey && toggles[s.toggleKey] === false;
    const state = userOff
      ? { state: "off", color: "#71717a", icon: "○", label: "Off (you)" }
      : classifyServiceState(jobRow);
    return { ...s, jobRow, state, userOff, toggles };
  });

  // Group by category, keeping catalog order within each.
  const byCategory = new Map();
  for (const s of enriched) {
    if (!byCategory.has(s.category)) byCategory.set(s.category, []);
    byCategory.get(s.category).push(s);
  }

  const total = enriched.length;
  const onCount = enriched.filter((s) => ["on", "running"].includes(s.state.state)).length;
  const issueCount = enriched.filter((s) => ["errored", "paused"].includes(s.state.state)).length;

  const expanded = getServicesExpanded();

  // Header — collapse toggle + summary stats
  const arrowSpan = el(
    "span",
    { style: "font-size:12px;color:var(--text-2);" },
    expanded ? "▾" : "▸",
  );
  const headerBtn = el(
    "button",
    {
      type: "button",
      style: [
        "background:transparent",
        "border:none",
        "cursor:pointer",
        "padding:10px 14px",
        "width:100%",
        "display:flex",
        "align-items:center",
        "gap:10px",
        "color:inherit",
        "text-align:left",
        "border:1px solid rgba(99,102,241,0.18)",
        "border-radius:10px",
        "background:linear-gradient(135deg,rgba(99,102,241,0.04),rgba(34,197,94,0.02))",
        "transition:border-color 120ms",
      ].join(";"),
      title: expanded ? "Click to collapse" : "Click to expand and see every always-on service",
      onclick: () => {
        const next = !getServicesExpanded();
        setServicesExpanded(next);
        const mount = document.getElementById("always-on-services-mount");
        if (mount) {
          const fresh = buildAlwaysOnServicesPanel(systemJobsResp, togglesResp);
          mount.replaceWith(fresh);
        }
        logTelemetry("automations", next ? "services_expand" : "services_collapse");
      },
      onmouseover: (e) => {
        e.currentTarget.style.borderColor = "rgba(99,102,241,0.4)";
      },
      onmouseout: (e) => {
        e.currentTarget.style.borderColor = "rgba(99,102,241,0.18)";
      },
    },
    [
      arrowSpan,
      el("span", { style: "font-size:16px;line-height:1;" }, icon("signal")),
      el("div", { style: "flex:1;" }, [
        el(
          "div",
          { style: "font-size:14px;font-weight:600;color:var(--text-1);" },
          "Always-on services",
        ),
        el(
          "div",
          { style: "font-size:11px;color:var(--text-2);margin-top:2px;" },
          `${onCount} on · ${total - onCount - issueCount} idle${issueCount > 0 ? ` · ${issueCount} need attention` : ""} · ${total} total`,
        ),
      ]),
      // State-color summary pip — quick scannable signal.
      el("div", { style: "display:flex;align-items:center;gap:6px;" }, [
        el("span", {
          style: `display:inline-block;width:8px;height:8px;border-radius:50%;background:${issueCount > 0 ? "#fbbf24" : onCount > 0 ? "var(--green)" : "#71717a"};`,
        }),
        el(
          "span",
          { style: "font-size:11px;color:var(--text-2);font-family:ui-monospace,monospace;" },
          issueCount > 0 ? "needs attention" : onCount > 0 ? "healthy" : "idle",
        ),
      ]),
    ],
  );

  const wrap = el("div", { id: "always-on-services-mount", style: "margin:6px 0 12px;" }, [
    headerBtn,
  ]);
  // Custom event from toggle switches — re-render in place so border-left
  // and state pill reflect the new ON/OFF state immediately.
  wrap.addEventListener("services-rerender", () => {
    const fresh = buildAlwaysOnServicesPanel(systemJobsResp, { toggles });
    wrap.replaceWith(fresh);
  });
  if (!expanded) return wrap;

  // Expanded body — grouped by category
  const body = el("div", {
    style: "display:flex;flex-direction:column;gap:18px;padding:14px 4px 8px;",
  });
  const orderedCategories = ["discovery", "enrichment", "engagement", "intel"];
  for (const catKey of orderedCategories) {
    const items = byCategory.get(catKey);
    if (!items || items.length === 0) continue;
    const meta = SERVICE_CATEGORY_META[catKey] || { icon: "·", label: catKey, blurb: "" };
    body.appendChild(buildServicesCategoryGroup(meta, items));
  }
  // Footer — explainer about toggle semantics
  body.appendChild(
    el(
      "div",
      {
        style: [
          "padding:10px 12px",
          "margin-top:4px",
          "background:rgba(255,255,255,0.02)",
          "border:1px dashed var(--border)",
          "border-radius:8px",
          "font-size:11px",
          "color:var(--text-2)",
          "line-height:1.5",
        ].join(";"),
      },
      [
        el(
          "div",
          { style: "color:var(--text-1);font-weight:500;margin-bottom:4px;" },
          "Toggle semantics",
        ),
        el(
          "div",
          {},
          "Services with a switch can be flipped OFF per-account — the engine will short-circuit before doing work for you. Services without a switch (greyed) run globally; their data is shared across all users. To stop those, manage the underlying prerequisite (delete a session, archive a config).",
        ),
      ],
    ),
  );

  wrap.appendChild(body);
  return wrap;
}

function buildServicesCategoryGroup(meta, items) {
  const wrap = el("div", { style: "display:flex;flex-direction:column;gap:6px;" });
  // Category header
  wrap.appendChild(
    el("div", { style: "display:flex;align-items:baseline;gap:8px;padding:0 4px 4px;" }, [
      // A mark when one is mapped, the original glyph when none is. icon.for()
      // returns null rather than a bad guess, and falling back beats a 16px hole.
      icon.for(meta.icon)
        ? icon(icon.for(meta.icon))
        : el("span", { style: "font-size:14px;line-height:1;" }, meta.icon),
      el(
        "div",
        {
          style:
            "font-size:12px;font-weight:600;color:var(--text-1);text-transform:uppercase;letter-spacing:0.05em;",
        },
        `${meta.label}`,
      ),
      el(
        "div",
        { style: "font-size:10px;color:var(--text-4);font-family:ui-monospace,monospace;" },
        `${items.length}`,
      ),
      meta.blurb
        ? el("div", { style: "font-size:11px;color:var(--text-4);font-style:italic;" }, meta.blurb)
        : null,
    ]),
  );

  // Service cards
  const grid = el("div", {
    style: "display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:8px;",
  });
  for (const s of items) grid.appendChild(buildServiceCard(s));
  wrap.appendChild(grid);
  return wrap;
}

// Compact ON/OFF toggle switch. Only rendered for services with a
// toggleKey. Calls service-toggle-set; mutates the in-memory toggles
// map and re-renders the panel section so the user sees instant
// feedback (otherwise the card would stay stale until next render).
function buildServiceToggleSwitch(service, currentlyOn) {
  const wrap = el(
    "button",
    {
      type: "button",
      style: [
        "position:relative",
        "display:inline-flex",
        "align-items:center",
        "width:34px",
        "height:18px",
        "padding:0",
        "border:none",
        "border-radius:9px",
        "background:" + (currentlyOn ? "rgba(34,197,94,0.4)" : "rgba(75,85,99,0.4)"),
        "cursor:pointer",
        "transition:background 150ms",
        "flex-shrink:0",
      ].join(";"),
      title: currentlyOn
        ? `Turn OFF — short-circuits ${service.label}`
        : `Turn ON — re-enables ${service.label}`,
      onclick: async (e) => {
        e.stopPropagation();
        const next = !currentlyOn;
        // Optimistic update — flip immediately, revert on failure.
        const knob = e.currentTarget.firstChild;
        e.currentTarget.style.background = next ? "rgba(34,197,94,0.4)" : "rgba(75,85,99,0.4)";
        knob.style.transform = next ? "translateX(16px)" : "translateX(0)";
        knob.style.background = next ? "var(--green)" : "#9ca3af";
        try {
          const r = await api("service-toggle-set", {
            body: { service: service.toggleKey, enabled: next },
          });
          if (r?.error) throw new Error(r.error);
          toast(`${service.label}: ${next ? "ON" : "OFF"}`, "ok", 1800);
          logTelemetry("automations", "service_toggle", {
            service: service.toggleKey,
            enabled: next,
          });
          // Mutate the cached toggles map so subsequent re-renders are
          // consistent without a full re-fetch.
          if (service.toggles) service.toggles[service.toggleKey] = next;
          // Refresh the panel so border-left + state pill update.
          const mount = document.getElementById("always-on-services-mount");
          if (mount) {
            mount.dispatchEvent(new CustomEvent("services-rerender", { bubbles: true }));
          }
        } catch (err) {
          // Revert
          e.currentTarget.style.background = currentlyOn
            ? "rgba(34,197,94,0.4)"
            : "rgba(75,85,99,0.4)";
          knob.style.transform = currentlyOn ? "translateX(16px)" : "translateX(0)";
          knob.style.background = currentlyOn ? "var(--green)" : "#9ca3af";
          toast(`Toggle failed: ${err.message || "unknown"}`, "err");
        }
      },
    },
    [
      el("span", {
        style: [
          "position:absolute",
          "top:2px",
          "left:2px",
          "width:14px",
          "height:14px",
          "border-radius:50%",
          "background:" + (currentlyOn ? "#22c55e" : "#9ca3af"),
          "transform:" + (currentlyOn ? "translateX(16px)" : "translateX(0)"),
          "transition:transform 150ms,background 150ms",
          "box-shadow:0 1px 3px rgba(0,0,0,0.4)",
        ].join(";"),
      }),
    ],
  );
  return wrap;
}

function buildServiceCard(s) {
  const j = s.jobRow;
  const st = s.state;
  const lastRun = j?.lastRunAt ? `${_liveFmtSince(j.lastRunAt)} ago` : "never";
  const nextRun = j?.nextRunAt
    ? `in ${_liveFmtSince(new Date(Date.now() + (new Date(j.nextRunAt).getTime() - Date.now())).toISOString())}`
    : "—";
  const errorCount = j?.errorCount || 0;
  const runCount = j?.runCount || 0;
  const successPct = runCount > 0 ? Math.round(((runCount - errorCount) / runCount) * 100) : null;

  const card = el(
    "div",
    {
      style: [
        "background:var(--bg-2)",
        "border:1px solid var(--border)",
        "border-left:3px solid " + st.color,
        "border-radius:8px",
        "padding:10px 12px",
        "display:flex",
        "flex-direction:column",
        "gap:6px",
        "cursor:pointer",
        "transition:border-color 120ms",
      ].join(";"),
      // Click → open per-service drawer with status + toggle + recent
      // runs + service-specific inline editor (keywords for signals,
      // subscriptions for Luma, etc). Replaces the old "navigate to a
      // generic management page" pattern that lost the user's context.
      onclick: (e) => {
        // Don't double-fire when the user clicks the toggle switch
        if (e.target.closest(".automation-pin-btn, button")) return;
        openServiceDrawer(s);
      },
      onmouseover: (e) => {
        e.currentTarget.style.borderColor = "#3a3a3e";
        e.currentTarget.style.borderLeftColor = st.color;
      },
      onmouseout: (e) => {
        e.currentTarget.style.borderColor = "var(--border)";
        e.currentTarget.style.borderLeftColor = st.color;
      },
    },
    [
      // Title row — icon + label + state pill + toggle switch
      el("div", { style: "display:flex;align-items:center;gap:8px;" }, [
        icon.for(s.icon)
          ? icon(icon.for(s.icon))
          : el(
              "span",
              { style: "font-size:14px;line-height:1;width:18px;text-align:center;" },
              s.icon,
            ),
        el("div", { style: "flex:1;min-width:0;" }, [
          el(
            "div",
            {
              style:
                "font-size:13px;font-weight:600;color:var(--text-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
            },
            s.label,
          ),
        ]),
        el(
          "span",
          {
            style: [
              "display:inline-flex",
              "align-items:center",
              "gap:4px",
              "padding:1px 8px",
              "border:1px solid " + st.color + "55",
              "background:" + st.color + "12",
              "color:" + st.color,
              "border-radius:9px",
              "font-size:10px",
              "font-weight:600",
              "font-family:ui-monospace,monospace",
              "white-space:nowrap",
            ].join(";"),
            title: st.label,
          },
          [
            icon.for(st.icon) ? icon(icon.for(st.icon)) : el("span", {}, st.icon),
            el("span", {}, st.label),
          ],
        ),
        // Toggle switch (only when service has a toggleKey). Greyed-out
        // services without a toggleKey simply omit this — clear visual
        // distinction between "you can flip this" and "global, can't flip."
        s.toggleKey ? buildServiceToggleSwitch(s, !s.userOff) : null,
      ]),
      // Description
      s.description
        ? el("div", { style: "font-size:11px;color:var(--text-2);line-height:1.5;" }, s.description)
        : null,
      // Stats row
      el(
        "div",
        {
          style:
            "display:flex;gap:14px;font-size:10px;color:var(--text-4);font-family:ui-monospace,monospace;flex-wrap:wrap;",
        },
        [
          j?.schedule ? el("span", {}, `⏱ ${j.schedule}`) : null,
          el("span", {}, `last ${lastRun}`),
          runCount > 0
            ? el(
                "span",
                {},
                `${runCount} runs${errorCount > 0 ? ` (${errorCount} err)` : ""}${successPct !== null ? ` · ${successPct}% ok` : ""}`,
              )
            : null,
        ].filter(Boolean),
      ),
      // Last error preview
      j?.lastError
        ? el(
            "div",
            {
              style: [
                "padding:4px 8px",
                "background:rgba(239,68,68,0.06)",
                "border:1px solid rgba(239,68,68,0.2)",
                "border-radius:5px",
                "font-size:10px",
                "color:var(--red)",
                "font-family:ui-monospace,monospace",
                "white-space:nowrap",
                "overflow:hidden",
                "text-overflow:ellipsis",
              ].join(";"),
              title: j.lastError,
            },
            [
              el("span", { style: "margin-right:4px;" }, icon("alert")),
              el("span", {}, j.lastError),
            ],
          )
        : null,
      // Click hint — every card opens a per-service drawer now.
      el("div", { style: "font-size:10px;color:#a5b4fc;font-weight:500;" }, "Click to configure →"),
    ],
  );
  return card;
}
