/* Voyage Fleet — Fleet Overview (home) + shared FleetTable + activity rail.
 * Exports: FleetOverview, FleetTable, ActivityRail, TenantRowActions.
 */

const { useState: useS_f, useMemo: useMemo_f, useRef: useRef_f } = React;
const F = window.FLEET;

/* ─── Row overflow actions menu ────────────────────────────────────────── */
function TenantRowActions({ tenant, inHeader }) {
  const [open, setOpen] = useS_f(false);
  const [modal, setModal] = useS_f(null); // 'handoff' | 'archive' | 'unarchive'
  const ref = useRef_f(null);
  useOutsideClick(ref, () => setOpen(false));
  const archived = tenant.status === "archived";
  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button onClick={e => { e.stopPropagation(); setOpen(o => !o); }} aria-label="Tenant actions" style={{
        width: 28, height: 28, borderRadius: 4, border: inHeader ? "1px solid var(--jrni-color-neutral-4)" : "1px solid transparent", background: open ? "var(--jrni-color-neutral-6)" : inHeader ? "white" : "transparent",
        cursor: "pointer", display: "grid", placeItems: "center", color: "var(--jrni-color-text-soft)", boxShadow: inHeader ? "var(--jrni-shadow-chrome)" : "none",
      }}><Icons.DotsV size={16} /></button>
      {open ? (
        <div style={{ position: "absolute", top: "calc(100% + 4px)", right: 0, width: 220, background: "white", borderRadius: 8, boxShadow: "var(--jrni-shadow-lg)", border: "1px solid var(--jrni-color-surface-border)", padding: 4, zIndex: 60 }} onClick={e => e.stopPropagation()}>
          <MenuItem icon={<Icons.Eye size={14} />} onClick={() => { setOpen(false); navigate("/tenant", { slug: tenant.slug }); }}>View detail</MenuItem>
          <MenuItem icon={<Icons.Drift size={14} />} disabled={archived} onClick={() => { setOpen(false); navigate("/tenant", { slug: tenant.slug, tab: "drift" }); }}>View component drift</MenuItem>
          <MenuItem icon={<Icons.Github size={14} />} hint="↗">Open GitHub repo</MenuItem>
          <MenuItem icon={<Icons.Cloudflare size={14} />} hint="↗">Open Cloudflare</MenuItem>
          <div style={{ height: 1, background: "var(--jrni-color-surface-border)", margin: "4px 0" }} />
          <MenuItem icon={<Icons.User size={14} />} onClick={() => { setOpen(false); setModal("handoff"); }}>Hand off ownership</MenuItem>
          <div style={{ height: 1, background: "var(--jrni-color-surface-border)", margin: "4px 0" }} />
          {archived
            ? <MenuItem icon={<Icons.Rollback size={14} />} onClick={() => { setOpen(false); setModal("unarchive"); }}>Unarchive tenant…</MenuItem>
            : <MenuItem icon={<Icons.Trash size={14} />} danger onClick={() => { setOpen(false); setModal("archive"); }}>Archive tenant…</MenuItem>}
        </div>
      ) : null}

      <Modal open={modal === "handoff"} onClose={() => setModal(null)} title={`Hand off ${tenant.slug}`} subtitle="Files a PR against ralph-ownership.yaml." width={420}
        footer={<React.Fragment><Button variant="tertiary" onClick={() => setModal(null)}>Cancel</Button><Button variant="primary" icon={<Icons.GitPr size={13} />} onClick={() => setModal(null)}>Open hand-off PR</Button></React.Fragment>}>
        <Field label="Hand off to operator"><Select value="marek" onChange={() => {}} options={[{ value: "marek", label: "Marek Singh — Customer Success" }, { value: "imani", label: "Imani Okafor — Customer Success" }, { value: "sven", label: "Sven Lindstrom — Platform Engineer" }]} /></Field>
      </Modal>
      <Modal open={modal === "archive"} onClose={() => setModal(null)} title={`Archive ${tenant.slug}`} width={450}
        footer={<React.Fragment><Button variant="tertiary" onClick={() => setModal(null)}>Cancel</Button><ClaudeCodeLink command={`voyage-fleet archive ${tenant.slug}`} label="Archive in Claude Code" variant="button" tone="primary" /></React.Fragment>}>
        <p style={{ margin: "0 0 12px", fontSize: 13.5, color: "var(--jrni-color-neutral-1)", lineHeight: 1.55 }}>Archiving pauses deployments and makes the tenant read-only (data is retained). It runs as a Ralph in a Claude Code session — the command is staged below.</p>
        <div style={{ padding: "9px 11px", background: "var(--jrni-color-surface-card-soft)", border: "1px solid var(--jrni-color-surface-border)", borderRadius: 6, fontFamily: "var(--jrni-font-family-mono)", fontSize: 12, color: "var(--jrni-color-neutral-1)" }}>$ voyage-fleet archive {tenant.slug}</div>
      </Modal>
      <Modal open={modal === "unarchive"} onClose={() => setModal(null)} title={`Unarchive ${tenant.slug}`} width={450}
        footer={<React.Fragment><Button variant="tertiary" onClick={() => setModal(null)}>Cancel</Button><ClaudeCodeLink command={`voyage-fleet unarchive ${tenant.slug}`} label="Unarchive in Claude Code" variant="button" tone="primary" /></React.Fragment>}>
        <p style={{ margin: "0 0 12px", fontSize: 13.5, color: "var(--jrni-color-neutral-1)", lineHeight: 1.55 }}>Unarchiving re-enables deployments and resumes the tenant's pipelines. It runs as a Ralph in a Claude Code session.</p>
        <div style={{ padding: "9px 11px", background: "var(--jrni-color-surface-card-soft)", border: "1px solid var(--jrni-color-surface-border)", borderRadius: 6, fontFamily: "var(--jrni-font-family-mono)", fontSize: 12, color: "var(--jrni-color-neutral-1)" }}>$ voyage-fleet unarchive {tenant.slug}</div>
      </Modal>
    </div>
  );
}

