/* ============================================================
   page-misc.jsx — Audit, Messages, Providers, Server
   ============================================================ */

/* Re-bind window exports from other Babel scripts */
const StatusPill   = window.StatusPill   || (({ children }) => React.createElement('span', null, children));
const parseDetail  = window.parseDetail  || ((body, status) => body?.message || `HTTP ${status}`);

/* Issue #32: human-readable audit action labels.
   Primary source of truth is the API field `action_label` (sent by every
   /api/audit row). This local table mirrors api.py:_AUDIT_ACTION_LABELS so
   we still render cleanly if the FE caches a row from an older API build,
   if action_label is missing, or if an action_type isn't in the table yet
   (last-resort fallback: capitalize underscores → spaces). */
const _AUDIT_ACTION_LABELS = {
  signin_recorded:               "Sign-in recorded",
  signoff_recorded:              "Sign-off recorded",
  format_strike:                 "Format strike",
  late_strike:                   "Late strike",
  missing_signoff:               "Missing sign-off",
  late_reason_waived:            "Late reason accepted",
  late_reason_rejected:          "Late reason rejected",
  dm_sent:                       "DM sent",
  dm_failed:                     "DM failed",
  public_reply:                  "Public reply (room)",
  flag:                          "Flagged",
  hr_doc_uploaded:               "HR document uploaded",
  hr_doc_viewed:                 "HR document viewed",
  hr_doc_deleted:                "HR document deleted",
  oncall_csv_import:             "On-call CSV imported",
  pto_approved:                  "PTO approved",
  pto_denied:                    "PTO denied",
  strike_warned:                 "Strike warning sent",
  strike_escalated:              "Strike escalated to admin",
  availability_ping_sent:        "Availability ping sent",
  availability_ping_responded:   "Availability ping responded",
  availability_miss_escalated:   "Availability miss escalated",
  user_edited:                   "User edited",
  team_created:                  "Team created",
  team_edited:                   "Team edited",
  policy_created:                "Policy created",
  policy_edited:                 "Policy edited",
  ai_query:                      "AI query",
  ai_provider_fallback:          "AI fallback (provider switch)",
  otp_sent:                      "OTP sent",
  otp_verified:                  "OTP verified",
  otp_failed:                    "OTP failed",
  trusted_device_added:          "Trusted device added",
  session_revoked:               "Session revoked",
  admin_manual_violation:        "Admin manual violation",
  format_violation:              "Format violation",
  admin_override_applied:        "Admin override applied",
  container_start:               "Container started",
  db_migration:                  "Database migration",
  health_check_fail:             "Health check failed",
  admin_manual_strike:           "Admin manual strike",
  thread_reply:                  "Bot thread reply",
  ip_red_flag:                   "IP red-flag",
  no_progress:                   "No progress detected",
  stuck_detected:                "Stuck task detected",
  stalled_task:                  "Stalled task",
  repeated_accomplishment:       "Repeated accomplishment",
  force_strike:                  "Force strike (admin)",
  force_clear_strikes:           "Strikes cleared (admin)",
  force_decrement_strike:        "Strike decremented (admin)",
};
function humanizeActionType(raw) {
  if (!raw) return '';
  if (_AUDIT_ACTION_LABELS[raw]) return _AUDIT_ACTION_LABELS[raw];
  const s = String(raw).replace(/_/g, ' ').trim();
  return s.charAt(0).toUpperCase() + s.slice(1);
}
// Expose so other Babel scripts (page-employees violations history,
// hud.jsx audit widget) can use the same table.
try { window.humanizeActionType = humanizeActionType; } catch (_) {}

/* ── Helpers ── */
function relTime(iso) {
  const d = new Date(iso);
  const raw = Math.floor((Date.now() - d) / 1000);
  // clock-skew safe — abs() the magnitude and prefix "in" when iso is in the future
  const sec = Math.abs(raw);
  const future = raw < 0;
  if (sec < 30) return "just now";
  const fmt = (n, unit) => future ? `in ${n}${unit}` : `${n}${unit} ago`;
  if (sec < 60)    return fmt(sec, "s");
  if (sec < 3600)  return fmt(Math.floor(sec/60), "m");
  if (sec < 86400) return fmt(Math.floor(sec/3600), "h");
  return fmt(Math.floor(sec/86400), "d");
}

