// Notifications view — providers, rules, recipients, test-send, incidents, log.
// All mutations go through admin auth (window.adminFetch). All hooks are
// declared above any early-return so hook order stays stable across
// the "auth needed" → "signed in" transition (this has crashed admin.jsx
// twice when the rule was violated).

const NOTIF_INPUT = {
  width: '100%', padding: '5px 9px', background: 'var(--surface)',
  border: '1px solid var(--border-2)', borderRadius: 2,
  color: 'var(--text)', fontFamily: 'var(--mono)', fontSize: 12, outline: 'none',
};

function SecretInput({ name, label, status, onSave, onClear }) {
  const [val, setVal] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const configured = status?.configured;
  const envOverride = status?.env;

  async function save() {
    if (!val.trim()) return;
    setBusy(true); setErr(null);
    try { await onSave(name, val); setVal(''); }
    catch (e) { setErr(e.message || 'save failed'); }
    finally { setBusy(false); }
  }
  async function clear() {
    setBusy(true); setErr(null);
    try { await onClear(name); }
    catch (e) { setErr(e.message || 'clear failed'); }
    finally { setBusy(false); }
  }

  const dirty = val.trim().length > 0;
  return (
    <div className="tweak-row">
      <label style={{display:'flex', alignItems:'center', gap:6}}>
        <span>{label}</span>
        {configured
          ? <span className="chip ok" style={{fontSize: 9}}>set{envOverride ? ' · env' : ' · file'}</span>
          : <span className="chip off" style={{fontSize: 9}}>missing</span>}
        {dirty && <span className="chip warn" style={{fontSize: 9}}>unsaved — click save</span>}
      </label>
      <div style={{ display: 'flex', gap: 6 }}>
        <input style={{ ...NOTIF_INPUT, borderColor: dirty ? 'var(--warn)' : 'var(--border-2)' }}
               type="password" autoComplete="off"
               placeholder={configured ? '••••••••  (leave blank to keep)' : 'paste value'}
               value={val} onChange={e => setVal(e.target.value)}
               onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); save(); } }} />
        <button type="button" className="btn" onClick={save} disabled={busy || !val.trim()}
                style={dirty ? {color:'var(--accent)', borderColor:'var(--accent-dim)', background:'var(--accent-bg)'} : {}}>
          {busy ? '…' : 'save'}
        </button>
        {configured && status?.file && (
          <button type="button" className="btn" onClick={clear} disabled={busy}
            style={{color: 'var(--crit)', borderColor: 'oklch(0.66 0.22 27 / 0.4)'}}>
            clear
          </button>
        )}
      </div>
      {err && <div style={{color:'var(--crit)', fontSize: 10}}>{err}</div>}
    </div>
  );
}