/* ─── Shared fleet table ───────────────────────────────────────────────── */
function FleetTable({ rows, selected, onToggleSelect, onToggleAll, extraCols = [], sort, onSort, dense }) {
  const allSelected = rows.length > 0 && rows.every(r => selected.has(r.slug));
  const someSelected = rows.some(r => selected.has(r.slug)) && !allSelected;
  const th = (label, key, w) => (
    <th style={{ textAlign: "left", padding: "0 12px", height: 38, fontSize: 11, fontWeight: 700, color: "var(--jrni-color-text-soft)", textTransform: "uppercase", letterSpacing: 0.04, whiteSpace: "nowrap", width: w, cursor: key && onSort ? "pointer" : "default", userSelect: "none" }}
      onClick={key && onSort ? () => onSort(key) : undefined}>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
        {label}
        {key && sort && sort.key === key ? <Icons.Chevron dir={sort.dir === "asc" ? "up" : "down"} size={11} /> : null}
      </span>
    </th>
  );
  return (
    <div style={{ border: "1px solid var(--jrni-color-surface-border)", borderRadius: 8, background: "white", overflow: "hidden" }}>
      <div style={{ overflowX: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--jrni-font-family-sans)" }}>
          <thead>
            <tr style={{ borderBottom: "1px solid var(--jrni-color-surface-border)", background: "var(--jrni-color-surface-card-soft)" }}>
              <th style={{ width: 40, padding: "0 0 0 14px" }}>
                <Checkbox checked={allSelected} indeterminate={someSelected} onChange={onToggleAll} />
              </th>
              {th("Tenant", "slug")}
              {th("Components")}
              {th("Modules")}
              {th("Env", "env", 64)}
              {th("Region", "region")}
              {th("Last deploy", "lastDeployDays")}
              {th("CI/CD", "ci")}
              {th("Drift", "drift")}
              {extraCols.map(c => th(c.label, c.key, c.w))}
              <th style={{ width: 44 }} />
            </tr>
          </thead>
          <tbody>
            {rows.map(t => {
              const isSel = selected.has(t.slug);
              return (
                <tr key={t.slug} onClick={() => navigate("/tenant", { slug: t.slug })} style={{
                  borderBottom: "1px solid var(--jrni-color-surface-border)", cursor: "pointer",
                  background: isSel ? "var(--jrni-color-primary-4)" : "white",
                }}
                  onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = "var(--jrni-color-surface-card-soft)"; }}
                  onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = "white"; }}>
                  <td style={{ padding: "0 0 0 14px" }} onClick={e => e.stopPropagation()}>
                    <Checkbox checked={isSel} onChange={() => onToggleSelect(t.slug)} />
                  </td>
                  <td style={{ padding: dense ? "8px 12px" : "10px 12px" }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontSize: 13.5, fontWeight: 600, color: "var(--jrni-color-neutral-1)", fontFamily: "var(--jrni-font-family-mono)" }}>{t.slug}</span>
                      {t.customOverlay ? <span title="Has customer-specific code overlay" style={{ fontSize: 9, fontWeight: 700, color: "var(--jrni-color-tertiary-purple)", background: "var(--jrni-color-primary-4)", border: "1px solid var(--jrni-color-primary-2)", borderRadius: 3, padding: "0 4px", letterSpacing: 0.03 }}>OVERLAY</span> : null}
                    </div>
                    <div style={{ fontSize: 12, color: "var(--jrni-color-text-soft)" }}>{t.name}</div>
                  </td>
                  <td style={{ padding: "0 12px" }}><CompositionCell tenant={t} /></td>
                  <td style={{ padding: "0 12px" }}><ModuleChips modules={t.modules} /></td>
                  <td style={{ padding: "0 12px" }}>
                    <span style={{ fontSize: 11, fontWeight: 600, padding: "1px 7px", borderRadius: 4, textTransform: "uppercase", letterSpacing: 0.03,
                      background: t.env === "prod" ? "var(--jrni-color-primary-3)" : "var(--jrni-color-neutral-5)",
                      color: t.env === "prod" ? "var(--jrni-color-primary-1)" : "var(--jrni-color-neutral-2)" }}>{t.env}</span>
                  </td>
                  <td style={{ padding: "0 12px", fontSize: 12.5, color: "var(--jrni-color-neutral-2)", whiteSpace: "nowrap" }}>{t.region}</td>
                  <td style={{ padding: "0 12px", fontSize: 12.5, color: "var(--jrni-color-neutral-2)", whiteSpace: "nowrap" }}>{t.lastDeployDays === 0 ? "Today" : `${t.lastDeployDays}d ago`}</td>
                  <td style={{ padding: "0 12px" }}><CiPill status={t.ci} small /></td>
                  <td style={{ padding: "0 12px" }}><DriftPill level={t.drift} /></td>
                  {extraCols.map(c => <td key={c.key} style={{ padding: "0 12px" }}>{c.render(t)}</td>)}
                  <td style={{ padding: "0 8px 0 0" }} onClick={e => e.stopPropagation()}><TenantRowActions tenant={t} /></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* ─── Filter bar ───────────────────────────────────────────────────────── */
function MultiFilter({ label, options, value, onChange }) {
  const [open, setOpen] = useS_f(false);
  const ref = useRef_f(null);
  useOutsideClick(ref, () => setOpen(false));
  const sel = value ? value.split(",").filter(Boolean) : [];
  const toggle = v => {
    const next = sel.includes(v) ? sel.filter(x => x !== v) : [...sel, v];
    onChange(next.join(","));
  };
  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button onClick={() => setOpen(o => !o)} style={{
        height: 32, padding: "0 10px", borderRadius: 6, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer",
        background: sel.length ? "var(--jrni-color-primary-4)" : "white", border: `1px solid ${sel.length ? "var(--jrni-color-primary-2)" : "var(--jrni-color-neutral-4)"}`,
        fontSize: 12.5, fontWeight: 500, color: "var(--jrni-color-neutral-1)", fontFamily: "var(--jrni-font-family-sans)", whiteSpace: "nowrap", boxShadow: "var(--jrni-shadow-chrome)",
      }}>
        {label}{sel.length ? <span style={{ fontSize: 11, fontWeight: 700, color: "var(--jrni-color-primary-1)" }}>· {sel.length}</span> : null}
        <Icons.Chevron size={11} />
      </button>
      {open ? (
        <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, minWidth: 180, background: "white", borderRadius: 8, boxShadow: "var(--jrni-shadow-lg)", border: "1px solid var(--jrni-color-surface-border)", padding: 6, zIndex: 60 }}>
          {options.map(o => (
            <button key={o.value} onClick={() => toggle(o.value)} style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "6px 8px", borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", fontSize: 13, color: "var(--jrni-color-neutral-1)", fontFamily: "var(--jrni-font-family-sans)", textAlign: "left" }}>
              <Checkbox checked={sel.includes(o.value)} onChange={() => toggle(o.value)} />
              <span style={{ flex: 1 }}>{o.label}</span>
            </button>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function SingleFilter({ label, value, onChange, options }) {
  const [open, setOpen] = useS_f(false);
  const ref = useRef_f(null);
  useOutsideClick(ref, () => setOpen(false));
  const active = value && value !== "any";
  const cur = options.find(o => o.value === value);
  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button onClick={() => setOpen(o => !o)} style={{ height: 32, padding: "0 10px", borderRadius: 6, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer", background: active ? "var(--jrni-color-primary-4)" : "white", border: `1px solid ${active ? "var(--jrni-color-primary-2)" : "var(--jrni-color-neutral-4)"}`, fontSize: 12.5, fontWeight: 500, color: "var(--jrni-color-neutral-1)", fontFamily: "var(--jrni-font-family-sans)", whiteSpace: "nowrap", boxShadow: "var(--jrni-shadow-chrome)" }}>
        {label}{active ? <span style={{ fontSize: 11, fontWeight: 700, color: "var(--jrni-color-primary-1)" }}>· {cur ? cur.label : value}</span> : null}<Icons.Chevron size={11} />
      </button>
      {open ? (
        <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, minWidth: 130, background: "white", borderRadius: 8, boxShadow: "var(--jrni-shadow-lg)", border: "1px solid var(--jrni-color-surface-border)", padding: 6, zIndex: 60 }}>
          {options.map(o => (
            <button key={o.value} onClick={() => { onChange(o.value === "any" ? "" : o.value); setOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "6px 8px", borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", fontSize: 13, color: "var(--jrni-color-neutral-1)", fontFamily: "var(--jrni-font-family-sans)", textAlign: "left" }}>
              {(value === o.value || (!value && o.value === "any")) ? <Icons.Check size={13} color="var(--jrni-color-primary-1)" /> : <span style={{ width: 13 }} />}{o.label}
            </button>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function SavedViewsButton({ params }) {
  const [open, setOpen] = useS_f(false);
  const ref = useRef_f(null);
  useOutsideClick(ref, () => setOpen(false));
  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button onClick={() => setOpen(o => !o)} style={{ height: 32, padding: "0 10px", borderRadius: 6, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer", background: "white", border: "1px solid var(--jrni-color-neutral-4)", fontSize: 12.5, fontWeight: 500, color: "var(--jrni-color-neutral-1)", boxShadow: "var(--jrni-shadow-chrome)", fontFamily: "var(--jrni-font-family-sans)" }}>
        <Icons.Star size={13} color="var(--jrni-color-text-soft)" />Saved views<Icons.Chevron size={11} />
      </button>
      {open ? (
        <div style={{ position: "absolute", top: "calc(100% + 4px)", right: 0, width: 264, background: "white", borderRadius: 8, boxShadow: "var(--jrni-shadow-lg)", border: "1px solid var(--jrni-color-surface-border)", padding: 6, zIndex: 60 }}>
          {F.SAVED_VIEWS.map(v => (
            <div key={v.id} style={{ display: "flex", alignItems: "center", gap: 6, padding: "2px 0" }}>
              <button onClick={() => { navigate("/", v.params); setOpen(false); }} style={{ flex: 1, display: "flex", alignItems: "center", gap: 8, padding: "7px 8px", borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", fontSize: 13, color: "var(--jrni-color-neutral-1)", textAlign: "left", fontFamily: "var(--jrni-font-family-sans)" }}>
                <Icons.Filter size={13} color="var(--jrni-color-text-soft)" /><span style={{ flex: 1 }}>{v.name}</span>
              </button>
              <button title="Rename" style={iconMini}><Icons.Pencil size={12} /></button>
              <button title="Delete" style={iconMini}><Icons.Trash size={12} /></button>
            </div>
          ))}
          <div style={{ height: 1, background: "var(--jrni-color-surface-border)", margin: "4px 0" }} />
          <button style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "8px", borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", fontSize: 13, fontWeight: 600, color: "var(--jrni-color-primary-1)", fontFamily: "var(--jrni-font-family-sans)" }}><Icons.Plus size={13} />Save current filters as view</button>
          <div style={{ fontSize: 10.5, color: "var(--jrni-color-text-soft)", padding: "2px 8px 4px", lineHeight: 1.4 }}>Views persist as a PR against <span style={{ fontFamily: "var(--jrni-font-family-mono)" }}>saved-views.yaml</span>.</div>
        </div>
      ) : null}
    </div>
  );
}
const iconMini = { width: 24, height: 24, borderRadius: 4, border: "none", background: "transparent", cursor: "pointer", display: "grid", placeItems: "center", color: "var(--jrni-color-text-soft)" };

function FleetFilters({ params }) {
  const set = setParam;
  const anyFilter = params.q || params.version || params.module || params.env || params.region || params.drift || params.ci || params.industry || params.pendingMig || params.hasPr || params.failedCi || params.revWindow;
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
      <div style={{ position: "relative", width: 220 }}>
        <span style={{ position: "absolute", left: 10, top: 9, pointerEvents: "none" }}><Icons.Search size={14} color="var(--jrni-color-neutral-3)" /></span>
        <input value={params.q || ""} onChange={e => set({ q: e.target.value, page: "" })} placeholder="Search tenants, modules…" style={{
          height: 32, width: "100%", boxSizing: "border-box", padding: "0 10px 0 32px", borderRadius: 6,
          border: "1px solid var(--jrni-color-neutral-4)", background: "white", fontSize: 13, fontFamily: "var(--jrni-font-family-sans)", color: "var(--jrni-color-neutral-1)", outline: "none", boxShadow: "var(--jrni-shadow-chrome)",
        }} />
      </div>
      <MultiFilter label="Components" value={params.module} onChange={v => set({ module: v, page: "" })} options={F.MODULES.map(m => ({ value: m.id, label: m.display }))} />
      <MultiFilter label="Environment" value={params.env} onChange={v => set({ env: v, page: "" })} options={[{ value: "prod", label: "Production" }, { value: "dev", label: "Development" }]} />
      <MultiFilter label="Region" value={params.region} onChange={v => set({ region: v, page: "" })} options={F.REGIONS.map(r => ({ value: r, label: r }))} />
      <MultiFilter label="Drift" value={params.drift} onChange={v => set({ drift: v, page: "" })} options={[{ value: "none", label: "Up to date" }, { value: "patch", label: "Patch behind" }, { value: "minor", label: "Minor behind" }, { value: "major", label: "Major behind" }]} />
      <SingleFilter label="Open PR" value={params.hasPr} onChange={v => set({ hasPr: v, page: "" })} options={[{ value: "any", label: "Any" }, { value: "yes", label: "Yes" }, { value: "no", label: "No" }]} />
      <SingleFilter label="Failed CI 24h" value={params.failedCi} onChange={v => set({ failedCi: v, page: "" })} options={[{ value: "any", label: "Any" }, { value: "1", label: "Yes" }]} />
      {anyFilter ? (
        <button onClick={() => navigate("/", {})} style={{ height: 32, padding: "0 10px", borderRadius: 6, border: "none", background: "transparent", color: "var(--jrni-color-primary-1)", fontSize: 12.5, fontWeight: 600, cursor: "pointer", fontFamily: "var(--jrni-font-family-sans)" }}>Clear</button>
      ) : null}
      <div style={{ flex: 1 }} />
      <SavedViewsButton params={params} />
    </div>
  );
}

/* ─── Filtering + sorting logic ────────────────────────────────────────── */
function applyFleetFilters(params) {
  const csv = k => (params[k] ? params[k].split(",").filter(Boolean) : []);
  const q = (params.q || "").toLowerCase();
  const modules = csv("module"), envs = csv("env"), regions = csv("region"), drifts = csv("drift"), cis = csv("ci"), industries = csv("industry");
  let rows = F.TENANTS.filter(t => {
    if (q && !(t.slug.includes(q) || t.name.toLowerCase().includes(q) || t.modules.some(m => m.includes(q)))) return false;
    if (modules.length && !modules.some(m => t.modules.includes(m))) return false;
    if (envs.length && !envs.includes(t.env)) return false;
    if (regions.length && !regions.includes(t.region)) return false;
    if (drifts.length && !drifts.includes(t.drift)) return false;
    if (cis.length && !cis.includes(t.ci)) return false;
    if (industries.length && !industries.includes(t.industry)) return false;
    if (params.pendingMig === "1" && !(t.pendingMigrations > 0)) return false;
    if (params.hasPr === "yes" && !t.openPR) return false;
    if (params.hasPr === "no" && t.openPR) return false;
    if (params.failedCi === "1" && t.ci !== "failing") return false;
    if (params.revWindow === "lt7" && !(t.lastRevDays < 7)) return false;
    if (params.revWindow === "7to30" && !(t.lastRevDays >= 7 && t.lastRevDays <= 30)) return false;
    if (params.revWindow === "gt30" && !(t.lastRevDays > 30)) return false;
    return true;
  });
  if (params.sort) {
    const [key, dir] = params.sort.split(":");
    const order = { none: 0, patch: 1, minor: 2, major: 3 };
    rows = [...rows].sort((a, b) => {
      let av = a[key], bv = b[key];
      if (key === "drift") { av = order[av]; bv = order[bv]; }
      if (av < bv) return dir === "asc" ? -1 : 1;
      if (av > bv) return dir === "asc" ? 1 : -1;
      return 0;
    });
  }
  return rows;
}

function Pagination({ total, page, limit }) {
  const pages = Math.ceil(total / limit);
  const start = total === 0 ? 0 : (page - 1) * limit + 1;
  const end = Math.min(total, page * limit);
  return (
    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "4px 2px", flexWrap: "wrap", gap: 12 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <span style={{ fontSize: 12.5, color: "var(--jrni-color-text-soft)" }}>{start}–{end} of <b style={{ color: "var(--jrni-color-neutral-1)" }}>{total}</b> tenants</span>
        <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
          <span style={{ fontSize: 12, color: "var(--jrni-color-text-soft)" }}>Per page</span>
          <Segmented size="sm" value={String(limit)} onChange={v => setParam({ limit: v, page: "" })} options={[{ value: "25", label: "25" }, { value: "50", label: "50" }, { value: "100", label: "100" }]} />
        </div>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
        <button disabled={page <= 1} onClick={() => setParam({ page: String(page - 1) })} style={pgBtn(page <= 1)}><Icons.Chevron dir="left" size={13} /></button>
        {Array.from({ length: pages }, (_, i) => i + 1).slice(Math.max(0, page - 3), Math.max(0, page - 3) + 5).map(p => (
          <button key={p} onClick={() => setParam({ page: String(p) })} style={{ minWidth: 30, height: 30, borderRadius: 6, border: `1px solid ${p === page ? "var(--jrni-color-primary-1)" : "var(--jrni-color-neutral-4)"}`, background: p === page ? "var(--jrni-color-primary-1)" : "white", color: p === page ? "white" : "var(--jrni-color-neutral-1)", fontSize: 12.5, fontWeight: 600, cursor: "pointer", fontFamily: "var(--jrni-font-family-sans)" }}>{p}</button>
        ))}
        <button disabled={page >= pages} onClick={() => setParam({ page: String(page + 1) })} style={pgBtn(page >= pages)}><Icons.Chevron dir="right" size={13} /></button>
      </div>
    </div>
  );
}
function pgBtn(disabled) {
  return { minWidth: 30, height: 30, borderRadius: 6, border: "1px solid var(--jrni-color-neutral-4)", background: "white", color: disabled ? "var(--jrni-color-neutral-3)" : "var(--jrni-color-neutral-1)", cursor: disabled ? "not-allowed" : "pointer", display: "grid", placeItems: "center", opacity: disabled ? 0.5 : 1 };
}

/* ─── Activity & alerts rail ───────────────────────────────────────────── */
function ActivityRail() {
  const [tab, setTab] = useS_f("alerts");
  return (
    <div style={{ width: 312, flexShrink: 0, borderLeft: "1px solid var(--jrni-color-surface-border)", background: "white", display: "flex", flexDirection: "column", minHeight: 0 }}>
      <div style={{ padding: "12px 16px 0" }}>
        <Segmented value={tab} onChange={setTab} options={[{ value: "alerts", label: "Alerts", icon: <span style={{ width: 6, height: 6, borderRadius: 999, background: "var(--jrni-color-semantic-red-1)" }} /> }, { value: "activity", label: "Activity" }]} />
      </div>
      <div style={{ flex: 1, overflowY: "auto", padding: "12px 16px" }}>
        {tab === "alerts" ? F.ALERTS.map((a, i) => (
          <div key={i} style={{ padding: "10px 0", borderBottom: i < F.ALERTS.length - 1 ? "1px solid var(--jrni-color-surface-border)" : "none" }}>
            <div style={{ display: "flex", gap: 9 }}>
              <span style={{ marginTop: 2, color: a.sev === "danger" ? "var(--jrni-color-semantic-red-1)" : a.sev === "warning" ? "var(--jrni-color-semantic-orange-1)" : "var(--jrni-color-semantic-blue-1)", flexShrink: 0 }}>
                {a.sev === "danger" ? <Icons.Warning size={15} /> : a.sev === "warning" ? <Icons.Info size={15} /> : <Icons.Info size={15} />}
              </span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: "var(--jrni-color-neutral-1)" }}>{a.title}</div>
                <div style={{ fontSize: 12, color: "var(--jrni-color-text-soft)", marginTop: 2, lineHeight: 1.45 }}>{a.body}</div>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 6 }}>
                  {a.target ? <button onClick={() => navigate("/tenant", { slug: a.target })} style={linkBtn}>{a.target}</button> : null}
                  <span style={{ fontSize: 11, color: "var(--jrni-color-neutral-3)" }}>{a.time}</span>
                </div>
              </div>
            </div>
          </div>
        )) : F.ACTIVITY.map((a, i) => (
          <div key={i} style={{ display: "flex", gap: 9, padding: "9px 0", borderBottom: i < F.ACTIVITY.length - 1 ? "1px solid var(--jrni-color-surface-border)" : "none" }}>
            <span style={{ marginTop: 1, flexShrink: 0 }}>{a.isBot ? <span style={{ width: 22, height: 22, borderRadius: 999, background: "var(--jrni-color-neutral-6)", border: "1px solid var(--jrni-color-surface-border-strong)", display: "grid", placeItems: "center", color: "var(--jrni-color-neutral-2)" }}>{a.actor === "GitHub Actions" ? <Icons.Github size={12} /> : <Icons.GitPr size={12} />}</span> : <Avatar initials={a.actor.split(" ").map(w => w[0]).join("")} color="var(--jrni-color-tertiary-light-blue)" size={22} />}</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12.5, color: "var(--jrni-color-neutral-1)", lineHeight: 1.45 }}>{a.text}</div>
              <div style={{ fontSize: 11, color: "var(--jrni-color-neutral-3)", marginTop: 2 }}>{a.actor} · {a.time}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
const linkBtn = { background: "none", border: "none", padding: 0, cursor: "pointer", fontSize: 11.5, fontWeight: 600, color: "var(--jrni-color-primary-1)", fontFamily: "var(--jrni-font-family-mono)" };

/* ─── Fleet Overview ───────────────────────────────────────────────────── */
function FleetOverview({ params }) {
  const [selected, setSelected] = useS_f(new Set());
  const limit = parseInt(params.limit || (window.FLEET.size === "large" ? "50" : "25"), 10);
  const page = parseInt(params.page || "1", 10);
  const sort = params.sort ? { key: params.sort.split(":")[0], dir: params.sort.split(":")[1] } : null;

  const filtered = useMemo_f(() => applyFleetFilters(params), [JSON.stringify(params)]);
  const total = filtered.length;
  const pageRows = filtered.slice((page - 1) * limit, page * limit);

  const toggleSelect = slug => setSelected(s => { const n = new Set(s); n.has(slug) ? n.delete(slug) : n.add(slug); return n; });
  const toggleAll = () => setSelected(s => { const all = pageRows.every(r => s.has(r.slug)); const n = new Set(s); pageRows.forEach(r => all ? n.delete(r.slug) : n.add(r.slug)); return n; });
  const onSort = key => setParam({ sort: sort && sort.key === key && sort.dir === "asc" ? `${key}:desc` : `${key}:asc` });

  const K = F.KPIS;
  const kpiActive = k => {
    if (k === "drifted") return params.drift === "minor,major" || params.drift === "major,minor";
    if (k === "upToDate") return params.drift === "none";
    if (k === "failedCi") return params.ci === "failing";
    return false;
  };

  const booting = useBoot(params);

  // P1.6 — empty-fleet first-run
  if (K.total === 0) {
    return (
      <div style={{ flex: 1, overflowY: "auto", display: "grid", placeItems: "center", padding: 40 }}>
        <div style={{ maxWidth: 560, textAlign: "center" }}>
          <span style={{ width: 60, height: 60, borderRadius: 14, background: "var(--jrni-color-primary-1)", color: "white", display: "grid", placeItems: "center", margin: "0 auto 16px" }}><Icons.Fleet size={30} color="white" /></span>
          <h1 style={{ margin: "0 0 6px", fontSize: 24, fontWeight: 700, color: "var(--jrni-color-neutral-1)", letterSpacing: -0.3 }}>Welcome to Fleet</h1>
          <p style={{ margin: "0 0 18px", fontSize: 14, color: "var(--jrni-color-text-soft)", lineHeight: 1.5 }}>Spin up your first customer tenant to get started.</p>
          <Button variant="primary" size="lg" icon={<Icons.Plus size={14} />} onClick={() => navigate("/new")}>New Tenant</Button>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12, marginTop: 28, textAlign: "left" }}>
            {[["Author a seam", "Compose modules in Claude Code.", "/seams", <Icons.Layers size={16} />], ["Spin up a tenant", "Create one from a seam.", "/new", <Icons.Rocket size={16} />], ["Trigger revs", "Roll out when Voyage ships.", "/drift", <Icons.Drift size={16} />]].map((s, i) => (
              <button key={i} onClick={() => navigate(s[2])} style={{ textAlign: "left", padding: 14, borderRadius: 8, border: "1px solid var(--jrni-color-surface-border)", background: "white", cursor: "pointer", fontFamily: "var(--jrni-font-family-sans)" }}>
                <span style={{ display: "inline-flex", color: "var(--jrni-color-primary-1)", marginBottom: 8 }}>{s[3]}</span>
                <div style={{ fontSize: 12, fontWeight: 600, color: "var(--jrni-color-text-soft)" }}>{i + 1}</div>
                <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--jrni-color-neutral-1)" }}>{s[0]}</div>
                <div style={{ fontSize: 12, color: "var(--jrni-color-text-soft)", marginTop: 2 }}>{s[1]}</div>
              </button>
            ))}
          </div>
        </div>
      </div>
    );
  }

  const sizeBanner = F.size === "tiny" ? { text: "Your fleet is small — onboard your next customer to grow it.", icon: <Icons.Plus size={14} /> }
    : F.size === "large" ? { text: "Tip — use ⌘K to find a tenant in this fleet.", icon: <Icons.Search size={14} /> } : null;

  return (
    <div style={{ display: "flex", minHeight: 0, flex: 1 }}>
      <div style={{ flex: 1, minWidth: 0, overflowY: "auto", padding: "20px 24px 28px", display: "flex", flexDirection: "column", gap: 16 }}>
        {sizeBanner ? (
          <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 14px", background: "var(--jrni-color-status-info-bg)", borderRadius: 8, border: "1px solid var(--jrni-color-semantic-blue-2)" }}>
            <span style={{ color: "var(--jrni-color-semantic-blue-1)", display: "inline-flex" }}>{sizeBanner.icon}</span>
            <span style={{ fontSize: 13, color: "var(--jrni-color-neutral-1)", flex: 1 }}>{sizeBanner.text}</span>
            {F.size === "large" ? <button onClick={() => navigate("/")} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--jrni-color-text-soft)" }}><Icons.X size={13} /></button> : null}
          </div>
        ) : null}
        <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
          <div>
            <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700, color: "var(--jrni-color-neutral-1)", letterSpacing: -0.3 }}>Fleet Overview</h1>
            <p style={{ margin: "3px 0 0", fontSize: 13.5, color: "var(--jrni-color-text-soft)" }}>{K.total} tenant deployments · {F.MODULES.length} Voyage components · as of {F.NOW.label} {F.NOW.time}</p>
          </div>
          <div style={{ display: "flex", gap: 8 }}>
            <Button variant="secondary" size="md" icon={<Icons.Csv size={14} />}>Export CSV</Button>
            <Button variant="secondary" size="md" icon={<Icons.Bolt size={14} />} onClick={() => navigate("/bulk", params.q || params.drift ? { from: "fleet" } : {})}>Bulk update</Button>
            <Button variant="primary" size="md" icon={<Icons.Plus size={13} />} onClick={() => navigate("/new")}>New tenant</Button>
          </div>
        </div>

        {/* KPI strip */}
        {booting ? (
          <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>{Array.from({ length: 7 }, (_, i) => <SkeletonTile key={i} />)}</div>
        ) : (
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <KpiTile label="Total tenants" value={K.total} icon={<Icons.Fleet size={13} />} active={!params.drift && !params.ci} onClick={() => navigate("/", {})} />
          <KpiTile label="Drifted" value={K.drifted} tone="warning" sub="≥1 component behind" icon={<Icons.Drift size={13} />} active={kpiActive("drifted")} onClick={() => navigate("/", { drift: "minor,major" })} />
          <KpiTile label="Up to date" value={K.upToDate} tone="success" sub="all components current" icon={<Icons.Check size={13} />} active={kpiActive("upToDate")} onClick={() => navigate("/", { drift: "none" })} />
          <KpiTile label="Failed CI / 24h" value={K.failedCi} tone="danger" icon={<Icons.Warning size={13} />} active={kpiActive("failedCi")} onClick={() => navigate("/", { ci: "failing" })} />
          <KpiTile label="Components updated / wk" value={K.componentsUpdatedWeek} tone="info" sub="new versions published" icon={<Icons.Release size={13} />} onClick={() => navigate("/modules")} />
          <KpiTile label="Pending migration" value={K.pendingMigration} tone="warning" icon={<Icons.Migration size={13} />} active={params.pendingMig === "1"} onClick={() => navigate("/", { pendingMig: "1" })} />
        </div>
        )}

        {/* filters */}
        <FleetFilters params={params} />

        {/* selection bar */}
        {selected.size > 0 ? (
          <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 14px", background: "var(--jrni-color-primary-4)", border: "1px solid var(--jrni-color-primary-2)", borderRadius: 8 }}>
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--jrni-color-primary-1)" }}>{selected.size} selected</span>
            <Button variant="primary" size="sm" icon={<Icons.Bolt size={12} />} onClick={() => navigate("/bulk", { from: "selection" })}>Plan component rev</Button>
            <Button variant="secondary" size="sm" icon={<Icons.GitPr size={12} />}>Open upgrade PRs</Button>
            <span style={{ fontSize: 11.5, color: "var(--jrni-color-text-soft)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icons.Terminal size={12} />Revs run in Claude Code</span>
            <button onClick={() => setSelected(new Set())} style={{ marginLeft: "auto", background: "none", border: "none", cursor: "pointer", color: "var(--jrni-color-primary-1)", fontSize: 12.5, fontWeight: 600 }}>Clear selection</button>
          </div>
        ) : null}

        {/* table */}
        {booting ? (
          <div style={{ border: "1px solid var(--jrni-color-surface-border)", borderRadius: 8, background: "white", overflow: "hidden" }}><SkeletonRows n={12} /></div>
        ) : total === 0 ? (
          <Empty icon={<Icons.Search size={20} />} title="No tenants match these filters" body="Try clearing a filter or widening your search." action={<Button variant="secondary" size="sm" onClick={() => navigate("/", {})}>Clear filters</Button>} />
        ) : (
          <FleetTable rows={pageRows} selected={selected} onToggleSelect={toggleSelect} onToggleAll={toggleAll} sort={sort} onSort={onSort} />
        )}
        {!booting && total > 0 ? <Pagination total={total} page={page} limit={limit} /> : null}
      </div>
      <ActivityRail />
    </div>
  );
}

Object.assign(window, { FleetOverview, FleetTable, ActivityRail, TenantRowActions, applyFleetFilters, Pagination, MultiFilter });