function absTime(iso) {
  return new Date(iso).toLocaleString([], { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' });
}

const KIND_COLOR = {
  'sign-in':   'ok',
  'sign_in':   'ok',
  'sign-off':  'info',
  'sign_off':  'info',
  'sick':      'warn',
  'pto':       'warn',
  'vacation':  'warn',
  'half_day':  'warn',
  'late':      'warn',
  'invalid':   'err',
  'format-violation': 'err',
  'unknown':   'mute',
};

function kindColor(k) { return KIND_COLOR[k] || 'mute'; }

const PAGE_SIZE = 50;

/* ============================================================
   MessagesPage
   ============================================================ */
/* ── Broadcast DM Section ── */
const BROADCAST_GROUPS = [
  { value: 'all',          label: 'Everyone' },
  { value: 'admins',       label: 'Admins only' },
  { value: 'absent_today', label: 'Absent today' },
  { value: 'late_today',   label: 'Late today' },
  { value: 'test_only',    label: 'Test accounts only' },
];
// NOTE: label values above are English lookup keys; wrapped with appT at the render site.

// Keep in sync with the backend cap in /api/messages/broadcast (api.py).
const MSG_MAX_LEN = 16000;

function BroadcastDmSection({ employees }) {
  const toast = useToast();
  const [mode, setMode]       = useState('group');   // 'team'|'group'|'manual'
  const [teams, setTeams]     = useState([]);
  const [selTeam, setSelTeam] = useState('');
  const [selGroup, setSelGroup] = useState('all');
  const [selManual, setSelManual] = useState([]);    // matrix_ids
  const [msgBody, setMsgBody] = useState('');
  const [preview, setPreview] = useState([]);        // resolved names preview
  const [sending, setSending] = useState(false);
  const [channel, setChannel] = useState('dm');   // 'dm' (direct msgs) | 'rooms' (post in team rooms)

  /* load teams once */
  useEffect(() => {
    fetch('/api/teams', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(rows => {
        // Filter FIRST, then seed selTeam from the same filtered list — otherwise
        // a team row with an empty/null name (which the dropdown drops) can sort
        // first and leave selTeam='' while the <select> visually shows a real team,
        // sending filter_value='' → 400 "filter_value must be a team name string".
        const valid = rows.filter(t => t.name || t.team_name);
        setTeams(valid);
        if (valid.length) setSelTeam(valid[0].name || valid[0].team_name || '');
      })
      .catch(() => {});
  }, []);

  /* resolve preview whenever mode/values/employees change */
  useEffect(() => {
    if (mode === 'manual') {
      const m = employees.filter(e => selManual.includes(e.matrix_id));
      setPreview(m.map(e => e.display_name));
    } else if (mode === 'team') {
      const m = employees.filter(e => e.team === selTeam);
      setPreview(m.map(e => e.display_name));
    } else {
      /* group — we can't perfectly resolve absent/late without a live query,
         show a label instead */
      const grp = BROADCAST_GROUPS.find(g => g.value === selGroup);
      setPreview(grp ? [(window.appLang&&window.appLang()==='ar') ? `(${(window.appT||(s=>s))(grp.label)} — يُحدَّد عند الإرسال)` : `(${grp.label} — resolved on send)`] : []);
    }
  }, [mode, selTeam, selGroup, selManual, employees]);

  const toggleManual = (mxid) => {
    setSelManual(prev => prev.includes(mxid) ? prev.filter(x => x !== mxid) : [...prev, mxid]);
  };

  const send = async () => {
    if (!msgBody.trim()) { toast.push({ msg: 'Message body is required', kind: 'err', icon: '!' }); return; }
    if (channel === 'dm' && mode === 'team' && !selTeam.trim()) {
      toast.push({ msg: 'Select a team first', kind: 'err', icon: '!' }); return;
    }
    if (channel === 'dm' && mode === 'manual' && selManual.length === 0) {
      toast.push({ msg: 'Pick at least one recipient', kind: 'err', icon: '!' }); return;
    }
    setSending(true);
    try {
      const filter_value = mode === 'manual' ? selManual : mode === 'team' ? selTeam : selGroup;
      const payload = channel === 'rooms'
        ? { channel: 'rooms', body: msgBody }
        : { filter_type: mode, filter_value, body: msgBody };
      const r = await fetch('/api/messages/broadcast', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify(payload),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(data.detail || `HTTP ${r.status}`);
      if (channel === 'rooms') {
        toast.push({ msg: `Posted to ${data.posted||0} room${(data.posted||0) !== 1 ? 's' : ''}`, kind: 'ok', icon: '✓' });
      } else {
        const sent = data.sent || 0, failed = data.failed || 0, skipped = data.skipped || 0;
        let msg = `Sent to ${sent} user${sent !== 1 ? 's' : ''}`;
        if (failed > 0)  msg += `, failed for ${failed}`;
        if (skipped > 0) msg += `, skipped ${skipped} (never-DM)`;
        if (failed > 0 || skipped > 0) msg += ' (see audit log)';
        const kind = (failed > 0 || skipped > 0) ? 'warn' : 'ok';
        toast.push({ msg, kind, icon: kind === 'warn' ? '!' : '✓' });
      }
      setMsgBody('');
      if (mode === 'manual') setSelManual([]);
    } catch (e) {
      toast.push({ msg: `Broadcast failed: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setSending(false);
    }
  };

  const previewFirst5 = preview.slice(0, 5);
  const previewRest   = preview.length - 5;

  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <SectionHeader title={(window.appT||(s=>s))("Send DM")}
          tip={(window.appT||(s=>s))("Compose a manual DM to one user as the bot. Optional template dropdown for prebuilt messages.")} sub={(window.appT||(s=>s))("Broadcast a direct message to one or more users")} />

      {/* Delivery channel: DM each person, or post one announcement in the team rooms */}
      <div style={{ display: 'flex', gap: 4, marginBottom: 12 }}>
        {[['dm','Direct messages'],['rooms','Post in team rooms']].map(([v,l]) => (
          <button key={v}
            className={`btn ${channel === v ? 'btn-pri' : 'btn-ghost'}`}
            style={{ fontSize: 12, padding: '4px 12px' }}
            onClick={() => setChannel(v)}
          >{(window.appT||(s=>s))(l)}</button>
        ))}
      </div>
      {channel === 'rooms' && (
        <div style={{ fontSize: 11, color: 'var(--text-3)', marginBottom: 10 }}>
          {(window.appT||(s=>s))("Posts one announcement into every team attendance + general room (everyone sees it in-channel).")}
        </div>
      )}

      {channel === 'dm' && (<>
      {/* Mode tabs */}
      <div style={{ display: 'flex', gap: 4, marginBottom: 12 }}>
        {[['group','By group'],['team','By team'],['manual','Pick manually']].map(([v,l]) => (
          <button
            key={v}
            className={`btn ${mode === v ? 'btn-pri' : 'btn-ghost'}`}
            style={{ fontSize: 12, padding: '4px 12px' }}
            onClick={() => setMode(v)}
          >{(window.appT||(s=>s))(l)}</button>
        ))}
      </div>

      {/* Mode-specific selector */}
      {mode === 'group' && (
        <select className="inp" value={selGroup} onChange={e => setSelGroup(e.target.value)} style={{ height: 32, marginBottom: 10 }}>
          {BROADCAST_GROUPS.map(g => <option key={g.value} value={g.value}>{(window.appT||(s=>s))(g.label)}</option>)}
        </select>
      )}
      {mode === 'team' && (
        <select className="inp" value={selTeam} onChange={e => setSelTeam(e.target.value)} style={{ height: 32, marginBottom: 10 }}>
          {teams.length === 0 && <option value="">{(window.appT||(s=>s))("No teams configured")}</option>}
          {teams.map(t => {
            const name = t.name || t.team_name || '';
            return <option key={name} value={name}>{name}</option>;
          })}
        </select>
      )}
      {mode === 'manual' && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 10, maxHeight: 120, overflowY: 'auto', padding: 2 }}>
          {employees.map(e => {
            const sel = selManual.includes(e.matrix_id);
            return (
              <button
                key={e.id}
                className={`btn ${sel ? 'btn-pri' : 'btn-ghost'}`}
                style={{ fontSize: 11, padding: '3px 8px', borderRadius: 99 }}
                onClick={() => toggleManual(e.matrix_id)}
              >{e.display_name}</button>
            );
          })}
        </div>
      )}

      {/* Recipient chip preview */}
      {preview.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 10, alignItems: 'center' }}>
          <span style={{ fontSize: 11, color: 'var(--text-3)', marginRight: 4 }}>{preview.length > 5 ? ((window.appLang&&window.appLang()==='ar') ? `${preview.length} مستلمين:` : `${preview.length} recipients:`) : (window.appT||(s=>s))('To:')}</span>
          {previewFirst5.map((n, i) => (
            <span key={i} style={{ fontSize: 11, background: 'var(--bg-elev-2)', borderRadius: 99, padding: '2px 8px' }}>{n}</span>
          ))}
          {previewRest > 0 && (
            <span style={{ fontSize: 11, color: 'var(--text-3)' }}>{(window.appLang&&window.appLang()==='ar') ? `و${previewRest} آخرين` : `and ${previewRest} more`}</span>
          )}
        </div>
      )}
      {preview.length === 0 && mode !== 'group' && (
        <div style={{ fontSize: 11, color: 'var(--text-3)', marginBottom: 10 }}>{(window.appT||(s=>s))("No recipients selected")}</div>
      )}
      </>)}

      {/* Message body */}
      <textarea
        className="inp"
        rows={4}
        value={msgBody}
        onChange={e => setMsgBody(e.target.value.slice(0, MSG_MAX_LEN))}
        maxLength={MSG_MAX_LEN}
        placeholder={(window.appT||(s=>s))("Type your message…")}
        style={{ width: '100%', resize: 'vertical', marginBottom: 4, boxSizing: 'border-box' }}
      />
      <div style={{ display: 'flex', justifyContent: 'flex-end', fontSize: 12,
                    color: msgBody.length > MSG_MAX_LEN * 0.9 ? 'var(--warn, #c47f17)' : 'var(--muted, #888)',
                    marginBottom: 10 }}>
        {msgBody.length.toLocaleString()} / {MSG_MAX_LEN.toLocaleString()}
      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <button className="btn btn-pri" onClick={send} disabled={sending || !msgBody.trim()}>
          {sending ? (window.appT||(s=>s))('Sending…') : <><Icon name="send" size={13}/> {(window.appT||(s=>s))(channel === 'rooms' ? 'Post announcement' : 'Send DM')}</>}
        </button>
      </div>
    </div>
  );
}

function MessagesPage() {
  const toast = useToast();

  /* filter state */
  const [qText, setQText]     = useState('');
  const [filterKind, setFK]   = useState('');
  const [filterUser, setFU]   = useState('');
  const [expanded, setExpand] = useState(null); // row id

  /* server state */
  const [items, setItems]   = useState([]);
  const [total, setTotal]   = useState(0);
  const [page, setPage]     = useState(1);
  const [loading, setLoad]  = useState(false);
  const [stats, setStats]   = useState(null);
  const [employees, setEmps] = useState([]);

  /* debounce helpers */
  const debRef = useRef(null);

  const buildParams = useCallback((pg) => {
    const p = new URLSearchParams();
    p.set('limit', String(PAGE_SIZE));
    p.set('offset', String((pg - 1) * PAGE_SIZE));
    if (filterKind) p.set('kind', filterKind);
    if (filterUser) p.set('user_id', filterUser);
    if (qText.trim()) p.set('q_text', qText.trim());
    return p.toString();
  }, [filterKind, filterUser, qText]);

  const load = useCallback(async (pg, notify) => {
    setLoad(true);
    try {
      const r = await fetch(`/api/messages?${buildParams(pg)}`, { credentials: 'same-origin' });
      if (!r.ok) throw new Error(`${r.status}`);
      const data = await r.json();
      setItems(data.items || []);
      setTotal(data.total || 0);
      setPage(pg);
      if (notify) toast.push({ msg: 'Refreshed', kind: 'ok', icon: '✓' });
    } catch (e) {
      toast.push({ msg: `Messages error: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setLoad(false);
    }
  }, [buildParams, toast]);

  /* load stats + employees once */
  useEffect(() => {
    fetch('/api/messages/stats', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => d && setStats(d))
      .catch(() => {});
    fetch('/api/employees', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(setEmps)
      .catch(() => {});
  }, []);

  /* initial load */
  useEffect(() => { load(1, false); }, []);

  /* debounced re-fetch when filters change */
  useEffect(() => {
    clearTimeout(debRef.current);
    debRef.current = setTimeout(() => {
      load(1, false);
      if (filterKind || filterUser || qText.trim()) {
        toast.push({ msg: 'Filter applied', kind: 'ok', icon: '✓' });
      }
    }, 380);
    return () => clearTimeout(debRef.current);
  }, [filterKind, filterUser, qText]);

  /* by_kind is a dict {kind: count} in the real API, not an array */
  const byKind = useMemo(() => {
    if (!stats) return [];
    const raw = stats.by_kind || {};
    return Object.entries(raw)
      .map(([kind, count]) => ({ kind, count }))
      .sort((a, b) => b.count - a.count);
  }, [stats]);

  const maxKindCount = useMemo(() => Math.max(...byKind.map(x => x.count), 1), [byKind]);

  const invalidCount = byKind.find(k => k.kind === 'invalid')?.count ?? 0;

  /* group items by kind for display */
  const grouped = useMemo(() => {
    const groups = {};
    for (const m of items) {
      const k = m.parsed_kind || 'unknown';
      if (!groups[k]) groups[k] = [];
      groups[k].push(m);
    }
    return Object.entries(groups);
  }, [items]);

  return (
    <div className="page">
      <PageHeader
        title={(window.appT||(s=>s))("Messages")}
        meta={(window.appT||(s=>s))("Attendance messages the bot has parsed and classified")}
        right={<Refresh onClick={() => load(page, true)} loading={loading} />}
      />

      {/* Broadcast DM */}
      <BroadcastDmSection employees={employees} />

      {/* Q11: parser backfill dry-run */}
      <ParseBackfillCard />

      {/* KPI row */}
      <div className="kpi-grid kpi-grid-3" style={{ marginBottom: 16 }}>
        <KPI label={(window.appT||(s=>s))("This period")}    value={(stats?.total ?? '—').toLocaleString?.()} icon={<Icon name="msg" size={14}/>} />
        <KPI label={(window.appT||(s=>s))("Invalid")}        value={invalidCount} accent="var(--err)" icon={<Icon name="alert" size={14}/>} />
        <KPI label={(window.appT||(s=>s))("Kinds tracked")}  value={byKind.length} accent="var(--brand)" icon={<Icon name="pulse" size={14}/>} />
      </div>

      {/* Breakdown chart */}
      {byKind.length > 0 && (
        <div className="card" style={{ marginBottom: 16 }}>
          <SectionHeader title={(window.appT||(s=>s))("Breakdown by kind")}
          tip={(window.appT||(s=>s))("Counts of standup messages grouped by parsed kind (signin / signoff / sick / pto / unknown). Last 30 days.")} sub={(window.appT||(s=>s))("Last 30 days")} />
          <div style={{ display:'grid', gridTemplateColumns:'260px 1fr', gap: '16px 20px', alignItems:'start' }}>
            <div style={{ display:'flex', justifyContent:'center', alignItems:'center' }}>
              <KindPie byKind={byKind} />
            </div>
            <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(180px, 1fr))', gap: '8px 16px' }}>
            {byKind.map(k => (
              <div key={k.kind}>
                <div style={{ display:'flex', justifyContent:'space-between', fontSize: 12, marginBottom: 3 }}>
                  <span style={{ textTransform:'capitalize' }}>{k.kind.replace(/_/g,' ')}</span>
                  <span className="mono">{k.count}</span>
                </div>
                <div className="kpi-bar">
                  <div className="kpi-bar-fill" style={{
                    width: `${(k.count / maxKindCount) * 100}%`,
                    background: `var(--${kindColor(k.kind) === 'mute' ? 'text-3' : kindColor(k.kind)})`,
                  }} />
                </div>
              </div>
            ))}
            </div>
          </div>
        </div>
      )}

      {/* Filters */}
      <div className="card">
        <div className="toolbar" style={{ flexWrap:'wrap', gap: 8 }}>
          <SearchInput
            value={qText}
            onChange={setQText}
            placeholder={(window.appT||(s=>s))("Search message body...")}
            style={{ flex: '1 1 200px' }}
          />
          <select
            className="inp"
            value={filterUser}
            onChange={e => setFU(e.target.value)}
            style={{ flex:'0 0 160px', height: 32 }}
          >
            <option value="">{(window.appT||(s=>s))("All employees")}</option>
            {employees.map(e => (
              <option key={e.id} value={e.id}>{e.display_name}</option>
            ))}
          </select>
          <select
            className="inp"
            value={filterKind}
            onChange={e => setFK(e.target.value)}
            style={{ flex:'0 0 140px', height: 32 }}
          >
            <option value="">{(window.appT||(s=>s))("All kinds")}</option>
            {['sign-in','sign_in','sign-off','sign_off','sick','pto','vacation','half_day','late','invalid','unknown'].map(k => (
              <option key={k} value={k}>{k.replace(/_/g,' ')}</option>
            ))}
          </select>
        </div>

        {/* Message list — grouped by kind */}
        {loading && items.length === 0 && (
          <div style={{ padding: 24 }}><Skel h={40} /><Skel h={40} style={{marginTop:8}} /><Skel h={40} style={{marginTop:8}} /></div>
        )}

        {!loading && items.length === 0 && (
          <Empty
            icon={
              <svg width="80" height="80" viewBox="0 0 80 80" fill="none" style={{opacity:0.35}}>
                <rect x="10" y="20" width="60" height="42" rx="8" stroke="currentColor" strokeWidth="3" fill="none"/>
                <path d="M10 42 L28 54 L40 46 L52 54 L70 42" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinejoin="round"/>
                <circle cx="25" cy="34" r="3" fill="currentColor"/>
                <circle cx="40" cy="34" r="3" fill="currentColor"/>
                <circle cx="55" cy="34" r="3" fill="currentColor"/>
              </svg>
            }
            title={(window.appT||(s=>s))("No messages yet")}
            subtitle={(window.appT||(s=>s))("Bot DMs and admin replies will appear here.")}
          />
        )}

        {grouped.map(([kind, msgs]) => (
          <div key={kind}>
            <div style={{
              padding: '6px 0 4px',
              fontSize: 11,
              fontWeight: 600,
              textTransform: 'uppercase',
              letterSpacing: '0.06em',
              color: 'var(--text-3)',
              borderTop: '1px solid var(--line)',
              marginTop: 8,
            }}>
              {kind.replace(/_/g,' ')} &middot; {msgs.length}
            </div>
            {msgs.map(m => (
              <MsgRow
                key={m.id}
                m={m}
                expanded={expanded === m.id}
                onToggle={() => setExpand(x => x === m.id ? null : m.id)}
              />
            ))}
          </div>
        ))}

        <Pagination
          page={page}
          pageSize={PAGE_SIZE}
          total={total}
          onChange={pg => load(pg, false)}
        />
      </div>
    </div>
  );
}

function MsgRow({ m, expanded, onToggle }) {
  const body = m.body || '';
  const preview = body.length > 120 ? body.slice(0, 120) + '…' : body;
  const name = m.display_name || m.matrix_id || '?';
  const initials = name.slice(0, 2).toUpperCase();
  const avatarPerson = { display_name: name, initials, avatar: ['#3b66f5', '#1d3286'] };

  return (
    <div
      className="msg-row"
      onClick={onToggle}
      style={{ cursor:'pointer', borderRadius:'var(--r-sm)', padding: '8px 4px', transition:'background .15s' }}
    >
      <div className="msg-head">
        <Avatar person={avatarPerson} size={24} showPip={false} />
        <span style={{ fontWeight: 500, fontSize: 12.5 }}>{name}</span>
        <Badge kind={kindColor(m.parsed_kind)}>{(m.parsed_kind || 'unknown').replace(/_/g,' ')}</Badge>
        {!m.is_valid && <Badge kind="err">{(window.appT||(s=>s))("invalid")}</Badge>}
        <span
          className="muted mono"
          style={{ marginLeft:'auto', fontSize: 11 }}
          title={absTime(m.sent_at)}
        >
          {relTime(m.sent_at)}
        </span>
      </div>
      {!expanded && <div className="msg-body" style={{ color:'var(--text-2)', fontSize: 12 }}>{preview}</div>}
      {expanded && (
        <div style={{ marginTop: 6 }}>
          <div className="msg-body" style={{ whiteSpace:'pre-wrap', fontSize: 12 }}>{body}</div>
          <div className="mono muted" style={{ fontSize: 10, marginTop: 6 }}>
            {absTime(m.sent_at)} &middot; event: {m.matrix_event_id || '—'} &middot; room: {m.room_id || '—'}
          </div>
          <ParseDebugPanel messageId={m.id} />
        </div>
      )}
    </div>
  );
}

function ParseDebugPanel({ messageId }) {
  // Q11: inline parser-trace inspector. Calls /api/parse/debug?message_id=...
  const [data, setData]   = useState(null);
  const [open, setOpen]   = useState(false);
  const [loading, setLd]  = useState(false);

  const load = async () => {
    setLd(true);
    try {
      const r = await fetch(`/api/parse/debug?message_id=${messageId}`, { credentials: 'same-origin' });
      if (r.ok) setData(await r.json());
    } catch (_) {}
    finally { setLd(false); }
  };

  useEffect(() => {
    if (open && !data) load();
  }, [open]);

  return (
    <div style={{ marginTop: 8, borderTop: '1px dashed var(--line)', paddingTop: 8 }}>
      <button className="btn btn-ghost"
        onClick={e => { e.stopPropagation(); setOpen(o => !o); }}
        style={{ fontSize: 11, padding: '2px 8px' }}>
        {open ? (window.appLang&&window.appLang()==='ar') ? '▼ إخفاء تتبّع التحليل' : '▼ Hide parse debug' : (window.appLang&&window.appLang()==='ar') ? '▶ عرض تتبّع التحليل' : '▶ Show parse debug'}
      </button>
      {open && (
        <div style={{ marginTop: 8, fontSize: 11, fontFamily: 'var(--font-mono)' }}>
          {loading && <Loading label={(window.appT||(s=>s))("Loading parser trace…")} rows={2} />}
          {data && (
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap: 12 }}>
              <div>
                <div style={{ fontWeight: 600, marginBottom: 4 }}>
                  {(window.appT||(s=>s))("Result")} <span style={{ color: data.is_valid ? 'var(--ok)' : 'var(--err)' }}>
                    ({data.kind}{data.is_valid ? '' : ' · ' + (window.appT||(s=>s))('invalid')})
                  </span>
                </div>
                <div style={{ marginBottom: 4 }}>{(window.appT||(s=>s))("Yesterday")} ({data.yesterday.length}):</div>
                <ul style={{ margin: 0, paddingLeft: 16 }}>
                  {data.yesterday.map((t, i) => <li key={i}>{t}</li>)}
                </ul>
                <div style={{ margin: '4px 0' }}>{(window.appT||(s=>s))("Today")} ({data.today.length}):</div>
                <ul style={{ margin: 0, paddingLeft: 16 }}>
                  {data.today.map((t, i) => <li key={i}>{t}</li>)}
                </ul>
                {data.has_blocker !== null && (
                  <div style={{ marginTop: 4 }}>
                    {(window.appT||(s=>s))("Blocker:")} <span style={{ color: data.has_blocker ? 'var(--warn, #daa30a)' : 'var(--ok)' }}>
                      {data.has_blocker ? ((window.appLang&&window.appLang()==='ar') ? `نعم — ${data.blocker_text}` : `yes — ${data.blocker_text}`) : (window.appT||(s=>s))('no')}
                    </span>
                  </div>
                )}
                {data.issues.length > 0 && (
                  <div style={{ marginTop: 4, color: 'var(--err)' }}>
                    {(window.appT||(s=>s))("Issues:")} {data.issues.join('; ')}
                  </div>
                )}
              </div>
              <div>
                <div style={{ fontWeight: 600, marginBottom: 4 }}>{(window.appT||(s=>s))("Line trace")}</div>
                <table className="t" style={{ fontSize: 10 }}>
                  <thead><tr>
                    <th>raw</th><th>sect</th><th>bul</th><th>hdr</th><th>cont</th>
                  </tr></thead>
                  <tbody>
                    {data.line_trace.map((ln, i) => (
                      <tr key={i}>
                        <td style={{ maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={ln.raw}>{ln.raw || <em className="muted">∅</em>}</td>
                        <td>{ln.section || '—'}</td>
                        <td>{ln.bullet ? '●' : ''}</td>
                        <td>{ln.header ? ln.header.section : ''}</td>
                        <td>{ln.indent_cont ? '↳' : ''}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function ParseBackfillCard() {
  // Q11: admin backfill — POST /api/parse/backfill with days=N, dry-run by default.
  const [days, setDays]     = useState(30);
  const [running, setRun]   = useState(false);
  const [result, setResult] = useState(null);
  const toast = useToast();

  const go = async () => {
    setRun(true); setResult(null);
    try {
      const r = await fetch('/api/parse/backfill', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ days, apply: false }),
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setResult(d);
      toast.push({ msg: `Backfill complete: ${d.changed}/${d.scanned} changed`, kind: 'ok' });
    } catch (e) {
      toast.push({ msg: 'Backfill failed: ' + e.message, kind: 'err' });
    } finally { setRun(false); }
  };

  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <SectionHeader
        title={(window.appT||(s=>s))("Re-parse historical messages")}
        tip={(window.appT||(s=>s))("Dry-run re-parse of the last N days' raw messages with the current parser. Shows which Checkin rows would change. Does not apply changes.")}
        sub={(window.appT||(s=>s))("Dry-run only — surfaces parse drift without mutating data")}
      />
      <div style={{ display:'flex', alignItems:'center', gap: 8, marginTop: 8 }}>
        <label style={{ fontSize: 12 }}>{(window.appT||(s=>s))("Days back:")}</label>
        <input type="number" className="inp" value={days} onChange={e => setDays(Number(e.target.value) || 30)}
          style={{ width: 80, height: 30 }} min={1} max={365} />
        <button className="btn btn-pri" disabled={running} onClick={go}>
          {running ? (window.appT||(s=>s))('Running…') : (window.appT||(s=>s))('Run dry-run')}
        </button>
      </div>
      {result && (
        <div style={{ marginTop: 12, fontSize: 12 }}>
          <div className="mono muted">
            {(window.appT||(s=>s))("Scanned:")} {result.scanned} · {(window.appT||(s=>s))("Changed:")} {result.changed} · {(window.appT||(s=>s))("Days:")} {result.days}
          </div>
          {result.per_user.length === 0 ? (
            <div className="muted" style={{ marginTop: 6 }}>{(window.appT||(s=>s))("No drift detected — parser output matches existing checkins.")}</div>
          ) : (
            <table className="t" style={{ fontSize: 11, marginTop: 6 }}>
              <thead><tr><th>{(window.appT||(s=>s))("User")}</th><th>{(window.appT||(s=>s))("Msgs changed")}</th><th>{(window.appT||(s=>s))("+items")}</th><th>{(window.appT||(s=>s))("−items")}</th><th>{(window.appT||(s=>s))("Sample")}</th></tr></thead>
              <tbody>
                {result.per_user.map(u => (
                  <tr key={u.user_id}>
                    <td>{u.user_id}</td>
                    <td>{u.messages_changed}</td>
                    <td style={{ color: 'var(--ok)' }}>+{u.items_added}</td>
                    <td style={{ color: 'var(--err)' }}>−{u.items_removed}</td>
                    <td className="mono muted" style={{ fontSize: 10, maxWidth: 320 }}>
                      {u.samples.map((s, i) => (
                        <div key={i}>#{s.message_id}: {[...(s.added||[]).map(t => '+' + t), ...(s.removed||[]).map(t => '−' + t)].join(' | ')}</div>
                      ))}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      )}
    </div>
  );
}

function KindPie({ byKind }) {
  const total = byKind.reduce((s, k) => s + (k.count || 0), 0);
  if (!total) return null;
  const size = 180, r = 60, c = 2 * Math.PI * r, cx = 90, cy = 90;
  const palette = ['#22c55e','#f59e0b','#ef4444','#06b6d4','#a855f7','#14b8a6','#64748b','#eab308'];
  let off = 0;
  const slices = byKind.map((k, i) => {
    const len = ((k.count || 0) / total) * c;
    const node = <circle key={k.kind} cx={cx} cy={cy} r={r} fill="none" stroke={palette[i % palette.length]} strokeWidth={22}
      strokeDasharray={`${len} ${c - len}`} strokeDashoffset={-off}><title>{`${k.kind}: ${k.count}`}</title></circle>;
    off += len;
    return node;
  });
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--line)" strokeWidth={22} />
      {slices}
      <text x={cx} y={cy - 2} textAnchor="middle" fill="var(--text)" fontSize={20} fontWeight={700} className="mono">{total}</text>
      <text x={cx} y={cy + 16} textAnchor="middle" fill="var(--text-3)" fontSize={11}>{(window.appT||(s=>s))("messages")}</text>
    </svg>
  );
}

/* ============================================================
   AuditPage
   ============================================================ */
function AuditPage() {
  const toast = useToast();

  /* filter state */
  const [qText, setQText]       = useState('');
  const [filterUser, setFU]     = useState('');
  const [filterAction, setFA]   = useState('');
  const [filterActor, setFAc]   = useState('');  // new: filter by actor label
  const [hideBot, setHideBot]   = useState(false); // hide bot-initiated rows
  const [fromDate, setFrom]     = useState('');
  const [toDate, setTo]         = useState('');

  /* data */
  const [allRows, setAllRows]   = useState([]);    // full fetched set (up to 500)
  const [loading, setLoad]      = useState(false);
  const [userCounts, setUC]     = useState([]);
  const [ucLoading, setUCLoad]  = useState(true);  // per-user-counts sidebar pending
  const [employees, setEmps]    = useState([]);
  const [page, setPage]         = useState(1);

  /* sort */
  const { sorted, sort, click } = useSortable([], { key: 'occurred_at', dir: 'desc' });

  const [sortedRows, setSortedRows] = useState([]);

  /* fetch both on mount + on filter apply */
  const load = useCallback(async (notify) => {
    setLoad(true);
    try {
      const p = new URLSearchParams({ limit: '500' });
      if (filterUser)   p.set('user_id', filterUser);
      if (filterAction) p.set('action_type', filterAction);
      if (filterActor)  p.set('actor', filterActor);
      if (hideBot)      p.set('hide_bot', '1');
      if (fromDate)     p.set('from_date', fromDate);
      if (toDate)       p.set('to_date', toDate);
      if (qText.trim()) p.set('q_text', qText.trim());

      const r = await fetch(`/api/audit?${p}`, { credentials: 'same-origin' });
      if (!r.ok) throw new Error(`${r.status}`);
      const data = await r.json();
      setAllRows(Array.isArray(data) ? data : []);
      setPage(1);
      if (notify) toast.push({ msg: 'Refreshed', kind: 'ok', icon: '✓' });
    } catch (e) {
      toast.push({ msg: `Audit error: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setLoad(false);
    }
  }, [filterUser, filterAction, filterActor, hideBot, fromDate, toDate, qText, toast]);

  useEffect(() => {
    setUCLoad(true);
    fetch('/api/audit/per-user-counts', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(setUC)
      .catch(() => {})
      .finally(() => setUCLoad(false));
    fetch('/api/employees', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(setEmps)
      .catch(() => {});
    load(false);
  }, []);

  /* The "Hide bot messages" toggle re-fetches immediately (unlike the text/date
     filters, which apply on the Apply button). Skip the initial mount render so
     we don't double-load. */
  const hideBotFirst = React.useRef(true);
  useEffect(() => {
    if (hideBotFirst.current) { hideBotFirst.current = false; return; }
    load(false);
  }, [hideBot]);

  /* client-side sort + paginate */
  const clientSorted = useMemo(() => {
    if (!allRows.length) return [];
    const dir = sort?.dir === 'asc' ? 1 : -1;
    const key = sort?.key || 'occurred_at';
    return [...allRows].sort((a, b) => {
      const av = a[key]; const bv = b[key];
      if (av == null && bv == null) return 0;
      if (av == null) return 1 * dir;
      if (bv == null) return -1 * dir;
      if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
      return String(av).localeCompare(String(bv)) * dir;
    });
  }, [allRows, sort]);

  const pageRows = useMemo(() => {
    const start = (page - 1) * PAGE_SIZE;
    return clientSorted.slice(start, start + PAGE_SIZE);
  }, [clientSorted, page]);

  const handleApplyFilters = () => {
    load(false);
    toast.push({ msg: 'Filter applied', kind: 'ok', icon: '✓' });
  };

  const handleExport = async (fmt = 'pdf') => {
    const label = fmt.toUpperCase();
    toast.push({ msg: `${label} downloading…`, kind: 'ok', icon: '✓' });
    try {
      const r = await fetch(`/api/audit/export?format=${fmt}`, { credentials: 'same-origin' });
      if (!r.ok) throw new Error(`${r.status}`);
      const blob = await r.blob();
      const filename = `audit-export.${fmt}`;
      // Prefer the cross-platform helper so iOS gets Share Sheet; fall back to
      // a plain <a download> for older bundles where saveDownload is missing.
      if (typeof window.saveDownload === 'function') {
        await window.saveDownload(blob, filename);
      } else {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url; a.download = filename; a.click();
        URL.revokeObjectURL(url);
      }
    } catch (e) {
      toast.push({ msg: `Export failed: ${e.message}`, kind: 'err', icon: '!' });
    }
  };

  const actionTypes = useMemo(() => {
    const seen = new Set(allRows.map(r => r.action_type).filter(Boolean));
    return Array.from(seen).sort();
  }, [allRows]);

  return (
    <div className="page">
      <PageHeader
        title={(window.appT||(s=>s))("Audit log")}
        meta={(window.appT||(s=>s))("Every action the bot took — 90-day retention")}
        right={
          <div style={{ display:'flex', gap: 8 }}>
            <Refresh onClick={() => load(true)} loading={loading} />
            <button className="btn btn-ghost" onClick={() => handleExport('csv')}
              title={(window.appT||(s=>s))("Full row-by-row dump (no row cap) for compliance / Excel")}>
              <Icon name="download" size={14}/> {(window.appT||(s=>s))("Export CSV")}
            </button>
            <button className="btn btn-ghost" onClick={() => handleExport('pdf')}
              title={(window.appT||(s=>s))("Human-readable summary (first 500 rows)")}>
              <Icon name="download" size={14}/> {(window.appT||(s=>s))("Export PDF")}
            </button>
          </div>
        }
      />

      <div style={{ display:'grid', gridTemplateColumns:'240px 1fr', gap: 16, alignItems:'start' }}>

        {/* Left sidebar — per-user counts */}
        <div className="card" style={{ position:'sticky', top: 16 }}>
          <SectionHeader title={(window.appT||(s=>s))("By user")}
          tip={(window.appT||(s=>s))("Per-user message counts in the 30-day window. Click a row to filter the message list.")} sub={(window.appT||(s=>s))("Click to filter")} />
          {ucLoading
            ? <Loading rows={4} label={(window.appT||(s=>s))("Loading…")} />
            : userCounts.length === 0 && <Empty icon="·" title={(window.appT||(s=>s))("No data")} />}
          {userCounts.map(u => (
            <div
              key={u.user_id}
              onClick={() => {
                const newId = String(u.user_id);
                setFU(v => v === newId ? '' : newId);
              }}
              style={{
                display:'flex', justifyContent:'space-between', alignItems:'center',
                padding: '6px 4px', cursor:'pointer', borderRadius:'var(--r-xs)',
                background: String(filterUser) === String(u.user_id) ? 'var(--bg-elev-2)' : 'transparent',
                fontSize: 12.5,
              }}
            >
              <span style={{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', flex:1 }}>{u.name}</span>
              <Badge kind="mute">{u.count}</Badge>
            </div>
          ))}
        </div>

        {/* Main audit table */}
        <div className="card" style={{ minWidth: 0 }}>
          {/* Filters toolbar */}
          <div style={{ display:'flex', flexWrap:'wrap', gap: 8, marginBottom: 12 }}>
            <SearchInput
              value={qText}
              onChange={setQText}
              placeholder={(window.appT||(s=>s))("Search body or trigger...")}
              style={{ flex: '1 1 180px' }}
            />
            <select
              className="inp"
              value={filterUser}
              onChange={e => setFU(e.target.value)}
              style={{ flex:'0 0 150px', height: 32 }}
            >
              <option value="">{(window.appT||(s=>s))("All users")}</option>
              {employees.map(e => (
                <option key={e.id} value={e.id}>{e.display_name}</option>
              ))}
            </select>
            <select
              className="inp"
              value={filterAction}
              onChange={e => setFA(e.target.value)}
              style={{ flex:'0 0 140px', height: 32 }}
            >
              <option value="">{(window.appT||(s=>s))("All actions")}</option>
              {actionTypes.map(a => (
                <option key={a} value={a}>
                  {(window.appT||(s=>s))((window.humanizeActionType && window.humanizeActionType(a)) || a)}
                </option>
              ))}
            </select>
            <input
              type="text" className="inp"
              value={filterActor} onChange={e => setFAc(e.target.value)}
              placeholder={(window.appT||(s=>s))("Actor (admin or bot)")}
              style={{ flex:'0 0 160px', height: 32 }}
              title={(window.appT||(s=>s))("Filter by actor — admin display name or bot label (case-insensitive substring)")}
            />
            <input
              type="date" className="inp"
              value={fromDate} onChange={e => setFrom(e.target.value)}
              style={{ flex:'0 0 130px', height: 32 }}
              title={(window.appT||(s=>s))("From date")}
            />
            <input
              type="date" className="inp"
              value={toDate} onChange={e => setTo(e.target.value)}
              style={{ flex:'0 0 130px', height: 32 }}
              title={(window.appT||(s=>s))("To date")}
            />
            <label style={{ display:'flex', alignItems:'center', gap: 6, fontSize: 12, whiteSpace:'nowrap' }}
                   title={(window.appT||(s=>s))("Hide bot-initiated rows (DMs, strikes, escalations, room posts, …) and show only human/admin actions")}>
              <input type="checkbox" checked={hideBot} onChange={e => setHideBot(e.target.checked)} />
              {(window.appT||(s=>s))("Hide bot messages")}
            </label>
            <button className="btn btn-pri" onClick={handleApplyFilters} disabled={loading}>
              {(window.appT||(s=>s))("Apply")}
            </button>
          </div>

          {/* Table */}
          {loading && allRows.length === 0 && (
            <div><Skel h={36} /><Skel h={36} style={{marginTop:4}} /><Skel h={36} style={{marginTop:4}} /></div>
          )}
          {!loading && allRows.length === 0 && (
            <Empty
              icon={
                <svg width="80" height="80" viewBox="0 0 80 80" fill="none" style={{opacity:0.35}}>
                  <path d="M40 10 L64 22 L64 42 C64 56 52 66 40 70 C28 66 16 56 16 42 L16 22 Z" stroke="currentColor" strokeWidth="3" fill="none" strokeLinejoin="round"/>
                  <path d="M30 40 L37 47 L52 32" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              }
              title={filterUser || filterAction || filterActor || fromDate || toDate || qText.trim() ? (window.appT||(s=>s))("No audit events match") : (window.appT||(s=>s))("No audit events recorded")}
              subtitle={filterUser || filterAction || filterActor || fromDate || toDate || qText.trim() ? (window.appT||(s=>s))("Try wider filters or a broader date range.") : (window.appT||(s=>s))("Activity will appear as users sign in and admins make changes.")}
            />
          )}
          {allRows.length > 0 && (
            <>
              <div style={{ overflowX:'auto' }}>
                <table className="t" style={{ width:'100%' }}>
                  <thead>
                    <tr>
                      <SortHead sort={sort} sortKey="occurred_at" click={click} label={(window.appT||(s=>s))("When")} width={130} />
                      <SortHead sort={sort} sortKey="actor" click={click} label={(window.appT||(s=>s))("Actor · Subject")} width={150} />
                      {/* Most audit rows are bot/enforcement actions with no HTTP
                          request to correlate, so Method/Path were ~always empty
                          (a wall of "—"). Collapsed into one IP/Request column;
                          method, full path and UA live in the row's expander. */}
                      <th title={(window.appT||(s=>s))("Originating request (only present for dashboard/API actions)")}>{(window.appT||(s=>s))("IP / Request")}</th>
                      <SortHead sort={sort} sortKey="action_type" click={click} label={(window.appT||(s=>s))("Action")} width={110} />
                      <SortHead sort={sort} sortKey="trigger" click={click} label={(window.appT||(s=>s))("Trigger")} width={90} />
                      <th>{(window.appT||(s=>s))("Result")}</th>
                    </tr>
                  </thead>
                  <tbody>
                    {pageRows.map((a, i) => (
                      <AuditRow key={a.id || i} a={a} onPickActor={setFAc} />
                    ))}
                  </tbody>
                </table>
              </div>
              <Pagination
                page={page}
                pageSize={PAGE_SIZE}
                total={allRows.length}
                onChange={setPage}
              />
              {allRows.length === 500 && (
                <div className="muted" style={{ fontSize: 11, marginTop: 6, textAlign:'right' }}>
                  {(window.appT||(s=>s))("Showing max 500 rows. Apply filters or date range to narrow results.")}
                </div>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function AuditRow({ a, onPickActor }) {
  const [open, setOpen] = useState(false);
  const [actBusy, setActBusy] = useState('');
  const [actDone, setActDone] = useState('');
  const toast = useToast();
  const trigger = a.trigger || '';
  const isIpFlag = a.action_type === 'ip_red_flag';
  // Permission: any manager/admin viewing this row can act on it (the API
  // re-checks _can_edit_user server-side, so manager-not-on-team yields 403).
  const canAct = !!(window.STATUS_DATA && window.STATUS_DATA.user &&
    ['admin', 'manager'].includes((window.STATUS_DATA.user.org_role || '').toLowerCase()));

  const doAck = async (e) => {
    e.stopPropagation();
    if (!a.id || actBusy) return;
    setActBusy('ack');
    try {
      const r = await fetch(`/api/ip-flags/${a.id}/ack`, {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
      });
      if (!r.ok) throw new Error(await r.text());
      setActDone('acked');
      toast.push({ kind: 'ok', icon: '✓', msg: 'Flag acknowledged' });
    } catch (err) {
      toast.push({ kind: 'err', icon: '!', msg: 'Ack failed: ' + (err.message || err) });
    } finally { setActBusy(''); }
  };

  const doSnooze = async (e) => {
    e.stopPropagation();
    if (!a.id || actBusy) return;
    setActBusy('snooze');
    try {
      const r = await fetch(`/api/ip-flags/${a.id}/snooze`, {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ days: 7 }),
      });
      if (!r.ok) throw new Error(await r.text());
      setActDone('snoozed');
      toast.push({ kind: 'ok', icon: '💤', msg: 'Snoozed for 7 days' });
    } catch (err) {
      toast.push({ kind: 'err', icon: '!', msg: 'Snooze failed: ' + (err.message || err) });
    } finally { setActBusy(''); }
  };

  const triggerColor = trigger === 'strike' ? 'err' : trigger === 'admin' ? 'info' : trigger === 'sign-in' || trigger === 'sign_in' ? 'ok' : 'mute';
  // Result column: HTTP status code if we matched an http_log row, else "—".
  const status = a.status;
  let resultBadge;
  if (status == null) {
    resultBadge = <span className="muted">—</span>;
  } else {
    const kind = status >= 500 ? 'err' : status >= 400 ? 'warn' : status >= 300 ? 'info' : 'ok';
    resultBadge = <Badge kind={kind}>{status}</Badge>;
  }

  return (
    <>
      <tr onClick={() => setOpen(x => !x)} style={{ cursor:'pointer' }}>
        <td className="mono" style={{ fontSize: 11, whiteSpace:'nowrap' }} title={absTime(a.occurred_at)}>
          {relTime(a.occurred_at)}
        </td>
        <td style={{ fontSize: 12 }}>
          {a.actor ? (
            <button
              type="button"
              onClick={(ev) => { ev.stopPropagation(); onPickActor && onPickActor(a.actor); }}
              title={(window.appLang&&window.appLang()==='ar') ? `تصفية سجل التدقيق حسب الفاعل: ${a.actor}` : `Filter audit log to actor: ${a.actor}`}
              style={{ background: 'none', border: 0, padding: 0, cursor: 'pointer', color: 'var(--text)', fontSize: 12, textAlign: 'left' }}
            >
              {a.actor}
            </button>
          ) : (a.user_display_name || '—')}
          {a.actor && a.user_display_name && a.actor !== a.user_display_name && (
            <div style={{ fontSize: 10.5, color: 'var(--text-3)' }}>→ {a.user_display_name}</div>
          )}
        </td>
        <td className="mono" style={{ fontSize: 11 }} title={[a.ip, a.method, a.path].filter(Boolean).join(' ') || ''}>
          {a.ip || a.method || a.path
            ? <span style={{ display:'inline-flex', gap:6, alignItems:'center' }}>
                {a.ip ? <span>{a.ip}</span> : null}
                {a.method ? <Badge kind="mute">{a.method}</Badge> : null}
              </span>
            : '—'}
        </td>
        {/* Issue #32: show human label (sent by API as action_label).
            Fall back to local humanizer if an older API didn't include it. */}
        <td><Badge kind="mute" title={a.action_type || ''}>{(window.appT||(s=>s))(a.action_label || humanizeActionType(a.action_type) || '—')}</Badge></td>
        <td><Badge kind={triggerColor}>{trigger || '—'}</Badge></td>
        <td>{resultBadge}</td>
      </tr>
      {open && (
        <tr>
          <td colSpan={6} style={{ background:'var(--bg-elev-1)', padding:'8px 12px' }}>
            <div style={{ fontSize: 12, color:'var(--text-2)', marginBottom: 4 }}>
              <strong>{(window.appT||(s=>s))("When:")}</strong> {absTime(a.occurred_at)}
              {a.target && <> &middot; <strong>{(window.appT||(s=>s))("Target:")}</strong> {a.target}</>}
              {a.method && <> &middot; <strong>{(window.appT||(s=>s))("Method:")}</strong> <span className="mono">{a.method}</span></>}
              {a.path && <> &middot; <strong>{(window.appT||(s=>s))("Path:")}</strong> <span className="mono">{a.path}</span></>}
              {a.user_agent && <> &middot; <strong>{(window.appT||(s=>s))("UA:")}</strong> <span className="mono" style={{ fontSize: 11 }}>{a.user_agent}</span></>}
            </div>
            {a.body && (
              <div style={{ fontFamily:'monospace', fontSize: 12, whiteSpace:'pre-wrap', color:'var(--text)', background:'var(--bg-elev-2)', borderRadius:'var(--r-xs)', padding: '6px 10px' }}>
                {a.body}
              </div>
            )}
            {isIpFlag && canAct && (
              <div style={{ display:'flex', gap:8, alignItems:'center', marginTop:8 }}>
                <button
                  onClick={doAck}
                  disabled={!!actBusy || actDone === 'acked'}
                  className="btn btn-sec"
                  style={{ fontSize:12, padding:'4px 10px' }}
                  title={(window.appT||(s=>s))("Mark this IP red-flag as reviewed")}
                >
                  {actDone === 'acked' ? '✓ ' + (window.appT||(s=>s))('Acknowledged') : (actBusy === 'ack' ? '…' : '✓ ' + (window.appT||(s=>s))('Acknowledge'))}
                </button>
                <button
                  onClick={doSnooze}
                  disabled={!!actBusy || actDone === 'snoozed'}
                  className="btn btn-sec"
                  style={{ fontSize:12, padding:'4px 10px' }}
                  title={(window.appT||(s=>s))("Suppress further red-flag DMs for this user for 7 days")}
                >
                  {actDone === 'snoozed' ? '💤 ' + (window.appT||(s=>s))('Snoozed 7d') : (actBusy === 'snooze' ? '…' : '💤 ' + (window.appT||(s=>s))('Snooze 7d'))}
                </button>
                <span style={{ fontSize:11, color:'var(--text-2)' }}>
                  {(window.appT||(s=>s))("Snooze suppresses repeat red-flag DMs for the affected user.")}
                </span>
              </div>
            )}
          </td>
        </tr>
      )}
    </>
  );
}

/* ── Configure Provider Modal ── */
function ProviderConfigModal({ provider, onClose, onSaved }) {
  const [key, setKey] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const toast = useToast();

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const save = async () => {
    if (!key.trim()) { setErr((window.appT||(s=>s))('API key is required')); return; }
    setBusy(true); setErr('');
    try {
      const r = await fetch('/api/providers/set-key', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ provider: provider.id, key: key.trim() }),
      });
      if (!r.ok) {
        const body = await r.json().catch(() => null);
        throw new Error(parseDetail(body, r.status));
      }
      toast.push({ msg: `${provider.name} API key saved`, kind: 'ok' });
      onSaved && onSaved();
      onClose();
    } catch(e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="drawer-overlay" onClick={onClose} style={{ zIndex: 300 }}>
      <div
        onClick={e => e.stopPropagation()}
        style={{
          position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
          background: 'var(--bg-elev-1)', border: '1px solid var(--line-hi)',
          borderRadius: 'var(--r-lg)', padding: 24, width: 380, maxWidth: '90vw',
          boxShadow: '0 24px 64px -16px #00000080', zIndex: 301,
        }}
      >
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 14 }}>{(window.appLang&&window.appLang()==='ar') ? `إعداد ${provider.name}` : `Configure ${provider.name}`}</div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>
        {err && (
          <div className="badge badge-err" style={{ marginBottom: 10, padding:'7px 12px', fontSize: 12 }}>
            <Icon name="alert" size={13}/> {err}
          </div>
        )}
        <label className="login-label">{(window.appT||(s=>s))("API Key")}</label>
        <input
          className="inp"
          type="password"
          value={key}
          onChange={e => setKey(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && save()}
          placeholder="sk-…"
          style={{ marginBottom: 16 }}
          autoFocus
        />
        <div style={{ display:'flex', gap: 8, justifyContent:'flex-end' }}>
          <button className="btn btn-ghost" onClick={onClose}>{(window.appT||(s=>s))("Cancel")}</button>
          <button className="btn btn-pri" onClick={save} disabled={busy}>
            {busy ? (window.appT||(s=>s))('Saving…') : <><Icon name="check" size={13}/> {(window.appT||(s=>s))('Save key')}</>}
          </button>
        </div>
      </div>
    </div>
  );
}

/* ─── Setup-guide link — every messaging integration card gets one.
       Opens the static how-to page in a new tab. ────────────────── */
function IntegrationGuideLink({ slug }) {
  return (
    <div style={{ display:'flex', justifyContent:'flex-end', marginBottom: 2 }}>
      <a href={`/static/v2/guides/${slug}.html`} target="_blank" rel="noreferrer"
         style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--brand)', textDecoration: 'none' }}>
        📖 {(window.appT||(s=>s))('Setup guide')} ↗
      </a>
    </div>
  );
}

/* ─── Slack OAuth app credentials card ──────────────────────── */
function SlackOAuthCredentialsCard() {
  const toast = useToast();
  const [cfg, setCfg] = React.useState(null);
  const [form, setForm] = React.useState({ client_id: '', client_secret: '', signing_secret: '' });
  const [busy, setBusy] = React.useState(false);

  const load = () => {
    fetch('/api/admin/slack/credentials', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setCfg(d); })
      .catch(() => {});
  };
  React.useEffect(() => { load(); }, []);

  const save = async () => {
    const body = {};
    if (form.client_id.trim()) body.client_id = form.client_id.trim();
    if (form.client_secret.trim()) body.client_secret = form.client_secret.trim();
    if (form.signing_secret.trim()) body.signing_secret = form.signing_secret.trim();
    if (Object.keys(body).length === 0) {
      toast.push({ msg: 'Nothing to save — paste at least one value', kind: 'warn' });
      return;
    }
    setBusy(true);
    try {
      const r = await fetch('/api/admin/slack/credentials', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!r.ok) {
        // Defend against HTML error pages (nginx 405/502, etc.)
        const text = await r.text().catch(() => '');
        const isJson = text.trim().startsWith('{');
        let msg = `Server error ${r.status}`;
        if (isJson) {
          try { msg = JSON.parse(text).detail || msg; } catch (_) {}
        }
        throw new Error(msg);
      }
      toast.push({ msg: 'Slack OAuth app credentials saved', kind: 'ok' });
      setForm({ client_id: '', client_secret: '', signing_secret: '' });
      load();
    } catch(e) {
      toast.push({ msg: 'Save failed: ' + e.message, kind: 'err' });
    } finally {
      setBusy(false);
    }
  };

  const allSet = cfg && cfg.client_id_set && cfg.client_secret_set && cfg.signing_secret_set;

  return (
    <div style={{ marginTop: 12, padding: '12px 14px', background: 'var(--bg-2)', borderRadius: 8, border: '1px solid var(--line)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
        <div style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--text-1)' }}>
          {(window.appT||(s=>s))("Slack OAuth app credentials")}
        </div>
        <Badge kind={allSet ? 'ok' : 'mute'}>{allSet ? (window.appT||(s=>s))('configured') : (window.appT||(s=>s))('not set')}</Badge>
      </div>
      <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginBottom: 10 }}>
        {(window.appT||(s=>s))('Required for "Connect Slack workspace" OAuth flow. Get these from your Slack app at')}{' '}
        <code>api.slack.com/apps</code> → Basic Information.
      </div>
      <div style={{ display: 'grid', gap: 8 }}>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 3, fontSize: 12 }}>
          <span style={{ color: 'var(--text-2)', display: 'flex', justifyContent: 'space-between' }}>
            <span>{(window.appT||(s=>s))("Client ID")}</span>
            {cfg && cfg.client_id_set && <span style={{ color: 'var(--ok)', fontSize: 10 }}>&#10003; {(window.appT||(s=>s))("saved")}</span>}
          </span>
          <input className="inp" value={form.client_id} onChange={e => setForm(f => ({...f, client_id: e.target.value}))}
            placeholder={cfg && cfg.client_id_set ? (window.appT||(s=>s))('Already saved — paste to overwrite') : '1234567890.123456789012'}
            style={{ fontFamily: 'var(--font-mono)' }} />
        </label>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 3, fontSize: 12 }}>
          <span style={{ color: 'var(--text-2)', display: 'flex', justifyContent: 'space-between' }}>
            <span>{(window.appT||(s=>s))("Client Secret")}</span>
            {cfg && cfg.client_secret_set && <span style={{ color: 'var(--ok)', fontSize: 10 }}>&#10003; {(window.appT||(s=>s))("saved")}</span>}
          </span>
          <input className="inp" type="password" value={form.client_secret} onChange={e => setForm(f => ({...f, client_secret: e.target.value}))}
            placeholder={cfg && cfg.client_secret_set ? (window.appT||(s=>s))('Already saved — paste to overwrite') : 'abcdef1234567890abcdef1234567890'}
            style={{ fontFamily: 'var(--font-mono)' }} />
        </label>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 3, fontSize: 12 }}>
          <span style={{ color: 'var(--text-2)', display: 'flex', justifyContent: 'space-between' }}>
            <span>{(window.appT||(s=>s))("Signing Secret")}</span>
            {cfg && cfg.signing_secret_set && <span style={{ color: 'var(--ok)', fontSize: 10 }}>&#10003; {(window.appT||(s=>s))("saved")}</span>}
          </span>
          <input className="inp" type="password" value={form.signing_secret} onChange={e => setForm(f => ({...f, signing_secret: e.target.value}))}
            placeholder={cfg && cfg.signing_secret_set ? (window.appT||(s=>s))('Already saved — paste to overwrite') : 'abcdef1234567890abcdef1234567890'}
            style={{ fontFamily: 'var(--font-mono)' }} />
        </label>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
        <button className="btn btn-pri" onClick={save} disabled={busy} style={{ fontSize: 12 }}>
          {busy ? '...' : (window.appT||(s=>s))('Save')}
        </button>
      </div>
    </div>
  );
}

/* ─── Slack provider card ──────────────────────────────────── */
function SlackProviderSection() {
  const toast = useToast();
  const [workspaces, setWorkspaces] = React.useState(null); // null = loading
  const [busy, setBusy] = React.useState(false);

  const load = async () => {
    try {
      const r = await fetch('/api/admin/slack/workspaces', { credentials: 'same-origin' });
      if (r.ok) setWorkspaces(await r.json());
      else setWorkspaces([]);
    } catch { setWorkspaces([]); }
  };

  React.useEffect(() => { load(); }, []);

  // Listen for postMessage from the OAuth popup
  React.useEffect(() => {
    const handler = (e) => {
      if (e.data && e.data.type === 'slack_installed') {
        load();
        toast.push({ msg: `Slack workspace "${e.data.team_name}" connected`, kind: 'ok' });
      }
    };
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, []);

  const openInstall = async () => {
    // Pre-check: verify credentials are configured before opening popup
    try {
      const r = await fetch('/api/admin/slack/credentials', { credentials: 'same-origin' });
      if (r.ok) {
        const d = await r.json();
        if (!d.client_id_set || !d.client_secret_set) {
          toast.push({ msg: 'Set Slack OAuth app Client ID and Client Secret first (see card below)', kind: 'warn' });
          return;
        }
      }
    } catch (_) {}
    window.open('/slack/install', 'slack_install', 'width=600,height=700');
  };

  const uninstall = async (ws) => {
    if (!confirm((window.appLang&&window.appLang()==='ar') ? `فصل مساحة عمل Slack "${ws.team_name || ws.team_id}"؟` : `Disconnect Slack workspace "${ws.team_name || ws.team_id}"?`)) return;
    setBusy(true);
    try {
      const r = await fetch('/api/admin/slack/uninstall', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ team_id: ws.team_id }),
      });
      if (!r.ok) throw new Error(`${r.status}`);
      toast.push({ msg: `Workspace "${ws.team_name || ws.team_id}" disconnected`, kind: 'ok' });
      load();
    } catch(e) {
      toast.push({ msg: `Uninstall failed: ${e.message}`, kind: 'err' });
    } finally { setBusy(false); }
  };

  const installed = workspaces && workspaces.length > 0;

  return (
    <div style={{ marginTop: 32 }}>
      <div style={{
        display:'flex', alignItems:'baseline', justifyContent:'space-between',
        marginBottom: 14,
      }}>
        <div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text)' }}>
          {(window.appT||(s=>s))("Messaging integrations")}
        </div>
        <div className="muted" style={{ fontSize: 11.5 }}>
          Slack · Microsoft Teams · Jira · WhatsApp ·{' '}
          <a href="/static/v2/guides/index.html" target="_blank" rel="noreferrer"
             style={{ color: 'var(--brand)', textDecoration: 'none', fontWeight: 600 }}>
            📖 {(window.appT||(s=>s))('Setup guides')} ↗
          </a>
        </div>
      </div>

      {/* Full-width section per provider — was a single 460px card with
          Teams nested under Slack, which collapsed everything into the
          left rail. Now each integration gets its own panel with the
          provider's branding/header so the surface area is balanced. */}
      <div style={{
        display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(320px, 1fr))',
        gap: 16,
      }}>
        {/* Slack section */}
        <div className="prov-card" style={{ padding: 16 }}>
          <IntegrationGuideLink slug="slack" />
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
            <div>
              <div style={{ fontWeight: 600, fontSize: 14 }}>Slack</div>
              <div className="muted mono" style={{ fontSize: 11 }}>
                {installed ? ((window.appLang&&window.appLang()==='ar') ? `${workspaces.length} مساحة عمل متصلة` : `${workspaces.length} workspace${workspaces.length>1?'s':''} connected`) : (window.appT||(s=>s))('Not connected')}
              </div>
            </div>
            <div style={{ display:'flex', gap: 4, flexDirection:'column', alignItems:'flex-end' }}>
              <Badge kind="info">{(window.appT||(s=>s))("messaging")}</Badge>
              {installed && <Badge kind="ok" dot>{(window.appT||(s=>s))("active")}</Badge>}
            </div>
          </div>
          <div style={{ fontSize: 12.5, color:'var(--text-2)', marginBottom: 10, lineHeight: 1.45 }}>
            {(window.appT||(s=>s))("Employees can post standups directly from Slack. Sign-ins, sign-offs, and status declarations route through the same pipeline as K9 Chat.")}
          </div>

          {workspaces === null && <Loading label={(window.appT||(s=>s))("Loading…")} rows={2} />}

          {workspaces && workspaces.map(ws => (
            <div key={ws.team_id} style={{
              background: 'var(--bg-2)', borderRadius: 6, padding: '8px 10px',
              marginBottom: 6, display:'flex', justifyContent:'space-between', alignItems:'center',
            }}>
              <div>
                <div style={{ fontSize: 13, fontWeight: 500 }}>{ws.team_name || ws.team_id}</div>
                <div className="muted mono" style={{ fontSize: 11 }}>
                  {ws.team_id} &middot; bot: {ws.bot_user_id || 'unknown'}
                </div>
                {ws.installed_at && (
                  <div className="muted" style={{ fontSize: 10.5 }}>
                    {(window.appLang&&window.appLang()==='ar') ? `تم التثبيت ${new Date(ws.installed_at).toLocaleDateString()}` : `Installed ${new Date(ws.installed_at).toLocaleDateString()}`}
                  </div>
                )}
              </div>
              <button
                className="btn btn-ghost"
                style={{ color: 'var(--danger)', borderColor: 'var(--danger)', fontSize: 11 }}
                disabled={busy}
                onClick={() => uninstall(ws)}
              >
                {(window.appT||(s=>s))("Disconnect")}
              </button>
            </div>
          ))}

          <SlackOAuthCredentialsCard />

          <div style={{ display:'flex', gap: 6, marginTop: 12 }}>
            <button className="btn btn-pri" onClick={openInstall}>
              {(window.appT||(s=>s))("Connect Slack workspace")}
            </button>
            {installed && (
              <button className="btn btn-ghost" onClick={load}>
                {(window.appT||(s=>s))("Refresh")}
              </button>
            )}
          </div>

          {/* Direct-config (single-workspace MVP) now lives inside the
              Slack column instead of as a separate full-width row below.
              Keeps the layout balanced and stops the empty right rail. */}
          <div style={{ marginTop: 18, borderTop: '1px solid var(--line)', paddingTop: 14 }}>
            <SlackDirectConfigSection />
          </div>
        </div>

        {/* Microsoft Teams section — no longer nested inside Slack */}
        <div className="prov-card" style={{ padding: 16 }}>
          <IntegrationGuideLink slug="teams" />
          <TeamsIntegrationCard />
        </div>

        {/* Jira (commercial v2 — per-tenant OAuth 2.0). */}
        <div className="prov-card" style={{ padding: 16 }}>
          <IntegrationGuideLink slug="jira" />
          <JiraV2IntegrationCard />
        </div>

        {/* GitLab / GitHub dev-activity (autopilot). No setup-guide page exists
            for this one yet, so no IntegrationGuideLink until guides/gitdev.html
            is authored. */}
        <div className="prov-card" style={{ padding: 16 }}>
          <GitDevIntegrationCard />
        </div>

        {/* WhatsApp section — moved out of the Server tab so all messaging
            providers live together. Component is exposed via window so we
            don't have to inline it. */}
        <div className="prov-card" style={{ padding: 16 }}>
          <IntegrationGuideLink slug="whatsapp" />
          {typeof window.WhatsAppIntegrationCard === 'function'
            ? React.createElement(window.WhatsAppIntegrationCard)
            : (
              <>
                <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
                  <div>
                    <div style={{ fontWeight: 600, fontSize: 14 }}>WhatsApp</div>
                    <div className="muted mono" style={{ fontSize: 11 }}>{(window.appT||(s=>s))("Twilio escalation channel")}</div>
                  </div>
                  <Badge kind="info">{(window.appT||(s=>s))("messaging")}</Badge>
                </div>
                <div style={{ fontSize: 12.5, color:'var(--text-2)', lineHeight: 1.45 }}>
                  {(window.appT||(s=>s))("WhatsApp settings now live in this tab. If you don't see the form yet, the Server tab's WhatsApp panel needs to be moved here in the next deploy — bump the page-misc cache-bust and re-check.")}
                </div>
              </>
            )
          }
        </div>
      </div>
    </div>
  );
}

/* ─── Teams integration card + install modal (CP2) ──────── */

function SlackDirectConfigSection() {
  const [cfg, setCfg]       = React.useState({ bot_token: '', signing_secret: '', channel_id: '', bot_token_set: false, signing_secret_set: false, channel_id_set: false });
  const [token, setToken]   = React.useState('');
  const [secret, setSecret] = React.useState('');
  const [chan, setChan]     = React.useState('');
  const [busy, setBusy]     = React.useState(false);
  const [tested, setTested] = React.useState(null);
  const toast = useToast();

  const reload = () => {
    fetch('/api/admin/slack/settings', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) { setCfg(d); setChan(d.channel_id || ''); } })
      .catch(() => {});
  };
  React.useEffect(() => { reload(); }, []);

  const save = async () => {
    setBusy(true);
    try {
      const body = {};
      if (token.trim()) body.bot_token = token.trim();
      if (secret.trim()) body.signing_secret = secret.trim();
      if (chan.trim()) body.channel_id = chan.trim();
      if (Object.keys(body).length === 0) { toast.push({ msg: 'Nothing to save', kind: 'warn' }); return; }
      const r = await fetch('/api/admin/slack/settings', {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!r.ok) {
        const text = await r.text().catch(() => '');
        const isJson = text.trim().startsWith('{');
        let msg = `Server error ${r.status}`;
        if (isJson) { try { msg = JSON.parse(text).detail || msg; } catch (_) {} }
        throw new Error(msg);
      }
      toast.push({ msg: 'Slack config saved', kind: 'ok' });
      setToken(''); setSecret('');
      reload();
    } catch(e) {
      toast.push({ msg: 'Save failed: ' + e.message, kind: 'err' });
    } finally {
      setBusy(false);
    }
  };

  const test = async () => {
    setBusy(true); setTested(null);
    try {
      // Hit Slack auth.test through the bot — we proxy by re-saving (no-op)
      // and reading state. For simplicity, we just re-fetch settings.
      // For now use auth.test through our backend: there's no dedicated endpoint, so
      // just verify by hitting GET /api/admin/slack/settings and ensuring bot_token_set is true.
      const r = await fetch('/api/admin/slack/settings', { credentials: 'same-origin' });
      const d = await r.json();
      const ok = d.bot_token_set && d.signing_secret_set && d.channel_id_set;
      setTested({ ok, ...d });
      toast.push({ msg: ok ? '✅ All 3 fields configured' : '⚠️ Missing one or more fields', kind: ok ? 'ok' : 'warn' });
    } catch(e) {
      toast.push({ msg: 'Check failed: ' + e.message, kind: 'err' });
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="prov-card" style={{ maxWidth: 540, marginTop: 16 }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
        <div>
          <div style={{ fontWeight: 500, fontSize: 14 }}>📨 {(window.appT||(s=>s))("Slack — direct config")}</div>
          <div className="muted" style={{ fontSize: 11.5 }}>
            {(window.appT||(s=>s))("Single-workspace MVP. Use this if you don't want OAuth. Paste the 3 values from your Slack app.")}
          </div>
        </div>
        <Badge kind={cfg.bot_token_set && cfg.channel_id_set && cfg.signing_secret_set ? 'ok' : 'mute'}>
          {cfg.bot_token_set && cfg.channel_id_set && cfg.signing_secret_set ? (window.appT||(s=>s))('configured') : (window.appT||(s=>s))('partial')}
        </Badge>
      </div>

      <div style={{ display:'grid', gap: 10, marginTop: 12 }}>
        <label style={{ display:'flex', flexDirection:'column', gap: 4, fontSize: 12 }}>
          <span style={{ color:'var(--text-2)', display:'flex', justifyContent:'space-between' }}>
            <span>{(window.appT||(s=>s))("Bot User OAuth Token")}</span>
            {cfg.bot_token_set && <span style={{ color:'var(--ok)', fontSize: 10 }}>✓ {(window.appT||(s=>s))("saved")} ({cfg.bot_token})</span>}
          </span>
          <input className="inp" type="password" value={token} onChange={e => setToken(e.target.value)}
            placeholder={cfg.bot_token_set ? (window.appT||(s=>s))('Already saved — paste here only to overwrite') : 'xoxb-…'}
            style={{ fontFamily:'var(--font-mono)' }} />
        </label>

        <label style={{ display:'flex', flexDirection:'column', gap: 4, fontSize: 12 }}>
          <span style={{ color:'var(--text-2)', display:'flex', justifyContent:'space-between' }}>
            <span>{(window.appT||(s=>s))("Signing Secret")}</span>
            {cfg.signing_secret_set && <span style={{ color:'var(--ok)', fontSize: 10 }}>✓ {(window.appT||(s=>s))("saved")} ({cfg.signing_secret})</span>}
          </span>
          <input className="inp" type="password" value={secret} onChange={e => setSecret(e.target.value)}
            placeholder={cfg.signing_secret_set ? (window.appT||(s=>s))('Already saved — paste here only to overwrite') : '…'}
            style={{ fontFamily:'var(--font-mono)' }} />
        </label>

        <label style={{ display:'flex', flexDirection:'column', gap: 4, fontSize: 12 }}>
          <span style={{ color:'var(--text-2)' }}>{(window.appT||(s=>s))("Channel ID (the standup channel)")}</span>
          <input className="inp" value={chan} onChange={e => setChan(e.target.value)}
            placeholder={(window.appT||(s=>s))("C…  (right-click channel in Slack → View channel details → bottom)")}
            style={{ fontFamily:'var(--font-mono)' }} />
        </label>
      </div>

      <div style={{ display:'flex', gap: 8, marginTop: 14, justifyContent:'flex-end' }}>
        <button className="btn btn-ghost" onClick={test} disabled={busy}>{(window.appT||(s=>s))("Check config")}</button>
        <button className="btn btn-pri" onClick={save} disabled={busy}>{busy ? '…' : (window.appT||(s=>s))('Save')}</button>
      </div>

      <div style={{ marginTop: 12, padding: 10, background:'var(--bg-2)', borderRadius: 6, fontSize: 11.5, color:'var(--text-2)' }}>
        <strong>{(window.appT||(s=>s))("Setup checklist:")}</strong><br/>
        1. Create Slack app at <code>api.slack.com/apps</code> using the manifest in <code>Help / API → Slack integration</code><br/>
        2. Install to workspace → copy Bot User OAuth Token (xoxb-…)<br/>
        3. Copy Signing Secret from Basic Information<br/>
        4. Invite the bot to your standup channel: <code>/invite @Status Bot</code><br/>
        5. Right-click channel → View details → copy Channel ID (C…)<br/>
        6. Paste all three above + Save<br/>
        7. Slack Event Subscriptions URL: <code>https://status.k9.ms/api/slack/events</code> (will turn green ✓)<br/>
        8. Subscribe bot to event: <code>message.channels</code>
      </div>
    </div>
  );
}


function TeamsInstallModal({ onClose, onSaved }) {
  const [form, setForm] = React.useState({ aad_tenant_id: '', app_id: '', app_password: '', service_url: '' });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const toast = useToast();

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const set = (k) => (e) => setForm(f => ({ ...f, [k]: e.target.value }));

  const save = async () => {
    if (!form.aad_tenant_id.trim() || !form.app_id.trim() || !form.app_password.trim()) {
      setErr((window.appT||(s=>s))('AAD Tenant ID, App ID, and App Password are required'));
      return;
    }
    setBusy(true); setErr('');
    try {
      const r = await fetch('/api/admin/teams/install', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify(form),
      });
      if (!r.ok) {
        const body = await r.json().catch(() => null);
        throw new Error(parseDetail(body, r.status));
      }
      toast.push({ msg: 'Teams installation saved', kind: 'ok' });
      onSaved && onSaved();
      onClose();
    } catch(e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

  const Field = ({ label, k, type = 'text', placeholder = '' }) => (
    <div style={{ marginBottom: 12 }}>
      <label className="login-label" style={{ display:'block', marginBottom: 4 }}>{label}</label>
      <input className="inp" type={type} value={form[k]} onChange={set(k)} placeholder={placeholder} />
    </div>
  );

  return (
    <div className="drawer-overlay" onClick={onClose} style={{ zIndex: 300 }}>
      <div
        onClick={e => e.stopPropagation()}
        style={{
          position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
          background: 'var(--bg-elev-1)', border: '1px solid var(--line-hi)',
          borderRadius: 'var(--r-lg)', padding: 24, width: 420, maxWidth: '92vw',
          boxShadow: '0 24px 64px -16px #00000080', zIndex: 301,
        }}
      >
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 14 }}>{(window.appT||(s=>s))("Configure Teams integration")}</div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>
        <div className="muted" style={{ fontSize: 12, marginBottom: 14, lineHeight: 1.5 }}>
          {(window.appT||(s=>s))("Create an Azure Bot at portal.azure.com, copy the credentials below, then side-load the manifest zip into your Teams tenant.")}
        </div>
        {err && (
          <div className="badge badge-err" style={{ marginBottom: 10, padding:'7px 12px', fontSize: 12 }}>
            <Icon name="alert" size={13}/> {err}
          </div>
        )}
        <Field label={(window.appT||(s=>s))("Azure AD Tenant ID")} k="aad_tenant_id" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
        <Field label={(window.appT||(s=>s))("App ID (Application Client ID)")} k="app_id" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
        <Field label={(window.appT||(s=>s))("App Password (Client Secret)")} k="app_password" type="password" placeholder={(window.appT||(s=>s))("secret value from Azure portal")} />
        <Field label={(window.appT||(s=>s))("Service URL (optional)")} k="service_url" placeholder="https://smba.trafficmanager.net/amer/" />
        <div style={{ display:'flex', gap: 8, justifyContent:'flex-end', marginTop: 4 }}>
          <button className="btn btn-ghost" onClick={onClose}>{(window.appT||(s=>s))("Cancel")}</button>
          <button className="btn btn-pri" onClick={save} disabled={busy}>
            {busy ? (window.appT||(s=>s))('Saving...') : <><Icon name="check" size={13}/> {(window.appT||(s=>s))('Save')}</>}
          </button>
        </div>
      </div>
    </div>
  );
}

/* ─── Jira v2 (commercial — Atlassian OAuth 2.0 per-tenant) ──────── */
function JiraV2IntegrationCard() {
  const [installs, setInstalls] = React.useState(null);
  const [creds, setCreds] = React.useState({ client_id: '', client_secret_set: false });
  const [credForm, setCredForm] = React.useState({ client_id: '', client_secret: '' });
  const [projects, setProjects] = React.useState([]);
  const [editingCreds, setEditingCreds] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const toast = useToast();

  const probe = async () => {
    try {
      const [s, c] = await Promise.all([
        fetch('/api/admin/jira/status', { credentials: 'same-origin' }),
        fetch('/api/admin/jira/credentials', { credentials: 'same-origin' }),
      ]);
      if (s.ok) setInstalls((await s.json()).installs || []);
      if (c.ok) {
        const cj = await c.json();
        setCreds(cj);
        setCredForm({ client_id: cj.client_id, client_secret: '' });
      }
    } catch (e) { /* ignore */ }
  };

  React.useEffect(() => {
    probe();
    const onMsg = (e) => { if (e.data && e.data.type === 'jira_installed') probe(); };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const loadProjects = async () => {
    try {
      const r = await fetch('/api/admin/jira/projects', { credentials: 'same-origin' });
      if (r.ok) setProjects((await r.json()).projects || []);
    } catch { setProjects([]); }
  };

  const saveCreds = async () => {
    if (!credForm.client_id || !credForm.client_secret) {
      toast.push({ msg: 'Client ID + secret required', kind: 'warn' }); return;
    }
    setBusy(true);
    try {
      const r = await fetch('/api/admin/jira/credentials', {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credForm),
      });
      if (!r.ok) throw new Error('save failed');
      toast.push({ msg: 'Atlassian OAuth credentials saved', kind: 'ok' });
      setEditingCreds(false);
      await probe();
    } catch (e) {
      toast.push({ msg: e.message, kind: 'err' });
    } finally { setBusy(false); }
  };

  const startInstall = () => {
    window.open('/jira/install', 'jira_install', 'width=700,height=800');
  };

  const setProject = async (p) => {
    setBusy(true);
    try {
      const r = await fetch('/api/admin/jira/project', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ project_key: p.key, project_name: p.name }),
      });
      if (!r.ok) throw new Error('save failed');
      toast.push({ msg: `Project ${p.key} set`, kind: 'ok' });
      await probe();
    } catch (e) { toast.push({ msg: e.message, kind: 'err' }); }
    finally { setBusy(false); }
  };

  const uninstall = async (id) => {
    if (!confirm((window.appT||(s=>s))('Disconnect this Jira installation?'))) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/jira/install/${id}`, {
        method: 'DELETE', credentials: 'same-origin',
      });
      toast.push({ msg: 'Disconnected', kind: 'ok' });
      await probe();
    } finally { setBusy(false); }
  };

  return (
    <div>
      <SectionHeader
        title={(window.appT||(s=>s))("Jira (Atlassian OAuth)")}
        tip={(window.appT||(s=>s))("Per-tenant Atlassian Cloud integration via OAuth 2.0 (3LO). Each tenant connects their own Atlassian site and picks a target project + status mapping.")}
        sub={installs === null ? (window.appT||(s=>s))('Loading…') :
             installs.length > 0 ? ((window.appLang&&window.appLang()==='ar') ? `${installs.length} موقع متصل` : `${installs.length} site(s) connected`) : (window.appT||(s=>s))('Not connected')}
      />
      {/* Step 1: ISV OAuth app credentials */}
      <div style={{ marginBottom: 12, padding: 10, background: 'var(--bg2)',
                    borderRadius: 6, fontSize: 13 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <strong>{(window.appT||(s=>s))("Atlassian app credentials")}</strong>
          <span className={`badge ${creds.client_id ? 'ok' : 'mute'}`}>
            {creds.client_id ? (window.appT||(s=>s))('configured') : (window.appT||(s=>s))('missing')}
          </span>
          <button className="btn btn-ghost" onClick={() => setEditingCreds(!editingCreds)}
                  style={{ marginLeft: 'auto', padding: '2px 8px', fontSize: 12 }}>
            {editingCreds ? (window.appT||(s=>s))('Cancel') : (window.appT||(s=>s))('Edit')}
          </button>
        </div>
        {editingCreds && (
          <div style={{ display: 'grid', gap: 6, marginTop: 8 }}>
            <input className="inp" placeholder={(window.appT||(s=>s))("Client ID")}
              value={credForm.client_id}
              onChange={e => setCredForm({ ...credForm, client_id: e.target.value })} />
            <input className="inp" type="password" placeholder={(window.appT||(s=>s))("Client Secret")}
              value={credForm.client_secret}
              onChange={e => setCredForm({ ...credForm, client_secret: e.target.value })} />
            <button className="btn btn-pri" onClick={saveCreds} disabled={busy}
                    style={{ width: 'fit-content' }}>
              {busy ? (window.appT||(s=>s))('Saving…') : (window.appT||(s=>s))('Save credentials')}
            </button>
            <div className="muted" style={{ fontSize: 11 }}>
              {(window.appT||(s=>s))("Create an OAuth 2.0 (3LO) app at developer.atlassian.com/console/myapps · Redirect URL:")} <code>{window.location.origin}/jira/oauth/callback</code>
              · {(window.appT||(s=>s))("Scopes:")} read:jira-work, write:jira-work, read:jira-user, offline_access
            </div>
          </div>
        )}
      </div>

      {/* Step 2: Install into a tenant */}
      {creds.client_id && (
        <button className="btn btn-pri" onClick={startInstall} disabled={busy}
                style={{ marginBottom: 12 }}>
          {installs && installs.length > 0 ? (window.appT||(s=>s))('Connect another site') : (window.appT||(s=>s))('Connect Jira site')}
        </button>
      )}

      {/* Step 3: per-install controls */}
      {installs && installs.map(i => (
        <div key={i.id} style={{ padding: 10, border: '1px solid var(--bd)',
                                   borderRadius: 6, marginBottom: 8 }}>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <strong>{i.site_url}</strong>
            <code className="mono" style={{ fontSize: 11 }}>{i.cloud_id}</code>
            <button className="btn btn-ghost" style={{ marginLeft: 'auto' }}
                    onClick={() => uninstall(i.id)}>{(window.appT||(s=>s))("Disconnect")}</button>
          </div>
          <div style={{ marginTop: 6, fontSize: 12 }}>
            {(window.appT||(s=>s))("Project:")}&nbsp;
            <span className={`badge ${i.project_key ? 'ok' : 'mute'}`}>
              {i.project_key || (window.appT||(s=>s))('not set')}
            </span>
            {i.project_name ? <span style={{ marginLeft: 6 }}>{i.project_name}</span> : null}
            <button className="btn btn-ghost"
                    style={{ marginLeft: 8, padding: '2px 8px', fontSize: 11 }}
                    onClick={loadProjects}>
              {projects.length ? (window.appT||(s=>s))('Refresh') : (window.appT||(s=>s))('Pick')}
            </button>
          </div>
          {projects.length > 0 && (
            <div style={{ marginTop: 6, display: 'grid', gap: 4 }}>
              {projects.slice(0, 8).map(p => (
                <button key={p.key} className="btn btn-ghost"
                        onClick={() => setProject(p)}
                        style={{ textAlign: 'left', padding: '4px 8px', fontSize: 12 }}>
                  <code>{p.key}</code> &nbsp; {p.name}
                </button>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

/* ─── GitLab / GitHub dev-activity card (autopilot) ──────────────────────
   Status:  GET  /api/integrations/gitdev/status → {gitlab_configured, github_configured, enabled}
   Token:   POST /api/integrations/gitdev/token  {provider, token}           (admin)
   Clear:   DELETE /api/integrations/gitdev/token?provider=gitlab|github
   Master switch + GitLab base URL are bot-behavior settings, so they read/write
   through the same registry endpoint the Bot Behavior page uses
   (gitdev.enabled / gitdev.gitlab_base_url). */
function GitDevIntegrationCard() {
  const toast = useToast();
  const [status, setStatus] = React.useState(null); // {gitlab_configured, github_configured, enabled}
  const [tokenForm, setTokenForm] = React.useState({ gitlab: '', github: '' });
  const [tokenBusy, setTokenBusy] = React.useState({});
  const [settingsState, setSettingsState] = React.useState({ enabled: null, gitlab_base_url: '' });
  const [baseUrlDraft, setBaseUrlDraft] = React.useState('');
  const [savingSetting, setSavingSetting] = React.useState(false);

  const loadStatus = async () => {
    try {
      const r = await fetch('/api/integrations/gitdev/status', { credentials: 'same-origin' });
      if (r.ok) setStatus(await r.json());
    } catch (_) { /* ignore */ }
  };

  const loadSettings = async () => {
    try {
      const r = await fetch('/api/bot-behavior/settings', { credentials: 'same-origin' });
      if (!r.ok) return;
      const d = await r.json();
      const list = d.settings || [];
      const enabledSetting = list.find(s => s.key === 'gitdev.enabled');
      const baseUrlSetting = list.find(s => s.key === 'gitdev.gitlab_base_url');
      const nextEnabled = enabledSetting ? !!enabledSetting.current_value : false;
      const nextBaseUrl = baseUrlSetting ? (baseUrlSetting.current_value || '') : '';
      setSettingsState({ enabled: nextEnabled, gitlab_base_url: nextBaseUrl });
      setBaseUrlDraft(nextBaseUrl);
    } catch (_) { /* ignore */ }
  };

  React.useEffect(() => { loadStatus(); loadSettings(); }, []);

  const saveToken = async (provider) => {
    const token = (tokenForm[provider] || '').trim();
    if (!token) {
      toast.push({ msg: (window.appT||(s=>s))('Paste a token first'), kind: 'warn' });
      return;
    }
    setTokenBusy(b => ({ ...b, [provider]: true }));
    try {
      const r = await fetch('/api/integrations/gitdev/token', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ provider, token }),
      });
      if (!r.ok) { const b = await r.json().catch(() => null); throw new Error(parseDetail(b, r.status)); }
      toast.push({ msg: `${provider === 'gitlab' ? 'GitLab' : 'GitHub'} token saved`, kind: 'ok' });
      setTokenForm(f => ({ ...f, [provider]: '' }));
      await loadStatus();
    } catch (e) {
      toast.push({ msg: `Save failed: ${e.message}`, kind: 'err' });
    } finally {
      setTokenBusy(b => ({ ...b, [provider]: false }));
    }
  };

  const clearToken = async (provider) => {
    const label = provider === 'gitlab' ? 'GitLab' : 'GitHub';
    if (!confirm((window.appT||(s=>s))(`Remove the stored ${label} token?`))) return;
    setTokenBusy(b => ({ ...b, [provider]: true }));
    try {
      const r = await fetch(`/api/integrations/gitdev/token?provider=${encodeURIComponent(provider)}`, {
        method: 'DELETE', credentials: 'same-origin',
      });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      toast.push({ msg: (window.appT||(s=>s))('Token removed'), kind: 'ok' });
      await loadStatus();
    } catch (e) {
      toast.push({ msg: `Failed: ${e.message}`, kind: 'err' });
    } finally {
      setTokenBusy(b => ({ ...b, [provider]: false }));
    }
  };

  const putBotSetting = async (key, value) => {
    const r = await fetch(`/api/bot-behavior/settings/${encodeURIComponent(key)}`, {
      method: 'PUT', credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ value, scope: 'global' }),
    });
    if (!r.ok) throw new Error(await r.text().catch(() => `HTTP ${r.status}`));
  };

  const toggleEnabled = async () => {
    const next = !settingsState.enabled;
    setSavingSetting(true);
    try {
      await putBotSetting('gitdev.enabled', next);
      setSettingsState(s => ({ ...s, enabled: next }));
      toast.push({ msg: next ? (window.appT||(s=>s))('Dev activity enabled') : (window.appT||(s=>s))('Dev activity disabled'), kind: 'ok' });
    } catch (e) {
      toast.push({ msg: `Failed: ${e.message}`, kind: 'err' });
    } finally {
      setSavingSetting(false);
    }
  };

  const saveBaseUrl = async () => {
    setSavingSetting(true);
    try {
      const trimmed = baseUrlDraft.trim();
      await putBotSetting('gitdev.gitlab_base_url', trimmed);
      setSettingsState(s => ({ ...s, gitlab_base_url: trimmed }));
      setBaseUrlDraft(trimmed);
      toast.push({ msg: (window.appT||(s=>s))('GitLab base URL saved'), kind: 'ok' });
    } catch (e) {
      toast.push({ msg: `Failed: ${e.message}`, kind: 'err' });
    } finally {
      setSavingSetting(false);
    }
  };

  const providerRow = (provider, label) => {
    const configured = !!(status && (provider === 'gitlab' ? status.gitlab_configured : status.github_configured));
    const busy = !!tokenBusy[provider];
    return (
      <div style={{ display: 'grid', gap: 6, marginBottom: 10 }} key={provider}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <strong style={{ fontSize: 12.5 }}>{label}</strong>
          <Badge kind={configured ? 'ok' : 'mute'}>
            {configured ? (window.appT||(s=>s))('configured') : (window.appT||(s=>s))('not set')}
          </Badge>
        </div>
        <div style={{ display: 'flex', gap: 6 }}>
          <input className="inp" type="password"
                 placeholder={configured ? (window.appT||(s=>s))('Already saved — paste to overwrite') : (window.appT||(s=>s))('Personal access token')}
                 value={tokenForm[provider]}
                 onChange={e => setTokenForm(f => ({ ...f, [provider]: e.target.value }))}
                 style={{ flex: 1, fontFamily: 'var(--font-mono)' }} />
          <button className="btn btn-pri" disabled={busy} onClick={() => saveToken(provider)}
                  style={{ fontSize: 12 }}>
            {busy ? (window.appT||(s=>s))('Saving…') : (window.appT||(s=>s))('Save')}
          </button>
          {configured && (
            <button className="btn btn-ghost" disabled={busy} onClick={() => clearToken(provider)}
                    style={{ fontSize: 12 }}>
              {(window.appT||(s=>s))('Clear')}
            </button>
          )}
        </div>
      </div>
    );
  };

  const connected = status && (status.gitlab_configured || status.github_configured);

  return (
    <div>
      <SectionHeader
        title={<><Icon name="plug" size={13}/> {(window.appT||(s=>s))("GitLab / GitHub — dev activity")}</>}
        tip={(window.appT||(s=>s))("Show each engineer's commits & merge requests in the daily digest and on demand.")}
        sub={status === null ? (window.appT||(s=>s))('Loading…') :
             connected ? (window.appT||(s=>s))('Connected') : (window.appT||(s=>s))('Not connected')}
      />

      <div style={{ marginBottom: 12, padding: 10, background: 'var(--bg-elev-1)',
                    border: '1px solid var(--line)', borderRadius: 6, fontSize: 12.5 }}>
        {providerRow('gitlab', 'GitLab')}
        {providerRow('github', 'GitHub')}
      </div>

      <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, cursor: 'pointer' }}>
        <input type="checkbox" checked={!!settingsState.enabled}
               disabled={savingSetting || settingsState.enabled === null}
               onChange={toggleEnabled} />
        <span style={{ fontSize: 12.5 }}>{(window.appT||(s=>s))('Enable dev activity in digest & on demand')}</span>
      </label>

      <div style={{ display: 'grid', gap: 4, marginBottom: 10 }}>
        <span style={{ fontSize: 11.5, color: 'var(--text-2)' }}>{(window.appT||(s=>s))('Self-hosted GitLab base URL')}</span>
        <div style={{ display: 'flex', gap: 6 }}>
          <input className="inp" value={baseUrlDraft} placeholder="https://gitlab.example.com"
                 onChange={e => setBaseUrlDraft(e.target.value)}
                 style={{ flex: 1, fontFamily: 'var(--font-mono)' }} />
          <button className="btn btn-ghost" disabled={savingSetting || baseUrlDraft.trim() === (settingsState.gitlab_base_url || '')}
                  onClick={saveBaseUrl} style={{ fontSize: 12 }}>
            {(window.appT||(s=>s))('Save')}
          </button>
        </div>
        <div className="muted" style={{ fontSize: 11 }}>
          {(window.appT||(s=>s))("Self-hosted GitLab? Set the base URL. Leave blank for gitlab.com.")}
        </div>
      </div>

      <div className="muted" style={{ fontSize: 11 }}>
        {(window.appT||(s=>s))("Tokens are stored encrypted. Once enabled, the daily admin digest shows 'N commits · M MRs' per person, and GET /api/devactivity/<user> returns a live breakdown.")}
      </div>
    </div>
  );
}


function TeamsIntegrationCard() {
  const [installations, setInstallations] = React.useState(null);
  const [showModal, setShowModal] = React.useState(false);
  const toast = useToast();

  const load = async () => {
    try {
      const r = await fetch('/api/admin/teams/installations', { credentials: 'same-origin' });
      if (r.ok) setInstallations(await r.json());
      else setInstallations([]);
    } catch(_) { setInstallations([]); }
  };

  React.useEffect(() => { load(); }, []);

  const remove = async (aad_tenant_id) => {
    if (!confirm((window.appLang&&window.appLang()==='ar') ? `إزالة تثبيت Teams للمستأجر ${aad_tenant_id}؟` : `Remove Teams installation for tenant ${aad_tenant_id}?`)) return;
    try {
      const r = await fetch(`/api/admin/teams/install/${encodeURIComponent(aad_tenant_id)}`, {
        method: 'DELETE', credentials: 'same-origin',
      });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      toast.push({ msg: 'Installation removed', kind: 'ok' });
      load();
    } catch(e) { toast.push({ msg: `Failed: ${e.message}`, kind: 'err' }); }
  };

  const isConfigured = installations && installations.length > 0;

  return (
    <div className={`prov-card ${isConfigured ? 'on' : ''}`}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
        <div>
          <div style={{ fontWeight: 500, fontSize: 14 }}>Microsoft Teams</div>
          <div className="muted mono" style={{ fontSize: 11 }}>Bot Framework / Azure AD</div>
        </div>
        <div style={{ display:'flex', gap: 4, flexDirection:'column', alignItems:'flex-end' }}>
          <Badge kind="info">{(window.appT||(s=>s))("messaging")}</Badge>
          {isConfigured && <Badge kind="ok" dot>{(window.appT||(s=>s))("connected")}</Badge>}
        </div>
      </div>
      <div style={{ fontSize: 12.5, color:'var(--text-2)', marginBottom: 10 }}>
        {(window.appT||(s=>s))("Let employees post standups directly in Microsoft Teams. Free-text Y/T/B and Adaptive Card form both supported. Multi-tenant; credentials stored encrypted.")}
      </div>

      {installations === null && (
        <Loading label={(window.appT||(s=>s))("Loading...")} rows={2} />
      )}

      {isConfigured && (
        <div style={{ marginBottom: 10 }}>
          {installations.map(inst => (
            <div key={inst.aad_tenant_id} style={{
              display:'flex', justifyContent:'space-between', alignItems:'center',
              fontSize: 12, padding:'5px 8px', background:'var(--bg-elev-2)',
              borderRadius: 6, marginBottom: 4,
            }}>
              <span className="mono" style={{ color:'var(--text-2)' }}>{inst.aad_tenant_id}</span>
              <div style={{ display:'flex', gap: 4 }}>
                <Badge kind="ok">app: {inst.app_id.slice(0,8)}...</Badge>
                <button
                  className="btn btn-ghost"
                  style={{ padding:'2px 6px', fontSize: 11 }}
                  onClick={() => remove(inst.aad_tenant_id)}
                >
                  {(window.appT||(s=>s))("Remove")}
                </button>
              </div>
            </div>
          ))}
        </div>
      )}

      <div style={{ display:'flex', gap: 6, flexWrap:'wrap' }}>
        <button className="btn btn-ghost" onClick={() => setShowModal(true)}>
          <Icon name="cog" size={12}/> {isConfigured ? (window.appT||(s=>s))('Add tenant') : (window.appT||(s=>s))('Configure credentials')}
        </button>
        <button
          className="btn btn-ghost"
          onClick={async () => {
            // If a tenant is configured the GET has a real GUID server-side.
            // If not, prompt for the Azure AD App ID inline so the user
            // can still build a working manifest without round-tripping
            // through the credentials modal. Empty input → server uses a
            // placeholder GUID with a clear "[TEMPLATE]" note in the
            // manifest description so the admin sees what to swap.
            let appId = '';
            if (!isConfigured) {
              appId = (window.prompt(
                (window.appT||(s=>s))("Azure AD App (Bot) ID — paste here to bake into the manifest, or leave blank to download a template you can edit in Teams' Developer Portal."),
                ''
              ) || '').trim();
            }
            const url = appId
              ? `/teams/manifest.zip?app_id=${encodeURIComponent(appId)}`
              : '/teams/manifest.zip';
            try {
              const r = await fetch(url, { credentials: 'same-origin' });
              if (!r.ok) {
                let detail = '';
                try { detail = (await r.json())?.detail || ''; } catch (_) {}
                throw new Error(`${r.status} ${detail || r.statusText}`);
              }
              const blob = await r.blob();
              const source = r.headers.get('X-App-Id-Source') || 'configured';
              const filename = 'standup-bot-teams.zip';
              if (typeof window.saveDownload === 'function') {
                await window.saveDownload(blob, filename);
              } else {
                const a = document.createElement('a');
                a.href = URL.createObjectURL(blob);
                a.download = filename;
                document.body.appendChild(a);
                a.click();
                a.remove();
              }
              toast.push({
                msg: source === 'template'
                  ? 'Manifest downloaded (TEMPLATE — edit App ID in Teams Dev Portal before publishing)'
                  : 'Manifest downloaded',
                kind: 'ok',
              });
            } catch (e) {
              toast.push({ msg: `Manifest download failed: ${e.message}`, kind: 'err' });
            }
          }}
        >
          <Icon name="download" size={12}/> {(window.appT||(s=>s))("Download manifest")}
        </button>
      </div>

      {showModal && (
        <TeamsInstallModal
          onClose={() => setShowModal(false)}
          onSaved={load}
        />
      )}
    </div>
  );
}

/* ─── Failover order ─────────────────────────────────────
   Lets an admin set the order the bot falls back through when the active AI
   provider errors/rate-limits. Add providers from the dropdown (configured
   ones selectable, keyless ones disabled); chain order = priority. */
function FailoverOrderSection() {
  const toast = useToast();
  const [data, setData]   = React.useState(null);   // {order, providers, active_provider}
  const [chain, setChain] = React.useState([]);      // ordered provider names
  const [busy, setBusy]   = React.useState(false);
  const [dirty, setDirty] = React.useState(false);

  const load = React.useCallback(async () => {
    try {
      const r = await fetch('/api/providers/fallback-order', { credentials: 'same-origin' });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      const j = await r.json();
      setData(j);
      setChain(j.order || []);
      setDirty(false);
    } catch (e) {
      toast.push({ msg: `Failed to load failover order: ${e.message}`, kind: 'err' });
    }
  }, []);
  React.useEffect(() => { load(); }, [load]);

  if (!data) return null;
  const byName = Object.fromEntries((data.providers || []).map(p => [p.name, p]));
  const notInChain = (data.providers || []).filter(p => !chain.includes(p.name));

  const statusLabel = (p) => p.active ? ' (' + (window.appT||(s=>s))('active') + ')' : p.configured ? ' (' + (window.appT||(s=>s))('configured') + ')' : ' (' + (window.appT||(s=>s))('no key') + ')';

  const addProvider = (name) => {
    if (!name || chain.includes(name)) return;
    setChain([...chain, name]); setDirty(true);
  };
  const removeProvider = (name) => { setChain(chain.filter(n => n !== name)); setDirty(true); };
  const move = (idx, dir) => {
    const j = idx + dir;
    if (j < 0 || j >= chain.length) return;
    const next = [...chain];
    [next[idx], next[j]] = [next[j], next[idx]];
    setChain(next); setDirty(true);
  };

  const save = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/providers/fallback-order', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ order: chain }),
      });
      if (!r.ok) { const b = await r.json().catch(() => null); throw new Error(parseDetail(b, r.status)); }
      toast.push({ msg: 'Failover order saved', kind: 'ok' });
      setDirty(false);
    } catch (e) {
      toast.push({ msg: `Save failed: ${e.message}`, kind: 'err' });
    } finally { setBusy(false); }
  };

  return (
    <div className="card" style={{ marginTop: 20, padding: 16 }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 6 }}>
        <div style={{ fontWeight: 600, fontSize: 14 }}>{(window.appT||(s=>s))("Failover order")}</div>
        {dirty && <Badge kind="mute">{(window.appT||(s=>s))("unsaved")}</Badge>}
      </div>
      <div className="muted" style={{ fontSize: 12.5, marginBottom: 12 }}>
        {(window.appT||(s=>s))("When the active provider errors or hits a rate limit, the bot retries down this list in order. Keyless providers can't be added until you set their API key.")}
      </div>

      {chain.length === 0 ? (
        <div className="muted" style={{ fontSize: 12.5, marginBottom: 10 }}>
          {(window.appT||(s=>s))("No custom chain — using the built-in default order.")}
        </div>
      ) : (
        <div style={{ display:'flex', flexDirection:'column', gap: 6, marginBottom: 12 }}>
          {chain.map((name, i) => {
            const p = byName[name] || { name, label: name, configured: false, active: false };
            return (
              <div key={name} style={{
                display:'flex', alignItems:'center', gap: 8, padding:'6px 10px',
                borderRadius: 8, background:'var(--bg-elev-1)', border:'1px solid var(--line)',
                opacity: p.configured ? 1 : 0.55,
              }}>
                <span className="mono muted" style={{ width: 20, textAlign:'right' }}>{i + 1}</span>
                <span style={{ flex: 1, fontSize: 13 }}>
                  {p.label}<span className="muted" style={{ fontSize: 11 }}>{statusLabel(p)}</span>
                </span>
                <button className="btn btn-ghost btn-sm" disabled={i === 0} onClick={() => move(i, -1)} title={(window.appT||(s=>s))("Move up")}>↑</button>
                <button className="btn btn-ghost btn-sm" disabled={i === chain.length - 1} onClick={() => move(i, 1)} title={(window.appT||(s=>s))("Move down")}>↓</button>
                <button className="btn btn-ghost btn-sm" onClick={() => removeProvider(name)} title={(window.appT||(s=>s))("Remove")}>✕</button>
              </div>
            );
          })}
        </div>
      )}

      <div style={{ display:'flex', gap: 8, alignItems:'center', flexWrap:'wrap' }}>
        <select
          className="input"
          defaultValue=""
          onChange={(e) => { addProvider(e.target.value); e.target.value = ''; }}
          style={{ minWidth: 220 }}
        >
          <option value="" disabled>{(window.appT||(s=>s))("Add provider to failover chain…")}</option>
          {notInChain.map(p => (
            <option key={p.name} value={p.name} disabled={!p.configured}>
              {p.label}{statusLabel(p)}
            </option>
          ))}
        </select>
        <button className="btn btn-pri" disabled={busy || !dirty} onClick={save}>
          {busy ? (window.appT||(s=>s))('Saving…') : (window.appT||(s=>s))('Save order')}
        </button>
        {dirty && (
          <button className="btn btn-ghost" disabled={busy} onClick={load}>{(window.appT||(s=>s))("Reset")}</button>
        )}
      </div>
    </div>
  );
}

function ProvidersPage() {
  const D = window.STATUS_DATA;
  const [providers, setProviders] = useState(D.PROVIDERS || []);
  const [configModal, setConfigModal] = useState(null); // provider being configured
  const toast = useToast();

  const makeActive = async (p) => {
    try {
      const r = await fetch('/api/providers/switch', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ provider: p.id }),
      });
      if (!r.ok) {
        const body = await r.json().catch(() => null);
        throw new Error(parseDetail(body, r.status));
      }
      // Refresh providers list — normalize raw API shape
      const rawFresh = await fetch('/api/providers', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : null);
      if (rawFresh) {
        const avail = Array.isArray(rawFresh) ? rawFresh : (rawFresh.available || []);
        const activeName = rawFresh.active?.name || rawFresh.active?.active_provider || p.id;
        setProviders(avail.map(x => ({
          id: x.name || x.id,
          name: x.display_name || x.name,
          default_model: x.default_model || x.summary_model || '',
          tagline: x.notes || x.tagline || '',
          tier: x.tier || 'unknown',
          configured: !!x.configured || !!x.api_key_configured,
          is_active: x.active || (x.name === activeName) || false,
        })));
      }
      toast.push({ msg: `${p.name} is now active`, kind: 'ok' });
    } catch(e) {
      toast.push({ msg: `Failed: ${e.message}`, kind: 'err' });
    }
  };

  return (
    <div className="page">
      <PageHeader
        title={(window.appT||(s=>s))("AI providers")}
        meta={(window.appT||(s=>s))("Choose the model that powers Ask Pulse and the morning roll-up")}
      />

      <div className="grid-cards-2">
        {providers.map(p => {
          const isActive = p.is_active || p.active;
          return (
            <div key={p.id} className={`prov-card ${isActive ? 'on' : ''}`}>
              <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
                <div>
                  <div style={{ fontWeight: 500, fontSize: 14 }}>{p.name}</div>
                  <div className="muted mono" style={{ fontSize: 11 }}>{p.default_model}</div>
                </div>
                <div style={{ display:'flex', gap: 4, flexDirection:'column', alignItems:'flex-end' }}>
                  <Badge kind={p.tier==='free'?'ok':p.tier==='self-host'?'info':p.tier==='mixed'?'mute':'mute'}>{(window.appT||(s=>s))(p.tier)}</Badge>
                  {isActive && <Badge kind="brand" dot>{(window.appT||(s=>s))("active")}</Badge>}
                </div>
              </div>
              <div style={{ fontSize: 12.5, color:'var(--text-2)', marginBottom: 10 }}>{p.tagline}</div>
              <div style={{ display:'flex', gap: 6 }}>
                {p.configured ? (
                  <>
                    {!isActive && (
                      <button className="btn btn-pri" onClick={() => makeActive(p)}>{(window.appT||(s=>s))("Make active")}</button>
                    )}
                    <button className="btn btn-ghost" onClick={() => setConfigModal(p)}>
                      <Icon name="cog" size={12}/> {(window.appT||(s=>s))("Configure")}
                    </button>
                  </>
                ) : (
                  <button className="btn btn-ghost" onClick={() => setConfigModal(p)}>
                    <Icon name="plus" size={12}/> {(window.appT||(s=>s))("Add API key")}
                  </button>
                )}
              </div>
            </div>
          );
        })}
      </div>

      {configModal && (
        <ProviderConfigModal
          provider={configModal}
          onClose={() => setConfigModal(null)}
          onSaved={async () => {
            const rawFresh = await fetch('/api/providers', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : null);
            if (rawFresh) {
              const avail = Array.isArray(rawFresh) ? rawFresh : (rawFresh.available || []);
              const activeName = rawFresh.active?.name || rawFresh.active?.active_provider || '';
              setProviders(avail.map(x => ({
                id: x.name || x.id,
                name: x.display_name || x.name,
                default_model: x.default_model || x.summary_model || '',
                tagline: x.notes || x.tagline || '',
                tier: x.tier || 'unknown',
                configured: !!x.configured || !!x.api_key_configured,
                is_active: x.active || (x.name === activeName) || false,
              })));
            }
          }}
        />
      )}

      <FailoverOrderSection />

      {/* Messaging Integrations (Slack / Microsoft Teams / Jira / WhatsApp).
          Re-enabled per request so admins can configure WhatsApp (Cloud API +
          escalation + inbound webhook) alongside the other channels. */}
      <SlackProviderSection />
    </div>
  );
}

/* ─── helpers ─────────────────────────────────────────── */
function fmtBytes(n) {
  if (n == null) return '—';
  if (n >= 1e9) return (n / 1e9).toFixed(1) + ' GB';
  if (n >= 1e6) return (n / 1e6).toFixed(1) + ' MB';
  if (n >= 1e3) return (n / 1e3).toFixed(1) + ' KB';
  return n + ' B';
}

/* ─── Run now modal ─────────────────────────────────────── */
function RunNowModal({ onClose }) {
  const toast = useToast();
  const [busy, setBusy] = React.useState(false);

  const download = async (fmt) => {
    setBusy(true);
    try {
      const r = await fetch(`/api/scheduler/run-now/daily-recompute?format=${fmt}`, {
        method: 'POST', credentials: 'include',
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const blob = await r.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `daily_recompute.${fmt}`;
      a.click();
      URL.revokeObjectURL(url);
      toast.push({ msg: `${fmt.toUpperCase()} downloaded`, kind: 'ok', icon: '✓' });
    } catch (e) {
      toast.push({ msg: `Failed: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setBusy(false);
    }
  };

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className="drawer-overlay" onClick={onClose} style={{ zIndex: 300 }}>
      <div
        onClick={e => e.stopPropagation()}
        style={{
          position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
          background: 'var(--bg-elev-1)', border: '1px solid var(--line-hi)',
          borderRadius: 'var(--r-lg)', padding: 24, width: 380, maxWidth: '90vw',
          boxShadow: '0 24px 64px -16px #00000080', zIndex: 301,
        }}
      >
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 14 }}>{(window.appT||(s=>s))("Run")} daily_recompute</div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>
        <div style={{ fontSize: 13, color: 'var(--text-2)', marginBottom: 20 }}>
          {(window.appT||(s=>s))("Runs the daily attendance recompute for today and returns a fresh executive report. Choose your download format.")}
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn btn-pri" style={{ flex: 1 }} onClick={() => download('pdf')} disabled={busy}>
            <Icon name="download" size={13} /> {(window.appT||(s=>s))("Download PDF")}
          </button>
          <button className="btn btn-ghost" style={{ flex: 1 }} onClick={() => download('xlsx')} disabled={busy}>
            <Icon name="download" size={13} /> {(window.appT||(s=>s))("Download XLSX")}
          </button>
        </div>
        {busy && <div className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>{(window.appT||(s=>s))("Computing report…")}</div>}
      </div>
    </div>
  );
}

/* ─── Branding section ─────────────────────────────────── */


/* ─── White-label section (#14) ─── */
function WhitelabelSection() {
  const toast = useToast();
  const [b, setB] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    fetch('/api/branding/all', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(setB)
      .catch(() => setB({}));
  }, []);
  const save = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/branding/all', {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          brand_name:    b.brand_name || '',
          slogan:        b.slogan || '',
          primary_color: b.primary_color || '#3b66f5',
          support_email: b.support_email || '',
          domain:        b.domain || '',
        }),
      });
      if (!r.ok) throw new Error(await r.text());
      const j = await r.json();
      setB(j);
      toast.push({ kind: 'ok', icon: '✓', msg: 'White-label saved.' });
      // Re-apply brand color + name immediately
      try {
        if (j.primary_color) document.documentElement.style.setProperty('--color-brand', j.primary_color);
        if (j.brand_name) document.title = j.brand_name;
      } catch(_) {}
    } catch (e) {
      toast.push({ kind: 'err', icon: '!', msg: 'Save failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };
  if (!b) return <div style={{ padding: 16 }}><Skel h={40} /></div>;
  return (
    <div>
      <div style={{ marginBottom: 12, padding: 10, background: 'var(--bg-elev-2)', border: '1px solid var(--line)', borderRadius: 8, fontSize: 12, color: 'var(--text-2)' }}>
        {(window.appT||(s=>s))("Make this dashboard your own. Brand name, slogan and primary color are applied immediately to the header and login screen. Domain is for your records — your ops team still owns the actual nginx + DNS configuration.")}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("Brand name")}</label>
          <input className="inp" value={b.brand_name || ''} placeholder="K9 Pulse"
            onChange={e => setB(prev => ({ ...prev, brand_name: e.target.value }))} />
        </div>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("Primary color")}</label>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input type="color" value={b.primary_color || '#3b66f5'} style={{ width: 40, height: 36, padding: 2, borderRadius: 6, border: '1px solid var(--line)' }}
              onChange={e => setB(prev => ({ ...prev, primary_color: e.target.value }))} />
            <input className="inp" value={b.primary_color || ''} placeholder="#3b66f5" style={{ flex: 1 }}
              onChange={e => setB(prev => ({ ...prev, primary_color: e.target.value }))} />
          </div>
        </div>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("Support email")}</label>
          <input className="inp" type="email" value={b.support_email || ''} placeholder="support@example.com"
            onChange={e => setB(prev => ({ ...prev, support_email: e.target.value }))} />
        </div>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("Custom domain")}</label>
          <input className="inp" value={b.domain || ''} placeholder="status.example.com"
            onChange={e => setB(prev => ({ ...prev, domain: e.target.value }))} />
        </div>
      </div>
      <div style={{ marginBottom: 12 }}>
        <label className="login-label">{(window.appT||(s=>s))("Slogan / tagline")}</label>
        <textarea className="inp" rows={2} value={b.slogan || ''} placeholder={(window.appT||(s=>s))("Your slogan here")}
          onChange={e => setB(prev => ({ ...prev, slogan: e.target.value }))}
          style={{ resize: 'vertical', fontFamily: 'inherit' }} />
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <button className="btn btn-pri" onClick={save} disabled={busy}>
          {busy ? (window.appT||(s=>s))('Saving…') : <><Icon name="check" size={13} /> {(window.appT||(s=>s))('Save white-label')}</>}
        </button>
      </div>
    </div>
  );
}


/* ─── Email Alerts section (#12) ─── */
// Common SMTP provider presets — fill host/port/security; user supplies creds.
const SMTP_PRESETS = {
  custom:    { label: 'Custom / private SMTP', host: '',                                   port: 587, security: 'starttls' },
  gmail:     { label: 'Gmail / Google Workspace', host: 'smtp.gmail.com',                  port: 587, security: 'starttls' },
  office365: { label: 'Microsoft 365 / Outlook',  host: 'smtp.office365.com',              port: 587, security: 'starttls' },
  ses:       { label: 'Amazon SES',               host: 'email-smtp.us-east-1.amazonaws.com', port: 587, security: 'starttls' },
  mailgun:   { label: 'Mailgun',                  host: 'smtp.mailgun.org',                port: 587, security: 'starttls' },
  postmark:  { label: 'Postmark',                 host: 'smtp.postmarkapp.com',            port: 587, security: 'starttls' },
  zoho:      { label: 'Zoho Mail',                host: 'smtp.zoho.com',                   port: 465, security: 'ssl' },
  sendgrid:  { label: 'SendGrid (SMTP relay)',    host: 'smtp.sendgrid.net',               port: 587, security: 'starttls' },
};

function EmailAlertsSection() {
  const toast = useToast();
  const T = window.appT || (s => s);
  const [cfg, setCfg] = React.useState(null);
  const [keyVal, setKeyVal] = React.useState('');   // sendgrid key (write-only)
  const [passVal, setPassVal] = React.useState(''); // smtp password (write-only)
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    fetch('/api/alerts/config', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(c => setCfg(c || {}))
      .catch(() => setCfg({}));
  }, []);
  const upd = (patch) => setCfg(prev => ({ ...prev, ...patch }));
  const applyPreset = (key) => {
    const p = SMTP_PRESETS[key]; if (!p) return;
    upd({ smtp_host: p.host, smtp_port: p.port, smtp_security: p.security });
  };
  const save = async () => {
    setBusy(true);
    try {
      const method = cfg.method === 'smtp' ? 'smtp' : 'sendgrid';
      const body = {
        enabled:    !!cfg.enabled,
        method,
        from_email: cfg.from_email || '',
        recipients: cfg.recipients || '',
      };
      if (method === 'sendgrid' && keyVal && keyVal.trim()) body.sendgrid_key = keyVal.trim();
      if (method === 'smtp') {
        body.smtp_host     = cfg.smtp_host || '';
        body.smtp_port     = parseInt(cfg.smtp_port, 10) || 587;
        body.smtp_security = cfg.smtp_security || 'starttls';
        body.smtp_user     = cfg.smtp_user || '';
        if (passVal && passVal.trim()) body.smtp_pass = passVal;
      }
      const r = await fetch('/api/alerts/config', {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!r.ok) throw new Error(await r.text());
      setCfg(await r.json());
      setKeyVal(''); setPassVal('');
      toast.push({ kind: 'ok', icon: '✓', msg: 'Alert config saved.' });
    } catch (e) {
      toast.push({ kind: 'err', icon: '!', msg: 'Save failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };
  const test = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/alerts/test', { method: 'POST', credentials: 'include' });
      const j = await r.json();
      toast.push({
        kind: j.ok ? 'ok' : 'warn',
        icon: j.ok ? '✓' : '!',
        msg: j.ok ? 'Test alert sent — check your inbox.' : 'Test failed; check transport settings + recipients.',
      });
    } catch (e) {
      toast.push({ kind: 'err', icon: '!', msg: 'Test failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };
  if (!cfg) return <div style={{ padding: 16 }}><Skel h={40} /></div>;
  const method = cfg.method === 'smtp' ? 'smtp' : 'sendgrid';
  return (
    <div>
      <div style={{ marginBottom: 12, padding: 10, background: 'var(--bg-elev-2)', border: '1px solid var(--line)', borderRadius: 8, fontSize: 12, color: 'var(--text-2)' }}>
        {T("Bot health alerts can be delivered via the SendGrid API or any standard / private SMTP server (Gmail, Microsoft 365, Amazon SES, Mailgun, Postmark, Zoho, or your own relay with STARTTLS / SSL). Alerts fire when the scheduler detects a recent")} <em>BotErrorLog</em> {T("entry (debounced to 1/hour per kind).")}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label className="login-label">{T("Email provider")}</label>
        <select className="inp"
          value={method === 'sendgrid'
            ? 'sendgrid'
            : (Object.keys(SMTP_PRESETS).find(k => SMTP_PRESETS[k].host && SMTP_PRESETS[k].host === cfg.smtp_host) || 'custom')}
          onChange={e => {
            const v = e.target.value;
            if (v === 'sendgrid') { upd({ method: 'sendgrid' }); return; }
            const pr = SMTP_PRESETS[v] || SMTP_PRESETS.custom;
            upd({ method: 'smtp', smtp_host: pr.host, smtp_port: pr.port, smtp_security: pr.security });
          }}>
          <option value="sendgrid">{T("SendGrid (HTTPS API)")}</option>
          <optgroup label={T("SMTP (private / standard)")}>
            {Object.keys(SMTP_PRESETS).map(k => <option key={k} value={k}>{SMTP_PRESETS[k].label}</option>)}
          </optgroup>
        </select>
        <div className="muted" style={{ fontSize: 11, marginTop: 4 }}>
          {T("Pick SendGrid (API) or any SMTP provider — Gmail, Microsoft 365, Amazon SES, Mailgun, Postmark, Zoho, SendGrid relay, or a custom/private server.")}
        </div>
      </div>

      {method === 'sendgrid' && (
        <div style={{ marginBottom: 12 }}>
          <label className="login-label">{T("SendGrid API key")} {cfg.sendgrid_key_set && <span className="muted" style={{fontSize:11}}>{T("(currently set — leave blank to keep)")}</span>}</label>
          <input className="inp" type="password" autoComplete="new-password" value={keyVal} placeholder="SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
            onChange={e => setKeyVal(e.target.value)} />
          <div className="muted" style={{ fontSize: 11, marginTop: 4 }}>
            {T("Free key at")} <a href="https://app.sendgrid.com/settings/api_keys" target="_blank" rel="noopener noreferrer" style={{color:'var(--brand)'}}>app.sendgrid.com/settings/api_keys</a>. {T("Verify your sender address first.")}
          </div>
        </div>
      )}

      {method === 'smtp' && (
        <div style={{ marginBottom: 12, padding: 12, background: 'var(--bg-elev-1)', border: '1px solid var(--line)', borderRadius: 8 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: 12, marginBottom: 12 }}>
            <div>
              <label className="login-label">{T("SMTP host")}</label>
              <input className="inp" value={cfg.smtp_host || ''} placeholder="smtp.yourdomain.com"
                onChange={e => upd({ smtp_host: e.target.value })} />
            </div>
            <div>
              <label className="login-label">{T("Port")}</label>
              <input className="inp" type="number" value={cfg.smtp_port || 587} placeholder="587"
                onChange={e => upd({ smtp_port: e.target.value })} />
            </div>
            <div>
              <label className="login-label">{T("Security")}</label>
              <select className="inp" value={cfg.smtp_security || 'starttls'} onChange={e => upd({ smtp_security: e.target.value })}>
                <option value="starttls">STARTTLS</option>
                <option value="ssl">SSL/TLS</option>
                <option value="none">{T("None (plain)")}</option>
              </select>
            </div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <div>
              <label className="login-label">{T("Username")}</label>
              <input className="inp" autoComplete="off" value={cfg.smtp_user || ''} placeholder="apikey / user@domain"
                onChange={e => upd({ smtp_user: e.target.value })} />
            </div>
            <div>
              <label className="login-label">{T("Password")} {cfg.smtp_pass_set && <span className="muted" style={{fontSize:11}}>{T("(set — blank to keep)")}</span>}</label>
              <input className="inp" type="password" autoComplete="new-password" value={passVal} placeholder="••••••••"
                onChange={e => setPassVal(e.target.value)} />
            </div>
          </div>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
        <div>
          <label className="login-label">{T("From email")}</label>
          <input className="inp" type="email" value={cfg.from_email || ''} placeholder="alerts@yourdomain.com"
            onChange={e => upd({ from_email: e.target.value })} />
        </div>
        <div>
          <label className="login-label">{T("Recipients (comma-separated)")}</label>
          <input className="inp" value={cfg.recipients || ''} placeholder="admin@example.com, ops@example.com"
            onChange={e => upd({ recipients: e.target.value })} />
        </div>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
        <input type="checkbox" id="alerts-enabled" checked={!!cfg.enabled}
          onChange={e => upd({ enabled: e.target.checked })} />
        <label htmlFor="alerts-enabled" style={{ fontSize: 13 }}>
          {T("Enable alerts (when off, the scheduler still logs but does not send)")}
        </label>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-ghost" onClick={test} disabled={busy || !cfg.enabled}>
          {T("Send test email")}
        </button>
        <button className="btn btn-pri" onClick={save} disabled={busy}>
          {busy ? T('Saving…') : <><Icon name="check" size={13} /> {T('Save alert config')}</>}
        </button>
      </div>
    </div>
  );
}

function BrandingSection() {
  const toast = useToast();
  const [branding, setBranding] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const fileRef = React.useRef();

  React.useEffect(() => {
    fetch('/api/admin/branding', { credentials: 'include' })
      .then(r => r.ok ? r.json() : {})
      .then(setBranding)
      .catch(() => setBranding({}));
  }, []);

  const save = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/admin/branding', {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          app_name: branding.app_name || '',
          accent_color: branding.accent_color || '',
          login_tagline: branding.login_tagline || '',
        }),
      });
      if (!r.ok) throw new Error(`${r.status}`);
      toast.push({ msg: 'Branding saved', kind: 'ok', icon: '✓' });
    } catch (e) {
      toast.push({ msg: `Save failed: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setBusy(false);
    }
  };

  const uploadFavicon = async (file) => {
    if (!file) return;
    setBusy(true);
    try {
      const fd = new FormData(); fd.append('file', file);
      const r = await fetch('/api/admin/branding/favicon', { method: 'POST', credentials: 'include', body: fd });
      if (!r.ok) throw new Error(await r.text());
      toast.push({ kind: 'ok', icon: '✓', msg: 'Favicon updated.' });
      // #37: cache-bust every <link rel="icon"> and any <img src="/api/branding/...">
      // so the new icon shows without a hard refresh.
      try {
        const bust = '?t=' + Date.now();
        document.querySelectorAll('link[rel~="icon"], link[rel="apple-touch-icon"]').forEach(el => {
          const base = (el.getAttribute('href') || '').split('?')[0];
          if (base.startsWith('/api/branding/')) el.href = base + bust;
        });
        document.querySelectorAll('img').forEach(el => {
          const base = (el.getAttribute('src') || '').split('?')[0];
          if (base.startsWith('/api/branding/')) el.src = base + bust;
        });
        window.dispatchEvent(new CustomEvent('branding-updated', { detail: { ts: Date.now() } }));
        window.dispatchEvent(new CustomEvent('status-data-refresh-requested'));
      } catch(_) {}
    } catch (e) {
      toast.push({ kind: 'err', icon: '!', msg: `Upload failed: ${e.message}` });
    } finally { setBusy(false); }
  };

  if (!branding) return <div style={{ padding: 16 }}><Skel h={40} /></div>;

  const accent = branding.accent_color || '#6366f1';

  return (
    <div>
      {/* Live preview topbar */}
      <div style={{
        marginBottom: 16, padding: '10px 16px',
        background: accent + '22', border: `1px solid ${accent}55`,
        borderRadius: 'var(--r-md)', display: 'flex', alignItems: 'center', gap: 12,
      }}>
        <img src="/api/branding/favicon" alt="" width={24} height={24}
          style={{ borderRadius: 5, objectFit: 'cover' }}
          onError={e => { e.currentTarget.style.display = 'none'; }} />
        <span style={{ fontWeight: 600, fontSize: 13, color: accent }}>
          {branding.app_name || (window.appT||(s=>s))('Attendance Bot')}
        </span>
        <span style={{ fontSize: 11, color: 'var(--text-3)' }}>— {(window.appT||(s=>s))("preview")}</span>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("App name")}</label>
          <input className="inp" value={branding.app_name || ''} onChange={e => setBranding(b => ({ ...b, app_name: e.target.value }))} placeholder={(window.appT||(s=>s))("Attendance Bot")} />
        </div>
        <div>
          <label className="login-label">{(window.appT||(s=>s))("Accent color")}</label>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input
              type="color"
              value={branding.accent_color || '#6366f1'}
              onChange={e => setBranding(b => ({ ...b, accent_color: e.target.value }))}
              style={{ width: 40, height: 36, padding: 2, borderRadius: 6, border: '1px solid var(--line)', cursor: 'pointer', background: 'transparent' }}
            />
            <input className="inp" value={branding.accent_color || ''} onChange={e => setBranding(b => ({ ...b, accent_color: e.target.value }))} placeholder="#6366f1" style={{ flex: 1 }} />
          </div>
        </div>
      </div>

      <div style={{ marginBottom: 12 }}>
        <label className="login-label">{(window.appT||(s=>s))("Login tagline")}</label>
        <textarea
          className="inp"
          value={branding.login_tagline || ''}
          onChange={e => setBranding(b => ({ ...b, login_tagline: e.target.value }))}
          placeholder={(window.appT||(s=>s))("Your team attendance dashboard")}
          rows={2}
          style={{ resize: 'vertical', fontFamily: 'inherit' }}
        />
      </div>

      <div style={{ display: 'flex', gap: 8, marginBottom: 12, alignItems: 'center' }}>
        <img src={`/api/branding/favicon?ts=${Math.floor(Date.now()/3600000)}`} alt="" width={48} height={48}
          style={{ borderRadius: 8, border: '1px solid var(--line)', objectFit: 'cover', background: 'var(--bg-elev-2)' }}
          onError={e => { e.currentTarget.style.display='none'; }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 13, marginBottom: 2 }}>{(window.appT||(s=>s))("Favicon / logo (PNG, JPG, ICO — max 1 MB)")}</div>
          <div className="muted" style={{ fontSize: 11.5 }}>{(window.appT||(s=>s))("Square images work best. Shown in browser tab and sidebar.")}</div>
        </div>
        <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/x-icon,.ico" hidden onChange={e => uploadFavicon(e.target.files[0])} />
        <button className="btn btn-ghost" onClick={() => fileRef.current?.click()} disabled={busy}>{(window.appT||(s=>s))("Upload")}</button>
      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
        <button className="btn btn-pri" onClick={save} disabled={busy}>
          {busy ? (window.appT||(s=>s))('Saving…') : <><Icon name="check" size={13} /> {(window.appT||(s=>s))('Save branding')}</>}
        </button>
      </div>
    </div>
  );
}

/* ─── Logs section ─────────────────────────────────────── */
function LogsSection() {
  const toast = useToast();
  const [rows, setRows] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [page, setPage] = React.useState(1);
  const [total, setTotal] = React.useState(0);
  const [filters, setFilters] = React.useState({
    q: '', from: '', to: '',
  });
  const LOG_SIZE = 50;

  /* Parse user-agent into a short readable label */
  const parseUA = (ua) => {
    if (!ua) return '—';
    const m = ua.match(/(Chrome|Firefox|Safari|Edge|OPR|Opera)[\/ ]([\d.]+)/i);
    const os = ua.match(/\((Windows|Mac OS X|Linux|Android|iOS)[^)]*\)/i);
    const browser = m ? `${m[1]} ${m[2].split('.')[0]}` : 'Unknown';
    const platform = os ? (os[1] === 'Mac OS X' ? 'macOS' : os[1]) : null;
    return platform ? `${browser} / ${platform}` : browser;
  };

  const load = React.useCallback(async (pg, notify) => {
    setLoading(true);
    try {
      const p = new URLSearchParams();
      p.set('limit', String(LOG_SIZE));
      p.set('offset', String((pg - 1) * LOG_SIZE));
      if (filters.from) p.set('from', filters.from);
      if (filters.to)   p.set('to', filters.to);
      if (filters.q)    p.set('q', filters.q);
      const r = await fetch(`/api/server/logs?${p}`, { credentials: 'include' });
      if (!r.ok) throw new Error(`${r.status}`);
      const data = await r.json();
      const arr = Array.isArray(data.logs) ? data.logs : (Array.isArray(data) ? data : []);
      setRows(arr);
      setTotal(typeof data.total === 'number' ? data.total : arr.length);
      setPage(pg);
      if (notify) toast.push({ msg: 'Refreshed', kind: 'ok', icon: '✓' });
    } catch (e) {
      toast.push({ msg: `Logs error: ${e.message}`, kind: 'err', icon: '!' });
    } finally {
      setLoading(false);
    }
  }, [filters, toast]);

  React.useEffect(() => { load(1, false); }, []);

  const statusKind = (s) => {
    if (s == null) return 'mute';
    const str = String(s);
    if (str.startsWith('2')) return 'ok';
    if (str.startsWith('4')) return 'warn';
    if (str.startsWith('5')) return 'err';
    return 'mute';
  };

  return (
    <div>
      {/* Filter toolbar */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 12 }}>
        <SearchInput
          value={filters.q}
          onChange={v => setFilters(f => ({ ...f, q: v }))}
          placeholder={(window.appT||(s=>s))("Search IP, path, user agent...")}
          style={{ flex: '1 1 200px' }}
        />
        <input type="date" className="inp" value={filters.from}
          onChange={e => setFilters(f => ({ ...f, from: e.target.value }))}
          style={{ flex: '0 0 130px', height: 32 }} title={(window.appT||(s=>s))("From date")} />
        <input type="date" className="inp" value={filters.to}
          onChange={e => setFilters(f => ({ ...f, to: e.target.value }))}
          style={{ flex: '0 0 130px', height: 32 }} title={(window.appT||(s=>s))("To date")} />
        <button className="btn btn-pri" onClick={() => load(1, false)} disabled={loading}>{(window.appT||(s=>s))("Apply")}</button>
        <Refresh onClick={() => load(1, true)} loading={loading} />
      </div>

      {loading && rows.length === 0 && (
        <div><Skel h={36} /><Skel h={36} style={{ marginTop: 4 }} /><Skel h={36} style={{ marginTop: 4 }} /></div>
      )}
      {!loading && rows.length === 0 && (
        <Empty icon="·" title={(window.appT||(s=>s))("No log entries")} subtitle={(window.appT||(s=>s))("Try widening filters or refreshing.")} />
      )}
      {rows.length > 0 && (
        <>
          <div style={{ overflowX: 'auto' }}>
            <table className="t" style={{ width: '100%' }}>
              <thead>
                <tr>
                  <th>{(window.appT||(s=>s))("When")}</th><th>{(window.appT||(s=>s))("Who")}</th><th>{(window.appT||(s=>s))("IP")}</th><th>{(window.appT||(s=>s))("Browser")}</th><th>{(window.appT||(s=>s))("Method")}</th><th>{(window.appT||(s=>s))("Path")}</th><th>{(window.appT||(s=>s))("Status")}</th><th>{(window.appT||(s=>s))("ms")}</th>
                </tr>
              </thead>
              <tbody>
                {rows.map((row, i) => (
                  <tr key={i}>
                    <td className="mono" style={{ fontSize: 11, whiteSpace: 'nowrap' }}>
                      {row.ts ? new Date(row.ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
                    </td>
                    <td style={{ fontSize: 12 }}>{row.user_display || '—'}</td>
                    <td className="mono" style={{ fontSize: 11 }}>{row.ip || '—'}</td>
                    <td style={{ fontSize: 11, color: 'var(--text-2)', maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={row.user_agent}>
                      {parseUA(row.user_agent)}
                    </td>
                    <td><Badge kind="mute">{row.method || '—'}</Badge></td>
                    <td className="mono" style={{ fontSize: 11, color: 'var(--text-2)', maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={row.path}>{row.path || '—'}</td>
                    <td><Badge kind={statusKind(row.status)}>{row.status != null ? String(row.status) : '—'}</Badge></td>
                    <td className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>{row.duration_ms != null ? row.duration_ms : '—'}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <Pagination page={page} pageSize={LOG_SIZE} total={total} onChange={pg => load(pg, false)} />
        </>
      )}
    </div>
  );
}

/* ─── Main ServerPage ──────────────────────────────────── */

// ─── Bot logic (admin-editable thresholds + keyword lists) ──────────────────
//
// Reorganised for readability:
//  1. "How it works" intro panel up top
//  2. Numeric thresholds split into 4 grouped cards (enforcement / timing /
//     wellbeing / activity) instead of one big mash
//  3. Plain-English labels + full-sentence descriptions (no more "min hours/day")
//  4. Bigger, less-muted hint text (fontSize 12, color text-2)
//  5. Each setting shows its current value vs. default with a one-click reset
//
// The set of underlying keys is unchanged so /api/bot-config keeps working.

const _BOT_LOGIC_GROUPS = [
  {
    id: 'enforcement',
    icon: '⚖️',
    title: 'Strike enforcement',
    blurb: 'How strict the bot is when someone misses a standup or files the wrong format.',
    fields: [
      {
        key: 'strike_cap',
        label: 'Strikes before admin alert',
        unit: 'strikes',
        help: 'After this many strikes in a row, the bot posts an admin report to the standup room and the user gets a "last warning" DM.',
      },
      {
        key: 'absent_minutes_warn',
        label: 'First ping if no sign-in (minutes)',
        unit: 'min after deadline',
        help: 'How long after the daily sign-in deadline before the bot DMs the user "didn\'t see your sign-in yet — everything OK?".',
      },
      {
        key: 'absent_minutes_strike',
        label: 'Auto-strike if still no sign-in (minutes)',
        unit: 'min after deadline',
        help: 'How long after the deadline before the bot records an absence-strike automatically (no manual admin click needed).',
      },
    ],
  },
  {
    id: 'timing',
    icon: '⏰',
    title: 'Timing & spacing',
    blurb: 'Grace windows and cooldowns so the bot doesn\'t feel naggy.',
    fields: [
      {
        key: 'late_buffer_minutes',
        label: 'Grace period after start time (minutes)',
        unit: 'min',
        help: 'A sign-in within this window of the user\'s expected start time is still counted as on-time (✅). Beyond it, the ack switches to the late-emoji ⏰ and a "what happened?" DM goes out.',
      },
      {
        key: 'dm_cooldown_seconds',
        label: 'Minimum gap between bot DMs (seconds)',
        unit: 's',
        help: 'Stops the bot from spamming a user with multiple DMs back-to-back. After any DM, no follow-up DM fires for at least this long.',
      },
      {
        key: 'auto_relisten_seconds',
        label: 'Voice "still listening" window (seconds)',
        unit: 's',
        help: 'After the user finishes one voice question, the mic stays open this long for a follow-up before the bot stops listening.',
      },
    ],
  },
  {
    id: 'wellbeing',
    icon: '💪',
    title: 'Workload & wellbeing',
    blurb: 'When to flag a short day, a long day, or genuine burnout.',
    fields: [
      {
        key: 'min_hours_per_day',
        label: 'Minimum expected hours per day',
        unit: 'hours',
        help: 'A workday shorter than this fraction of the team\'s "normal" hours triggers a soft "short day" tag in the report.',
      },
      {
        key: 'short_day_pct',
        label: 'Short-day threshold (% of expected)',
        unit: '%',
        help: 'A day below this % of the team\'s normal hours is marked as "short". Example: team norm is 8h, threshold 60% → anything under 4h 48m is flagged.',
      },
      {
        key: 'burnout_hours_per_day',
        label: 'Burnout-warning cap (hours)',
        unit: 'hours',
        help: 'A day longer than this triggers a private DM to the user ("seems like a long day — make sure to rest").',
      },
    ],
  },
  {
    id: 'activity',
    icon: '🛌',
    title: 'Inactivity tracking',
    blurb: 'How long a user can be silent before the bot checks in.',
    fields: [
      {
        key: 'inactivity_hours',
        label: 'Idle hours before "stuck?" DM',
        unit: 'hours',
        help: 'If the user signed in but no progress message lands within this many hours, the bot DMs "any blockers? need help?".',
      },
    ],
  },
];

const _BOT_LOGIC_KEYWORDS = [
  {
    key: 'critical_keywords',
    label: 'Emergency keywords',
    help: 'Words/phrases that will page the on-call team the moment they appear in a standup or DM. Comma-separated.',
    placeholder: 'urgent, p0, outage, fire',
  },
  {
    key: 'sick_keywords',
    label: 'Sick-day keywords',
    help: 'Fuzzy phrases that the bot recognises as "I\'m off sick today" — auto-records a sick PTO block.',
    placeholder: 'sick, flu, fever, doctor',
  },
  {
    key: 'pto_keywords',
    label: 'Vacation / time-off keywords',
    help: 'Fuzzy phrases the bot treats as PTO requests. Triggers the PTO confirmation flow.',
    placeholder: 'vacation, ooo, day off, holiday',
  },
  {
    key: 'signin_shortcut_keywords',
    label: 'Sign-in shortcuts',
    help: 'Short forms accepted as a valid sign-in even without the full template. Use sparingly — too many shortcuts and people stop writing real updates.',
    placeholder: 'in, here, signing in, online',
  },
  {
    key: 'wake_phrases',
    label: 'AI bot wake phrases',
    help: 'Phrases that route a DM to the AI assistant instead of the standup parser.',
    placeholder: 'hey bot, ai, ask',
  },
];

function _BotLogicGroup({ group, settings, defaults, onChange }) {
  return (
    <div style={{
      background:'var(--bg-elev-1)',
      border:'1px solid var(--line)',
      borderRadius:'var(--r-md, 8px)',
      padding:'14px 16px',
    }}>
      <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:4 }}>
        <span style={{ fontSize:18 }}>{group.icon}</span>
        <span style={{ fontSize:14, fontWeight:600 }}>{(window.appT||(s=>s))(group.title)}</span>
      </div>
      <div style={{ fontSize:12.5, color:'var(--text-2)', marginBottom:14 }}>
        {(window.appT||(s=>s))(group.blurb)}
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(260px, 1fr))', gap:14 }}>
        {group.fields.map(f => {
          const cur = settings[f.key];
          const def = defaults[f.key];
          const isCustom = cur !== def && cur != null && cur !== '';
          return (
            <label key={f.key} style={{ display:'flex', flexDirection:'column', gap:4 }}>
              <span style={{ fontSize:13, fontWeight:500 }}>{(window.appT||(s=>s))(f.label)}</span>
              <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                <input
                  type="number"
                  value={cur ?? ''}
                  onChange={e => onChange(f.key, e.target.value === '' ? '' : Number(e.target.value))}
                  style={{ flex:1, padding:'6px 10px', borderRadius:6, border:'1px solid var(--line)', fontSize:13 }}
                />
                <span style={{ fontSize:11.5, color:'var(--text-3)', whiteSpace:'nowrap' }}>{(window.appT||(s=>s))(f.unit)}</span>
              </div>
              <span style={{ fontSize:12, color:'var(--text-2)', lineHeight:1.4 }}>
                {(window.appT||(s=>s))(f.help)}
              </span>
              <div style={{ fontSize:11, color:'var(--text-3)' }}>
                {(window.appT||(s=>s))("Default")} <span className="mono">{String(def ?? '—')}</span>
                {isCustom && (
                  <button
                    onClick={() => onChange(f.key, def)}
                    className="btn-link"
                    style={{ marginLeft:8, fontSize:11, padding:0, background:'none', border:'none', color:'var(--brand)', cursor:'pointer' }}
                  >{(window.appT||(s=>s))("reset")}</button>
                )}
              </div>
            </label>
          );
        })}
      </div>
    </div>
  );
}

function BotLogicSection() {
  const toast = useToast();
  const [data, setData] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    fetch('/api/bot-config', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(setData)
      .catch(() => setData({ settings: {}, defaults: {} }));
  }, []);
  if (!data) return <Loading label={(window.appT||(s=>s))("Loading bot logic…")} rows={2} />;

  const s = data.settings || {};
  const d = data.defaults || {};
  const setVal = (k, v) => setData({ ...data, settings: { ...s, [k]: v } });

  const save = async () => {
    setBusy(true);
    try {
      const updates = { ...s };
      const r = await fetch('/api/bot-config', {
        method:'PUT', credentials:'include',
        headers:{ 'Content-Type':'application/json' },
        body: JSON.stringify({ updates }),
      });
      if (!r.ok) throw new Error(await r.text());
      const j = await r.json();
      setData({ settings: j.settings, defaults: d });
      toast.push({ kind:'ok', icon:'✓', msg:'Bot logic saved.' });
    } catch (e) {
      toast.push({ kind:'err', icon:'!', msg:'Save failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const reset = (k) => setVal(k, d[k]);
  const listToText = (v) => Array.isArray(v) ? v.join(', ') : (v || '');

  // Count pending diffs across all sections.
  const allKeys = [
    ..._BOT_LOGIC_GROUPS.flatMap(g => g.fields.map(f => f.key)),
    ..._BOT_LOGIC_KEYWORDS.map(f => f.key),
  ];
  const pending = allKeys.filter(k => {
    if (Array.isArray(s[k]) || Array.isArray(d[k])) {
      return JSON.stringify(s[k] || []) !== JSON.stringify(d[k] || []);
    }
    return s[k] !== d[k] && s[k] != null && s[k] !== '';
  }).length;

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:18 }}>
      {/* ── How it works intro ─────────────────────────────────────── */}
      <div style={{
        background:'var(--bg-elev-2)',
        border:'1px solid var(--line)',
        borderRadius:'var(--r-md, 8px)',
        padding:'12px 16px',
        fontSize:13, lineHeight:1.5, color:'var(--text-2)',
      }}>
        <div style={{ fontWeight:600, color:'var(--text)', marginBottom:6 }}>{(window.appT||(s=>s))("How the bot decides things")}</div>
        {(window.appT||(s=>s))("Every Matrix message is parsed by the bot and routed through these rules. Numbers below control")} <strong>{(window.appT||(s=>s))("when")}</strong> {(window.appT||(s=>s))("the bot pings, warns, or strikes; keyword lists control")} <strong>{(window.appT||(s=>s))("what")}</strong> {(window.appT||(s=>s))("it recognises as sick-day, PTO, or emergency language. Saved changes go live within ~60s — no rebuild required.")}
      </div>

      {/* ── Numeric threshold groups ───────────────────────────────── */}
      {_BOT_LOGIC_GROUPS.map(g => (
        <_BotLogicGroup key={g.id} group={g} settings={s} defaults={d} onChange={setVal} />
      ))}

      {/* ── Keyword lists ──────────────────────────────────────────── */}
      <div style={{
        background:'var(--bg-elev-1)',
        border:'1px solid var(--line)',
        borderRadius:'var(--r-md, 8px)',
        padding:'14px 16px',
      }}>
        <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:4 }}>
          <span style={{ fontSize:18 }}>🔤</span>
          <span style={{ fontSize:14, fontWeight:600 }}>{(window.appT||(s=>s))("Keyword lists")}</span>
        </div>
        <div style={{ fontSize:12.5, color:'var(--text-2)', marginBottom:14 }}>
          {(window.appT||(s=>s))("Phrases the bot treats as triggers. Comma-separated. Fuzzy-matched (close spellings + word variants count too).")}
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
          {_BOT_LOGIC_KEYWORDS.map(f => {
            const cur = s[f.key];
            const def = d[f.key];
            const isCustom = Array.isArray(cur) && Array.isArray(def) &&
              cur.join('|') !== def.join('|');
            return (
              <label key={f.key} style={{ display:'flex', flexDirection:'column', gap:4 }}>
                <span style={{ fontSize:13, fontWeight:500 }}>{(window.appT||(s=>s))(f.label)}</span>
                <textarea
                  rows={2}
                  value={listToText(cur)}
                  placeholder={(window.appT||(s=>s))(f.placeholder)}
                  onChange={e => setVal(f.key, e.target.value.split(',').map(x => x.trim()).filter(Boolean))}
                  style={{ padding:'8px 10px', borderRadius:6, border:'1px solid var(--line)', fontFamily:'monospace', fontSize:12.5, lineHeight:1.5 }}
                />
                <div style={{ fontSize:12, color:'var(--text-2)', lineHeight:1.4 }}>
                  {(window.appT||(s=>s))(f.help)}
                </div>
                {isCustom && (
                  <button
                    onClick={() => reset(f.key)}
                    className="btn-link"
                    style={{ alignSelf:'flex-start', fontSize:11, padding:0, background:'none', border:'none', color:'var(--brand)', cursor:'pointer' }}
                  >{(window.appLang&&window.appLang()==='ar') ? `إعادة للوضع الافتراضي (${def.length} عناصر)` : `reset to default (${def.length} items)`}</button>
                )}
              </label>
            );
          })}
        </div>
      </div>

      {/* ── Save bar ───────────────────────────────────────────────── */}
      <div style={{
        position:'sticky', bottom:0, zIndex:5,
        background:'var(--bg)',
        borderTop:'1px solid var(--line)',
        padding:'10px 0',
        display:'flex', alignItems:'center', gap:12,
      }}>
        <button
          className="btn btn-pri"
          disabled={busy || pending === 0}
          onClick={save}
          style={{ padding:'8px 16px' }}
        >
          {busy ? (window.appT||(s=>s))('Saving…') : pending > 0 ? ((window.appLang&&window.appLang()==='ar') ? `حفظ ${pending} تغيير` : `Save ${pending} change${pending === 1 ? '' : 's'}`) : (window.appT||(s=>s))('No changes')}
        </button>
        <span style={{ fontSize:12, color:'var(--text-2)' }}>
          {(window.appT||(s=>s))("Changes apply within ~60s (config cache TTL). Restart the bot container for instant pickup.")}
        </span>
      </div>
    </div>
  );
}

/* IpLearningSection — Phase 3 inline knobs for the per-team IP red-flag
   detector. Lists each team and exposes the four threshold columns plus
   the master enable flag. Each save goes through PATCH /api/team-settings/
   {team_name} which already emits a 90s undo toast. */
function IpLearningSection() {
  const EditableCell = window.EditableCell;
  const editMode     = window.useEditMode ? window.useEditMode() : false;
  const [teams, setTeams] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const toast = useToast();

  React.useEffect(() => {
    let alive = true;
    fetch('/api/teams', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(rows => { if (alive) setTeams(rows); })
      .catch(() => { if (alive) setTeams([]); });
    return () => { alive = false; };
  }, [tick]);

  const reload = () => setTick(t => t + 1);

  const patch = async (team_name, field, value) => {
    const r = await fetch(`/api/team-settings/${encodeURIComponent(team_name || '')}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'same-origin',
      body: JSON.stringify({ [field]: value }),
    });
    if (!r.ok) {
      const body = await r.json().catch(() => null);
      const msg = (body && (body.detail || body.error)) || `HTTP ${r.status}`;
      toast.push({ msg: `Save failed: ${msg}`, kind: 'err' });
      throw new Error(msg);
    }
    const data = await r.json().catch(() => ({}));
    if (data && data.undo_token) {
      const token = data.undo_token;
      toast.push({
        msg: `Saved ${field} for ${team_name || '(default)'}. Undo?`,
        kind: 'ok',
        actions: [{
          label: 'Undo',
          onClick: async () => {
            const ur = await fetch(`/api/admin/undo/v2/${token}`, { method: 'POST', credentials:'same-origin' });
            if (ur.ok) toast.push({ msg: 'Reverted', kind:'ok' });
            else toast.push({ msg: 'Undo failed', kind:'err' });
            reload();
          },
        }],
      });
    } else {
      toast.push({ msg: `Saved ${field}`, kind:'ok' });
    }
    reload();
  };

  if (teams == null) return <Loading label={(window.appT||(s=>s))("Loading…")} rows={2} />;
  if (teams.length === 0) return <div className="muted" style={{ fontSize: 12 }}>{(window.appT||(s=>s))("No teams found.")}</div>;
  if (!editMode) {
    return (
      <div className="muted" style={{ fontSize: 12 }}>
        {(window.appT||(s=>s))("Enable Edit mode (top-right) to inline-edit per-team thresholds. Read-only summary:")}
        <table style={{ width:'100%', marginTop: 12, fontSize: 12 }}>
          <thead><tr style={{ color:'var(--text-2)' }}>
            <th style={{ textAlign:'left' }}>{(window.appT||(s=>s))("Team")}</th><th>{(window.appT||(s=>s))("Enabled")}</th><th>{(window.appT||(s=>s))("Min samples")}</th><th>{(window.appT||(s=>s))("Match %")}</th><th>{(window.appT||(s=>s))("Throttle (h)")}</th>
          </tr></thead>
          <tbody>
            {teams.map(t => (
              <tr key={t.team_name} style={{ borderTop:'1px solid var(--line)' }}>
                <td style={{ padding:'6px 4px' }}>{t.team_name || (window.appT||(s=>s))('(default)')}</td>
                <td style={{ textAlign:'center' }}>{t.ip_redflag_enabled === false ? '—' : '✓'}</td>
                <td style={{ textAlign:'center' }}>{t.ip_redflag_min_samples ?? 5}</td>
                <td style={{ textAlign:'center' }}>{t.ip_redflag_min_match_pct ?? 60}</td>
                <td style={{ textAlign:'center' }}>{t.ip_redflag_throttle_hours ?? 24}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  return (
    <table style={{ width:'100%', fontSize: 12 }}>
      <thead><tr style={{ color:'var(--text-2)' }}>
        <th style={{ textAlign:'left', padding:'6px 4px' }}>{(window.appT||(s=>s))("Team")}</th>
        <th style={{ textAlign:'center' }}>{(window.appT||(s=>s))("Enabled")}</th>
        <th style={{ textAlign:'center' }}>{(window.appT||(s=>s))("Min samples")}</th>
        <th style={{ textAlign:'center' }}>{(window.appT||(s=>s))("Match %")}</th>
        <th style={{ textAlign:'center' }}>{(window.appT||(s=>s))("Throttle (h)")}</th>
      </tr></thead>
      <tbody>
        {teams.map(t => (
          <tr key={t.team_name} style={{ borderTop:'1px solid var(--line)' }}>
            <td style={{ padding:'8px 4px' }}>{t.team_name || (window.appT||(s=>s))('(default)')}</td>
            <td style={{ textAlign:'center' }}>
              <input type="checkbox" checked={t.ip_redflag_enabled !== false}
                onChange={e => patch(t.team_name, 'ip_redflag_enabled', e.target.checked)} />
            </td>
            <td style={{ textAlign:'center' }}>
              <EditableCell value={t.ip_redflag_min_samples ?? 5} kind="text"
                onSave={v => patch(t.team_name, 'ip_redflag_min_samples', parseInt(v, 10) || 0)} />
            </td>
            <td style={{ textAlign:'center' }}>
              <EditableCell value={t.ip_redflag_min_match_pct ?? 60} kind="text"
                onSave={v => patch(t.team_name, 'ip_redflag_min_match_pct', parseInt(v, 10) || 0)} />
            </td>
            <td style={{ textAlign:'center' }}>
              <EditableCell value={t.ip_redflag_throttle_hours ?? 24} kind="text"
                onSave={v => patch(t.team_name, 'ip_redflag_throttle_hours', parseInt(v, 10) || 0)} />
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}


/* ─── Guardian control panel (on-demand self-healing agent) ─────────── */
function GuardianPanel() {
  const [st, setSt] = React.useState(null);
  const [runs, setRuns] = React.useState([]);
  const [busy, setBusy] = React.useState(null);
  const toast = useToast();
  const T = (window.appT || ((s) => s));

  const loadStatus = () => fetch('/api/admin/guardian/status', { credentials: 'include' })
    .then(r => r.ok ? r.json() : null).then(j => setSt(j)).catch(() => {});
  const loadRuns = () => fetch('/api/admin/guardian/runs?limit=8', { credentials: 'include' })
    .then(r => r.ok ? r.json() : []).then(j => setRuns(Array.isArray(j) ? j : [])).catch(() => {});

  React.useEffect(() => {
    let canc = false;
    const tick = () => { if (canc) return; loadStatus(); loadRuns(); };
    tick();
    const id = setInterval(tick, 5000);
    return () => { canc = true; clearInterval(id); };
  }, []);

  const run = async (command, label) => {
    setBusy(command);
    try {
      const r = await fetch('/api/admin/guardian/run', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ command }),
      });
      const j = await r.json().catch(() => ({}));
      if (r.ok) {
        toast.push({ msg: `${label} queued — Guardian will run it shortly`, kind: 'ok', icon: '✓' });
        loadRuns();
      } else {
        toast.push({ msg: `Failed to queue ${label}: ${j.detail || r.status}`, kind: 'err', icon: '!' });
      }
    } catch (e) {
      toast.push({ msg: `Error: ${e}`, kind: 'err', icon: '!' });
    } finally {
      setTimeout(() => setBusy(null), 1500);
    }
  };

  const d = (st && st.data) || {};
  const alive = !!(st && st.alive);
  const probes = d.last_probes || {};
  const fmtAge = (s) => s == null ? '—' : (s < 90 ? `${Math.round(s)}s ago` : `${Math.round(s / 60)}m ago`);
  const statusKind = (s) => s === 'done' ? 'ok' : s === 'error' ? 'err' : s === 'running' ? 'warn' : 'mute';

  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <SectionHeader title={T("Guardian — self-healing agent")}
        tip={T("Guardian monitors the bot, auto-heals outages, and watches the bot's logic for regressions. Trigger checks on demand here.")}
        sub={alive ? (T("Live · heartbeat ") + fmtAge(st && st.age_seconds)) : T("Heartbeat stale — Guardian may be down")} />

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center', marginBottom: 14 }}>
        <Badge kind={alive ? 'ok' : 'err'} dot>{alive ? T('Healthy') : T('Down?')}</Badge>
        {d.pause && <Badge kind="warn">{T('PAUSED')}</Badge>}
        <span className="muted">{T('Uptime 24h')}: <b>{d.uptime_24h != null ? d.uptime_24h + '%' : '—'}</b></span>
        <span className="muted">{T('Open incidents')}: <b>{d.open_incidents != null ? d.open_incidents : '—'}</b></span>
        <span className="muted">{T('Logic gate')}: <b>{d.baseline_count != null ? d.baseline_count + ' tests' : '—'}</b></span>
        <span style={{ display: 'flex', gap: 5, alignItems: 'center' }} title={T('Probes P1–P7')}>
          {['P1','P2','P3','P4','P5','P6','P7'].map(pk => (
            <span key={pk} title={pk}
              style={{ width: 9, height: 9, borderRadius: 9,
                background: probes[pk] === false ? 'var(--err,#e5484d)' : (probes[pk] ? 'var(--ok,#30a46c)' : 'var(--muted,#888)') }} />
          ))}
        </span>
      </div>

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
        <button className="btn btn-pri btn-sm" disabled={busy === 'probe'} onClick={() => run('probe', T('Health probes'))}>
          {busy === 'probe' ? T('Queuing…') : T('Run probes now')}</button>
        <button className="btn btn-ghost btn-sm" disabled={busy === 'logic-check'} onClick={() => run('logic-check', T('Logic check'))}>
          {busy === 'logic-check' ? T('Queuing…') : T('Run logic check')}</button>
        <button className="btn btn-ghost btn-sm" disabled={busy === 'test-now'} onClick={() => run('test-now', T('E2E test'))}>
          {busy === 'test-now' ? T('Queuing…') : T('Run E2E test')}</button>
      </div>

      <div className="muted" style={{ fontSize: 12, marginBottom: 6 }}>{T('Recent runs')}</div>
      {runs.length === 0 && <div className="muted" style={{ fontSize: 12 }}>{T('No runs yet.')}</div>}
      {runs.map(r => (
        <div key={r.id} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '6px 0', borderTop: '1px solid var(--line,#2222)' }}>
          <Badge kind={statusKind(r.status)}>{r.status}</Badge>
          <b style={{ minWidth: 84 }}>{r.command}</b>
          <span className="muted mono" style={{ fontSize: 11, flex: 1, wordBreak: 'break-word', whiteSpace: 'pre-wrap', maxHeight: 90, overflow: 'auto' }}>
            {r.result || (r.status === 'running' ? T('running…') : '')}</span>
          <span className="muted" style={{ fontSize: 10, whiteSpace: 'nowrap' }}>{(r.completed_at || r.requested_at || '').replace('T', ' ').slice(5, 19)}</span>
        </div>
      ))}
    </div>
  );
}

// ── Machine access: API tokens, outbound webhooks, CSV export ──────────────
// A token is a durable key to this API and a webhook is a standing outbound data
// flow, so both are superadmin-only. A token acts AS a user and can never widen
// that user's authority — an employee's token still can't read a manager's data.
// The plaintext is shown ONCE here and is not recoverable afterwards.
function MachineAccessSection() {
  const T = (window.appT || (s => s));
  const toast = (window.useToast ? window.useToast() : { push: () => {} });
  const [tokens, setTokens] = useState([]);
  const [hooks, setHooks] = useState([]);
  const [events, setEvents] = useState([]);
  const [deliveries, setDeliveries] = useState([]);
  const [minted, setMinted] = useState(null);        // shown once, then gone
  const [tName, setTName] = useState('');
  const [tScopes, setTScopes] = useState('read');
  const [tDays, setTDays] = useState('');
  const [hName, setHName] = useState('');
  const [hUrl, setHUrl] = useState('');
  const [hSecret, setHSecret] = useState('');
  const [hEvents, setHEvents] = useState('*');

  const load = async () => {
    try {
      const [a, b, c] = await Promise.all([
        fetch('/api/integrations/tokens', { credentials: 'include' }),
        fetch('/api/integrations/webhooks', { credentials: 'include' }),
        fetch('/api/integrations/webhooks/deliveries?limit=20', { credentials: 'include' }),
      ]);
      if (a.ok) setTokens((await a.json()).items || []);
      if (b.ok) {
        const j = await b.json();
        setHooks(j.items || []);
        setEvents(j.available_events || []);
      }
      if (c.ok) setDeliveries((await c.json()).items || []);
    } catch (_) {}
  };
  useEffect(() => { load(); }, []);

  const mint = async () => {
    if (!tName.trim()) { toast.push({ msg: T('Name the token so you can recognise it later.'), kind: 'err' }); return; }
    const r = await fetch('/api/integrations/tokens', {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: tName, scopes: tScopes,
        expires_in_days: tDays ? Number(tDays) : null,
      }),
    });
    if (!r.ok) { toast.push({ msg: T('Could not create token.'), kind: 'err' }); return; }
    const j = await r.json();
    setMinted(j);
    setTName('');
    await load();
  };

  const revoke = async id => {
    if (!window.confirm(T('Revoke this token? Anything using it stops working on its next request.'))) return;
    await fetch(`/api/integrations/tokens/${id}`, { method: 'DELETE', credentials: 'include' });
    await load();
  };

  const addHook = async () => {
    const r = await fetch('/api/integrations/webhooks', {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: hName, url: hUrl, secret: hSecret, events: hEvents }),
    });
    if (!r.ok) {
      const detail = await r.json().catch(() => ({}));
      toast.push({ msg: (detail.detail || T('Could not create webhook.')), kind: 'err' });
      return;
    }
    setHName(''); setHUrl(''); setHSecret('');
    await load();
  };

  const patchHook = async (id, patch) => {
    await fetch(`/api/integrations/webhooks/${id}`, {
      method: 'PATCH', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch),
    });
    await load();
  };

  const testHook = async id => {
    await fetch(`/api/integrations/webhooks/${id}/test`, { method: 'POST', credentials: 'include' });
    toast.push({ msg: T('Signed ping queued — it goes out on the next delivery pass (≤1 min).'), kind: 'ok' });
    setTimeout(load, 3000);
  };

  const delHook = async id => {
    if (!window.confirm(T('Delete this webhook? Queued but undelivered events for it are dropped.'))) return;
    await fetch(`/api/integrations/webhooks/${id}`, { method: 'DELETE', credentials: 'include' });
    await load();
  };

  const EXPORTS = [
    { path: '/api/export/attendance.csv', label: T('Attendance (per day)') },
    { path: '/api/export/hours.csv', label: T('Hours (totals per employee)') },
    { path: '/api/export/efficiency.csv', label: T('Efficiency (per week)') },
  ];

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
      {/* CSV export */}
      <div>
        <SectionHeader title={T('CSV export')}
          tip={T('Downloads exactly what you are allowed to see — a manager gets their own team, an admin gets everyone. The same URLs work with an API token, so a BI tool can pull them on a schedule.')} />
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: '6px 0' }}>
          {EXPORTS.map(e => (
            <a key={e.path} className="btn btn-ghost" href={e.path} download>{e.label}</a>
          ))}
        </div>
      </div>

      {/* Tokens */}
      <div>
        <SectionHeader title={T('API tokens')}
          tip={T("A token acts as the user it belongs to and can never do more than that user can. Read-only tokens are refused any request that would change something. Only a hash is stored — the plaintext is shown once, here, and cannot be recovered.")}
          sub={`${tokens.filter(t => !t.revoked_at).length} ${T('active')}`} />
        {minted && (
          <div className="card" style={{ padding: 10, marginBottom: 10, border: '1px solid #b26a00' }}>
            <div className="small" style={{ fontWeight: 600, marginBottom: 4 }}>
              {T('Copy this now — it is not shown again and cannot be recovered.')}
            </div>
            <code className="mono" style={{ fontSize: 12, wordBreak: 'break-all' }}>{minted.token}</code>
            <div style={{ marginTop: 6 }}>
              <button className="btn btn-ghost" onClick={() => {
                try { navigator.clipboard.writeText(minted.token); toast.push({ msg: T('Copied.'), kind: 'ok' }); } catch (_) {}
              }}>{T('Copy')}</button>
              <button className="btn btn-ghost" onClick={() => setMinted(null)}>{T('Done')}</button>
            </div>
          </div>
        )}
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', marginBottom: 8 }}>
          <input placeholder={T('name (e.g. grafana)')} value={tName}
                 onChange={e => setTName(e.target.value)} style={{ fontSize: 12 }} />
          <select value={tScopes} onChange={e => setTScopes(e.target.value)} style={{ fontSize: 12 }}>
            <option value="read">{T('read-only')}</option>
            <option value="read,write">{T('read + write')}</option>
          </select>
          <input placeholder={T('expires in days (blank = never)')} value={tDays}
                 onChange={e => setTDays(e.target.value.replace(/[^0-9]/g, ''))}
                 style={{ fontSize: 12, width: 170 }} />
          <button className="btn btn-ghost" onClick={mint}>{T('Create token')}</button>
        </div>
        <table className="t" style={{ fontSize: 12 }}>
          <thead><tr>
            <th>{T('Name')}</th><th>{T('Prefix')}</th><th>{T('Scopes')}</th>
            <th>{T('Last used')}</th><th>{T('Expires')}</th><th>{T('State')}</th><th></th>
          </tr></thead>
          <tbody>
            {tokens.length === 0 && (
              <tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 12 }}>
                {T('No tokens.')}</td></tr>
            )}
            {tokens.map(t => (
              <tr key={t.id}>
                <td>{t.name}</td>
                <td className="mono">{t.prefix}…</td>
                <td>{(t.scopes || []).join(', ')}</td>
                <td className="mono">{t.last_used_at ? t.last_used_at.slice(0, 16).replace('T', ' ') : '—'}</td>
                <td className="mono">{t.expires_at ? t.expires_at.slice(0, 10) : T('never')}</td>
                <td>{t.revoked_at
                  ? <Badge kind="err">{T('revoked')}</Badge>
                  : <Badge kind="ok" dot>{T('active')}</Badge>}</td>
                <td>{!t.revoked_at && (
                  <button className="btn btn-ghost" onClick={() => revoke(t.id)}>{T('Revoke')}</button>
                )}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Webhooks */}
      <div>
        <SectionHeader title={T('Outbound webhooks')}
          tip={T('We POST a signed JSON body to your URL when something happens here. Every delivery carries X-K9-Signature (HMAC-SHA256 of the exact body with your secret) so you can verify it. Failures are retried with backoff, then abandoned with the reason recorded.')}
          sub={events.length ? `${T('events')}: ${events.join(', ')}` : ''} />
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', marginBottom: 8 }}>
          <input placeholder={T('name')} value={hName} onChange={e => setHName(e.target.value)} style={{ fontSize: 12 }} />
          <input placeholder="https://example.com/hook" value={hUrl} onChange={e => setHUrl(e.target.value)} style={{ fontSize: 12, minWidth: 220 }} />
          <input placeholder={T('shared secret')} value={hSecret} onChange={e => setHSecret(e.target.value)} style={{ fontSize: 12 }} />
          <input placeholder={T('events (* = all)')} value={hEvents} onChange={e => setHEvents(e.target.value)} style={{ fontSize: 12, width: 150 }} />
          <button className="btn btn-ghost" onClick={addHook}>{T('Add webhook')}</button>
        </div>
        <table className="t" style={{ fontSize: 12 }}>
          <thead><tr>
            <th>{T('Name')}</th><th>{T('URL')}</th><th>{T('Events')}</th>
            <th>{T('Last')}</th><th>{T('State')}</th><th></th>
          </tr></thead>
          <tbody>
            {hooks.length === 0 && (
              <tr><td colSpan={6} className="muted" style={{ textAlign: 'center', padding: 12 }}>
                {T('No webhooks.')}</td></tr>
            )}
            {hooks.map(h => (
              <tr key={h.id}>
                <td>{h.name}</td>
                <td className="mono" style={{ maxWidth: 260, overflow: 'hidden', textOverflow: 'ellipsis' }}>{h.url}</td>
                <td>{h.events}</td>
                <td className="mono">{h.last_status || '—'}
                  {h.consecutive_failures > 0 && <span className="muted"> ·{h.consecutive_failures} {T('fails')}</span>}</td>
                <td>{h.circuit_open
                  ? <Badge kind="err">{T('paused — too many failures')}</Badge>
                  : (h.active ? <Badge kind="ok" dot>{T('active')}</Badge>
                              : <Badge kind="mute">{T('off')}</Badge>)}</td>
                <td style={{ whiteSpace: 'nowrap' }}>
                  <button className="btn btn-ghost" onClick={() => testHook(h.id)}>{T('Test')}</button>
                  <button className="btn btn-ghost" onClick={() => patchHook(h.id, { active: !h.active })}>
                    {h.active ? T('Disable') : T('Enable')}
                  </button>
                  <button className="btn btn-ghost" onClick={() => delHook(h.id)}>{T('Delete')}</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        {deliveries.length > 0 && (
          <div style={{ marginTop: 8 }}>
            <div className="small muted" style={{ marginBottom: 4 }}>{T('Recent deliveries')}</div>
            <table className="t" style={{ fontSize: 11 }}>
              <thead><tr><th>{T('Event')}</th><th>{T('Tries')}</th><th>{T('Status')}</th><th>{T('Result')}</th></tr></thead>
              <tbody>
                {deliveries.map(d => (
                  <tr key={d.id}>
                    <td>{d.event}</td>
                    <td className="mono">{d.attempts}</td>
                    <td className="mono">{d.last_status || (d.last_error ? T('error') : '—')}</td>
                    <td className="muted">
                      {d.delivered_at ? T('delivered')
                        : d.gave_up_at ? T('gave up')
                        : `${T('retrying')} ${d.next_attempt_at ? d.next_attempt_at.slice(11, 16) : ''}`}
                      {d.last_error ? ` · ${d.last_error}` : ''}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
}

function ServerPage() {
  const D = window.STATUS_DATA;
  const [health, setHealth] = React.useState({});
  const [resources, setResources] = React.useState(null);
  const [connectivity, setConn] = React.useState(null);
  const [latency, setLatency] = React.useState(null);   // from /api/health/detailed connectivity sub-dict
  const [runModal, setRunModal] = React.useState(false);
  // Promoted to "admin or above" so moderators (Miro) get the live
  // Server polling / scheduler view. Superadmins (Hany) also pass.
  const isAdmin = (window.isAdminOrAbove ? window.isAdminOrAbove(D.ME) : D.ME?.org_role === 'admin');
  const toast = useToast();

  /* Poll /api/server-health every 5s for uptime + scheduler jobs */
  React.useEffect(() => {
    let canc = false;
    const load = () => fetch('/api/server-health', { credentials: 'include' })
      .then(r => r.ok ? r.json() : {})
      .then(j => { if (!canc) setHealth(j); })
      .catch(() => {});
    load();
    const id = setInterval(load, 5000);
    return () => { canc = true; clearInterval(id); };
  }, []);

  /* Fetch host-resources + connectivity + latency every 3s for the
     "live" feel — the donuts smoothly animate between samples via the
     stroke-dashoffset transition, so the dial looks continuous even
     though we sample discretely. (3s is the floor we can hit without
     making the host probe rate-limit itself.) */
  React.useEffect(() => {
    if (!isAdmin) return;
    let canc = false;
    const load = async () => {
      try {
        const [resR, conR, detR] = await Promise.all([
          fetch('/api/server/host-resources', { credentials: 'include' }),
          fetch('/api/server/connectivity', { credentials: 'include' }),
          fetch('/api/health/detailed', { credentials: 'include' }),
        ]);
        if (!canc) {
          if (resR.ok) setResources(await resR.json());
          if (conR.ok) setConn(await conR.json());
          if (detR.ok) {
            const det = await detR.json();
            setLatency(det.connectivity || null);
          }
        }
      } catch (_) {}
    };
    load();
    const id = setInterval(load, 3000);
    return () => { canc = true; clearInterval(id); };
  }, [isAdmin]);

  const us = health.uptime_seconds || health.uptime_s || 0;
  const uptime = `${Math.floor(us / 86400)}d ${Math.floor((us % 86400) / 3600)}h ${Math.floor((us % 3600) / 60)}m`;

  return (
    <div className="page">
      <PageHeader
        title={(window.appT||(s=>s))("Server")}
        meta={(window.appT||(s=>s))("Host resources, connectivity, scheduler, branding, and request logs")}
        right={<Refresh onClick={() => { try { window.dispatchEvent(new CustomEvent('status-data-refresh-requested')); } catch(_) {} }} />}
      />

      <GuardianPanel />

      {/* 1. Host resources — live donuts (CPU / memory / disk) */}
      <div className="card" style={{ marginBottom: 16 }}>
        <SectionHeader title={(window.appT||(s=>s))("Host resources")}
          tip={(window.appT||(s=>s))("Live CPU / memory / disk on the VPS hosting the bot. Sampled every 3s; donuts smoothly animate between samples.")} sub={(window.appT||(s=>s))("Live · polled every 3s — requires admin")} />
        {!resources && !isAdmin && (
          <Empty icon="·" title={(window.appT||(s=>s))("Admin access required")} subtitle={(window.appT||(s=>s))("Host resource metrics are visible to admins only.")} />
        )}
        {!resources && isAdmin && (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
            {[0,1,2,3].map(i => <Skel key={i} h={60} />)}
          </div>
        )}
        {resources && (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 16 }}>
            <ResourceCard
              label={(window.appT||(s=>s))("CPU")}
              pct={resources.cpu_percent}
              detail={`${resources.cpu_percent}%`}
            />
            <ResourceCard
              label={(window.appT||(s=>s))("Memory")}
              pct={resources.memory?.percent}
              detail={`${resources.memory?.used_mb?.toFixed(0)} / ${resources.memory?.total_mb?.toFixed(0)} MB`}
            />
            <ResourceCard
              label={(window.appT||(s=>s))("Disk")}
              pct={resources.disk?.percent}
              detail={`${resources.disk?.used_gb?.toFixed(1)} / ${resources.disk?.total_gb?.toFixed(1)} GB`}
            />
            <div style={{ padding: '12px 14px', background: 'var(--bg-elev-1)', border: '1px solid var(--line)', borderRadius: 'var(--r-md)' }}>
              <div className="eyebrow" style={{ fontSize: 11, marginBottom: 6 }}>{(window.appT||(s=>s))("Network (total)")}</div>
              <div style={{ display: 'flex', gap: 16 }}>
                <div>
                  <div className="muted" style={{ fontSize: 10, marginBottom: 2 }}>{(window.appT||(s=>s))("Sent")}</div>
                  <div className="mono" style={{ fontSize: 13 }}>{fmtBytes(resources.network?.bytes_sent)}</div>
                </div>
                <div>
                  <div className="muted" style={{ fontSize: 10, marginBottom: 2 }}>{(window.appT||(s=>s))("Recv")}</div>
                  <div className="mono" style={{ fontSize: 13 }}>{fmtBytes(resources.network?.bytes_recv)}</div>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>

      {/* 2. Connectivity — live service-health donuts */}
      <div className="card" style={{ marginBottom: 16 }}>
        <SectionHeader title={(window.appT||(s=>s))("Connectivity")}
          tip={(window.appT||(s=>s))("Health of the bot's outbound connections — Matrix homeserver, DB, AI providers, mail relay. Auto-refreshes every 3s.")} sub={(window.appT||(s=>s))("Live service health · polled every 3s")} />
        {!connectivity && isAdmin && <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12 }}>{[0,1,2,3].map(i => <Skel key={i} h={140} />)}</div>}
        {!connectivity && !isAdmin && <Empty icon="·" title={(window.appT||(s=>s))("Admin access required")} />}
        {connectivity && (
          <>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 12 }}>
              {[
                { label: (window.appT||(s=>s))('K9 sync'),     ok: !!connectivity.bot_synced,    detail: connectivity.bot_matrix_id || '—' },
                { label: (window.appT||(s=>s))('Database'),    ok: !!connectivity.db_ok,         detail: 'SQLite' },
                { label: (window.appT||(s=>s))('AI provider'), ok: !!connectivity.ai_provider_ok,detail: (window.appT||(s=>s))('Active') },
                { label: (window.appT||(s=>s))('Synapse'),     ok: !!connectivity.synapse_ok,    detail: connectivity.synapse_version || connectivity.homeserver || '—' },
              ].map(({ label, ok, detail }) => (
                <ServiceHealthDonut key={label} label={label} ok={ok} detail={detail} />
              ))}
            </div>
            {/* Latency metrics from /api/health/detailed */}
            {latency && (
              <div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
                {[
                  {
                    label: (window.appT||(s=>s))('DB roundtrip'),
                    value: latency.db_roundtrip_ms != null ? `${latency.db_roundtrip_ms} ms` : '—',
                  },
                  {
                    label: (window.appT||(s=>s))('Chat ping'),
                    value: latency.ping_chat_k9ms_ms != null ? `${latency.ping_chat_k9ms_ms} ms` : '— (' + (window.appT||(s=>s))('container') + ')',
                  },
                  {
                    label: (window.appT||(s=>s))('K9 sync lag'),
                    value: latency.matrix_sync_lag_s != null ? `${latency.matrix_sync_lag_s} s` : '—',
                  },
                ].map(({ label, value }) => (
                  <div key={label} style={{ padding: '8px 12px', background: 'var(--bg-elev-2)', border: '1px solid var(--line)', borderRadius: 'var(--r-sm)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                    <span style={{ fontSize: 11, color: 'var(--text-3)' }}>{label}</span>
                    <span className="mono" style={{ fontSize: 12 }}>{value}</span>
                  </div>
                ))}
              </div>
            )}
          </>
        )}
      </div>

      {/* 3. Scheduler */}
      <div className="card" style={{ marginBottom: 16 }}>
        <SectionHeader
          title={(window.appT||(s=>s))("Scheduler")}
          tip={(window.appT||(s=>s))("Status of every APScheduler cron job — next run, last run, missed runs. Useful for verifying a job actually fires.")}
          sub={(window.appLang&&window.appLang()==='ar') ? `${(health.scheduler_jobs || []).length} مهمة مُهيّأة` : `${(health.scheduler_jobs || []).length} jobs configured`}
          right={
            isAdmin && (
              <button className="btn btn-ghost" onClick={() => setRunModal(true)}>
                <Icon name="refresh" size={12} /> {(window.appT||(s=>s))("Run daily_recompute now")}
              </button>
            )
          }
        />
        {(health.scheduler_jobs || []).length === 0 && (
          <Empty icon="·" title={(window.appT||(s=>s))("No scheduler data")} subtitle={(window.appT||(s=>s))("Scheduler info loads from /api/server-health.")} />
        )}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 10 }}>
          {(health.scheduler_jobs || []).map(j => (
            <div key={j.name} style={{ padding: '10px 12px', background: 'var(--bg-elev-1)', border: '1px solid var(--line)', borderRadius: 'var(--r-md)' }}>
              <div style={{ fontSize: 13, fontWeight: 500, marginBottom: 2 }}>{j.name}</div>
              <div className="muted mono" style={{ fontSize: 11 }}>{j.cron}</div>
              <div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-2)' }}>
                {(window.appT||(s=>s))("Next:")} <span className="mono">{j.next || '—'}</span>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* 4. Monitoring & Alerts (admin-only) */}
      {isAdmin && (
        <div className="card" style={{ marginBottom: 16 }}>
          <SectionHeader title={(window.appT||(s=>s))("Monitoring & Alerts")}
          tip={(window.appT||(s=>s))("Email alert rules. When an enabled rule fires, the bot emails alert_email_recipients via SendGrid.")} sub={(window.appT||(s=>s))("Host probe every 60s — email on threshold breach")} />
          <MonitoringSection />
        </div>
      )}

      {/* 5. Branding (admin-only) */}
      {isAdmin && (
        <>
          <div className="card" style={{ marginBottom: 16 }}>
            <SectionHeader title={(window.appT||(s=>s))("Branding")}
          tip={(window.appT||(s=>s))("Org name, primary color, favicon, logo. Changes auto-reload across all open dashboards (#37) — no hard refresh needed.")} sub={(window.appT||(s=>s))("App name, accent color, login tagline, and favicon")} />
            <BrandingSection />
          </div>

          <div className="card" style={{marginBottom: 16}}>
            <SectionHeader title={'⚪ ' + (window.appT||(s=>s))("White-label")}
          tip={(window.appT||(s=>s))("Customize app_name, accent color, login background. Settings persist per-tenant.")} sub={(window.appT||(s=>s))("Brand name, color, slogan, support email, custom domain")} />
            <div style={{padding: 16}}><WhitelabelSection /></div>
          </div>

          <div className="card" style={{marginBottom: 16}}>
            <SectionHeader title={'📧 ' + (window.appT||(s=>s))("Email alerts")}
          tip={(window.appT||(s=>s))("SMTP/SendGrid config. Test button verifies credentials end-to-end before saving.")} sub={(window.appT||(s=>s))("SendGrid API or any SMTP provider — alerts when the bot logs an error")} />
            <div style={{padding: 16}}><EmailAlertsSection /></div>
          </div>

          {/* Bot logic moved to the Bot Behavior tab (next to Automations). */}

          <div className="card" style={{marginBottom: 16}}>
            <SectionHeader title={'🔑 ' + (window.appT||(s=>s))("Machine access (API tokens, webhooks, CSV)")}
          tip={(window.appT||(s=>s))("Give another system read access to this API without sharing a login: mint a scoped token, subscribe a signed webhook, or pull the CSV exports. Superadmin only.")} sub={(window.appT||(s=>s))("Tokens act as their owner and can never exceed that user's permissions")} />
            <div style={{padding: 16}}><MachineAccessSection /></div>
          </div>

          <div className="card" style={{marginBottom: 16}}>
            <SectionHeader title={'🛡️ ' + (window.appT||(s=>s))("IP red-flag detection")}
          tip={(window.appT||(s=>s))("Per-user IP-fingerprint detection. When a sign-in comes from a far-from-baseline IP, flag the audit row + DM admin. Tune samples / match_pct / throttle_hours here.")} sub={(window.appT||(s=>s))("Per-team thresholds for the suspicious-IP detector. Edit mode required.")} />
            <div style={{padding: 16}}><IpLearningSection /></div>
          </div>
        </>
      )}

      {/* WhatsApp section moved to the Providers tab so all messaging
          integrations (Slack + Microsoft Teams + WhatsApp) live in one
          place. The component is still defined in this file (exposed as
          window.WhatsAppIntegrationCard) and mounted by the Providers
          page. Kept the Server tab clear of messaging UI. */}

      {/* 7. Recent logs */}
      <div className="card">
        <SectionHeader title={(window.appT||(s=>s))("Request logs")}
          tip={(window.appT||(s=>s))("Last 1000 HTTP requests to the dashboard API. Filter by IP, status, path, method, user. Used to debug 4xx/5xx spikes.")} sub={(window.appT||(s=>s))("Authenticated login events and actions")} />
        <LogsSection />
      </div>

      {runModal && <RunNowModal onClose={() => setRunModal(false)} />}
    </div>
  );
}

/* ── WhatsAppSection ── */
function WhatsAppSection() {
  const [cfg, setCfg] = React.useState(null);
  const [form, setForm] = React.useState({ phone_number_id: '', business_id: '', access_token: '', template_name: 'support_escalation', verify_token: '', enabled: false });
  const [testPhone, setTestPhone] = React.useState('');
  const [saving, setSaving] = React.useState(false);
  const [testing, setTesting] = React.useState(false);
  const [testOk, setTestOk] = React.useState(null); // null | true | false
  const toast = useToast();

  const load = React.useCallback(() => {
    fetch('/api/settings/whatsapp', { credentials: 'include' })
      .then(r => r.ok ? r.json() : {})
      .then(j => {
        setCfg(j);
        setForm(f => ({
          phone_number_id: j.phone_number_id || '',
          business_id: j.business_id || '',
          access_token: '',  // never pre-fill token
          template_name: j.template_name || 'support_escalation',
          verify_token: '',  // write-only; show a "set" hint via cfg.verify_token_set
          enabled: !!j.enabled,
        }));
        setTestOk(j.last_test_ok || null);
      })
      .catch(() => setCfg({}));
  }, []);

  React.useEffect(() => { load(); }, [load]);

  const handleSave = async () => {
    setSaving(true);
    try {
      const r = await fetch('/api/settings/whatsapp', {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form),
      });
      if (r.ok) { toast.push({ msg: 'WhatsApp settings saved', kind: 'ok', icon: '✓' }); load(); }
      else { const e = await r.json().catch(() => ({})); toast.push({ msg: e.detail || 'Save failed', kind: 'err', icon: '!' }); }
    } catch (e) { toast.push({ msg: String(e), kind: 'err', icon: '!' }); }
    setSaving(false);
  };

  const handleTest = async () => {
    if (!testPhone.trim()) { toast.push({ msg: 'Enter a phone number to test', kind: 'warn', icon: '!' }); return; }
    setTesting(true);
    setTestOk(null);
    try {
      const r = await fetch('/api/whatsapp/test', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to_phone: testPhone.trim() }),
      });
      if (r.ok) { setTestOk(true); toast.push({ msg: 'Test message sent', kind: 'ok', icon: '✓' }); }
      else { setTestOk(false); const e = await r.json().catch(() => ({})); toast.push({ msg: e.detail || 'Send failed', kind: 'err', icon: '!' }); }
    } catch (e) { setTestOk(false); toast.push({ msg: String(e), kind: 'err', icon: '!' }); }
    setTesting(false);
  };

  if (!cfg) return <div style={{ padding: 12 }}><Skel h={40} /></div>;

  const inp = { className: 'inp', style: { width: '100%', marginBottom: 10 } };

  return (
    <div style={{ padding: '0 2px' }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 10 }}>
        <div>
          <label className="field-label">{(window.appT||(s=>s))("Phone Number ID")}</label>
          <input {...inp} placeholder="1234567890" value={form.phone_number_id}
            onChange={e => setForm(f => ({ ...f, phone_number_id: e.target.value }))} />
        </div>
        <div>
          <label className="field-label">{(window.appT||(s=>s))("WhatsApp Business ID")}</label>
          <input {...inp} placeholder="9876543210" value={form.business_id}
            onChange={e => setForm(f => ({ ...f, business_id: e.target.value }))} />
        </div>
      </div>
      <div style={{ marginBottom: 10 }}>
        <label className="field-label">{(window.appT||(s=>s))("Access Token")} {cfg.access_token_masked ? <span className="muted" style={{ fontWeight: 400 }}>({cfg.access_token_masked})</span> : null}</label>
        <input {...inp} type="password" placeholder={(window.appT||(s=>s))("Leave blank to keep existing")}
          value={form.access_token} onChange={e => setForm(f => ({ ...f, access_token: e.target.value }))} />
      </div>
      <div style={{ marginBottom: 10 }}>
        <label className="field-label">{(window.appT||(s=>s))("Template name")}</label>
        <input {...inp} value={form.template_name}
          onChange={e => setForm(f => ({ ...f, template_name: e.target.value }))} />
        <div className="muted" style={{ fontSize: 11, marginTop: -6, marginBottom: 8 }}>
          {(window.appT||(s=>s))("Must be a Meta-approved template. Default:")} <code>support_escalation</code>
        </div>
      </div>
      <div style={{ marginBottom: 10 }}>
        <label className="field-label">{(window.appT||(s=>s))("Inbound webhook verify token")} {cfg.verify_token_set ? <span className="muted" style={{ fontWeight: 400 }}>({(window.appT||(s=>s))("set")})</span> : null}</label>
        <input {...inp} type="password" placeholder={(window.appT||(s=>s))("Leave blank to keep existing")}
          value={form.verify_token} onChange={e => setForm(f => ({ ...f, verify_token: e.target.value }))} />
        <div className="muted" style={{ fontSize: 11, marginTop: -6, marginBottom: 8 }}>
          {(window.appT||(s=>s))("Paste this same token into Meta → WhatsApp → Configuration → Webhook. Callback URL:")} <code>{(typeof window!=='undefined'? window.location.origin : '')}/api/whatsapp/webhook</code>
        </div>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
        <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
          <input type="checkbox" checked={form.enabled} onChange={e => setForm(f => ({ ...f, enabled: e.target.checked }))} />
          <span style={{ fontSize: 13 }}>{(window.appT||(s=>s))("Enable WhatsApp escalation")}</span>
        </label>
        <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
          {testOk === true && <span style={{ color: 'var(--green)', fontSize: 12 }}>{(window.appT||(s=>s))("Last test: OK")}</span>}
          {testOk === false && <span style={{ color: 'var(--red)', fontSize: 12 }}>{(window.appT||(s=>s))("Last test: FAILED")}</span>}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
        <button className="btn-primary" onClick={handleSave} disabled={saving} style={{ minWidth: 110 }}>
          {saving ? (window.appT||(s=>s))('Saving...') : (window.appT||(s=>s))('Save settings')}
        </button>
        <input className="inp" style={{ flex: 1, minWidth: 160 }} placeholder={(window.appLang&&window.appLang()==='ar') ? '+15550001234 — إرسال تجريبي' : '+15550001234 — test send'}
          value={testPhone} onChange={e => setTestPhone(e.target.value)} />
        <button className="btn-secondary" onClick={handleTest} disabled={testing}>
          {testing ? (window.appT||(s=>s))('Sending...') : (window.appT||(s=>s))('Test send')}
        </button>
      </div>
      <div className="muted" style={{ fontSize: 11, marginTop: 10 }}>
        {(window.appT||(s=>s))("After 5 minutes of no K9 acknowledgement following a critical-keyword escalation, the bot will WhatsApp all on-call users who have a cell phone on file.")}
      </div>
    </div>
  );
}
// Re-expose so the Providers tab (above) can mount WhatsApp alongside
// Slack + Microsoft Teams without the Server tab having to render it.
window.WhatsAppIntegrationCard = WhatsAppSection;

/* ── MonitoringSection ── */
function MonitoringSection() {
  const [probe, setProbe] = React.useState(null);
  const [alerts, setAlerts] = React.useState([]);
  const [cfg, setCfg] = React.useState({ recipients: [], enabled: true });
  const [recipInput, setRecipInput] = React.useState('');
  const [saving, setSaving] = React.useState(false);
  const toast = useToast();

  const load = React.useCallback(async () => {
    try {
      const [pr, ar, cr] = await Promise.all([
        fetch('/api/server/probe/latest', { credentials: 'include' }),
        fetch('/api/server/alerts/recent?limit=20', { credentials: 'include' }),
        fetch('/api/settings/alerts', { credentials: 'include' }),
      ]);
      if (pr.ok) setProbe(await pr.json());
      if (ar.ok) setAlerts(await ar.json());
      if (cr.ok) {
        const c = await cr.json();
        setCfg(c);
        setRecipInput((c.recipients || []).join(', '));
      }
    } catch (_) {}
  }, []);

  React.useEffect(() => {
    load();
    const id = setInterval(load, 30000);
    return () => clearInterval(id);
  }, [load]);

  const handleSave = async () => {
    setSaving(true);
    const recipients = recipInput.split(',').map(s => s.trim()).filter(Boolean);
    try {
      const r = await fetch('/api/settings/alerts', {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ recipients, enabled: cfg.enabled }),
      });
      if (r.ok) {
        toast('Alert settings saved', 'ok');
        load();
      } else {
        toast('Save failed', 'err');
      }
    } catch (_) { toast('Save failed', 'err'); }
    setSaving(false);
  };

  const probeCols = probe ? [
    { label: (window.appT||(s=>s))('CPU'), val: probe.cpu_pct != null ? `${probe.cpu_pct}%` : '—', warn: probe.cpu_pct > 85 },
    { label: (window.appT||(s=>s))('Memory'), val: probe.mem_pct != null ? `${probe.mem_pct}%` : '—', warn: probe.mem_pct > 85 },
    { label: (window.appT||(s=>s))('Disk'), val: probe.disk_pct != null ? `${probe.disk_pct}%` : '—', warn: probe.disk_pct > 85 },
    { label: (window.appT||(s=>s))('K9 sync'), val: probe.matrix_sync_status || '—', warn: probe.matrix_sync_status !== 'synced' },
    { label: (window.appT||(s=>s))('DB'), val: probe.db_status || '—', warn: probe.db_status !== 'ok' },
    { label: (window.appT||(s=>s))('AI'), val: probe.ai_provider_status || '—', warn: false },
  ] : [];

  return (
    <div>
      {/* Latest probe */}
      {!probe && <Empty icon="·" title={(window.appT||(s=>s))("No probe data yet")} subtitle={(window.appT||(s=>s))("Probe runs every 60s.")} />}
      {probe && (
        <div style={{ marginBottom: 16 }}>
          <div className="muted" style={{ fontSize: 11, marginBottom: 8 }}>
            {(window.appT||(s=>s))("Last probe:")} <span className="mono">{probe.ts ? new Date(probe.ts + 'Z').toLocaleTimeString() : '—'}</span>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))', gap: 10 }}>
            {probeCols.map(c => (
              <div key={c.label} style={{ padding: '8px 12px', background: 'var(--bg-elev-1)', border: `1px solid ${c.warn ? 'var(--warn)' : 'var(--line)'}`, borderRadius: 'var(--r-md)' }}>
                <div style={{ fontSize: 10, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>{c.label}</div>
                <div style={{ fontSize: 13, fontWeight: 500, color: c.warn ? 'var(--warn)' : 'var(--text-1)' }}>{c.val}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Recent alerts */}
      <div style={{ marginBottom: 16 }}>
        <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8, color: 'var(--text-2)' }}>{(window.appT||(s=>s))("Recent Alerts")}</div>
        {alerts.length === 0 && <div className="muted" style={{ fontSize: 12 }}>{(window.appT||(s=>s))("No alerts fired.")}</div>}
        {alerts.length > 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {alerts.map(a => (
              <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '6px 10px', background: 'var(--bg-elev-1)', border: '1px solid var(--line)', borderRadius: 'var(--r-sm)', fontSize: 12 }}>
                <StatusPill kind={a.cleared_at ? 'good' : (a.severity === 'critical' ? 'bad' : 'warn')}>
                  {a.cleared_at ? (window.appT||(s=>s))('cleared') : (window.appT||(s=>s))(a.severity)}
                </StatusPill>
                <span style={{ fontWeight: 500 }}>{a.rule_name}</span>
                {a.value && <span className="muted mono">{a.value}</span>}
                <span className="muted" style={{ marginLeft: 'auto', fontSize: 10 }}>{a.ts ? new Date(a.ts + 'Z').toLocaleString() : ''}</span>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Recipients editor */}
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 10, flexWrap: 'wrap' }}>
        <div style={{ flex: '1 1 260px' }}>
          <label style={{ fontSize: 11, color: 'var(--text-3)', display: 'block', marginBottom: 4 }}>{(window.appT||(s=>s))("Alert recipients (comma-separated)")}</label>
          <input
            className="input"
            value={recipInput}
            onChange={e => setRecipInput(e.target.value)}
            placeholder="ops@example.com, admin@example.com"
            style={{ width: '100%', fontSize: 13 }}
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <label style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', userSelect: 'none' }}>
            <input type="checkbox" checked={cfg.enabled} onChange={e => setCfg(c => ({ ...c, enabled: e.target.checked }))} />
            {(window.appT||(s=>s))("Enabled")}
          </label>
          <button className="btn btn-primary" onClick={handleSave} disabled={saving} style={{ fontSize: 12 }}>
            {saving ? (window.appT||(s=>s))('Saving…') : (window.appT||(s=>s))('Save')}
          </button>
        </div>
      </div>
    </div>
  );
}

/* ── Resource card — live donut ──
   Drop-in replacement for the previous progress-bar version. SVG donut
   that smoothly animates as the percentage ticks live (the parent polls
   every few seconds; the stroke-dashoffset transition handles the visual
   interpolation between samples so the dial looks continuous). */
function ResourceCard({ label, pct, detail, accent }) {
  const v = Math.max(0, Math.min(100, Number(pct) || 0));
  const color = accent || (v > 85 ? 'var(--err)' : v > 65 ? 'var(--warn)' : 'var(--ok)');
  const SIZE = 96, STROKE = 10;
  const r = (SIZE - STROKE) / 2;
  const c = 2 * Math.PI * r;
  const offset = c * (1 - v / 100);
  return (
    <div style={{
      padding: '14px 14px 12px',
      background: 'var(--bg-elev-1)', border: '1px solid var(--line)',
      borderRadius: 'var(--r-md)',
      display:'flex', flexDirection:'column', alignItems:'center', gap: 6,
    }}>
      <div className="eyebrow" style={{ fontSize: 11, alignSelf:'flex-start' }}>{label}</div>
      <div style={{ position:'relative', width: SIZE, height: SIZE }}>
        <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`}>
          <circle cx={SIZE/2} cy={SIZE/2} r={r}
            stroke="var(--bg-elev-2)" strokeWidth={STROKE} fill="none" />
          <circle cx={SIZE/2} cy={SIZE/2} r={r}
            stroke={color} strokeWidth={STROKE} fill="none"
            strokeDasharray={c} strokeDashoffset={offset}
            strokeLinecap="round"
            transform={`rotate(-90 ${SIZE/2} ${SIZE/2})`}
            style={{ transition: 'stroke-dashoffset .6s cubic-bezier(.4,0,.2,1), stroke .4s' }} />
        </svg>
        <div style={{
          position:'absolute', inset: 0,
          display:'flex', alignItems:'center', justifyContent:'center',
          flexDirection:'column',
          fontFamily:'var(--font-mono)',
        }}>
          <div style={{ fontSize: 18, fontWeight: 600, color }}>{v.toFixed(1)}%</div>
        </div>
      </div>
      <div className="muted mono" style={{ fontSize: 11, textAlign:'center' }}>{detail || ' '}</div>
    </div>
  );
}

/* ── Service health donut for the Connectivity row ──
   Tiny version of ResourceCard but binary (ok / not ok) — fills the
   ring fully when ok, empty when not, with a coloured center dot. */
function ServiceHealthDonut({ label, ok, detail }) {
  const SIZE = 80, STROKE = 9;
  const r = (SIZE - STROKE) / 2;
  const c = 2 * Math.PI * r;
  const color = ok ? 'var(--ok)' : 'var(--err)';
  return (
    <div style={{
      padding: '12px 12px 10px',
      background: 'var(--bg-elev-1)', border: '1px solid var(--line)',
      borderRadius: 'var(--r-md)',
      display:'flex', flexDirection:'column', alignItems:'center', gap: 4,
    }}>
      <div className="eyebrow" style={{ fontSize: 10.5, alignSelf:'flex-start' }}>{label}</div>
      <div style={{ position:'relative', width: SIZE, height: SIZE }}>
        <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`}>
          <circle cx={SIZE/2} cy={SIZE/2} r={r}
            stroke="var(--bg-elev-2)" strokeWidth={STROKE} fill="none" />
          <circle cx={SIZE/2} cy={SIZE/2} r={r}
            stroke={color} strokeWidth={STROKE} fill="none"
            strokeDasharray={c} strokeDashoffset={ok ? 0 : c * 0.85}
            strokeLinecap="round"
            transform={`rotate(-90 ${SIZE/2} ${SIZE/2})`}
            style={{ transition: 'stroke-dashoffset .5s cubic-bezier(.4,0,.2,1)' }} />
        </svg>
        <div style={{
          position:'absolute', inset: 0,
          display:'flex', alignItems:'center', justifyContent:'center',
        }}>
          <div style={{
            width: 14, height: 14, borderRadius:'50%',
            background: color,
            boxShadow: `0 0 12px -2px ${ok ? '#10b98166' : '#dc262666'}`,
          }} />
        </div>
      </div>
      <div style={{ fontSize: 11.5, fontWeight: 500, color, marginTop: 2 }}>
        {ok ? (window.appT||(s=>s))('OK') : (window.appT||(s=>s))('Down')}
      </div>
      {detail && (
        <div className="muted mono" style={{ fontSize: 10, textAlign:'center',
          maxWidth: 100, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}
          title={detail}>{detail}</div>
      )}
    </div>
  );
}

window.AuditPage = AuditPage;
window.MessagesPage = MessagesPage;
window.ProvidersPage = ProvidersPage;
window.ServerPage = ServerPage;
// Exposed so the Bot Behavior tab can render it next to Automations.
window.BotLogicSection = BotLogicSection;
