// 共用 — Masthead, Nav, Ticker, Footer
const { useMemo, useState, useEffect, useRef } = React;

// ── Ruflo Sidebar helpers (pure DOM, no React state needed for open/close) ──
function _rufloOpen()  { document.body.setAttribute('data-ruflo','1'); }
function _rufloClose() { document.body.removeAttribute('data-ruflo'); }
function _rufloToggle(){ document.body.hasAttribute('data-ruflo') ? _rufloClose() : _rufloOpen(); }
window.toggleRufloPanel = _rufloToggle;

// QuoteHub v2 核心（fetch攔截+跨頁快取+訂閱制主迴圈+時段感知+退避+狀態燈）
// 已抽成獨立檔案 quote-hub.js（見該檔頭註解 / QUOTE_LOOP.md），此處只留 React 包裝層。
// quote-hub.js 必須在本檔之前載入（見各頁 <script> 順序），提供 window.QuoteHub。

// 全站刷新 hook：回傳遞增計數，掛進 useEffect deps 即可在刷新鈕按下時重新抓取
window.useQhRefresh = function() {
  var pair = React.useState(0);
  var setN = pair[1];
  React.useEffect(function() {
    function bump() { setN(function(x) { return x + 1; }); }
    document.addEventListener('qh:refresh', bump);
    return function() { document.removeEventListener('qh:refresh', bump); };
  }, []);
  return pair[0];
};

// 給非 QuoteHub 攔截範圍內的 type（如 list）用：qhN>0（使用者按過刷新鈕）時在 URL 加時間戳記，
// 同時繞過瀏覽器磁碟快取與 CF 邊緣快取（query string 不同=快取 key 不同）；平常輪詢(qhN=0)不受影響。
window.qhBustUrl = function(url, qhN) {
  return qhN > 0 ? url + (url.indexOf('?') === -1 ? '?' : '&') + '_cb=' + Date.now() : url;
};

// QuoteHub v2 訂閱 hook:spec = {type, codes, params, skip}
// 回傳 {data, stale, t};組件卸載自動退訂。取代各頁自有 setInterval。
window.useQuote = function(spec) {
  var pair = React.useState({ data: null, stale: false, t: 0 });
  React.useEffect(function() {
    if (!spec || spec.skip || !spec.type) return;
    return window.QuoteHub.subscribe({
      type: spec.type, codes: spec.codes, params: spec.params,
      onData: function(d, m) { pair[1]({ data: d, stale: !!m.stale, t: m.t }); }
    });
  }, [spec && spec.type, !!(spec && spec.skip),
      JSON.stringify((spec && spec.codes) || ''),
      JSON.stringify((spec && spec.params) || '')]);
  return pair[0];
};

const RUFLO_DEPTS = [
  { id:'data', name:'A · 數據中台', color:'#2a6b4c', agents:[
    { id:'d1', name:'數據 Lead',      status:'active', link:'大盤總覽.html',         task:'stock.js · KV 快取 · health check' },
    { id:'d2', name:'數據驗證師',     status:'active', link:'大盤總覽.html',         task:'異常偵測 · 備援邏輯 · warmup' },
    { id:'d5', name:'數據管道',       status:'idle',   link:'大盤總覽.html',         task:'FinMind API · 排程抓取 · ETL' },
    { id:'d6', name:'備援爬蟲',       status:'idle',   link:'大盤總覽.html',         task:'Yahoo/FinMind 失效備援 · scrapy/scrapy' },
    { id:'d3', name:'新聞情緒師',     status:'active', link:'大盤總覽.html',         task:'NLP · RSS · 市場情緒' },
    { id:'d4', name:'總經研究員',     status:'idle',   link:'另類指標.html',         task:'匯率 · 利率 · 地緣政治' },
    { id:'d7', name:'開放資料師',     status:'idle',   link:'大盤總覽.html',         task:'TWSE/TPEx/期交所/MOPS OpenAPI · DDChen666/tw_trademarket_info' },
    { id:'d8', name:'即時串流師',     status:'idle',   link:'大盤總覽.html',         task:'WebSocket 即時報價推送 · websockets/ws' },
  ]},
  { id:'design', name:'B · 設計部', color:'#5e3a8e', agents:[
    { id:'b1', name:'設計 Lead',      status:'active', link:'大盤總覽.html',         task:'Formosa & Co. 品牌系統 · 設計決策' },
    { id:'b2', name:'UI 設計師',      status:'active', link:'大盤總覽.html',         task:'版面 · 色彩 · 排版 · styles.css · tabler-icons' },
    { id:'b3', name:'圖示師',         status:'idle',   link:'寶島產業流域圖鑑.html', task:'SVG 節點圖示 · K線視覺元素 · phosphor-icons' },
    { id:'b4', name:'UX 研究員',      status:'idle',   link:'黃金交叉雷達.html',     task:'新手引導設計 · 功能可發現性 · 金叉心法類按鈕' },
    { id:'b5', name:'動畫設計師',     status:'idle',   link:'大盤總覽.html',         task:'頁面過渡 · Lottie 載入動畫 · airbnb/lottie-web' },
    { id:'b6', name:'手機設計師',     status:'idle',   link:'大盤總覽.html',         task:'RWD · touch 手勢 · 375px 版面 · pmndrs/use-gesture' },
  ]},
  { id:'engineering', name:'C · 工程部', color:'#2c5e8e', agents:[
    { id:'c1', name:'工程 Lead',      status:'active', link:'大盤總覽.html',         task:'shared.jsx · 架構整合 · 跨頁一致性' },
    { id:'c2', name:'前端工程師',     status:'active', link:'大盤總覽.html',         task:'JSX 頁面實作 · React 組件 · 樣式實作' },
    { id:'c3', name:'K線師',          status:'active', link:'個股技術分析.html',     task:'lightweight-charts · 蠟燭圖 · 縮放 · 觸控' },
    { id:'c4', name:'指標師',         status:'idle',   link:'個股技術分析.html',     task:'MACD · RSI · BB · scan.jsx · pandas-ta 參考' },
    { id:'c5', name:'CF 工程師',      status:'idle',   link:'大盤總覽.html',         task:'Cloudflare Workers · Pages Functions · KV · cloudflare/workers-sdk' },
    { id:'c6', name:'測試工程師',     status:'idle',   link:'大盤總覽.html',         task:'前端自動化測試 · 跨瀏覽器驗證 · cypress-io/cypress' },
    { id:'c7', name:'PWA 工程師',     status:'idle',   link:'大盤總覽.html',         task:'Service Worker · 離線快取 · GoogleChrome/workbox' },
  ]},
  { id:'quant', name:'D · 策略研究室', color:'#8a6a2a', agents:[
    { id:'q1', name:'量化 Lead',      status:'active', link:'黃金交叉雷達.html',     task:'scan.jsx · screener.jsx · 訊號設計' },
    { id:'q2', name:'回測師',         status:'idle',   link:'策略回測.html',         task:'策略驗證 · Bot4 · 期望值計算' },
    { id:'q3', name:'股票分析師',     status:'active', link:'個股技術分析.html',     task:'技術面 · 籌碼 · 個股評分' },
    { id:'q4', name:'回測工程師',     status:'idle',   link:'策略回測.html',         task:'MA · RSI · MACD 回測邏輯 · ta-lib/ta-lib-python' },
    { id:'q5', name:'風控顧問',       status:'idle',   link:'自選股.html',           task:'停損 · 部位管理 · 波動率' },
    { id:'q6', name:'產業分析師',     status:'active', link:'寶島產業流域圖鑑.html', task:'供應鏈 · 趨勢 · 題材輪動' },
    { id:'q7', name:'量能分析師',     status:'idle',   link:'黃金交叉雷達.html',     task:'資金流向 · 量價關係 · 向量化回測 · polakowo/vectorbt' },
    { id:'q8', name:'選擇權師',       status:'idle',   link:'另類指標.html',         task:'隱含波動率 · B-S 定價 · Put/Call Ratio · vollib/py_vollib' },
    { id:'q9', name:'選股師',         status:'active', link:'選股工坊.html',           task:'投資/投機雙模式選股 · 多因子評分矩陣 · quantopian/alphalens' },
    { id:'q10', name:'基本面研究員',  status:'active', link:'企業基本面.html',         task:'企業段位 · 護城河量化 · 健檢紅綠燈 · 估值與分析師共識' },
    { id:'q11', name:'技術版圖策展員', status:'active', link:'企業基本面.html',        task:'tech-moat.js 維護 · 法說會/技術論壇追蹤 · 新公司建檔' },
  ]},
  { id:'qa', name:'E · 品保部', color:'#6b2a3c', agents:[
    { id:'p1', name:'品保 Lead',      status:'active', link:'大盤總覽.html',         task:'程式碼審查 · 部署驗證' },
    { id:'p2', name:'資安稽核',       status:'idle',   link:'大盤總覽.html',         task:'API 安全 · CORS · 資料洩漏' },
    { id:'p3', name:'效能稽核師',     status:'idle',   link:'大盤總覽.html',         task:'頁面載入 · API 回應時間 · GoogleChrome/lighthouse' },
    { id:'p4', name:'E2E 測試師',     status:'idle',   link:'大盤總覽.html',         task:'UI 自動化測試 · 跨瀏覽器 · microsoft/playwright' },
    { id:'p5', name:'無障礙稽核師',   status:'idle',   link:'大盤總覽.html',         task:'WCAG 合規 · 鍵盤導覽 · 螢幕閱讀器 · dequelabs/axe-core' },
  ]},
  { id:'quant-trade', name:'F · 量化交易部', color:'#4a2a6b', agents:[
    { id:'e1', name:'訊號師 Lead',    status:'active', link:'黃金交叉雷達.html',     task:'買賣訊號 · 進出場條件 · twopirllc/pandas-ta' },
    { id:'e2', name:'SMC 分析師',     status:'idle',   link:'個股技術分析.html',     task:'OB · BOS · FVG · joshyattridge/smc' },
    { id:'e3', name:'部位管理師',     status:'idle',   link:'策略回測.html',         task:'Kelly公式 · 資金分配 · microsoft/qlib' },
    { id:'e4', name:'加密貨幣師',     status:'active', link:'策略回測.html',         task:'Bot4 多交易所策略 · 多空切換 · freqtrade/freqtrade' },
    { id:'e5', name:'市場狀態師',     status:'idle',   link:'黃金交叉雷達.html',     task:'趨勢/震盪/高波動狀態偵測 · HMM · polakowo/vectorbt' },
  ]},
  { id:'admin', name:'G · 行政部', color:'#3a5a3a', agents:[
    { id:'g1', name:'行政 Lead',      status:'active', link:'大盤總覽.html',         task:'行政協調 · SOP 制定 · 跨部門溝通' },
    { id:'g2', name:'部署長',         status:'idle',   link:'大盤總覽.html',         task:'wrangler 部署 · seed key 版本號 · CF 環境變數' },
    { id:'g3', name:'文管員',         status:'idle',   link:'大盤總覽.html',         task:'CLAUDE.md · MEMORY.md · TEAM.md 維護 · 記憶體清理' },
    { id:'g4', name:'版本發布師',     status:'idle',   link:'大盤總覽.html',         task:'版本號規則 · changelog · semantic-release/semantic-release' },
    { id:'g5', name:'資料管理師',     status:'idle',   link:'大盤總覽.html',         task:'API 文件規範 · 資料品質 · IBM/openapi-validator' },
  ]},
];

// 供 lazy 載入的團隊頁（team-page.jsx）取用
window.RUFLO_DEPTS = RUFLO_DEPTS;

// ── 創作者識別碼（PIN 由伺服器驗證，不存在前端）─────────────
var CREATOR_TOKEN = 'ruflo_creator'; // localStorage 存的登入狀態標記

// ── 各專員建議事項（依職責預設，可擴充）──────────────────────
var AGENT_ADVICE = window.AGENT_ADVICE = {
  'd1': ['確認 stock.js 所有 API 端點回應時間 < 2s','KV 快取策略：熱門股加預熱 warmup','health check 增加 FinMind / Yahoo Finance 可用性監控'],
  'd2': ['kvLastGood 韌性層維護：新外部來源端點預設包覆','health 端點監控 tpex_quotes/tradingview，節點封鎖時通報','TPEx 改版偵測：BWIBBU 欄位/dailyQuotes 格式變動已三度發生，解析必須依欄位名動態定位','FinMind 免費額度監控，超量自動切換備援'],
  'd5': ['建立 FinMind 批次抓取排程，降低重複呼叫','評估 Cloudflare KV 存放季報資料，降低 API 依賴','研究 MOPS 官方 OpenAPI 替代 HTML 爬取'],
  'd3': ['Yahoo News RSS 備援至 Google News RSS','新聞情緒分類加入正負面關鍵詞過濾','考慮 RSS 快取策略，避免每次重新抓取'],
  'd4': ['每週追蹤 VIX / 美債殖利率 / 匯率三項總體指標','建立總體環境評分，納入大盤評級','研究聯準會會議日程對台股影響模型'],
  'b1': ['Formosa & Co. 設計系統文件化（color token / typography scale）','審查各頁面品牌一致性（字體、間距、金色用法）','設計新頁面進入動畫規範'],
  'b2': ['styles.css 整理：消除重複 class，統一命名規則','tabler-icons 或 phosphor-icons 替換現有 emoji 圖示','Dark mode token 前期研究'],
  'b3': ['node-icons.jsx 補充 AI/雲端/金融科技類圖示','K線圖視覺：D/E 事件標記 SVG 設計稿','圖示一致性審查：各頁面 icon 風格統一'],
  'b4': ['新手引導 onboarding flow 設計（首次訪問引導）','金叉心法按鈕 A/B 測試：位置 / 文案 / 顏色','黃金交叉雷達使用流程優化：從掃描到 TradingView 一氣呵成'],
  'c1': ['shared.jsx RufloSidebar 增加專員互動功能（進行中）','NavBar 加入頁面活躍狀態追蹤','評估將 data.js 改為動態從 API 抓取'],
  'c2': ['基本面頁面改善：無資料顯示更友善訊息','同業比較表加入股價走勢迷你圖','企業基本面 05 投資者上看潛力 — ADR 換算說明優化'],
  'c3': ['K線圖加除息/財報事件 D/E 標記（已在待辦）','評估加入成交量 VPVR Profile','研究 lightweight-charts Price Line 動態標記 API'],
  'c4': ['整合 MACD 進出場策略至黃金交叉雷達 scan.jsx（已在待辦）','RSI + MACD 雙重背離確認，降低假訊號率','研究 KDJ 指標加入輔助確認','背離回看參數 40 根改動態（ATR 波動率決定）'],
  'c5': ['評估 Pages Functions 加 retry 邏輯（Yahoo 429 問題）','KV 快取策略：區分熱門股 TTL vs 冷門股','研究 Durable Objects 作為即時報價訊道'],
  'd6': ['建立 Yahoo Finance 備援爬蟲（scrapy/scrapy 參考）','FinMind 額度滿載時自動切換備援數據源','評估 TWSE/TPEx OpenAPI 作為完全免費備援'],
  'p3': ['每次部署後跑 Lighthouse mobile 評分（目標 Performance > 80）','監控 API 平均回應時間（/api/stock 各 type）','bundle size 追蹤：unpkg CDN 版本固定，避免意外升級'],
  'g1': ['建立部署 SOP 文件（版本號規則 / 部署前清單）','統籌跨部門任務優先序（每週一整理 backlog）','協調設計部 + 工程部 handoff 流程'],
  'g2': ['wrangler 部署自動化腳本優化（deploy-cf.ps1）','seed key 版本號管理規則文件化','CF 環境變數清單維護（FMP_KEY / CREATOR_PIN / CACHE_TTL）'],
  'g3': ['每週更新 CLAUDE.md Compact Instructions 段落','MEMORY.md 過期記憶清理（超過 30 天的 project 記憶）','TEAM.md 同步最新部門結構與 Repo 參考'],
  'g4': ['semantic-release 規則研究：conventional commit → 自動版本號','建立 CHANGELOG.md 自動生成流程','定義版本號規則：MAJOR 破壞性 / MINOR 新功能 / PATCH 修復'],
  'g5': ['API 端點文件化（stock.js 所有 type 的 request/response schema）','用 IBM/openapi-validator 規範 API 回應格式一致性','資料品質 SLA：每日數據缺失率 < 1%'],
  'd7': ['研究期交所 OpenAPI：Put/Call Ratio · 未平倉量 · 大額交易人','MOPS 公開資訊觀測站：月營收 · 重大訊息 API','TWSE open API v2 深度整合，替換現有 HTML 解析'],
  'd8': ['評估 WebSocket 替換現有輪詢（dashboard 即時價格）','研究 Cloudflare Durable Objects 作為 WS 廣播節點','ws 函式庫架構研究：client reconnection · heartbeat 邏輯'],
  'b5': ['Lottie 載入動畫設計（掃描進行中 / API 讀取中）','頁面切換過渡效果設計稿（fade vs slide 比較）','K線圖首次載入骨架屏（skeleton）動畫'],
  'b6': ['手機版 375px 全頁測試，記錄 overflow 問題清單','touch swipe 切換 tab（個股技術分析三個 tab）','研究 pmndrs/use-gesture 手勢庫整合到 K線圖縮放'],
  'c6': ['cypress-io/cypress 安裝 + 基礎測試架構建立','測試優先順序：掃描結果出現 / API 正確回傳 / Modal 開關','CI 整合：部署前自動跑回歸測試'],
  'c7': ['評估黃金交叉雷達加入 PWA manifest（可安裝到手機桌面）','workbox 快取策略：static assets precache + API stale-while-revalidate','研究 Background Sync 在市場時間推送掃描結果'],
  'q7': ['vectorbt 量能分析：成交量 SMA × 突破確認','量增價漲 / 量縮價跌判斷邏輯整合到 scan.jsx','黃金交叉搭配量能確認：volume > 20日均量 × 1.5 條件'],
  'q8': ['期交所 Put/Call Ratio 整合到另類指標頁','隱含波動率 IV 計算（Black-Scholes，py_vollib 參考）','選擇權到期日前後台股波動模式研究'],
  'e4': ['freqtrade 策略框架研究：ROI table × stoploss × trailing stop','Bot4 多交易所設定：Binance / Bybit / OKX 並行','加密貨幣市場狀態過濾：BTC.D 主導率影響山寨幣策略'],
  'e5': ['HMM 市場狀態偵測：趨勢多 / 趨勢空 / 震盪 三態','vectorbt 狀態切換回測：趨勢策略 vs 均值回歸策略選擇','TAIEX 20 日 ATR 波動率分位數作為狀態切換閾值'],
  'p4': ['playwright 測試套件：黃金交叉雷達掃描 → 點擊結果 → 跳轉個股','跨瀏覽器測試矩陣：Chrome / Safari / Firefox / Mobile Safari','API mock 整合：playwright route() 攔截 /api/stock 測試離線狀態'],
  'p5': ['axe-core 整合：每次部署後自動跑無障礙掃描','鍵盤導覽地圖設計：Tab 順序 · Focus ring 可見性','色彩對比度審查：paper-beige #f6f1e4 vs ink-navy #1a2438 WCAG AA'],
  'q1': ['黃金交叉雷達加入「MACD 進出場建議」展開列','screener 篩選條件加入 EPS YoY > 10% 基本面過濾','掃描結果加入強弱排序（signal count × volume）'],
  'q2': ['查看策略實驗室今日訊號（雷達區→策略實驗室，每日自動掃主力34檔×5策略）','回測架構評估：加入手續費、滑點模擬','策略實驗室參數尋優榜的樣本外驗證（走 Phase 4 開發端流程）','建立勝率追蹤 Dashboard（月更新）'],
  'q3': ['個股評分加入基本面 P/E、EPS YoY 權重','自選股加入評分變動通知','籌碼連續 N 日同向計數顯示優化'],
  'q4': ['EPS 圖加去年同季對比虛線（已在待辦）','MACD 黃金交叉後 N 根勝率統計','回測框架加入 OB 命中率記錄'],
  'q5': ['停損優化：用 ATR × 1.5 取代固定%','部位大小建議：Kelly 公式概念整合','波動率監控：VIX > 20 時降低部位'],
  'q6': ['台股產業流域圖 — 加入 AI 相關族群','AI / 半導體題材輪動追蹤','供應鏈圖加入月營收 YoY 顏色編碼'],
  'p1': ['部署前跑 lighthouse 效能評分','API 回應格式統一化審查（error schema）','測試所有頁面在 mobile 375px 的顯示'],
  'p2': ['stock.js CORS 設定審查','API key（FMP）是否有 referer 限制設定','確認 localStorage 不存放敏感資料'],
  'e1': ['盯策略實驗室今日訊號榜：SMC需求區/黃金交叉觸發清單（lab_daily 每日午夜自動掃）','整合 SMC + MACD 雙策略加權訊號公式設計','研究 twopirllc/pandas-ta Williams %R 作為輔助','台股進出場訊號條件測試（2330 回歷史驗證）'],
  'e2': ['OB 命中率監控：每次 OB 進場後記錄結果','優化 Swing lookback 參數（現 5 根）','研究 FVG 填補速度規律作為進場時機'],
  'e3': ['Kelly 公式模組設計（f* = edge / odds）','研究 microsoft/qlib 因子框架','部位管理建議：最大單筆部位 < 總資金 20%'],
  'q9': ['設計投資模式評分矩陣（月營收加速 + 法人連買 + ROE + EPS）','設計投機模式評分矩陣（MACD 交叉 + 放量突破 + 外資翻買）','定義各因子閾值與權重，輸出 0-100 分複合評分'],
  'q10': ['企業段位五維權重季度回測：段位 vs 後續報酬驗證','護城河量化閾值調校（毛利率/ROE 連續季數門檻按產業分組）','健檢紅綠燈擴充：存貨週轉惡化、應收帳款暴增等盈餘品質燈號','MOPS 研發費用抓取評估，補技術護城河量化缺口'],
  'q11': ['每季法說會後更新 tech-moat.js（A16/CoWoS 產能/DTC 進度）','擴檔優先序：IC 設計（達發/譜瑞外）→ 散熱第二梯隊 → 軍工鏈','status 升降規則文件化：何時 lead→par（對手量產追平）','上櫃技術股深挖：穎崴/雍智/M31/愛普'],
};