function ProvidersCard({ data, onConfigSave, onSecretSave, onSecretClear }) {
  const [emailEnabled, setEmailEnabled] = React.useState(data?.providers?.email?.enabled || false);
  const [emailFrom, setEmailFrom] = React.useState(data?.providers?.email?.from || '');
  const [smsEnabled, setSmsEnabled] = React.useState(data?.providers?.sms?.enabled || false);
  const [smsFrom, setSmsFrom] = React.useState(data?.providers?.sms?.fromNumber || '');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    setEmailEnabled(data?.providers?.email?.enabled || false);
    setEmailFrom(data?.providers?.email?.from || '');
    setSmsEnabled(data?.providers?.sms?.enabled || false);
    setSmsFrom(data?.providers?.sms?.fromNumber || '');
  }, [data?.providers?.email?.enabled, data?.providers?.email?.from,
      data?.providers?.sms?.enabled, data?.providers?.sms?.fromNumber]);

  async function saveConfig() {
    setBusy(true); setErr(null);
    try {
      await onConfigSave({
        providers: {
          email: { enabled: emailEnabled, from: emailFrom },
          sms:   { enabled: smsEnabled,   fromNumber: smsFrom },
        }
      });
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  }

  const ps = data?.providersStatus || {};
  return (
    <div className="card" style={{padding:0}}>
      <div className="card-head">
        <h3>Providers</h3>
        <span className="spacer" style={{flex:1}} />
        <span className="sub">Resend for email · Twilio for SMS</span>
      </div>
      <div className="card-body" style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 16}}>
        <div style={{display:'flex', flexDirection:'column', gap: 10}}>
          <div style={{display:'flex', alignItems:'center', gap: 8}}>
            <span className="chip accent">email · resend</span>
            <span className="spacer" style={{flex:1}} />
            <label style={{fontSize: 10, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
              <input type="checkbox" checked={emailEnabled} onChange={e => setEmailEnabled(e.target.checked)} />
              enabled
            </label>
            {ps.email?.ready
              ? <span className="chip ok" style={{fontSize: 9}}>ready</span>
              : <span className="chip crit" style={{fontSize: 9}}>not ready</span>}
          </div>
          <div className="tweak-row">
            <label>from address</label>
            <input style={NOTIF_INPUT} placeholder="alerts@yourdomain.com" value={emailFrom}
                   onChange={e => setEmailFrom(e.target.value)} />
          </div>
          <SecretInput name="resend_api_key" label="Resend API key"
            status={ps.email?.apiKey} onSave={onSecretSave} onClear={onSecretClear} />
        </div>
        <div style={{display:'flex', flexDirection:'column', gap: 10}}>
          <div style={{display:'flex', alignItems:'center', gap: 8}}>
            <span className="chip accent">sms · twilio</span>
            <span className="spacer" style={{flex:1}} />
            <label style={{fontSize: 10, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
              <input type="checkbox" checked={smsEnabled} onChange={e => setSmsEnabled(e.target.checked)} />
              enabled
            </label>
            {ps.sms?.ready
              ? <span className="chip ok" style={{fontSize: 9}}>ready</span>
              : <span className="chip crit" style={{fontSize: 9}}>not ready</span>}
          </div>
          <div className="tweak-row">
            <label>from number (E.164)</label>
            <input style={NOTIF_INPUT} placeholder="+359..." value={smsFrom}
                   onChange={e => setSmsFrom(e.target.value)} />
          </div>
          <SecretInput name="twilio_account_sid" label="Twilio Account SID"
            status={ps.sms?.sid} onSave={onSecretSave} onClear={onSecretClear} />
          <SecretInput name="twilio_auth_token" label="Twilio Auth Token"
            status={ps.sms?.token} onSave={onSecretSave} onClear={onSecretClear} />
        </div>
      </div>
      <div style={{padding: '0 12px 12px', display:'flex', gap: 8, alignItems:'center'}}>
        <span style={{fontSize: 10, color:'var(--text-mute)'}}>
          Env vars override the values saved via UI. Files live in <code>backend/</code> with mode 0600.
        </span>
        <span className="spacer" style={{flex:1}} />
        {err && <span style={{color:'var(--crit)', fontSize: 11}}>{err}</span>}
        <button type="button" className="btn" onClick={saveConfig} disabled={busy}
          style={{color:'var(--accent)', borderColor:'var(--accent-dim)', background:'var(--accent-bg)'}}>
          {busy ? 'saving…' : 'save providers'}
        </button>
      </div>
    </div>
  );
}

function RulesCard({ data, onConfigSave }) {
  const r = data?.rules || {};
  const [form, setForm] = React.useState(r);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  React.useEffect(() => { setForm(r); }, [JSON.stringify(r)]);

  function set(k, v) { setForm(f => ({ ...f, [k]: v })); }
  function setQH(k, v) { setForm(f => ({ ...f, quietHours: { ...f.quietHours, [k]: v } })); }

  async function save() {
    setBusy(true); setErr(null);
    try { await onConfigSave({ rules: form }); }
    catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  }

  const num = (k, label, hint) => (
    <div className="tweak-row">
      <label>{label}{hint && <span style={{textTransform:'none', color:'var(--text-mute)'}}> · {hint}</span>}</label>
      <input style={NOTIF_INPUT} type="number" min="0" value={form[k] ?? ''}
             onChange={e => set(k, Number(e.target.value))} />
    </div>
  );

  return (
    <div className="card" style={{padding:0}}>
      <div className="card-head">
        <h3>Rules</h3>
        <span className="spacer" style={{flex:1}} />
        <span className="sub">how the engine decides when and how much to send</span>
      </div>
      <div className="card-body" style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap: 12}}>
        {num('consecutiveFailures', 'consecutive failures', 'debounce flaps')}
        {num('cooldownMinutes', 'cooldown (min)', 'between notifications per incident')}
        {num('escalateAfterMinutes', 'escalate after (min)', 'email → email+SMS')}
        {num('reminderIntervalMinutes', 'reminder interval (min)', 'while still down')}
        {num('maxEmailsPerHour', 'max emails / hour', 'global rate cap')}
        {num('maxSmsPerDay', 'max SMS / day', 'global rate cap')}
        <div className="tweak-row">
          <label>recovery notice</label>
          <label style={{fontSize: 11, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
            <input type="checkbox" checked={!!form.notifyOnRecovery}
                   onChange={e => set('notifyOnRecovery', e.target.checked)} />
            notify when the node recovers
          </label>
        </div>
        <div className="tweak-row">
          <label>quiet hours</label>
          <label style={{fontSize: 11, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
            <input type="checkbox" checked={!!form.quietHours?.enabled}
                   onChange={e => setQH('enabled', e.target.checked)} />
            silence non-crit reminders
          </label>
        </div>
        <div className="tweak-row">
          <label>quiet start / end (h)</label>
          <div style={{display:'flex', gap:6}}>
            <input style={NOTIF_INPUT} type="number" min="0" max="23" value={form.quietHours?.startHour ?? 22}
                   onChange={e => setQH('startHour', Number(e.target.value))} />
            <input style={NOTIF_INPUT} type="number" min="0" max="23" value={form.quietHours?.endHour ?? 7}
                   onChange={e => setQH('endHour', Number(e.target.value))} />
          </div>
        </div>
      </div>
      <div style={{padding: '0 12px 10px', fontSize: 10, letterSpacing: '0.08em',
                    textTransform: 'uppercase', color: 'var(--text-faint)', marginTop: 6}}>
        Anomaly detection (statistical, no ML)
      </div>
      <div className="card-body" style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap: 12, paddingTop: 0}}>
        <div className="tweak-row">
          <label>anomaly detection</label>
          <label style={{fontSize: 11, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
            <input type="checkbox" checked={!!form.anomalyEnabled}
                   onChange={e => set('anomalyEnabled', e.target.checked)} />
            flag nodes whose latency is Nσ above the 7d baseline for the current hour
          </label>
        </div>
        <div className="tweak-row">
          <label>also send email on anomaly</label>
          <label style={{fontSize: 11, color:'var(--text-dim)', display:'flex', gap:4, alignItems:'center'}}>
            <input type="checkbox" checked={!!form.anomalyNotify}
                   onChange={e => set('anomalyNotify', e.target.checked)}
                   disabled={!form.anomalyEnabled} />
            (only if detection above is on)
          </label>
        </div>
        <div className="tweak-row">
          <label>sigma threshold</label>
          <input style={NOTIF_INPUT} type="number" min="1" step="0.5"
                 value={form.anomalySigma ?? 3}
                 onChange={e => set('anomalySigma', Number(e.target.value))} />
        </div>
        <div className="tweak-row">
          <label>min baseline samples</label>
          <input style={NOTIF_INPUT} type="number" min="2"
                 value={form.anomalyMinSamples ?? 20}
                 onChange={e => set('anomalyMinSamples', Number(e.target.value))} />
        </div>
        <div className="tweak-row">
          <label>consec samples needed</label>
          <input style={NOTIF_INPUT} type="number" min="1"
                 value={form.anomalyConsec ?? 2}
                 onChange={e => set('anomalyConsec', Number(e.target.value))} />
        </div>
        <div style={{fontSize: 10, color: 'var(--text-mute)', alignSelf: 'end'}}>
          Baseline uses rolling 7d samples at the current hour-of-day (UTC).
        </div>
      </div>
      <div style={{padding:'0 12px 12px', display:'flex', alignItems:'center', gap: 8}}>
        <span style={{fontSize: 10, color:'var(--text-mute)'}}>
          Trigger statuses: <code>{(form.triggerStatuses || []).join(', ')}</code>
        </span>
        <span className="spacer" style={{flex:1}} />
        {err && <span style={{color:'var(--crit)', fontSize: 11}}>{err}</span>}
        <button className="btn" onClick={save} disabled={busy}
          style={{color:'var(--accent)', borderColor:'var(--accent-dim)', background:'var(--accent-bg)'}}>
          {busy ? 'saving…' : 'save rules'}
        </button>
      </div>
    </div>
  );
}

function RecipientsCard({ data, groups, onAdd, onPatch, onDelete }) {
  const [form, setForm] = React.useState({ name:'', email:'', phone:'', channels:['email'], groups:['*'], enabled:true });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [editing, setEditing] = React.useState(null); // recipient being edited inline
  const [confirmDelete, setConfirmDelete] = React.useState(null);
  React.useEffect(() => {
    if (!confirmDelete) return;
    const t = setTimeout(() => setConfirmDelete(null), 4000);
    return () => clearTimeout(t);
  }, [confirmDelete]);

  const recipients = data?.recipients || [];

  function toggleList(list, item) {
    return list.includes(item) ? list.filter(x => x !== item) : [...list, item];
  }

  async function submit(e) {
    e.preventDefault(); setBusy(true); setErr(null);
    try {
      await onAdd(form);
      setForm({ name:'', email:'', phone:'', channels:['email'], groups:['*'], enabled:true });
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  }

  return (
    <div className="card" style={{padding:0}}>
      <div className="card-head">
        <h3>Recipients</h3>
        <span className="sub">{recipients.length} configured</span>
      </div>
      <form onSubmit={submit} className="card-body"
            style={{display:'grid', gridTemplateColumns:'1fr 1.4fr 1.2fr 1fr 1.2fr auto', gap: 10, alignItems:'end'}}>
        <div className="tweak-row">
          <label>name</label>
          <input style={NOTIF_INPUT} required value={form.name}
                 onChange={e => setForm({ ...form, name: e.target.value })} />
        </div>
        <div className="tweak-row">
          <label>email</label>
          <input style={NOTIF_INPUT} type="email" value={form.email}
                 onChange={e => setForm({ ...form, email: e.target.value })} />
        </div>
        <div className="tweak-row">
          <label>phone</label>
          <input style={NOTIF_INPUT} placeholder="+359..." value={form.phone}
                 onChange={e => setForm({ ...form, phone: e.target.value })} />
        </div>
        <div className="tweak-row">
          <label>channels</label>
          <div className="seg">
            <button type="button" className={form.channels.includes('email') ? 'on' : ''}
                    onClick={() => setForm({ ...form, channels: toggleList(form.channels, 'email') })}>email</button>
            <button type="button" className={form.channels.includes('sms') ? 'on' : ''}
                    onClick={() => setForm({ ...form, channels: toggleList(form.channels, 'sms') })}>sms</button>
          </div>
        </div>
        <div className="tweak-row">
          <label>groups</label>
          <div className="seg" style={{flexWrap:'wrap'}}>
            <button type="button" className={form.groups.includes('*') ? 'on' : ''}
                    onClick={() => setForm({ ...form, groups: form.groups.includes('*') ? [] : ['*'] })}>all</button>
            {(groups || []).map(g => (
              <button key={g} type="button"
                      className={form.groups.includes(g) ? 'on' : ''}
                      onClick={() => {
                        const without = form.groups.filter(x => x !== '*' && x !== g);
                        setForm({ ...form, groups: form.groups.includes(g) ? without : [...without, g] });
                      }}>{g}</button>
            ))}
          </div>
        </div>
        <button type="submit" className="btn" disabled={busy || !form.name || (!form.email && !form.phone)}
          style={{color:'var(--accent)', borderColor:'var(--accent-dim)', background:'var(--accent-bg)', padding:'6px 14px'}}>
          {busy ? 'adding…' : 'add'}
        </button>
      </form>
      {err && <div style={{padding:'0 12px 8px', color:'var(--crit)', fontSize:11}}>{err}</div>}
      <div style={{overflow:'auto'}}>
        <table className="tbl">
          <thead>
            <tr>
              <th>name</th><th>email</th><th>phone</th><th>channels</th><th>groups</th>
              <th>status</th><th style={{textAlign:'right'}}>actions</th>
            </tr>
          </thead>
          <tbody>
            {recipients.map(r => (
              <tr key={r.id}>
                <td style={{fontWeight:500}}>{r.name}</td>
                <td className="mute">{r.email || <span style={{color:'var(--text-mute)'}}>—</span>}</td>
                <td className="mute">{r.phone || <span style={{color:'var(--text-mute)'}}>—</span>}</td>
                <td>{(r.channels || []).map(c => <span key={c} className="chip accent" style={{marginRight:4}}>{c}</span>)}</td>
                <td>{(r.groups || ['*']).map(g => <span key={g} className="chip" style={{marginRight:4, background:'var(--surface-3)', color:'var(--text-dim)'}}>{g}</span>)}</td>
                <td>
                  <span className={`chip ${r.enabled ? 'ok' : 'off'}`}>{r.enabled ? 'enabled' : 'disabled'}</span>
                </td>
                <td style={{textAlign:'right'}}>
                  <div style={{display:'inline-flex', gap:4}}>
                    <button className="btn" onClick={() => onPatch(r.id, { enabled: !r.enabled })}>
                      {r.enabled ? 'disable' : 'enable'}
                    </button>
                    {confirmDelete === r.id ? (
                      <button className="btn" onClick={() => { setConfirmDelete(null); onDelete(r.id); }}
                        style={{color:'var(--crit)', borderColor:'oklch(0.66 0.22 27 / 0.5)', background:'var(--crit-bg)'}}>
                        confirm?
                      </button>
                    ) : (
                      <button className="btn" onClick={() => setConfirmDelete(r.id)}>delete</button>
                    )}
                  </div>
                </td>
              </tr>
            ))}
            {recipients.length === 0 && (
              <tr><td colSpan="7" style={{padding: 20, textAlign:'center', color:'var(--text-mute)'}}>
                No recipients yet — add one above to start receiving alerts.
              </td></tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function TestSendCard({ onSend, providersStatus }) {
  const [channel, setChannel] = React.useState('email');
  const [to, setTo] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState(null);

  const ready = channel === 'email' ? providersStatus?.email?.ready : providersStatus?.sms?.ready;

  async function send(e) {
    e.preventDefault(); setBusy(true); setResult(null);
    try {
      const r = await onSend(channel, to.trim());
      setResult(r);
    } catch (e2) {
      setResult({ ok: false, error: e2.message });
    } finally { setBusy(false); }
  }

  return (
    <form onSubmit={send} className="card" style={{padding:0}}>
      <div className="card-head">
        <h3>Test send</h3>
        <span className="sub">counts against rate limits</span>
      </div>
      <div className="card-body" style={{display:'grid', gridTemplateColumns:'auto 1fr auto', gap: 10, alignItems:'end'}}>
        <div className="tweak-row">
          <label>channel</label>
          <div className="seg">
            <button type="button" className={channel === 'email' ? 'on' : ''} onClick={() => setChannel('email')}>email</button>
            <button type="button" className={channel === 'sms' ? 'on' : ''} onClick={() => setChannel('sms')}>sms</button>
          </div>
        </div>
        <div className="tweak-row">
          <label>to</label>
          <input style={NOTIF_INPUT} required placeholder={channel === 'email' ? 'you@example.com' : '+359...'}
                 value={to} onChange={e => setTo(e.target.value)} />
        </div>
        <button type="submit" className="btn" disabled={busy || !to.trim() || !ready}
          title={!ready ? `${channel} provider is not ready — save API key and enable it first` : ''}
          style={{color:'var(--accent)', borderColor:'var(--accent-dim)', background:'var(--accent-bg)', padding:'6px 14px'}}>
          {busy ? 'sending…' : 'send test'}
        </button>
      </div>
      {!ready && (
        <div style={{padding:'0 12px 8px', fontSize: 11, color:'var(--warn)'}}>
          ⚠ {channel.toUpperCase()} provider not ready. Check: "enabled" toggle, "from" address, and saved API key above.
        </div>
      )}
      {result && (
        <div style={{padding:'0 12px 10px', fontSize: 11,
                     color: result.ok ? 'var(--ok)' : 'var(--crit)'}}>
          {result.ok ? '✓ sent' : `✗ ${result.error}${result.retryable ? ' (retryable)' : ''}`}
        </div>
      )}
    </form>
  );
}

function IncidentsCard({ incidents, nodes, onAck }) {
  const byId = React.useMemo(() => {
    const m = {}; for (const n of nodes || []) m[n.id] = n; return m;
  }, [nodes]);

  if (!incidents || incidents.length === 0) {
    return (
      <div className="card" style={{padding:0}}>
        <div className="card-head"><h3>Active incidents</h3><span className="sub">0</span></div>
        <div className="card-body" style={{color:'var(--text-mute)', fontSize: 12}}>No active incidents — all quiet.</div>
      </div>
    );
  }

  return (
    <div className="card" style={{padding:0}}>
      <div className="card-head"><h3>Active incidents</h3><span className="sub">{incidents.length}</span></div>
      <div style={{overflow:'auto'}}>
        <table className="tbl">
          <thead>
            <tr><th>node</th><th>status</th><th>since</th><th>level</th><th>last notified</th><th style={{textAlign:'right'}}>actions</th></tr>
          </thead>
          <tbody>
            {incidents.map(i => {
              const n = byId[i.nodeId] || {};
              return (
                <tr key={i.nodeId}>
                  <td style={{fontWeight:500}}>{n.displayName || n.hostname || i.nodeId}</td>
                  <td><span className={`chip ${i.lastStatus}`}>{i.lastStatus}</span></td>
                  <td className="mute">{window.fmtRel(i.startedAt)}</td>
                  <td>
                    {i.level === 0 && <span className="chip off">tracking</span>}
                    {i.level === 1 && <span className="chip accent">email</span>}
                    {i.level === 2 && <span className="chip crit">email + sms</span>}
                    {i.acked && <span className="chip warn" style={{marginLeft: 4}}>ack</span>}
                    {i.snoozedUntil && <span className="chip warn" style={{marginLeft: 4}}>snoozed</span>}
                  </td>
                  <td className="mute">{i.lastNotifiedAt ? window.fmtRel(i.lastNotifiedAt) : '—'}</td>
                  <td style={{textAlign:'right'}}>
                    <div style={{display:'inline-flex', gap:4}}>
                      {!i.acked && <button className="btn" onClick={() => onAck(i.nodeId, null)}>ack</button>}
                      <button className="btn" onClick={() => onAck(i.nodeId, 60)}>snooze 1h</button>
                      <button className="btn" onClick={() => onAck(i.nodeId, 240)}>4h</button>
                      <button className="btn" onClick={() => onAck(i.nodeId, 1440)}>24h</button>
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function LogCard({ entries }) {
  return (
    <div className="card" style={{padding:0}}>
      <div className="card-head">
        <h3>Recent sends</h3>
        <span className="sub">{(entries || []).length} entries · newest first</span>
      </div>
      <div style={{overflow:'auto', maxHeight: 320}}>
        <table className="tbl">
          <thead>
            <tr><th>when</th><th>channel</th><th>to</th><th>node</th><th>subject</th><th>result</th></tr>
          </thead>
          <tbody>
            {(entries || []).map((e, i) => (
              <tr key={i}>
                <td className="mute">{window.fmtRel(e.ts)}</td>
                <td>{e.channel}</td>
                <td className="mute">{e.to || '—'}</td>
                <td className="mute">{e.nodeId || (e.test ? '(test)' : '—')}</td>
                <td className="mute" style={{maxWidth:240, overflow:'hidden', textOverflow:'ellipsis'}}>{e.subject}</td>
                <td>{e.ok
                  ? <span className="chip ok">ok</span>
                  : <span className={`chip ${e.retryable ? 'warn' : 'crit'}`}>{e.retryable ? 'retryable' : 'failed'}</span>}
                </td>
              </tr>
            ))}
            {(entries || []).length === 0 && (
              <tr><td colSpan="6" style={{padding: 20, textAlign:'center', color:'var(--text-mute)'}}>No sends yet.</td></tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function Notifications({ runtimeNodes }) {
  const auth = window.useAdminAuth();
  const [data, setData] = React.useState(null);       // /api/admin/notifications payload
  const [incidents, setIncidents] = React.useState([]);
  const [log, setLog] = React.useState([]);
  const [loadErr, setLoadErr] = React.useState(null);

  const groups = React.useMemo(
    () => (window.deriveGroups ? window.deriveGroups(runtimeNodes || []) : []),
    [runtimeNodes]
  );

  async function loadAll() {
    setLoadErr(null);
    try {
      const [cfg, inc, lg] = await Promise.all([
        window.adminFetch('/api/admin/notifications'),
        window.adminFetch('/api/admin/notifications/incidents'),
        window.adminFetch('/api/admin/notifications/log?limit=100'),
      ]);
      setData(cfg); setIncidents(inc); setLog(lg);
    } catch (e) {
      if (e.message === 'unauthorized') { setData(null); }
      else setLoadErr(e.message);
    }
  }

  React.useEffect(() => { if (auth.authed) loadAll(); else setData(null); }, [auth.authed]);
  React.useEffect(() => {
    if (!auth.authed) return;
    const t = setInterval(() => {
      window.adminFetch('/api/admin/notifications/incidents').then(setIncidents).catch(()=>{});
      window.adminFetch('/api/admin/notifications/log?limit=100').then(setLog).catch(()=>{});
    }, 15000);
    return () => clearInterval(t);
  }, [auth.authed]);

  async function saveConfig(patch) {
    await window.adminFetch('/api/admin/notifications/config', { method:'PUT', body: JSON.stringify(patch) });
    await loadAll();
  }
  async function saveSecret(name, value) {
    await window.adminFetch('/api/admin/notifications/secrets', {
      method: 'PUT', body: JSON.stringify({ key: name, value }),
    });
    await loadAll();
  }
  async function clearSecret(name) {
    await window.adminFetch(`/api/admin/notifications/secrets/${name}`, { method: 'DELETE' });
    await loadAll();
  }
  async function addRecipient(patch) {
    await window.adminFetch('/api/admin/notifications/recipients', {
      method: 'POST', body: JSON.stringify(patch),
    });
    await loadAll();
  }
  async function patchRecipient(rid, patch) {
    await window.adminFetch(`/api/admin/notifications/recipients/${rid}`, {
      method: 'PATCH', body: JSON.stringify(patch),
    });
    await loadAll();
  }
  async function deleteRecipient(rid) {
    await window.adminFetch(`/api/admin/notifications/recipients/${rid}`, { method: 'DELETE' });
    await loadAll();
  }
  async function testSend(channel, to) {
    return await window.adminFetch('/api/admin/notifications/test', {
      method: 'POST', body: JSON.stringify({ channel, to }),
    });
  }
  async function ackIncident(node_id, snoozeMinutes) {
    const body = snoozeMinutes ? JSON.stringify({ snoozeMinutes }) : null;
    await window.adminFetch(`/api/admin/notifications/incidents/${node_id}/ack`, {
      method: 'POST', body,
    });
    const inc = await window.adminFetch('/api/admin/notifications/incidents');
    setIncidents(inc);
  }

  // Early returns must come AFTER all hooks above.
  if (!auth.authed) {
    return <div style={{ position:'relative', minHeight: 300 }}><window.AdminAuthPrompt /></div>;
  }
  if (loadErr) return <div style={{padding:20, color:'var(--crit)'}}>Failed to load: {loadErr}</div>;
  if (!data) return <div style={{padding:20, color:'var(--text-mute)'}}>Loading…</div>;

  return (
    <div style={{padding: 12, display:'flex', flexDirection:'column', gap: 12}}>
      <ProvidersCard data={data} onConfigSave={saveConfig}
                     onSecretSave={saveSecret} onSecretClear={clearSecret} />
      <RulesCard data={data} onConfigSave={saveConfig} />
      <div style={{display:'grid', gridTemplateColumns:'2fr 1fr', gap: 12}}>
        <IncidentsCard incidents={incidents} nodes={runtimeNodes} onAck={ackIncident} />
        <TestSendCard onSend={testSend} providersStatus={data.providersStatus} />
      </div>
      <RecipientsCard data={data} groups={groups}
                      onAdd={addRecipient} onPatch={patchRecipient} onDelete={deleteRecipient} />
      <LogCard entries={log} />
    </div>
  );
}

Object.assign(window, { Notifications });
