/**
 * Mira Console 完整版（Arcrun#3 console 系，2026-07-04 總管派工）
 *
 * 視覺：claude design 定稿「紙感暖黑 2a」（system-dev/docs/6-user/Mira Console 設計規劃/
 * Mira Console.dc.html + Mira Style Guide.dc.html）——紙紋底 repeating-linear-gradient、
 * 明體標題（Songti TC 級聯）、琥珀 var(--amber) 強調、墨綠呼吸球嵌「安」字、正文 ≥16px、
 * 手機優先 390px + ≥1024px 側欄、頁面本體不出水平捲軸。
 *
 * 資訊架構：brief 8 頁（docs/1-vision/mira-console-design-brief.md）＝
 *   1 登入/首次設定 2 駕駛艙 3 總庫搜尋 4 卡片詳頁+關聯 5 工作流 6 憑證管理
 *   7 分流台（原收件匣，Arcrun#9 改裝） 8 設定。
 * 單檔 HTML + 原生 JS hash routing（#/cockpit …），零外部資源、零 build step。
 *
 * 薄殼（rule 07）：零業務邏輯，只 fetch 既有 API 渲染。各頁資料源（誠實原則，無源的顯示
 * 誠實空狀態，禁假資料）：
 *   - 駕駛艙：GET /console/dashboard-data（免登入聚合端點，console-dashboard.ts）
 *   - 總庫搜尋：GET /kbdb/search（mode=semantic 未開 Vectorize 會誠實降級 + capability_hint；
 *     chips 用 entry_type——KBDB 真能篩的欄位，不編造「電腦筆記/手機筆記」等不存在的分類）；
 *     頭部統計來自 GET /console/kb-scale-data（精耕層 wiki 卡/三元組/已嵌入——leo 2026-07-07
 *     裁：45.8 萬 14-E 搬遷遺產 deprecated 不再當招牌數字；搜尋本身仍搜全庫）
 *   - 卡片詳頁：GET /kbdb/entries/:id；關聯視圖走 GET /kbdb/graph/neighbors/:name
 *     （cypher 代轉 kbdb-graph-plugin，kbdb-proxy.ts；查無 triplet → 「尚無關聯資料」）
 *   - 工作流：GET /webhooks/named（list）+ POST /webhooks/named/:name/trigger（手動觸發）
 *     + GET /workflows/:name/executions（最近執行）；釘選存 localStorage
 *   - 憑證：GET /credentials/catalog（D1 目錄唯讀，絕不含密文）+ POST /credentials（新增）
 *     + PUT /credentials/:name（整筆替換）；刪除後端未接 D1（T9 範圍）→ 標「即將開通」
 *   - 分流台：GET /console/triage-data（需 session——待辦/訊息原文屬機敏，計數才免登入）。
 *     資料源二合一（kb-ingest SDD R7 / Arcrun#9）：entry_type=todo（Logseq 萃取）＋
 *     entry_type=inbox（Telegram）；三欄 80/15/5（ai／collab／leo）＋ per-project chips
 *     ＋已完成預設隱藏可切換。分欄判定在 lib/console-triage-model.ts（純函式），頁面只渲染。
 *     勾掉/還原走 POST /console/triage-check（同 session 保護；leo 2026-07-08 拍板）——
 *     雙向銷案語意：console 勾掉＝終局（checked_via:console，萃取端絕不復活）、Logseq 改
 *     DONE 由萃取端 PATCH（checked_via:logseq）；console 只忠實顯示非 done 項。
 *   - 設定：vectorize 狀態探測（讀 /kbdb/search 回應的 mode/capability_hint，無新端點；
 *     開關本體需部署端 config kbdb_embed + acr update → 誠實文案講清楚、不假裝能遠端開）
 *     + MCP token TTL 誠實佔位（/console/settings-data；可調功能＝Arcrun#19）
 *     + /console/setup/reset 換帳密 + 系統資訊（精耕層規模）
 *
 * 2026-07-07 fix/console-truth-audit（總管稽核派工 + leo 裁決）：
 *   1. 全站日期/時間顯示統一 Asia/Taipei（lib/taipei-time.ts，與駕駛艙 #36 台北日判定同套；
 *      原本用瀏覽器本地 getter，換裝置就換日期；憑證頁加顯時分——7/6 22:57 建立不再被誤讀）。
 *   2. 總庫搜尋/設定頁頭部統計改精耕層（458,732 是真數字——cypher 與 base 一手同值實證——
 *      但 leo 裁 14-E 遺產 deprecated 不再顯示）。
 *   3. 駕駛艙「今日完成/今日路線」接 sprint 任務板真資料（dashboard-data 端，同 #36 模式）。
 *
 * 認證模型沿 v1（console-auth.ts）：單一管理員 email+password，session token 在 localStorage，
 * 實際打 /kbdb/* 用後端回的固定租戶字串（CONSOLE_TENANT）。駕駛艙免登入版在 /console/dashboard。
 *
 * v0 的 components/recipes 查詢區（Arcrun#3 原搜尋台）收進「工作流」頁下方折疊區，不砍功能。
 *
 * 2026-07-04 leo 實測三回饋（總管派工二輪）：
 *   1. 深/淺主題：CSS custom properties 兩份色板（:root=淺色紙感宣紙米白+墨字，
 *      :root[data-theme=dark]=原定稿暖黑不動），預設淺色，選擇存 localStorage
 *      （key arcrun_console_theme），head 預載腳本防閃色。切換入口＝登入/首次設定頁小鈕、
 *      側欄項、設定頁正式開關。金琥珀例外（按鈕漸層/狀態球/toast/gcenter）兩版不變。
 *   2. 登入/首次設定置中：.authwrap.view.on 補 display:flex（原被 .view.on 的 block 蓋掉 → 靠左）。
 *   3. fetch 韌性：friendlyErr() 把裸 Failed to fetch / Load failed 轉「連線中斷」誠實文案；
 *      駕駛艙 60 秒定時器常駐，網路恢復自動刷回。
 */
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { TAIPEI_CLIENT_JS } from '../lib/taipei-time';

export const consoleRouter = new Hono<{ Bindings: Bindings }>();