// ── 輕量隊員面板（點按鈕彈出，點外面關閉）────────────────────
function TeamPanel() {
  var open    = useState(false); var setOpen = open[1]; open = open[0];
  var ref     = useRef(null);

  useEffect(function() {
    if (!open) return;
    function handler(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener('mousedown', handler);
    return function() { document.removeEventListener('mousedown', handler); };
  }, [open]);

  return React.createElement('div', { style:{ position:'relative' }, ref: ref },
    React.createElement('button', {
      className: 'ruflo-toggle-btn',
      onClick: function() { setOpen(function(v){ return !v; }); },
      title: '分析隊',
      style: open ? { background:'var(--ink)', color:'var(--paper-bright)' } : {},
    },
      _I_TEAM,
      React.createElement('span', { className:'kai', style:{ fontSize:'var(--fs-nano)' } }, '隊員')
    ),
    open && React.createElement('div', {
      style:{
        position:'absolute', top:'calc(100% + 8px)', right:0, zIndex:500,
        width:300, maxHeight:'70vh', overflowY:'auto',
        background:'var(--paper-bright)', border:'1px solid var(--ink)',
        boxShadow:'4px 4px 0 rgba(26,36,56,.12)',
      }
    },
      React.createElement('div', { style:{ borderBottom:'1px solid var(--paper-line)', padding:'10px 14px', display:'flex', justifyContent:'space-between', alignItems:'center' } },
        React.createElement('span', { style:{ fontFamily:'Times New Roman', fontStyle:'italic', fontSize:12, letterSpacing:'.1em', color:'var(--ink-mute)' } }, 'Analysis Team'),
        React.createElement('span', { className:'kai', style:{ fontSize:'var(--fs-nano)', color:'var(--gold-deep)' } },
          RUFLO_DEPTS.reduce(function(s,d){ return s + d.agents.filter(function(a){ return a.status==='active'; }).length; },0) + ' active'
        )
      ),
      RUFLO_DEPTS.map(function(dept) {
        var active = dept.agents.filter(function(a){ return a.status==='active'; });
        var idle   = dept.agents.filter(function(a){ return a.status!=='active'; });
        return React.createElement('div', { key: dept.id, style:{ borderBottom:'1px solid var(--paper-line)' } },
          React.createElement('div', { style:{ padding:'8px 14px 6px', display:'flex', alignItems:'center', gap:8, borderLeft:'3px solid '+dept.color } },
            React.createElement('span', { className:'kai', style:{ fontSize:'var(--fs-nano)', fontWeight:700, flex:1 } }, dept.name),
            React.createElement('span', { style:{ fontSize:'var(--fs-nano)', color: dept.color, letterSpacing:'.05em' } }, active.length+'/'+dept.agents.length)
          ),
          dept.agents.map(function(ag) {
            return React.createElement('a', {
              key: ag.id, href: ag.link,
              style:{
                display:'flex', alignItems:'center', gap:8,
                padding:'5px 14px 5px 20px', textDecoration:'none', color:'inherit',
                fontSize:12, transition:'background .1s',
              },
              onMouseEnter: function(e){ e.currentTarget.style.background='var(--paper-deep)'; },
              onMouseLeave: function(e){ e.currentTarget.style.background=''; },
            },
              React.createElement('span', { style:{
                width:5, height:5, borderRadius:'50%', flexShrink:0,
                background: ag.status==='active' ? 'var(--bull)' : 'var(--paper-line)',
              }}),
              React.createElement('span', { className:'kai' }, ag.name),
              React.createElement('span', { style:{ fontSize:'var(--fs-nano)', color:'var(--ink-mute)', marginLeft:'auto', maxWidth:140, textAlign:'right', lineHeight:1.3 } }, ag.task.split('·')[0].trim())
            );
          })
        );
      })
    )
  );
}

function RufloSidebar() {
  var tab       = useState('team');  var setTab       = tab[1];       tab       = tab[0];
  var exData    = useState(true);    var setExData    = exData[1];    exData    = exData[0];
  var exDesign  = useState(false);   var setExDesign  = exDesign[1];  exDesign  = exDesign[0];
  var exEng     = useState(false);   var setExEng     = exEng[1];     exEng     = exEng[0];
  var exQuant   = useState(false);   var setExQuant   = exQuant[1];   exQuant   = exQuant[0];
  var exQa      = useState(false);   var setExQa      = exQa[1];      exQa      = exQa[0];
  var exTrade   = useState(false);   var setExTrade   = exTrade[1];   exTrade   = exTrade[0];
  var exAdmin   = useState(false);   var setExAdmin   = exAdmin[1];   exAdmin   = exAdmin[0];
  var selAgS    = useState(null);    var setSelAg     = selAgS[1];    var selAg = selAgS[0];
  var agTaskS   = useState('');      var setAgTask    = agTaskS[1];   var agTask = agTaskS[0];
  var creator   = useState(function(){ return localStorage.getItem('tw_creator')===CREATOR_TOKEN; });
  var isCreator = creator[0]; var setCreator = creator[1];
  var pinS      = useState('');      var setPin = pinS[1];      var pin = pinS[0];
  var pinErrS   = useState(false);   var setPinErr = pinErrS[1]; var pinErr = pinErrS[0];
  var showPinS  = useState(false);   var setShowPin = showPinS[1]; var showPin = showPinS[0];
  var todoCat = useState('今天');     var setTodoCat = todoCat[1]; todoCat = todoCat[0];
  var newTodo = useState('');         var setNewTodo = newTodo[1]; newTodo = newTodo[0];
  var noteT   = useState('');         var setNoteT   = noteT[1]; noteT   = noteT[0];
  var noteC   = useState('');         var setNoteC   = noteC[1]; noteC   = noteC[0];
  var search  = useState('');         var setSearch  = search[1]; search  = search[0];
  var visitDays = useState(null);     var setVisitDays = visitDays[1]; visitDays = visitDays[0];

  var todosS = useState(function() { try { return JSON.parse(localStorage.getItem('tw_ruflo_todos')||'[]'); } catch(e) { return []; } });
  var todos = todosS[0]; var setTodos = todosS[1];
  var notesS = useState(function() { try { return JSON.parse(localStorage.getItem('tw_ruflo_notes')||'[]'); } catch(e) { return []; } });
  var notes = notesS[0]; var setNotes = notesS[1];

  useEffect(function(){ localStorage.setItem('tw_ruflo_todos', JSON.stringify(todos)); }, [todos]);
  useEffect(function(){ localStorage.setItem('tw_ruflo_notes', JSON.stringify(notes)); }, [notes]);

  // ── KV 單向同步（本地 → KV，讓 Claude 可讀）─────────────────
  // 注意：只推不拉，避免舊 KV 資料覆蓋剛 seed 的本地資料
  var _kvTimer = useRef(null);
  useEffect(function() {
    if (_kvTimer.current) clearTimeout(_kvTimer.current);
    _kvTimer.current = setTimeout(function() {
      var secret = localStorage.getItem('tw_creator_secret') || '';
      if (!secret) return;
      fetch('/api/stock?type=tasks', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + secret,
        },
        body: JSON.stringify({ todos: todos }),
      }).catch(function(){});
    }, 3000);
  }, [todos]);

  function addTodo() {
    if (!newTodo.trim()) return;
    var cat = todoCat === '已完成' ? '今天' : todoCat;
    setTodos(function(t){ return t.concat([{ id: Date.now(), text: newTodo.trim(), cat: cat, done: false }]); });
    setNewTodo('');
  }
  function doneTodo(id) { setTodos(function(t){ return t.map(function(x){ return x.id===id ? Object.assign({},x,{done:!x.done}) : x; }); }); }
  function delTodo(id)  { setTodos(function(t){ return t.filter(function(x){ return x.id!==id; }); }); }
  function addNote() {
    if (!noteT.trim()) return;
    setNotes(function(n){ return n.concat([{ id: Date.now(), title: noteT.trim(), content: noteC.trim() }]); });
    setNoteT(''); setNoteC('');
  }
  function delNote(id) { setNotes(function(n){ return n.filter(function(x){ return x.id!==id; }); }); }

  function tryUnlock() {
    fetch('/api/stock?type=auth', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ pin: pin }),
    })
      .then(function(r) { return r.json(); })
      .then(function(d) {
        if (d.token) {
          localStorage.setItem('tw_creator', CREATOR_TOKEN);
          localStorage.setItem('tw_creator_secret', d.token);
          setCreator(true); setShowPin(false); setPin(''); setPinErr(false);
        } else {
          setPinErr(true); setPin('');
        }
      })
      .catch(function() { setPinErr(true); setPin(''); });
  }
  function logout() {
    localStorage.removeItem('tw_creator');
    localStorage.removeItem('tw_creator_secret');
    setCreator(false); setSelAg(null);
  }

  function assignTask() {
    if (!agTask.trim() || !selAg) return;
    var text = '[' + selAg.name + '] ' + agTask.trim();
    setTodos(function(t){ return t.concat([{ id: Date.now(), text: text, cat: '本週', done: false, agent: selAg.id }]); });
    setAgTask('');
  }

  function onSearch(e) {
    if (e.key !== 'Enter') return;
    var code = search.trim().replace(/\D/g,'');
    if (code) { _rufloClose(); window.location.href = '個股技術分析.html?code='+code; }
  }

  var todayN = todos.filter(function(t){ return t.cat==='今天' && !t.done; }).length;
  var weekN  = todos.filter(function(t){ return t.cat==='本週' && !t.done; }).length;
  var doneN  = todos.filter(function(t){ return t.done; }).length;
  var shownTodos = todoCat==='已完成'
    ? todos.filter(function(t){ return t.done; })
    : todos.filter(function(t){ return t.cat===todoCat && !t.done; });

  var exMap    = { data: exData, design: exDesign, engineering: exEng, quant: exQuant, qa: exQa, 'quant-trade': exTrade, admin: exAdmin };
  var setExMap = { data: setExData, design: setExDesign, engineering: setExEng, quant: setExQuant, qa: setExQa, 'quant-trade': setExTrade, admin: setExAdmin };

  return React.createElement(React.Fragment, null,
    React.createElement('div', { className:'rs-backdrop', onClick: _rufloClose }),
    React.createElement('aside', { className:'rs-panel' },

      /* Header */
      React.createElement('div', { className:'rs-header' },
        React.createElement('div', { className:'rs-logo' },
          React.createElement('div', { className:'rs-logo-mark' }, 'R'),
          React.createElement('div', null,
            React.createElement('div', { className:'rs-logo-name' }, 'RUFLO'),
            React.createElement('div', { className:'rs-logo-sub kai' }, isCreator ? '創 作 者 模 式' : '訪 客 模 式')
          )
        ),
        React.createElement('div', { style:{ display:'flex', alignItems:'center', gap:6 }},
          /* 鎖頭按鈕 */
          React.createElement('button', {
            title: isCreator ? '登出創作者模式' : '創作者登入',
            onClick: function(){ if(isCreator){ logout(); } else { setShowPin(!showPin); setPinErr(false); setPin(''); } },
            style:{
              background:'none', border:'1px solid',
              borderColor: isCreator ? 'var(--bull)' : 'var(--paper-line)',
              color: isCreator ? 'var(--bull)' : 'var(--ink-mute)',
              borderRadius:3, padding:'3px 8px', cursor:'pointer', fontSize:13,
            }
          }, isCreator ? '🔓' : '🔒'),
          React.createElement('button', { className:'rs-close', onClick: _rufloClose }, '✕')
        )
      ),

      /* PIN 輸入框（僅未解鎖時顯示） */
      showPin && !isCreator && React.createElement('div', { style:{
        padding:'10px 14px', background:'rgba(0,0,0,0.3)',
        borderBottom:'1px solid var(--paper-line)',
        display:'flex', gap:8, alignItems:'center',
      }},
        React.createElement('input', {
          type:'password',
          className:'rs-inp',
          placeholder:'輸入創作者密碼…',
          value: pin,
          autoFocus: true,
          onChange: function(e){ setPin(e.target.value); setPinErr(false); },
          onKeyDown: function(e){ if(e.key==='Enter') tryUnlock(); },
          style:{ flex:1, fontSize:13, borderColor: pinErr ? 'var(--bear)' : '', letterSpacing:'.15em' }
        }),
        React.createElement('button', {
          onClick: tryUnlock,
          style:{ background:'var(--gold)', border:'none', color:'#fff', padding:'6px 12px', cursor:'pointer', fontSize:13, borderRadius:2 }
        }, '解鎖'),
        pinErr && React.createElement('span', { style:{ fontSize:'var(--fs-nano)', color:'var(--bear)' }}, '密碼錯誤')
      ),

      /* Quick search */
      React.createElement('div', { className:'rs-quicksearch' },
        React.createElement('span', { className:'rs-qs-icon' }, '⌕'),
        React.createElement('input', {
          className:'rs-qs-input kai', placeholder:'輸入代號 Enter 跳轉…',
          value: search,
          onChange: function(e){ setSearch(e.target.value); },
          onKeyDown: onSearch
        })
      ),

      /* Tabs */
      React.createElement('div', { className:'rs-tabs' },
        [['team','◈ 隊員'], ['todos','◉ 計畫'], ['notes','◎ 更新日誌']].concat(isCreator ? [['visits','◑ 訪客']] : []).map(function(p){
          var cnt = p[0]==='todos' ? (todayN+weekN) : 0;
          return React.createElement('button', {
            key: p[0],
            className: 'rs-tab'+(tab===p[0] ? ' rs-tab-on' : ''),
            onClick: function(){
              setTab(p[0]);
              if (p[0]==='visits' && visitDays === null) {
                fetch('/api/stock?type=visits')
                  .then(function(r){ return r.json(); })
                  .then(function(d){ setVisitDays(d.days || []); })
                  .catch(function(){ setVisitDays([]); });
              }
            }
          },
            React.createElement('span', { className:'kai' }, p[1]),
            cnt > 0 && React.createElement('span', { className:'rs-tab-badge' }, cnt)
          );
        })
      ),

      /* Body */
      React.createElement('div', { className:'rs-body' },

        /* 隊員 */
        tab==='team' && React.createElement('div', null,

          /* 專員詳情面板（僅創作者） */
          isCreator && selAg && React.createElement('div', { style:{
            margin:'0 0 12px', padding:'14px 14px 12px',
            background:'var(--paper)', border:'1px solid var(--paper-line)',
            borderLeft:'3px solid var(--gold)',
          }},
            /* 頂列：名稱 + 關閉 */
            React.createElement('div', { style:{ display:'flex', alignItems:'center', marginBottom:10, gap:8 }},
              React.createElement('div', { style:{
                width:8, height:8, borderRadius:'50%', flexShrink:0,
                background: selAg.status==='active' ? 'var(--bull)' : 'var(--ink-mute)',
              }}),
              React.createElement('div', { className:'kai', style:{ fontSize:14, color:'var(--ink)', flex:1 }}, selAg.name),
              React.createElement('a', { href: selAg.link, onClick: _rufloClose, style:{
                fontSize:'var(--fs-nano)', color:'var(--gold-deep)', fontFamily:'Times New Roman,serif',
                fontStyle:'italic', marginRight:8, textDecoration:'none',
              }}, '→ 跳轉頁面'),
              React.createElement('button', {
                onClick: function(){ setSelAg(null); setAgTask(''); },
                style:{ background:'none', border:'none', color:'var(--ink-mute)', cursor:'pointer', fontSize:14, padding:'0 2px' }
              }, '✕')
            ),
            /* 職責 */
            React.createElement('div', { style:{ fontSize:'var(--fs-nano)', color:'var(--ink-mute)', fontFamily:'Times New Roman,serif', fontStyle:'italic', marginBottom:10 }}, selAg.task),

            /* 建議事項 */
            React.createElement('div', { className:'kai', style:{ fontSize:'var(--fs-nano)', color:'var(--gold-deep)', letterSpacing:'.08em', marginBottom:6 }}, '◈ 建議做的事'),
            React.createElement('div', null,
              (AGENT_ADVICE[selAg.id] || ['此專員尚無預設建議']).map(function(tip, i){
                return React.createElement('div', { key:i, style:{
                  display:'flex', gap:8, alignItems:'flex-start',
                  padding:'6px 0', borderBottom:'1px solid var(--paper-line)',
                }},
                  React.createElement('span', { style:{ color:'var(--gold)', fontSize:'var(--fs-nano)', flexShrink:0, marginTop:1 }}, '·'),
                  React.createElement('span', { className:'kai', style:{ fontSize:'var(--fs-xs)', color:'var(--ink)', lineHeight:1.6 }}, tip),
                  React.createElement('button', {
                    title:'加入待辦',
                    onClick: function(){
                      var text = '['+selAg.name+'] '+tip;
                      setTodos(function(t){ return t.concat([{ id:Date.now(), text:text, cat:'本週', done:false, agent:selAg.id }]); });
                    },
                    style:{ background:'none', border:'none', cursor:'pointer', color:'var(--gold-deep)', fontSize:14, padding:'0 2px', flexShrink:0 }
                  }, '＋')
                );
              })
            ),

            /* 派遣任務 input */
            React.createElement('div', { style:{ display:'flex', gap:6, marginTop:10 }},
              React.createElement('input', {
                className:'rs-inp kai',
                placeholder:'派遣自訂任務（Enter 確認）…',
                value: agTask,
                onChange: function(e){ setAgTask(e.target.value); },
                onKeyDown: function(e){ if(e.key==='Enter') assignTask(); },
                style:{ flex:1, fontSize:12 }
              }),
              React.createElement('button', {
                className:'rs-add-btn', onClick: assignTask,
                style:{ padding:'0 10px', fontSize:13 }
              }, '＋')
            )
          ),

          /* 部門列表 */
          RUFLO_DEPTS.map(function(dept){
            var ex = exMap[dept.id];
            var setEx = setExMap[dept.id];
            var activeN = dept.agents.filter(function(a){ return a.status==='active'; }).length;
            return React.createElement('div', { key: dept.id, className:'rs-dept', style:{ borderLeftColor: dept.color } },
              React.createElement('button', { className:'rs-dept-hd', onClick: function(){ setEx(!ex); } },
                React.createElement('span', { className:'rs-dept-arrow', style:{ transform: ex?'rotate(90deg)':'' } }, '›'),
                React.createElement('span', { className:'kai rs-dept-name' }, dept.name),
                React.createElement('span', { className:'rs-dept-pill', style:{ background: dept.color+'22', color: dept.color } },
                  activeN+'/'+dept.agents.length
                )
              ),
              ex && dept.agents.map(function(ag){
                var isSel = isCreator && selAg && selAg.id === ag.id;
                return React.createElement('div', {
                  key: ag.id,
                  className: 'rs-agent',
                  style:{ cursor: isCreator ? 'pointer' : 'default', background: isSel ? 'rgba(184,146,77,0.08)' : '', borderLeft: isSel ? '2px solid var(--gold)' : '' },
                  onClick: function(){ if(!isCreator) return; setSelAg(isSel ? null : Object.assign({}, ag)); setAgTask(''); }
                },
                  React.createElement('span', { className:'rs-ag-dot'+(ag.status==='active'?' rs-ag-dot-on':''), style:{ background: ag.status==='active'?dept.color:'' } }),
                  React.createElement('div', { className:'rs-ag-body' },
                    React.createElement('div', { className:'kai rs-ag-name' }, ag.name),
                    React.createElement('div', { className:'rs-ag-task' }, ag.task)
                  ),
                  isCreator && React.createElement('span', { className:'rs-ag-arr', style:{ color: isSel ? 'var(--gold)' : '' } }, isSel ? '▾' : '›')
                );
              })
            );
          }),
          React.createElement('div', { className:'rs-sys-footer' },
            React.createElement('div', { className:'rs-sys-row' }, React.createElement('span',null,'Model'), React.createElement('span',{className:'rs-sys-val'},'claude-sonnet-4-6')),
            React.createElement('div', { className:'rs-sys-row' }, React.createElement('span',null,'Swarm'), React.createElement('span',{className:'rs-sys-val'},'hierarchical · 15 max')),
            React.createElement('div', { className:'rs-sys-row' }, React.createElement('span',null,'Status'), React.createElement('span',{className:'rs-sys-val',style:{color:'#4caf7d'}},'operational'))
          )
        ),

        /* 計畫（待辦）— 記錄將來要做的事 */
        tab==='todos' && React.createElement('div', { className:'rs-flex-col' },
          React.createElement('div', { className:'kai', style:{ fontSize:'var(--fs-nano)', color:'var(--ink-mute)', padding:'6px 14px 2px', letterSpacing:'.06em' } },
            '📋 記錄將來要做的功能與改進計畫'
          ),
          React.createElement('div', { className:'rs-cat-row' },
            [['今天',todayN],['本週',weekN],['已完成',doneN]].map(function(p){
              return React.createElement('button', {
                key: p[0],
                className:'rs-cat-btn'+(todoCat===p[0]?' rs-cat-on':''),
                onClick: function(){ setTodoCat(p[0]); }
              },
                React.createElement('span',{className:'kai'},p[0]),
                p[1]>0 && React.createElement('span',{className:'rs-cat-n'},p[1])
              );
            })
          ),
          React.createElement('div', { className:'rs-todo-list' },
            shownTodos.length===0 && React.createElement('div',{className:'kai rs-empty'}, todoCat==='已完成'?'尚無完成項目':'清單是空的'),
            shownTodos.map(function(todo){
              return React.createElement('div',{key:todo.id,className:'rs-todo-item', style:{flexWrap:'wrap'}},
                isCreator && React.createElement('button',{className:'rs-chk'+(todo.done?' rs-chk-done':''),onClick:function(){doneTodo(todo.id);}}, todo.done?'✓':'○'),
                !isCreator && React.createElement('span',{className:'rs-chk'+(todo.done?' rs-chk-done':''), style:{cursor:'default'}}, todo.done?'✓':'○'),
                React.createElement('div',{style:{flex:1,minWidth:0}},
                  React.createElement('span',{className:'kai rs-todo-txt'+(todo.done?' rs-done':'')}, todo.text),
                  todo.agent && React.createElement('span',{style:{
                    display:'inline-block', fontSize:'var(--fs-nano)', color:'var(--gold-deep)',
                    background:'rgba(184,146,77,0.1)', padding:'0 5px', marginLeft:5,
                    fontFamily:'Times New Roman,serif', fontStyle:'italic',
                  }}, todo.agent)
                ),
                isCreator && React.createElement('button',{className:'rs-del',onClick:function(){delTodo(todo.id);}}, '✕')
              );
            })
          ),
          isCreator && React.createElement('div', { className:'rs-add-todo' },
            React.createElement('input',{
              className:'rs-inp kai', placeholder:'新增計畫項目（Enter 確認）…',
              value: newTodo,
              onChange: function(e){ setNewTodo(e.target.value); },
              onKeyDown: function(e){ if(e.key==='Enter') addTodo(); }
            }),
            React.createElement('button',{className:'rs-add-btn',onClick:addTodo}, '＋')
          )
        ),

        /* 更新日誌（筆記）— 記錄錯誤修復與功能更新 */
        tab==='notes' && React.createElement('div', { className:'rs-flex-col' },
          React.createElement('div', { className:'kai', style:{ fontSize:'var(--fs-nano)', color:'var(--ink-mute)', padding:'6px 14px 2px', letterSpacing:'.06em' } },
            '🔧 記錄 Bug 修復、功能更新、審查結果'
          ),
          React.createElement('div', { className:'rs-note-list' },
            notes.length===0 && React.createElement('div',{className:'kai rs-empty'},'尚無更新記錄'),
            notes.slice().reverse().map(function(note){
              return React.createElement('div',{key:note.id,className:'rs-note-card'},
                React.createElement('div',{className:'rs-note-top'},
                  React.createElement('div',{style:{flex:1}},
                    React.createElement('div',{className:'kai rs-note-title'},note.title),
                    React.createElement('div',{style:{display:'flex',gap:6,marginTop:2,alignItems:'center'}},
                      note.agent && React.createElement('span',{style:{
                        fontSize:'var(--fs-nano)', color:'var(--gold-deep)', fontStyle:'italic',
                        fontFamily:'Times New Roman,serif',
                      }}, note.agent),
                      note.ts && React.createElement('span',{style:{
                        fontSize:'var(--fs-nano)', color:'var(--ink-mute)',
                      }}, note.ts)
                    )
                  ),
                  isCreator && React.createElement('button',{className:'rs-del rs-note-del',onClick:function(){delNote(note.id);}}, '✕')
                ),
                note.content && React.createElement('div',{className:'rs-note-body'},note.content)
              );
            })
          ),
          isCreator && React.createElement('div', { className:'rs-add-note' },
            React.createElement('input',{
              className:'rs-inp kai', placeholder:'標題（如：修復 histColor Bug）',
              value: noteT,
              onChange: function(e){ setNoteT(e.target.value); },
              style:{ marginBottom:6 }
            }),
            React.createElement('textarea',{
              className:'rs-inp kai', placeholder:'說明修復內容、錯誤原因、更新細節…', rows:3,
              value: noteC,
              onChange: function(e){ setNoteC(e.target.value); },
              style:{ resize:'none' }
            }),
            React.createElement('button',{className:'rs-add-btn kai',style:{width:'100%',marginTop:6,fontSize:13},onClick:addNote}, '＋ 新增更新記錄')
          )
        )

        ,

        /* 訪客紀錄（創作者限定） */
        tab==='visits' && isCreator && React.createElement('div', { style:{ padding:'0 2px' } },
          visitDays === null
            ? React.createElement('p', { style:{ color:'var(--ink-mute)', fontSize:13, textAlign:'center', marginTop:24 } }, '載入中…')
            : visitDays.length === 0
              ? React.createElement('p', { style:{ color:'var(--ink-mute)', fontSize:13, textAlign:'center', marginTop:24 } }, '尚無訪客記錄')
              : (function(){
                  var max = Math.max.apply(null, visitDays.map(function(d){ return d.uniq; })) || 1;
                  return React.createElement('div', { style:{ display:'flex', flexDirection:'column', gap:6, marginTop:8 } },
                    React.createElement('p', { style:{ fontSize:'var(--fs-nano)', color:'var(--ink-mute)', marginBottom:4 } }, '近 14 天不重複裝置（每裝置每日計 1）'),
                    visitDays.map(function(d){
                      var pct = Math.round(d.uniq / max * 100);
                      var isToday = d.date === new Date().toISOString().slice(0,10);
                      return React.createElement('div', { key: d.date, style:{ display:'flex', alignItems:'center', gap:8 } },
                        React.createElement('span', { style:{ fontSize:'var(--fs-nano)', fontFamily:'monospace', color: isToday ? 'var(--gold)' : 'var(--ink-mute)', minWidth:72 } }, d.date),
                        React.createElement('div', { style:{ flex:1, background:'var(--paper-line)', borderRadius:2, height:8 } },
                          React.createElement('div', { style:{ width: pct+'%', height:'100%', background: isToday ? 'var(--gold)' : 'var(--ink)', borderRadius:2, transition:'width .3s' } })
                        ),
                        React.createElement('span', { style:{ fontSize:12, color:'var(--ink)', minWidth:20, textAlign:'right' } }, d.uniq)
                      );
                    }),
                    React.createElement('button', {
                      className:'rs-add-btn kai',
                      style:{ width:'100%', marginTop:10, fontSize:12 },
                      onClick: function(){
                        setVisitDays(null);
                        fetch('/api/stock?type=visits')
                          .then(function(r){ return r.json(); })
                          .then(function(d){ setVisitDays(d.days || []); })
                          .catch(function(){ setVisitDays([]); });
                      }
                    }, '↻ 重新整理')
                  );
                })()
        )

      ) // end rs-body
    ) // end rs-panel
  ); // end Fragment
}

