// admin.jsx — IT 관리 콘솔 (터미널 풍)

const { useState, useEffect, useMemo, useRef } = React;

// ─── Fetch helper ──────────────────────────────────
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ?? '';

async function api(path, { method = 'GET', body } = {}) {
  const res = await fetch(path, {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...(method !== 'GET' ? { 'X-CSRF-TOKEN': csrfToken } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    const error = new Error(err.message ?? `${method} ${path} → ${res.status}`);
    error.status = res.status;
    error.payload = err;
    throw error;
  }
  return res.status === 204 ? null : res.json();
}

const ADMIN_TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "amber"
}/*EDITMODE-END*/;

// ─── Toast (v0.8.15 W2) ────────────────────────────
// 15 mutation handler 응답의 시각 알림. fixed bottom-right, fade-out 3s.
// success (result.ok === true) → amber accent / error → red --err.
// 단일 element 재사용 (queue 미지원 — over-engineering 회피, M6).
function showToast(result) {
  if (typeof document === 'undefined') return;
  let host = document.getElementById('admin-toast-host');
  if (!host) {
    host = document.createElement('div');
    host.id = 'admin-toast-host';
    host.style.cssText = [
      'position:fixed',
      'bottom:16px',
      'right:16px',
      'min-width:240px',
      'max-width:480px',
      'padding:10px 14px',
      'border-radius:6px',
      'font-family:JetBrains Mono, monospace',
      'font-size:12px',
      'z-index:9999',
      'opacity:0',
      'transition:opacity 200ms ease-in-out',
      'pointer-events:none',
      'white-space:pre-wrap',
      'word-break:break-word',
    ].join(';');
    document.body.appendChild(host);
  }
  const ok = result && result.ok === true;
  host.style.background = ok ? 'rgba(38, 30, 12, 0.96)' : 'rgba(54, 18, 22, 0.96)';
  host.style.color = ok ? '#f5c97a' : '#f48a7c';
  host.style.border = ok ? '1px solid #b88a3a' : '1px solid #b8473a';
  const code = result && result.code ? `[${result.code}] ` : '';
  const msg = (result && result.message) ? result.message : (ok ? 'ok' : 'error');
  host.textContent = `${ok ? '✓' : '✗'} ${code}${msg}`;
  host.style.opacity = '1';
  clearTimeout(showToast._timer);
  showToast._timer = setTimeout(() => { host.style.opacity = '0'; }, 3200);
}

// fetch helper wrapper — 결과를 toast 로 알리고 catch/throw 정형화.
async function postHandler(handlerName, body) {
  try {
    const res = await api(`/admin/console?handler=${handlerName}`, { method: 'POST', body });
    showToast(res);
    return res;
  } catch (e) {
    const fail = (e.payload && typeof e.payload === 'object')
      ? { ok: false, code: e.payload.code || 'network', message: e.payload.message || e.message }
      : { ok: false, code: 'network', message: e.message };
    showToast(fail);
    throw e;
  }
}

// ─── Tiny components ───────────────────────────────
function StatusDot({ tone, label }) {
  return <span className={`adot adot-${tone}`} title={label} />;
}

function Bar({ value, max, tone = 'ok' }) {
  const pct = Math.min(100, (value / max) * 100);
  return (
    <div className="abar">
      <div className={`abar-fill abar-${tone}`} style={{ width: `${pct}%` }} />
    </div>
  );
}

function fmtKrw(value) {
  if (value === null || value === undefined || value === '') return '—';
  const n = Number(value);
  return Number.isFinite(n) ? `₩${Math.round(n).toLocaleString()}` : '—';
}

function fmtPctRate(value) {
  if (value === null || value === undefined || value === '') return '—';
  const n = Number(value);
  return Number.isFinite(n) ? `${(n * 100).toFixed(1)}%` : '—';
}

function Sparkline({ data, w = 90, h = 20 }) {
  if (!data || !data.length) return null;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * w;
    const y = h - ((v - min) / range) * h;
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(' ');
  return <svg className="aspark" width={w} height={h}><polyline points={pts} fill="none" /></svg>;
}

// ─── Sections ──────────────────────────────────────
function Overview() {
  const s = SYS_STATUS;
  const budgetPct = (s.budget.spentUsd / s.budget.budgetUsd * 100).toFixed(1);
  return (
    <div className="ov">
      <div className="ov-grid">
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone={s.health} /> SYSTEM</div>
          <div className="ov-stack">
            <div><span className="ov-k">uptime</span><span className="ov-v">{s.uptime}</span></div>
            <div><span className="ov-k">build</span><span className="ov-v">{s.build}</span></div>
            <div><span className="ov-k">region</span><span className="ov-v">{s.region}</span></div>
          </div>
        </div>
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone="ok" /> CLUSTER</div>
          <div className="ov-stack">
            <div><span className="ov-k">nodes</span><span className="ov-v">{s.cluster.ready}/{s.cluster.nodes}</span></div>
            <div><span className="ov-k">pods</span><span className="ov-v">{s.cluster.pods}</span></div>
            <div><span className="ov-k">restarts/24h</span><span className="ov-v">{s.cluster.restarts24h}</span></div>
          </div>
        </div>
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone={budgetPct > 80 ? 'warn' : 'ok'} /> LLM BUDGET · {s.budget.periodLabel}</div>
          <div className="ov-budget">
            <span className="ov-budget-num">${s.budget.spentUsd}</span>
            <span className="ov-budget-of">/ ${s.budget.budgetUsd}</span>
            <span className="ov-budget-pct">{budgetPct}%</span>
          </div>
          <Bar value={s.budget.spentUsd} max={s.budget.budgetUsd} tone={budgetPct > 80 ? 'warn' : 'ok'} />
        </div>
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone="ok" /> RUNTIME</div>
          <div className="ov-stack">
            <div><span className="ov-k">in-flight</span><span className="ov-v">{s.inflight}</span></div>
            <div><span className="ov-k">queue</span><span className="ov-v">{s.queue}</span></div>
            <div><span className="ov-k">errors/24h</span><span className="ov-v">{s.errors24h}</span></div>
          </div>
        </div>
      </div>
    </div>
  );
}

function LlmRouting() {
  const [rules, setRules] = useState(ROUTING_RULES);
  const [editing, setEditing] = useState(null); // null | { idx, rule }
  const [saveError, setSaveError] = useState(null);

  const openEdit = (idx) => { setSaveError(null); setEditing({ idx, rule: { ...rules[idx] } }); };
  const openNew = () => { setSaveError(null); setEditing({ idx: -1, rule: { tag: '', provider: 'anthropic', model: '', mode: 'on-demand', budget: '$1/d', hits24h: 0, fallback: '' } }); };
  const saveRule = async (draft) => {
    try {
      const result = await api('/admin/console/api/routing-rules', { method: 'PUT', body: draft });
      setRules(prev => editing.idx === -1
        ? [...prev, result.rule ?? draft]
        : prev.map((r, i) => i === editing.idx ? (result.rule ?? draft) : r));
      setEditing(null);
    } catch (e) {
      setSaveError(e.message);
    }
  };
  const deleteRule = async () => {
    if (editing.idx < 0) return;
    const rule = rules[editing.idx];
    try {
      await api(`/admin/console/api/routing-rules/${encodeURIComponent(rule.tag)}`, { method: 'DELETE' });
      setRules(prev => prev.filter((_, i) => i !== editing.idx));
      setEditing(null);
    } catch (e) {
      setSaveError(e.message);
    }
  };

  return (
    <>
      <h3 className="aslab">PROVIDERS</h3>
      <table className="atbl">
        <thead><tr><th>provider</th><th>models</th><th>status</th><th>p50 latency</th><th>calls/24h</th><th>cost/24h</th><th>base url</th></tr></thead>
        <tbody>
          {LLM_PROVIDERS.map(p => (
            <tr key={p.id}>
              <td className="t-id">{p.name}</td>
              <td className="t-num">{p.models ?? '—'}</td>
              <td><StatusDot tone={p.status === 'ok' ? 'ok' : 'warn'} /> <span className="t-faint">{p.status ?? '—'}</span></td>
              <td className="t-num">{p.latency != null ? `${p.latency}ms` : '—'}</td>
              <td className="t-num">{(p.calls24h ?? 0).toLocaleString()}</td>
              <td className="t-num">${(p.cost24h ?? 0).toFixed(2)}</td>
              <td className="t-mono t-faint">{p.baseUrl}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <div className="aslab-row" style={{ marginTop: 8 }}>
        <h3 className="aslab">ROUTING RULES <span className="ahint">(tag → provider/model)</span></h3>
        <button className="abtn abtn-pri" onClick={openNew}>+ new rule</button>
      </div>
      <table className="atbl atbl-clickable">
        <thead><tr><th>tag</th><th>provider</th><th>model</th><th>mode</th><th>budget</th><th>hits/24h</th><th>fallback</th><th></th></tr></thead>
        <tbody>
          {rules.map((r, i) => (
            <tr key={r.tag + i} onClick={() => openEdit(i)}>
              <td className="t-id">{r.tag}</td>
              <td>{r.provider}</td>
              <td className="t-mono">{r.model}</td>
              <td><span className={`pill pill-${r.mode}`}>{r.mode}</span></td>
              <td className="t-num">{r.budget}</td>
              <td className="t-num">{(r.hits24h || 0).toLocaleString()}</td>
              <td className="t-faint t-mono">{r.fallback}</td>
              <td><span className="t-faint" style={{ fontSize: 11 }}>edit ›</span></td>
            </tr>
          ))}
        </tbody>
      </table>

      {editing && (
        <RoutingEditor
          rule={editing.rule}
          isNew={editing.idx === -1}
          onCancel={() => { setEditing(null); setSaveError(null); }}
          onSave={saveRule}
          onDelete={deleteRule}
          saveError={saveError}
        />
      )}
    </>
  );
}

// ─── Routing rule editor (modal) ────────────────────────────
function RoutingEditor({ rule, isNew, onCancel, onSave, onDelete, saveError }) {
  const [draft, setDraft] = useState(rule);
  const [models, setModels] = useState({ state: 'idle', list: [] }); // idle | loading | ok | err | rate-limited
  const [fbProvider, fbModel] = (draft.fallback || '').split('/');

  // 모델 조회 (live fetch — provider 변경 시 자동 호출)
  const fetchModels = async (providerId) => {
    if (!providerId) return;
    setModels({ state: 'loading', list: [] });
    try {
      const data = await api(`/admin/console/api/providers/${encodeURIComponent(providerId)}/models`);
      setModels({ state: 'ok', list: data.list, fetchedAt: data.fetchedAt });
    } catch (e) {
      if (e.status === 429) {
        setModels({
          state: 'rate-limited',
          list: [],
          retryAfterSec: e.payload?.retryAfterSec ?? 30,
        });
      } else {
        setModels({ state: 'err', list: [], message: e.message });
      }
    }
  };
  useEffect(() => { fetchModels(draft.provider); }, [draft.provider]);

  const update = (patch) => setDraft(d => ({ ...d, ...patch }));
  const set = (k) => (e) => update({ [k]: e.target.value });

  const meta = ALL_PROVIDERS_META[draft.provider];
  const valid = draft.tag.trim() && draft.model && draft.mode;

  return (
    <div className="amodal-back" onClick={onCancel}>
      <div className="amodal" onClick={e => e.stopPropagation()}>
        <header className="amodal-head">
          <span className="ahead-prompt">$</span>
          <span className="ahead-cmd">{isNew ? 'routing rule create' : 'routing rule edit'}</span>
          <button className="amodal-x" onClick={onCancel}>esc</button>
        </header>
        <div className="amodal-body">
          <div className="afield">
            <label>tag <span className="afield-hint">(unique routing key — agent.purpose 형식 권장)</span></label>
            <input
              className="ainput t-mono"
              value={draft.tag}
              onChange={set('tag')}
              placeholder="e.g. designer.copy.ja"
              autoFocus={isNew}
            />
          </div>

          <div className="afield-row">
            <div className="afield">
              <label>provider</label>
              {/* 2026-05-17 운영자 directive — LLM_PROVIDERS (vault stored 만) 만 선택 가능.
                  미저장 provider 는 API KEYS 의 '+ provider 추가' 로 먼저 등록해야 라우팅 사용 가능. */}
              <select className="ainput t-mono" value={draft.provider} onChange={(e) => update({ provider: e.target.value, model: '' })}>
                {LLM_PROVIDERS.length === 0 && (
                  <option value="" disabled>— 등록된 provider 없음 (API KEYS 섹션에서 추가) —</option>
                )}
                {LLM_PROVIDERS.map(p => (
                  <option key={p.name} value={p.name}>{p.displayName || p.name}</option>
                ))}
              </select>
            </div>
            <div className="afield">
              <label>
                model
                <span className="afield-hint">
                  · {models.state === 'loading' && <span className="t-acc">loading…</span>}
                  {models.state === 'ok' && <span className="t-faint">{models.list.length} fetched · {models.fetchedAt}</span>}
                  {models.state === 'rate-limited' && <span className="t-warn">cooldown — retry in {models.retryAfterSec}s</span>}
                  {models.state === 'err' && <span className="t-err">fetch failed — {models.message}</span>}
                </span>
              </label>
              <select className="ainput t-mono" value={draft.model} onChange={set('model')} disabled={models.state !== 'ok'}>
                <option value="">— select model —</option>
                {models.list.filter(m => !m.deprecated).map(m => (
                  <option key={m.id} value={m.id}>
                    {m.id} · ctx {(m.ctx / 1000).toFixed(0)}k {m.local ? '· local' : `· $${m.inputPer1k.toFixed(4)}/1k in · $${m.outputPer1k.toFixed(4)}/1k out`}
                  </option>
                ))}
                {models.list.some(m => m.deprecated) && (
                  <optgroup label="deprecated">
                    {models.list.filter(m => m.deprecated).map(m => (
                      <option key={m.id} value={m.id}>{m.id} (deprecated)</option>
                    ))}
                  </optgroup>
                )}
              </select>
              {meta && (
                <button className="abtn abtn-tiny" type="button" onClick={() => fetchModels(draft.provider)} style={{ marginTop: 4 }}>
                  ↻ {meta.validateLabel}
                </button>
              )}
            </div>
          </div>

          <div className="afield-row">
            <div className="afield">
              <label>mode</label>
              <select className="ainput t-mono" value={draft.mode} onChange={set('mode')}>
                {ROUTING_MODES.map(m => <option key={m} value={m}>{m}</option>)}
              </select>
            </div>
            <div className="afield">
              <label>budget <span className="afield-hint">(LlmCostGuard cap)</span></label>
              <input className="ainput t-mono" value={draft.budget} onChange={set('budget')} placeholder="e.g. $5/d" />
            </div>
          </div>

          <div className="afield">
            <label>fallback <span className="afield-hint">(provider/model — 1차 실패 시)</span></label>
            <div className="afield-row" style={{ gap: 8 }}>
              <select
                className="ainput t-mono"
                style={{ flex: 1 }}
                value={fbProvider || ''}
                onChange={(e) => update({ fallback: e.target.value ? `${e.target.value}/${fbModel || ''}` : '' })}
              >
                <option value="">— none —</option>
                <option value="manual">manual (사장 결재)</option>
                {/* fallback provider 도 vault stored 만 (운영자 directive 2026-05-17) */}
                {LLM_PROVIDERS.map(p => (
                  <option key={p.name} value={p.name}>{p.displayName || p.name}</option>
                ))}
              </select>
              {fbProvider && fbProvider !== 'manual' && (
                <select
                  className="ainput t-mono"
                  style={{ flex: 1 }}
                  value={fbModel || ''}
                  onChange={(e) => update({ fallback: `${fbProvider}/${e.target.value}` })}
                >
                  <option value="">— model —</option>
                  {(PROVIDER_MODELS[fbProvider] || []).filter(m => !m.deprecated).map(m => (
                    <option key={m.id} value={m.id}>{m.id}</option>
                  ))}
                </select>
              )}
            </div>
          </div>
        </div>
        <footer className="amodal-foot">
          {saveError && <span className="t-err" style={{ fontSize: 12, alignSelf: 'center' }}>⚠ {saveError}</span>}
          {!isNew && <button className="abtn abtn-danger" onClick={onDelete}>delete rule</button>}
          <span style={{ flex: 1 }} />
          <button className="abtn" onClick={onCancel}>cancel</button>
          <button className="abtn abtn-pri" disabled={!valid} onClick={() => onSave(draft)}>
            {isNew ? 'create' : 'save'}
          </button>
        </footer>
      </div>
    </div>
  );
}

function CostGuard() {
  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">BUCKETS · TODAY</h3>
        <span className="ahint">spend (USD) / daily limit</span>
      </div>
      <div className="cost-grid">
        {COST_BUCKETS.map(b => {
          const pct = (b.spent / b.limit * 100);
          const tone = pct > 90 ? 'err' : pct > 75 ? 'warn' : 'ok';
          return (
            <div key={b.id} className="cost-card">
              <div className="cost-head">
                <span className="cost-id">{b.id}</span>
                <span className={`cost-trend trend-${b.trend.startsWith('+') ? 'up' : b.trend.startsWith('-') ? 'down' : 'flat'}`}>{b.trend}</span>
              </div>
              <div className="cost-num">
                <span className="cost-spent">${b.spent.toFixed(2)}</span>
                <span className="cost-of">/ ${b.limit.toFixed(2)}</span>
              </div>
              <Bar value={b.spent} max={b.limit} tone={tone} />
              <div className="cost-pct">{pct.toFixed(0)}%</div>
            </div>
          );
        })}
      </div>
      <h3 className="aslab">HISTORY · LAST 7 DAYS (USD)</h3>
      <div className="cost-history">
        {COST_HISTORY.map((v, i) => (
          <div key={i} className="hbar">
            <div className="hbar-fill" style={{ height: `${(v / Math.max(...COST_HISTORY)) * 100}%` }} />
            <div className="hbar-lbl">${v.toFixed(1)}</div>
            <div className="hbar-day">D-{COST_HISTORY.length - 1 - i}</div>
          </div>
        ))}
      </div>

      {/* v0.10.8 W2 (Spec 2026-05-17 §4.2.1) — /admin/worklog/curator 흡수: 부서 필터 + recentCalls collapse */}
      <DepartmentRecentCallsCollapse />
    </>
  );
}

// v0.10.8 W2 — #cost 의 신규 sub-section. 부서 필터 chip + recent LLM calls page list.
// data source = DEPARTMENT_RECENT_CALLS (default page=1) + /admin/console/api/department/recent-calls fetch.
function DepartmentRecentCallsCollapse() {
  const [open, setOpen] = useState(false);
  const [dept, setDept] = useState(DEPARTMENT_RECENT_CALLS.departmentFilter ?? '');
  const [page, setPage] = useState(DEPARTMENT_RECENT_CALLS.page ?? 1);
  const [data, setData] = useState(DEPARTMENT_RECENT_CALLS);
  const [loading, setLoading] = useState(false);

  const fetchPage = async (nextDept, nextPage) => {
    setLoading(true);
    try {
      const qs = new URLSearchParams();
      if (nextDept) qs.set('dept', nextDept);
      qs.set('page', String(nextPage));
      const res = await api(`/admin/console/api/department/recent-calls?${qs.toString()}`);
      setData(res);
      setPage(res.page ?? nextPage);
    } catch (e) {
      showToast({ ok: false, code: 'fetch-failed', message: e.message });
    } finally {
      setLoading(false);
    }
  };

  const onDeptChange = async (next) => {
    setDept(next);
    if (open) await fetchPage(next, 1);
  };
  const onToggle = async () => {
    const next = !open;
    setOpen(next);
    if (next && data.rows.length === 0) await fetchPage(dept, page);
  };

  const totalPages = Math.max(1, Math.ceil((data.totalCount ?? 0) / (data.pageSize ?? 50)));

  return (
    <div style={{ marginTop: 20, borderTop: '1px solid rgba(245,201,122,0.18)', paddingTop: 14 }}>
      <div className="aslab-row">
        <h3 className="aslab" style={{ cursor: 'pointer' }} onClick={onToggle}>
          {open ? '▼' : '▶'} LLM CALLS · RECENT (부서별)
        </h3>
        <span className="ahint">/admin/worklog/curator 흡수 · LLM 호출 사후 검토</span>
      </div>

      {open && (
        <>
          <div className="aslab-row" style={{ gap: 6, flexWrap: 'wrap' }}>
            <button className={`abtn ${!dept ? 'abtn-pri' : ''}`} onClick={() => onDeptChange('')}>전체</button>
            {KNOWN_DEPT_SLUGS.map(d => (
              <button key={d} className={`abtn ${dept === d ? 'abtn-pri' : ''}`} onClick={() => onDeptChange(d)}>
                {d}
              </button>
            ))}
            <span style={{ flex: 1 }} />
            <span className="t-faint t-mono" style={{ fontSize: 11 }}>
              {loading ? 'loading…' : `${data.totalCount} rows · page ${page}/${totalPages}`}
            </span>
          </div>

          <table className="atbl" style={{ marginTop: 6 }}>
            <thead>
              <tr>
                <th>time</th>
                <th>bucket</th>
                <th>department</th>
                <th>agent</th>
                <th>model</th>
                <th>tokens (in/out)</th>
                <th>cost</th>
              </tr>
            </thead>
            <tbody>
              {data.rows.map(r => (
                <tr key={r.id}>
                  <td className="t-mono" style={{ fontSize: 11 }}>{new Date(r.calledAt).toLocaleString('en-GB')}</td>
                  <td className="t-mono" style={{ fontSize: 11 }}>{r.bucket ?? <span className="t-faint">(전역)</span>}</td>
                  <td><span className={`tag tag-${r.department === '(전역)' ? 'sys' : 'agent'}`}>{r.department}</span></td>
                  <td className="t-mono" style={{ fontSize: 11 }}>{r.agentName}</td>
                  <td className="t-mono" style={{ fontSize: 11 }}>{r.model}</td>
                  <td className="t-num">{r.promptTokens} / {r.completionTokens}</td>
                  <td className="t-num">${r.costUsd.toFixed(4)}</td>
                </tr>
              ))}
              {data.rows.length === 0 && !loading && (
                <tr><td colSpan={7} className="t-faint" style={{ textAlign: 'center', padding: 12 }}>호출 기록 없음.</td></tr>
              )}
            </tbody>
          </table>

          {totalPages > 1 && (
            <div className="aslab-row" style={{ marginTop: 6, justifyContent: 'flex-end', gap: 4 }}>
              <button className="abtn" disabled={page <= 1 || loading} onClick={() => fetchPage(dept, page - 1)}>← prev</button>
              <button className="abtn" disabled={page >= totalPages || loading} onClick={() => fetchPage(dept, page + 1)}>next →</button>
            </div>
          )}
        </>
      )}
    </div>
  );
}

function Jobs() {
  const [busy, setBusy] = useState(null);

  // v0.10.8 W2 (Spec 2026-05-17 §4.2.3) — /admin/jobs/run 흡수: ▶ run 액션 inline.
  const onRunNow = async (recurringJobId) => {
    setBusy(`run:${recurringJobId}`);
    try {
      await postHandler('JobRunNow', { recurringJobId });
    } catch { /* toast already shown */ }
    finally { setBusy(null); }
  };

  return (
    <>
      <table className="atbl">
        <thead><tr><th>job</th><th>state</th><th>last run</th><th>duration</th><th>next</th><th>schedule</th><th>fail/7d</th><th>actions</th></tr></thead>
        <tbody>
          {JOBS.map(j => (
            <tr key={j.id} className={`job-${j.state}`}>
              <td className="t-id">{j.id}</td>
              <td><span className={`pill pill-${j.state}`}>{j.state}</span></td>
              <td className="t-mono">{j.last}</td>
              <td className="t-mono">{j.dur}</td>
              <td className="t-mono">{j.next}</td>
              <td className="t-mono t-faint">{j.schedule}</td>
              <td className="t-num">{j.fail7d > 0 ? <span className="t-warn">{j.fail7d}</span> : '0'}</td>
              <td>
                <button
                  className="abtn"
                  style={{ marginRight: 4 }}
                  disabled={busy === `run:${j.id}`}
                  title="RecurringJob 즉시 실행 (Hangfire trigger)"
                  onClick={() => onRunNow(j.id)}
                >
                  {busy === `run:${j.id}` ? '…' : '▶ run'}
                </button>
                {j.error && <span className="t-err" title={j.error}>⚠ {j.error}</span>}
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* v0.10.8 W2 (Spec 2026-05-17 §4.2.1 + §4.2.4) — recentRuns collapse + ⚠ abort 액션 inline. */}
      <DepartmentRecentRunsCollapse />
    </>
  );
}

// v0.10.8 W2 — #jobs 의 신규 sub-section. 부서별 최근 10 run + running 시 ⚠ abort.
function DepartmentRecentRunsCollapse() {
  const [open, setOpen] = useState(false);
  const [dept, setDept] = useState(DEPARTMENT_RECENT_RUNS.departmentFilter ?? '');
  const [data, setData] = useState(DEPARTMENT_RECENT_RUNS);
  const [loading, setLoading] = useState(false);
  const [abortFor, setAbortFor] = useState(null); // {department, jobName}
  const [reason, setReason] = useState('');
  const [busy, setBusy] = useState(null);

  const fetchRuns = async (nextDept) => {
    setLoading(true);
    try {
      const qs = new URLSearchParams();
      if (nextDept) qs.set('dept', nextDept);
      const res = await api(`/admin/console/api/department/recent-runs?${qs.toString()}`);
      setData(res);
    } catch (e) {
      showToast({ ok: false, code: 'fetch-failed', message: e.message });
    } finally {
      setLoading(false);
    }
  };

  const onDeptChange = async (next) => {
    setDept(next);
    if (open) await fetchRuns(next);
  };
  const onToggle = async () => {
    const next = !open;
    setOpen(next);
    if (next && data.rows.length === 0) await fetchRuns(dept);
  };
  const onAbortConfirm = async (run) => {
    if (!reason.trim()) {
      showToast({ ok: false, code: 'input-invalid', message: 'reason 필수 (감사 로그).' });
      return;
    }
    setBusy(`abort:${run.id}`);
    try {
      await postHandler('JobAbort', { department: run.department, reason: reason.trim() });
      setAbortFor(null);
      setReason('');
      await fetchRuns(dept);
    } catch { /* toast */ }
    finally { setBusy(null); }
  };

  return (
    <div style={{ marginTop: 20, borderTop: '1px solid rgba(245,201,122,0.18)', paddingTop: 14 }}>
      <div className="aslab-row">
        <h3 className="aslab" style={{ cursor: 'pointer' }} onClick={onToggle}>
          {open ? '▼' : '▶'} JOB RUNS · RECENT (부서별)
        </h3>
        <span className="ahint">/admin/progress/curator + /admin/worklog/* 흡수 · running 시 ⚠ abort</span>
      </div>

      {open && (
        <>
          <div className="aslab-row" style={{ gap: 6, flexWrap: 'wrap' }}>
            <button className={`abtn ${!dept ? 'abtn-pri' : ''}`} onClick={() => onDeptChange('')}>전체</button>
            {KNOWN_DEPT_SLUGS.map(d => (
              <button key={d} className={`abtn ${dept === d ? 'abtn-pri' : ''}`} onClick={() => onDeptChange(d)}>
                {d}
              </button>
            ))}
            <span style={{ flex: 1 }} />
            <span className="t-faint t-mono" style={{ fontSize: 11 }}>
              {loading ? 'loading…' : `${data.rows.length} runs`}
            </span>
          </div>

          <table className="atbl" style={{ marginTop: 6 }}>
            <thead>
              <tr>
                <th>dept</th>
                <th>job</th>
                <th>state</th>
                <th>started</th>
                <th>step</th>
                <th>hangfire</th>
                <th>actions</th>
              </tr>
            </thead>
            <tbody>
              {data.rows.map(r => (
                <tr key={r.id}>
                  <td><span className="tag tag-agent">{r.department}</span></td>
                  <td className="t-mono" style={{ fontSize: 11 }}>{r.jobName}</td>
                  <td><span className={`pill pill-${r.status}`}>{r.status}</span></td>
                  <td className="t-mono" style={{ fontSize: 11 }}>{new Date(r.startedAt).toLocaleString('en-GB')}</td>
                  <td className="t-num">{r.currentStep ?? '—'}/{r.stepCount}</td>
                  <td className="t-mono t-faint" style={{ fontSize: 10 }}>{r.hangfireJobId ?? '—'}</td>
                  <td>
                    {r.canAbort && (
                      <button
                        className="abtn"
                        disabled={busy === `abort:${r.id}`}
                        title="활성 run 강제 종료"
                        onClick={() => { setAbortFor(r); setReason(''); }}
                      >
                        ⚠ abort
                      </button>
                    )}
                    {r.errorText && <span className="t-err" title={r.errorText} style={{ marginLeft: 6 }}>⚠</span>}
                  </td>
                </tr>
              ))}
              {data.rows.length === 0 && !loading && (
                <tr><td colSpan={7} className="t-faint" style={{ textAlign: 'center', padding: 12 }}>job run 없음.</td></tr>
              )}
            </tbody>
          </table>

          {abortFor && (
            <div className="amodal-back" onClick={() => setAbortFor(null)}>
              <div className="amodal" onClick={e => e.stopPropagation()} style={{ maxWidth: 480 }}>
                <header className="amodal-head">
                  <span className="ahead-prompt">!</span>
                  <span className="ahead-cmd">abort {abortFor.department} · {abortFor.jobName}</span>
                  <button className="amodal-x" onClick={() => setAbortFor(null)}>esc</button>
                </header>
                <div className="amodal-body">
                  <div className="afield">
                    <label>reason <span className="afield-hint">· audit log 기록용 (필수)</span></label>
                    <textarea
                      className="ainput t-mono"
                      style={{ width: '100%', minHeight: 60 }}
                      value={reason}
                      onChange={(e) => setReason(e.target.value)}
                      placeholder="예: cost guard breach / wrong seed / hang detected"
                    />
                  </div>
                  <div className="aslab-row" style={{ marginTop: 8 }}>
                    <span style={{ flex: 1 }} />
                    <button className="abtn" onClick={() => setAbortFor(null)}>cancel</button>
                    <button
                      className="abtn abtn-pri"
                      disabled={!reason.trim() || busy === `abort:${abortFor.id}`}
                      onClick={() => onAbortConfirm(abortFor)}
                    >
                      {busy === `abort:${abortFor.id}` ? 'aborting…' : '⚠ abort confirm'}
                    </button>
                  </div>
                </div>
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

function Db() {
  return (
    <>
      <div className="ov-grid">
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone="ok" /> POSTGRES</div>
          <div className="ov-stack">
            <div><span className="ov-k">host</span><span className="ov-v t-mono">{DB_INFO.host}</span></div>
            <div><span className="ov-k">version</span><span className="ov-v">{DB_INFO.version}</span></div>
            <div><span className="ov-k">size</span><span className="ov-v">{DB_INFO.size}</span></div>
            <div><span className="ov-k">uptime</span><span className="ov-v">{DB_INFO.uptime}</span></div>
          </div>
        </div>
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone="ok" /> CONNECTIONS</div>
          <div className="ov-budget">
            <span className="ov-budget-num">{DB_INFO.connections}</span>
            <span className="ov-budget-of">/ {DB_INFO.maxConn}</span>
          </div>
          <Bar value={DB_INFO.connections} max={DB_INFO.maxConn} tone="ok" />
          <div className="ov-stack" style={{ marginTop: 10 }}>
            <div><span className="ov-k">replica lag</span><span className="ov-v">{DB_INFO.replicaLag}</span></div>
            <div><span className="ov-k">last backup</span><span className="ov-v">{DB_INFO.lastBackup}</span></div>
          </div>
        </div>
        <div className="ov-card">
          <div className="ov-card-head"><StatusDot tone={MIGRATIONS_PENDING > 0 ? 'warn' : 'ok'} /> MIGRATIONS</div>
          <div className="ov-stack">
            <div><span className="ov-k">pending</span><span className="ov-v">{MIGRATIONS_PENDING}</span></div>
            <div><span className="ov-k">last applied</span><span className="ov-v t-mono">{MIGRATIONS_LAST}</span></div>
          </div>
        </div>
      </div>
      <h3 className="aslab">TOP TABLES</h3>
      <table className="atbl">
        <thead><tr><th>table</th><th>rows</th><th>size</th><th>last vacuum</th></tr></thead>
        <tbody>
          {DB_TABLES.map(t => (
            <tr key={t.name}>
              <td className="t-id">{t.name}</td>
              <td className="t-num">{t.rows.toLocaleString()}</td>
              <td className="t-num">{t.size}</td>
              <td><StatusDot tone={t.tone} /> <span className="t-mono t-faint">{t.vacuum}</span></td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

function Keys() {
  const [keys, setKeys] = useState(API_KEYS);
  const [validating, setValidating] = useState({}); // keyId -> bool
  const [editing, setEditing] = useState(null); // null | { idx, key } (-1 = new)
  const [reveal, setReveal] = useState({});
  const [saveError, setSaveError] = useState(null);

  const validate = async (id) => {
    setValidating(v => ({ ...v, [id]: true }));
    try {
      const result = await api(`/admin/console/api/api-keys/${encodeURIComponent(id)}/validate`, { method: 'POST' });
      setKeys(prev => prev.map(k => {
        if (k.id !== id) return k;
        return {
          ...k,
          validation: {
            ok: result.ok,
            modelsFound: result.modelsFound,
            latencyMs: result.latencyMs,
            billing: null,
            message: result.message,
          },
          validatedAt: '방금',
          validatedBy: 'manual',
        };
      }));
    } catch (e) {
      setKeys(prev => prev.map(k => k.id === id
        ? { ...k, validation: { ok: false, message: e.message, modelsFound: null, latencyMs: null, billing: null } }
        : k));
    } finally {
      setValidating(v => ({ ...v, [id]: false }));
    }
  };

  const validateAll = () => keys.forEach(k => validate(k.id));

  const openEdit = (idx) => { setSaveError(null); setEditing({ idx, key: { ...keys[idx], secret: '' } }); };
  // '+ provider 추가' (2026-05-17 운영자 directive) — AVAILABLE_PROVIDERS 의 1번째 항목으로 초기화.
  // key id = provider 의 credentialKey 자동 도출 (예: 'llm.openai.apikey'). 사용자는 secret 만 입력.
  const openNew = () => {
    setSaveError(null);
    const first = AVAILABLE_PROVIDERS[0];
    if (!first) return; // dropdown 비어 있으면 (모든 provider stored) noop
    setEditing({ idx: -1, key: {
      id: first.credentialKey, provider: first.id, scope: '*', secret: '',
      rotated: '방금', expires: '90d', status: 'ok', lastUsed: '—',
      masked: '', validatedAt: null, validatedBy: null,
      validation: { ok: null, modelsFound: null, latencyMs: null, billing: null, message: '저장 후 validate를 실행하세요' },
    } });
  };
  const saveKey = async (next) => {
    try {
      const result = await api('/admin/console/api/api-keys', {
        method: 'PUT',
        body: { key: next.id, secret: next.secret ?? '' },
      });
      const masked = next.secret ? maskSecret(next.secret) : next.masked;
      setKeys(prev => editing.idx === -1
        ? [...prev, { ...next, masked, validation: result.key ?? next.validation }]
        : prev.map((k, i) => i === editing.idx ? { ...next, masked } : k));
      setEditing(null);
    } catch (e) {
      setSaveError(e.message);
    }
  };
  const deleteKey = async () => {
    if (editing.idx < 0) return;
    const k = keys[editing.idx];
    try {
      await api(`/admin/console/api/api-keys/${encodeURIComponent(k.id)}`, { method: 'DELETE' });
      setKeys(prev => prev.filter((_, i) => i !== editing.idx));
      setEditing(null);
    } catch (e) {
      setSaveError(e.message);
    }
  };

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">API KEYS</h3>
        <span className="ahint">credential vault — values are envelope-encrypted at rest</span>
        <span style={{ flex: 1 }} />
        <button className="abtn" onClick={validateAll}>↻ validate all</button>
        <button
          className="abtn abtn-pri"
          onClick={openNew}
          disabled={AVAILABLE_PROVIDERS.length === 0}
          title={AVAILABLE_PROVIDERS.length === 0 ? '모든 카탈로그 provider 가 이미 저장됨' : '카탈로그 미저장 provider 추가'}
        >+ provider 추가</button>
      </div>
      <table className="atbl atbl-clickable">
        <thead><tr>
          <th>key id</th><th>provider</th><th>scope</th><th>secret</th>
          <th>rotated</th><th>expires</th><th>last used</th>
          <th>last validated</th><th>health</th><th></th>
        </tr></thead>
        <tbody>
          {keys.map((k, i) => {
            const meta = ALL_PROVIDERS_META[k.provider];
            const v = k.validation ?? { ok: true, message: '', latencyMs: null };
            const isValidating = validating[k.id];
            const tone = isValidating ? 'warn' : v.ok === false ? 'err' : k.status === 'warn' ? 'warn' : k.status === 'stale' ? 'err' : 'ok';
            return (
              <tr key={`${k.id}__${i}`} onClick={() => openEdit(i)}>
                <td className="t-id t-mono">{k.id}</td>
                <td>
                  <span className={`provtag provtag-${k.provider}`}>{meta?.name || k.provider}</span>
                </td>
                <td className="t-mono t-faint">{k.scope}</td>
                <td className="t-mono t-faint">
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                    {reveal[k.id] ? k.secret : k.masked}
                    <button
                      className="abtn abtn-tiny"
                      onClick={(e) => { e.stopPropagation(); setReveal(r => ({ ...r, [k.id]: !r[k.id] })); }}
                    >{reveal[k.id] ? 'hide' : 'show'}</button>
                  </span>
                </td>
                <td className="t-mono">{k.rotated}</td>
                <td className={`t-mono ${k.expires === '7d' ? 't-warn' : k.expires === 'never' ? 't-faint' : ''}`}>{k.expires}</td>
                <td className="t-mono t-faint">{k.lastUsed}</td>
                <td className="t-mono t-faint">
                  {k.validatedAt}
                  {k.validatedBy && <span className="vbadge"> · {k.validatedBy}</span>}
                </td>
                <td>
                  <div className="vhealth">
                    <StatusDot tone={tone} />
                    <span className={`t-${tone}`}>
                      {isValidating ? 'checking…' : v.ok ? 'ok' : 'fail'}
                    </span>
                    {v.latencyMs && !isValidating && <span className="t-faint t-mono">{v.latencyMs}ms</span>}
                  </div>
                  <div className="vmsg t-faint">{v.message}</div>
                </td>
                <td>
                  <button
                    className="abtn"
                    onClick={(e) => { e.stopPropagation(); validate(k.id); }}
                    disabled={isValidating}
                  >
                    {isValidating ? '...' : 'validate'}
                  </button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>

      {editing && (
        <KeyEditor
          k={editing.key}
          isNew={editing.idx === -1}
          onCancel={() => { setEditing(null); setSaveError(null); }}
          onSave={saveKey}
          onDelete={deleteKey}
          saveError={saveError}
        />
      )}
    </>
  );
}

function maskSecret(s) {
  if (!s) return '';
  if (s.length <= 12) return s.slice(0, 3) + '···' + s.slice(-3);
  return s.slice(0, 8) + '···' + s.slice(-4);
}

function KeyEditor({ k, isNew, onCancel, onSave, onDelete, saveError }) {
  const [draft, setDraft] = useState(k);
  const [check, setCheck] = useState({ state: 'idle' }); // idle | running | ok | err
  const update = (patch) => setDraft(d => ({ ...d, ...patch }));
  const set = (key) => (e) => update({ [key]: e.target.value });
  // ALL_PROVIDERS_META 는 정적 — sample/keyPrefix/validateLabel 의 source. AVAILABLE_PROVIDERS 에서
  // 본 메타 없는 provider (예: 향후 추가될 신규 catalog row) 도 안전하게 동작하도록 fallback 제공.
  const meta = ALL_PROVIDERS_META[draft.provider] ?? { sample: '', baseUrl: '', validateLabel: 'GET /v1/models' };
  const valid = draft.id.trim() && draft.provider && (isNew ? draft.secret : true);

  // isNew = '+ provider 추가' dialog — dropdown 변경 시 key id (= credentialKey) 자동 도출.
  // editing existing = provider 변경 불가능 (key id 가 vault primary key 라 변경 = 신규 INSERT 와 동치).
  const onProviderChange = (newProviderId) => {
    if (!isNew) return;
    const candidate = AVAILABLE_PROVIDERS.find(p => p.id === newProviderId);
    if (!candidate) return;
    update({ provider: candidate.id, id: candidate.credentialKey });
    setCheck({ state: 'idle' });
  };

  const runCheck = async () => {
    setCheck({ state: 'running' });
    try {
      const result = await api('/admin/console/api/api-keys/validate', {
        method: 'POST',
        body: { provider: draft.provider, secret: draft.secret ?? '' },
      });
      setCheck({
        state: 'ok',
        ok: result.ok,
        message: result.message,
        latencyMs: result.latencyMs,
        modelsFound: result.modelsFound,
      });
    } catch (e) {
      setCheck({ state: 'err', message: e.message });
    }
  };

  return (
    <div className="amodal-back" onClick={onCancel}>
      <div className="amodal" onClick={e => e.stopPropagation()}>
        <header className="amodal-head">
          <span className="ahead-prompt">$</span>
          <span className="ahead-cmd">{isNew ? 'provider 추가' : 'apikey edit'}</span>
          <button className="amodal-x" onClick={onCancel}>esc</button>
        </header>
        <div className="amodal-body">
          <div className="afield-row">
            <div className="afield">
              <label>key id <span className="afield-hint">(unique vault handle)</span></label>
              <input
                className="ainput t-mono"
                value={draft.id}
                onChange={set('id')}
                placeholder="e.g. claude.prod"
                disabled={!isNew}
                autoFocus={isNew}
              />
            </div>
            <div className="afield">
              <label>provider</label>
              {isNew ? (
                // '+ provider 추가' = AVAILABLE_PROVIDERS 만 dropdown (운영자 directive 2026-05-17).
                // 카탈로그의 모든 provider 가 아닌 vault 미저장 candidate 만 노출.
                <select
                  className="ainput t-mono"
                  value={draft.provider}
                  onChange={(e) => onProviderChange(e.target.value)}
                >
                  {AVAILABLE_PROVIDERS.map(p => (
                    <option key={p.id} value={p.id}>
                      {ALL_PROVIDERS_META[p.id]?.name ?? p.displayName} · {p.credentialKey}
                    </option>
                  ))}
                </select>
              ) : (
                // editing existing — provider 변경 불가능 (key id = vault primary key).
                <input
                  className="ainput t-mono"
                  value={ALL_PROVIDERS_META[draft.provider]?.name ?? draft.provider}
                  disabled
                />
              )}
            </div>
          </div>

          <div className="afield-row">
            <div className="afield">
              <label>scope <span className="afield-hint">(라우팅 태그 prefix · * 면 전역)</span></label>
              <input className="ainput t-mono" value={draft.scope} onChange={set('scope')} placeholder="* | curation | listing" />
            </div>
            <div className="afield">
              <label>expires</label>
              <select className="ainput t-mono" value={draft.expires} onChange={set('expires')}>
                <option value="7d">7d</option>
                <option value="30d">30d</option>
                <option value="90d">90d</option>
                <option value="180d">180d</option>
                <option value="never">never</option>
              </select>
            </div>
          </div>

          <div className="afield">
            <label>
              secret <span className="afield-hint">
                {isNew ? `· ${meta.sample}` : '· 새 값을 입력하면 회전됩니다 (비워두면 유지)'}
              </span>
            </label>
            <input
              className="ainput t-mono"
              type="text"
              value={draft.secret}
              onChange={(e) => { update({ secret: e.target.value }); setCheck({ state: 'idle' }); }}
              placeholder={isNew ? meta.sample : '(unchanged)'}
              spellCheck={false}
              autoComplete="off"
            />
          </div>

          <div className="vbox">
            <div className="vbox-head">
              <span className="t-faint">VALIDATION · {meta.validateLabel}</span>
              <button
                className="abtn abtn-pri"
                type="button"
                disabled={!draft.secret || check.state === 'running'}
                onClick={runCheck}
              >
                {check.state === 'running' ? 'checking…' : '↻ validate now'}
              </button>
            </div>
            {check.state === 'idle' && (
              <div className="vbox-body t-faint">
                ↳ 시크릿을 입력한 뒤 validate를 누르면 provider에 ping을 날려 응답을 확인합니다.
              </div>
            )}
            {check.state === 'running' && (
              <div className="vbox-body t-acc">
                ↳ {meta.baseUrl || 'provider'}{meta.listEndpoint || ''} … <span className="blink">▮</span>
              </div>
            )}
            {check.state === 'ok' && (
              <div className={`vbox-body ${check.ok === false ? 'vbox-err' : 'vbox-ok'}`}>
                <StatusDot tone={check.ok === false ? 'warn' : 'ok'} />
                <span className={check.ok === false ? 't-warn' : 't-ok'}>
                  {check.ok === false ? 'ADVISORY' : `200 OK${check.latencyMs ? ` · ${check.latencyMs}ms` : ''}`}
                </span>
                <div className="t-faint" style={{ marginTop: 4 }}>↳ {check.message}</div>
              </div>
            )}
            {check.state === 'err' && (
              <div className="vbox-body vbox-err">
                <StatusDot tone="err" /> <span className="t-err">FAIL</span>
                <div className="t-faint" style={{ marginTop: 4 }}>↳ {check.message}</div>
              </div>
            )}
          </div>
        </div>
        <footer className="amodal-foot">
          {saveError && <span className="t-err" style={{ fontSize: 12, alignSelf: 'center' }}>⚠ {saveError}</span>}
          {!isNew && <button className="abtn abtn-danger" onClick={onDelete}>delete key</button>}
          <span style={{ flex: 1 }} />
          <button className="abtn" onClick={onCancel}>cancel</button>
          <button className="abtn abtn-pri" disabled={!valid} onClick={() => onSave(draft)}>
            {isNew ? 'create & store' : 'save'}
          </button>
        </footer>
      </div>
    </div>
  );
}

const AUDIT_VIEWS = ['agent-log', 'auto-curation', 'seed-lifecycle'];

function parseAdminHash() {
  if (typeof window === 'undefined') return { section: 'overview', params: new URLSearchParams() };
  const raw = (window.location.hash || '').replace(/^#/, '');
  if (!raw) return { section: 'overview', params: new URLSearchParams() };
  const idx = raw.indexOf('?');
  const section = idx >= 0 ? raw.slice(0, idx) : raw;
  const params = new URLSearchParams(idx >= 0 ? raw.slice(idx + 1) : '');
  return { section, params };
}

function resolveAuditViewFromHash() {
  const { section, params } = parseAdminHash();
  if (section !== 'audit') return 'agent-log';
  const v = params.get('view');
  return AUDIT_VIEWS.includes(v) ? v : 'agent-log';
}

function resolveAutoCurationDecisionTypeFromHash() {
  const { section, params } = parseAdminHash();
  if (section !== 'audit' || params.get('view') !== 'auto-curation') return '';
  const t = params.get('decisionType') || params.get('status') || '';
  return KNOWN_DECISION_TYPES.includes(t) ? t : '';
}

function resolveAutoCurationBucketFromHash() {
  const { section, params } = parseAdminHash();
  if (section !== 'audit' || params.get('view') !== 'auto-curation') return '';
  const t = params.get('selectionBucket') || '';
  return ['selected', 'not_selected', 'pending'].includes(t) ? t : '';
}

// v0.10.8 W2 (Spec 2026-05-17 §4.2.2) — /admin/auto-curation/decisions + seed-lifecycle 흡수.
// 3-view selector: agent-log (기존) / auto-curation (신규) / seed-lifecycle (신규).
// URL hash sync = #audit?view=auto-curation 형태로 deep link 호환.
function Audit() {
  const initialView = resolveAuditViewFromHash();
  const [view, setView] = useState(initialView);

  const onChangeView = (next) => {
    setView(next);
    if (typeof window !== 'undefined') {
      const newHash = next === 'agent-log' ? 'audit' : `audit?view=${next}`;
      if (window.location.hash.replace(/^#/, '') !== newHash) {
        window.history.pushState(null, '', '#' + newHash);
      }
    }
  };

  useEffect(() => {
    const onHash = () => setView(resolveAuditViewFromHash());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  return (
    <>
      <div className="aslab-row" style={{ gap: 8 }}>
        <h3 className="aslab" style={{ margin: 0 }}>VIEW</h3>
        <select className="ainput t-mono" value={view} onChange={(e) => onChangeView(e.target.value)} style={{ minWidth: 240 }}>
          <option value="agent-log">agent_decision_log (tail 50)</option>
          <option value="auto-curation">자동 큐레이션 결정 (auto_reject / approval / rejection)</option>
          <option value="seed-lifecycle">Seed Lifecycle (curator)</option>
        </select>
        <span className="ahint" style={{ flex: 1 }}>// 사후 검토 surface (read-only). hash deep-link 호환.</span>
      </div>

      {view === 'agent-log' && <AuditAgentLogView />}
      {view === 'auto-curation' && <AuditAutoCurationView />}
      {view === 'seed-lifecycle' && <AuditSeedLifecycleView />}
    </>
  );
}

// 기존 agent_decision_log tail 50 view.
function AuditAgentLogView() {
  const [filter, setFilter] = useState('');
  const filtered = AUDIT_LOG.filter(l =>
    !filter || l.agent.toLowerCase().includes(filter.toLowerCase())
            || l.decision.toLowerCase().includes(filter.toLowerCase())
            || l.tag.includes(filter.toLowerCase())
  );
  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">AGENT_DECISION_LOG · TAIL</h3>
        <input
          className="afilter"
          placeholder="grep agent / decision / tag..."
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
        />
      </div>
      <div className="audit">
        {filtered.map((l, i) => (
          <div key={i} className="audit-row">
            <span className="audit-ts">{l.ts}</span>
            <span className={`audit-tag tag-${l.tag}`}>{l.tag}</span>
            <span className="audit-agent">{l.agent}</span>
            <span className="audit-arrow">→</span>
            <span className="audit-decision">{l.decision}</span>
            <span className="audit-subject">{l.subject}</span>
            <span className="audit-reason">// {l.reason}</span>
            <span className="audit-cost">{l.cost}</span>
          </div>
        ))}
      </div>
    </>
  );
}

// v0.10.8 W2 — /admin/auto-curation/decisions 흡수. DecisionType 필터 chip + 50-row pagination.
function AuditAutoCurationView() {
  const initialType = resolveAutoCurationDecisionTypeFromHash() || AUTO_CURATION_DECISIONS.decisionTypeFilter || '';
  const initialBucket = initialType ? '' : (resolveAutoCurationBucketFromHash() || AUTO_CURATION_DECISIONS.selectionBucketFilter || '');
  const initialNeedsFetch = initialType !== (AUTO_CURATION_DECISIONS.decisionTypeFilter ?? '')
    || initialBucket !== (AUTO_CURATION_DECISIONS.selectionBucketFilter ?? '');
  const [data, setData] = useState(initialNeedsFetch ? { ...AUTO_CURATION_DECISIONS, rows: [], totalCount: 0, page: 1 } : AUTO_CURATION_DECISIONS);
  const [type, setType] = useState(initialType);
  const [bucket, setBucket] = useState(initialBucket);
  const [page, setPage] = useState(initialNeedsFetch ? 1 : (AUTO_CURATION_DECISIONS.page ?? 1));
  const [loading, setLoading] = useState(initialNeedsFetch);

  const fetchPage = async (nextType, nextPage, nextBucket = bucket) => {
    setLoading(true);
    try {
      const qs = new URLSearchParams();
      if (nextType) qs.set('decisionType', nextType);
      if (!nextType && nextBucket) qs.set('selectionBucket', nextBucket);
      qs.set('page', String(nextPage));
      const res = await api(`/admin/console/api/auto-curation/decisions?${qs.toString()}`);
      setData(res);
      setPage(res.page ?? nextPage);
    } catch (e) {
      showToast({ ok: false, code: 'fetch-failed', message: e.message });
    } finally {
      setLoading(false);
    }
  };

  const onTypeChange = (next) => {
    setType(next);
    setBucket('');
    fetchPage(next, 1, '');
    if (typeof window !== 'undefined') {
      const qs = new URLSearchParams();
      qs.set('view', 'auto-curation');
      if (next) qs.set('decisionType', next);
      window.history.pushState(null, '', `#audit?${qs.toString()}`);
    }
  };

  const onBucketChange = (next) => {
    setBucket(next);
    setType('');
    fetchPage('', 1, next);
    if (typeof window !== 'undefined') {
      const qs = new URLSearchParams();
      qs.set('view', 'auto-curation');
      if (next) qs.set('selectionBucket', next);
      window.history.pushState(null, '', `#audit?${qs.toString()}`);
    }
  };

  useEffect(() => {
    if (initialNeedsFetch) fetchPage(initialType, 1, initialBucket);
  }, []);

  useEffect(() => {
    const onHash = () => {
      const nextType = resolveAutoCurationDecisionTypeFromHash();
      const nextBucket = nextType ? '' : resolveAutoCurationBucketFromHash();
      setType(nextType);
      setBucket(nextBucket);
      fetchPage(nextType, 1, nextBucket);
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  const totalPages = Math.max(1, Math.ceil((data.totalCount ?? 0) / (data.pageSize ?? 50)));

  return (
    <>
      <div className="ov-grid" style={{ gridTemplateColumns: 'repeat(4, minmax(120px, 1fr))', marginTop: 8, marginBottom: 8 }}>
        <button className={`ov-card ${!bucket && !type ? 'abtn-pri' : ''}`} onClick={() => onBucketChange('')} style={{ textAlign: 'left', cursor: 'pointer' }}>
          <span className="ov-k">분석 상품</span>
          <span className="ov-v t-mono">{(data.analyzedCount ?? data.totalCount ?? 0).toLocaleString()}</span>
        </button>
        <button className={`ov-card ${bucket === 'selected' ? 'abtn-pri' : ''}`} onClick={() => onBucketChange('selected')} style={{ textAlign: 'left', cursor: 'pointer' }}>
          <span className="ov-k">선정 상품</span>
          <span className="ov-v t-mono">{(data.selectedCount ?? 0).toLocaleString()}</span>
        </button>
        <button className={`ov-card ${bucket === 'not_selected' ? 'abtn-pri' : ''}`} onClick={() => onBucketChange('not_selected')} style={{ textAlign: 'left', cursor: 'pointer' }}>
          <span className="ov-k">미선정 상품</span>
          <span className="ov-v t-mono">{(data.notSelectedCount ?? 0).toLocaleString()}</span>
        </button>
        <button className={`ov-card ${bucket === 'pending' ? 'abtn-pri' : ''}`} onClick={() => onBucketChange('pending')} style={{ textAlign: 'left', cursor: 'pointer' }}>
          <span className="ov-k">평가대기</span>
          <span className="ov-v t-mono">{(data.pendingCount ?? 0).toLocaleString()}</span>
        </button>
      </div>

      <div className="aslab-row" style={{ gap: 6, flexWrap: 'wrap' }}>
        <button className={`abtn ${!type ? 'abtn-pri' : ''}`} onClick={() => onTypeChange('')}>전체</button>
        {KNOWN_DECISION_TYPES.map(t => (
          <button key={t} className={`abtn ${type === t ? 'abtn-pri' : ''}`} onClick={() => onTypeChange(t)}>
            {t}
          </button>
        ))}
        <span style={{ flex: 1 }} />
        <span className="t-faint t-mono" style={{ fontSize: 11 }}>
          {loading ? 'loading…' : `${data.totalCount} rows · page ${page}/${totalPages}`}
        </span>
      </div>

      <table className="atbl" style={{ marginTop: 6 }}>
        <thead>
          <tr>
            <th>decided</th>
            <th>상품</th>
            <th>선정</th>
            <th>score / ROI</th>
            <th>구체 근거</th>
            <th>registration</th>
          </tr>
        </thead>
        <tbody>
          {data.rows.map(r => (
            <tr key={r.id}>
              <td className="t-mono" style={{ fontSize: 11 }}>{new Date(r.decidedAt).toLocaleString('en-GB')}</td>
              <td>
                <div style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 260 }}>
                  {r.thumbnailUrl && <img src={r.thumbnailUrl} alt="" style={{ width: 38, height: 38, objectFit: 'cover', borderRadius: 4 }} />}
                  <div style={{ minWidth: 0 }}>
                    <div title={r.productTitle} style={{ maxWidth: 360, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {r.sourceUrl ? <a href={r.sourceUrl} target="_blank" rel="noreferrer">{r.productTitle || r.poolCandidateIdShort}</a> : (r.productTitle || r.poolCandidateIdShort)}
                    </div>
                    <div className="t-faint t-mono" style={{ fontSize: 10 }}>
                      {r.sourceChannel ?? '—'} · {r.sourceProductId ?? r.poolCandidateIdShort} · {r.category ?? '미분류'} · {fmtKrw(r.originalPrice)}
                    </div>
                  </div>
                </div>
              </td>
              <td>
                <span className={`pill pill-${r.selectionBucket === 'not_selected' ? 'failed' : r.selectionBucket === 'selected' ? 'success' : 'scheduled'}`}>
                  {r.selectionLabel}
                </span>
                <div className="t-faint t-mono" style={{ fontSize: 10, marginTop: 3 }}>{r.decisionType}</div>
              </td>
              <td className="t-mono" style={{ fontSize: 11 }}>
                <div>Stage3 {r.stage3Score ?? '—'}</div>
                <div>ROI {fmtPctRate(r.preciseRoiRate ?? r.estimatedRoiRate)}</div>
                <div className="t-faint">매가 {fmtKrw(r.estimatedSellingPriceKrw)} · 마진 {fmtKrw(r.estimatedMarginKrw)}</div>
              </td>
              <td className="t-mono" style={{ fontSize: 11, maxWidth: 520, whiteSpace: 'normal' }} title={r.reasonsJson}>
                {r.selectionBasis || r.reasonsPreview}
              </td>
              <td className="t-mono t-faint" style={{ fontSize: 10 }}>{r.productRegistrationId ?? '—'}</td>
            </tr>
          ))}
          {data.rows.length === 0 && !loading && (
            <tr><td colSpan={5} className="t-faint" style={{ textAlign: 'center', padding: 12 }}>결정 기록 없음.</td></tr>
          )}
        </tbody>
      </table>

      {totalPages > 1 && (
        <div className="aslab-row" style={{ marginTop: 6, justifyContent: 'flex-end', gap: 4 }}>
          <button className="abtn" disabled={page <= 1 || loading} onClick={() => fetchPage(type, page - 1, bucket)}>← prev</button>
          <button className="abtn" disabled={page >= totalPages || loading} onClick={() => fetchPage(type, page + 1, bucket)}>next →</button>
        </div>
      )}
    </>
  );
}

// v0.10.8 W2 — /admin/worklog/curator seed lifecycle tab 흡수. seed 사후 추적.
function AuditSeedLifecycleView() {
  const [data, setData] = useState(SEED_LIFECYCLE);
  const [page, setPage] = useState(SEED_LIFECYCLE.page ?? 1);
  const [loading, setLoading] = useState(false);

  const fetchPage = async (nextPage) => {
    setLoading(true);
    try {
      const qs = new URLSearchParams();
      qs.set('page', String(nextPage));
      const res = await api(`/admin/console/api/seed-lifecycle?${qs.toString()}`);
      setData(res);
      setPage(res.page ?? nextPage);
    } catch (e) {
      showToast({ ok: false, code: 'fetch-failed', message: e.message });
    } finally {
      setLoading(false);
    }
  };

  const totalPages = Math.max(1, Math.ceil((data.totalCount ?? 0) / (data.pageSize ?? 50)));

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">SEED LIFECYCLE · CURATOR</h3>
        <span className="t-faint t-mono" style={{ fontSize: 11, marginLeft: 'auto' }}>
          {loading ? 'loading…' : `${data.totalCount} seeds · page ${page}/${totalPages}`}
        </span>
      </div>

      <table className="atbl">
        <thead>
          <tr>
            <th>handle</th>
            <th>stage</th>
            <th>status changed</th>
            <th>created</th>
            <th>seedId</th>
          </tr>
        </thead>
        <tbody>
          {data.rows.map(r => (
            <tr key={r.seedId}>
              <td className="t-id">{r.seedHandle}</td>
              <td><span className={`pill pill-${r.currentStage === 'approved' || r.currentStage === 'active' ? 'success' : r.currentStage === 'rejected' || r.currentStage === 'retire_candidate' || r.currentStage === 'retired' ? 'failed' : 'scheduled'}`}>{r.currentStage}</span></td>
              <td className="t-mono" style={{ fontSize: 11 }}>{new Date(r.statusChangedAt).toLocaleString('en-GB')}</td>
              <td className="t-mono t-faint" style={{ fontSize: 11 }}>{new Date(r.createdAt).toLocaleString('en-GB')}</td>
              <td className="t-mono t-faint" style={{ fontSize: 10 }}>{r.seedId}</td>
            </tr>
          ))}
          {data.rows.length === 0 && !loading && (
            <tr><td colSpan={5} className="t-faint" style={{ textAlign: 'center', padding: 12 }}>seed 기록 없음.</td></tr>
          )}
        </tbody>
      </table>

      {totalPages > 1 && (
        <div className="aslab-row" style={{ marginTop: 6, justifyContent: 'flex-end', gap: 4 }}>
          <button className="abtn" disabled={page <= 1 || loading} onClick={() => fetchPage(page - 1)}>← prev</button>
          <button className="abtn" disabled={page >= totalPages || loading} onClick={() => fetchPage(page + 1)}>next →</button>
        </div>
      )}
    </>
  );
}

function Telemetry() {
  const t = TELEMETRY;
  const reqHist = t.history.map(h => h[0]);
  const errHist = t.history.map(h => h[1]);
  const latHist = t.history.map(h => h[2]);
  return (
    <>
      <div className="ov-grid">
        <div className="ov-card">
          <div className="ov-card-head">REQ RATE</div>
          <div className="ov-budget"><span className="ov-budget-num">{t.reqRate}</span><span className="ov-budget-of">req/min</span></div>
          <Sparkline data={reqHist} w={220} h={28} />
        </div>
        <div className="ov-card">
          <div className="ov-card-head">ERROR RATE</div>
          <div className="ov-budget"><span className="ov-budget-num">{t.errorRate}%</span></div>
          <Sparkline data={errHist} w={220} h={28} />
        </div>
        <div className="ov-card">
          <div className="ov-card-head">P50 LATENCY</div>
          <div className="ov-budget"><span className="ov-budget-num">{t.p50}</span><span className="ov-budget-of">ms</span></div>
          <Sparkline data={latHist} w={220} h={28} />
        </div>
        <div className="ov-card">
          <div className="ov-card-head">PERCENTILES</div>
          <div className="ov-stack">
            <div><span className="ov-k">p50</span><span className="ov-v t-mono">{t.p50}ms</span></div>
            <div><span className="ov-k">p95</span><span className="ov-v t-mono">{t.p95}ms</span></div>
            <div><span className="ov-k">p99</span><span className="ov-v t-mono">{t.p99}ms</span></div>
          </div>
        </div>
      </div>
      <h3 className="aslab">OTel · 24h totals</h3>
      <div className="ov-stack ov-stack-h">
        <div><span className="ov-k">traces</span><span className="ov-v t-mono">{t.trace24h.toLocaleString()}</span></div>
        <div><span className="ov-k">spans</span><span className="ov-v t-mono">{t.spans24h.toLocaleString()}</span></div>
      </div>
    </>
  );
}

// ─── Kill Switch (v0.8.15 — replaces Flags) ─────────────────
// /admin/governance/kill-switch 흡수. 4-scope (all/agent/skill/plugin) + activate inline form
// + active list + recent history timeline.
function KillSwitch() {
  const data = KILL_SWITCHES ?? { active: [], recentHistory: [] };
  const [scope, setScope] = useState('all');
  const [scopeRef, setScopeRef] = useState(''); // agent/skill/plugin id
  const [reason, setReason] = useState('');
  const [expiresIn, setExpiresIn] = useState('never');
  const [busy, setBusy] = useState(false);

  const fullScope = scope === 'all' ? 'all' : (scopeRef ? `${scope}:${scopeRef}` : scope);

  const onActivate = async () => {
    if (!reason.trim()) {
      showToast({ ok: false, code: 'input-invalid', message: 'reason 은 필수 (감사 로그).' });
      return;
    }
    if (scope !== 'all' && !scopeRef.trim()) {
      showToast({ ok: false, code: 'input-invalid', message: `${scope} 시 id 필수.` });
      return;
    }
    setBusy(true);
    try {
      const scopeForServer = scope === 'all' ? 'All' :
        scope === 'agent' ? 'Agent' :
        scope === 'skill' ? 'Skill' :
        scope === 'plugin' ? 'Plugin' : scope;
      await postHandler('KillSwitchActivate', {
        scope: scopeForServer,
        targetId: scope === 'all' ? null : scopeRef.trim(),
        reason: reason.trim(),
        activatedBy: 'admin-console',
      });
      setReason('');
      setScopeRef('');
    } catch { /* toast already shown */ }
    finally { setBusy(false); }
  };

  const onDeactivate = async (id) => {
    setBusy(true);
    try {
      await postHandler('KillSwitchDeactivate', { id });
    } catch { /* toast already shown */ }
    finally { setBusy(false); }
  };

  return (
    <>
      <h3 className="aslab">+ ACTIVATE KILL SWITCH</h3>
      <div className="killsw-form">
        <div className="afield-row">
          <div className="afield">
            <label>scope</label>
            <select className="ainput t-mono" value={scope} onChange={(e) => setScope(e.target.value)}>
              <option value="all">all (전역)</option>
              <option value="agent">agent:&lt;slug&gt;</option>
              <option value="skill">skill:&lt;id&gt;</option>
              <option value="plugin">plugin:&lt;name&gt;</option>
            </select>
          </div>
          {scope !== 'all' && (
            <div className="afield">
              <label>{scope} id</label>
              <input
                className="ainput t-mono"
                value={scopeRef}
                onChange={(e) => setScopeRef(e.target.value)}
                placeholder={scope === 'agent' ? 'curator' : scope === 'skill' ? 'listing.publish' : 'paperless-bridge'}
              />
            </div>
          )}
          <div className="afield">
            <label>expires in</label>
            <select className="ainput t-mono" value={expiresIn} onChange={(e) => setExpiresIn(e.target.value)}>
              <option value="never">never (영구)</option>
              <option value="1h">1h</option>
              <option value="6h">6h</option>
              <option value="24h">24h</option>
              <option value="7d">7d</option>
            </select>
          </div>
        </div>
        <div className="afield">
          <label>reason <span className="afield-hint">(필수 — 감사 로그)</span></label>
          <textarea
            className="ainput t-mono"
            rows={2}
            value={reason}
            onChange={(e) => setReason(e.target.value)}
            placeholder="예: Rakuten 429 rate limit / production rollback / 운영자 검토 중"
          />
        </div>
        <div className="aslab-row" style={{ marginTop: 4 }}>
          <span className="t-faint" style={{ fontSize: 11 }}>
            scope = <span className="t-mono">{fullScope}</span> · expires = <span className="t-mono">{expiresIn}</span>
          </span>
          <span style={{ flex: 1 }} />
          <button
            className="abtn abtn-pri"
            disabled={busy}
            onClick={onActivate}
            title="POST /admin/console?handler=KillSwitchActivate"
          >
            {busy ? 'activating…' : 'activate'}
          </button>
        </div>
      </div>

      <h3 className="aslab" style={{ marginTop: 14 }}>ACTIVE <span className="ahint">({data.active.length})</span></h3>
      {data.active.length === 0 ? (
        <div className="t-faint" style={{ padding: '8px 0', fontSize: 11.5 }}>
          ↳ 활성 kill switch 없음.
        </div>
      ) : (
        <div className="killsw-list">
          {data.active.map(k => (
            <div key={k.id} className="killsw-row killsw-active">
              <div className="killsw-scope">
                <StatusDot tone="err" />
                <span className="t-mono">{k.scope}</span>
                {k.expiresAt && (
                  <span className="killsw-expires t-faint">expires {new Date(k.expiresAt).toLocaleString('en-GB')}</span>
                )}
                {!k.expiresAt && <span className="killsw-expires t-warn">영구</span>}
              </div>
              <div className="killsw-reason">// {k.reason}</div>
              <div className="killsw-meta t-faint">
                <span>by {k.activatedBy}</span>
                <span>· {k.activatedAt ? new Date(k.activatedAt).toLocaleString('en-GB') : '—'}</span>
                <button
                  className="abtn abtn-tiny"
                  disabled={busy}
                  onClick={() => onDeactivate(k.id)}
                  title="POST /admin/console?handler=KillSwitchDeactivate"
                >deactivate</button>
              </div>
            </div>
          ))}
        </div>
      )}

      <h3 className="aslab" style={{ marginTop: 14 }}>RECENT HISTORY <span className="ahint">(최근 50)</span></h3>
      {data.recentHistory.length === 0 ? (
        <div className="t-faint" style={{ padding: '8px 0', fontSize: 11.5 }}>↳ history 없음.</div>
      ) : (
        <div className="killsw-list">
          {data.recentHistory.map(k => (
            <div key={k.id} className="killsw-row killsw-history">
              <div className="killsw-scope">
                <StatusDot tone={k.deactivatedAt ? 'ok' : 'warn'} />
                <span className="t-mono">{k.scope}</span>
              </div>
              <div className="killsw-reason t-faint">// {k.reason}</div>
              <div className="killsw-meta t-faint">
                <span>by {k.activatedBy}</span>
                <span>· {k.activatedAt ? new Date(k.activatedAt).toLocaleString('en-GB') : '—'}</span>
                {k.deactivatedAt && <span>→ {new Date(k.deactivatedAt).toLocaleString('en-GB')}</span>}
              </div>
            </div>
          ))}
        </div>
      )}
    </>
  );
}

// ─── Tool Manifest (v0.8.15 — 7 sub-tab) ────────────────────
// /admin/{agents,skills,prompts,packages,marketplace,mcp-servers,plugins} 흡수.
// chip selector (기존 abtn-tiny 패턴) + 각 sub-tab 안에서 atbl-clickable + 상세 modal.
function ToolManifest() {
  const data = TOOL_MANIFEST;
  const [tab, setTab] = useState('agents');
  const [detail, setDetail] = useState(null); // { kind, row }

  const tabs = [
    { id: 'agents',   label: 'agents',   count: data.agents?.length ?? 0 },
    { id: 'skills',   label: 'skills',   count: data.skills?.length ?? 0 },
    { id: 'prompts',  label: 'prompts',  count: data.prompts?.length ?? 0 },
    { id: 'packages', label: 'packages', count: data.packages?.length ?? 0 },
    { id: 'market',   label: 'market',   count: null },
    { id: 'mcp',      label: 'mcp',      count: data.mcpServers?.length ?? 0 },
    { id: 'plugins',  label: 'plugins',  count: data.plugins?.length ?? 0 },
  ];

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">MANIFEST</h3>
        <div className="chip-group">
          {tabs.map(t => (
            <button
              key={t.id}
              className={`abtn abtn-tiny ${tab === t.id ? 'abtn-pri' : ''}`}
              onClick={() => setTab(t.id)}
            >
              {t.label}{t.count != null ? ` · ${t.count}` : ''}
            </button>
          ))}
        </div>
      </div>

      {tab === 'agents'   && <ManifestAgents   rows={data.agents   ?? []} onSelect={r => setDetail({ kind: 'agents',   row: r })} />}
      {tab === 'skills'   && <ManifestSkills   rows={data.skills   ?? []} onSelect={r => setDetail({ kind: 'skills',   row: r })} />}
      {tab === 'prompts'  && <ManifestPrompts  rows={data.prompts  ?? []} onSelect={r => setDetail({ kind: 'prompts',  row: r })} />}
      {tab === 'packages' && <ManifestPackages rows={data.packages ?? []} onSelect={r => setDetail({ kind: 'packages', row: r })} />}
      {tab === 'market'   && <ManifestMarket   overview={data.marketplace ?? {}} />}
      {tab === 'mcp'      && <ManifestMcp      rows={data.mcpServers ?? []} onSelect={r => setDetail({ kind: 'mcp', row: r })} />}
      {tab === 'plugins'  && <ManifestPlugins  rows={data.plugins  ?? []} onSelect={r => setDetail({ kind: 'plugins', row: r })} />}

      {detail && <ManifestDetailModal {...detail} onClose={() => setDetail(null)} />}
    </>
  );
}

function ManifestAgents({ rows, onSelect }) {
  if (rows.length === 0) {
    return <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 적재된 agent 없음.</div>;
  }
  return (
    <table className="atbl atbl-clickable">
      <thead><tr>
        <th>slug</th><th>name</th><th>status</th><th>skills</th><th>prompts</th>
        <th>workflows</th><th>last published</th><th>manifest hash</th>
      </tr></thead>
      <tbody>
        {rows.map(r => (
          <tr key={r.slug} onClick={() => onSelect(r)}>
            <td className="t-id t-mono">{r.slug}</td>
            <td>{r.name}</td>
            <td><span className={`pill pill-${r.status === 'active' ? 'success' : r.status === 'draft' ? 'scheduled' : 'failed'}`}>{r.status}</span></td>
            <td className="t-num">{r.skillCount ?? 0}</td>
            <td className="t-num">{r.promptCount ?? 0}</td>
            <td className="t-num">{r.workflowCount ?? 0}</td>
            <td className="t-mono t-faint">
              {r.lastPublishedAt ? new Date(r.lastPublishedAt).toLocaleString('en-GB') : '—'}
              {r.lastPublishedBy && <span className="t-faint"> · {r.lastPublishedBy}</span>}
            </td>
            <td className="t-mono t-faint" title={r.manifestHash}>{(r.manifestHash || '').slice(0, 10)}…</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function ManifestSkills({ rows, onSelect }) {
  const [dept, setDept] = useState('all');
  const [status, setStatus] = useState('all');
  const depts = ['all', 'curator', 'designer', 'listing-order', 'cs', 'tax', 'shared'];
  const statuses = ['all', 'active', 'draft', 'disabled'];
  const filtered = rows.filter(r =>
    (dept === 'all' || r.dept === dept) &&
    (status === 'all' || r.status === status));

  return (
    <>
      <div className="chip-group" style={{ marginBottom: 8 }}>
        <span className="t-faint" style={{ fontSize: 10.5, marginRight: 4 }}>dept</span>
        {depts.map(d => (
          <button key={d} className={`abtn abtn-tiny ${dept === d ? 'abtn-pri' : ''}`} onClick={() => setDept(d)}>{d}</button>
        ))}
        <span style={{ width: 14 }} />
        <span className="t-faint" style={{ fontSize: 10.5, marginRight: 4 }}>status</span>
        {statuses.map(s => (
          <button key={s} className={`abtn abtn-tiny ${status === s ? 'abtn-pri' : ''}`} onClick={() => setStatus(s)}>{s}</button>
        ))}
      </div>
      {filtered.length === 0 ? (
        <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 일치하는 skill 없음.</div>
      ) : (
        <table className="atbl atbl-clickable">
          <thead><tr>
            <th>id</th><th>title</th><th>dept</th><th>status</th><th>ver</th>
            <th>p50</th><th>runs/wk</th><th>cost/wk</th><th>approval</th><th>drift</th>
            <th>last edit</th>
          </tr></thead>
          <tbody>
            {filtered.map(r => (
              <tr key={r.id} onClick={() => onSelect(r)}>
                <td className="t-id t-mono">{r.id}</td>
                <td>{r.title}</td>
                <td className="t-faint t-mono">{r.dept}</td>
                <td><span className={`pill pill-${r.status === 'active' ? 'success' : r.status === 'draft' ? 'scheduled' : 'failed'}`}>{r.status}</span></td>
                <td className="t-mono t-faint">{r.version}</td>
                <td className="t-num">{r.runtimeP50Ms != null ? `${r.runtimeP50Ms}ms` : '—'}</td>
                <td className="t-num">{(r.weekRuns ?? 0).toLocaleString()}</td>
                <td className="t-num">{r.weekCostKrw ? `₩${Number(r.weekCostKrw).toLocaleString()}` : '—'}</td>
                <td>{r.requiresApproval ? <span className="pill pill-failed">required</span> : <span className="t-faint">—</span>}</td>
                <td><DriftBadge drift={r.drift} /></td>
                <td className="t-mono t-faint">
                  {r.lastEditAt ? new Date(r.lastEditAt).toLocaleString('en-GB') : '—'}
                  {r.lastEditBy && <span className="t-faint"> · {r.lastEditBy}</span>}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </>
  );
}

function DriftBadge({ drift }) {
  const d = drift ?? { status: 'ok' };
  const tone = d.status === 'ok' ? 'ok' : d.status === 'mismatch' ? 'err' : 'warn';
  const label = d.status === 'ok' ? 'ok' : d.status === 'jsonOnly' ? 'json-only' : d.status === 'dbOnly' ? 'db-only' : 'mismatch';
  return (
    <span className={`drift drift-${tone}`} title={d.detail || label}>
      <StatusDot tone={tone} /> <span style={{ fontSize: 10 }}>{label}</span>
    </span>
  );
}

function ManifestPrompts({ rows, onSelect }) {
  const [status, setStatus] = useState('all');
  const statuses = ['all', 'active', 'draft', 'archived'];
  const filtered = rows.filter(r => status === 'all' || r.status === status);
  return (
    <>
      <div className="chip-group" style={{ marginBottom: 8 }}>
        <span className="t-faint" style={{ fontSize: 10.5, marginRight: 4 }}>status</span>
        {statuses.map(s => (
          <button key={s} className={`abtn abtn-tiny ${status === s ? 'abtn-pri' : ''}`} onClick={() => setStatus(s)}>{s}</button>
        ))}
      </div>
      {filtered.length === 0 ? (
        <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 일치하는 prompt 없음.</div>
      ) : (
        <table className="atbl atbl-clickable">
          <thead><tr>
            <th>name</th><th>agent</th><th>version</th><th>status</th>
            <th>rollout %</th><th>rollback target</th><th>updated</th>
          </tr></thead>
          <tbody>
            {filtered.map(r => (
              <tr key={`${r.agentSlug}/${r.name}/${r.version}`} onClick={() => onSelect(r)}>
                <td className="t-id t-mono">{r.name}</td>
                <td className="t-faint t-mono">{r.agentSlug}</td>
                <td className="t-mono">{r.version}</td>
                <td><span className={`pill pill-${r.status === 'active' ? 'success' : r.status === 'draft' ? 'scheduled' : 'failed'}`}>{r.status}</span></td>
                <td className="t-num">{r.rolloutPercentage != null ? `${Number(r.rolloutPercentage).toFixed(0)}%` : '—'}</td>
                <td className="t-mono t-faint">{r.rollbackTarget || '—'}</td>
                <td className="t-mono t-faint">
                  {r.updatedAt ? new Date(r.updatedAt).toLocaleString('en-GB') : '—'}
                  {r.updatedBy && <span className="t-faint"> · {r.updatedBy}</span>}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </>
  );
}

function ManifestPackages({ rows, onSelect }) {
  if (rows.length === 0) {
    return <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 적재된 package 없음.</div>;
  }
  return (
    <table className="atbl atbl-clickable">
      <thead><tr>
        <th>slug</th><th>department</th><th>version</th>
        <th>skills</th><th>prompts</th><th>workflows</th><th>installed</th>
      </tr></thead>
      <tbody>
        {rows.map(r => (
          <tr key={r.slug} onClick={() => onSelect(r)}>
            <td className="t-id t-mono">{r.slug}</td>
            <td className="t-faint">{r.department}</td>
            <td className="t-mono">{r.version}</td>
            <td className="t-num">{r.skillCount ?? 0}</td>
            <td className="t-num">{r.promptCount ?? 0}</td>
            <td className="t-num">{r.workflowCount ?? 0}</td>
            <td className="t-num">
              {r.installedSkillCount ?? 0}
              {r.installedSkillCount !== r.skillCount && (
                <span className="t-warn" title="drift detected"> ⚠</span>
              )}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function ManifestMarket({ overview }) {
  const m = overview ?? {};
  return (
    <div className="ov-grid">
      <div className="ov-card">
        <div className="ov-card-head"><StatusDot tone="ok" /> PACKAGES</div>
        <div className="ov-budget"><span className="ov-budget-num">{m.packagesCount ?? 0}</span></div>
      </div>
      <div className="ov-card">
        <div className="ov-card-head"><StatusDot tone="ok" /> CATALOG</div>
        <div className="ov-budget"><span className="ov-budget-num">{m.catalogCount ?? 0}</span></div>
      </div>
      <div className="ov-card">
        <div className="ov-card-head"><StatusDot tone="ok" /> PLUGINS</div>
        <div className="ov-budget"><span className="ov-budget-num">{m.pluginsCount ?? 0}</span></div>
      </div>
      <div className="ov-card">
        <div className="ov-card-head"><StatusDot tone={(m.driftCount ?? 0) > 0 ? 'warn' : 'ok'} /> DRIFT</div>
        <div className="ov-budget">
          <span className="ov-budget-num">{m.driftCount ?? 0}</span>
          <span className="ov-budget-of">items mismatched</span>
        </div>
        <div className="t-faint" style={{ fontSize: 11, marginTop: 4 }}>
          snapshot {m.snapshotAt ? new Date(m.snapshotAt).toLocaleString('en-GB') : '—'}
        </div>
      </div>
    </div>
  );
}

function ManifestMcp({ rows, onSelect }) {
  const [transport, setTransport] = useState('all');
  const transports = ['all', 'http', 'stdio'];
  const filtered = rows.filter(r => transport === 'all' || r.transportType === transport);
  return (
    <>
      <div className="chip-group" style={{ marginBottom: 8 }}>
        <span className="t-faint" style={{ fontSize: 10.5, marginRight: 4 }}>transport</span>
        {transports.map(t => (
          <button key={t} className={`abtn abtn-tiny ${transport === t ? 'abtn-pri' : ''}`} onClick={() => setTransport(t)}>{t}</button>
        ))}
      </div>
      {filtered.length === 0 ? (
        <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 일치하는 mcp server 없음.</div>
      ) : (
        <table className="atbl atbl-clickable">
          <thead><tr>
            <th>slug</th><th>url</th><th>transport</th><th>auth</th>
            <th>status</th><th>tools</th><th>last discovery</th>
          </tr></thead>
          <tbody>
            {filtered.map(r => (
              <tr key={r.slug} onClick={() => onSelect(r)}>
                <td className="t-id t-mono">{r.slug}</td>
                <td className="t-mono t-faint">{r.url}</td>
                <td><span className="pill pill-scheduled">{r.transportType}</span></td>
                <td className="t-faint t-mono">{r.authRef || '—'}</td>
                <td>
                  <StatusDot tone={r.status === 'ok' ? 'ok' : r.status === 'degraded' ? 'warn' : 'err'} />
                  <span className="t-faint"> {r.status}</span>
                </td>
                <td className="t-num">{r.discoveredToolCount ?? 0}</td>
                <td className="t-mono t-faint">{r.lastDiscoveryAt ? new Date(r.lastDiscoveryAt).toLocaleString('en-GB') : '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </>
  );
}

function ManifestPlugins({ rows, onSelect }) {
  const [status, setStatus] = useState('all');
  const statuses = ['all', 'active', 'disabled', 'error'];
  const filtered = rows.filter(r => status === 'all' || r.status === status);
  return (
    <>
      <div className="chip-group" style={{ marginBottom: 8 }}>
        <span className="t-faint" style={{ fontSize: 10.5, marginRight: 4 }}>status</span>
        {statuses.map(s => (
          <button key={s} className={`abtn abtn-tiny ${status === s ? 'abtn-pri' : ''}`} onClick={() => setStatus(s)}>{s}</button>
        ))}
      </div>
      {filtered.length === 0 ? (
        <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 일치하는 plugin 없음.</div>
      ) : (
        <table className="atbl atbl-clickable">
          <thead><tr>
            <th>name</th><th>version</th><th>source</th><th>status</th>
            <th>tools</th><th>registered</th>
          </tr></thead>
          <tbody>
            {filtered.map(r => (
              <tr key={`${r.name}@${r.version}`} onClick={() => onSelect(r)}>
                <td className="t-id t-mono">{r.name}</td>
                <td className="t-mono">{r.version}</td>
                <td className="t-faint t-mono">{r.source}</td>
                <td><span className={`pill pill-${r.status === 'active' ? 'success' : r.status === 'disabled' ? 'scheduled' : 'failed'}`}>{r.status}</span></td>
                <td className="t-num">{r.exposedToolCount ?? 0}</td>
                <td className="t-mono t-faint">{r.registeredAt ? new Date(r.registeredAt).toLocaleString('en-GB') : '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </>
  );
}

function ManifestDetailModal({ kind, row, onClose }) {
  const [busy, setBusy] = useState(false);
  const cmd =
    kind === 'agents'   ? `agent show ${row.slug}` :
    kind === 'skills'   ? `skill show ${row.id}` :
    kind === 'prompts'  ? `prompt show ${row.agentSlug}/${row.name}` :
    kind === 'packages' ? `package show ${row.slug}` :
    kind === 'mcp'      ? `mcp show ${row.slug}` :
    kind === 'plugins'  ? `plugin show ${row.name}` :
    'detail';

  const run = async (fn) => {
    setBusy(true);
    try { await fn(); } catch { /* toast */ }
    finally { setBusy(false); }
  };

  // kind 별 mutation 버튼.
  const actions = (() => {
    if (kind === 'agents' && row.slug) {
      return (
        <>
          <button className="abtn" disabled={busy} title="Publish 는 manifest JSON 편집 별도 페이지 (후속) — 본 modal 은 snapshot 만." onClick={() => showToast({ ok: false, code: 'agent-publish-needs-editor', message: 'agent publish 는 manifest 편집기 후속 PR.' })}>publish</button>
        </>
      );
    }
    if (kind === 'prompts' && row.name) {
      // row 는 PromptRow {Name, AgentSlug, Version, Status, RolloutPercentage, ..., RollbackTarget}.
      // promptId 가 snapshot 에 없음 — W1 의 PromptRow 가 id 미보존. 후속 PR (D-W2-2).
      return (
        <>
          <span className="t-faint" style={{ fontSize: 11 }}>↳ prompt id 미노출 (W1 snapshot 한계, D-W2-2). 별도 관리 UI 필요.</span>
        </>
      );
    }
    if (kind === 'mcp' && row.slug) {
      // row 는 McpServerRow {Slug, Url, TransportType, AuthRef, Status, DiscoveredToolCount, LastDiscoveryAt}.
      // mcp server id 가 snapshot 에 없음 — Slug=Name 매핑이라 refresh 시 별도 lookup 필요.
      return (
        <>
          <span className="t-faint" style={{ fontSize: 11 }}>↳ refresh 는 server id 가 필요 (W1 snapshot 의 slug 만 노출, D-W2-2).</span>
        </>
      );
    }
    if (kind === 'plugins' && row.name) {
      // row 는 PluginRow {Name, Version, Source, Status, ExposedToolCount, RegisteredAt}.
      // plugin id 가 snapshot 에 없음 (D-W2-2).
      return (
        <>
          <span className="t-faint" style={{ fontSize: 11 }}>↳ status 전이는 plugin id 필요 (W1 snapshot 한계, D-W2-2).</span>
        </>
      );
    }
    if (kind === 'packages' && row.slug) {
      return (
        <button
          className="abtn abtn-pri"
          disabled={busy}
          onClick={() => run(() => postHandler('SkillImport', { department: row.slug }))}
          title="POST /admin/console?handler=SkillImport"
        >{busy ? 'importing…' : `import ${row.slug}`}</button>
      );
    }
    return null;
  })();

  return (
    <div className="amodal-back" onClick={onClose}>
      <div className="amodal amodal-wide" onClick={e => e.stopPropagation()}>
        <header className="amodal-head">
          <span className="ahead-prompt">$</span>
          <span className="ahead-cmd">{cmd}</span>
          <button className="amodal-x" onClick={onClose}>esc</button>
        </header>
        <div className="amodal-body">
          <div className="t-faint" style={{ fontSize: 11, marginBottom: 6 }}>
            ↳ snapshot · mutation 는 footer 의 action 버튼 (kind 별 분기).
          </div>
          <pre className="manifest-dump">
            {JSON.stringify(row, null, 2)}
          </pre>
        </div>
        <footer className="amodal-foot">
          {actions}
          <span style={{ flex: 1 }} />
          <button className="abtn" onClick={onClose}>close</button>
        </footer>
      </div>
    </div>
  );
}

// ─── Policy Sandbox (v0.8.15 — preset + sliders + backtest + active list) ──
// /admin/governance/policies 흡수. 30d backtest fixtures.
function PolicySandbox() {
  const data = POLICY_SANDBOX;
  const presets = data.presets ?? [];
  const [presetId, setPresetId] = useState(presets[0]?.id ?? '');
  const preset = presets.find(p => p.id === presetId);
  const [rules, setRules] = useState(() => preset ? preset.rules.map(r => ({ ...r })) : []);
  const [policyFilter, setPolicyFilter] = useState('all');
  const [busy, setBusy] = useState(false);
  const [showCreate, setShowCreate] = useState(false);
  const [newName, setNewName] = useState('');
  const [newVersion, setNewVersion] = useState('1.0.0');
  const [newPriority, setNewPriority] = useState(100);
  const policies = (data.activePolicies ?? []).filter(p => policyFilter === 'all' || p.status === policyFilter);
  const policyStatuses = ['all', 'active', 'draft', 'archived'];

  // backtest = client-side preset slider 평가 (server-side fixture projection 은 후속 PR).
  // 본 button 은 명시적 toast 만 emit — 별도 mutation 없음.
  const onBacktest = () => {
    showToast({
      ok: true,
      code: 'backtest-client',
      message: `30d fixture ${preset ? preset.rules.length : 0} rule 시뮬레이션 (서버 wire 후속 PR — D-W2-3).`,
    });
  };

  const buildPolicyJsonFromPreset = () => {
    if (!preset) return '{}';
    // 1st-level axis key = preset id 의 prefix (pricing.approval → "approval").
    const dot = preset.id.indexOf('.');
    const axis = dot < 0 ? preset.id : preset.id.substring(dot + 1);
    const body = {};
    rules.forEach(r => { body[r.key] = r.currentValue; });
    return JSON.stringify({ [axis]: body }, null, 2);
  };

  const onCreatePolicy = async () => {
    if (!newName.trim() || !preset) {
      showToast({ ok: false, code: 'input-invalid', message: 'name + preset 필수.' });
      return;
    }
    setBusy(true);
    try {
      await postHandler('PolicyCreate', {
        policyName: newName.trim(),
        version: newVersion.trim(),
        policyJson: buildPolicyJsonFromPreset(),
        priority: Number(newPriority) || 100,
      });
      setShowCreate(false);
      setNewName('');
    } catch { /* toast */ }
    finally { setBusy(false); }
  };

  const onPromotePolicy = async (id) => {
    setBusy(true);
    try { await postHandler('PolicyPromote', { policyId: id }); } catch {}
    finally { setBusy(false); }
  };

  // re-init rules when preset changes
  useEffect(() => {
    if (preset) setRules(preset.rules.map(r => ({ ...r })));
  }, [presetId]);

  const fixtures = preset
    ? (data.fixtures ?? []).filter(f => f.presetId === preset.id)
    : (data.fixtures ?? []);

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">PRESET</h3>
        {presets.length === 0 ? (
          <span className="t-faint" style={{ fontSize: 11 }}>no presets configured</span>
        ) : (
          <select className="ainput t-mono" value={presetId} onChange={e => setPresetId(e.target.value)} style={{ minWidth: 240 }}>
            {presets.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
        )}
        <span style={{ flex: 1 }} />
        <button className="abtn" onClick={onBacktest} title="client-side simulation; server fixture wire 후속 PR">↻ backtest</button>
      </div>

      {preset && (
        <>
          <div className="t-faint" style={{ fontSize: 11, marginBottom: 6 }}>{preset.desc}</div>
          <div className="rule-grid">
            {rules.map((r, i) => (
              <div key={r.key} className="rule-card">
                <div className="rule-head">
                  <span className="rule-lbl">{r.label}</span>
                  <span className="rule-val t-mono">{Number(r.currentValue).toFixed(r.step < 1 ? 2 : 0)}{r.unit ? ` ${r.unit}` : ''}</span>
                </div>
                <input
                  type="range"
                  min={r.min}
                  max={r.max}
                  step={r.step}
                  value={r.currentValue}
                  onChange={e => setRules(prev => prev.map((p, idx) => idx === i ? { ...p, currentValue: Number(e.target.value) } : p))}
                  className="rule-slider"
                />
                <div className="rule-foot t-faint">
                  <span>{r.min}{r.unit ? ` ${r.unit}` : ''}</span>
                  <span>{r.max}{r.unit ? ` ${r.unit}` : ''}</span>
                </div>
              </div>
            ))}
          </div>

          <h3 className="aslab">BACKTEST · 30d fixtures <span className="ahint">({fixtures.length} events)</span></h3>
          {fixtures.length === 0 ? (
            <div className="t-faint" style={{ padding: '8px 0', fontSize: 11.5 }}>↳ fixture 없음.</div>
          ) : (
            <table className="atbl">
              <thead><tr><th>at</th><th>event</th><th>actual</th><th>desc</th></tr></thead>
              <tbody>
                {fixtures.slice(0, 30).map(f => (
                  <tr key={`${f.presetId}/${f.eventId}`}>
                    <td className="t-mono t-faint">{f.at}</td>
                    <td className="t-id t-mono">{f.eventId}</td>
                    <td className="t-mono">{f.actual}</td>
                    <td className="t-faint">{f.desc}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </>
      )}

      <div className="aslab-row" style={{ marginTop: 14 }}>
        <h3 className="aslab">ACTIVE POLICIES</h3>
        <div className="chip-group">
          {policyStatuses.map(s => (
            <button key={s} className={`abtn abtn-tiny ${policyFilter === s ? 'abtn-pri' : ''}`} onClick={() => setPolicyFilter(s)}>{s}</button>
          ))}
        </div>
        <span style={{ flex: 1 }} />
        <button
          className="abtn abtn-pri"
          disabled={busy || !preset}
          onClick={() => setShowCreate(s => !s)}
          title="POST /admin/console?handler=PolicyCreate"
        >+ new policy</button>
      </div>
      {showCreate && preset && (
        <div className="afield" style={{ display: 'flex', gap: 8, marginTop: 6, alignItems: 'flex-end', flexWrap: 'wrap' }}>
          <div className="afield" style={{ flex: '1 1 200px' }}>
            <label>name</label>
            <input className="ainput t-mono" value={newName} onChange={e => setNewName(e.target.value)} placeholder={`e.g. ${preset.id}.v1`} />
          </div>
          <div className="afield" style={{ flex: '0 0 100px' }}>
            <label>version</label>
            <input className="ainput t-mono" value={newVersion} onChange={e => setNewVersion(e.target.value)} />
          </div>
          <div className="afield" style={{ flex: '0 0 100px' }}>
            <label>priority</label>
            <input type="number" className="ainput t-mono" value={newPriority} onChange={e => setNewPriority(e.target.value)} />
          </div>
          <button className="abtn abtn-pri" disabled={busy} onClick={onCreatePolicy}>{busy ? 'creating…' : 'create'}</button>
          <button className="abtn" disabled={busy} onClick={() => setShowCreate(false)}>cancel</button>
        </div>
      )}
      {policies.length === 0 ? (
        <div className="t-faint" style={{ padding: '8px 0', fontSize: 11.5 }}>↳ 일치하는 policy 없음.</div>
      ) : (
        <table className="atbl">
          <thead><tr>
            <th>name</th><th>axisKind</th><th>status</th><th>createdBy</th><th>createdAt</th><th></th>
          </tr></thead>
          <tbody>
            {policies.map(p => (
              <tr key={p.id || p.name}>
                <td className="t-id t-mono">{p.name}</td>
                <td><span className="pill pill-scheduled">{p.axisKind}</span></td>
                <td><span className={`pill pill-${p.status === 'active' ? 'success' : p.status === 'draft' ? 'scheduled' : 'failed'}`}>{p.status}</span></td>
                <td className="t-faint t-mono">{p.createdBy || '—'}</td>
                <td className="t-mono t-faint">{p.createdAt ? new Date(p.createdAt).toLocaleString('en-GB') : '—'}</td>
                <td>
                  {p.status === 'draft' && p.id && (
                    <button
                      className="abtn abtn-tiny abtn-pri"
                      disabled={busy}
                      onClick={() => onPromotePolicy(p.id)}
                      title="POST /admin/console?handler=PolicyPromote"
                    >promote</button>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </>
  );
}

// ─── Tasks (v0.8.15 — neue 섹션) ────────────────────────────
// /admin/tasks 흡수. filter chips + grep + row click → detail modal.
function Tasks() {
  const recent = TASKS.recent ?? [];
  const [stateFilter, setStateFilter] = useState('all');
  const [grep, setGrep] = useState('');
  const [detail, setDetail] = useState(null);
  const states = ['all', 'queued', 'running', 'completed', 'failed', 'cancelled'];

  const filtered = recent.filter(t =>
    (stateFilter === 'all' || t.state === stateFilter) &&
    (!grep || (t.slug || '').toLowerCase().includes(grep.toLowerCase()) || (t.taskId || '').toLowerCase().includes(grep.toLowerCase()))
  );

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">TASKS · TAIL</h3>
        <div className="chip-group">
          {states.map(s => (
            <button key={s} className={`abtn abtn-tiny ${stateFilter === s ? 'abtn-pri' : ''}`} onClick={() => setStateFilter(s)}>{s}</button>
          ))}
        </div>
        <input
          className="afilter"
          placeholder="grep slug / taskId..."
          value={grep}
          onChange={e => setGrep(e.target.value)}
        />
      </div>
      {filtered.length === 0 ? (
        <div className="t-faint" style={{ padding: '10px 0', fontSize: 11.5 }}>↳ 일치하는 task 없음.</div>
      ) : (
        <table className="atbl atbl-clickable">
          <thead><tr>
            <th>taskId</th><th>slug</th><th>state</th><th>correlation</th>
            <th>started</th><th>duration</th><th>steps</th><th></th>
          </tr></thead>
          <tbody>
            {filtered.map(t => (
              <tr key={t.taskId} onClick={() => setDetail(t)}>
                <td className="t-id t-mono">{t.taskId}</td>
                <td className="t-mono">{t.slug}</td>
                <td><span className={`pill task-state task-state-${t.state}`}>{t.state}</span></td>
                <td className="t-mono t-faint">{t.correlationId || '—'}</td>
                <td className="t-mono t-faint">{t.startedAt ? new Date(t.startedAt).toLocaleString('en-GB') : '—'}</td>
                <td className="t-num">{t.durationMs != null ? `${t.durationMs}ms` : '—'}</td>
                <td className="t-num">{t.stepCount ?? 0}</td>
                <td>{t.canCancel ? <span className="t-faint" style={{ fontSize: 11 }}>cancellable</span> : <span className="t-faint" style={{ fontSize: 11 }}>—</span>}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}

      {detail && <TaskDetailModal task={detail} onClose={() => setDetail(null)} />}
    </>
  );
}

function TaskDetailModal({ task, onClose }) {
  const [busy, setBusy] = useState(false);
  const copyCorrelation = () => {
    if (task.correlationId && navigator.clipboard) navigator.clipboard.writeText(task.correlationId);
  };
  const onCancel = async () => {
    setBusy(true);
    try {
      await postHandler('TaskCancel', { taskId: task.taskId });
      onClose();
    } catch { /* toast already */ }
    finally { setBusy(false); }
  };
  return (
    <div className="amodal-back" onClick={onClose}>
      <div className="amodal amodal-wide" onClick={e => e.stopPropagation()}>
        <header className="amodal-head">
          <span className="ahead-prompt">$</span>
          <span className="ahead-cmd">task show {task.taskId}</span>
          <button className="amodal-x" onClick={onClose}>esc</button>
        </header>
        <div className="amodal-body">
          <div className="ov-stack">
            <div><span className="ov-k">taskId</span><span className="ov-v t-mono">{task.taskId}</span></div>
            <div><span className="ov-k">slug</span><span className="ov-v t-mono">{task.slug}</span></div>
            <div><span className="ov-k">state</span><span className="ov-v"><span className={`pill task-state task-state-${task.state}`}>{task.state}</span></span></div>
            <div>
              <span className="ov-k">correlationId</span>
              <span className="ov-v t-mono">
                {task.correlationId || '—'}
                {task.correlationId && (
                  <button className="abtn abtn-tiny" style={{ marginLeft: 6 }} onClick={copyCorrelation}>copy</button>
                )}
              </span>
            </div>
            <div><span className="ov-k">startedAt</span><span className="ov-v t-mono">{task.startedAt ? new Date(task.startedAt).toLocaleString('en-GB') : '—'}</span></div>
            <div><span className="ov-k">duration</span><span className="ov-v t-mono">{task.durationMs != null ? `${task.durationMs}ms` : '—'}</span></div>
            <div><span className="ov-k">steps</span><span className="ov-v t-mono">{task.stepCount ?? 0}</span></div>
          </div>
          <div className="t-faint" style={{ fontSize: 11, marginTop: 10 }}>
            ↳ step timeline 은 후속 PR (agent_run_steps join + lazy fetch).
          </div>
        </div>
        <footer className="amodal-foot">
          <span style={{ flex: 1 }} />
          <button
            className="abtn abtn-danger"
            disabled={busy || !task.canCancel}
            onClick={onCancel}
            title={task.canCancel ? "POST /admin/console?handler=TaskCancel" : "terminal state — cancel 무의미"}
          >{busy ? 'cancelling…' : 'cancel'}</button>
          <button className="abtn" onClick={onClose}>close</button>
        </footer>
      </div>
    </div>
  );
}

// ─── Site (마스트헤드 회사명) ─────────────────────────────
// 결재일보·결재함 마스트헤드 타이틀(회사명) 단일 row 편집.
// data shape = { companyName, displayTitle, isCustomized, updatedAt }
const SITE_DATA = __INJECT__.site ?? { companyName: '', displayTitle: '결재', isCustomized: false, updatedAt: null };

function Site() {
  const [companyName, setCompanyName] = useState(SITE_DATA.companyName ?? '');
  const [displayTitle, setDisplayTitle] = useState(SITE_DATA.displayTitle ?? '결재');
  const [updatedAt, setUpdatedAt] = useState(SITE_DATA.updatedAt);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState(null);
  const [saved, setSaved] = useState(false);

  const trimmed = companyName.trim();
  const overLimit = trimmed.length > 30;
  const previewTitle = trimmed.length > 0 ? trimmed : '결재';

  const save = async () => {
    setSaveError(null);
    setSaved(false);
    setSaving(true);
    try {
      const result = await api('/admin/console/api/site-settings', {
        method: 'PUT',
        body: { companyName: trimmed },
      });
      setCompanyName(result.companyName ?? '');
      setDisplayTitle(result.displayTitle ?? '결재');
      setUpdatedAt(result.updatedAt);
      setSaved(true);
    } catch (e) {
      setSaveError(e.message);
    } finally {
      setSaving(false);
    }
  };

  return (
    <>
      {saveError && (
        <div className="amodal-back" onClick={() => setSaveError(null)}>
          <div className="amodal" onClick={e => e.stopPropagation()} style={{ maxWidth: 480 }}>
            <header className="amodal-head">
              <span className="ahead-prompt">!</span>
              <span className="ahead-cmd">site · error</span>
              <button className="amodal-x" onClick={() => setSaveError(null)}>esc</button>
            </header>
            <div className="amodal-body">
              <div className="t-err">⚠ {saveError}</div>
            </div>
          </div>
        </div>
      )}

      <div className="aslab-row">
        <h3 className="aslab">COMPANY NAME <span className="ahint">(결재일보 · 결재함 마스트헤드)</span></h3>
        {updatedAt && (
          <span className="t-faint t-mono" style={{ fontSize: 11 }}>
            updated {new Date(updatedAt).toLocaleString('en-GB')}
          </span>
        )}
      </div>
      <div className="afield">
        <label>
          companyName
          <span className="afield-hint"> · 1~30자, 비우면 기본값 "결재" 표시</span>
        </label>
        <input
          className="ainput t-mono"
          value={companyName}
          onChange={(e) => { setCompanyName(e.target.value); setSaved(false); }}
          maxLength={30}
          placeholder="예: 갤럭시"
          spellCheck={false}
        />
        {overLimit && <div className="t-err" style={{ fontSize: 12, marginTop: 4 }}>30자 초과</div>}
      </div>

      <div className="vbox" style={{ marginTop: 4 }}>
        <div className="vbox-head">
          <span className="t-faint">↳ preview (마스트헤드 표시)</span>
        </div>
        <div className="vbox-body">
          <div className="t-mono" style={{ fontSize: 22, fontWeight: 700 }}>{previewTitle}</div>
          <div className="t-faint" style={{ fontSize: 11, marginTop: 4 }}>
            현재 적용 = <span className="t-mono">{displayTitle}</span>
          </div>
        </div>
      </div>

      <div className="aslab-row" style={{ marginTop: 8 }}>
        <span style={{ flex: 1 }}>
          {saved && <span className="t-ok" style={{ fontSize: 12 }}>✓ 저장됨</span>}
        </span>
        <button className="abtn abtn-pri" disabled={overLimit || saving} onClick={save}>
          {saving ? 'saving…' : '↻ save'}
        </button>
      </div>

      {/* v0.10.8 W2 (Spec 2026-05-17 §4.2.4) — /admin/category-markup + /edit 흡수 */}
      <CategoryMarkupSection />
    </>
  );
}

// v0.10.8 W2 — #site 의 신규 sub-section: Category Markup CRUD row.
// 목록 = category / markup × / notes / updatedAt + inline edit + delete + 신규 add.
function CategoryMarkupSection() {
  const [rows, setRows] = useState(CATEGORY_MARKUP.rows);
  const [editing, setEditing] = useState(null); // null | { category, markup, notes, isNew }
  const [busy, setBusy] = useState(null);

  const refresh = async () => {
    try {
      const res = await api('/admin/console/api/category-markup');
      setRows(res.rows ?? []);
    } catch (e) {
      showToast({ ok: false, code: 'fetch-failed', message: e.message });
    }
  };

  const onCreate = () => setEditing({ category: '', markup: 2.0, notes: '', isNew: true });
  const onEdit = (row) => setEditing({ category: row.category, markup: row.markup, notes: row.notes ?? '', isNew: false });
  const onCancel = () => setEditing(null);

  const onSave = async () => {
    if (!editing.category.trim()) {
      showToast({ ok: false, code: 'input-invalid', message: 'category 필수.' });
      return;
    }
    if (editing.markup < 0.5 || editing.markup > 10) {
      showToast({ ok: false, code: 'input-invalid', message: 'markup 은 0.5 ~ 10 사이.' });
      return;
    }
    setBusy('save');
    try {
      const handler = editing.isNew ? 'CategoryMarkupCreate' : 'CategoryMarkupUpdate';
      await postHandler(handler, {
        category: editing.category.trim(),
        markup: Number(editing.markup),
        notes: editing.notes.trim() || null,
      });
      setEditing(null);
      await refresh();
    } catch { /* toast */ }
    finally { setBusy(null); }
  };

  const onDelete = async (category) => {
    if (!confirm(`'${category}' 삭제 하시겠습니까?`)) return;
    setBusy(`del:${category}`);
    try {
      await postHandler('CategoryMarkupDelete', { category });
      await refresh();
    } catch { /* toast */ }
    finally { setBusy(null); }
  };

  return (
    <div style={{ marginTop: 20, borderTop: '1px solid rgba(245,201,122,0.18)', paddingTop: 14 }}>
      <div className="aslab-row">
        <h3 className="aslab">CATEGORY MARKUP <span className="ahint">(/admin/category-markup 흡수)</span></h3>
        <span style={{ flex: 1 }} />
        <button className="abtn" onClick={onCreate} disabled={!!editing}>+ row 추가</button>
      </div>

      <table className="atbl">
        <thead>
          <tr>
            <th>category</th>
            <th>markup ×</th>
            <th>notes</th>
            <th>updated</th>
            <th>actions</th>
          </tr>
        </thead>
        <tbody>
          {rows.map(r => (
            <tr key={r.category}>
              <td className="t-id">{r.category}</td>
              <td className="t-num">{r.markup.toFixed(3)}×</td>
              <td className="t-mono" style={{ fontSize: 11 }}>{r.notes ?? <span className="t-faint">—</span>}</td>
              <td className="t-faint t-mono" style={{ fontSize: 11 }}>{new Date(r.updatedAt).toLocaleString('en-GB')}</td>
              <td>
                <button className="abtn" onClick={() => onEdit(r)} disabled={!!editing} style={{ marginRight: 4 }}>edit</button>
                <button className="abtn" onClick={() => onDelete(r.category)} disabled={busy === `del:${r.category}`}>
                  {busy === `del:${r.category}` ? '…' : '✗ del'}
                </button>
              </td>
            </tr>
          ))}
          {rows.length === 0 && (
            <tr><td colSpan={5} className="t-faint" style={{ textAlign: 'center', padding: 12 }}>category markup 없음.</td></tr>
          )}
        </tbody>
      </table>

      {editing && (
        <div className="afield" style={{ background: 'rgba(255,255,255,0.02)', padding: 10, borderRadius: 4, marginTop: 8 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 8 }}>
            <label>
              category <span className="afield-hint">· PK (e.g. 07_001)</span>
              <input
                className="ainput t-mono"
                value={editing.category}
                onChange={(e) => setEditing({ ...editing, category: e.target.value })}
                disabled={!editing.isNew}
                maxLength={128}
                placeholder="07_001"
              />
            </label>
            <label>
              markup × <span className="afield-hint">· 0.5 ~ 10</span>
              <input
                className="ainput t-mono"
                type="number"
                min={0.5}
                max={10}
                step={0.05}
                value={editing.markup}
                onChange={(e) => setEditing({ ...editing, markup: parseFloat(e.target.value) || 0 })}
              />
            </label>
            <label style={{ gridColumn: '1 / 3' }}>
              notes
              <input
                className="ainput t-mono"
                value={editing.notes}
                onChange={(e) => setEditing({ ...editing, notes: e.target.value })}
                placeholder="(optional)"
              />
            </label>
          </div>
          <div className="aslab-row" style={{ marginTop: 8 }}>
            <span className="t-faint" style={{ fontSize: 11, flex: 1 }}>
              Markup = JP 시장가 multiplier (Stage 3+4 ROI 추정 외부화 상수)
            </span>
            <button className="abtn" onClick={onCancel}>cancel</button>
            <button className="abtn abtn-pri" onClick={onSave} disabled={busy === 'save'}>
              {busy === 'save' ? 'saving…' : '↻ save'}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── DocStorage (Paperless-ngx) ─────────────────────────────
// Paperless Integration E1 (Spec 2026-05-07 §2.1 §4.1) — admin UI 가
// settings + api token + test connection 을 직접 조작.
// data shape = { settings, hasToken, lastConnectionTest }
const PAPERLESS_DATA = __INJECT__.paperless ?? { settings: null, hasToken: false, lastConnectionTest: null };

function DocStorage() {
  const initial = PAPERLESS_DATA.settings ?? {
    baseUrl: '', correspondentName: '', expiryFieldName: '',
    orderIdFieldName: '', kindFieldName: '',
    updatedAt: null, updatedBy: null,
  };
  const [draft, setDraft] = useState(initial);
  const [hasToken, setHasToken] = useState(PAPERLESS_DATA.hasToken);
  const [tokenInput, setTokenInput] = useState('');
  const [lastTest, setLastTest] = useState(PAPERLESS_DATA.lastConnectionTest);
  const [testing, setTesting] = useState(false);
  const [saving, setSaving] = useState(false);
  const [tokenSaving, setTokenSaving] = useState(false);
  const [saveError, setSaveError] = useState(null);

  const update = (patch) => setDraft(d => ({ ...d, ...patch }));
  const set = (k) => (e) => update({ [k]: e.target.value });

  const settingsValid = draft.baseUrl.trim() && draft.correspondentName.trim()
    && draft.expiryFieldName.trim() && draft.orderIdFieldName.trim() && draft.kindFieldName.trim();

  const saveSettings = async () => {
    setSaveError(null);
    setSaving(true);
    try {
      const result = await api('/admin/console/api/paperless/settings', {
        method: 'PUT',
        body: {
          baseUrl: draft.baseUrl.trim(),
          correspondentName: draft.correspondentName.trim(),
          expiryFieldName: draft.expiryFieldName.trim(),
          orderIdFieldName: draft.orderIdFieldName.trim(),
          kindFieldName: draft.kindFieldName.trim(),
        },
      });
      update({ updatedAt: result.updatedAt, updatedBy: result.updatedBy });
    } catch (e) {
      setSaveError(e.message);
    } finally {
      setSaving(false);
    }
  };

  const saveToken = async () => {
    if (!tokenInput.trim()) return;
    setSaveError(null);
    setTokenSaving(true);
    try {
      await api('/admin/console/api/paperless/api-token', {
        method: 'PUT',
        body: { token: tokenInput.trim() },
      });
      setHasToken(true);
      setTokenInput('');
    } catch (e) {
      setSaveError(e.message);
    } finally {
      setTokenSaving(false);
    }
  };

  const deleteToken = async () => {
    setSaveError(null);
    setTokenSaving(true);
    try {
      await api('/admin/console/api/paperless/api-token', { method: 'DELETE' });
      setHasToken(false);
    } catch (e) {
      setSaveError(e.message);
    } finally {
      setTokenSaving(false);
    }
  };

  const runTest = async () => {
    setSaveError(null);
    setTesting(true);
    try {
      const result = await api('/admin/console/api/paperless/test-connection', {
        method: 'POST',
        body: {},
      });
      setLastTest({
        success: result.success,
        statusCode: result.statusCode,
        failureReason: result.failureReason,
        username: result.username,
        customFieldNames: result.customFieldNames,
        testedAt: result.testedAt,
      });
    } catch (e) {
      setSaveError(e.message);
    } finally {
      setTesting(false);
    }
  };

  return (
    <>
      {saveError && (
        <div className="amodal-back" onClick={() => setSaveError(null)}>
          <div className="amodal" onClick={e => e.stopPropagation()} style={{ maxWidth: 480 }}>
            <header className="amodal-head">
              <span className="ahead-prompt">!</span>
              <span className="ahead-cmd">paperless · error</span>
              <button className="amodal-x" onClick={() => setSaveError(null)}>esc</button>
            </header>
            <div className="amodal-body">
              <div className="t-err">⚠ {saveError}</div>
            </div>
          </div>
        </div>
      )}

      <div className="aslab-row">
        <h3 className="aslab">SETTINGS <span className="ahint">(paperless-ngx 연결 + custom field 매핑)</span></h3>
        {draft.updatedAt && (
          <span className="t-faint t-mono" style={{ fontSize: 11 }}>
            updated {new Date(draft.updatedAt).toLocaleString('en-GB')} · {draft.updatedBy ?? '—'}
          </span>
        )}
      </div>
      <div className="afield">
        <label>baseUrl <span className="afield-hint">(absolute URL — http(s)://host:port/)</span></label>
        <input className="ainput t-mono" value={draft.baseUrl} onChange={set('baseUrl')} placeholder="https://paperless.home.local/" />
      </div>
      <div className="afield-row">
        <div className="afield">
          <label>correspondentName</label>
          <input className="ainput t-mono" value={draft.correspondentName} onChange={set('correspondentName')} placeholder="PurchaseAgent" />
        </div>
        <div className="afield">
          <label>kindFieldName</label>
          <input className="ainput t-mono" value={draft.kindFieldName} onChange={set('kindFieldName')} placeholder="kind" />
        </div>
      </div>
      <div className="afield-row">
        <div className="afield">
          <label>expiryFieldName</label>
          <input className="ainput t-mono" value={draft.expiryFieldName} onChange={set('expiryFieldName')} placeholder="expiry" />
        </div>
        <div className="afield">
          <label>orderIdFieldName</label>
          <input className="ainput t-mono" value={draft.orderIdFieldName} onChange={set('orderIdFieldName')} placeholder="order_id" />
        </div>
      </div>
      <div className="aslab-row" style={{ marginTop: 4 }}>
        <span style={{ flex: 1 }} />
        <button className="abtn abtn-pri" disabled={!settingsValid || saving} onClick={saveSettings}>
          {saving ? 'saving…' : '↻ save settings'}
        </button>
      </div>

      <h3 className="aslab" style={{ marginTop: 18 }}>API TOKEN <span className="ahint">(write-only — Infisical vault 저장)</span></h3>
      <div className="afield">
        <label>
          token
          <span className="afield-hint">
            · {hasToken ? <span className="t-ok">stored</span> : <span className="t-warn">missing</span>}
          </span>
        </label>
        <div className="afield-row" style={{ gap: 8 }}>
          <input
            className="ainput t-mono"
            type="password"
            value={tokenInput}
            onChange={(e) => setTokenInput(e.target.value)}
            placeholder={hasToken ? '(unchanged — type new value to rotate)' : 'paperless-ngx api token'}
            spellCheck={false}
            autoComplete="off"
            style={{ flex: 1 }}
          />
          <button className="abtn abtn-pri" disabled={!tokenInput.trim() || tokenSaving} onClick={saveToken}>
            {tokenSaving ? '…' : hasToken ? 'rotate' : 'save'}
          </button>
          {hasToken && (
            <button className="abtn abtn-danger" disabled={tokenSaving} onClick={deleteToken}>
              delete
            </button>
          )}
        </div>
      </div>

      <h3 className="aslab" style={{ marginTop: 18 }}>TEST CONNECTION <span className="ahint">(GET users/me + GET custom_fields)</span></h3>
      <div className="vbox">
        <div className="vbox-head">
          <span className="t-faint">↳ uses stored baseUrl + token (cache 5분)</span>
          <button className="abtn abtn-pri" type="button" disabled={testing} onClick={runTest}>
            {testing ? 'checking…' : '↻ test now'}
          </button>
        </div>
        {!lastTest && !testing && (
          <div className="vbox-body t-faint">
            ↳ 결과 없음 — test 버튼을 눌러 paperless 인스턴스 연결을 확인하세요.
          </div>
        )}
        {testing && (
          <div className="vbox-body t-acc">
            ↳ paperless · users/me + custom_fields … <span className="blink">▮</span>
          </div>
        )}
        {!testing && lastTest && (
          <div className={`vbox-body ${lastTest.success ? 'vbox-ok' : 'vbox-err'}`}>
            <StatusDot tone={lastTest.success ? 'ok' : 'err'} />
            <span className={lastTest.success ? 't-ok' : 't-err'}>
              {lastTest.success
                ? `200 OK · user=${lastTest.username ?? '—'}`
                : `FAIL${lastTest.statusCode ? ` · ${lastTest.statusCode}` : ''}`}
            </span>
            <span className="t-faint t-mono" style={{ fontSize: 11, marginLeft: 8 }}>
              {lastTest.testedAt ? new Date(lastTest.testedAt).toLocaleString('en-GB') : '—'}
            </span>
            {lastTest.failureReason && (
              <div className="t-faint" style={{ marginTop: 4 }}>↳ {lastTest.failureReason}</div>
            )}
            {lastTest.success && lastTest.customFieldNames && lastTest.customFieldNames.length > 0 && (
              <div style={{ marginTop: 6 }}>
                <span className="t-faint">custom fields ({lastTest.customFieldNames.length}):</span>
                <div className="t-mono" style={{ marginTop: 2, fontSize: 12 }}>
                  {lastTest.customFieldNames.map(n => (
                    <span key={n} className="provtag" style={{ marginRight: 4 }}>{n}</span>
                  ))}
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    </>
  );
}

// ─── Chat Prompts (v0.9.19 Spec 2026-05-15 §5) ────────────────
// chat agent + Briefing system prompt DB editing surface.
// 9 row listing → row click → textarea expand → Save → POST handler → toast + cache invalidate.
// Placeholder helper: chat.* → {{COMMON_RULES}}, briefing.* → {{KPI_DUMP}} + {{DECISIONS_DUMP}}.
function placeholderHint(slug) {
  if (slug && slug.startsWith('chat.')) return '권장 placeholder: {{COMMON_RULES}}';
  if (slug && slug.startsWith('briefing.')) return '권장 placeholder: {{KPI_DUMP}}, {{DECISIONS_DUMP}}';
  if (slug === 'common-rules') return '5 부서 공통 규칙 — placeholder 자유';
  return '';
}

function ChatPrompts() {
  const initialRows = (CHAT_PROMPTS.rows ?? []).map(r => ({ ...r }));
  const [rows, setRows] = useState(initialRows);
  const [expandedSlug, setExpandedSlug] = useState(null);
  const [drafts, setDrafts] = useState({}); // slug → unsaved content
  const [saving, setSaving] = useState(null); // slug currently saving

  const onToggle = (slug) => {
    setExpandedSlug(prev => prev === slug ? null : slug);
    if (drafts[slug] === undefined) {
      const row = rows.find(r => r.slug === slug);
      if (row) {
        setDrafts(d => ({ ...d, [slug]: row.content ?? '' }));
      }
    }
  };

  const onChange = (slug, value) => {
    setDrafts(d => ({ ...d, [slug]: value }));
  };

  const onReset = (slug) => {
    const row = rows.find(r => r.slug === slug);
    setDrafts(d => ({ ...d, [slug]: row?.content ?? '' }));
  };

  const onSave = async (slug) => {
    const content = drafts[slug] ?? '';
    if (!content) {
      showToast({ ok: false, code: 'content-empty', message: 'content 비어있음' });
      return;
    }
    setSaving(slug);
    try {
      const res = await postHandler('ChatPromptSave', { slug, content });
      if (res && res.ok) {
        // local snapshot 갱신 — version+1, length 동기 (다음 GET 까지 reload 없이 UI 일관).
        setRows(prev => prev.map(r => r.slug === slug
          ? { ...r, content, length: content.length, version: (r.version ?? 0) + 1, updatedAt: new Date().toISOString() }
          : r));
      }
    } catch (_) {
      // toast 는 postHandler 에서 표시.
    } finally {
      setSaving(null);
    }
  };

  if (rows.length === 0) {
    return (
      <div className="t-faint">chat_prompts 미적재 — startup loader 실패 또는 DI 미등록. <span className="t-mono">/admin/console</span> 재진입.</div>
    );
  }

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">CHAT PROMPTS <span className="ahint">(chat agent + Briefing system prompt · 즉시 편집)</span></h3>
        <span className="t-faint" style={{ fontSize: 11 }}>{rows.length} slugs</span>
      </div>

      <div className="atbl">
        <table>
          <thead>
            <tr>
              <th>slug</th>
              <th>version</th>
              <th>length</th>
              <th>updated</th>
              <th>by</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => {
              const isExpanded = expandedSlug === r.slug;
              const draft = drafts[r.slug];
              const dirty = draft !== undefined && draft !== r.content;
              return (
                <React.Fragment key={r.slug}>
                  <tr
                    className={`atbl-row ${isExpanded ? 'is-active' : ''}`}
                    onClick={() => onToggle(r.slug)}
                    style={{ cursor: 'pointer' }}
                  >
                    <td className="t-mono">
                      <span style={{ marginRight: 6 }}>{isExpanded ? '▾' : '▸'}</span>
                      {r.slug}
                    </td>
                    <td className="t-mono">v{r.version ?? 1}</td>
                    <td className="t-mono">{(r.length ?? 0).toLocaleString()} chars</td>
                    <td className="t-faint t-mono" style={{ fontSize: 11 }}>
                      {r.updatedAt ? new Date(r.updatedAt).toLocaleString('en-GB') : '—'}
                    </td>
                    <td className="t-faint t-mono" style={{ fontSize: 11 }}>{r.updatedBy ?? '—'}</td>
                    <td>
                      {dirty && <span className="t-warn" style={{ fontSize: 11 }}>● dirty</span>}
                    </td>
                  </tr>
                  {isExpanded && (
                    <tr>
                      <td colSpan={6} style={{ padding: '8px 0' }}>
                        <div className="afield">
                          <label>
                            content
                            <span className="afield-hint"> · {placeholderHint(r.slug)}</span>
                          </label>
                          <textarea
                            className="ainput t-mono"
                            value={draft ?? ''}
                            onChange={(e) => onChange(r.slug, e.target.value)}
                            rows={24}
                            spellCheck={false}
                            style={{ width: '100%', fontFamily: 'JetBrains Mono, monospace', fontSize: 12, minHeight: 360 }}
                            onClick={(e) => e.stopPropagation()}
                          />
                          <div className="t-faint" style={{ fontSize: 11, marginTop: 4 }}>
                            {(draft ?? '').length.toLocaleString()} / 20,000 chars
                          </div>
                        </div>
                        <div className="aslab-row" style={{ marginTop: 8 }}>
                          <span style={{ flex: 1 }}>
                            <span className="t-faint" style={{ fontSize: 11 }}>
                              저장 시 chat_prompts.version+1 + cache invalidate (chat agent 다음 turn 부터 반영).
                            </span>
                          </span>
                          <button
                            className="abtn"
                            disabled={!dirty || saving === r.slug}
                            onClick={(e) => { e.stopPropagation(); onReset(r.slug); }}
                          >
                            ↺ reset
                          </button>
                          <button
                            className="abtn abtn-pri"
                            disabled={!dirty || saving === r.slug}
                            onClick={(e) => { e.stopPropagation(); onSave(r.slug); }}
                          >
                            {saving === r.slug ? 'saving…' : '↻ save'}
                          </button>
                        </div>
                      </td>
                    </tr>
                  )}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>
      </div>
    </>
  );
}

// ─── Departments (v0.10.0 Spec 2026-05-15 §7) ─────────────────
// 5 부서 + Sub-agent 추가 + Tool Policy + Tool Grant (CoreTool/MCP XOR) 일괄 관리.
// Default 5 부서 (curator/designer/listing-order/cs/tax) 의 Delete 는 client+server 차단.
function Departments() {
  const initRows = (DEPARTMENTS.rows ?? []);
  const initSubs = (DEPARTMENTS.subAgents ?? []);
  const initBindings = (DEPARTMENTS.bindings ?? []);
  const [rows] = useState(initRows);
  const [subs, setSubs] = useState(initSubs);
  const [bindings, setBindings] = useState(initBindings);
  const [expandedSlug, setExpandedSlug] = useState(null);
  const [showAddFor, setShowAddFor] = useState(null); // dept slug to show "+ sub-agent" form
  const [addForm, setAddForm] = useState({ slug: '', displayName: '', icon: '', toolPolicy: 'NoTools', isActive: true });
  const [showGrantFor, setShowGrantFor] = useState(null); // agent slug to show grant form
  const [grantForm, setGrantForm] = useState({ bindingType: 'CoreTool', toolSlug: '', mcpServerId: '', permission: 'read' });
  const [busy, setBusy] = useState(null);

  const onToggle = (slug) => {
    setExpandedSlug(prev => prev === slug ? null : slug);
    setShowAddFor(null);
    setShowGrantFor(null);
  };

  const onAddSubAgent = (deptSlug) => {
    setShowAddFor(deptSlug);
    setAddForm({ slug: `${deptSlug}.`, displayName: '', icon: '', toolPolicy: 'NoTools', isActive: true });
  };

  const onSaveSubAgent = async (deptSlug) => {
    if (!addForm.slug || !addForm.displayName) {
      showToast({ ok: false, code: 'input-invalid', message: 'slug + displayName 필수' });
      return;
    }
    if (addForm.slug === deptSlug || !addForm.slug.startsWith(`${deptSlug}.`)) {
      showToast({ ok: false, code: 'slug-format', message: `sub-agent slug 은 '${deptSlug}.xxx' 형식.` });
      return;
    }
    setBusy(`add:${deptSlug}`);
    try {
      const res = await postHandler('DepartmentSave', {
        slug: addForm.slug,
        displayName: addForm.displayName,
        icon: addForm.icon,
        toolPolicy: addForm.toolPolicy,
        parentSlug: deptSlug,
        isActive: addForm.isActive,
      });
      if (res && res.ok) {
        // Local snapshot 갱신 — page reload 회피 (다음 GET 까지 일관).
        const now = new Date().toISOString();
        setSubs(prev => [
          ...prev,
          {
            slug: addForm.slug,
            parentSlug: deptSlug,
            displayName: addForm.displayName,
            icon: addForm.icon,
            toolPolicy: addForm.toolPolicy,
            isActive: addForm.isActive,
            updatedAt: now,
          },
        ]);
        setShowAddFor(null);
      }
    } catch (_) {}
    finally { setBusy(null); }
  };

  const onDeleteSubAgent = async (sub) => {
    // ★ default 5 부서 slug 는 절대 client-side 부터 차단.
    if (DEPARTMENT_ROOT_SLUGS.includes(sub.slug)) {
      showToast({ ok: false, code: 'root-department-protected', message: `5 default 부서 삭제 불가 (${sub.slug})` });
      return;
    }
    if (typeof window !== 'undefined' && !window.confirm(`Sub-agent '${sub.slug}' 를 삭제하시겠습니까?`)) {
      return;
    }
    setBusy(`del:${sub.slug}`);
    try {
      const res = await postHandler('DepartmentDelete', { slug: sub.slug });
      if (res && res.ok) {
        setSubs(prev => prev.filter(s => s.slug !== sub.slug));
        setBindings(prev => prev.filter(b => b.agentSlug !== sub.slug));
      }
    } catch (_) {}
    finally { setBusy(null); }
  };

  const onShowGrant = (agentSlug, currentPolicy) => {
    setShowGrantFor(agentSlug);
    const bindingType = currentPolicy === 'McpOnly' ? 'McpServer' : 'CoreTool';
    setGrantForm({ bindingType, toolSlug: '', mcpServerId: '', permission: 'read' });
  };

  const onSaveGrant = async (agentSlug) => {
    const isMcp = grantForm.bindingType === 'McpServer';
    if (!isMcp && !grantForm.toolSlug) {
      showToast({ ok: false, code: 'input-invalid', message: 'toolSlug 필수' });
      return;
    }
    if (isMcp && !grantForm.mcpServerId) {
      showToast({ ok: false, code: 'input-invalid', message: 'mcpServerId 필수' });
      return;
    }
    setBusy(`grant:${agentSlug}`);
    try {
      const res = await postHandler('ToolBindingGrant', {
        agentSlug,
        bindingType: grantForm.bindingType,
        toolSlug: isMcp ? null : grantForm.toolSlug,
        mcpServerId: isMcp ? grantForm.mcpServerId : null,
        permission: grantForm.permission,
      });
      if (res && res.ok) {
        // local snapshot — id 는 server 가 부여하지만 page reload 가 일관성 보장.
        if (typeof window !== 'undefined') window.location.reload();
      }
    } catch (_) {}
    finally { setBusy(null); }
  };

  const onRevokeGrant = async (binding) => {
    if (typeof window !== 'undefined' && !window.confirm(`Tool grant 해제하시겠습니까? (${binding.toolSlug ?? binding.mcpServerId})`)) {
      return;
    }
    setBusy(`revoke:${binding.id}`);
    try {
      const res = await postHandler('ToolBindingRevoke', { bindingId: binding.id });
      if (res && res.ok) {
        setBindings(prev => prev.filter(b => b.id !== binding.id));
      }
    } catch (_) {}
    finally { setBusy(null); }
  };

  if (rows.length === 0) {
    return (
      <div className="t-faint">
        agent_departments 미적재 — DepartmentCacheStartupLoader 실패 또는 DI 미등록. <span className="t-mono">/admin/console</span> 재진입.
      </div>
    );
  }

  return (
    <>
      <div className="aslab-row">
        <h3 className="aslab">DEPARTMENTS <span className="ahint">(5 부서 · Sub-agent 추가 · Tool Grant 일괄 관리)</span></h3>
        <span className="t-faint" style={{ fontSize: 11 }}>{rows.length} dept · {subs.length} sub-agent · {bindings.length} grant</span>
      </div>

      <div className="atbl">
        <table>
          <thead>
            <tr>
              <th>slug</th>
              <th>display name</th>
              <th>tool policy</th>
              <th>status</th>
              <th>cutover</th>
              <th>manifest</th>
              <th>updated</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => {
              const isExpanded = expandedSlug === r.slug;
              const deptSubs = subs.filter(s => s.parentSlug === r.slug);
              const deptBindings = bindings.filter(b => b.agentSlug === r.slug);
              return (
                <React.Fragment key={r.slug}>
                  <tr
                    className={`atbl-row ${isExpanded ? 'is-active' : ''}`}
                    onClick={() => onToggle(r.slug)}
                    style={{ cursor: 'pointer' }}
                  >
                    <td className="t-mono">
                      <span style={{ marginRight: 6 }}>{isExpanded ? '▾' : '▸'}</span>
                      <span style={{ marginRight: 4 }}>{r.icon ?? '◯'}</span>
                      {r.slug}
                    </td>
                    <td>{r.displayName}</td>
                    <td className="t-mono"><PolicyBadge value={r.toolPolicy} /></td>
                    <td className="t-mono">{r.isActive ? <span className="t-ok">● active</span> : <span className="t-faint">○ inactive</span>}</td>
                    <td className="t-mono">{r.isCutover ? <span className="t-ok">cutover</span> : <span className="t-faint">in-process</span>}</td>
                    <td className="t-faint t-mono">v{r.manifestVersion ?? 1}</td>
                    <td className="t-faint t-mono" style={{ fontSize: 11 }}>{r.updatedAt ? new Date(r.updatedAt).toLocaleString('en-GB') : '—'}</td>
                    <td className="t-faint t-mono" style={{ fontSize: 11 }}>
                      {deptSubs.length > 0 && <span>{deptSubs.length} sub</span>}
                      {deptBindings.length > 0 && <span style={{ marginLeft: 6 }}>{deptBindings.length} grant</span>}
                    </td>
                  </tr>
                  {isExpanded && (
                    <tr>
                      <td colSpan={8} style={{ padding: '8px 0' }}>
                        <DepartmentDetail
                          dept={r}
                          subs={deptSubs}
                          bindings={deptBindings}
                          allBindings={bindings}
                          showAddFor={showAddFor}
                          showGrantFor={showGrantFor}
                          addForm={addForm}
                          grantForm={grantForm}
                          busy={busy}
                          onAddSubAgent={onAddSubAgent}
                          onAddFormChange={(k, v) => setAddForm(f => ({ ...f, [k]: v }))}
                          onSaveSubAgent={onSaveSubAgent}
                          onCancelAdd={() => setShowAddFor(null)}
                          onDeleteSubAgent={onDeleteSubAgent}
                          onShowGrant={onShowGrant}
                          onGrantFormChange={(k, v) => setGrantForm(f => ({ ...f, [k]: v }))}
                          onSaveGrant={onSaveGrant}
                          onCancelGrant={() => setShowGrantFor(null)}
                          onRevokeGrant={onRevokeGrant}
                        />
                      </td>
                    </tr>
                  )}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>
      </div>
    </>
  );
}

function PolicyBadge({ value }) {
  // McpOnly = ok (외부 도구만, 안전 격리) / SharedReadOnly = warn (Core API read) / NoTools = faint.
  const tone = value === 'McpOnly' ? 'ok' : value === 'SharedReadOnly' ? 'warn' : 'faint';
  return <span className={`t-${tone}`}>{value ?? '—'}</span>;
}

function DepartmentDetail({
  dept, subs, bindings, allBindings,
  showAddFor, showGrantFor, addForm, grantForm, busy,
  onAddSubAgent, onAddFormChange, onSaveSubAgent, onCancelAdd, onDeleteSubAgent,
  onShowGrant, onGrantFormChange, onSaveGrant, onCancelGrant, onRevokeGrant,
}) {
  const isRoot = DEPARTMENT_ROOT_SLUGS.includes(dept.slug);
  return (
    <div style={{ paddingLeft: 24, paddingRight: 12 }}>
      <div className="aslab-row">
        <span className="t-faint" style={{ fontSize: 11 }}>
          Sub-agent ({subs.length}) · Tool Grant ({bindings.length})
        </span>
        <button
          className="abtn"
          onClick={() => onAddSubAgent(dept.slug)}
          disabled={busy === `add:${dept.slug}` || showAddFor === dept.slug}
        >
          + sub-agent
        </button>
        <button
          className="abtn"
          onClick={() => onShowGrant(dept.slug, dept.toolPolicy)}
          disabled={dept.toolPolicy === 'NoTools' || busy === `grant:${dept.slug}` || showGrantFor === dept.slug}
          title={dept.toolPolicy === 'NoTools' ? 'NoTools policy — tool grant 불가' : ''}
        >
          + tool grant
        </button>
      </div>

      {showAddFor === dept.slug && (
        <div className="afield" style={{ background: 'rgba(255,255,255,0.02)', padding: 8, borderRadius: 4, marginTop: 6 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
            <label>
              slug ({dept.slug}.xxx)
              <input className="ainput t-mono" value={addForm.slug} onChange={(e) => onAddFormChange('slug', e.target.value)} />
            </label>
            <label>
              display name
              <input className="ainput" value={addForm.displayName} onChange={(e) => onAddFormChange('displayName', e.target.value)} />
            </label>
            <label>
              icon (optional, 1 char)
              <input className="ainput" value={addForm.icon} onChange={(e) => onAddFormChange('icon', e.target.value)} maxLength={8} />
            </label>
            <label>
              tool policy
              <select className="ainput" value={addForm.toolPolicy} onChange={(e) => onAddFormChange('toolPolicy', e.target.value)}>
                <option value="NoTools">NoTools</option>
                <option value="SharedReadOnly">SharedReadOnly</option>
                <option value="McpOnly">McpOnly</option>
              </select>
            </label>
          </div>
          <div className="aslab-row" style={{ marginTop: 6 }}>
            <label style={{ flex: 1 }}>
              <input
                type="checkbox"
                checked={addForm.isActive}
                onChange={(e) => onAddFormChange('isActive', e.target.checked)}
              /> active
            </label>
            <button className="abtn" onClick={onCancelAdd}>cancel</button>
            <button className="abtn abtn-pri" disabled={busy === `add:${dept.slug}`} onClick={() => onSaveSubAgent(dept.slug)}>
              {busy === `add:${dept.slug}` ? 'saving…' : '↻ save'}
            </button>
          </div>
        </div>
      )}

      {subs.length > 0 && (
        <div className="atbl" style={{ marginTop: 6 }}>
          <table>
            <thead>
              <tr>
                <th>sub-agent slug</th>
                <th>display name</th>
                <th>policy</th>
                <th>status</th>
                <th>grants</th>
                <th>updated</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {subs.map(s => {
                const subBindings = allBindings.filter(b => b.agentSlug === s.slug);
                return (
                  <tr key={s.slug}>
                    <td className="t-mono">
                      <span style={{ marginRight: 4 }}>{s.icon ?? '·'}</span>{s.slug}
                    </td>
                    <td>{s.displayName}</td>
                    <td className="t-mono"><PolicyBadge value={s.toolPolicy} /></td>
                    <td className="t-mono">{s.isActive ? <span className="t-ok">●</span> : <span className="t-faint">○</span>}</td>
                    <td className="t-faint t-mono">{subBindings.length}</td>
                    <td className="t-faint t-mono" style={{ fontSize: 11 }}>{s.updatedAt ? new Date(s.updatedAt).toLocaleString('en-GB') : '—'}</td>
                    <td>
                      <button
                        className="abtn"
                        onClick={() => onDeleteSubAgent(s)}
                        disabled={busy === `del:${s.slug}`}
                      >
                        {busy === `del:${s.slug}` ? '…' : '✗ delete'}
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {showGrantFor === dept.slug && (
        <div className="afield" style={{ background: 'rgba(255,255,255,0.02)', padding: 8, borderRadius: 4, marginTop: 6 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
            <label>
              binding type
              <select className="ainput" value={grantForm.bindingType} onChange={(e) => onGrantFormChange('bindingType', e.target.value)}>
                {dept.toolPolicy !== 'McpOnly' && <option value="CoreTool">CoreTool</option>}
                {dept.toolPolicy !== 'SharedReadOnly' && <option value="McpServer">McpServer</option>}
              </select>
            </label>
            <label>
              permission
              <select className="ainput" value={grantForm.permission} onChange={(e) => onGrantFormChange('permission', e.target.value)}>
                <option value="read">read</option>
                <option value="write">write</option>
              </select>
            </label>
            {grantForm.bindingType === 'CoreTool' ? (
              <label style={{ gridColumn: '1 / 3' }}>
                tool slug (Core Tool API)
                <select className="ainput t-mono" value={grantForm.toolSlug} onChange={(e) => onGrantFormChange('toolSlug', e.target.value)}>
                  <option value="">— select tool —</option>
                  {CORE_TOOL_INVENTORY.map(t => (
                    <option key={t} value={t}>{t}</option>
                  ))}
                </select>
              </label>
            ) : (
              <label style={{ gridColumn: '1 / 3' }}>
                mcp server id (Guid, registered in /admin-api/mcp-servers)
                <input
                  className="ainput t-mono"
                  placeholder="00000000-0000-0000-0000-000000000000"
                  value={grantForm.mcpServerId}
                  onChange={(e) => onGrantFormChange('mcpServerId', e.target.value)}
                />
              </label>
            )}
          </div>
          <div className="aslab-row" style={{ marginTop: 6 }}>
            <span className="t-faint" style={{ fontSize: 11, flex: 1 }}>
              저장 시 ON DELETE CASCADE — 부서 삭제 자동 정리. write grant 는 자기 부서 한정.
            </span>
            <button className="abtn" onClick={onCancelGrant}>cancel</button>
            <button className="abtn abtn-pri" disabled={busy === `grant:${dept.slug}`} onClick={() => onSaveGrant(dept.slug)}>
              {busy === `grant:${dept.slug}` ? 'saving…' : '↻ grant'}
            </button>
          </div>
        </div>
      )}

      {bindings.length > 0 && (
        <div className="atbl" style={{ marginTop: 6 }}>
          <table>
            <thead>
              <tr>
                <th>type</th>
                <th>target</th>
                <th>permission</th>
                <th>granted</th>
                <th>by</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {bindings.map(b => (
                <tr key={b.id}>
                  <td className="t-mono">{b.bindingType}</td>
                  <td className="t-mono">{b.toolSlug ?? b.mcpServerId ?? '—'}</td>
                  <td className="t-mono">{b.permission}</td>
                  <td className="t-faint t-mono" style={{ fontSize: 11 }}>{b.grantedAt ? new Date(b.grantedAt).toLocaleString('en-GB') : '—'}</td>
                  <td className="t-faint t-mono" style={{ fontSize: 11 }}>{b.grantedBy ?? '—'}</td>
                  <td>
                    <button className="abtn" onClick={() => onRevokeGrant(b)} disabled={busy === `revoke:${b.id}`}>
                      {busy === `revoke:${b.id}` ? '…' : '✗ revoke'}
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

const SECTION_RENDER = {
  overview: Overview, site: Site, llm: LlmRouting, cost: CostGuard, jobs: Jobs,
  db: Db, keys: Keys, audit: Audit, metrics: Telemetry,
  docs: DocStorage,
  manifest: ToolManifest, sandbox: PolicySandbox, flags: KillSwitch, tasks: Tasks,
  prompts: ChatPrompts,
  departments: Departments,
};

const SECTION_TITLES = {
  overview: 'system overview',
  site: 'site title (마스트헤드 회사명)',
  llm: 'llm routing & providers',
  cost: 'cost guard buckets',
  jobs: 'hangfire jobs',
  db: 'postgres / migrations',
  keys: 'api key inventory',
  audit: 'agent_decision_log',
  metrics: 'opentelemetry metrics',
  docs: 'paperless document storage',
  manifest: 'tool manifest (agents · skills · prompts · packages · market · mcp · plugins)',
  sandbox: 'policy sandbox · presets + backtest',
  flags: 'kill switch · scope + reason + cooldown',
  tasks: 'tasks list & lifecycle',
  prompts: 'chat prompts (chat agent · briefing) · db editing',
  departments: 'departments (5 부서 · sub-agent · tool grant)',
};

// ─── App ───────────────────────────────────────────
// v0.8.15 W3 (Plan 2026-05-12 §6 M7) — 9 dispersed admin URL 의 301 redirect 대상이 hash anchor
// (#manifest / #sandbox / #flags / #tasks) 를 포함. mount 시 hash 를 parse 해 해당 섹션을 활성화
// + hashchange listener 로 브라우저 back/forward + 외부 deep link 호환.
function resolveInitialSection() {
  if (typeof window === 'undefined') return 'overview';
  const { section } = parseAdminHash();
  return ADMIN_NAV.some(n => n.id === section) ? section : 'overview';
}

function AdminApp() {
  const [tweaks, setTweak] = useTweaks(ADMIN_TWEAK_DEFAULTS);
  const [section, setSectionState] = useState(resolveInitialSection);
  const [time, setTime] = useState(() => new Date());

  // setSection wrapper — section 전환 시 URL hash 도 sync (브라우저 history 갱신).
  // M7 = bookmark / 외부 link 호환. pushState 사용 (replaceState 면 back 누적 0).
  const setSection = (id) => {
    setSectionState(id);
    if (typeof window !== 'undefined' && window.location.hash.replace(/^#/, '') !== id) {
      window.history.pushState(null, '', '#' + id);
    }
  };

  useEffect(() => {
    document.body.classList.add('admin-body');
    return () => document.body.classList.remove('admin-body');
  }, []);
  useEffect(() => {
    document.body.dataset.accent = tweaks.accent;
  }, [tweaks.accent]);
  useEffect(() => {
    const t = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  // hashchange listener — browser back/forward / 외부 anchor jump.
  useEffect(() => {
    const onHash = () => {
      const next = parseAdminHash().section || 'overview';
      if (ADMIN_NAV.some(n => n.id === next)) {
        setSectionState(next);
      }
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  const Section = SECTION_RENDER[section];
  const tt = time.toLocaleTimeString('en-GB');

  return (
    <div className="admin">
      <aside className="anav">
        <div className="anav-brand">
          <div className="anav-prompt">~/purchaseagent$</div>
          <div className="anav-title">admin.console</div>
          <div className="anav-sub">v0.5.1 · ops</div>
        </div>
        <nav className="anav-list">
          {ADMIN_NAV.map(n => (
            <button
              key={n.id}
              className={`anav-item ${section === n.id ? 'is-active' : ''}`}
              onClick={() => setSection(n.id)}
            >
              <span className="anav-icon">{n.icon}</span>
              <span className="anav-label">{n.label}</span>
              <span className="anav-cmd">$ {n.cmd}</span>
            </button>
          ))}
        </nav>
        <div className="anav-foot">
          <a href="/" className="anav-back">← gazette mode</a>
          <div className="anav-clock">
            <StatusDot tone="ok" />
            <span className="anav-clock-tt">{tt}</span>
          </div>
        </div>
      </aside>

      <main className="amain">
        <header className="ahead">
          <div className="ahead-crumb">
            <span className="ahead-prompt">$</span>
            <span className="ahead-cmd">{ADMIN_NAV.find(n => n.id === section).cmd}</span>
            <span className="ahead-cursor">█</span>
          </div>
          <div className="ahead-title">{SECTION_TITLES[section]}</div>
          <div className="ahead-meta">
            <span><StatusDot tone="ok" /> healthy</span>
            <span>uptime <b className="t-mono">{SYS_STATUS.uptime}</b></span>
            <span>build <b className="t-mono">{SYS_STATUS.build}</b></span>
          </div>
        </header>
        <div className="abody">
          <Section />
        </div>
        <footer className="afoot">
          <span>// PurchaseAgent ops console — internal use only</span>
          <span>render {tt}</span>
        </footer>
      </main>

      <TweaksPanel>
        <TweakSection label="액센트" />
        <TweakRadio
          label="컬러"
          value={tweaks.accent}
          options={['amber', 'green', 'cyan']}
          onChange={(v) => setTweak('accent', v)}
        />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<AdminApp />);