function renderConsoleHtml(registryBase: string, brand: string, profile: string): string {
  // Console profile（config 裁剪，非 auth——#24/#25 多人 Portal 是另一回事）：
  //   full（未設即此）＝全套，Mira 實例現行為一字不變。
  //   rag＝企業產品樣：只留 總庫搜尋/工作流/設定（card 是搜尋的詳頁，跟著留），落地＝搜尋頁。
  // 隱藏頁的 view div 仍在 DOM（display:none、路由 guard 導回，永遠點不進）——
  // 因 creds/inbox 有 top-level addEventListener 綁定，抽掉 div 會讓整包 JS 啟動即炸，
  // 純渲染層裁剪以「導不進去」為準，不為了抽 DOM 把 null-guard 灑滿全檔。
  const rag = profile === 'rag';
  const views = rag
    ? ['search', 'card', 'workflows', 'settings']
    : ['cockpit', 'search', 'card', 'workflows', 'creds', 'inbox', 'settings'];
  const home = rag ? 'search' : 'cockpit';
  return `<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${brand} Console</title>
<script>
  // 主題預載（防閃色）：預設淺色（leo 2026-07-04 明示），選擇存 localStorage
  document.documentElement.setAttribute('data-theme', (function () {
    try { return localStorage.getItem('arcrun_console_theme') === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; }
  })());
</script>
<style>
  * { box-sizing: border-box; }
  /* ── 主題色板（一套結構兩份色值）：預設淺色＝宣紙米白紙紋＋墨色文字；深色＝定稿紙感暖黑 2a 原值不動 ── */
  :root {
    --paper-a: #f4eddc; --paper-b: #f1e9d6;          /* 宣紙米白紙紋 */
    --ink: #2f2a20; --ink-rgb: 30,24,14;             /* 墨色文字 */
    --amber: #8a5f1e; --amber-rgb: 138,95,30;        /* 琥珀沿用、調深到淺底可讀 */
    --ok: #1d7a48; --ok-rgb: 29,122,72; --ok-soft: #1d6b40;
    --err: #b03a26; --err-rgb: 176,58,38;
    --well: rgba(30,24,14,.06); --well2: rgba(30,24,14,.08);
    --bar-bg: #efe7d3; --track: rgba(30,24,14,.12); --knob: #ffffff;
  }
  :root[data-theme="dark"] {
    --paper-a: #191410; --paper-b: #1b1611;
    --ink: #ede4d3; --ink-rgb: 237,228,211;
    --amber: #e8b45a; --amber-rgb: 232,180,90;
    --ok: #7fe0a8; --ok-rgb: 63,190,120; --ok-soft: #a8e8c4;
    --err: #e58575; --err-rgb: 217,95,76;
    --well: rgba(0,0,0,.25); --well2: rgba(0,0,0,.3);
    --bar-bg: #17120e; --track: rgba(255,255,255,.08); --knob: #ede4d3;
  }
  html, body { margin: 0; background: repeating-linear-gradient(0deg,var(--paper-a) 0px,var(--paper-a) 3px,var(--paper-b) 3px,var(--paper-b) 4px); color: var(--ink);
    font-family: -apple-system, "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
  input, textarea, button { font-family: inherit; }
  ::placeholder { color: rgba(var(--ink-rgb),.35); }
  input:focus, textarea:focus { outline: 2px solid rgba(var(--amber-rgb),.5); outline-offset: 1px; }
  a { color: var(--amber); }
  .serif { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; }
  .mono { font-family: ui-monospace, Menlo, monospace; }
  .muted { color: rgba(var(--ink-rgb),.55); }
  .dim { color: rgba(var(--ink-rgb),.4); }
  .err { color: var(--err); }
  .ok { color: var(--ok); }
  @keyframes breatheGreen { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--ok-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--ok-rgb),.55); } }
  @keyframes breatheAmber { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--amber-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--amber-rgb),.6); } }
  @keyframes breatheRed { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--err-rgb),.4); } 50% { box-shadow: 0 0 44px 16px rgba(var(--err-rgb),.65); } }

  /* ── auth 全屏 ── */
  .authwrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
  .authbox { width: 100%; max-width: 400px; display: flex; flex-direction: column; gap: 24px; text-align: center; }
  .brand { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 44px; letter-spacing: .24em; color: var(--amber); }
  .brand-sub { margin-top: 10px; font-size: 15px; color: rgba(var(--ink-rgb),.55); letter-spacing: .1em; }

  /* ── 版面骨架 ── */
  #shell { display: none; min-height: 100vh; }
  #shell.on { display: flex; }
  #sidenav { display: none; }
  main { flex: 1; min-width: 0; padding-bottom: 96px; }
  .page { max-width: 980px; margin: 0 auto; padding: 0 20px 40px; }
  .page.narrow { max-width: 760px; }
  .page.wide { max-width: 1080px; }
  .pagehead { padding: 22px 2px 14px; border-bottom: 2px solid rgba(var(--amber-rgb),.4); display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
  .pagehead .t { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; letter-spacing: .2em; white-space: nowrap; }
  .pagehead .m { font-size: 13.5px; color: rgba(var(--ink-rgb),.45); }
  #tabbar { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bar-bg); border-top: 2px solid rgba(var(--amber-rgb),.4); display: none; z-index: 50; padding-bottom: env(safe-area-inset-bottom); }
  #tabbar.on { display: flex; }
  #tabbar .tab { flex: 1; text-align: center; padding: 15px 0 13px; font-size: 14.5px; cursor: pointer; min-height: 44px; color: rgba(var(--ink-rgb),.5); border-top: 2px solid transparent; margin-top: -2px; }
  #tabbar .tab.on { color: var(--amber); font-weight: 600; border-top-color: var(--amber); }
  @media (min-width: 1024px) {
    #tabbar.on { display: none; }
    main { padding-bottom: 20px; }
    #sidenav { display: flex; width: 216px; flex: none; position: sticky; top: 0; height: 100vh; border-right: 2px solid rgba(var(--amber-rgb),.35); padding: 28px 0; flex-direction: column; gap: 4px; }
    #sidenav .logo { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 26px; letter-spacing: .22em; color: var(--amber); padding: 0 24px 20px; }
    #sidenav .nav { padding: 13px 24px; font-size: 16px; cursor: pointer; letter-spacing: .08em; color: rgba(var(--ink-rgb),.6); border-right: 2px solid transparent; }
    #sidenav .nav.on { color: var(--amber); background: rgba(var(--amber-rgb),.08); border-right-color: var(--amber); font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-weight: 600; }
    #sidenav .foot { margin-top: auto; padding: 0 24px; font-size: 12.5px; color: rgba(var(--ink-rgb),.35); line-height: 1.7; }
  }
  .view { display: none; }
  .view.on { display: block; }
  /* 登入/首次設定＝flex 置中（.view.on 的 display:block 會蓋掉 .authwrap 的 flex → 內容靠左，leo 實測回饋修正） */
  .authwrap.view.on { display: flex; }

  /* ── 元件 ── */
  .btn { padding: 13px; font-size: 16px; font-weight: 600; border-radius: 10px; border: none; background: linear-gradient(90deg,#b98330,#e8b45a); color: #241804; cursor: pointer; }
  .btn:disabled { opacity: .45; cursor: default; }
  .btn2 { padding: 11px 16px; font-size: 15px; border-radius: 10px; border: 1px solid rgba(var(--amber-rgb),.6); background: none; color: var(--amber); cursor: pointer; }
  .btn3 { padding: 11px 16px; font-size: 15px; border-radius: 10px; border: 1px solid rgba(var(--ink-rgb),.25); background: none; color: rgba(var(--ink-rgb),.7); cursor: pointer; }
  .btn-danger { padding: 12px 18px; font-size: 15px; border-radius: 10px; border: 1px solid rgba(var(--err-rgb),.5); background: none; color: var(--err); cursor: pointer; }
  .btn-danger:disabled { opacity: .4; cursor: default; }
  .txt { width: 100%; padding: 13px 15px; font-size: 16px; border-radius: 10px; border: 1px solid rgba(var(--ink-rgb),.2); background: rgba(var(--ink-rgb),.05); color: var(--ink); }
  textarea.txt { resize: vertical; font-family: ui-monospace, Menlo, monospace; font-size: 14.5px; }
  .panel { padding: 18px; border-radius: 13px; background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.12); }
  .card-item { border-radius: 13px; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.1); }
  .card-item.hl { border-color: rgba(var(--amber-rgb),.3); }
  .tag { display: inline-block; padding: 3px 9px; border-radius: 5px; background: rgba(var(--amber-rgb),.12); color: var(--amber); font-size: 12.5px; }
  .tag.green { background: rgba(var(--ok-rgb),.12); color: var(--ok); }
  .chip { flex: none; padding: 9px 16px; font-size: 14.5px; border-radius: 999px; cursor: pointer; border: 1px solid rgba(var(--ink-rgb),.18); background: none; color: rgba(var(--ink-rgb),.6); }
  .chip.on { border-color: rgba(var(--amber-rgb),.6); background: rgba(var(--amber-rgb),.14); color: var(--amber); font-weight: 600; }
  .hscroll { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
  .subhead { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); }
  .honest { padding: 22px; border-radius: 13px; border: 1px dashed rgba(var(--amber-rgb),.4); background: rgba(var(--amber-rgb),.06); text-align: center; }
  .honest .h { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 19px; color: var(--amber); margin-bottom: 8px; }
  .honest .b { font-size: 15px; color: rgba(var(--ink-rgb),.6); line-height: 1.7; }
  #toast { position: fixed; left: 50%; bottom: 110px; transform: translateX(-50%); z-index: 99; padding: 13px 22px; border-radius: 11px; background: #e8b45a; color: #241804; font-size: 15.5px; font-weight: 600; box-shadow: 0 6px 24px rgba(0,0,0,.5); white-space: nowrap; display: none; }

  /* ── 駕駛艙 ── */
  .orb-row { display: flex; align-items: center; gap: 20px; padding: 26px 2px 20px; }
  .orb { width: 84px; height: 84px; border-radius: 50%; flex: none; display: grid; place-items: center; }
  .orb span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 30px; font-weight: 600; color: rgba(10,20,14,.85); text-shadow: 0 1px 0 rgba(255,255,255,.25); }
  .orb-title { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; font-weight: 600; }
  .orb-sub { margin-top: 5px; font-size: 15px; color: rgba(var(--ink-rgb),.6); line-height: 1.55; }
  .cockgrid { display: grid; grid-template-columns: 1fr; gap: 0 40px; }
  @media (min-width: 1024px) { .cockgrid { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); } }
  .bricks { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .brick { padding: 16px; border-radius: 12px; }
  .brick.amber { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.22); }
  .brick.plain { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.14); cursor: pointer; }
  .brick .lbl { font-size: 13.5px; color: rgba(var(--ink-rgb),.55); margin-bottom: 6px; }
  .brick .num { font-family: ui-monospace, Menlo, monospace; font-size: 26px; color: var(--amber); }
  .brick .num small { font-size: 15px; color: rgba(var(--ink-rgb),.5); }
  .bar { margin-top: 10px; height: 6px; border-radius: 3px; background: var(--track); }
  .bar > i { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg,#b98330,#e8b45a); transition: width .6s; }
  .wait-box { margin-top: 14px; padding: 20px; border-radius: 12px; border: 1px dashed rgba(var(--ok-rgb),.3); background: rgba(var(--ok-rgb),.05); }
  .wait-box.has { border-color: rgba(var(--amber-rgb),.45); background: rgba(var(--amber-rgb),.05); }
  .wait-head { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); margin-bottom: 10px; text-align: center; }
  .wait-none { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 20px; color: var(--ok); letter-spacing: .08em; text-align: center; }
  .wait-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-top: 8px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.3); font-size: 16px; line-height: 1.5; }
  .wait-item .dm { color: var(--amber); font-size: 17px; flex: none; }
  ul.route { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
  ul.route li { display: flex; align-items: flex-start; gap: 12px; padding: 13px 16px; border-radius: 11px; background: rgba(var(--ink-rgb),.045); border: 1px solid transparent; font-size: 16px; line-height: 1.4; }
  ul.route li.doing { background: rgba(var(--amber-rgb),.09); border-color: rgba(var(--amber-rgb),.3); }
  ul.route li .ic { flex: none; font-size: 15px; margin-top: 2px; }
  ul.route li.done { color: rgba(var(--ink-rgb),.65); } ul.route li.done .ic { color: var(--ok); }
  ul.route li.doing .ic { color: var(--amber); }
  ul.route li.todo { color: rgba(var(--ink-rgb),.6); } ul.route li.todo .ic { color: rgba(var(--ink-rgb),.35); }
  ul.route li.blocked .ic { color: var(--err); }

  /* ── 搜尋 ── */
  .searchrow { display: flex; gap: 10px; align-items: stretch; }
  .searchrow input { flex: 1; min-width: 0; padding: 16px 18px; font-size: 18px; border-radius: 13px; border: 1.5px solid rgba(var(--amber-rgb),.35); background: rgba(var(--ink-rgb),.05); color: var(--ink); }
  .sembtn { flex: none; padding: 0 20px; font-size: 16px; border-radius: 13px; cursor: pointer; letter-spacing: .1em; border: 1.5px solid rgba(var(--ink-rgb),.2); background: none; color: rgba(var(--ink-rgb),.55); }
  .sembtn.on { border-color: var(--amber); background: rgba(var(--amber-rgb),.15); color: var(--amber); font-weight: 600; }
  .kgrid { display: grid; grid-template-columns: 1fr; gap: 12px; padding-bottom: 8px; }
  @media (min-width: 700px) { .kgrid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } }
  .kcard { padding: 18px; border-radius: 13px; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.1); cursor: pointer; display: flex; flex-direction: column; gap: 9px; }
  .kcard:hover { border-color: rgba(var(--amber-rgb),.45); background: rgba(var(--amber-rgb),.06); }
  .kcard .kt { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 18.5px; font-weight: 600; line-height: 1.45; word-break: break-word; }
  .kcard .ks { font-size: 15px; color: rgba(var(--ink-rgb),.65); line-height: 1.65; word-break: break-word; }
  .kcard .km { display: flex; gap: 10px; align-items: center; margin-top: auto; font-size: 13px; flex-wrap: wrap; }

  /* ── 卡片詳頁 ── */
  .cardwrap { display: flex; flex-wrap: wrap; gap: 32px; padding: 24px 0 40px; }
  .cardmain { flex: 1 1 540px; min-width: 0; }
  .cardside { flex: 1 1 360px; min-width: 0; }
  .cardtitle { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 28px; font-weight: 600; line-height: 1.4; word-break: break-word; }
  .md { margin-top: 18px; }
  .md p { font-size: 16.5px; line-height: 1.85; color: rgba(var(--ink-rgb),.82); margin: 8px 0; word-break: break-word; }
  .md h1, .md h2, .md h3, .md h4 { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-weight: 600; color: var(--amber); margin: 18px 0 6px; line-height: 1.4; }
  .md h1 { font-size: 22px; } .md h2 { font-size: 20px; } .md h3 { font-size: 18px; } .md h4 { font-size: 17px; }
  .md ul, .md ol { margin: 8px 0; padding-left: 24px; }
  .md li { font-size: 16.5px; line-height: 1.8; color: rgba(var(--ink-rgb),.82); margin: 4px 0; word-break: break-word; }
  .md blockquote { margin: 10px 0; padding: 12px 18px; border-left: 3px solid rgba(var(--amber-rgb),.5); font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; line-height: 1.8; color: rgba(var(--ink-rgb),.7); }
  .md code { font-family: ui-monospace, Menlo, monospace; font-size: 14.5px; background: var(--well2); border-radius: 4px; padding: 1px 6px; word-break: break-all; }
  .md pre { background: var(--well2); border: 1px solid rgba(var(--ink-rgb),.1); border-radius: 10px; padding: 14px; overflow-x: auto; }
  .md pre code { background: none; padding: 0; }
  .srcline { margin-top: 24px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 11px; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.12); }
  .graphbox { position: relative; height: 340px; border-radius: 14px; background: var(--well); border: 1px solid rgba(var(--ink-rgb),.1); overflow: hidden; }
  .gnode { position: absolute; transform: translate(-50%,-50%); max-width: 96px; padding: 8px 11px; border-radius: 999px; text-align: center; font-size: 12.5px; line-height: 1.3; cursor: pointer; word-break: break-word; background: rgba(var(--ok-rgb),.1); border: 1px solid rgba(var(--ok-rgb),.35); color: var(--ok-soft); }
  .gcenter { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 104px; height: 104px; border-radius: 50%; background: radial-gradient(circle at 36% 30%,rgba(242,209,148,.95),rgba(232,180,90,.9) 55%,rgba(138,95,30,.95)); display: grid; place-items: center; text-align: center; padding: 8px; box-shadow: 0 0 30px rgba(var(--amber-rgb),.3); }
  .gcenter span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 13.5px; font-weight: 600; line-height: 1.35; color: #241804; word-break: break-word; }
  .nbrow { display: flex; align-items: center; gap: 11px; padding: 12px 14px; border-radius: 10px; cursor: pointer; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.1); font-size: 15.5px; }

  /* ── 工作流 / 憑證 / 分流台 清單 ── */
  .listcol { display: flex; flex-direction: column; gap: 10px; padding: 16px 0 8px; }
  .rowhead { display: flex; align-items: center; gap: 12px; padding: 16px 18px; cursor: pointer; }
  .rowbody { padding: 0 18px 18px; border-top: 1px solid rgba(var(--ink-rgb),.1); }
  .pinbtn { flex: none; width: 34px; height: 34px; border-radius: 8px; border: none; cursor: pointer; font-size: 17px; background: rgba(var(--ink-rgb),.06); color: rgba(var(--ink-rgb),.4); }
  .pinbtn.on { background: rgba(var(--amber-rgb),.15); color: var(--amber); }
  .urlbox { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 10px; background: var(--well); border: 1px solid rgba(var(--ink-rgb),.1); min-width: 0; }
  .urlbox .u { font-family: ui-monospace, Menlo, monospace; font-size: 13.5px; color: var(--amber); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .avatar { width: 38px; height: 38px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.25); display: grid; place-items: center; font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; color: var(--amber); flex: none; }
  .maskline { display: flex; align-items: center; gap: 10px; padding: 14px; border-radius: 10px; background: var(--well); border: 1px solid rgba(var(--ink-rgb),.1); }
  /* 分流台（Arcrun#9）：三欄 80/15/5 手機直排；欄色點 ai=綠 collab=琥珀 leo=紅 */
  .tri-sec { margin-top: 24px; }
  .tri-sechead { display: flex; align-items: baseline; gap: 9px; }
  .tri-sechead .dot { flex: none; width: 9px; height: 9px; border-radius: 50%; align-self: center; }
  .tri-sec.ai .dot { background: var(--ok); } .tri-sec.collab .dot { background: var(--amber); } .tri-sec.leo .dot { background: var(--err); }
  .tri-sechead .nm { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; letter-spacing: .14em; }
  .tri-sechead .pc { font-size: 12.5px; color: rgba(var(--ink-rgb),.4); }
  .tri-sechead .ct { margin-left: auto; font-family: ui-monospace, Menlo, monospace; font-size: 14px; color: rgba(var(--ink-rgb),.55); }
  .tri-item { padding: 14px 16px; border-radius: 12px; margin-top: 9px; }
  .tri-item.new { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.28); }
  .tri-sec.leo .tri-item.new { background: rgba(var(--err-rgb),.06); border-color: rgba(var(--err-rgb),.3); }
  .tri-item.done { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.1); opacity: .7; }
  .tri-item.done .tx { text-decoration: line-through; color: rgba(var(--ink-rgb),.6); }
  .tri-item .tx { font-size: 16.5px; line-height: 1.6; word-break: break-word; }
  .tri-item .mt { margin-top: 9px; display: flex; gap: 7px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: rgba(var(--ink-rgb),.45); }
  .tri-item .row { display: flex; gap: 12px; align-items: flex-start; }
  .tri-item .row .tx { flex: 1; min-width: 0; }
  /* 勾掉鈕：≥44px 圓形（手機好按），綠框空心＝待勾；按下去由後端回寫才算數（誠實，不先假裝成功） */
  .tri-check { flex: none; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; font-size: 19px; line-height: 1;
    border: 1.5px solid rgba(var(--ok-rgb),.55); background: rgba(var(--ok-rgb),.06); color: var(--ok); }
  .tri-check:active { background: rgba(var(--ok-rgb),.22); }
  .tri-check:disabled { opacity: .45; cursor: default; }
  /* 還原鈕（誤勾救濟，只出現在「顯示已完成」清單）：小顆但仍 44px 高好按 */
  .tri-restore { flex: none; min-height: 32px; padding: 5px 14px; border-radius: 999px; cursor: pointer; font-size: 13px;
    border: 1px solid rgba(var(--amber-rgb),.5); background: none; color: var(--amber); }
  .tri-restore:disabled { opacity: .45; cursor: default; }
  .tag.red { background: rgba(var(--err-rgb),.12); color: var(--err); }
  .tag.dim { background: rgba(var(--ink-rgb),.08); color: rgba(var(--ink-rgb),.55); }

  /* ── 設定 ── */
  .setcol { display: flex; flex-direction: column; gap: 14px; padding: 20px 0 40px; }
  .switch { flex: none; margin-left: auto; width: 56px; height: 32px; border-radius: 999px; padding: 3px; background: rgba(var(--ink-rgb),.15); transition: background .25s; }
  .switch.on { background: rgba(var(--ok-rgb),.5); }
  .switch i { display: block; width: 26px; height: 26px; border-radius: 50%; background: var(--knob); box-shadow: 0 1px 3px rgba(0,0,0,.25); transition: transform .25s; }
  .switch.on i { transform: translateX(24px); }
  .kvline { display: flex; justify-content: space-between; gap: 12px; font-size: 15px; margin: 5px 0; }
</style>
</head>
<body>

<!-- 登入 -->
<div class="authwrap view" id="v-login">
  <div class="authbox">
    <div>
      <div class="brand">${brand}</div>
      <div class="brand-sub">私人系統・僅供擁有者進入</div>
    </div>
    <div style="display:flex;flex-direction:column;gap:12px;text-align:left">
      <input type="email" id="login-email" class="txt" placeholder="Email" style="padding:15px 16px;font-size:17px;border-radius:11px">
      <input type="password" id="login-password" class="txt" placeholder="密碼" style="padding:15px 16px;font-size:17px;border-radius:11px">
      <button class="btn" id="login-submit" style="margin-top:6px;padding:15px;font-size:17px;letter-spacing:.2em">進入</button>
      <div id="login-status" class="err" style="font-size:14px;min-height:1.2em"></div>
    </div>
    <div style="font-size:13.5px;color:rgba(var(--ink-rgb),.4);line-height:1.7">這是一個人的智慧總部。<br>若你不是擁有者，這裡沒有你要找的東西。</div>
    ${rag ? '' : '<a href="/console/dashboard" style="font-size:13.5px;color:rgba(var(--amber-rgb),.7)">免登入看駕駛艙 ›</a>'}
    <button class="btn3 themelabel" data-themetoggle style="align-self:center;padding:8px 16px;font-size:13.5px;border-radius:999px">☾ 切深色</button>
  </div>
</div>

<!-- 首次設定 -->
<div class="authwrap view" id="v-setup">
  <div class="authbox" style="max-width:440px;text-align:left">
    <div style="text-align:center">
      <div class="brand" style="font-size:38px">${brand}</div>
      <div class="serif" style="margin-top:12px;font-size:21px">首次設定</div>
      <div style="margin-top:8px;font-size:15px;color:rgba(var(--ink-rgb),.55);line-height:1.7">系統偵測到尚未設定擁有者帳號。<br>設定一組 Email 與密碼，之後只有你能進入。</div>
    </div>
    <div style="display:flex;flex-direction:column;gap:12px">
      <input type="email" id="setup-email" class="txt" placeholder="你的 Email" style="padding:15px 16px;font-size:17px;border-radius:11px">
      <input type="password" id="setup-password" class="txt" placeholder="設定密碼（至少 8 碼）" style="padding:15px 16px;font-size:17px;border-radius:11px">
      <input type="password" id="setup-password2" class="txt" placeholder="再輸入一次密碼" style="padding:15px 16px;font-size:17px;border-radius:11px">
      <button class="btn" id="setup-submit" style="margin-top:6px;padding:15px;font-size:17px;letter-spacing:.12em">完成設定並進入</button>
      <div id="setup-status" class="err" style="font-size:14px;min-height:1.2em"></div>
    </div>
    <button class="btn3 themelabel" data-themetoggle style="align-self:center;padding:8px 16px;font-size:13.5px;border-radius:999px">☾ 切深色</button>
  </div>
</div>

<!-- 主應用 -->
<div id="shell">
  <div id="sidenav">
    <div class="logo">${brand}</div>
    ${rag ? '' : '<div class="nav" data-nav="cockpit">駕駛艙</div>'}
    <div class="nav" data-nav="search">總庫搜尋</div>
    <div class="nav" data-nav="workflows">工作流</div>
    ${rag ? '' : `<div class="nav" data-nav="creds">憑證管理</div>
    <div class="nav" data-nav="inbox">分流台</div>`}
    <div class="nav" data-nav="settings">設定</div>
    <div class="nav themelabel" data-themetoggle style="margin-top:8px;font-size:14.5px">☾ 切深色</div>
    <div class="foot" id="side-foot"></div>
  </div>
  <main>

  <!-- 駕駛艙 -->
  <div class="view page" id="v-cockpit">
    <div class="pagehead"><span class="t">${brand}<span style="font-size:14px;letter-spacing:.3em;color:rgba(var(--ink-rgb),.5)">　控制台</span></span><span class="m serif" id="ck-date"></span></div>
    <div class="cockgrid">
      <div>
        <div class="orb-row">
          <div class="orb" id="ck-orb" style="background:radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)"><span id="ck-char">…</span></div>
          <div>
            <div class="orb-title" id="ck-title">載入中</div>
            <div class="orb-sub" id="ck-sub"></div>
          </div>
        </div>
        <div class="bricks">
          <div class="brick amber">
            <div class="lbl">今日完成</div>
            <div class="num"><span id="ck-done">–</span><small> / <span id="ck-total">–</span> 件</small></div>
            <div class="bar"><i id="ck-bar" style="width:0%"></i></div>
          </div>
          <div class="brick plain" data-nav="inbox">
            <div class="lbl">收件匣未處理</div>
            <div class="num"><span id="ck-inbox">–</span><small> 條</small></div>
            <div class="lbl" style="margin:10px 0 0">來自 Telegram</div>
          </div>
        </div>
        <div class="wait-box" id="ck-waitbox">
          <div class="wait-head">等你的事</div>
          <div id="ck-wait" class="wait-none">載入中…</div>
          <div id="ck-waitmeta" style="margin-top:10px;text-align:center;font-size:12.5px;color:rgba(var(--ink-rgb),.45);line-height:1.7"></div>
        </div>
      </div>
      <div>
        <div style="display:flex;justify-content:space-between;align-items:baseline;margin:24px 0 10px">
          <span class="subhead">今日路線</span><span id="ck-route-src" style="font-size:13px;color:rgba(var(--ink-rgb),.4)">狀態即時同步</span>
        </div>
        <ul class="route" id="ck-today"><li class="todo"><span class="ic">○</span><span class="muted">載入中…</span></li></ul>
        <div class="subhead" id="ck-weekhead" style="display:none;margin:18px 0 10px">本週</div>
        <ul class="route" id="ck-week"></ul>
        <div style="margin:16px 0 8px;text-align:center;font-size:12.5px;color:rgba(var(--ink-rgb),.35);line-height:1.8" id="ck-stamp">每 60 秒自動刷新</div>
      </div>
    </div>
  </div>

  <!-- 總庫搜尋 -->
  <div class="view page wide" id="v-search">
    <div class="pagehead"><span class="t">總庫搜尋</span><span class="m mono" id="se-scale"></span></div>
    <div style="padding-top:20px">
      <div class="searchrow">
        <input id="se-q" placeholder="搜尋知識…" autocomplete="off">
        <button class="sembtn" id="se-sem">語意</button>
      </div>
      <div class="hscroll" id="se-chips" style="margin-top:12px"></div>
      <div id="se-banner"></div>
      <div id="se-count" style="margin:20px 0 12px;font-size:14px;color:rgba(var(--ink-rgb),.5)"></div>
      <div class="kgrid" id="se-results"></div>
    </div>
  </div>

  <!-- 卡片詳頁 -->
  <div class="view page wide" id="v-card">
    <div class="pagehead" style="justify-content:flex-start;gap:14px">
      <button class="btn2" style="border:none;padding:0;font-size:16px" data-nav="search">‹ 搜尋</button>
      <span class="serif" style="font-size:17px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.55)">知識卡片</span>
    </div>
    <div class="cardwrap">
      <div class="cardmain" id="cd-main"><div class="muted" style="padding:24px 0">載入中…</div></div>
      <div class="cardside">
        <div class="subhead" style="margin:6px 0 12px;display:block">關聯視圖</div>
        <div id="cd-graph"></div>
        <div id="cd-neighbors" style="margin-top:16px;display:flex;flex-direction:column;gap:7px"></div>
      </div>
    </div>
  </div>

  <!-- 工作流 -->
  <div class="view page narrow" id="v-workflows">
    <div class="pagehead"><span class="t">工作流</span><span class="m" id="wf-count"></span></div>
    <input id="wf-q" class="txt" placeholder="搜尋工作流…" style="margin-top:18px" autocomplete="off">
    <div class="listcol" id="wf-list"><div class="muted">載入中…</div></div>
    <div style="margin-top:26px">
      <div class="subhead" style="display:block;margin-bottom:10px">零件與 Recipes（框架資源）</div>
      <div class="searchrow">
        <input id="cr-q" class="txt" placeholder="如「telegram send」「gsheets append」" style="font-size:15px;padding:12px 14px;border-radius:11px;border-width:1px">
        <button class="btn2" id="cr-go" style="flex:none">搜</button>
      </div>
      <div id="cr-out" style="margin-top:10px"></div>
    </div>
  </div>

  <!-- 憑證管理 -->
  <div class="view page narrow" id="v-creds">
    <div class="pagehead"><span class="t">憑證管理</span><button class="btn2" id="cred-addbtn" style="padding:9px 16px;font-size:14.5px">＋ 新增憑證</button></div>
    <div style="margin-top:16px;padding:13px 16px;border-radius:11px;background:rgba(var(--ok-rgb),.05);border:1px solid rgba(var(--ok-rgb),.2);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
      <span class="ok">🛡 保險箱原則</span>：系統只保存目錄與加密後的值，<b style="color:var(--ink)">任何頁面都看不到密文內容</b>。只能整筆替換或刪除。
    </div>
    <div id="cred-form" style="display:none;margin-top:14px" class="panel">
      <div class="serif" style="font-size:17px;color:var(--amber);margin-bottom:10px">新增憑證</div>
      <div style="display:flex;flex-direction:column;gap:10px">
        <input id="cred-name" class="txt" placeholder="名稱（英數與底線，例：openai_main_key）">
        <input id="cred-service" class="txt" placeholder="服務（例：OpenAI）">
        <textarea id="cred-secret" class="txt" placeholder="密文值（保存後不再顯示）" rows="2"></textarea>
        <button class="btn" id="cred-save">加密保存</button>
        <div id="cred-form-status" style="font-size:14px;min-height:1.2em"></div>
      </div>
    </div>
    <div class="listcol" id="cred-list"><div class="muted">載入中…</div></div>
  </div>

  <!-- 分流台（原收件匣，Arcrun#9 改裝＝全部待辦分流台） -->
  <div class="view page narrow" id="v-inbox">
    <div class="pagehead"><span class="t">分流台</span><span class="m">全部待辦・80/15/5</span></div>
    <div class="hscroll" id="tg-chips" style="margin-top:14px"></div>
    <div id="tg-out" style="padding-top:2px"><div class="muted" style="padding:14px 0">載入中…</div></div>
  </div>

  <!-- 設定 -->
  <div class="view page narrow" id="v-settings">
    <div class="pagehead"><span class="t">設定</span></div>
    <div class="setcol">
      <div class="panel">
        <div style="display:flex;align-items:center;gap:14px">
          <div style="min-width:0">
            <div style="font-size:17px;font-weight:600">深色模式</div>
            <div style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">預設淺色（紙感）。切換立即生效，選擇記在這台裝置（localStorage）。</div>
          </div>
          <div class="switch" id="st-theme-switch" data-themetoggle style="cursor:pointer"><i></i></div>
        </div>
      </div>
      <div class="panel">
        <div style="display:flex;align-items:center;gap:14px">
          <div style="min-width:0">
            <div style="font-size:17px;font-weight:600">語意搜尋（vectorize）</div>
            <div id="st-vec" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">狀態偵測中…</div>
          </div>
          <div class="switch" id="st-vec-switch" title="此開關不能遠端改，只如實顯示狀態"><i></i></div>
        </div>
        <div style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
          誠實提示：開關真相＝部署端 <code style="font-size:12.5px">~/.arcrun/config.yaml</code> 的 <code style="font-size:12.5px">kbdb_embed: true</code> ＋ <code style="font-size:12.5px">acr update</code> 重部署（#32 教訓：wrangler 直推改的形態 config 要同步）。Console 只如實顯示狀態，不假裝能遠端開啟。目前狀態：<b style="color:var(--amber)" id="st-vec-state">偵測中…</b>
        </div>
      </div>
      <div class="panel">
        <div style="font-size:17px;font-weight:600">MCP token 有效期（TTL）</div>
        <div id="st-mcp-ttl" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">讀取中…</div>
        <div style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
          誠實佔位：此頁<b style="color:var(--amber)">還不能改</b>這個值——可調功能＝Arcrun#19（設定頁改→存 KBDB→發 token 時讀）。實作前要調整請在部署端 env <code style="font-size:12.5px">MCP_TOKEN_TTL</code>（mcp worker）設定。
        </div>
      </div>
      <div class="panel">
        <div style="font-size:17px;font-weight:600">更換帳號密碼</div>
        <div style="margin-top:4px;font-size:14px;color:rgba(var(--ink-rgb),.55)">需輸入舊密碼驗證身分</div>
        <div style="margin-top:14px;display:flex;flex-direction:column;gap:10px">
          <input type="password" id="st-pw-old" class="txt" placeholder="舊密碼">
          <input type="email" id="st-pw-email" class="txt" placeholder="Email（可沿用或換新）">
          <input type="password" id="st-pw-new" class="txt" placeholder="新密碼（至少 8 碼）">
          <button class="btn" id="st-pw-save">更新密碼</button>
          <div id="st-pw-status" style="font-size:14px;min-height:1.2em"></div>
        </div>
      </div>
      ${rag ? '' : `<div class="panel" style="display:flex;align-items:center;gap:12px;cursor:pointer" data-nav="creds">
        <div>
          <div style="font-size:17px;font-weight:600">憑證管理</div>
          <div style="margin-top:4px;font-size:14px;color:rgba(var(--ink-rgb),.55)">只保存目錄與加密值，不可回看</div>
        </div>
        <span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);font-size:18px">›</span>
      </div>`}
      <div class="panel">
        <div style="font-size:17px;font-weight:600;margin-bottom:12px">系統資訊</div>
        <div id="st-info"><div class="muted">載入中…</div></div>
      </div>
      <button class="btn3" id="st-logout" style="padding:14px;font-size:16px;border-radius:11px">登出</button>
    </div>
  </div>

  </main>
</div>

<!-- 手機底部導覽 -->
<div id="tabbar">
  ${rag ? '' : '<div class="tab" data-nav="cockpit">駕駛艙</div>'}
  <div class="tab" data-nav="search">搜尋</div>
  <div class="tab" data-nav="workflows">工作流</div>
  ${rag ? '' : '<div class="tab" data-nav="inbox">分流台</div>'}
  <div class="tab" data-nav="settings">設定</div>
</div>

<div id="toast"></div>

<script>
(function () {
  'use strict';
  // 台北時間 helper（lib/taipei-time.ts 注入，與駕駛艙 #36 台北日判定同一套）——
  // console 所有日期/時間顯示固定 Asia/Taipei，不隨看的裝置時區漂移
  ${TAIPEI_CLIENT_JS}
  var REGISTRY_BASE = ${JSON.stringify(registryBase)};
  var $ = function (id) { return document.getElementById(id); };

  // ── 狀態 ──
  var S = {
    token: localStorage.getItem('arcrun_console_session') || '',
    tenant: '',
    chip: '',              // entry_type filter（'' = 全部）
    semantic: false,
    cardId: '',
    graphCenter: '',
    wfCache: null,
    pins: (function () { try { return JSON.parse(localStorage.getItem('arcrun_console_pins') || '{}'); } catch (e) { return {}; } })()
  };

  function esc(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }
  function toast(msg) {
    var el = $('toast');
    el.textContent = msg;
    el.style.display = 'block';
    clearTimeout(toast._t);
    toast._t = setTimeout(function () { el.style.display = 'none'; }, 2000);
  }
  function apiHeaders() { return { 'X-Arcrun-API-Key': S.tenant }; }

  // fetch 失敗（斷網）的裸 TypeError 訊息（Failed to fetch / Load failed）→ 友善誠實文案
  function friendlyErr(e) {
    var m = e && e.message ? String(e.message) : String(e);
    if (/failed to fetch|load failed|networkerror|network request failed/i.test(m)) return '連線中斷——請檢查網路後重試';
    return m;
  }

  // ── 主題（深/淺）：預設淺色，選擇存 localStorage（head 預載腳本已先套用防閃色）──
  function getTheme() { return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; }
  function setTheme(t) {
    document.documentElement.setAttribute('data-theme', t);
    try { localStorage.setItem('arcrun_console_theme', t); } catch (e) { /* 私密模式存不了就只影響本次 */ }
    syncThemeUI();
  }
  function syncThemeUI() {
    var dark = getTheme() === 'dark';
    document.querySelectorAll('.themelabel').forEach(function (el) { el.textContent = dark ? '☀ 切淺色' : '☾ 切深色'; });
    var sw = $('st-theme-switch');
    if (sw) sw.classList.toggle('on', dark);
  }
  document.addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-themetoggle]');
    if (t) setTheme(getTheme() === 'dark' ? 'light' : 'dark');
  });
  syncThemeUI();

  // created_at 容錯：epoch 秒 / 毫秒 / ISO / 'YYYY-MM-DD HH:MM:SS'
  function parseAtMs(v) {
    if (v == null || v === '') return null;
    if (typeof v === 'number') return v < 1e12 ? v * 1000 : v;
    if (/^\\d+$/.test(v)) { var n = Number(v); return n < 1e12 ? n * 1000 : n; }
    var s = /T/.test(v) ? v : String(v).replace(' ', 'T') + 'Z';
    var ms = Date.parse(s);
    return isNaN(ms) ? null : ms;
  }
  // 顯示一律台北時間（bug 修正：原本用瀏覽器本地 getFullYear/getHours——顯示結果
  // 隨看的裝置時區漂移，且與後端「台北日」判定不同套；憑證頁 7/6 22:57（台北）建立的
  // credential 在非 +8 裝置上會顯示成另一天）
  function fmtDate(v) {
    var ms = parseAtMs(v);
    return ms == null ? '' : taipeiDateStr(ms);
  }
  function fmtDateTime(v) {
    var ms = parseAtMs(v);
    return ms == null ? '' : taipeiDateTimeStr(ms);
  }

  // ── 極簡 Markdown 渲染（先全 escape 再上格式，防 XSS）──
  function mdInline(s) {
    return s
      .replace(/\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>')
      .replace(/(^|[^\\\\])\\x60([^\\x60]+)\\x60/g, '$1<code>$2</code>');
  }
  function mdRender(src) {
    var lines = String(src || '').split(/\\r?\\n/);
    var out = [], inCode = false, listMode = '';
    function closeList() { if (listMode) { out.push(listMode === 'ul' ? '</ul>' : '</ol>'); listMode = ''; } }
    for (var i = 0; i < lines.length; i++) {
      var raw = lines[i];
      if (/^\\s*\\x60\\x60\\x60/.test(raw)) {
        closeList();
        if (inCode) { out.push('</code></pre>'); inCode = false; }
        else { out.push('<pre><code>'); inCode = true; }
        continue;
      }
      if (inCode) { out.push(esc(raw) + '\\n'); continue; }
      var line = esc(raw);
      var h = raw.match(/^(#{1,4})\\s+(.*)$/);
      if (h) { closeList(); out.push('<h' + h[1].length + '>' + mdInline(esc(h[2])) + '</h' + h[1].length + '>'); continue; }
      if (/^\\s*&gt;\\s?/.test(line)) { closeList(); out.push('<blockquote>' + mdInline(line.replace(/^\\s*&gt;\\s?/, '')) + '</blockquote>'); continue; }
      var li = raw.match(/^\\s*[-*+]\\s+(.*)$/);
      if (li) { if (listMode !== 'ul') { closeList(); out.push('<ul>'); listMode = 'ul'; } out.push('<li>' + mdInline(esc(li[1])) + '</li>'); continue; }
      var oli = raw.match(/^\\s*\\d+[.)]\\s+(.*)$/);
      if (oli) { if (listMode !== 'ol') { closeList(); out.push('<ol>'); listMode = 'ol'; } out.push('<li>' + mdInline(esc(oli[1])) + '</li>'); continue; }
      if (/^\\s*$/.test(raw)) { closeList(); continue; }
      closeList();
      out.push('<p>' + mdInline(line) + '</p>');
    }
    closeList();
    if (inCode) out.push('</code></pre>');
    return out.join('');
  }

  // ── 路由 ──
  // VIEWS / HOME 由 server 依 CONSOLE_PROFILE 注入（full＝全套 7 頁落地駕駛艙；rag＝4 頁落地搜尋）。
  // 不在 VIEWS 裡的 hash（含被 profile 裁掉的頁）一律導回 HOME——手打 #/cockpit 也進不去。
  var VIEWS = ${JSON.stringify(views)};
  var HOME = ${JSON.stringify(home)};
  function currentRoute() {
    var h = location.hash.replace(/^#\\/?/, '');
    var seg = h.split('/');
    return { raw: seg[0] || '', view: VIEWS.indexOf(seg[0]) >= 0 ? seg[0] : HOME, arg: seg.slice(1).join('/') };
  }
  function nav(view, arg) {
    location.hash = '#/' + view + (arg ? '/' + encodeURIComponent(arg) : '');
  }
  function showView(name) {
    VIEWS.forEach(function (v) { $('v-' + v).classList.toggle('on', v === name); });
    var activeKey = name === 'card' ? 'search' : name;
    document.querySelectorAll('#sidenav .nav, #tabbar .tab').forEach(function (el) {
      el.classList.toggle('on', el.getAttribute('data-nav') === activeKey);
    });
    window.scrollTo(0, 0);
  }
  var LOADERS = { cockpit: loadCockpit, search: initSearchOnce, card: loadCard, workflows: loadWorkflows, creds: loadCreds, inbox: loadTriage, settings: loadSettings };
  function route() {
    var r = currentRoute();
    // 隱藏頁/不存在頁 guard：hash 不在 VIEWS → 改寫成 HOME（hashchange 會再進來一次正常渲染）
    if (r.raw && r.raw !== r.view) { location.hash = '#/' + r.view; return; }
    if (r.view === 'card' && r.arg) S.cardId = decodeURIComponent(r.arg);
    showView(r.view);
    if (LOADERS[r.view]) LOADERS[r.view]();
  }
  window.addEventListener('hashchange', route);
  document.addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-nav]');
    if (t) nav(t.getAttribute('data-nav'));
  });

  // ── 認證 ──
  function showAuth(which) {
    $('v-login').classList.toggle('on', which === 'login');
    $('v-setup').classList.toggle('on', which === 'setup');
    $('shell').classList.remove('on');
    $('tabbar').classList.remove('on');
  }
  function showApp() {
    $('v-login').classList.remove('on');
    $('v-setup').classList.remove('on');
    $('shell').classList.add('on');
    $('tabbar').classList.add('on');
    $('side-foot').innerHTML = '租戶 ' + esc(S.tenant) + '<br>' + esc(location.host);
    if (!location.hash) location.hash = '#/' + HOME;
    route();
  }
  function init() {
    if (S.token) {
      fetch('/console/session', { headers: { Authorization: 'Bearer ' + S.token } })
        .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('session 失效')); })
        .then(function (d) { S.tenant = d.tenant || ''; showApp(); })
        .catch(function () {
          S.token = ''; localStorage.removeItem('arcrun_console_session');
          checkConfigured();
        });
    } else {
      checkConfigured();
    }
  }
  function checkConfigured() {
    fetch('/console/auth-status')
      .then(function (r) { return r.json(); })
      .then(function (d) { showAuth(d.configured ? 'login' : 'setup'); })
      .catch(function () { showAuth('login'); });
  }
  function handleAuthResponse(d) {
    S.token = d.session_token;
    localStorage.setItem('arcrun_console_session', S.token);
    S.tenant = d.tenant || '';
    showApp();
  }
  $('login-submit').addEventListener('click', function () {
    var email = $('login-email').value.trim(), password = $('login-password').value;
    $('login-status').textContent = '';
    fetch('/console/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: password }) })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) { if (!x.ok) { $('login-status').textContent = x.d.error || '登入失敗'; return; } handleAuthResponse(x.d); })
      .catch(function (e) { $('login-status').textContent = '請求失敗：' + friendlyErr(e); });
  });
  $('login-password').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') $('login-submit').click(); });
  $('setup-submit').addEventListener('click', function () {
    var email = $('setup-email').value.trim(), pw = $('setup-password').value, pw2 = $('setup-password2').value;
    if (pw !== pw2) { $('setup-status').textContent = '兩次密碼不一致'; return; }
    $('setup-status').textContent = '';
    fetch('/console/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: pw }) })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) { if (!x.ok) { $('setup-status').textContent = x.d.error || '設定失敗'; return; } handleAuthResponse(x.d); })
      .catch(function (e) { $('setup-status').textContent = '請求失敗：' + friendlyErr(e); });
  });

  // ── 駕駛艙 ──
  var LIGHT = {
    green: { ch: '安', title: '系統運轉中', grad: 'radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)', anim: 'breatheGreen' },
    yellow: { ch: '趕', title: '落後趕工中', grad: 'radial-gradient(circle at 36% 30%,#f2d194,#e8b45a 55%,#8a5f1e 100%)', anim: 'breatheAmber' },
    red: { ch: '滯', title: '卡住或斷訊', grad: 'radial-gradient(circle at 36% 30%,#f0a094,#d95f4c 55%,#7e2c20 100%)', anim: 'breatheRed' }
  };
  var TASK_ICONS = { done: '✓', doing: '◐', todo: '○', blocked: '●' };
  var CNUM = ['零','一','二','三','四','五','六','七','八','九','十'];
  function cnDay(n) { return n <= 10 ? CNUM[n] : (n < 20 ? '十' + (n % 10 ? CNUM[n % 10] : '') : CNUM[Math.floor(n / 10)] + '十' + (n % 10 ? CNUM[n % 10] : '')); }
  function taskLine(t) {
    var cls = TASK_ICONS[t.status] ? t.status : 'blocked';
    return '<li class="' + cls + '"><span class="ic">' + (TASK_ICONS[t.status] || '●') + '</span><span>' + esc(t.title) + '</span></li>';
  }
  function ckAge(m) {
    if (m == null || m < 0) return '時間不明';
    if (m < 60) return m + ' 分鐘前';
    if (m < 2880) return Math.round(m / 60) + ' 小時前';
    return Math.round(m / 1440) + ' 天前';
  }
  function loadCockpit() {
    var nowTpe = taipeiMonthDay(Date.now()); // 頁首日期＝台北日，不吃裝置時區
    $('ck-date').textContent = CNUM[nowTpe.month] + '月' + cnDay(nowTpe.day) + '日';
    fetch('/console/dashboard-data')
      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function (d) {
        var cfg = LIGHT[d.light] || LIGHT.green;
        var orb = $('ck-orb');
        orb.style.background = cfg.grad;
        orb.style.animation = cfg.anim + ' 3.4s ease-in-out infinite';
        $('ck-char').textContent = cfg.ch;
        $('ck-title').textContent = cfg.title;
        $('ck-sub').textContent = (d.last_beat
          ? d.last_beat.actor + '・' + d.last_beat.ago_minutes + ' 分鐘前' + (d.last_beat.note ? '・' + d.last_beat.note : '')
          : '尚無心跳資料') + (d.light !== 'green' && d.light_reason ? '（' + d.light_reason + '）' : '');
        var done = d.today_done || 0, total = d.today_total || 0;
        $('ck-done').textContent = done; $('ck-total').textContent = total;
        $('ck-bar').style.width = (total ? Math.round(done / total * 100) : 0) + '%';
        $('ck-inbox').textContent = d.inbox_new || 0;
        // 等你的事：來源與維護時間攤開講（fix/console-dashboard-live-data）——
        // Gitea sprint 等leo清單是活資料源；fallback dash_wait 一定標 stale，不假裝是今天的
        var wb = $('ck-waitbox'), body = $('ck-wait'), wmeta = $('ck-waitmeta');
        var wm = d.waiting_meta || {};
        if (d.waiting && d.waiting.length) {
          wb.classList.add('has'); body.className = '';
          body.innerHTML = d.waiting.map(function (w) {
            return '<div class="wait-item"><span class="dm">' + (w.urgency ? esc(w.urgency) : '◆') + '</span><span>' +
              (w.id ? '<b>#' + esc(w.id) + '</b>　' : '') + esc(w.title) + '</span></div>';
          }).join('');
        } else {
          wb.classList.remove('has'); body.className = 'wait-none';
          body.textContent = wm.source === 'none' ? '（管線未接）' : '無，你不用做任何事';
        }
        if (wm.source === 'gitea_sprint') {
          wmeta.innerHTML = '來源：sprint 等leo清單・維護於 ' + ckAge(wm.updated_ago_minutes) +
            (wm.stale ? '<br><span class="err">⚠ 清單超過 2 天沒維護，可能過時</span>' : '');
        } else if (wm.source === 'kbdb_dash_wait') {
          wmeta.innerHTML = '<span class="err">⚠ ' + esc(wm.note || 'dash_wait 殘資料') + '・上次寫入 ' + ckAge(wm.updated_ago_minutes) + '，可能過時</span>';
        } else if (wm.source === 'none') {
          wmeta.innerHTML = '<span class="err">管線未接：Gitea sprint 清單與 dash_wait 皆無資料</span>';
        } else {
          wmeta.textContent = '';
        }
        // 今日路線：sprint 任務板優先（來源與更新時間攤開講）；dash_task fallback 沿舊誠實降級
        var rm = d.route_meta || {};
        var today = (d.tasks || []).filter(function (t) { return t.scope === 'today'; });
        var week = (d.tasks || []).filter(function (t) { return t.scope === 'week'; });
        if (rm.source === 'gitea_sprint_board') {
          $('ck-route-src').textContent = '來源 sprint 任務板・更新於 ' + ckAge(rm.updated_ago_minutes);
          var staleHead = rm.is_today ? '' :
            '<li class="todo"><span class="ic">○</span><span class="muted">⚠ 今日任務板未更新（最後 ' + ckAge(rm.updated_ago_minutes) + '）——以下是板上現況</span></li>';
          $('ck-today').innerHTML = staleHead + (today.length
            ? today.map(taskLine).join('')
            : '<li class="todo"><span class="ic">○</span><span class="muted">任務板上沒有可解析的事項</span></li>');
        } else if (rm.is_today || !today.length) {
          $('ck-route-src').textContent = '來源 dash_task';
          $('ck-today').innerHTML = today.length ? today.map(taskLine).join('') : '<li class="todo"><span class="ic">○</span><span class="muted">今日無排定項目——dash_task 今天沒有寫入</span></li>';
        } else {
          $('ck-route-src').textContent = '來源 dash_task';
          $('ck-today').innerHTML = '<li class="todo"><span class="ic">○</span><span class="muted">今日尚無路線寫入——以下是 ' + ckAge(rm.updated_ago_minutes) + '的殘留路線</span></li>' + today.map(taskLine).join('');
        }
        $('ck-weekhead').style.display = week.length ? '' : 'none';
        $('ck-week').innerHTML = week.map(taskLine).join('');
        $('ck-stamp').textContent = '每 60 秒自動刷新・上次 ' + taipeiTimeStr(Date.parse(d.generated_at)) + '（台北）';
      })
      .catch(function (e) {
        $('ck-char').textContent = '？';
        $('ck-title').textContent = '讀不到狀態';
        $('ck-sub').innerHTML = '<span class="err">' + esc(friendlyErr(e)) + '・每 60 秒自動重試</span>';
      });
  }
  // 60 秒定時器無條件常駐：斷網期間照跑，網路恢復後下一輪自動刷新回正常畫面
  setInterval(function () { if (currentRoute().view === 'cockpit' && $('shell').classList.contains('on')) loadCockpit(); }, 60000);

  // ── 總庫搜尋 ──
  // chips = KBDB 真能篩的 entry_type（誠實：不編造「電腦筆記/手機筆記」等 KBDB 沒有的分類軸）
  var CHIPS = [
    { label: '全部', v: '' },
    { label: '筆記 note', v: 'note' },
    { label: '區塊 block', v: 'block' },
    { label: '數值 value', v: 'value' },
    { label: '工作流 workflow', v: 'workflow' }
  ];
  var searchInited = false;
  function initSearchOnce() {
    if (searchInited) { return; }
    searchInited = true;
    renderChips();
    loadScale();
  }
  function renderChips() {
    $('se-chips').innerHTML = CHIPS.map(function (c, i) {
      return '<button class="chip' + (S.chip === c.v ? ' on' : '') + '" data-chip="' + i + '">' + esc(c.label) + '</button>';
    }).join('');
  }
  $('se-chips').addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-chip]');
    if (!t) return;
    S.chip = CHIPS[Number(t.getAttribute('data-chip'))].v;
    renderChips();
    if ($('se-q').value.trim()) doSearch();
  });
  // 頭部統計＝精耕層 live 數字（leo 2026-07-07 裁：45.8 萬 14-E 搬遷遺產已 deprecated、
  // 之後要刪——不再拿遺產總數撐場面，只顯示真的新的）。搜尋本身仍搜全庫，資料不藏。
  function loadScale() {
    fetch('/console/kb-scale-data')
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (d) {
        if (!d) return;
        var n = function (v) { return v == null ? '？' : v.toLocaleString(); };
        var parts = ['wiki 卡 ' + n(d.wiki_card_total), '三元組 ' + n(d.triplets_total), '已嵌入 ' + n(d.embedded)];
        var latest = d.wiki_card_latest_ago_minutes;
        $('se-scale').textContent = '精耕層 ' + parts.join('・') +
          (latest != null && latest >= 0 ? '・最近寫入 ' + ckAge(latest) : '');
      })
      .catch(function () { /* 規模感拿不到不擋搜尋 */ });
  }
  $('se-sem').addEventListener('click', function () {
    S.semantic = !S.semantic;
    $('se-sem').classList.toggle('on', S.semantic);
    if ($('se-q').value.trim()) doSearch();
  });
  $('se-q').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doSearch(); });
  function entryTitle(e) {
    if (e.page_name) return e.page_name;
    var first = String(e.content || '').split(/\\r?\\n/).find(function (l) { return l.trim(); }) || '';
    first = first.replace(/^#+\\s*/, '').replace(/^[-*>]\\s*/, '').trim();
    if (first.length > 60) first = first.slice(0, 60) + '…';
    return first || '（無標題・' + (e.entry_type || 'entry') + '）';
  }
  function entrySnippet(e) {
    var lines = String(e.content || '').split(/\\r?\\n/).filter(function (l) { return l.trim(); });
    var body = lines.slice(1).join(' ').replace(/\\s+/g, ' ').trim();
    if (!body) body = lines.join(' ');
    if (body.length > 140) body = body.slice(0, 140) + '…';
    return body;
  }
  function doSearch() {
    var q = $('se-q').value.trim();
    if (!q) { $('se-count').textContent = '請輸入查詢字'; return; }
    $('se-banner').innerHTML = '';
    $('se-count').textContent = '查詢中…';
    $('se-results').innerHTML = '';
    var url = '/kbdb/search?q=' + encodeURIComponent(q) + '&mode=' + (S.semantic ? 'semantic' : 'keyword');
    if (S.chip) url += '&entry_type=' + encodeURIComponent(S.chip);
    fetch(url, { headers: apiHeaders() })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
      .then(function (x) {
        if (!x.ok) { $('se-count').innerHTML = '<span class="err">' + esc(x.d.error || ('查詢失敗（HTTP ' + x.status + '）')) + '</span>'; return; }
        var d = x.d;
        if (S.semantic && d.mode === 'keyword') {
          $('se-banner').innerHTML = '<div class="honest" style="margin-top:18px"><div class="h">語意搜尋尚未啟用</div><div class="b">語意搜尋用「意思」找資料，不是字面比對。<br>' + esc(d.capability_hint || '部署端尚未開啟 Vectorize——不會假裝有語意結果，以下是關鍵字結果。') + '</div></div>';
        }
        var entries = d.entries || [];
        $('se-count').textContent = '命中 ' + entries.length + ' 筆・模式 ' + (d.mode || 'keyword') + '・搜尋範圍＝全庫（含 14-E 遺產）';
        if (!entries.length) {
          $('se-results').innerHTML = '<div class="muted" style="padding:30px 10px;text-align:center;grid-column:1/-1">找不到「' + esc(q) + '」——換個關鍵字試試。</div>';
          return;
        }
        $('se-results').innerHTML = entries.map(function (e) {
          // 精耕層（wiki_card）綠標突顯；其餘照 entry_type 原樣（不誤標——block/value/note
          // 大宗是 14-E 遺產，但 entry_type 本身不足以斷言，僅精耕層正向標記）
          var typeTag = e.entry_type === 'wiki_card'
            ? '<span class="tag green">wiki_card・精耕</span>'
            : '<span class="tag">' + esc(e.entry_type || 'entry') + '</span>';
          return '<div class="kcard" data-card="' + esc(e.id) + '">' +
            '<div class="kt">' + esc(entryTitle(e)) + '</div>' +
            '<div class="ks">' + esc(entrySnippet(e)) + '</div>' +
            '<div class="km">' + typeTag +
            (e.source ? '<span class="dim">' + esc(e.source) + '</span>' : '') +
            '<span class="dim mono" style="margin-left:auto">' + esc(fmtDate(e.created_at)) + '</span></div></div>';
        }).join('');
      })
      .catch(function (e) { $('se-count').innerHTML = '<span class="err">請求失敗：' + esc(friendlyErr(e)) + '</span>'; });
  }
  $('se-results').addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-card]');
    if (t) nav('card', t.getAttribute('data-card'));
  });

  // ── 卡片詳頁 + 關聯 ──
  function loadCard() {
    if (!S.cardId) { $('cd-main').innerHTML = '<div class="muted" style="padding:24px 0">沒有指定卡片——從「總庫搜尋」點一張進來。</div>'; renderGraphEmpty('尚未載入卡片'); return; }
    $('cd-main').innerHTML = '<div class="muted" style="padding:24px 0">載入中…</div>';
    fetch('/kbdb/entries/' + encodeURIComponent(S.cardId), { headers: apiHeaders() })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        if (!x.ok || !x.d.entry) { $('cd-main').innerHTML = '<div class="err" style="padding:24px 0">' + esc(x.d.error || '讀不到這張卡片') + '</div>'; renderGraphEmpty('讀不到卡片'); return; }
        var e = x.d.entry;
        var title = entryTitle(e);
        var html = '<div class="cardtitle">' + esc(title) + '</div>' +
          '<div style="display:flex;gap:10px;align-items:center;margin-top:12px;font-size:13.5px;flex-wrap:wrap">' +
          '<span class="tag">' + esc(e.entry_type || 'entry') + '</span>' +
          (e.page_name ? '<span class="tag green">' + esc(e.page_name) + '</span>' : '') +
          '<span class="muted mono">' + esc(fmtDateTime(e.created_at)) + '</span></div>' +
          '<div class="md">' + mdRender(e.content || '（此條目沒有內文）') + '</div>' +
          (e.source
            ? '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span><span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(e.source) + '</span></div>'
            : '');
        $('cd-main').innerHTML = html;
        var nodeName = e.page_name || title;
        S.graphCenter = nodeName;
        loadNeighbors(nodeName);
      })
      .catch(function (er) { $('cd-main').innerHTML = '<div class="err" style="padding:24px 0">請求失敗：' + esc(friendlyErr(er)) + '</div>'; renderGraphEmpty('請求失敗'); });
  }
  function renderGraphEmpty(msg) {
    $('cd-graph').innerHTML = '<div class="graphbox" style="display:grid;place-items:center;height:160px"><span class="muted" style="font-size:14px;padding:0 20px;text-align:center">' + esc(msg) + '</span></div>';
    $('cd-neighbors').innerHTML = '';
  }
  function loadNeighbors(name) {
    renderGraphEmpty('查詢關聯中…');
    fetch('/kbdb/graph/neighbors/' + encodeURIComponent(name), { headers: apiHeaders() })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
      .then(function (x) {
        if (!x.ok) { renderGraphEmpty(x.d.error || ('關聯查詢失敗（HTTP ' + x.status + '）')); return; }
        var neighbors = x.d.neighbors || [];
        var edges = x.d.edges || [];
        if (!neighbors.length) { renderGraphEmpty('尚無關聯資料——這張卡還沒有 triplet 連進知識圖譜。'); return; }
        renderGraph(name, neighbors.slice(0, 10));
        $('cd-neighbors').innerHTML = edges.slice(0, 12).map(function (t) {
          var other = t.subject === name ? t.object : t.subject;
          return '<div class="nbrow" data-gnode="' + esc(other) + '">' +
            '<span class="tag green" style="flex:none">' + esc(t.predicate || '關聯') + '</span>' +
            '<span style="word-break:break-word">' + esc(other) + '</span>' +
            '<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">›</span></div>';
        }).join('');
      })
      .catch(function (e) { renderGraphEmpty('關聯服務不可達：' + friendlyErr(e)); });
  }
  function renderGraph(center, nodes) {
    var n = nodes.length || 1;
    var lines = '', chips = '';
    for (var i = 0; i < n; i++) {
      var a = (-90 + i * (360 / n)) * Math.PI / 180;
      var x = 50 + 39 * Math.cos(a), y = 50 + 37 * Math.sin(a);
      lines += '<line x1="50" y1="50" x2="' + x.toFixed(2) + '" y2="' + y.toFixed(2) + '" stroke="rgba(var(--ok-rgb),.3)" stroke-width="1" vector-effect="non-scaling-stroke" stroke-dasharray="4 4"/>';
      chips += '<div class="gnode" style="left:' + x.toFixed(2) + '%;top:' + y.toFixed(2) + '%" data-gnode="' + esc(nodes[i]) + '">' + esc(nodes[i]) + '</div>';
    }
    var shortCenter = center.length > 16 ? center.slice(0, 16) + '…' : center;
    $('cd-graph').innerHTML = '<div class="graphbox">' +
      '<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute;inset:0;width:100%;height:100%">' + lines + '</svg>' +
      '<div class="gcenter"><span>' + esc(shortCenter) + '</span></div>' + chips +
      '<div style="position:absolute;right:12px;bottom:10px;font-size:11.5px;color:rgba(var(--ink-rgb),.35)">╌ 知識圖譜鄰居（點節點展開）</div></div>';
  }
  document.addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-gnode]');
    if (!t) return;
    var name = t.getAttribute('data-gnode');
    S.graphCenter = name;
    loadNeighbors(name);
  });

  // ── 工作流 ──
  function loadWorkflows() {
    fetch('/webhooks/named', { headers: apiHeaders() })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        if (!x.ok) { $('wf-list').innerHTML = '<div class="err">' + esc(x.d.error || '讀取失敗') + '</div>'; return; }
        S.wfCache = x.d.workflows || [];
        $('wf-count').textContent = S.wfCache.length + ' 條自動化';
        renderWorkflows();
      })
      .catch(function (e) { $('wf-list').innerHTML = '<div class="err">請求失敗：' + esc(friendlyErr(e)) + '</div>'; });
  }
  $('wf-q').addEventListener('input', function () { renderWorkflows(); });
  function renderWorkflows(openName) {
    var list = S.wfCache || [];
    var q = $('wf-q').value.trim().toLowerCase();
    if (q) list = list.filter(function (w) { return (w.name + ' ' + (w.description || '')).toLowerCase().indexOf(q) >= 0; });
    list = list.slice().sort(function (a, b) { return (S.pins[b.name] ? 1 : 0) - (S.pins[a.name] ? 1 : 0); });
    if (!list.length) {
      $('wf-list').innerHTML = '<div class="honest"><div class="h">' + (q ? '沒有符合的工作流' : '還沒有任何工作流') + '</div><div class="b">' + (q ? '換個關鍵字試試。' : '用 acr push 部署第一條 workflow 後，這裡就會出現。') + '</div></div>';
      return;
    }
    $('wf-list').innerHTML = list.map(function (w) {
      var pinned = !!S.pins[w.name];
      var open = w.name === openName;
      return '<div class="card-item' + (pinned ? ' hl' : '') + '" data-wf="' + esc(w.name) + '">' +
        '<div class="rowhead" data-wftoggle="' + esc(w.name) + '">' +
        '<button class="pinbtn' + (pinned ? ' on' : '') + '" data-wfpin="' + esc(w.name) + '" title="釘選">' + (pinned ? '★' : '☆') + '</button>' +
        '<div style="min-width:0"><div style="font-size:17px;font-weight:600;line-height:1.4;word-break:break-word">' + esc(w.name) + '</div>' +
        '<div style="margin-top:3px;font-size:13.5px;color:rgba(var(--ink-rgb),.5)">' + esc(w.cron_expr ? '排程 ' + w.cron_expr : '事件觸發') + (w.created_at ? '・建立 ' + esc(fmtDate(w.created_at)) : '') + '</div></div>' +
        '<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">' + (open ? '▾' : '›') + '</span></div>' +
        (open ? renderWfBody(w) : '') + '</div>';
    }).join('');
    if (openName) fetchWfRuns(openName);
  }
  function renderWfBody(w) {
    return '<div class="rowbody">' +
      '<div style="margin-top:14px;font-size:15.5px;line-height:1.7;color:rgba(var(--ink-rgb),.7)">' + (w.description ? esc(w.description) : '<span class="dim">（未填描述——re-deploy 時補一句「能做什麼」它才搜得到）</span>') + '</div>' +
      '<div class="urlbox" style="margin-top:14px"><span style="font-size:12.5px;letter-spacing:.1em;color:rgba(var(--ink-rgb),.45);flex:none">觸發網址</span>' +
      '<span class="u">' + esc(w.webhook_url || '') + '</span>' +
      '<button class="btn2" style="margin-left:auto;flex:none;padding:7px 13px;font-size:13.5px" data-wfcopy="' + esc(w.webhook_url || '') + '">複製</button></div>' +
      '<div style="margin-top:14px;font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5)">最近執行</div>' +
      '<div id="wf-runs" style="margin-top:8px;display:flex;flex-direction:column;gap:6px"><span class="dim" style="font-size:14px">載入中…</span></div>' +
      '<button class="btn" style="margin-top:16px;width:100%" data-wfrun="' + esc(w.name) + '">手動觸發</button>' +
      '<div id="wf-runmsg" style="margin-top:8px;font-size:14px;min-height:1.2em"></div></div>';
  }
  var wfOpen = '';
  $('wf-list').addEventListener('click', function (ev) {
    var pin = ev.target.closest('[data-wfpin]');
    if (pin) {
      var name = pin.getAttribute('data-wfpin');
      if (S.pins[name]) delete S.pins[name]; else S.pins[name] = true;
      localStorage.setItem('arcrun_console_pins', JSON.stringify(S.pins));
      renderWorkflows(wfOpen);
      ev.stopPropagation();
      return;
    }
    var copy = ev.target.closest('[data-wfcopy]');
    if (copy) {
      var url = copy.getAttribute('data-wfcopy');
      (navigator.clipboard ? navigator.clipboard.writeText(url) : Promise.reject()).then(
        function () { toast('已複製觸發網址'); },
        function () { toast('複製失敗——手動選取網址'); }
      );
      ev.stopPropagation();
      return;
    }
    var run = ev.target.closest('[data-wfrun]');
    if (run) { triggerWf(run.getAttribute('data-wfrun'), run); ev.stopPropagation(); return; }
    var tog = ev.target.closest('[data-wftoggle]');
    if (tog) {
      var n = tog.getAttribute('data-wftoggle');
      wfOpen = wfOpen === n ? '' : n;
      renderWorkflows(wfOpen);
    }
  });
  function fetchWfRuns(name) {
    var box = document.getElementById('wf-runs');
    if (!box) return;
    fetch('/workflows/' + encodeURIComponent(name) + '/executions?limit=3', { headers: apiHeaders() })
      .then(function (r) { return r.json(); })
      .then(function (d) {
        var runs = (d.data && d.data.executions) || [];
        if (!runs.length) { box.innerHTML = '<span class="dim" style="font-size:14px">尚無執行紀錄</span>'; return; }
        box.innerHTML = runs.map(function (r) {
          var okRun = r.verdict === 'success';
          return '<div style="display:flex;gap:10px;align-items:baseline;font-size:14.5px">' +
            '<span style="flex:none;font-size:10px;color:' + (okRun ? 'var(--ok)' : 'var(--err)') + '">●</span>' +
            '<span style="color:rgba(var(--ink-rgb),.7)">' + (okRun ? '成功' : '失敗') + (typeof r.duration_ms === 'number' ? '・' + r.duration_ms + 'ms' : '') + (r.message && !okRun ? '・' + esc(String(r.message).slice(0, 60)) : '') + '</span>' +
            '<span class="dim mono" style="margin-left:auto;font-size:12.5px">' + esc(r.recorded_at ? fmtDateTime(r.recorded_at) : (r.timestamp ? fmtDateTime(Number(r.timestamp)) : '')) + '</span></div>';
        }).join('');
      })
      .catch(function () { box.innerHTML = '<span class="dim" style="font-size:14px">執行紀錄讀取失敗</span>'; });
  }
  function triggerWf(name, btn) {
    btn.disabled = true; btn.textContent = '執行中…';
    var msg = document.getElementById('wf-runmsg');
    fetch('/webhooks/named/' + encodeURIComponent(name) + '/trigger', {
      method: 'POST',
      headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
      body: '{}'
    })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        btn.disabled = false; btn.textContent = '手動觸發';
        if (x.ok && x.d.success !== false) {
          if (msg) msg.innerHTML = '<span class="ok">✓ 執行成功' + (typeof x.d.duration_ms === 'number' ? '・' + x.d.duration_ms + 'ms' : '') + '</span>';
          toast('「' + name + '」執行完成');
        } else {
          if (msg) msg.innerHTML = '<span class="err">✕ ' + esc(x.d.error || '執行失敗') + '</span>';
        }
        fetchWfRuns(name);
      })
      .catch(function (e) {
        btn.disabled = false; btn.textContent = '手動觸發';
        if (msg) msg.innerHTML = '<span class="err">請求失敗：' + esc(friendlyErr(e)) + '</span>';
      });
  }

  // ── 零件 / recipes（v0 搜尋台保留區）──
  $('cr-go').addEventListener('click', doCompSearch);
  $('cr-q').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doCompSearch(); });
  function doCompSearch() {
    var q = $('cr-q').value.trim();
    if (!q) return;
    var out = $('cr-out');
    out.innerHTML = '<div class="muted">查詢中…</div>';
    Promise.allSettled([
      fetch(REGISTRY_BASE + '/components/search?q=' + encodeURIComponent(q)).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }),
      fetch('/public-recipes?q=' + encodeURIComponent(q)).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
    ]).then(function (rs) {
      var html = '';
      var comp = rs[0];
      html += '<div class="dim" style="font-size:12.5px;letter-spacing:.1em;margin:10px 0 6px">COMPONENTS</div>';
      if (comp.status === 'fulfilled' && comp.value.ok) {
        var results = (comp.value.d.data && comp.value.d.data.results) || [];
        html += results.length
          ? results.map(function (c) { return '<div class="card-item" style="padding:10px 14px;margin-bottom:6px;font-size:14px"><span class="tag">' + esc(c.canonical_id || c.id) + '</span> <span class="muted">' + esc(c.description || c.name || '') + '</span></div>'; }).join('')
          : '<div class="muted" style="font-size:14px">沒查到符合的 component</div>';
      } else {
        html += '<div class="err" style="font-size:14px">components 查詢失敗（零件庫不可達）</div>';
      }
      var rec = rs[1];
      html += '<div class="dim" style="font-size:12.5px;letter-spacing:.1em;margin:12px 0 6px">RECIPES</div>';
      if (rec.status === 'fulfilled' && rec.value.ok) {
        var d = rec.value.d;
        if (d.found === false) {
          html += '<div class="muted" style="font-size:14px">' + esc(d.hint || '沒查到符合的 recipe') + '</div>';
        } else {
          var recipes = d.recipes || [];
          html += recipes.length
            ? recipes.map(function (r) { return '<div class="card-item" style="padding:10px 14px;margin-bottom:6px;font-size:14px"><span class="tag">' + esc(r.canonical_id) + '</span> <span class="muted">' + esc(r.description || r.display_name || '') + '</span></div>'; }).join('')
            : '<div class="muted" style="font-size:14px">沒查到符合的 recipe</div>';
        }
      } else {
        html += '<div class="err" style="font-size:14px">recipes 查詢失敗</div>';
      }
      out.innerHTML = html;
    });
  }

  // ── 憑證管理 ──
  $('cred-addbtn').addEventListener('click', function () {
    var f = $('cred-form');
    f.style.display = f.style.display === 'none' ? '' : 'none';
  });
  $('cred-save').addEventListener('click', function () {
    var name = $('cred-name').value.trim(), service = $('cred-service').value.trim(), value = $('cred-secret').value;
    var st = $('cred-form-status');
    if (!name || !value) { st.innerHTML = '<span class="err">名稱與密文值必填</span>'; return; }
    if (!/^\\w+$/.test(name)) { st.innerHTML = '<span class="err">名稱只能包含英文字母、數字和底線</span>'; return; }
    st.textContent = '保存中…';
    fetch('/credentials', {
      method: 'POST',
      headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
      body: JSON.stringify({ name: name, value: value, service: service || undefined })
    })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        if (!x.ok || x.d.success === false) { st.innerHTML = '<span class="err">' + esc(x.d.error || '保存失敗') + '</span>'; return; }
        st.textContent = '';
        $('cred-name').value = ''; $('cred-service').value = ''; $('cred-secret').value = '';
        $('cred-form').style.display = 'none';
        toast('已加密保存');
        loadCreds();
      })
      .catch(function (e) { st.innerHTML = '<span class="err">請求失敗：' + esc(friendlyErr(e)) + '</span>'; });
  });
  var credOpen = '';
  var credCache = [];
  function loadCreds() {
    fetch('/credentials/catalog', { headers: apiHeaders() })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        if (!x.ok || x.d.success === false) {
          $('cred-list').innerHTML = '<div class="honest"><div class="h">目錄讀取失敗</div><div class="b">' + esc(x.d.error || '未知錯誤') + '</div></div>';
          return;
        }
        credCache = x.d.credentials || [];
        renderCreds();
      })
      .catch(function (e) { $('cred-list').innerHTML = '<div class="err">請求失敗：' + esc(friendlyErr(e)) + '</div>'; });
  }
  function renderCreds() {
    if (!credCache.length) {
      $('cred-list').innerHTML = '<div class="honest"><div class="h">還沒有任何憑證</div><div class="b">按「＋ 新增憑證」加入第一筆。目錄只存名稱與服務，密文進 Workers Secrets、不可回看。</div></div>';
      return;
    }
    $('cred-list').innerHTML = credCache.map(function (c) {
      var open = credOpen === c.name;
      var initial = (c.service || c.name || '?').slice(0, 1).toUpperCase();
      return '<div class="card-item' + (open ? ' hl' : '') + '">' +
        '<div class="rowhead" data-credtoggle="' + esc(c.name) + '">' +
        '<span class="avatar">' + esc(initial) + '</span>' +
        '<div style="min-width:0"><div style="font-size:16.5px;font-weight:600;word-break:break-all">' + esc(c.name) + '</div>' +
        '<div style="margin-top:2px;font-size:13.5px;color:rgba(var(--ink-rgb),.5)">' + esc(c.service || '未填服務') + '・建立於 ' + esc(fmtDateTime(c.created_at) || '—') + '（台北）</div></div>' +
        '<div style="margin-left:auto;text-align:right;font-size:12.5px;color:rgba(var(--ink-rgb),.4);flex:none">最後使用<br>' + esc(c.last_used_at ? fmtDateTime(c.last_used_at) : '尚未記錄') + '</div></div>' +
        (open
          ? '<div class="rowbody">' +
            '<div class="maskline" style="margin-top:14px"><span class="mono" style="font-size:16px;letter-spacing:.3em;color:rgba(var(--ink-rgb),.4)">••••••••••••••••</span><span style="margin-left:auto;font-size:12.5px;color:rgba(var(--ink-rgb),.4)">已加密・不可回看</span></div>' +
            '<textarea class="txt" id="cred-newval" placeholder="貼上新值以整筆替換…" rows="2" style="margin-top:10px"></textarea>' +
            '<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">' +
            '<button class="btn" style="flex:1;min-width:140px" data-credreplace="' + esc(c.name) + '">整筆替換</button>' +
            '<button class="btn-danger" disabled title="刪除端點尚未接上新目錄（T9 範圍）">刪除・即將開通</button></div>' +
            '<div id="cred-msg" style="margin-top:8px;font-size:14px;min-height:1.2em"></div></div>'
          : '') +
        '</div>';
    }).join('');
  }
  $('cred-list').addEventListener('click', function (ev) {
    var rep = ev.target.closest('[data-credreplace]');
    if (rep) {
      var name = rep.getAttribute('data-credreplace');
      var ta = document.getElementById('cred-newval');
      var msg = document.getElementById('cred-msg');
      var val = ta ? ta.value : '';
      if (!val.trim()) { if (msg) msg.innerHTML = '<span class="err">先貼上新值</span>'; return; }
      rep.disabled = true;
      var cur = credCache.find(function (c) { return c.name === name; });
      fetch('/credentials/' + encodeURIComponent(name), {
        method: 'PUT',
        headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
        body: JSON.stringify({ value: val, service: cur && cur.service ? cur.service : undefined })
      })
        .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
        .then(function (x) {
          rep.disabled = false;
          if (!x.ok || x.d.success === false) { if (msg) msg.innerHTML = '<span class="err">' + esc(x.d.error || '替換失敗') + '</span>'; return; }
          credOpen = '';
          toast('「' + name + '」已整筆替換');
          loadCreds();
        })
        .catch(function (e) { rep.disabled = false; if (msg) msg.innerHTML = '<span class="err">請求失敗：' + esc(friendlyErr(e)) + '</span>'; });
      return;
    }
    var tog = ev.target.closest('[data-credtoggle]');
    if (tog) {
      var n = tog.getAttribute('data-credtoggle');
      credOpen = credOpen === n ? '' : n;
      renderCreds();
    }
  });

  // ── 分流台（原收件匣，Arcrun#9：todo＋inbox 二源、三欄 80/15/5、per-project chips）──
  // 頁面只渲染 /console/triage-data 的結果（分欄/計數判定在後端純函式）；chips 切換與
  // 「顯示已完成」是同一包資料的前端過濾，零額外請求（leo 手機看，省來回）。
  var TG = { data: null, project: '', showDone: false };
  var TG_TIERS = [
    { key: 'ai', nm: 'AI 會幫我搞定', pc: '80%', empty: '這欄空的——還沒有分給 AI 的待辦' },
    { key: 'collab', nm: '協作・AI 做完你接手', pc: '15%', empty: '這欄空的——沒有要你配合的事' },
    { key: 'leo', nm: '非我不可', pc: '5%', empty: '這欄空的——沒有非你不可的事' }
  ];
  function triItem(m) {
    var done = m.status === 'done';
    var meta = '';
    if (m.marker) meta += '<span class="tag dim">' + esc(m.marker) + '</span>';
    meta += '<span class="tag">' + esc(m.project || '未分項') + '</span>';
    if (m.source) meta += '<span>' + esc(m.source) + '</span>';
    if (m.unclassified && m.origin === 'todo') meta += '<span class="tag red">未分流</span>';
    // 誤勾救濟：done 清單裡給「還原」（status 回 new + 移除 checked_via/checked_at，端點同一個）
    if (done) meta += '<button class="tri-restore" data-trirestore="' + esc(m.id) + '">還原</button>';
    meta += '<span class="mono" style="margin-left:auto">' + esc(fmtDateTime(m.at)) + '</span>';
    // 勾掉＝雙向銷案的 console 端終局（checked_via:console，萃取端絕不復活）；
    // 後端回寫成功才改本地狀態移出三欄（done 預設隱藏），不樂觀假裝
    var body = done
      ? '<div class="tx">' + esc(m.text) + '</div>'
      : '<div class="row"><div class="tx">' + esc(m.text) + '</div><button class="tri-check" data-tricheck="' + esc(m.id) + '" title="勾掉（銷案）">✓</button></div>';
    return '<div class="tri-item ' + (done ? 'done' : 'new') + '">' + body + '<div class="mt">' + meta + '</div></div>';
  }
  function triChip(label, val, on) {
    return '<button class="chip' + (on ? ' on' : '') + '" data-triproj="' + esc(val) + '">' + esc(label) + '</button>';
  }
  function renderTriage() {
    var d = TG.data || {};
    var items = d.items || [];
    var out = $('tg-out');
    // per-project chips：全部＋各 project＋（有未分項才出）未分項
    var chips = triChip('全部', '', TG.project === '');
    (d.projects || []).forEach(function (p) { chips += triChip(p, p, TG.project === p); });
    if (items.some(function (i) { return !i.project; })) chips += triChip('未分項', '__none__', TG.project === '__none__');
    $('tg-chips').innerHTML = items.length ? chips : '';
    if (!items.length) {
      out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">分流台是空的</div><div class="b">資料源＝KBDB todo entries（Logseq 萃取，ingest 灌入中）＋ inbox entries（Telegram）。<br>寫入方尚未灌資料時這裡誠實顯示空。</div></div>';
      return;
    }
    var vis = items.filter(function (i) {
      return TG.project === '' || (TG.project === '__none__' ? !i.project : i.project === TG.project);
    });
    var doneN = vis.filter(function (i) { return i.status === 'done'; }).length;
    var html = '';
    TG_TIERS.forEach(function (t) {
      var news = vis.filter(function (i) { return i.tier === t.key && i.status !== 'done'; });
      var dones = TG.showDone ? vis.filter(function (i) { return i.tier === t.key && i.status === 'done'; }) : [];
      html += '<div class="tri-sec ' + t.key + '"><div class="tri-sechead"><span class="dot"></span><span class="nm serif">' + t.nm + '</span><span class="pc">' + t.pc + '</span><span class="ct">' + news.length + ' 件</span></div>';
      if (!news.length && !dones.length) html += '<div class="muted" style="font-size:14px;padding:10px 2px 0">' + t.empty + '</div>';
      html += news.map(triItem).join('') + dones.map(triItem).join('');
      html += '</div>';
    });
    html += '<div style="margin:24px 0 20px;text-align:center"><button class="btn3" id="tg-donetoggle" style="padding:9px 18px;font-size:13.5px;border-radius:999px">' +
      (TG.showDone ? '隱藏已完成' : '顯示已完成（' + doneN + '）') + '</button></div>';
    out.innerHTML = html;
    $('tg-donetoggle').addEventListener('click', function () { TG.showDone = !TG.showDone; renderTriage(); });
  }
  $('tg-chips').addEventListener('click', function (ev) {
    var t = ev.target.closest('[data-triproj]');
    if (!t) return;
    TG.project = t.getAttribute('data-triproj');
    renderTriage();
  });
  // 勾掉/還原：POST /console/triage-check（console session 保護的小端點——瀏覽器沒有 KBDB
  // token，PATCH 由 server 端做，先 GET 原 entry 整串回寫防蓋掉別的欄位）。成功才動本地
  // 狀態重繪（該筆立即移出/回到三欄），失敗誠實 toast、按鈕解鎖可重試。
  function triageAct(id, action, btn) {
    btn.disabled = true;
    fetch('/console/triage-check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + S.token },
      body: JSON.stringify({ entry_id: id, action: action })
    })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
      .then(function (x) {
        if (!x.ok) { btn.disabled = false; toast(x.d.error || ('失敗（HTTP ' + x.status + '）')); return; }
        var items = (TG.data && TG.data.items) || [];
        for (var i = 0; i < items.length; i++) {
          if (items[i].id === id) items[i].status = (action === 'restore' ? 'new' : 'done');
        }
        renderTriage();
        toast(action === 'restore' ? '已還原' : '已勾掉');
      })
      .catch(function (e) { btn.disabled = false; toast('請求失敗：' + friendlyErr(e)); });
  }
  $('tg-out').addEventListener('click', function (ev) {
    var chk = ev.target.closest('[data-tricheck]');
    if (chk) { triageAct(chk.getAttribute('data-tricheck'), 'check', chk); return; }
    var rst = ev.target.closest('[data-trirestore]');
    if (rst) triageAct(rst.getAttribute('data-trirestore'), 'restore', rst);
  });
  function loadTriage() {
    var out = $('tg-out');
    out.innerHTML = '<div class="muted" style="padding:14px 0">載入中…</div>';
    fetch('/console/triage-data', { headers: { Authorization: 'Bearer ' + S.token } })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
      .then(function (x) {
        if (!x.ok) {
          out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">讀不到分流台</div><div class="b">' + esc(x.d.error || ('HTTP ' + x.status)) + '</div></div>';
          return;
        }
        TG.data = x.d;
        // 記著的 project 篩選若已不存在（資料變了）→ 退回全部，別讓頁面空得不明不白
        if (TG.project && TG.project !== '__none__' && (x.d.projects || []).indexOf(TG.project) < 0) TG.project = '';
        renderTriage();
      })
      .catch(function (e) { out.innerHTML = '<div class="err" style="padding:14px 0">請求失敗：' + esc(friendlyErr(e)) + '</div>'; });
  }

  // ── 設定 ──
  function loadSettings() {
    // vectorize 狀態：探測既有 /kbdb/search 的 mode / capability_hint（無新端點，誠實讀降級訊號）
    fetch('/kbdb/search?q=mira&mode=semantic', { headers: apiHeaders() })
      .then(function (r) { return r.json(); })
      .then(function (d) {
        var on = d.mode === 'semantic';
        $('st-vec-switch').classList.toggle('on', on);
        $('st-vec').textContent = on
          ? '已啟用・語意搜尋可用（搜尋頁切「語意」）'
          : '未啟用・' + (d.capability_hint || '部署端尚未開啟 Vectorize（kbdb_embed）');
        $('st-vec-state').textContent = on ? '已啟用（live 探測 mode=semantic）' : '未啟用（live 探測降級 keyword）';
      })
      .catch(function () {
        $('st-vec').textContent = '狀態偵測失敗（KBDB 不可達）';
        $('st-vec-state').textContent = '偵測失敗';
      });
    // MCP token TTL（誠實佔位：只顯示目前生效值，不假裝能改）
    fetch('/console/settings-data')
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (d) {
        if (!d) { $('st-mcp-ttl').textContent = '讀取失敗'; return; }
        var days = Math.round(d.mcp_token_ttl_seconds / 86400 * 10) / 10;
        $('st-mcp-ttl').textContent = '目前生效：' + d.mcp_token_ttl_seconds.toLocaleString() + ' 秒（約 ' + days + ' 天）・' +
          (d.mcp_token_ttl_source === 'env' ? '來自部署端 env MCP_TOKEN_TTL' : '未設 env，用預設值 30 天');
      })
      .catch(function () { $('st-mcp-ttl').textContent = '讀取失敗'; });
    // 系統資訊：worker 自報 + 精耕層規模（14-E 遺產總數不再顯示，leo 2026-07-07 裁）
    Promise.allSettled([
      fetch('/').then(function (r) { return r.json(); }),
      fetch('/console/kb-scale-data').then(function (r) { return r.ok ? r.json() : null; })
    ]).then(function (rs) {
      var svc = rs[0].status === 'fulfilled' ? rs[0].value : {};
      var kb = rs[1].status === 'fulfilled' ? rs[1].value : null;
      var n = function (v) { return v == null ? '？' : v.toLocaleString(); };
      var rows = '';
      rows += '<div class="kvline"><span class="muted">服務</span><span class="mono" style="font-size:14px">' + esc(svc.service || 'arcrun-cypher-executor') + '</span></div>';
      rows += '<div class="kvline"><span class="muted">版本</span><span class="mono" style="color:var(--amber)">' + esc(svc.version || '—') + '</span></div>';
      rows += '<div class="kvline"><span class="muted">部署位置</span><span class="mono" style="font-size:14px">' + esc(location.host) + '</span></div>';
      rows += '<div class="kvline"><span class="muted">租戶</span><span class="mono">' + esc(S.tenant) + '</span></div>';
      rows += '<div class="kvline"><span class="muted">精耕層</span><span class="mono" style="color:var(--amber)">' + (kb ? 'wiki 卡 ' + n(kb.wiki_card_total) + '・三元組 ' + n(kb.triplets_total) + '・已嵌入 ' + n(kb.embedded) : '讀取失敗') + '</span></div>';
      $('st-info').innerHTML = rows;
    });
  }
  $('st-pw-save').addEventListener('click', function () {
    var oldPw = $('st-pw-old').value, email = $('st-pw-email').value.trim(), newPw = $('st-pw-new').value;
    var st = $('st-pw-status');
    if (!oldPw) { st.innerHTML = '<span class="err">請先輸入舊密碼</span>'; return; }
    if (!email) { st.innerHTML = '<span class="err">請輸入 Email（沿用原本的也要填）</span>'; return; }
    if (!newPw || newPw.length < 8) { st.innerHTML = '<span class="err">新密碼至少 8 碼</span>'; return; }
    st.textContent = '更新中…';
    fetch('/console/setup/reset', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ current_password: oldPw, email: email, password: newPw })
    })
      .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
      .then(function (x) {
        if (!x.ok) { st.innerHTML = '<span class="err">' + esc(x.d.error || '更新失敗') + '</span>'; return; }
        st.innerHTML = '<span class="ok">密碼已更新，下次登入用新帳密</span>';
        $('st-pw-old').value = ''; $('st-pw-email').value = ''; $('st-pw-new').value = '';
        toast('密碼已更新');
      })
      .catch(function (e) { st.innerHTML = '<span class="err">請求失敗：' + esc(friendlyErr(e)) + '</span>'; });
  });
  $('st-vec-switch').addEventListener('click', function () {
    toast('此開關不能遠端改——部署端 config.yaml 開 kbdb_embed 後 acr update 重部署');
  });
  $('st-logout').addEventListener('click', function () {
    var t = S.token;
    if (t) fetch('/console/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + t } }).catch(function () {});
    S.token = ''; S.tenant = '';
    localStorage.removeItem('arcrun_console_session');
    location.hash = '';
    checkConfigured();
  });

  init();
})();
</script>
</body>
</html>
`;
}

// GET /console — Mira Console 完整版（Arcrun#3 console 系）。registry base 現算：同帳號
// arcrun-registry worker（WORKER_SUBDOMAIN 沿用既有 KBDB_BASE_URL 那套組法），沒設就退回官方公開 registry。
consoleRouter.get('/console', (c) => {
  const subdomain = c.env.WORKER_SUBDOMAIN;
  const registryBase = subdomain
    ? `https://arcrun-registry.${subdomain}.workers.dev`
    : 'https://registry.arcrun.dev';
  // 品牌字樣（Arcrun#21）：引擎預設 Arcrun，實例可用 CONSOLE_BRAND 覆蓋（如 "Arcrun RAG"）
  // 頁面組成（console-profile-trim）：CONSOLE_PROFILE 未設＝full 全套；"rag"＝企業產品樣（搜尋落地）
  return c.html(renderConsoleHtml(registryBase, c.env.CONSOLE_BRAND || 'Arcrun', c.env.CONSOLE_PROFILE || 'full'));
});
