/* WAREHOUSE 2.0 · SHIELD 值守駕駛艙 — ADMIN 權力面,真後端
   系統體徵(/api/shield/status,30s 輪詢)+ 白名單修復(POST /api/shield/repair,
   逐字對齊 scripts/shieldctl.py ACTIONS)+ 值守記錄(status 內時間線/守護日誌/風險事件)。
   政策:允許直接調管理端點;每個動作行內確認;失敗把後端 error 原文紅字呈現;全程審計。 */
(() => {
const W2 = window.W2;
const { t } = window.W2_LANG;
window.W2_LANG.addEN({
  "SHIELD 值守": "SHIELD Watch",
  "權力面 · 全程審計": "POWER PLANE · FULLY AUDITED",
  "系統體徵 · 白名單修復 · 值守記錄 —— 管理端點直連,每個動作行內確認並寫入審計": "System vitals · whitelist repairs · watch log — direct admin endpoints, inline confirmation, fully audited",
  "刷新": "Refresh",
  "暫停輪詢": "Pause polling",
  "繼續輪詢": "Resume polling",
  "狀態拉取失敗": "Status fetch failed",
  /* 拒絕面 */
  "權限不足": "ACCESS DENIED",
  "SHIELD 值守是 ADMIN 權力面:僅平台所有者或 L5 系統管理員可進入,其他身分一律拒絕。": "SHIELD Watch is an ADMIN power plane: platform owners or L5 system admins only — every other identity is refused.",
  "當前身分": "Current identity",
  "問秘書怎麼開通權限": "Ask Secretary about access",
  "我在 SHIELD 值守頁被擋住了,提示沒有 ADMIN 權限。幫我看看誰能授權、怎麼開通": "I'm blocked on the SHIELD page — no ADMIN access. Find out who can grant it and how",
  /* Band A */
  "系統體徵": "System vitals",
  "30 秒自動輪詢 /api/shield/status": "auto-polls /api/shield/status every 30s",
  "跑一次體檢": "Run healthcheck",
  "體檢中…": "Checking…",
  "API 存活": "API alive",
  "在線": "Online", "離線": "Offline",
  "完整性": "Integrity",
  "一致": "Consistent", "告警": "Alert",
  "未關閉事件": "Open incidents",
  "待覆核 AI 風險": "Open AI risks",
  "宗": "", "條": "", "行": "",
  "待連接": "Awaiting link",
  "全部關閉": "All closed",
  "無待覆核": "None open",
  "去覆核 →": "Review →",
  "總體態勢": "Posture",
  "嚴重度": "Severity",
  "健康": "Healthy", "觀察": "Watch", "降級": "Degraded", "容量風險": "Capacity risk",
  "運行抖動": "Flapping", "事件中": "Incident", "完整性告警": "Integrity alert",
  "攻擊態勢": "Under attack", "未接入": "Offline",
  "內存佔用": "Memory used", "磁盤佔用": "Disk used",
  "請求速率": "Req rate", "5xx 錯誤率": "5xx rate", "主進程 FD": "Main FD",
  "登入失敗率": "Login fails", "SSH 失敗率": "SSH fails",
  "最新指標": "Latest metric",
  "AI 引擎": "AI engine", "已接入": "Connected",
  "消防員": "Firefighter",
  "體徵指標待接入:消防員數據庫尚未回報,或後端未返回 live_metrics。": "Vitals pending: the firefighter database has not reported yet, or the backend returned no live_metrics.",
  "健康探活結論": "Healthcheck verdict",
  "只讀動作:POST /api/shield/repair {action:\"healthcheck\"},探活 /api/ai/health,不需確認。": "Read-only: POST /api/shield/repair {action:\"healthcheck\"} probes /api/ai/health — no confirmation needed.",
  "體檢未通過": "Check failed",
  "通過": "Passed",
  "成功 · 空響應": "OK · empty response",
  "耗時": "took",
  "狀態碼": "HTTP",
  /* Band B */
  "修復動作": "Repair actions",
  "五個白名單動作 · 委派 shieldctl · 絕不裸 shell · 每一下都寫入審計": "Five whitelisted actions · delegated to shieldctl · never raw shell · every click audited",
  "高風險": "HIGH RISK", "低風險": "LOW RISK",
  "重啟 API 服務": "Restart API service",
  "systemctl restart warehouse-api,重啟前後各跑一次健康探活。": "systemctl restart warehouse-api, with health probes before and after.",
  "重啟期間 API 短暫斷檔,進行中的請求會中斷。": "API briefly unavailable during restart; in-flight requests are dropped.",
  "重啟消防員守護": "Restart firefighter daemon",
  "systemctl restart warehouse-firefighter。": "systemctl restart warehouse-firefighter.",
  "值守短暫空窗,不影響業務 API。": "Brief watch gap; business API unaffected.",
  "重載 Nginx 配置": "Reload Nginx config",
  "先 nginx -t 驗證配置,通過才 systemctl reload nginx。": "Runs nginx -t first; reloads only if the config test passes.",
  "平滑重載,不中斷現有連接。": "Graceful reload; existing connections stay alive.",
  "重啟 Nginx 服務": "Restart Nginx service",
  "先 nginx -t 驗證配置,通過才 systemctl restart nginx。": "Runs nginx -t first; restarts only if the config test passes.",
  "入口斷流數秒,全站短暫不可達。": "Gateway down for seconds; the whole site is briefly unreachable.",
  "清除健康失敗計數": "Clear health-fail flag",
  "刪除 database/.integrity/health_fail 標記檔。": "Deletes the database/.integrity/health_fail flag file.",
  "只清標記,不動服務;守護將重新計數。": "Clears the flag only; no service touched — the guardian recounts.",
  "作用": "Action", "風險": "Risk",
  "執行": "Execute",
  "確認執行「{name}」?": "Confirm executing \"{name}\"?",
  "確認執行": "Confirm run", "取消": "Cancel",
  "執行中…": "Executing…",
  "完成": "Done",
  "已實際執行": "Applied",
  "DRY-RUN 演練": "DRY-RUN rehearsal",
  "後端未開 SHIELD_REPAIR_APPLY,本次僅演練未動系統。": "SHIELD_REPAIR_APPLY is off on the backend — rehearsal only, nothing touched.",
  "退出碼": "Exit code",
  "後端返回": "Backend result",
  /* Band C */
  "值守記錄": "Watch log",
  "消防員時間線 · 守護日誌 · AI 風險事件(取自 status 快照)": "Firefighter timeline · guardian tail · AI risk events (from the status snapshot)",
  "事件流見審計頁 →": "Full trail on Audit page →",
  "進行中事件": "Open incidents",
  "消防員最近事件": "Recent firefighter events",
  "守護日誌尾": "Guardian log tail",
  "AI 風險事件": "AI risk events",
  "暫無": "None",
  "進行中": "open",
  "暫無值守事件": "No watch events yet",
  "消防員尚未接入或暫無事件;完整操作留痕見審計頁。": "Firefighter not linked or no events yet; the full operation trail lives on the Audit page.",
  "去審計頁": "Open Audit",
  "偵測": "Detect", "觸發": "Trigger", "診斷": "Diagnose", "脈衝": "Pulse",
  "狀態轉移": "Transition", "解除": "Resolve",
  "修復計劃": "Repair plan", "修復執行": "Repair exec", "修復復檢": "Repair verify",
  "修復失敗": "Repair failed", "修復完成": "Repair resolved", "修復": "Repair", "事件": "Event",
  "未完成": "not done", "已記錄": "logged", "狀態異常": "state anomaly",
  "ADMIN 權力面:每一次修復都由後端寫入審計日誌(shield_repair),並受 SHIELD_REPAIR_APPLY 雙閘保護。": "ADMIN power plane: every repair is audit-logged by the backend (shield_repair) and double-gated by SHIELD_REPAIR_APPLY.",
});

const { useState: _s, useEffect: _e, useMemo: _mm, useCallback: _cb, useRef: _r } = React;
const { Icon: I, Btn: B, Tag: T, Label: LB, Empty: EM, Kpi, Meter, Folio, Band, pad2, num } = W2;
const ask = (p) => W2.openSecretary(p);

/* ── 詞表與小工具 ── */
const S = (v) => (v === null || v === undefined || v === "") ? "—" : String(v);
const fmtTs = (v) => v ? String(v).replace("T", " ").slice(0, 19) : "—";
const STATE_META = {
  healthy: ["ok", "健康"], watch: ["warn", "觀察"], degraded: ["warn", "降級"],
  "capacity-risk": ["warn", "容量風險"], "runtime-flapping": ["warn", "運行抖動"],
  incident: ["bad", "事件中"], "integrity-alert": ["bad", "完整性告警"],
  "under-attack": ["bad", "攻擊態勢"], offline: ["plain", "未接入"],
};
const StateTag = ({ st }) => {
  const meta = STATE_META[String(st || "")];
  return meta ? <T tone={meta[0]} dot>{t(meta[1])}</T> : <T tone="plain">{st || "—"}</T>;
};
const KIND_META = {
  detect: ["warn", "偵測"], trigger: ["warn", "觸發"], diagnose: ["inv", "診斷"], pulse: ["inv", "脈衝"],
  transition: ["plain", "狀態轉移"], resolve: ["ok", "解除"],
  repair_plan: ["warn", "修復計劃"], repair_execute: ["ok", "修復執行"], repair_verify: ["ok", "修復復檢"],
  repair_failed: ["bad", "修復失敗"], repair_resolved: ["ok", "修復完成"],
};
const kindMeta = (k) => KIND_META[k] || (String(k || "").indexOf("repair_") === 0 ? ["ok", "修復"] : ["plain", k || "事件"]);
const parseJ = (v) => {
  if (v && typeof v === "object") return v;
  if (typeof v !== "string") return null;
  try { return JSON.parse(v); } catch (e) { return null; }
};
const tlText = (it) => {
  const kind = String((it && it.kind) || "");
  const p = parseJ(it && it.content);
  if (p) {
    if (kind.indexOf("repair_") === 0) return (p.action || "repair") + " · " + (p.ok === false ? t("未完成") : t("已記錄")) + (p.reason ? " · " + String(p.reason).slice(0, 60) : "");
    if (kind === "detect") return String(p.state || t("狀態異常"));
    if (kind === "trigger") return String(p.type || "trigger");
    if (p.summary) return String(p.summary).slice(0, 150);
    try { return JSON.stringify(p).slice(0, 150); } catch (e) { return "—"; }
  }
  return String(it && it.content != null ? it.content : "—").slice(0, 150);
};
const GUARD_ALERT_RE = /INTEGRITY VIOLATION|HEALTH FAIL|WATCHDOG/;
const PRE = { fontFamily: "var(--f-mono)", fontSize: 10.5, lineHeight: 1.55, whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 150, overflow: "auto", background: "var(--paper-2)", border: "1px solid var(--hair-soft)", padding: 10, margin: "8px 0 0" };

/* 修復失敗:後端 error 原文(結果體 error → stderr → 頂層 error) */
const failText = (resp) => {
  const r = (resp && resp.result) || {};
  return String(r.error || (resp && resp.stderr) || (resp && resp.error) || "").trim();
};

/* ── 白名單動作卡(逐字對齊 scripts/shieldctl.py ACTIONS)── */
const REPAIRS = [
  { id: "restart-api", icon: "cpu", high: true, name: "重啟 API 服務",
    does: "systemctl restart warehouse-api,重啟前後各跑一次健康探活。",
    risk: "重啟期間 API 短暫斷檔,進行中的請求會中斷。" },
  { id: "restart-firefighter", icon: "flame", high: false, name: "重啟消防員守護",
    does: "systemctl restart warehouse-firefighter。",
    risk: "值守短暫空窗,不影響業務 API。" },
  { id: "reload-nginx", icon: "refresh", high: false, name: "重載 Nginx 配置",
    does: "先 nginx -t 驗證配置,通過才 systemctl reload nginx。",
    risk: "平滑重載,不中斷現有連接。" },
  { id: "restart-nginx", icon: "swap", high: true, name: "重啟 Nginx 服務",
    does: "先 nginx -t 驗證配置,通過才 systemctl restart nginx。",
    risk: "入口斷流數秒,全站短暫不可達。" },
  { id: "clear-health-flag", icon: "checkCircle", high: false, name: "清除健康失敗計數",
    does: "刪除 database/.integrity/health_fail 標記檔。",
    risk: "只清標記,不動服務;守護將重新計數。" },
];

const RepairResult = ({ resp, err }) => {
  if (err) return (
    <div className="mono" style={{ fontSize: 11, color: "var(--red)", wordBreak: "break-word", lineHeight: 1.6 }}>✗ {err}</div>
  );
  if (!resp) return null;
  const empty = Object.keys(resp).length === 0;
  const failed = resp.ok === false;
  const meta = [
    resp.elapsed_ms != null ? t("耗時") + " " + resp.elapsed_ms + "ms" : "",
    resp.returncode != null ? t("退出碼") + " " + resp.returncode : "",
  ].filter(Boolean).join(" · ");
  if (failed) {
    const errText = failText(resp);
    return (
      <div className="col g6">
        <div className="row g8 wrap">
          <T tone="bad" dot>{t("修復失敗")}</T>
          {meta && <span className="num muted" style={{ fontSize: 10.5 }}>{meta}</span>}
        </div>
        <div className="mono" style={{ fontSize: 11, color: "var(--red)", wordBreak: "break-word", lineHeight: 1.6 }}>✗ {errText || t("體檢未通過")}</div>
        {resp.result && Object.keys(resp.result || {}).length > 0 && <pre style={PRE}>{JSON.stringify(resp.result, null, 2)}</pre>}
      </div>
    );
  }
  return (
    <div className="col g6">
      <div className="row g8 wrap">
        <T tone="inv"><I name="check" size={10}/>{t("完成")}</T>
        {empty && <T tone="plain">{t("成功 · 空響應")}</T>}
        {resp.applied === true && <T tone="ok" dot>{t("已實際執行")}</T>}
        {resp.applied === false && <T tone="warn">{t("DRY-RUN 演練")}</T>}
        {meta && <span className="num muted" style={{ fontSize: 10.5 }}>{meta}</span>}
      </div>
      {resp.applied === false && <div className="muted" style={{ fontSize: 10.5, lineHeight: 1.6 }}>{t("後端未開 SHIELD_REPAIR_APPLY,本次僅演練未動系統。")}</div>}
      {resp.result && Object.keys(resp.result || {}).length > 0 && (
        <details>
          <summary className="mono muted" style={{ fontSize: 10, letterSpacing: ".08em", cursor: "pointer" }}>{t("後端返回")}</summary>
          <pre style={PRE}>{JSON.stringify(resp.result, null, 2)}</pre>
        </details>
      )}
    </div>
  );
};

const RepairCard = ({ a, idx }) => {
  const [phase, setPhase] = _s("idle"); // idle | confirm | busy | done
  const [resp, setResp] = _s(null);
  const [err, setErr] = _s("");
  const run = () => {
    setPhase("busy"); setErr(""); setResp(null);
    W2.post("/api/shield/repair", { action: a.id })
      .then((d) => { setResp(d && typeof d === "object" ? d : {}); setPhase("done"); })
      .catch((e) => { setErr(e.message || String(e)); setPhase("done"); });
  };
  return (
    <div className="panel rise" style={{ padding: 18, borderColor: a.high ? "var(--red)" : "var(--hair)", animationDelay: (idx * .05) + "s", display: "flex", flexDirection: "column", gap: 10 }}>
      <div className="row spread g10">
        <div className="row g10">
          <I name={a.icon} size={16} color={a.high ? "var(--red)" : "var(--ink)"}/>
          <span style={{ fontWeight: 750, fontSize: 14.5, letterSpacing: "-.02em" }}>{t(a.name)}</span>
        </div>
        {a.high ? <T tone="redinv">{t("高風險")}</T> : <T tone="plain">{t("低風險")}</T>}
      </div>
      <div className="mono muted" style={{ fontSize: 10, letterSpacing: ".08em" }}>{a.id}</div>
      <div style={{ fontSize: 12, lineHeight: 1.6, color: "var(--ink-2)" }}>
        <span className="label dim" style={{ fontSize: 8.5, marginRight: 6 }}>{t("作用")}</span>{t(a.does)}
      </div>
      <div style={{ fontSize: 12, lineHeight: 1.6, color: a.high ? "var(--red)" : "var(--ink-3)" }}>
        <span className="label" style={{ fontSize: 8.5, marginRight: 6, color: a.high ? "var(--red)" : "var(--ink-3)" }}>{t("風險")}</span>{t(a.risk)}
      </div>
      <div style={{ marginTop: "auto" }}>
        {phase === "confirm" ? (
          <div className="col g8" style={{ border: "1px solid var(--red)", background: "var(--red-soft)", padding: 10 }}>
            <span style={{ fontSize: 12.5, fontWeight: 700, color: "var(--red)" }}>{t("確認執行「{name}」?", { name: t(a.name) })}</span>
            <div className="row g8">
              <B size="sm" kind="red" onClick={run}>{t("確認執行")}</B>
              <B size="sm" kind="ghost" onClick={() => setPhase("idle")}>{t("取消")}</B>
            </div>
          </div>
        ) : phase === "busy" ? (
          <div className="step-line"><I name="refresh" size={10}/>{t("執行中…")}</div>
        ) : (
          <div className="col g10">
            {phase === "done" && <RepairResult resp={resp} err={err}/>}
            <B size="sm" kind={a.high ? "red" : ""} icon="arrow" onClick={() => setPhase("confirm")}>{t("執行")}</B>
          </div>
        )}
      </div>
    </div>
  );
};

/* ── 頁面 ── */
const Page = () => {
  const user = window.W2_USER || {};
  const lvl = Math.max(0, ...((user.roles || []).map((r) => Number(r.level) || 0)));
  // L11 平台超級管理員專屬(後端 PLATFORM_OWNER_LEVEL=11;所有者自動合成 L11)
  const allowed = !!window.W2_IS_OWNER || lvl >= 11;

  const [st, setSt] = _s(null);        // /api/shield/status 快照
  const [stErr, setStErr] = _s("");
  const [beat, setBeat] = _s(null);    // 心跳:最近一次輪詢時刻
  const [paused, setPaused] = _s(false);
  const [tick, setTick] = _s(0);
  const [hc, setHc] = _s(null);        // 體檢響應
  const [hcErr, setHcErr] = _s("");
  const [hcBusy, setHcBusy] = _s(false);
  const [hcAt, setHcAt] = _s(null);

  const load = _cb(() => {
    W2.json("/api/shield/status")
      .then((d) => { setSt(d && typeof d === "object" ? d : {}); setStErr(""); })
      .catch((e) => { setSt((p) => p || {}); setStErr(e.message || String(e)); })
      .then(() => setBeat(new Date()));
  }, []);
  _e(() => { if (allowed) load(); }, [tick]);
  _e(() => {
    if (!allowed || paused) return undefined;
    const id = setInterval(() => setTick((v) => v + 1), 30000);
    return () => clearInterval(id);
  }, [paused, allowed]);

  const runHealthcheck = () => {
    if (hcBusy) return;
    setHcBusy(true); setHcErr(""); setHc(null);
    W2.post("/api/shield/repair", { action: "healthcheck" })
      .then((d) => setHc(d && typeof d === "object" ? d : {}))
      .catch((e) => setHcErr(e.message || String(e)))
      .then(() => { setHcBusy(false); setHcAt(new Date()); });
  };

  const ready = !!(st && st.ok === true);
  const incidents = (st && Array.isArray(st.open_incidents)) ? st.open_incidents : [];
  const timeline = (st && Array.isArray(st.recent_timeline)) ? st.recent_timeline : [];
  const guardian = (st && Array.isArray(st.guardian_tail)) ? st.guardian_tail : [];
  const risks = (st && Array.isArray(st.recent_ai_risk_events)) ? st.recent_ai_risk_events : [];
  /* 體徵量表只在消防員真的回報過指標時呈現(latest_metric_at 非空);
     否則後端 live_metrics 全是 0.0 佔位,顯示 0% 會誤導,如實退化成「待連接」。 */
  const metrics = (st && st.latest_metric_at && st.topology && st.topology.live_metrics && typeof st.topology.live_metrics === "object") ? st.topology.live_metrics : null;
  const eng = (st && st.ai_engine && typeof st.ai_engine === "object") ? st.ai_engine : null;
  const guardAlert = guardian.some((l) => GUARD_ALERT_RE.test(String(l)));
  const stateBad = ["incident", "integrity-alert", "under-attack"].indexOf(String(st && st.state)) >= 0;
  const openRisk = st && st.open_ai_risk_events != null ? num(st.open_ai_risk_events) : null;

  const clock = (d) => d ? pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()) : "—";
  const pending = <T tone="plain">{t("待連接")}</T>;

  /* 體檢結論分級 */
  const hcGrade = _mm(() => {
    if (hcErr) return ["bad", t("體檢未通過")];
    if (!hc) return null;
    const r = hc.result || {};
    if (hc.ok === false || r.status === "unhealthy") return ["bad", t("體檢未通過")];
    if (r.status === "healthy") return ["ok", t("健康")];
    if (Object.keys(hc).length === 0) return ["plain", t("成功 · 空響應")];
    return ["ok", t("通過")];
  }, [hc, hcErr]);

  /* ── 頂部:ADMIN 徽記 + 心跳 ── */
  const adminStrip = (
    <div className="row spread wrap g10" style={{ padding: "14px 0 0" }}>
      <span className="label" style={{ color: "var(--red)" }}>ADMIN — {t("權力面 · 全程審計")}</span>
      {allowed && <div className="row g10">
        {!paused && <span className="blink-dot" style={{ background: stErr ? "var(--red)" : "var(--ok)" }}/>}
        <span className="mono" style={{ fontSize: 10.5, letterSpacing: ".12em", color: "var(--ink-2)" }}>
          {paused ? "PAUSED" : "LIVE"} · {clock(beat)} · 30s
        </span>
      </div>}
    </div>
  );

  const folio = (
    <Folio no="17" en="SHIELD" title={t("SHIELD 值守")}
      sub={t("系統體徵 · 白名單修復 · 值守記錄 —— 管理端點直連,每個動作行內確認並寫入審計")}
      right={allowed && <>
        <B icon={paused ? "arrow" : "clock"} onClick={() => setPaused((v) => !v)}>{paused ? t("繼續輪詢") : t("暫停輪詢")}</B>
        <B icon="refresh" onClick={() => setTick((v) => v + 1)}>{t("刷新")}</B>
        <B kind="primary" icon="shield" disabled={hcBusy} onClick={runHealthcheck}>{hcBusy ? t("體檢中…") : t("跑一次體檢")}</B>
      </>}/>
  );

  /* ── 權限拒絕面 ── */
  if (!allowed) return (<>
    {adminStrip}
    {folio}
    <div className="rise" style={{ animationDelay: ".05s", padding: "40px 0" }}>
      <div className="panel" style={{ borderColor: "var(--red)", maxWidth: 560, margin: "0 auto", padding: "34px 34px 30px" }}>
        <LB red style={{ marginBottom: 14 }}>ACCESS DENIED</LB>
        <div style={{ fontSize: 30, fontWeight: 800, letterSpacing: "-.035em", color: "var(--red)", marginBottom: 14 }}>{t("權限不足")}</div>
        <div style={{ fontSize: 13, lineHeight: 1.7, color: "var(--ink-2)", marginBottom: 10 }}>
          {t("SHIELD 值守是 ADMIN 權力面:僅平台所有者或 L5 系統管理員可進入,其他身分一律拒絕。")}
        </div>
        <div className="mono muted" style={{ fontSize: 11, marginBottom: 22 }}>
          {t("當前身分")} · {S(user.display_name || user.username)} · L{lvl}{window.W2_IS_OWNER ? " · OWNER" : ""}
        </div>
        <B icon="sparkle" onClick={() => ask(t("我在 SHIELD 值守頁被擋住了,提示沒有 ADMIN 權限。幫我看看誰能授權、怎麼開通"))}>{t("問秘書怎麼開通權限")}</B>
      </div>
    </div>
  </>);

  return (<>
    {adminStrip}
    {folio}
    {stErr && (
      <div className="mono" style={{ fontSize: 11, color: "var(--red)", padding: "10px 0 0", wordBreak: "break-word" }}>
        ✗ {t("狀態拉取失敗")}:{stErr}
      </div>
    )}

    {/* ═══ Band A · 系統體徵 ═══ */}
    <div className="kpi-band" style={{ marginTop: 8 }}>
      <Kpi label={t("API 存活")} value={stErr ? t("離線") : ready ? t("在線") : "—"} red={!!stErr} delay={0}
        foot={ready ? <StateTag st={st.state}/> : stErr ? <T tone="bad" dot>{t("離線")}</T> : pending}/>
      <Kpi label={t("完整性")}
        value={guardian.length ? (guardAlert ? t("告警") : t("一致")) : "—"}
        red={guardAlert} delay={.05}
        foot={guardian.length
          ? <span className="muted num" style={{ fontSize: 11.5 }}>{t("守護日誌尾")} · {guardian.length} {t("行")}</span>
          : pending}/>
      <Kpi label={t("未關閉事件")} value={st === null ? "—" : ready || st.open_incidents ? incidents.length : "—"} unit={t("宗")}
        red={incidents.length > 0 || stateBad} delay={.1}
        foot={incidents.length
          ? <span className="muted" style={{ fontSize: 11.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{S(incidents[0].title || incidents[0].signature)}</span>
          : ready ? <T tone="ok" dot>{t("全部關閉")}</T> : pending}/>
      <Kpi label={t("待覆核 AI 風險")} value={openRisk == null ? "—" : openRisk} unit={t("條")} red={!!openRisk} delay={.15}
        foot={openRisk
          ? <button className="tag redinv" style={{ cursor: "pointer" }} onClick={() => { location.hash = "#/logs"; }}>{t("去覆核 →")}</button>
          : openRisk === 0 ? <T tone="ok" dot>{t("無待覆核")}</T> : pending}/>
    </div>

    <Band no="A" title={t("系統體徵")} sub={t("30 秒自動輪詢 /api/shield/status")} delay={.1}
      right={<div className="row g10 wrap">
        {ready && <span className="mono muted" style={{ fontSize: 10.5 }}>{t("嚴重度")} {num(st.severity)}/5</span>}
        {ready && <StateTag st={st.state}/>}
      </div>}>
      {metrics ? (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "16px 40px" }}>
          <Meter label={t("內存佔用")} count={Math.round(num(metrics.mem_used_pct))} total={100}
            color={num(metrics.mem_used_pct) >= 90 ? "var(--red)" : "var(--ink)"}/>
          <Meter label={t("磁盤佔用")} count={Math.round(num(metrics.disk_used_pct))} total={100}
            color={num(metrics.disk_used_pct) >= 90 ? "var(--red)" : "var(--ink)"}/>
          <div className="row g14 wrap" style={{ alignItems: "baseline" }}>
            {[[t("請求速率"), num(metrics.req_rate)], [t("5xx 錯誤率"), num(metrics.err5xx_rate)],
              [t("主進程 FD"), num(metrics.main_fd)], [t("登入失敗率"), num(metrics.login_fail_rate)],
              [t("SSH 失敗率"), num(metrics.ssh_fail_rate)]].map(([k, v]) => (
              <span key={k} className="col g2">
                <LB dim style={{ fontSize: 8 }}>{k}</LB>
                <span className="num" style={{ fontSize: 15, fontWeight: 700 }}>{v}</span>
              </span>
            ))}
          </div>
        </div>
      ) : (
        <div className="row g10 wrap" style={{ padding: "4px 0 2px" }}>
          {pending}
          <span className="muted" style={{ fontSize: 12 }}>{t("體徵指標待接入:消防員數據庫尚未回報,或後端未返回 live_metrics。")}</span>
        </div>
      )}

      <div className="row g14 wrap" style={{ marginTop: 18, paddingTop: 14, borderTop: "1px solid var(--hair)" }}>
        <span className="row g8">
          <LB dim>{t("消防員")}</LB>
          {st === null ? pending : st.firefighter_available ? <T tone="ok" dot>{t("在線")}</T> : st.firefighter_available === false ? <T tone="bad" dot>{t("離線")}</T> : pending}
        </span>
        <span className="row g8">
          <LB dim>{t("AI 引擎")}</LB>
          {eng ? (eng.connected
            ? <T tone="ok" dot>{t("已接入")}</T>
            : <T tone={eng.configured ? "warn" : "plain"}>{t("未接入")}</T>) : pending}
          {eng && eng.node && <span className="muted" style={{ fontSize: 11 }}>{eng.node}</span>}
          {eng && eng.latency_ms != null && <span className="num muted" style={{ fontSize: 11 }}>{eng.latency_ms}ms</span>}
        </span>
        <span className="row g8">
          <LB dim>{t("最新指標")}</LB>
          <span className="num muted" style={{ fontSize: 11.5 }}>{fmtTs(st && st.latest_metric_at)}</span>
        </span>
      </div>

      {/* 體檢結論 */}
      <div style={{ marginTop: 18, paddingTop: 14, borderTop: "1px solid var(--hair)" }}>
        <div className="row spread wrap g10" style={{ marginBottom: 8 }}>
          <div className="row g10">
            <LB dim>{t("健康探活結論")}</LB>
            {hcAt && <span className="mono muted" style={{ fontSize: 10 }}>{clock(hcAt)}</span>}
          </div>
          <B size="sm" icon="shield" disabled={hcBusy} onClick={runHealthcheck}>{hcBusy ? t("體檢中…") : t("跑一次體檢")}</B>
        </div>
        {hcBusy && <div className="step-line"><I name="refresh" size={10}/>{t("體檢中…")}</div>}
        {!hcBusy && hcGrade && (
          <div className="col g6">
            <div className="row g8 wrap">
              <T tone={hcGrade[0]} dot={hcGrade[0] !== "plain"}>{hcGrade[1]}</T>
              {hc && hc.elapsed_ms != null && <span className="num muted" style={{ fontSize: 10.5 }}>{t("耗時")} {hc.elapsed_ms}ms</span>}
              {hc && hc.result && hc.result.http_status != null && <span className="num muted" style={{ fontSize: 10.5 }}>{t("狀態碼")} {hc.result.http_status}</span>}
            </div>
            {(hcErr || (hc && (hc.ok === false || (hc.result || {}).status === "unhealthy"))) && (
              <div className="mono" style={{ fontSize: 11, color: "var(--red)", wordBreak: "break-word", lineHeight: 1.6 }}>
                ✗ {hcErr || failText(hc) || t("體檢未通過")}
              </div>
            )}
          </div>
        )}
        {!hcBusy && !hcGrade && <div className="muted" style={{ fontSize: 11.5, lineHeight: 1.6 }}>{t("只讀動作:POST /api/shield/repair {action:\"healthcheck\"},探活 /api/ai/health,不需確認。")}</div>}
      </div>
    </Band>

    {/* ═══ Band B · 修復動作 ═══ */}
    <Band no="B" title={t("修復動作")} sub={t("五個白名單動作 · 委派 shieldctl · 絕不裸 shell · 每一下都寫入審計")} delay={.15}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }}>
        {REPAIRS.map((a, i) => <RepairCard key={a.id} a={a} idx={i}/>)}
      </div>
      <div className="muted" style={{ fontSize: 10.5, marginTop: 14, lineHeight: 1.6 }}>
        {t("ADMIN 權力面:每一次修復都由後端寫入審計日誌(shield_repair),並受 SHIELD_REPAIR_APPLY 雙閘保護。")}
      </div>
    </Band>

    {/* ═══ Band C · 值守記錄 ═══ */}
    <Band no="C" title={t("值守記錄")} sub={t("消防員時間線 · 守護日誌 · AI 風險事件(取自 status 快照)")} delay={.2}
      right={<B size="sm" kind="ghost" icon="clipboard" onClick={() => { location.hash = "#/logs"; }}>{t("事件流見審計頁 →")}</B>}>
      {(incidents.length || timeline.length || guardian.length || risks.length) ? (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(340px, 1fr))", gap: "24px 48px" }}>
          <div className="col" style={{ minWidth: 0 }}>
            {!!incidents.length && (<>
              <LB red style={{ fontSize: 8.5, marginBottom: 6 }}>{t("進行中事件")}</LB>
              <div style={{ borderTop: "2px solid var(--rule)", marginBottom: 16 }}>
                {incidents.map((inc, i) => (
                  <div key={inc.id != null ? inc.id : "i" + i} className="ledger-row">
                    <span className="lr-idx">{pad2(i + 1)}</span>
                    <span className="mono" style={{ fontSize: 11, fontWeight: 700, flexShrink: 0 }}>INC-{S(inc.id)}</span>
                    <div className="col g4" style={{ flex: 1, minWidth: 0 }}>
                      <span style={{ fontWeight: 650, fontSize: 12.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{S(inc.title || inc.signature)}</span>
                      <span className="num muted" style={{ fontSize: 10.5 }}>{fmtTs(inc.opened_at)} · {t("嚴重度")} {num(inc.severity)}</span>
                    </div>
                    <StateTag st={inc.state}/>
                  </div>
                ))}
              </div>
            </>)}
            <LB dim style={{ fontSize: 8.5, marginBottom: 6 }}>{t("消防員最近事件")}</LB>
            <div style={{ borderTop: "2px solid var(--rule)" }}>
              {timeline.length ? timeline.map((it, i) => {
                const meta = kindMeta(String(it.kind || ""));
                return (
                  <div key={"t" + i} className="ledger-row">
                    <span className="lr-idx">{pad2(i + 1)}</span>
                    <span className="num muted" style={{ width: 128, fontSize: 11, flexShrink: 0 }}>{fmtTs(it.ts)}</span>
                    <T tone={meta[0]}>{t(meta[1])}</T>
                    <span className="muted" style={{ flex: 1, minWidth: 0, fontSize: 11.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{tlText(it)}</span>
                  </div>
                );
              }) : <div className="muted" style={{ fontSize: 12, padding: "12px 0" }}>{t("暫無")}</div>}
            </div>
          </div>
          <div className="col" style={{ minWidth: 0 }}>
            <LB dim style={{ fontSize: 8.5, marginBottom: 6 }}>{t("守護日誌尾")}</LB>
            <div style={{ borderTop: "2px solid var(--rule)", padding: "10px 0 16px" }}>
              {guardian.length ? guardian.slice(-8).map((line, i) => (
                <div key={"g" + i} className="mono" style={{ fontSize: 10.5, lineHeight: 1.7, wordBreak: "break-word", color: GUARD_ALERT_RE.test(String(line)) ? "var(--red)" : "var(--ink-3)" }}>{String(line)}</div>
              )) : <div className="muted" style={{ fontSize: 12 }}>{t("暫無")}</div>}
            </div>
            <LB dim style={{ fontSize: 8.5, marginBottom: 6 }}>{t("AI 風險事件")}</LB>
            <div style={{ borderTop: "2px solid var(--rule)" }}>
              {risks.length ? risks.map((r, i) => (
                <div key={r.id != null ? r.id : "r" + i} className="ledger-row">
                  <span className="lr-idx">{pad2(i + 1)}</span>
                  <div className="col g4" style={{ flex: 1, minWidth: 0 }}>
                    <span className="mono" style={{ fontSize: 11.5, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{S(r.command)}</span>
                    <span className="num muted" style={{ fontSize: 10.5 }}>{fmtTs(r.created_at)} · {S(r.actor_name)}</span>
                  </div>
                  {String(r.status || "") === "open" ? <T tone="warn" dot>{t("進行中")}</T> : <T tone="plain">{S(r.status)}</T>}
                </div>
              )) : <div className="muted" style={{ fontSize: 12, padding: "12px 0" }}>{t("暫無")}</div>}
            </div>
          </div>
        </div>
      ) : (
        <EM icon="shield" title={t("暫無值守事件")} sub={t("消防員尚未接入或暫無事件;完整操作留痕見審計頁。")}
          action={<B size="sm" icon="clipboard" onClick={() => { location.hash = "#/logs"; }}>{t("去審計頁")}</B>}/>
      )}
    </Band>
  </>);
};

window.W2.PAGES["shield"] = Page;
})();