// ── 初始計畫種子（只跑一次，之後由 API 維護）──────────────────
(function seedPlans() {
  // 版本號 = 每次部署更新，讓計畫與日誌自動刷新
  var SEED_KEY = 'tw_ruflo_seeded_v107_20260723';
  window.TW_SEED_VERSION = SEED_KEY.replace('tw_ruflo_seeded_', ''); // 團隊頁 feed 顯示用
  // 自動清除所有舊版 seed key，確保新版本強制重新執行
  Object.keys(localStorage).filter(function(k){ return k.startsWith('tw_ruflo_seeded_') && k !== SEED_KEY; }).forEach(function(k){ localStorage.removeItem(k); });
  if (localStorage.getItem(SEED_KEY)) return;
  var t = Date.now();
  // 保留使用者透過 API 手動新增的任務（沒有 seeded 標記的）
  var _existing = [];
  try { _existing = JSON.parse(localStorage.getItem('tw_ruflo_todos') || '[]'); } catch(_) {}
  var _userAdded = _existing.filter(function(item) { return !item.seeded; });
  var todos = [];
  var plans = [
    // ── QuoteHub v2 全站市價自動更新迴圈（設計文件 QUOTE_LOOP.md）──
    { text:'[engineering-lead] QuoteHub v2 規劃完成：單一主迴圈+訂閱制+安全七鐵律+專員監控分工 → QUOTE_LOOP.md', cat:'本週', done: true },
    { text:'[engineering-lead] QuoteHub v2 Step1：shared.jsx 主迴圈 + subscribe API + health 物件（與 v1 並存）', cat:'本週', done: true },
    { text:'[engineering-lead] QuoteHub v2 Step2：時段感知(台/美/加密)+失敗退避+髒資料驗證+stale標示', cat:'本週', done: true },
    { text:'[frontend-lead] QuoteHub v2 Step3：11 頁全遷移 subscribe（TickerTape/大盤/產業地圖/自選/個股/ETF/當沖/期貨/警示/美股/加密），alerts 逐檔改batch、期貨夜盤時段、v1心跳退役', cat:'本週', done: true },
    { text:'[data-lead] QuoteHub v2 Step4：Masthead 🟢🟡🔴 狀態燈(hover看各type健康度)+qh:error事件+紅燈console提示查type=health', cat:'本週', done: true },
    // ── 已完成 ──────────────────────────────────────────────────
    { text:'[quant-lead] 修正 MACD 評分：加入黃金/死叉交叉加權', cat:'本週', done: true },
    { text:'[quant-lead] RSI 改分段評分、背離偵測', cat:'本週', done: true },
    { text:'[chart-specialist] Bollinger Band + ATR 波動率', cat:'本週', done: true },
    { text:'[quant-lead] 支撐壓力改用 Pivot Point（60日）', cat:'本週', done: true },
    { text:'[tw-stock-analyst] 法人連續N日同向偵測', cat:'本週', done: true },
    { text:'[chart-specialist] K線圖 EMA 20/50/100/200', cat:'本週', done: true },
    { text:'[chart-specialist] MACD 4色直方圖（CM_MacD_Ult_MTF）', cat:'本週', done: true },
    { text:'[frontend-lead] 個股技術分析 + 企業基本面合併頁面（tab）', cat:'本週', done: true },
    { text:'[ui-coder] 合併頁面設計修正（移除重複 header）', cat:'本週', done: true },
    { text:'[ui-coder] 同業比較批次 P/E / P/B / 殖利率', cat:'本週', done: true },
    { text:'[kline-specialist] lightweight-charts 升級替換手刻 SVG K線（縮放+觸控+跨面板十字線）', cat:'本週', done: true },
    { text:'[frontend-lead] 新增 E · 量化交易部（訊號師/SMC分析師/部位管理師）、前端工坊拆分 kline+indicator 專員', cat:'本週', done: true },
    { text:'[data-lead] 季損益/資產負債/現金流量 棄用 Yahoo Finance，改用 FinMind 免費 API', cat:'本週', done: true },
    { text:'[ui-coder] Section 05 改為「投資者上看潛力」，串接 FMP 機構分析師共識目標價（台幣換算）', cat:'本週', done: true },
    // ── 待辦 ────────────────────────────────────────────────────
    { text:'[indicator-specialist] EPS 圖加去年同季對比虛線', cat:'本週', done: true },
    { text:'[backtester] 基本面 P/E、EPS YoY 納入評分權重', cat:'本週', done: true },
    { text:'[kline-specialist] K線圖加除息/財報事件 D/E 標記', cat:'本週', done: true },
    { text:'[data-validator] 基本面無資料時顯示更友善的提示', cat:'本週', done: true },
    { text:'[smc-analyst] 移植 joshyattridge/smc OB/BOS/FVG → signal.jsx SignalPanel', cat:'本週', done: true },
    { text:'[indicator-specialist] MACD 黃金/死亡交叉 + 多空背離策略 → signal.jsx MACDStrategyPanel', cat:'本週', done: true },
    { text:'[ui-coder] RufloSidebar 創作者/訪客權限分離（PIN 鎖 + 專員互動面板）', cat:'本週', done: true },
    // ── 優先任務（本週需完成）────────────────────────────────
    { text:'[indicator-specialist] 黃金交叉雷達 scan.jsx × MACD 進出場策略同步：展開列顯示明確進場價 / 出場目標 / 停損價', cat:'本週', done: true },
    { text:'[indicator-specialist] MACDStrategyPanel 加入明確數字：進場價（均線/OB）、T1出場、T2出場、停損（不要區間，要明確價位）', cat:'本週', done: true },
    { text:'[kline-specialist] chart-pro.jsx 修復即時 K 棒：將 TWSE MIS 即時報價合併為今日未完成 bar 加入圖表末端', cat:'本週', done: true },
    // ── 持續監控任務 ─────────────────────────────────────────
    { text:'[quant-lead] 統籌 scan.jsx + signal.jsx 整合架構設計，確保資料流一致', cat:'本週', done: true },
    { text:'[data-lead] stock.js 新增 FMP 五個美股財務端點（fmp_income/ratios/key_metrics/earnings/dividends）', cat:'本週', done: true },
    { text:'[ui-coder] 美股個股頁加入 FMP 四大區塊：季報圖表/財務比率/葛拉漢估值/財報日曆', cat:'本週', done: true },
    { text:'[frontend-lead] 美股總覽財報行事曆升級 FMP 即時（10支主力股 beat/miss 追蹤）', cat:'本週', done: true },
    { text:'[indicator-specialist] fundamental.jsx Section 05 加 EPS 驚喜記錄 / Section 08 加 EBITDA + Graham Number', cat:'本週', done: true },
    { text:'[data-lead] 擴充 TW_ADR_MAP：聯電 2303/中華電 2412/日月光 3711', cat:'本週', done: true },
    { text:'[indicator-specialist] 監控 MACD 策略績效：記錄黃金交叉後 N 根勝率，調整背離回看參數（現 40 根）', cat:'本週', done: true },
    { text:'[indicator-specialist] 研究 pandas-ta KDJ，評估加入進出場輔助確認', cat:'本週', done: true },
    { text:'[portfolio-manager] 研究 microsoft/qlib，設計 Kelly 公式部位計算模組', cat:'本週', done: true },
    // ── 2026-06-05 新增 ──────────────────────────────────────
    { text:'[kline-specialist] K線圖 MACD 進出場標記：黃金交叉▲買/死亡交叉▼賣標點在蠟燭圖上', cat:'本週', done: true },
    { text:'[kline-specialist] K線圖新增日/週/月/季切換（OHLCV 客戶端聚合）', cat:'本週', done: true },
    { text:'[ui-coder] Section 05 估值改為四方法：Graham Number / Lynch PEG / 5年DCF / Forward P/E（市場法）', cat:'本週', done: true },
    { text:'[data-lead] FMP 新增 {code}.TW 路徑：台幣直接目標價，支援無ADR台股', cat:'本週', done: true },
    { text:'[data-lead] 修復同業排名：company-db.js 未載入個股頁，補加 script tag', cat:'本週', done: true },
    { text:'[data-lead] 修復股利記錄：parseDividends 欄位名稱錯誤（cash_dividend → CashEarningsDistribution）', cat:'本週', done: true },
    { text:'[data-lead] 修復法人籌碼：fetchChipSummary 比對中文名稱，FinMind 已改英文（Foreign_Investor/Investment_Trust）', cat:'本週', done: true },
    { text:'[data-lead] 修正 TW_ADR_MAP：移除錯誤 2882→IMOS，改正 8150→IMOS，新增 2409→AUO、3697→HIMX', cat:'本週', done: true },
    { text:'[ui-coder] 估值新增分析師評等升降紀錄（FMP grades-latest，近5筆）', cat:'本週', done: true },
    { text:'[ui-coder] 導覽列移除「籌碼雷達」（功能已在大盤總覽），警示設定保留', cat:'本週', done: true },
    { text:'[kline-specialist] K線圖均線：週月季年四條 SMA 合為一個 chip 開關', cat:'本週', done: true },
    { text:'[data-lead] 美股即時數據：FMP batch quote（sectors/movers）+ 盤前盤後', cat:'本週', done: true },
    { text:'[data-lead] 擴充 FMP .TW 覆蓋率調查：哪些台股有機構分析師目標價', cat:'本週', done: true },
    // ── 2026-06-05 午後 ──────────────────────────────────────
    { text:'[data-lead] 修復 us_chart_meta 三層 fallback（FMP→Yahoo chart→Yahoo v7/quote），price=0 不快取', cat:'本週', done: true },
    { text:'[data-lead] us_intraday_fmp 加入 Yahoo Finance fallback（FMP 免費版不含日內資料）', cat:'本週', done: true },
    { text:'[kline-specialist] 美股 K 線圖 range buttons 移入 ChartPro 工具列（chartpro.jsx rangeTabs prop），仿台股排版', cat:'本週', done: true },
    { text:'[kline-specialist] chart-pro.jsx 新增 rangeTabs prop：外部傳入時取代 TF_PRESETS 按鈕', cat:'本週', done: true },
    { text:'[kline-specialist] 修復美股個股頁缺少 chart-pro.css 引入，cp-toolbar 排版崩潰', cat:'本週', done: true },
    { text:'[data-lead] chart-pro.jsx exchange 欄位改動態（stock?.exchange || TWSE），美股顯示 NYSE/NASDAQ', cat:'本週', done: true },
    // ── 2026-06-05 隊員新增 ──────────────────────────────────
    // A · 數據中台
    { text:'[data-lead] 確認 stock.js 所有 API 端點回應時間 < 2s', cat:'本週', done: true },
    { text:'[data-lead] KV 快取策略：熱門股加預熱 warmup', cat:'本週', done: true },
    { text:'[data-lead] health check 增加 FinMind / Yahoo Finance 可用性監控', cat:'本週', done: true },
    { text:'[data-validator] 月營收 MOPS 爬取異常偵測', cat:'本週', done: true },
    { text:'[data-pipeline] 建立 FinMind 批次抓取排程，降低重複呼叫', cat:'本週', done: true },
    { text:'[data-pipeline] 評估 Cloudflare KV 存放季報資料，降低 API 依賴', cat:'本週', done: true },
    { text:'[data-pipeline] 研究 MOPS 官方 OpenAPI 替代 HTML 爬取', cat:'本週', done: true },
    { text:'[news-sentiment-analyst] RSS 快取策略：避免每次重新抓取', cat:'本週', done: true },
    { text:'[news-sentiment-analyst] 新聞情緒分類加入正負面關鍵詞過濾', cat:'本週', done: true },
    // B · 設計部
    { text:'[design-lead] Formosa & Co. 設計系統文件化（color token / typography scale）', cat:'本週', done: true },
    { text:'[design-lead] 審查各頁面品牌一致性（字體、間距、金色用法）', cat:'本週', done: true },
    { text:'[ui-designer] styles.css 整理：消除重複 class，統一命名規則', cat:'本週', done: true },
    { text:'[icon-artist] node-icons.jsx 補充 AI/雲端/金融科技類圖示', cat:'本週', done: true },
    { text:'[icon-artist] 圖示一致性審查：各頁面 icon 風格統一', cat:'本週', done: true },
    { text:'[qa-lead] 全站三線體檢＋P0安全修補＋效能優化（stock_bundle/production React，2026-07-18）', cat:'本週', done: true },
    // C · 工程部
    { text:'[engineering-lead] NavBar 加入頁面活躍狀態追蹤', cat:'本週', done: true },
    { text:'[frontend-engineer] 同業比較表加入股價走勢迷你圖', cat:'本週', done: true },
    { text:'[frontend-engineer] 基本面頁面改善：無資料顯示支援訊息', cat:'本週', done: true },
    { text:'[frontend-engineer] 企業基本面 05 投資者上看潛力 — ADR 換算說明優化', cat:'本週', done: true },
    { text:'[kline-specialist] 研究 lightweight-charts Price Line 動態標記 API', cat:'本週', done: true },
    { text:'[indicator-specialist] RSI + MACD 雙重背離確認，降低假訊號率', cat:'本週', done: true },
    { text:'[indicator-specialist] 背離回看參數 40 根改動態（ATR 波動率決定）', cat:'本週', done: true },
    // D · 策略研究室
    { text:'[quant-lead] 黃金交叉雷達加入「MACD 進出場建議」展開列', cat:'本週', done: true },
    { text:'[industry-analyst] 台股產業流域圖 — 加入 AI 相關族群', cat:'本週', done: true },
    { text:'[industry-analyst] AI / 半導體題材輪動追蹤', cat:'本週', done: true },
    // E · 品保部
    { text:'[qa-lead] 部署前跑 lighthouse 效能評分', cat:'本週' },
    { text:'[qa-lead] API 回應格式統一化審查（error schema）', cat:'本週', done: true },
    // UX
    { text:'[ux-researcher] 黃金交叉頁面使用流程優化：從搜尋到 TradingView 一氣呵成', cat:'本週', done: true },
    // ── 2026-06-08 智慧選股雷達（多因子篩選升級）────────────────
    { text:'[stock-picker] 設計投資/投機雙模式評分矩陣：各因子權重 + 閾值定義（0-100 複合分）', cat:'下週', done: true },
    { text:'[data-pipeline] 批次拉取全市場月營收 YoY + 三大法人連買天數，組成選股資料集', cat:'下週', done: true },
    { text:'[indicator-specialist] 實作突破均線放量1.5x偵測 + 外資3日翻買偵測（screener.jsx）', cat:'下週', done: true },
    { text:'[quant-lead] 統籌 screener.jsx 升級架構：投資/投機 Tab + 多因子批次評分流程', cat:'下週', done: true },
    { text:'[frontend-engineer] screener.jsx UI：雙模式 Tab + 評分卡片 + 排名榜（依分數排序）', cat:'下週', done: true },
    { text:'[risk-manager] 投機模式每檔加入停損建議價（前低或 -7%）', cat:'下週', done: true },
    // ── 2026-06-08 選股工坊整合 ──────────────────────────────────
    { text:'[frontend-engineer] 合併多因子篩選.html + 策略中心.html → 選股工坊.html（stock-lab.jsx，三Tab統一頁）', cat:'下週', done: true },
    { text:'[quant-lead] stock-lab.jsx 統一資料載入：單次 OHLCV 批次同時計算 candles + MA/RSI/MACD 指標', cat:'下週', done: true },
    { text:'[data-lead] 建立 FinMind 批次抓取排程，降低重複呼叫', cat:'下週' },
    { text:'[design-lead] Formosa & Co. 設計系統文件化（color token / typography scale）', cat:'下週' },
    { text:'[icon-artist] 圖示一致性審查：各頁面 icon 風格統一', cat:'下週' },
    { text:'[qa-lead] 部署前跑 lighthouse 效能評分', cat:'下週' },
    // ── 2026-06-11 設計升級層 v2 ──────────────────────────────
    { text:'[design-lead] 設計升級層 v2：金箔微細節（選取/捲軸/焦點/Masthead燙金/kicker金線/表格金邊書籤/stat金刻度/ticker淡出）', cat:'本週', done: true },
    // ── 2026-06-11 互動升級層 v2.5 ───────────────────────────
    { text:'[ui-designer] 滾動進場：IO+MutationObserver，捲到視窗才浮現（取代載入即播）', cat:'本週', done: true },
    { text:'[ui-designer] 數字滾動全站化：stat-cell/big-num 首次進入視窗 0→目標值', cat:'本週', done: true },
    { text:'[ui-designer] 墨水風 tooltip [data-tip] 通用元件，Masthead 按鈕套用', cat:'本週', done: true },
    { text:'[engineering-lead] Command Palette ⌘K：頁面跳轉+代號直達+公司搜尋', cat:'本週', done: true },
    { text:'[ui-designer] Ticker LIVE 呼吸點 + 項目可點擊跳轉', cat:'本週', done: true },
    { text:'[kline-specialist] K線拖曳測量工具：⊿按鈕開啟，拖出區間顯示價差/%/K棒數', cat:'本週', done: true },
    { text:'[ui-designer] 企業基本面美化層：數字回歸襯線、印章式估值結論、首字下沉、表格金邊書籤、11 section 滾動進場', cat:'本週', done: true },
    { text:'[ui-designer] 月營收金幣疊柱體（YoY正=金幣/衰退=銅幣/最新月金圈）+ 季損益鈔票疊柱體（虧損下疊銅紅鈔）', cat:'本週', done: true },
    { text:'[icon-artist] 產業地圖流域升級第一波：河道化+金沙粒子+捲軸進場+城鎮旗點燈+山海地形底圖', cat:'本週', done: true },
    { text:'[icon-artist] 產業地圖第二波：鏡頭 pan/zoom 聚焦 + 市況旗（節點代表股即時漲跌）', cat:'本週', done: true },
    { text:'[quant-lead] 五力全市場掃描：calcLiteScore 技術五力（趨勢25+動能20+中性22=67pts）批次跑411檔 → 選股工坊 Tab 5「五力排行榜」，Minervini趨勢樣板+IBD RS α+MACD+RSI，TWII基準，跌破年線封頂54，含強多/趨勢完美篩選', cat:'本週', done: true },
    // ── 2026-06-11 UI 套件 + QuoteHub ────────────────────────
    { text:'[engineering-lead] 全站注入 UI 套件 CDN+SRI：anime.js/Lucide 全頁、Swiper/ECharts 重點頁', cat:'本週', done: true },
    { text:'[ui-coder] 現金流量表改 ECharts 三序列分組柱（營業CF/自由CF/資本支出）', cat:'本週', done: true },
    { text:'[ui-coder] 法人籌碼改 ECharts 堆疊柱（外資/投信/自營商）+ 合計折線，近20交易日', cat:'本週', done: true },
    { text:'[engineering-lead] QuoteHub 全站報價中樞：fetch攔截+同頁去重+sessionStorage跨頁快取+20s心跳刷新+Masthead報價時間戳', cat:'本週', done: true },
    { text:'[engineering-lead] 全站報價刷新鈕：Masthead ↻ + qh:refresh 事件廣播，大盤(指數/熱力/籌碼/排行)+產業地圖+個股+基本面同步重抓', cat:'本週', done: true },
    // ── 2026-06-11 五力評分 v3 ───────────────────────────────
    { text:'[quant-lead] 個股評分改五力 v3：Minervini趨勢樣板25 + IBD相對動能20(α vs TWII) + 法人籌碼15 + Piotroski品質20 + Graham估值20', cat:'本週', done: true },
    { text:'[quant-lead] 風險旗標封頂機制：Z<1.8/跌破年線→鎖中性，RSI頂背離/外資連5賣→鎖偏多', cat:'本週', done: true },
    { text:'[ui-coder] ScoreMatrix 五力雷達圖(ECharts)+準則chips+旗標badge；us_quote API 開放^指數代號', cat:'本週', done: true },
    // ── 2026-06-11 基本面升級+釋義 ──────────────────────────
    { text:'[ui-coder] 基本面12 section 全加「釋義」教育層：是什麼/怎麼看/留意 三段式說明', cat:'本週', done: true },
    { text:'[quant-lead] 財務比率區加三率走勢圖(ECharts)+三率三升/齊降偵測badge+杜邦分析(ROE=淨利率×週轉×槓桿,近4季vs前4季)', cat:'本週', done: true },
    // ── 2026-06-12 EPS+同業升級 ─────────────────────────────
    { text:'[ui-coder] EPS 專屬 ECharts 圖：年增綠/年減紅柱+去年同季金虛線+YoY%右軸折線+TTM/連N季年增統計', cat:'本週', done: true },
    { text:'[ui-coder] 同業比較升級：中位數摘要列(P/E/P/B/殖利率+本檔溢折價chips)+5日漲跌%+P/E×P/B估值地圖散佈圖', cat:'本週', done: true },
    { text:'[industry-analyst] 產業地圖擴充8產業：被動元件/PCB/網通(主流)+生技醫療/軍工航太/綠能儲能(新興)+紡織成衣/食品餐飲(傳統)，12→20產業411家公司', cat:'本週', done: true },
    { text:'[design-lead] 產業地圖雙風格切換：經濟學人資訊圖表風(紅頂線+圓面積=市值+漲幅紅點+Source落款)/水墨流域風，localStorage記憶偏好', cat:'本週', done: true },
    { text:'[icon-artist] 流域圖鑑生態升級：蝴蝶拍翅沿曲線飛舞×2+白鷺鷥滑翔+魚群洄游最寬河道+雲霧飄山巒；玻璃感節點(半透明+高光+hover掃光)', cat:'本週', done: true },
    { text:'[design-lead] 寶島聚落第三模式：台灣島SVG+8聚落(竹科/台中精機/高雄重工…)呼吸金點+產業chips點選跳轉流域圖', cat:'本週', done: true },
    // ── 2026-06-12 個股頁基本面真實化 ───────────────────────
    { text:'[data-validator] 個股頁基本面卡真實化：bwibbu即時P/E·P/B·殖利率 + 季報TTM算EPS/ROE/營收年增/盈餘年增（原為mock fallback），8Q EPS圖改真實季報+年增綠紅柱+去年同季金虛線+YoY%標註+資料來源footnote', cat:'本週', done: true },
    { text:'[ui-designer] 三大法人籌碼圖改硬幣疊：外資墨幣/投信金幣/自營鋼藍幣，買超零軸上疊/賣超下疊，端面亮色+鑄紋，hover墨卡顯示三法人數值', cat:'本週', done: true },
    { text:'[quant-lead] 基本面 Section 00 企業段位總評：五段位印章(夯/頂級/人上人/NPC/拉完了，成長30+獲利25+體質20+估值15+籌碼10) + 15項健檢紅綠燈 + 同業PR值 + 本益比河流圖(2年PER百分位五色帶)', cat:'本週', done: true },
    { text:'[frontend-lead] 個股頁搜尋修復：證券分類(個股/ETF/特別股/TDR)，個股優先排序(代號完全符合>開頭>名稱)，ETF墊後+標籤，取8筆——原本ETF排前面把個股洗出前6筆', cat:'本週', done: true },
    { text:'[quant-lead] 基本面 Section M 企業護城河：量化四痕跡(獲利持續力30/定價權30/現金製造力20/資本效率20)判寬/窄/無護城河印章 + 五型護城河痕跡比對(純規則，無AI：無形資產/轉換成本/成本優勢/規模效率/網絡效應)', cat:'本週', done: true },
    { text:'[industry-analyst] 技術版圖資料庫 tech-moat.js：20檔科技主力股技術平台拆解(如台積電 Logic/Advanced Packaging/Silicon Photonics)，領先/同步/追趕三級+對手清單，護城河區優先顯示，未建檔fallback五型比對；後續補聯電DTC+10檔上櫃(信驊/群聯/祥碩/力旺/譜瑞/元太/世界/家登/精測/中美晶)', cat:'本週', done: true },
    { text:'[data-lead] 修復上櫃清單：TPEx dailyQuotes 改版 aaData→tables[] 動態欄位解析 + OpenAPI fallback 換瀏覽器UA(原被重導/errors循環)，過濾權證，個股分析搜尋恢復上櫃899檔', cat:'本週', done: true },
    { text:'[data-lead] 分析師共識全覆蓋：analyst_targets 加 TradingView Scanner fallback(上市櫃全有,FMP僅ADR股)，估值區新增共識卡：目標均價+上檔%+機構評級+高低區間bar(現價▾/均價金線)', cat:'本週', done: true },
    { text:'[frontend-lead] 個股分析市場分類標示：搜尋下拉+頁頭顯示上市(墨框)/上櫃(金框)/ETF標籤，fullList保留market欄位', cat:'本週', done: true },
    { text:'[data-lead] 上櫃清單三層降級：TPEx間歇封鎖CF節點IP → 成功存KV 7天 → 失敗讀KV最後成功版 → FinMind TaiwanStockInfo最終備援(無價格但可搜)，雙鴻3324全節點可搜', cat:'本週', done: true },
    { text:'[data-lead] 系統基礎設施升級·資料源韌性層：通用 kvLastGood(成功存KV最後成功版/失敗回放) 套用 list/bwibbu/bwibbu_batch/analyst_targets，health 端點擴充 tpex_quotes/tpex_openapi/tradingview 三檢查可判定節點封鎖', cat:'本週', done: true },
    { text:'[quant-lead] 專員擴編：D部新增 q10 基本面研究員(段位/護城河/健檢/估值調校) + q11 技術版圖策展員(tech-moat.js法說會維護)，d2 數據驗證師職責更新為韌性層維護', cat:'本週', done: true },
    { text:'[frontend-engineer] 自選股交易記錄升級四件套：配息記錄divLegs(除息日/每股股利/自動算入帳+含息浮動) + 含稅費損益平衡價break-even + 停損停利價設定(觸價亮燈⚠/◎+距離%) + CSV匯出(買賣配息明細含BOM)', cat:'本週', done: true },
    { text:'[frontend-engineer] 費用計算改元大證券標準：零股(<1000股)手續費低消$1(整股$20)、證交稅三選一(股票0.3%/ETF 0.1%/現股當沖0.15%)、無條件捨去；isETF重構為taxRate，break-even同步', cat:'本週', done: true },
    { text:'[ui-designer] 自選股美學升級層v2·手寫帳冊風：倉位卡左緣硃砂帳線+頭列金線舒展、展開區橫格帳冊紙+帳頁翻開動畫、已清倉硃砂「結清」騎縫章、KPI金刻度浮起、表格金邊書籤、費率列雙細線天頭、wl-card懸停❦花飾', cat:'本週', done: true },
    { text:'[frontend-engineer] 交易績效未平倉損益追蹤修復：TradePerf 直抓倉位代號 TWSE MIS 即時報價(peer_prices, 30s刷新)蓋過收盤清單——原本倉位不在自選清單/上櫃備援清單無價格時浮動損益空白', cat:'本週', done: true },
    { text:'[frontend-lead] 自選股全面重設計v3「持股帳房」：單一資料模型(觀察/持有/結清一筆倉位,tw_portfolio_v3自動遷移舊雙系統)，總帳Hero(市值/今日/浮動/已實現/股利)+快速加入(觀察/建倉)+持有中帳冊卡(現價/保本/停損利/配息)+持股配置帶+觀察名單表+結算績效(KPI含息/月度P&L/結清帳目表/CSV)，紅漲綠跌全頁統一', cat:'本週', done: true },
    // ── 2026-06-13 美股總覽 ─────────────────────────────
    { text:'[quant-lead] 美股總覽新增「總統週期羅盤」：四年週期定位(選後/期中/選前/大選年平均報酬)+期中年12月平均路徑SVG(開局震盪/4-9月低谷/9-10月落底窗/Q4起漲四階段)+現在位置金標+期中選舉倒數；純日期規則無API，統計源 Stock Trader\'s Almanac 1950–2024(道瓊1924以來型態一致)', cat:'本週', done: true },
    { text:'[quant-lead] 週期羅盤升級四線合成圖：所有期中年(墨)/總統第6年(紅)/共和黨第2任期中年(綠,2026三重命中,上半年偏強9月+1%低點年底+8%)/STA綜合週期(金虛線)+圖例；主檔us-dashboard.jsx(大盤總覽?market=us)+舊美股總覽.html同步', cat:'本週', done: true },
    // ── 2026-06-19 路線圖完成批 ─────────────────────────
    { text:'[fundamental-researcher] 企業段位五維權重回測：段位 vs 後續3月報酬驗證', cat:'本週', done: true },
    { text:'[tech-moat-curator] tech-moat.js 擴檔：達發/穎崴/雍智/M31，status 升降規則文件化', cat:'本週', done: true },
    { text:'[quant-lead] 回測引擎升級：自訂 MA/MACD 週期、停損%、權益曲線、最大回撤（BacktestEngine 元件）', cat:'本週', done: true },
    { text:'[quant-lead] MACD 直方圖篩選器 Step 3/4：aqua/blue/maroon/red 四色 histColor 欄位 + UI 篩選列 + power-confirm preset 同步', cat:'本週', done: true },
    // ── 2026-06-22 美股升級：補齊台股進階功能 ───────────────
    { text:'[fundamental-researcher] 美股個股頁新增企業段位總評：五段位印章(夯/頂級/人上人/NPC/拉完了，成長30+獲利25+體質20+估值15+分析師動向10) + 14項健檢紅綠燈，移植自台股fundamental.jsx computeTier，改用FMP季報/比率/股利/分析師共識資料', cat:'本週', done: true },
    { text:'[fundamental-researcher] 美股個股頁新增企業護城河：量化四痕跡(獲利持續力30/定價權30/現金製造力20/資本效率20)判寬/窄/無護城河印章，移植自台股computeMoat，改用FMP季報數據', cat:'本週', done: true },
    { text:'[quant-lead] 美股總覽新增五力排行榜：對60檔主力美股批次抓Yahoo us_quote 2年日K，calcLiteScore技術五力(趨勢樣板25+RS/MACD/RSI動能20+中性22，跌破年線封頂54)，基準S&P500(^GSPC)，移植自台股stock-lab.jsx，按鈕觸發避免每次進站打API', cat:'本週', done: true },
    // ── 2026-06-25 美股全面健檢：API修復+市況旗 ──────────────
    { text:'[data-lead] 修復美股個股頁財報六大區塊全滅：FMP income/balance-sheet/cash-flow-statement v3端點下架(403)，stable端點轉付費(402)，加SEC EDGAR companyfacts免費備援(企業段位/護城河/健康評分/季損益/資產負債/現金流全部復活，財年末季因10-K不單獨標記留空屬已知限制)', cat:'本週', done: true },
    { text:'[data-lead] 新增 us_batch_quote 端點：任意1-60檔美股代號批次即時報價(Yahoo chart per ticker)', cat:'本週', done: true },
    { text:'[frontend-engineer] 修復美股產業地圖市況旗：priceMap從未傳入IndustryMap元件，節點代表股漲跌與側欄報價皆為寫死靜態數字，改為切換產業時批次抓即時報價覆蓋', cat:'本週', done: true },
    { text:'[data-validator] fmp_institutional 機構持股同步嘗試stable端點，SEC無對應免費資料源，失敗時維持優雅降級(顯示「暫無機構持股資料」不影響其他區塊)', cat:'本週', done: true },
    { text:'[qa-lead] 美股5頁與台股對應頁面視覺比對(dashboard/stock/industry-map/watchlist)：Masthead大標題/配色/排版結構已一致，無需大改', cat:'本週', done: true },
    // ── 2026-06-25 美股個股基本面對齊台股豐富度 ──────────────
    { text:'[ui-coder] 美股個股頁載入ECharts，移植EPS專屬圖(年增配色柱+去年同季金虛線+YoY%折線+TTM+連N季年增徽章)', cat:'本週', done: true },
    { text:'[quant-lead] 美股現金流量表改ECharts三序列分組柱(Operating/Free/CapEx)，財務比率新增三率走勢線+三率三升/齊降徽章+杜邦分析(ROE=淨利率×週轉率×槓桿)', cat:'本週', done: true },
    { text:'[data-lead] 新增fmp_profile端點+企業簡介區塊(產業分類/員工數/CEO/描述)', cat:'本週', done: true },
    { text:'[data-lead] 修復同業比較P/E與市值永遠顯示「—」：us_quote的Yahoo chart meta無trailingPE/marketCap欄位，改用us_chart_meta(FMP stable quote+ratios-ttm)，加中位數摘要chip+P/E比較柱圖', cat:'本週', done: true },
    { text:'[qa-lead] 修復CashFlowSection/RatiosSection違反React Hooks規則(useRef/useEffect寫在early return之後)導致渲染警告', cat:'本週', done: true },
    // ── 2026-06-25 台股個股數據失效修復 ──────────────────────
    { text:'[data-lead] 修復realtime/peer_prices：TWSE MIS對CF節點IP間歇封鎖(本機curl正常/Worker打520)，加Yahoo Finance chart備援(.TW/.TWO)+kvLastGood回放', cat:'本週', done: true },
    { text:'[data-lead] chip_summary/chipboard加kvLastGood：FinMind匿名IP共享額度打滿時402無等價免費備援，至少回放最後成功版', cat:'本週', done: true },
    // ── 2026-06-25 大盤指數/產業熱力即時化 ───────────────────
    { text:'[frontend-lead] 修復大盤總覽指數卡大標題數字不跟即時報價更新：原本只取90日歷史最後收盤(載入時抓一次不再更新)，改優先用liveIndex即時報價', cat:'本週', done: true },
    { text:'[data-lead] 新增sector_quotes端點：批次抓20產業62檔成分股MIS即時報價(沿用fetchBatchRealtime+Yahoo備援+kvLastGood)，產業熱力改盤中即時(原本依賴EOD收盤資料集，盤中不會動)', cat:'本週', done: true },
    // ── 2026-06-25 期貨總覽新頁面 ─────────────────────────────
    { text:'[data-lead] 研究TAIFEX免費資料源：發現mis.taifex.com.tw/futures/api/getQuoteList為免金鑰HTTP POST快照查詢(不需WebSocket/Durable Objects)，新增futures_quote端點，三組(台指期系列/其他指數期貨/個股期貨)+kvLastGood韌性層', cat:'本週', done: true },
    { text:'[frontend-lead] 新增期貨.html+futures.jsx：台指期/小台指/微台指/中型100、電子/永續/東證/道瓊/那斯達克/費半、台積電等9檔個股期貨熱門榜三大區塊卡片，NavBar加期貨入口', cat:'本週', done: true },
    { text:'[data-lead] 修復期貨請求量控管：新增kvFreshOrFetch 45s共享冷卻快取，原本每訪客每20s心跳各自打TAIFEX(3組19個CID查詢)無冷卻可能是間歇520元凶，前端輪詢也脫鉤改獨立60s', cat:'本週', done: true },
    { text:'[qa-lead] 持續觀察：TAIFEX跟TWSE同款CF節點間歇520，無等價免費備援(Yahoo不收台灣期貨)，加完冷卻窗後待觀察是否降低封鎖頻率', cat:'本週' },
    // ── 2026-07-19 雷達區大改版：策略實驗室四期藍圖（docs/superpowers/specs）──
    { text:'[quant-lead] 策略實驗室 Phase 1 地基：quant-lib.js 共用量化模組（指標+trade/event雙回測引擎，與舊 backtest.jsx 逐筆等價驗證通過）＋strategies.js 策略庫（5策略+教學metadata+paramSpace）＋stock.js tw_history 多年日線 API（Yahoo 5y，KV 7天）', cat:'本週', done: true },
    { text:'[frontend-lead] 策略實驗室 Phase 2：雷達區第6分頁——策略書架(直排書封卡/風險金印/硃砂慎章)+回測工作台(paramSpace表單/瀏覽器chunked尋優可中止)+成果版面(echarts權益+回撤雙圖/KPI/交易表/參數尋優榜點擊套用)+lab-icons.jsx 手繪插畫+投資課三帖+AI大白話點綴；獨立頁策略實驗室.html；E2E六步驟全過', cat:'本週', done: true },
    { text:'[data-lead] 策略實驗室 Phase 3：type=lab_daily 每日自動掃描（board批次續傳：lab-progress/lab-batch-lock KV+waitUntil每批5檔+昨日墊檔；Functions 副作用 import quant-lib/strategies 走 globalThis）＋前端今日訊號橫條＋q2/e1 部門建議掛鉤＋shechdule cron-worker 每日午夜連打保底', cat:'本週', done: true },
    { text:'[quant-lead] 策略實驗室 Phase 4：scripts/optimize-strategies.mjs walk-forward 尋優（moat10×5y，前70%IS/後30%OOS）＋optimize-strategy skill＋STRATEGY_LOG＋首輪實跑：4策略樣本外過關回寫best（macd/pullback/rsi/smc），ma60_breakout 樣本外虧損依護欄拒絕——四期藍圖全部結案', cat:'本週', done: true },
    // ── 2026-07-19 雷達區→完整量化平台四期藍圖（spec: docs/superpowers/specs/2026-07-19-quant-platform-design.md）──
    { text:'[design-lead] 量化平台 Phase 1 地基：--fs-* 九級字階+--term-* 終端面板token（雜誌×終端混血，紅漲綠跌限.qd-term）+quant-desk.css 元件庫+jsx-loader 隨選編譯（首屏 Babel 340KB→150KB）+六分頁字級放大token收斂+六tab逐切E2E', cat:'本週', done: true },
    { text:'[data-lead] 量化平台 Phase 2：D1 signal_history/brain_verdicts 訊號史庫+type=brain 金融大腦（lab五力+board裁決+軋空+TAIEX MA20 確定性彙總零新增AI，composite=30/30/25/15）+signal_backfill 回填(滿15天,批≤8碼)+signal_stats+cron 鏈尾 brain×1/backfill×2；本地與正式站雙驗證，D1 開始累積', cat:'本週', done: true },
    { text:'[frontend-lead] 量化平台 Phase 3：研判中樞分頁 desk 上線為雷達區預設頁（終端hero mega金字+研判大印+echarts信心儀表+34檔訊號燈牆+tile五力雷達詳情+命中率追蹤表/熱力日曆含冷啟動空狀態+方法論紙張收尾）+echarts-formosa 雙主題+獨立頁研判中樞.html；七tab E2E 全綠 desk 首屏1.6s', cat:'本週', done: true },
    { text:'[frontend-lead] 量化平台 Phase 4：AI金融團隊分頁 The Firm 上線（雷達區第8 tab+獨立頁AI金融團隊.html；七部門卡片牆+7位部門長手繪半身像職業道具+42專員monogram roster+AGENT_ADVICE側卡+工作區深連結+每日產出登記簿終端面板 feed=brain確定性映射零AI）——量化平台四期藍圖全部結案', cat:'本週', done: true },
    { text:'[frontend-lead] 雷達區分頁瘦身 8→4（使用者回饋分頁太多）：研判中樞(收編機構覆蓋展開段+The Firm入口卡)/訊號掃描(黃金交叉◉選股工坊○ pill)/籌碼風向(券資比+三大法人堆疊)/策略實驗室；舊 tab 參數 scan/stocklab/margin/chip/coverage/team 全相容轉址；E2E 12 項全過', cat:'本週', done: true },
  ];
  plans.forEach(function(p, i) {
    todos.push({ id: t + i, text: p.text, cat: p.cat, done: p.done || false, agent: 'tw-stock-analyst', seeded: true });
  });
  // 將使用者手動新增的任務附加在末尾
  todos = todos.concat(_userAdded);
  localStorage.setItem('tw_ruflo_todos', JSON.stringify(todos));
  // 種子資料寫入後推到 KV，讓 Claude 可讀
  try {
    fetch('/api/stock?type=tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ todos: todos }),
    });
  } catch(_) {}

  var notes = [];
  notes.push({
    id: t + 127,
    title: '2026-06-11 · 產業地圖流域升級第一波',
    content: '✅ 河道化：細線→真河道（河岸暈+河身+墨金水紋三層），寬度依兩端公司數變化（價值流量）\n✅ 金沙粒子：每條河道 2 枚金沙沿貝茲曲線順流而下（animateMotion），選中節點時相連河道全亮、其餘退淡\n✅ 捲軸進場：羅盤旋轉定位 → 河道從上游往下游逐段畫出 → 城鎮依欄位節奏蓋章落下\n✅ 城鎮旗幟：每節點插欄位色三角旗，hover 旗幟放大 + 金色點燈光暈\n✅ 山海地形底圖：上游山脈剪影、中游平原田紋、下游海岸波浪+戎克船（淡墨 watermark）\n📌 industry-map.jsx 三頁共用（台股產業地圖/寶島流域圖鑑/美股產業地圖）同步升級；reduced-motion 全停用、金沙不渲染\n⏭ 第二波待辦：鏡頭 pan/zoom + 市況旗',
    agent: 'icon-artist',
    ts: '6/11',
  });
  notes.push({
    id: t + 126,
    title: '2026-06-11 · 月營收金幣疊 + 季損益鈔票疊',
    content: '✅ 月營收趨勢柱體改「金幣疊」：橢圓硬幣逐枚堆疊，SVG 金屬漸層 + 頂面亮色鑄紋內圈\n✅ 語意配色：YoY 正成長=金幣、衰退=銅幣、最新月份頂面金圈標記\n✅ 季損益四圖柱體改「鈔票疊」：紙鈔交錯微偏移手疊感，最上層鈔票有內框+中央圓章\n✅ 獲利往上疊、虧損往下疊銅紅鈔；EPS 去年同季金色虛線保留\n✅ hover 提示卡照舊（月份/營收/YoY）\n📌 純 SVG 實作（fundamental.jsx RevenueChart / QuarterlyChart），無新依賴',
    agent: 'ui-designer',
    ts: '6/11',
  });
  notes.push({
    id: t + 125,
    title: '2026-06-11 · 企業基本面美化層',
    content: '✅ 數字字體統一：股價/估值/KPI/表格 -apple-system → Times New Roman 等寬襯線（全站雜誌風一致）\n✅ 區塊頭：金線懸停舒展 + 雙細線收尾 + 章節號金影\n✅ 封面：雙細線（呼應 Masthead）+ 股價金色底線\n✅ 估值結論改硃砂印章風：雙框 + 微旋轉（強力買進/合理/偏貴）\n✅ 表格：表頭 small-caps 金棕 + 懸停列金邊書籤（與全站 ed-table 一致）\n✅ 公司簡介首字下沉（drop cap 雜誌頭版工藝)\n✅ 估值卡/供應鏈節點/美國關係卡懸停浮起、游標滑移動畫\n✅ 11 個 section 接上滾動進場 + 股價/KPI/估值數字滾動\n📌 全在 fundamental.css 末尾「基本面美化層」區塊，可整塊回退',
    agent: 'ui-designer',
    ts: '6/11',
  });
  notes.push({
    id: t + 124,
    title: '2026-06-11 · 互動升級層 v2.5（生動感+互動性）',
    content: '✅ 滾動進場：捲到視窗才浮現，雜誌翻頁節奏（IO+MutationObserver 支援 React 動態渲染）\n✅ 數字滾動：stat-cell/big-num 首次可見時 0→目標值（只動文字節點，不破壞子元素）\n✅ 墨水風 tooltip：[data-tip] 通用，Masthead 警示/圖鑑/隊員按鈕套用\n✅ Command Palette：⌘K/Ctrl+K 喚出，頁面跳轉+台股代號/美股 ticker 直達+公司名搜尋，鍵盤↑↓Enter\n✅ Ticker：LIVE 金色呼吸點固定左緣，項目可點擊（指數→大盤、其餘→另類指標）\n✅ K線測量工具：工具列⊿按鈕，拖曳顯示價差/%/K棒數，近頂緣資訊卡自動翻轉\n📌 全部支援 reduced-motion；Palette 為 vanilla DOM 不依賴 React',
    agent: 'frontend-lead',
    ts: '6/11',
  });
  notes.push({
    id: t + 123,
    title: '2026-06-11 · 設計升級層 v2（視覺精緻度）',
    content: '✅ 文字選取金箔底、捲軸紙張底+鍍金懸停、鍵盤焦點金圈\n✅ Masthead 雙線中央燙金（書名頁工藝）\n✅ Section kicker 前置金短線（雜誌欄目標記）\n✅ frame-card 懸停內框鍍金（與金角舒展呼應）\n✅ 表格懸停列左緣金邊書籤 + 微縮進\n✅ stat-cell 頂緣金刻度\n✅ Ticker 左右墨色淡出（印刷帶質感）\n✅ 按鈕按壓下沉回饋、平滑捲動\n📌 全部 14 頁共用 styles.css 受益，整塊可回退，支援 reduced-motion',
    agent: 'design-lead',
    ts: '6/11',
  });
  notes.push({
    id: t + 100,
    title: '股票分析師審查 個股技術分析 2026-06-03',
    content: '🔴 MACD評分只看OSC>0，漏掉交叉訊號\n🔴 支撐壓力固定±3%不符實際\n🔴 基本面未納入評分\n🔵 缺Bollinger Band、ATR\n🔵 成交量缺MA20\n🟡 EPS缺去年同季對比\n🟡 RSI缺背離偵測',
    agent: 'tw-stock-analyst',
    ts: '6/3 03:00',
  });
  notes.push({
    id: t + 101,
    title: 'frontend-lead 完成 個股+基本面 頁面合併',
    content: '✅ fundamental.jsx 嵌入模式（externalCode prop）\n✅ stock.jsx tab bar（技術分析 / 企業基本面）\n✅ 個股技術分析.html 載入 fundamental.jsx\n✅ 企業基本面.html 自動轉址\n✅ 導覽列整併',
    agent: 'frontend-lead',
    ts: '6/3',
  });
  notes.push({
    id: t + 107,
    title: '任務派遣 2026-06-03',
    content: '📋 indicator-specialist：scan.jsx × MACD 整合 + 明確進出場價格\n📋 kline-specialist：chart-pro.jsx 即時 K 棒修復（TWSE MIS → 今日 bar）\n📋 quant-lead：統籌 scan + signal 架構\n✅ CLAUDE.md 全面更新（5部門、完整 API 清單、Compact Instructions）',
    agent: 'frontend-lead',
    ts: '6/3',
  });
  notes.push({
    id: t + 106,
    title: 'smc-analyst + indicator-specialist 進出場訊號面板',
    content: '✅ signal.jsx 新建（SignalPanel + MACDStrategyPanel）\n✅ 個股技術分析第三 tab「進出場訊號」\n✅ SMC：Swing H/L、OB 需求區、FVG 未填缺口（joshyattridge/smc）\n✅ MACD：黃金/死亡交叉偵測、多空背離（twopirllc/pandas-ta）\n✅ 進場區/出場目標/停損/R:R 自動計算\n✅ 6 條件評分 + 條件明細 ✅⚠️❌\n待辦：雙策略整合加權、KDJ 輔助確認、績效監控',
    agent: 'indicator-specialist',
    ts: '6/3',
  });
  notes.push({
    id: t + 105,
    title: 'ui-coder 投資者上看潛力 FMP 機構目標價',
    content: '✅ FMP analyst-estimates + price-target-summary 串接（TSM ADR）\n✅ USD → 台幣換算：USD × USDTWD（open.er-api.com）÷ ADR比例\n✅ 三欄：歷史均值/近一年共識/最新季度目標價（台幣+上漲%）\n✅ EPS 預估表（保守/共識/樂觀 × 未來3年）\n✅ 無 ADR 股票 fallback 到歷史 P/E 計算式\n待辦：擴充 TW_ADR_MAP（2303/2412/3711）',
    agent: 'ui-coder',
    ts: '6/3',
  });
  notes.push({
    id: t + 103,
    title: 'kline-specialist lightweight-charts 升級',
    content: '✅ 手刻 SVG (~550行) → lightweight-charts (~220行)\n✅ 蠟燭圖原生渲染，滾輪縮放、pinch zoom\n✅ EMA 20/50/100/200 保留（4色）\n✅ MACD 4色直方圖（CM_MacD_Ult_MTF）\n✅ Bollinger Bands、RSI、成交量 overlay\n✅ 跨面板時間軸同步（main+macd+rsi）\n📚 Repo: github.com/tradingview/lightweight-charts',
    agent: 'kline-specialist',
    ts: '6/3',
  });
  notes.push({
    id: t + 104,
    title: 'frontend-lead 新設 E · 量化交易部',
    content: '✅ 5部門架構完成（A數據/B前端/C策略/D品保/E量化交易）\n✅ B前端工坊拆分：chart-specialist → kline-specialist + indicator-specialist\n✅ A數據中台加 data-pipeline 專員\n✅ sidebar exMap 加 quant-trade 展開狀態\n✅ TEAM.md 更新含 Repo 參考連結\n📚 E部門 Repo: pandas-ta / joshyattridge/smc / microsoft/qlib',
    agent: 'frontend-lead',
    ts: '6/3',
  });
  notes.push({
    id: t + 108,
    title: 'admin-lead 全員到齊：各部門補充專員 + Repo',
    content: '✅ 7部門 39 位專員全員到齊\n✅ A +2：d7 開放資料師（DDChen666/tw_trademarket_info）· d8 即時串流師（websockets/ws）\n✅ B +2：b5 動畫設計師（airbnb/lottie-web）· b6 手機設計師（pmndrs/use-gesture）\n✅ C +2：c6 測試工程師（cypress-io/cypress）· c7 PWA工程師（GoogleChrome/workbox）\n✅ D +2：q7 量能分析師（polakowo/vectorbt）· q8 選擇權師（vollib/py_vollib）\n✅ E +2：p4 E2E測試師（microsoft/playwright）· p5 無障礙稽核師（dequelabs/axe-core）\n✅ F +2：e4 加密貨幣師（freqtrade/freqtrade）· e5 市場狀態師（polakowo/vectorbt）\n✅ G +2：g4 版本發布師（semantic-release）· g5 資料管理師（IBM/openapi-validator）\n✅ TEAM.md + AGENT_ADVICE 全同步',
    agent: 'admin-lead',
    ts: '6/3',
  });
  notes.push({
    id: t + 102,
    title: 'ui-coder 同業比較改版',
    content: '✅ 新增 bwibbu_batch API（TWSE+TPEx 一次拿全部）\n✅ PeerTable 顯示 P/E、P/B、殖利率、bar chart\n✅ 最低本益比標記綠點\n✅ 點擊同業跳轉個股技術分析頁\n待辦：EPS去年同季、D/E標記、基本面納入評分',
    agent: 'ui-coder',
    ts: new Date().toLocaleString('zh-TW', { month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit' }),
  });
  notes.push({
    id: t + 114,
    title: '2026-06-05 · 第四批實作完成（8項）',
    content: '✅ styles.css 新增6個utility class（text-bull/bear/gold/mute, font-kai/roman）\n✅ node-icons.jsx 新增6個圖示（AiChip/Cloud/Neural/DataPipeline/Security/Finance）\n✅ chart-pro.jsx Price Line（scan→URL params→stock.jsx→createPriceLine 進場/T1/T2/停損4條水平線）\n✅ scan.jsx 快速搜尋（代號/名稱即時過濾 + 空結果直連個股頁）\n✅ scan.jsx URL highlight（?highlight=CODE 自動捲動定位）\n✅ stock.js apiError() helper 統一 error schema（error/code/ts格式）\n✅ stock.js 月營收異常偵測（ZERO_REVENUE/SHARP_DECLINE/UNUSUAL_SPIKE）\n✅ MOPS 兩路徑皆回傳 anomalies 欄位',
    agent: 'engineering-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 113,
    title: '2026-06-05 · 第三批實作完成（6項）',
    content: '✅ 新聞 KV 快取（市場5min/個股10min，情緒評分後才存）\n✅ 關鍵詞情緒預判（BULL/BEAR各12詞，命中直接用跳過AI，減少Workers AI用量）\n✅ warmup 熱門股擴充至15支（科技/AI伺服器/金融/電信/傳產龍頭）\n✅ 產業流域圖補充世芯6278(AI ASIC)、AI應用軟體節點（智擎/叡揚）\n✅ AI/半導體輪動追蹤面板（7族群即時強弱燈，掛在 app.jsx 產業圖下方）\n✅ MACD 績效追蹤 PerfPanel（📌記錄訊號→localStorage，自動更新報酬率，勝率統計，固定在頁面底部）',
    agent: 'engineering-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 112,
    title: '2026-06-05 · 第二批實作完成（7項）',
    content: '✅ RSI+MACD 雙重背離確認（單一降權灰框，雙重⚡金框）\n✅ 背離回看參數動態化（ATR波動率：高→20根，低→60根，UI顯示當前值）\n✅ K線圖 D/E 事件標記（D=除息金色圓，E=財報藍綠方塊，已整合進 fundamental→chartDividends 管線）\n✅ 新增 tw_events API（FinMind TaiwanStockDividend + earnings_cal）\n✅ 同業比較表迷你折線圖（TWSE STOCK_DAY 近5日，非同步載入不阻塞）\n✅ 新增 peer_mini API（批次取近5日收盤價）\n✅ 基本面 P/E + EPS YoY 納入評分（P/E<15→+2, EPS>20%→+2，UI 顯示評分卡）',
    agent: 'engineering-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 111,
    title: '2026-06-05 · 第一批實作完成（5項）',
    content: '✅ NavBar 活躍頁面高亮（pathname+search 雙重比對，向後相容）\n✅ EPS 季度圖加去年同季對比虛線（strokeDasharray gold虛線 + 數值標籤）\n✅ 基本面無資料友善提示（10處統一 📊 emoji + 來源說明）\n✅ Section 05 ADR 換算說明優化（完整公式 USD×匯率÷股數，.TW直接標示）\n✅ health check 強化（FinMind/Yahoo/TWSE_MIS 並行測試 + 5s timeout）\n✅ 新增 us_extended API（FMP 盤前盤後，fallback Yahoo）\n✅ TW_ADR_MAP 2303/2412/3711 已完成（上次已做）\n⚙️ seed 邏輯修正：保留使用者手動新增任務（seeded 標記）',
    agent: 'engineering-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 110,
    title: '2026-06-05 · 隊員新增 28 項待辦任務',
    content: '📋 A數據中台：確認API回應時間/KV warmup/health check/月營收異常偵測/批次排程/KV季報/MOPS OpenAPI/RSS快取/新聞情緒過濾\n📋 B設計部：設計系統文件化/品牌一致性審查/styles.css整理/AI圖示補充/圖示風格統一\n📋 C工程部：NavBar活躍追蹤/同業迷你圖/基本面無資料提示/ADR說明優化/Price Line研究/雙重背離/動態回看參數\n📋 D策略室：scan.jsx加MACD建議展開列/AI族群/半導體輪動\n📋 E品保：lighthouse評分/error schema審查\n📋 UX：黃金交叉流程優化\n🔧 修正：seed 邏輯改為保留使用者手動新增任務（seeded 標記）',
    agent: 'admin-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 109,
    title: '2026-06-05 · 全面修復 + 估值升級',
    content: '✅ K線圖 MACD 買賣標記：黃金交叉▲買、死亡交叉▼賣\n✅ K線圖 MA 均線：週月季年 SMA 合一 chip 開關\n✅ 估值四方法：Graham / Lynch PEG / 5yr DCF / Forward P/E（市場法）\n✅ FMP .TW 路徑：台幣目標價，非ADR股票也能用\n✅ 分析師評等升降紀錄（grades-latest 近5筆）\n✅ TW_ADR_MAP 修正：8150→IMOS（修錯誤），新增 2409→AUO、3697→HIMX\n✅ 修復同業排名：company-db.js 未載入個股頁\n✅ 修復股利記錄：FinMind 欄位名稱 CashEarningsDistribution\n✅ 修復法人籌碼：FinMind 改回英文名稱 Foreign_Investor/Investment_Trust\n✅ 導覽列移除籌碼雷達，警示設定保留',
    agent: 'frontend-lead',
    ts: '6/5',
  });
  notes.push({
    id: t + 120,
    title: '2026-06-05 · 美股K線圖全面升級',
    content: '✅ chart-pro.jsx 新增 rangeTabs prop：外部傳入時取代內建 TF_PRESETS 按鈕\n✅ 美股個股頁 range 按鈕（1D/1W/1M/3M/6M/1Y/2Y/5Y）移入 ChartPro 工具列，仿台股排版\n✅ 修復美股個股頁缺少 chart-pro.css 引入（cp-toolbar flex 排版崩潰）\n✅ exchange 欄位動態化（stock?.exchange || TWSE），美股顯示 NYSE/NASDAQ\n✅ ChartPro 股票名稱優先用 meta.name（Apple Inc.），非中文名',
    agent: 'kline-specialist',
    ts: '6/5 下午',
  });
  notes.push({
    id: t + 121,
    title: '2026-06-05 · 美股 API 三層 fallback + 快取保護',
    content: '✅ us_chart_meta 新增第三層 fallback：FMP quote → Yahoo chart meta → Yahoo v7/quote realtime\n✅ us_chart_meta 快取保護：price=0 時不寫入 KV（防止錯誤結果被快取 5 分鐘）\n✅ us_intraday_fmp 新增 Yahoo Finance fallback（FMP 免費版不含日內資料）\n✅ Yahoo intraday 格式：date 欄位補 ISO 時間戳 new Date(t*1000).toISOString()\n✅ 前端 price 計算改用 meta?.price > 0 ? meta.price : lastCandle（解決技術分析顯示 0 元）',
    agent: 'data-lead',
    ts: '6/5 下午',
  });
  notes.push({
    id: t + 122,
    title: '2026-06-05 · RufloSidebar ↔ KV 雙向同步',
    content: '✅ stock.js 新增 tasks 端點（GET 讀 / POST 寫，TTL 90 天）\n✅ CORS 更新支援 POST 方法\n✅ shared.jsx 頁面載入時從 KV 拉最新任務狀態，seeded 任務以 KV 為準\n✅ todos 變更後 3 秒 throttle 推送到 KV\n✅ seedPlans 種子完成後主動推一次 KV\n✅ 建立 reference_task_sync.md + feedback_task_completion.md 記憶\n📌 效果：Claude 現在可查 /api/stock?type=tasks 讀到完整即時任務清單',
    agent: 'engineering-lead',
    ts: '6/5 下午',
  });
  notes.push({
    id: t + 128,
    title: '2026-06-19 · 路線圖五項完成批（除 Telegram）',
    content: '✅ tech-moat.js 擴檔：達發3692/穎崴6438/雍智6785/M31 6643 各技術域 lead/par/chase 建檔；status 升降規則文件化（量產追平→par/獨家→lead/被超越→chase）\n✅ 産業地圖第二波：industry-map.jsx RiverMap 接通 vp 狀態到 SVG viewBox；wheel zoom、拖曳 pan、三按鈕控制列（＋/－/⌂）、左下縮放%；市況旗：取市值最大代表股，priceMap.chg% ▲/▼ 彩色旗幟顯示於節點右下角\n✅ 回測引擎升級：backtest.jsx 新增 BacktestEngine 元件，自訂 fast/slow/sig/stopLoss，ECharts 權益曲線（進出場標記+100基線），KPI 格：總報酬/最大回撤/勝率/交易次數；交易日誌表（死叉/觸停損）\n✅ MACD 直方圖篩選器 Step 3/4：scan.jsx FILTERS.hist 四色選項（aqua/blue/maroon/red），histColor 欄位 useMemo 過濾，UI 篩選列帶色色塊，power-confirm preset 預設 aqua\n✅ 企業段位前瞻回測：fundamental.jsx TierBacktestPanel，記錄當日段位+股價到 localStorage tw_tier_bt_v1，30天後回顯現價報酬差；TierSection 接 currentPrice prop',
    agent: 'quant-lead',
    ts: '6/19',
  });
  localStorage.setItem('tw_ruflo_notes', JSON.stringify(notes));
  localStorage.setItem(SEED_KEY, '1');
})();

// ── 隊員 API — 讓 Agent 寫入計畫與更新日誌 ──────────────────
window.RUFLO_AGENT_API = {
  // 新增計畫（将来要做的事）
  plan: function(text, cat, agent) {
    var todos = [];
    try { todos = JSON.parse(localStorage.getItem('tw_ruflo_todos') || '[]'); } catch(_) {}
    todos.push({
      id:    Date.now(),
      text:  (agent ? '[' + agent + '] ' : '') + text,
      cat:   cat || '本週',
      done:  false,
      agent: agent || '',
    });
    localStorage.setItem('tw_ruflo_todos', JSON.stringify(todos));
  },
  // 新增更新日誌（錯誤修復/功能更新記錄）
  log: function(title, content, agent) {
    var notes = [];
    try { notes = JSON.parse(localStorage.getItem('tw_ruflo_notes') || '[]'); } catch(_) {}
    var ts = new Date().toLocaleString('zh-TW', { month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit' });
    notes.push({
      id:      Date.now(),
      title:   title,
      content: content || '',
      agent:   agent || '',
      ts:      ts,
    });
    localStorage.setItem('tw_ruflo_notes', JSON.stringify(notes));
  },
  // 標記計畫完成
  done: function(keyword) {
    var todos = [];
    try { todos = JSON.parse(localStorage.getItem('tw_ruflo_todos') || '[]'); } catch(_) {}
    todos = todos.map(function(t) {
      return t.text.includes(keyword) ? Object.assign({}, t, { done: true }) : t;
    });
    localStorage.setItem('tw_ruflo_todos', JSON.stringify(todos));
  },
};

const NAV_PAGES = [
  { id: 'dashboard', label: '大盤總覽',     en: 'Dashboard',   href: '大盤總覽.html' },
  { id: 'industry',  label: '產業地圖',     en: 'Industry Map', href: '產業地圖.html' },
  { id: 'stock',     label: '個股技術分析', en: 'Stock Quote',  href: '個股技術分析.html' },
  { id: 'radarzone', label: '雷達區',       en: 'Radar Zone',   href: '雷達區.html' },
  { id: 'backtest',  label: '臥龍薦股錄',   en: 'Oracle Picks', href: '策略回測.html' },
  { id: 'etf',       label: 'ETF 總覽',     en: 'ETF Hub',      href: 'ETF總覽.html' },
  { id: 'watchlist', label: '自選股',       en: 'Watchlist',    href: '自選股.html' },
  { id: 'oracle',    label: '另類指標',     en: 'Oddities',     href: '另類指標.html' },
  { id: 'futures',   label: '期貨',         en: 'Futures',      href: '期貨.html' },
];
window.NAV_PAGES = NAV_PAGES;

const US_NAV_PAGES = [
  { id: 'us-dashboard', label: '大盤總覽',   en: 'US Overview',  href: '大盤總覽.html?market=us' },
  { id: 'us-stock',     label: '個股分析',   en: 'Stock Detail', href: '美股個股.html' },
  { id: 'us-industry',  label: '產業地圖',   en: 'Industry Map', href: '產業地圖.html?market=us' },
  { id: 'us-watchlist', label: '自選股',     en: 'Watchlist',    href: '自選股.html?market=us' },
];
window.US_NAV_PAGES = US_NAV_PAGES;

const CRYPTO_NAV_PAGES = [
  { id: 'crypto-overview', label: '加密總覽', en: 'Overview',    href: '加密總覽.html' },
  { id: 'crypto-markets',  label: '幣種行情', en: 'Markets',     href: '幣種行情.html' },
  { id: 'crypto-onchain',  label: '鏈上分析', en: 'On-Chain',    href: '鏈上分析.html' },
  { id: 'crypto-infra',    label: '基礎設施', en: 'Infra Radar', href: '基礎設施.html' },
];
window.CRYPTO_NAV_PAGES = CRYPTO_NAV_PAGES;

function USNavBar({ active }) {
  // Auto-detect current page when active prop is not provided
  var _active = active || (function() {
    var pathname = window.location.pathname.split('/').pop() || '';
    var search = window.location.search;
    for (var i = 0; i < US_NAV_PAGES.length; i++) {
      var p = US_NAV_PAGES[i];
      var hrefFile = p.href.split('?')[0];
      var hrefSearch = p.href.includes('?') ? '?' + p.href.split('?')[1] : '';
      if (pathname === hrefFile && (hrefSearch === '' || search === hrefSearch)) return p.id;
    }
    return '';
  })();
  return (
    <nav className="nav-bar">
      <div className="nav-bar-inner">
        {US_NAV_PAGES.map(p => (
          <a key={p.id} href={p.href}
            className={`nav-link ${_active === p.id ? 'active' : ''}`}
            aria-current={_active === p.id ? 'page' : undefined}>
            <span className="nav-kai kai">{p.label}</span>
            <span className="nav-en">{p.en}</span>
          </a>
        ))}
      </div>
    </nav>
  );
}


function CryptoNavBar({ active }) {
  var _active = active || (function() {
    var pathname = window.location.pathname.split('/').pop() || '';
    for (var i = 0; i < CRYPTO_NAV_PAGES.length; i++) {
      if (pathname === CRYPTO_NAV_PAGES[i].href) return CRYPTO_NAV_PAGES[i].id;
    }
    return '';
  })();
  return (
    <nav className="nav-bar">
      <div className="nav-bar-inner">
        {CRYPTO_NAV_PAGES.map(p => (
          <a key={p.id} href={p.href}
            className={`nav-link ${_active === p.id ? 'active' : ''}`}
            aria-current={_active === p.id ? 'page' : undefined}>
            <span className="nav-kai kai">{p.label}</span>
            <span className="nav-en">{p.en}</span>
          </a>
        ))}
      </div>
    </nav>
  );
}

// ── PWA Bootstrap (runs once on every page load) ──────────
(function() {
  if (!document.querySelector('link[rel="manifest"]')) {
    var ml = document.createElement('link');
    ml.rel = 'manifest'; ml.href = '/manifest.json';
    document.head.appendChild(ml);
  }
  if (!document.querySelector('link[rel="apple-touch-icon"]')) {
    var al = document.createElement('link');
    al.rel = 'apple-touch-icon'; al.href = '/icon-192.svg';
    document.head.appendChild(al);
  }
  if (!document.querySelector('meta[name="theme-color"]')) {
    var tm = document.createElement('meta');
    tm.name = 'theme-color'; tm.content = '#1a2438';
    document.head.appendChild(tm);
  }
  if (!document.querySelector('meta[name="apple-mobile-web-app-capable"]')) {
    var ac = document.createElement('meta');
    ac.name = 'apple-mobile-web-app-capable'; ac.content = 'yes';
    document.head.appendChild(ac);
    var as_ = document.createElement('meta');
    as_.name = 'apple-mobile-web-app-status-bar-style'; as_.content = 'black-translucent';
    document.head.appendChild(as_);
    var at = document.createElement('meta');
    at.name = 'apple-mobile-web-app-title'; at.content = '台股圖鑑';
    document.head.appendChild(at);
  }
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistrations().then(function(regs) {
      regs.forEach(function(r) { r.unregister(); });
    });
    if (window.caches) {
      caches.keys().then(function(keys) {
        keys.forEach(function(k) { caches.delete(k); });
      });
    }
  }
})();

window.claudeComplete = async function({ messages }) {
  // 創作者模式解鎖後自動帶 token:後端限流(_ratelimit.js)對創作者豁免,
  // 訪客維持每小時上限(防外人燒 AI 額度,2026-07-18)
  var headers = { 'Content-Type': 'application/json' };
  var creatorSecret = localStorage.getItem('tw_creator_secret') || '';
  if (creatorSecret) headers['X-Creator-Token'] = creatorSecret;
  const r = await fetch('/api/llm', {
    method: 'POST',
    headers: headers,
    body: JSON.stringify({ messages }),
  });
  if (!r.ok) {
    const err = await r.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${r.status}`);
  }
  const data = await r.json();
  return data.reply || '';
};

const _I_BOOK   = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 5a2 2 0 0 1 2-2h6v18H5a2 2 0 0 1-2-2zM21 5a2 2 0 0 0-2-2h-6v18h6a2 2 0 0 0 2-2z"/></svg>);
const _I_SEARCH = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>);
const _I_TEAM   = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="9" cy="7" r="3"/><path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/><circle cx="18" cy="8" r="2"/><path d="M22 21v-1a3 3 0 0 0-2-2.83"/></svg>);

function MarketSwitcher({ market = 'tw' }) {
  const tapCount = useRef(0);
  const tapTimer = useRef(null);

  function handleActiveTap(e) {
    e.preventDefault();
    tapCount.current++;
    clearTimeout(tapTimer.current);
    tapTimer.current = setTimeout(function() { tapCount.current = 0; }, 800);
    if (tapCount.current >= 3) {
      tapCount.current = 0;
    }
  }

  return (
    <div className="mkt-switcher">
      <a className={`mkt-btn ${market === 'tw' ? 'mkt-active' : ''}`}
         href="大盤總覽.html"
         onClick={market === 'tw' ? handleActiveTap : undefined}>
        <span className="mkt-flag">TW</span>
        <span className="mkt-name">臺灣</span>
      </a>
      <a className={`mkt-btn ${market === 'us' ? 'mkt-active' : ''}`}
         href="美股總覽.html"
         onClick={market === 'us' ? handleActiveTap : undefined}>
        <span className="mkt-flag">US</span>
        <span className="mkt-name">美股</span>
      </a>
      <a className={`mkt-btn ${market === 'crypto' ? 'mkt-active' : ''}`} href="加密總覽.html">
        <span className="mkt-flag" style={{ fontSize: 14 }}>₿</span>
        <span className="mkt-name">加密</span>
      </a>
    </div>
  );
}

// ── 警示面板 ─────────────────────────────────────────────────
const _ALERT_KEY = 'tw_alerts_v1';
function _loadAlerts() { try { return JSON.parse(localStorage.getItem(_ALERT_KEY)||'[]'); } catch { return []; } }
function _saveAlerts(a) { localStorage.setItem(_ALERT_KEY, JSON.stringify(a)); }

function AlertPanel({ onClose }) {
  const [alerts, setAlerts] = useState(_loadAlerts);
  const [form, setForm] = useState({ code:'', type:'above', price:'' });
  const [prices, setPrices] = useState({});

  useEffect(() => {
    const codes = [...new Set(alerts.map(a => a.code))];
    if (!codes.length) return;
    codes.forEach(code => {
      fetch(`/api/stock?type=realtime&code=${code}`)
        .then(r => r.json())
        .then(d => {
          const p = parseFloat(d?.msgArray?.[0]?.z || d?.msgArray?.[0]?.y || 0);
          if (p) setPrices(prev => ({ ...prev, [code]: p }));
        }).catch(() => {});
    });
  }, [alerts.length]);

  function add() {
    const p = parseFloat(form.price);
    if (!form.code || isNaN(p)) return;
    const updated = [...alerts, { id: Date.now(), code: form.code.trim(), type: form.type, price: p, triggered: false }];
    setAlerts(updated); _saveAlerts(updated);
    setForm({ code:'', type:'above', price:'' });
  }

  function del(id) { const u = alerts.filter(a => a.id !== id); setAlerts(u); _saveAlerts(u); }
  function reset(id) { const u = alerts.map(a => a.id===id ? {...a, triggered:false} : a); setAlerts(u); _saveAlerts(u); }

  const active = alerts.filter(a => !a.triggered);
  const fired  = alerts.filter(a => a.triggered);

  return (
    <div style={{ position:'fixed', top:54, left:12, width:340, background:'var(--paper,#f6f1e4)', border:'1px solid var(--rule,#ccc)', borderRadius:4, boxShadow:'0 8px 32px rgba(0,0,0,.25)', zIndex:9000, fontFamily:'BiauKai,標楷體,serif', color:'var(--ink,#1a2438)' }}>
      <div style={{ display:'flex', alignItems:'center', padding:'10px 14px', borderBottom:'1px solid var(--rule,#ccc)' }}>
        <span style={{ fontWeight:'bold', flex:1 }}>⚠ 警示設定</span>
        <a href="警示設定.html" style={{ fontSize:'var(--fs-nano)', color:'var(--gold-deep,#b8924d)', marginRight:12 }}>完整頁面</a>
        <button onClick={onClose} style={{ background:'none', border:'none', cursor:'pointer', fontSize:16, color:'#999' }}>✕</button>
      </div>

      {/* 新增 */}
      <div style={{ padding:'10px 14px', borderBottom:'1px solid var(--rule,#ccc)', display:'flex', gap:6 }}>
        <input placeholder="代號" value={form.code} onChange={e=>setForm(f=>({...f,code:e.target.value}))} maxLength={6}
          style={{ width:70, padding:'6px 8px', border:'1px solid var(--rule,#ccc)', borderRadius:2, fontSize:13, background:'#fff', color:'var(--ink,#1a2438)' }}/>
        <select value={form.type} onChange={e=>setForm(f=>({...f,type:e.target.value}))}
          style={{ flex:1, padding:'6px 8px', border:'1px solid var(--rule,#ccc)', borderRadius:2, fontSize:12, background:'#fff', color:'var(--ink,#1a2438)' }}>
          <option value="above">突破</option>
          <option value="below">跌破</option>
        </select>
        <input placeholder="價格" type="number" value={form.price} onChange={e=>setForm(f=>({...f,price:e.target.value}))}
          style={{ width:80, padding:'6px 8px', border:'1px solid var(--rule,#ccc)', borderRadius:2, fontSize:13, background:'#fff', color:'var(--ink,#1a2438)' }}/>
        <button onClick={add} style={{ padding:'6px 12px', background:'var(--gold-deep,#b8924d)', color:'#fff', border:'none', borderRadius:2, cursor:'pointer', fontSize:13 }}>＋</button>
      </div>

      {/* 列表 */}
      <div style={{ maxHeight:300, overflowY:'auto' }}>
        {alerts.length === 0 && <div style={{ padding:'16px', color:'#999', fontSize:13, textAlign:'center' }}>尚無警示</div>}
        {active.map(a => {
          const cur = prices[a.code];
          return (
            <div key={a.id} style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 14px', borderBottom:'1px solid #eee' }}>
              <span style={{ fontSize:16 }}>{a.type==='above'?'📈':'📉'}</span>
              <div style={{ flex:1, fontSize:13 }}>
                <b>{a.code}</b> {a.type==='above'?'突破':'跌破'} {a.price}
                {cur && <span style={{ fontSize:'var(--fs-nano)', color:'#888', marginLeft:6 }}>現 {cur}</span>}
              </div>
              <button onClick={()=>del(a.id)} style={{ background:'none', border:'none', cursor:'pointer', color:'#bbb', fontSize:14 }}>×</button>
            </div>
          );
        })}
        {fired.map(a => (
          <div key={a.id} style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 14px', borderBottom:'1px solid #eee', opacity:.6 }}>
            <span>✅</span>
            <div style={{ flex:1, fontSize:13 }}><b>{a.code}</b> 已觸發</div>
            <button onClick={()=>reset(a.id)} style={{ fontSize:'var(--fs-nano)', background:'none', border:'1px solid #ccc', borderRadius:2, cursor:'pointer', padding:'2px 6px' }}>重置</button>
            <button onClick={()=>del(a.id)} style={{ background:'none', border:'none', cursor:'pointer', color:'#bbb', fontSize:14 }}>×</button>
          </div>
        ))}
      </div>
    </div>
  );
}

function _getOrCreateSid() {
  try {
    var k = 'tw_sid';
    var v = localStorage.getItem(k);
    if (!v) { v = Math.random().toString(36).slice(2, 10) + Date.now().toString(36); localStorage.setItem(k, v); }
    return v.slice(0, 32).replace(/[^a-z0-9]/gi, '');
  } catch(e) { return 'anon'; }
}

// 全站報價刷新鈕：清 QuoteHub 快取 + 廣播 qh:refresh，各頁監聽後重抓
function GlobalRefreshBtn() {
  const [spinning, setSpinning] = useState(false);
  function doRefresh() {
    if (spinning) return;
    setSpinning(true);
    if (window.QuoteHub) window.QuoteHub.refreshAll();
    setTimeout(function() { setSpinning(false); }, 1200);
  }
  return (
    <button className="icon-btn" onClick={doRefresh} data-tip="全站報價刷新"
      style={{ background:'none', cursor:'pointer' }}>
      <span style={{
        display:'inline-block', fontSize:15, lineHeight:1,
        transition:'transform 1s ease',
        transform: spinning ? 'rotate(720deg)' : 'none',
        color: spinning ? 'var(--gold)' : 'inherit'
      }}>↻</span>
    </button>
  );
}

function Masthead({ active = '', market = 'tw' }) {
  const [showAlerts, setShowAlerts] = useState(false);
  useEffect(function() {
    var sid = _getOrCreateSid();
    var page = (window.location.pathname.split('/').pop() || 'index').slice(0, 40);
    fetch('/api/stock?type=ping&sid=' + sid + '&page=' + encodeURIComponent(page)).catch(function(){});
  }, []);
  const today = new Date();
  const dateStr = today.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
  const lunarDate = today.toLocaleDateString('zh-TW', { year: 'numeric', month: 'long', day: 'numeric' });
  const brandSub = market === 'crypto' ? '數 位 資 產 圖 鑑' : market === 'us' ? '美 國 股 市 圖 鑑' : '臺 灣 股 市 圖 鑑';
  const homeHref = market === 'crypto' ? '加密總覽.html' : market === 'us' ? '美股總覽.html' : '大盤總覽.html';
  const firedCount = _loadAlerts().filter(a => a.triggered).length;
  return (
    <>
    {showAlerts && <AlertPanel onClose={() => setShowAlerts(false)} />}
    <header className="masthead">
      <div className="brand">
        <button onClick={() => setShowAlerts(v => !v)} data-tip="警示設定" style={{
          background: 'none', border: 'none', cursor: 'pointer', padding: '4px 8px',
          color: firedCount > 0 ? '#e63946' : 'var(--ink-mute,#6b7280)',
          fontSize: 18, position: 'relative', lineHeight: 1,
        }}>
          ⚠
          {firedCount > 0 && (
            <span style={{ position:'absolute', top:0, right:0, background:'#e63946', color:'#fff', borderRadius:'50%', fontSize:'var(--fs-nano)', width:14, height:14, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:'sans-serif' }}>{firedCount}</span>
          )}
        </button>
        <a href={homeHref} style={{ display:'flex', alignItems:'flex-end', gap:12, textDecoration:'none', color:'inherit' }}>
          <div className="brand-mark">
            <svg viewBox="0 0 32 32" fill="none">
              <path d="M5 25 L11 16 L17 21 L23 9 L27 14" stroke="currentColor" strokeWidth="2"
                    strokeLinecap="round" strokeLinejoin="round" />
              <circle cx="23" cy="9" r="1.6" fill="currentColor" />
            </svg>
          </div>
          <div>
            <div className="brand-title">Formosa <span style={{ fontStyle: 'italic', color: 'var(--gold)' }}>&amp;</span> Co.</div>
            <span className="brand-sub kai">{brandSub}</span>
          </div>
        </a>
      </div>
      <div className="masthead-meta">
        <b>vol. xii · no. 6</b>
        <br/>
        <span style={{ fontFamily: 'BiauKai, 標楷體, serif', letterSpacing: '.15em' }}>
          台 北 · {lunarDate}
        </span>
        <span style={{ margin: '0 8px' }}>—</span>
        {dateStr}
        <br/>
        <span className="qh-light" style={{ fontSize: 8, marginRight: 4, color: '#2e7d32', cursor: 'help' }}>●</span>
        <span className="qh-clock" style={{ fontSize: 'var(--fs-nano)', fontStyle: 'italic', color: 'var(--gold-deep)', letterSpacing: '.08em' }}></span>
      </div>
      <div className="masthead-actions">
        <GlobalRefreshBtn />
        <SearchBoxGlobal />
        <MarketSwitcher market={market} />
        {market === 'tw' && (
          <a className="icon-btn" href="寶島產業流域圖鑑.html" data-tip="產業流域圖鑑">{_I_BOOK}</a>
        )}
        <button className="ruflo-toggle-btn" onClick={_rufloToggle} data-tip="分析隊 · 計畫與日誌">
          {_I_TEAM}
          <span className="kai" style={{ fontSize:'var(--fs-nano)' }}>隊員</span>
        </button>
      </div>
    </header>
    </>
  );
}

function NavBar({ active }) {
  // Auto-detect current page when active prop is not provided
  var _active = active || (function() {
    var pathname = window.location.pathname.split('/').pop() || '';
    var search = window.location.search;
    for (var i = 0; i < NAV_PAGES.length; i++) {
      var p = NAV_PAGES[i];
      if (p.dev) continue;
      var hrefFile = p.href.split('?')[0];
      var hrefSearch = p.href.includes('?') ? '?' + p.href.split('?')[1] : '';
      if (pathname === hrefFile && (hrefSearch === '' || search === hrefSearch)) return p.id;
    }
    return '';
  })();
  return (
    <nav className="nav-bar">
      <div className="nav-bar-inner">
        {NAV_PAGES.map(p => p.dev ? (
          <span key={p.id} className="nav-link nav-link-dev" title="開發中，敬請期待">
            <span className="nav-kai kai">{p.label}</span>
            <span className="nav-en">Dev</span>
          </span>
        ) : (
          <a key={p.id} href={p.href}
            className={`nav-link ${_active === p.id ? 'active' : ''}`}
            aria-current={_active === p.id ? 'page' : undefined}>
            <span className="nav-kai kai">{p.label}</span>
            <span className="nav-en">{p.en}</span>
          </a>
        ))}
      </div>
    </nav>
  );
}

function TickerTape() {
  var [liveItems, setLiveItems] = useState(window.MARKET_TICKER || []);

  useEffect(function() {   // QuoteHub v2 訂閱:自有 timer 與 qh:refresh 監聽都交給主迴圈
    return window.QuoteHub.subscribe({ type: 'taiex', onData: function(d) {
      var msgs = d.msgArray || [];
      var base = (window.MARKET_TICKER || []).map(function(t) { return Object.assign({}, t); });
      msgs.forEach(function(msg) {
        var cur  = parseFloat(msg.z || msg.c || '0') || 0;
        var prev = parseFloat(msg.y || '0') || 0;
        if (cur <= 0) return;
        var chg  = prev > 0 ? cur - prev : 0;
        var n = msg.n || '';
        if (n.indexOf('加權') !== -1) {
          base[0] = { code:'TAIEX', name:'加權指數', value: cur, chg: chg };
        } else if (n.indexOf('上櫃') !== -1) {
          base[1] = { code:'OTC', name:'櫃買指數', value: cur, chg: chg };
        }
      });
      setLiveItems(base);
    } });
  }, []);

  // 點擊跳轉:指數 → 大盤總覽,其餘(匯率/原物料/費半)→ 另類指標
  function tickerHref(code) {
    if (code === 'TAIEX' || code === 'OTC') return '大盤總覽.html';
    return '另類指標.html';
  }

  var items = [...liveItems, ...liveItems];
  return (
    <div className="ticker">
      <span className="ticker-live"><i className="live-dot" />Live</span>
      <div className="ticker-track">
        {items.map((m, i) => (
          <a key={i} className="ticker-item" href={tickerHref(m.code)}>
            <span className="name kai">{m.name}</span>
            <span className="val">{m.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}</span>
            <span className={`chg ${m.chg >= 0 ? 'up' : 'dn'}`}>
              {m.chg >= 0 ? '+' : ''}{m.chg.toFixed(2)}
            </span>
          </a>
        ))}
      </div>
    </div>
  );
}

function SearchBoxGlobal() {
  const [q, setQ] = useState('');
  const [open, setOpen] = useState(false);
  const allCompanies = useMemo(() => {
    const list = [];
    (window.INDUSTRIES || []).forEach(ind => {
      ['upstream', 'midstream', 'downstream'].forEach(seg => {
        (ind.nodes[seg] || []).forEach(n => {
          n.companies.forEach(c => list.push({ ...c, indId: ind.id, indName: ind.name }));
        });
      });
    });
    return list;
  }, []);
  const results = useMemo(() => {
    if (!q.trim()) return [];
    const Q = q.trim().toLowerCase();
    const seen = new Set();
    return allCompanies
      .filter(c => {
        if (seen.has(c.code)) return false;
        if (c.code.includes(Q) || c.name.includes(q.trim())) { seen.add(c.code); return true; }
        return false;
      })
      .slice(0, 8);
  }, [q, allCompanies]);
  return (
    <div style={{ position: 'relative' }}>
      <div className="search-box">
        {_I_SEARCH}
        <input
          placeholder="Search by code or 名稱 …"
          value={q}
          onChange={e => { setQ(e.target.value); setOpen(true); }}
          onFocus={() => setOpen(true)}
          onBlur={() => setTimeout(() => setOpen(false), 200)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && results[0]) {
              location.href = `個股技術分析.html?code=${results[0].code}`;
            }
          }}
        />
      </div>
      {open && results.length > 0 && (
        <div className="search-results">
          {results.map(r => (
            <a key={r.code} className="search-result" href={`個股技術分析.html?code=${r.code}`}
               onMouseDown={(e) => { e.preventDefault(); location.href=`個股技術分析.html?code=${r.code}`; }}>
              <div className="code">{r.code}</div>
              <div>
                <div className="name">{r.name}</div>
                <div className="ind">{r.indName}</div>
              </div>
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

function InstallBanner() {
  var [prompt, setPrompt] = useState(null);
  var [visible, setVisible] = useState(false);
  var [dismissed, setDismissed] = useState(
    function() { return localStorage.getItem('pwa_install_dismissed') === '1'; }
  );

  useEffect(function() {
    if (dismissed) return;
    var handler = function(e) {
      e.preventDefault();
      setPrompt(e);
      setVisible(true);
    };
    window.addEventListener('beforeinstallprompt', handler);
    return function() { window.removeEventListener('beforeinstallprompt', handler); };
  }, [dismissed]);

  function install() {
    if (!prompt) return;
    prompt.prompt();
    prompt.userChoice.then(function() { setVisible(false); setPrompt(null); });
  }
  function dismiss() {
    localStorage.setItem('pwa_install_dismissed', '1');
    setVisible(false);
    setDismissed(true);
  }

  if (!visible || dismissed) return null;
  return (
    <div style={{
      position:'fixed', bottom:66, left:12, right:12, zIndex:300,
      background:'var(--ink)', color:'var(--paper-bright)',
      padding:'12px 16px', display:'flex', alignItems:'center', gap:12,
      boxShadow:'0 4px 20px rgba(0,0,0,.35)',
    }}>
      <div style={{ flex:1 }}>
        <div style={{ fontFamily:'BiauKai,標楷體,serif', fontSize:14, letterSpacing:'.08em' }}>加入主畫面</div>
        <div style={{ fontSize:'var(--fs-nano)', color:'rgba(246,241,228,.65)', marginTop:2 }}>像 App 一樣開啟，支援離線瀏覽</div>
      </div>
      <button onClick={install} style={{
        background:'var(--gold)', color:'var(--ink)', border:'none',
        padding:'7px 14px', cursor:'pointer', fontFamily:'BiauKai,標楷體,serif',
        fontSize:13, letterSpacing:'.06em', flexShrink:0,
      }}>安裝</button>
      <button onClick={dismiss} style={{
        background:'transparent', border:'1px solid rgba(246,241,228,.3)',
        color:'rgba(246,241,228,.65)', padding:'6px 10px', cursor:'pointer',
        fontSize:12, flexShrink:0,
      }}>✕</button>
    </div>
  );
}

const _ICON_BAR   = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="3" y="13" width="4" height="8"/><rect x="10" y="8" width="4" height="13"/><rect x="17" y="3" width="4" height="18"/></svg>);
const _ICON_CHART = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><polyline points="3,17 7,11 11,14 15,7 19,10"/><line x1="19" y1="10" x2="21" y2="10"/></svg>);
const _ICON_RADAR = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/><path d="M11 7v4l2.5 2.5"/></svg>);
const _ICON_STAR  = (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/></svg>);

const MOBILE_TABS = [
  { id: 'dashboard', label: '大盤', href: '大盤總覽.html',    keywords: ['大盤','dashboard'],   icon: _ICON_BAR   },
  { id: 'stock',     label: '個股', href: '個股技術分析.html', keywords: ['個股技術'],           icon: _ICON_CHART },
  { id: 'radarzone', label: '雷達', href: '雷達區.html', keywords: ['雷達','radar'],          icon: _ICON_RADAR },
  { id: 'watchlist', label: '自選', href: '自選股.html',       keywords: ['自選股.html'],        icon: _ICON_STAR  },
];
const US_MOBILE_TABS = [
  { id: 'us-dashboard', label: '大盤', href: '美股總覽.html',    keywords: ['美股總覽'],    icon: _ICON_BAR   },
  { id: 'us-stock',     label: '個股', href: '美股個股.html',    keywords: ['美股個股'],    icon: _ICON_CHART },
  { id: 'us-industry',  label: '產業', href: '美股產業地圖.html',keywords: ['美股產業'],    icon: _ICON_RADAR },
  { id: 'us-watchlist', label: '自選', href: '美股自選股.html',  keywords: ['美股自選股'],  icon: _ICON_STAR  },
];

function MobileNav() {
  var href = window.location.href;
  var isUS = /美股/.test(decodeURIComponent(href));
  var tabs = isUS ? US_MOBILE_TABS : MOBILE_TABS;
  var hrefLow = href.toLowerCase();
  var active = '';
  tabs.forEach(function(tab) {
    tab.keywords.forEach(function(kw) {
      if (decodeURIComponent(hrefLow).includes(kw.toLowerCase())) active = tab.id;
    });
  });
  return (
    <nav className="mobile-bottom-nav" aria-label="主選單">
      {tabs.map(function(tab) {
        return (
          <a key={tab.id} href={tab.href}
             className={'mbn-item' + (active === tab.id ? ' mbn-active' : '')}>
            <span className="mbn-icon">{tab.icon}</span>
            <span className="mbn-label kai">{tab.label}</span>
          </a>
        );
      })}
    </nav>
  );
}

function FooterBar() {
  return (
    <>
      <RufloSidebar />
      <InstallBanner />
      <MobileNav />
      <footer className="foot">
        <span>© MMXXVI <em style={{ fontStyle: 'italic' }}>Formosa &amp; Co.</em> · All cartographies reserved.</span>
        <span className="crest">「 觀 山 知 流 · 識 流 知 海 」</span>
        <span>Data shown for design demonstration.</span>
      </footer>
    </>
  );
}

function SectionHead({ kicker, title, titleEn, sub }) {
  return (
    <div className="sec-head">
      {kicker && <div className="sec-kicker">{kicker}</div>}
      <div className="sec-title-row">
        <h2 className="sec-title">
          <span className="ornament">❦</span>
          <span className="kai">{title}</span>
          {titleEn && <span className="sec-title-en">{titleEn}</span>}
        </h2>
        {sub && <div className="sec-sub kai">{sub}</div>}
      </div>
    </div>
  );
}

// ── 跨頁即時報價共享（localStorage + BroadcastChannel）──────
const _RT_CACHE_KEY = 'tw_rt_cache_v1';
const _RT_CHANNEL   = 'tw_rt_update';

function rtCacheWrite(code, price, prev, name) {
  if (!code || !(price > 0)) return;
  try {
    const cache = JSON.parse(localStorage.getItem(_RT_CACHE_KEY) || '{}');
    cache[code] = { price, prev, name: name || code, ts: Date.now() };
    // 保留最近 30 筆，依時間排序
    const trimmed = {};
    Object.keys(cache)
      .sort(function(a, b) { return cache[b].ts - cache[a].ts; })
      .slice(0, 30)
      .forEach(function(k) { trimmed[k] = cache[k]; });
    localStorage.setItem(_RT_CACHE_KEY, JSON.stringify(trimmed));
  } catch(_) {}
  // BroadcastChannel — 通知其他已開啟的頁面
  try {
    var ch = new BroadcastChannel(_RT_CHANNEL);
    ch.postMessage({ code, price, prev, name: name || code, ts: Date.now() });
    ch.close();
  } catch(_) {}
}

function rtCacheRead() {
  try { return JSON.parse(localStorage.getItem(_RT_CACHE_KEY) || '{}'); }
  catch(_) { return {}; }
}

Object.assign(window, { Masthead, NavBar, USNavBar, CryptoNavBar, TickerTape, FooterBar, SectionHead, SearchBoxGlobal, MobileNav, InstallBanner, MarketSwitcher, RufloSidebar, rtCacheWrite, rtCacheRead });

(function() {
  var seq = '';
  var t = null;
  document.addEventListener('keydown', function(e) {
    var tag = e.target.tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || e.target.isContentEditable) return;
    seq += e.key.toLowerCase();
    if (seq.length > 3) seq = seq.slice(-3);
    clearTimeout(t);
    t = setTimeout(function() { seq = ''; }, 1500);
    if (seq === 'cry') window.location.href = '加密貨幣.html';
  });
})();

/* ============================================================
   互動升級層 v2.5 — 2026-06-11
   1) 滾動進場(IO + MutationObserver,支援 React 動態渲染)
   2) 數字滾動 countUp(首次進入視窗,只動文字節點)
   3) Command Palette ⌘K / Ctrl+K(vanilla DOM,不依賴 React)
   ============================================================ */
(function interactionLayer() {
  var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* ── 1. 滾動進場 ─────────────────────────────────────── */
  var SR_SEL = '.frame-card, .sec-head, .stat-strip, .fund-section';
  if (!REDUCED && 'IntersectionObserver' in window) {
    document.body.classList.add('sr-on');
    var batchN = 0, batchT = null;
    var io = new IntersectionObserver(function(entries) {
      entries.forEach(function(en) {
        if (!en.isIntersecting) return;
        // 同一波進場的元素做小階梯延遲(雜誌翻頁節奏)
        en.target.style.setProperty('--sr-delay', Math.min(batchN * 0.07, 0.42) + 's');
        batchN++;
        clearTimeout(batchT);
        batchT = setTimeout(function() { batchN = 0; }, 220);
        en.target.classList.remove('sr-hide');
        en.target.classList.add('sr-in');
        io.unobserve(en.target);
      });
    }, { threshold: 0.06, rootMargin: '0px 0px -4% 0px' });

    var srAttach = function(el) {
      if (el.dataset.srObs) return;
      el.dataset.srObs = '1';
      el.classList.add('sr-hide');
      io.observe(el);
    };
    var srScan = function(root) {
      if (root.nodeType !== 1) return;
      if (root.matches && root.matches(SR_SEL)) srAttach(root);
      if (root.querySelectorAll) root.querySelectorAll(SR_SEL).forEach(srAttach);
    };
    srScan(document.body);
    new MutationObserver(function(muts) {
      muts.forEach(function(m) { m.addedNodes.forEach(srScan); });
    }).observe(document.body, { childList: true, subtree: true });
  }

  /* ── 2. 數字滾動 countUp ─────────────────────────────── */
  var CU_SEL = '.stat-cell .val, .big-num, .fund-price, .fund-metric-tile .mt-value, .fund-val-price';
  if (!REDUCED && 'IntersectionObserver' in window) {
    var easeOut = function(p) { return 1 - Math.pow(1 - p, 3); };
    var animateNode = function(textNode, numStr) {
      var target = parseFloat(numStr.replace(/,/g, ''));
      if (!isFinite(target) || Math.abs(target) > 1e12) return;
      var decimals = (numStr.split('.')[1] || '').length;
      var hasComma = numStr.indexOf(',') !== -1;
      var full = textNode.data;
      var t0 = null;
      var DUR = 750;
      var step = function(ts) {
        if (!t0) t0 = ts;
        var p = Math.min((ts - t0) / DUR, 1);
        var v = target * easeOut(p);
        var s = v.toLocaleString('en-US', {
          minimumFractionDigits: decimals, maximumFractionDigits: decimals,
          useGrouping: hasComma,
        });
        textNode.data = full.replace(numStr, s);
        if (p < 1) requestAnimationFrame(step);
        else textNode.data = full; // 保證收在精確原值
      };
      requestAnimationFrame(step);
    };
    var cuIO = new IntersectionObserver(function(entries) {
      entries.forEach(function(en) {
        if (!en.isIntersecting) return;
        cuIO.unobserve(en.target);
        var el = en.target;
        if (el.dataset.counted) return;
        el.dataset.counted = '1';
        // 只處理第一個文字節點,保留子元素(如單位 small)
        var tn = null;
        for (var c = el.firstChild; c; c = c.nextSibling) {
          if (c.nodeType === 3 && /\d/.test(c.data)) { tn = c; break; }
        }
        if (!tn) return;
        var m = tn.data.match(/-?\d[\d,]*\.?\d*/);
        if (m) animateNode(tn, m[0]);
      });
    }, { threshold: 0.4 });
    var cuAttach = function(el) {
      if (el.dataset.cuObs || el.closest('.ticker')) return;
      el.dataset.cuObs = '1';
      cuIO.observe(el);
    };
    var cuScan = function(root) {
      if (root.nodeType !== 1) return;
      if (root.matches && root.matches(CU_SEL)) cuAttach(root);
      if (root.querySelectorAll) root.querySelectorAll(CU_SEL).forEach(cuAttach);
    };
    cuScan(document.body);
    new MutationObserver(function(muts) {
      muts.forEach(function(m) { m.addedNodes.forEach(cuScan); });
    }).observe(document.body, { childList: true, subtree: true });
  }

  /* ── 3. Command Palette ⌘K ───────────────────────────── */
  var ckpEl = null, ckpSel = 0, ckpItems = [];

  function ckpPages() {
    var out = [];
    (window.NAV_PAGES || []).forEach(function(p) { if (!p.dev) out.push({ zh: p.label, en: p.en, href: p.href, tag: '台股' }); });
    (window.US_NAV_PAGES || []).forEach(function(p) { out.push({ zh: p.label, en: p.en, href: p.href, tag: '美股' }); });
    (window.CRYPTO_NAV_PAGES || []).forEach(function(p) { out.push({ zh: p.label, en: p.en, href: p.href, tag: '加密' }); });
    return out;
  }

  var _ckpCompanies = null;
  function ckpCompanies() {
    if (_ckpCompanies) return _ckpCompanies;
    var list = [];
    (window.INDUSTRIES || []).forEach(function(ind) {
      ['upstream', 'midstream', 'downstream'].forEach(function(seg) {
        ((ind.nodes || {})[seg] || []).forEach(function(n) {
          (n.companies || []).forEach(function(c) {
            list.push({ zh: c.name, en: c.code, href: '個股技術分析.html?code=' + c.code, tag: '個股' });
          });
        });
      });
    });
    _ckpCompanies = list;
    return list;
  }

  function ckpFilter(q) {
    q = q.trim();
    var out = [];
    if (!q) return ckpPages();
    var lq = q.toLowerCase();
    // 代號直達
    if (/^\d{4,6}[A-Za-z]?$/.test(q)) {
      out.push({ zh: '個股技術分析 ' + q, en: 'TW Stock', href: '個股技術分析.html?code=' + q, tag: '直達' });
    } else if (/^[A-Za-z][A-Za-z.\-]{0,5}$/.test(q)) {
      out.push({ zh: '美股個股 ' + q.toUpperCase(), en: 'US Stock', href: '美股個股.html?code=' + q.toUpperCase(), tag: '直達' });
    }
    ckpPages().forEach(function(p) {
      if (p.zh.indexOf(q) !== -1 || p.en.toLowerCase().indexOf(lq) !== -1) out.push(p);
    });
    ckpCompanies().forEach(function(c) {
      if (out.length >= 12) return;
      if (c.zh.indexOf(q) !== -1 || c.en.indexOf(q) !== -1) out.push(c);
    });
    return out.slice(0, 12);
  }

  function ckpRender(q) {
    ckpItems = ckpFilter(q);
    ckpSel = 0;
    var list = ckpEl.querySelector('.ckp-list');
    list.innerHTML = '';
    if (!ckpItems.length) {
      var em = document.createElement('div');
      em.className = 'ckp-empty';
      em.textContent = 'No matches — 查無結果';
      list.appendChild(em);
      return;
    }
    ckpItems.forEach(function(it, i) {
      var row = document.createElement('div');
      row.className = 'ckp-item' + (i === 0 ? ' sel' : '');
      row.innerHTML =
        '<span class="ckp-zh"></span><span class="ckp-en"></span><span class="ckp-tag"></span>';
      row.children[0].textContent = it.zh;
      row.children[1].textContent = it.en;
      row.children[2].textContent = it.tag;
      row.addEventListener('mouseenter', function() { ckpSelect(i); });
      row.addEventListener('click', function() { ckpGo(i); });
      list.appendChild(row);
    });
  }

  function ckpSelect(i) {
    ckpSel = i;
    var rows = ckpEl.querySelectorAll('.ckp-item');
    rows.forEach(function(r, j) { r.classList.toggle('sel', j === i); });
    if (rows[i]) rows[i].scrollIntoView({ block: 'nearest' });
  }

  function ckpGo(i) {
    var it = ckpItems[i];
    if (it) window.location.href = it.href;
  }

  function ckpClose() {
    if (ckpEl) { ckpEl.remove(); ckpEl = null; }
  }

  function ckpOpen() {
    if (ckpEl) return;
    ckpEl = document.createElement('div');
    ckpEl.className = 'ckp-backdrop';
    ckpEl.innerHTML =
      '<div class="ckp-panel">' +
        '<div class="ckp-input-row">' +
          '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>' +
          '<input class="ckp-input" placeholder="搜尋頁面、代號、公司⋯" />' +
          '<span class="ckp-kbd">esc</span>' +
        '</div>' +
        '<div class="ckp-list"></div>' +
      '</div>';
    ckpEl.addEventListener('mousedown', function(e) { if (e.target === ckpEl) ckpClose(); });
    document.body.appendChild(ckpEl);
    var input = ckpEl.querySelector('.ckp-input');
    input.addEventListener('input', function() { ckpRender(input.value); });
    input.addEventListener('keydown', function(e) {
      if (e.key === 'ArrowDown') { e.preventDefault(); ckpSelect(Math.min(ckpSel + 1, ckpItems.length - 1)); }
      else if (e.key === 'ArrowUp') { e.preventDefault(); ckpSelect(Math.max(ckpSel - 1, 0)); }
      else if (e.key === 'Enter') { e.preventDefault(); ckpGo(ckpSel); }
      else if (e.key === 'Escape') { e.preventDefault(); ckpClose(); }
    });
    ckpRender('');
    input.focus();
  }

  document.addEventListener('keydown', function(e) {
    if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
      e.preventDefault();
      ckpEl ? ckpClose() : ckpOpen();
    } else if (e.key === 'Escape' && ckpEl) {
      ckpClose();
    }
  });
  window.openCommandPalette = ckpOpen;
})();
