36cdc8fba6
① tests/portal-admin-ai.test.ts(6 項綠)=**route 消失就會紅**的迴歸防線。 這條 route 以前不存在、前端卻一直在打它 ⇒ key 從來沒存進去(藍字=假綠), 所以最該守的就是「它還在不在」。覆蓋:未登入 401(**不是 404**=route 真的在)/ 非 admin 403/GET 回 has_key 布林且回應不含任何金鑰欄位(D36)/ 空 body 400 不假裝成功/只改偏好不誤報 has_key/同測試內寫→讀一致。 註:vitest-pool-workers 預設 isolatedStorage=true ⇒ 跨 it 的 KV 會還原, 驗來回一致必須在同一個 it 內完成(我第一版寫成跨 it,測試如實抓出來了)。 ② console/index.html 與 console/dashboard/index.html 也有同一個寫死 `|| "https://cypher.arcrun.dev"` fallback(與 portal 同族)⇒ 一併拔掉。 靜態 config.js 保留無妨:build-ui-bundle.mjs 的 SKIP 會跳過它、改由 worker 動態產生。 驗證:cypher tsc 綠、全套 248 passed(9 既有失敗未變); UI 產物 grep 寫死 fallback 歸 0;stage verify.sh 仍 11/3(無迴歸)。
301 lines
20 KiB
HTML
301 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="zh-Hant">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Arcrun 駕駛艙</title>
|
||
<script>
|
||
// 主題預載(防閃色):預設淺色(leo 2026-07-04 明示),與 /console 共用同一 localStorage key
|
||
document.documentElement.setAttribute('data-theme', (function () {
|
||
try { return localStorage.getItem('arcrun_console_theme') === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; }
|
||
})());
|
||
</script>
|
||
<style>
|
||
/* Mira Console 定稿視覺(紙感「2a」,Mira Style Guide 2026-07-04):
|
||
紙紋底 repeating-linear-gradient、明體標題級聯、琥珀強調、呼吸狀態球嵌單字。
|
||
2026-07-04 二輪:CSS custom properties 兩份色板——預設淺色(宣紙米白+墨字),深色=原定稿暖黑不動。 */
|
||
* { box-sizing: border-box; }
|
||
:root {
|
||
--paper-a: #f4eddc; --paper-b: #f1e9d6;
|
||
--ink: #2f2a20; --ink-rgb: 30,24,14;
|
||
--amber: #8a5f1e; --amber-rgb: 138,95,30;
|
||
--ok: #1d7a48; --ok-rgb: 29,122,72;
|
||
--err: #b03a26; --err-rgb: 176,58,38;
|
||
--track: rgba(30,24,14,.12);
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--paper-a: #191410; --paper-b: #1b1611;
|
||
--ink: #ede4d3; --ink-rgb: 237,228,211;
|
||
--amber: #e8b45a; --amber-rgb: 232,180,90;
|
||
--ok: #7fe0a8; --ok-rgb: 63,190,120;
|
||
--err: #e58575; --err-rgb: 217,95,76;
|
||
--track: rgba(255,255,255,.08);
|
||
}
|
||
html, body { margin: 0; background: repeating-linear-gradient(0deg,var(--paper-a) 0px,var(--paper-a) 3px,var(--paper-b) 3px,var(--paper-b) 4px); color: var(--ink);
|
||
font-family: -apple-system, "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
|
||
.serif { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; }
|
||
main { max-width: 560px; margin: 0 auto; padding: 0 20px 40px; }
|
||
.pagehead { padding: 22px 2px 14px; border-bottom: 2px solid rgba(var(--amber-rgb),.4); display: flex; justify-content: space-between; align-items: baseline; }
|
||
.pagehead .title { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; letter-spacing: .2em; }
|
||
.pagehead .title small { font-size: 14px; letter-spacing: .3em; color: rgba(var(--ink-rgb),.5); }
|
||
.pagehead .date { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 14px; color: rgba(var(--ink-rgb),.55); }
|
||
.orb-row { display: flex; align-items: center; gap: 20px; padding: 26px 2px 20px; }
|
||
.orb { width: 84px; height: 84px; border-radius: 50%; flex: none; display: grid; place-items: center; }
|
||
.orb span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 30px; font-weight: 600; color: rgba(10,20,14,.85); text-shadow: 0 1px 0 rgba(255,255,255,.25); }
|
||
.orb-title { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; font-weight: 600; }
|
||
.orb-sub { margin-top: 5px; font-size: 15px; color: rgba(var(--ink-rgb),.6); line-height: 1.55; }
|
||
@keyframes breatheGreen { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--ok-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--ok-rgb),.55); } }
|
||
@keyframes breatheAmber { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--amber-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--amber-rgb),.6); } }
|
||
@keyframes breatheRed { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--err-rgb),.4); } 50% { box-shadow: 0 0 44px 16px rgba(var(--err-rgb),.65); } }
|
||
.bricks { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.brick { padding: 16px; border-radius: 12px; }
|
||
.brick.amber { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.22); }
|
||
.brick.plain { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.14); }
|
||
.brick .lbl { font-size: 13.5px; color: rgba(var(--ink-rgb),.55); margin-bottom: 6px; }
|
||
.brick .num { font-family: ui-monospace, Menlo, monospace; font-size: 26px; color: var(--amber); }
|
||
.brick .num small { font-size: 15px; color: rgba(var(--ink-rgb),.5); }
|
||
.bar { margin-top: 10px; height: 6px; border-radius: 3px; background: var(--track); }
|
||
.bar > i { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg,#b98330,#e8b45a); transition: width .6s; }
|
||
.wait-box { margin-top: 14px; padding: 20px; border-radius: 12px; border: 1px dashed rgba(var(--ok-rgb),.3); background: rgba(var(--ok-rgb),.05); }
|
||
.wait-box.has { border-color: rgba(var(--amber-rgb),.45); background: rgba(var(--amber-rgb),.05); }
|
||
.wait-head { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); margin-bottom: 10px; text-align: center; }
|
||
.wait-none { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 20px; color: var(--ok); letter-spacing: .08em; text-align: center; }
|
||
.wait-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-top: 8px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.3); font-size: 16px; line-height: 1.5; }
|
||
.wait-item .dm { color: var(--amber); font-size: 17px; flex: none; }
|
||
.wait-meta { margin-top: 10px; text-align: center; font-size: 12.5px; color: rgba(var(--ink-rgb),.45); line-height: 1.7; }
|
||
.wait-meta .warn { color: var(--err); }
|
||
.subhead { display: flex; justify-content: space-between; align-items: baseline; margin: 24px 0 10px; }
|
||
.subhead .t { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); }
|
||
.subhead .m { font-size: 13px; color: rgba(var(--ink-rgb),.4); }
|
||
ul.route { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||
ul.route li { display: flex; align-items: flex-start; gap: 12px; padding: 13px 16px; border-radius: 11px; background: rgba(var(--ink-rgb),.045); border: 1px solid transparent; font-size: 16px; line-height: 1.4; }
|
||
ul.route li.doing { background: rgba(var(--amber-rgb),.09); border-color: rgba(var(--amber-rgb),.3); }
|
||
ul.route li .ic { flex: none; font-size: 15px; margin-top: 2px; }
|
||
ul.route li.done { color: rgba(var(--ink-rgb),.65); }
|
||
ul.route li.done .ic { color: var(--ok); }
|
||
ul.route li.doing .ic { color: var(--amber); }
|
||
ul.route li.todo { color: rgba(var(--ink-rgb),.6); }
|
||
ul.route li.todo .ic { color: rgba(var(--ink-rgb),.35); }
|
||
ul.route li.blocked .ic { color: var(--err); }
|
||
ul.route.faded li { opacity: .55; }
|
||
.sys { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
|
||
.sys .row { display: flex; justify-content: space-between; align-items: baseline; padding: 10px 14px; border-radius: 10px; background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.12); font-size: 14.5px; }
|
||
.sys .row .k { color: rgba(var(--ink-rgb),.6); }
|
||
.sys .row .v { font-family: ui-monospace, Menlo, monospace; font-size: 14px; }
|
||
.sys .ok { color: var(--ok); }
|
||
.sys .bad { color: var(--err); }
|
||
.sys .off { color: rgba(var(--ink-rgb),.5); }
|
||
.muted { color: rgba(var(--ink-rgb),.45); font-size: 14px; }
|
||
.err { color: var(--err); font-size: 14px; }
|
||
.stamp { margin: 16px 0 8px; text-align: center; font-size: 12.5px; color: rgba(var(--ink-rgb),.35); line-height: 1.8; }
|
||
.enter { display: block; text-align: center; font-size: 13.5px; color: rgba(var(--amber-rgb),.75); text-decoration: none; margin-top: 6px; }
|
||
.theme-btn { flex: none; margin-left: 12px; width: 34px; height: 34px; border-radius: 50%; border: 1px solid rgba(var(--ink-rgb),.25); background: none; color: rgba(var(--ink-rgb),.65); font-size: 16px; cursor: pointer; line-height: 1; align-self: center; }
|
||
</style>
|
||
<script src="/config.js"></script>
|
||
<script>
|
||
// 2026-08-01(arcrun-rag#10 同族):拔掉寫死中央位址的 fallback。
|
||
// apiBase 由 worker 動態產生的 /config.js 注入;缺它就讓它明顯壞掉,
|
||
// **不要靜默把請求(可能含金鑰)送去中央實例**。
|
||
window.ARCRUN_API_BASE = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.apiBase) || "";
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<div class="pagehead">
|
||
<div class="title serif">Arcrun<small> 駕駛艙</small></div>
|
||
<div style="display:flex;align-items:baseline">
|
||
<div class="date serif" id="date-str"></div>
|
||
<button class="theme-btn" id="theme-btn" title="切換深/淺色">☾</button>
|
||
</div>
|
||
</div>
|
||
<div class="orb-row">
|
||
<div class="orb" id="orb" style="background:radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)"><span id="orb-char">…</span></div>
|
||
<div>
|
||
<div class="orb-title" id="orb-title">載入中</div>
|
||
<div class="orb-sub" id="orb-sub"></div>
|
||
</div>
|
||
</div>
|
||
<div class="bricks">
|
||
<div class="brick amber">
|
||
<div class="lbl">今日完成</div>
|
||
<div class="num"><span id="done-n">–</span><small> / <span id="total-n">–</span> 件</small></div>
|
||
<div class="bar"><i id="bar-fill" style="width:0%"></i></div>
|
||
</div>
|
||
<div class="brick plain">
|
||
<div class="lbl">收件匣未處理</div>
|
||
<div class="num"><span id="inbox-n">–</span><small> 條</small></div>
|
||
<div class="lbl" style="margin:10px 0 0">來自 Telegram</div>
|
||
</div>
|
||
</div>
|
||
<div class="wait-box" id="wait-box">
|
||
<div class="wait-head">等你的事</div>
|
||
<div id="wait-body" class="wait-none">載入中…</div>
|
||
<div class="wait-meta" id="wait-meta"></div>
|
||
</div>
|
||
<div class="subhead"><span class="t">今日路線</span><span class="m" id="route-m"></span></div>
|
||
<ul class="route" id="today-list"><li class="todo"><span class="ic">○</span>載入中…</li></ul>
|
||
<div class="subhead" id="week-head" style="display:none"><span class="t">本週</span></div>
|
||
<ul class="route" id="week-list"></ul>
|
||
<div class="subhead"><span class="t">系統狀況</span><span class="m">live 健康信號</span></div>
|
||
<div class="sys" id="sys-list"><div class="row"><span class="k">載入中…</span></div></div>
|
||
<div class="stamp" id="stamp">每 60 秒自動刷新</div>
|
||
<a class="enter" href="/console">進入完整控制台 ›</a>
|
||
</main>
|
||
<script>
|
||
(function () {
|
||
var API_BASE = window.ARCRUN_API_BASE || '';
|
||
// 台北時間 helper(lib/taipei-time.ts 注入,與 server 判定同一套——顯示不隨看的裝置時區漂移)
|
||
var TAIPEI_OFFSET_MS = 28800000; // UTC+8,台北無 DST
|
||
function tpePad(n) { n = String(n); return n.length < 2 ? '0' + n : n; }
|
||
function taipeiDayKey(ms) { return new Date(ms + TAIPEI_OFFSET_MS).toISOString().slice(0, 10); }
|
||
function taipeiDateStr(ms) { return taipeiDayKey(ms); }
|
||
function taipeiDateTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return taipeiDayKey(ms) + ' ' + tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()); }
|
||
function taipeiTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()) + ':' + tpePad(d.getUTCSeconds()); }
|
||
function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { month: d.getUTCMonth() + 1, day: d.getUTCDate() }; }
|
||
const $ = (id) => document.getElementById(id);
|
||
const LIGHT = {
|
||
green: { ch: '安', title: '系統運轉中', grad: 'radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)', anim: 'breatheGreen' },
|
||
yellow: { ch: '趕', title: '落後趕工中', grad: 'radial-gradient(circle at 36% 30%,#f2d194,#e8b45a 55%,#8a5f1e 100%)', anim: 'breatheAmber' },
|
||
red: { ch: '滯', title: '卡住或斷訊', grad: 'radial-gradient(circle at 36% 30%,#f0a094,#d95f4c 55%,#7e2c20 100%)', anim: 'breatheRed' }
|
||
};
|
||
const ICONS = { done: '✓', doing: '◐', todo: '○', blocked: '●' };
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
}
|
||
function taskLine(t) {
|
||
const cls = ICONS[t.status] ? t.status : 'blocked';
|
||
const ic = ICONS[t.status] || '●';
|
||
return '<li class="' + cls + '"><span class="ic">' + ic + '</span><span>' + esc(t.title) + '</span></li>';
|
||
}
|
||
function humanAge(m) {
|
||
if (m == null || m < 0) return '時間不明';
|
||
if (m < 60) return m + ' 分鐘前';
|
||
if (m < 2880) return Math.round(m / 60) + ' 小時前';
|
||
return Math.round(m / 1440) + ' 天前';
|
||
}
|
||
const CNUM = ['零','一','二','三','四','五','六','七','八','九','十'];
|
||
function cnDay(n) { return n <= 10 ? CNUM[n] : (n < 20 ? '十' + (n % 10 ? CNUM[n % 10] : '') : CNUM[Math.floor(n / 10)] + '十' + (n % 10 ? CNUM[n % 10] : '')); }
|
||
// 頁首日期=台北日(原本用瀏覽器本地時區,換裝置會漂)
|
||
const nowTpe = taipeiMonthDay(Date.now());
|
||
$('date-str').textContent = CNUM[nowTpe.month] + '月' + cnDay(nowTpe.day) + '日';
|
||
// 深/淺切換(與 /console 共用 arcrun_console_theme;預設淺色)
|
||
function syncThemeBtn() { $('theme-btn').textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '☀' : '☾'; }
|
||
$('theme-btn').addEventListener('click', () => {
|
||
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', next);
|
||
try { localStorage.setItem('arcrun_console_theme', next); } catch (e) { /* 私密模式忽略 */ }
|
||
syncThemeBtn();
|
||
});
|
||
syncThemeBtn();
|
||
// fetch 失敗(斷網)的裸訊息 → 友善誠實文案;60 秒定時器常駐,網路恢復自動刷回
|
||
function friendlyErr(e) {
|
||
const m = e && e.message ? String(e.message) : String(e);
|
||
return /failed to fetch|load failed|networkerror|network request failed/i.test(m) ? '連線中斷' : m;
|
||
}
|
||
function sysRow(k, v, cls) {
|
||
return '<div class="row"><span class="k">' + esc(k) + '</span><span class="v ' + cls + '">' + esc(v) + '</span></div>';
|
||
}
|
||
async function load() {
|
||
try {
|
||
const res = await fetch(API_BASE + '/console/dashboard-data');
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const d = await res.json();
|
||
const cfg = LIGHT[d.light] || LIGHT.green;
|
||
const orb = $('orb');
|
||
orb.style.background = cfg.grad;
|
||
orb.style.animation = cfg.anim + ' 3.4s ease-in-out infinite';
|
||
$('orb-char').textContent = cfg.ch;
|
||
$('orb-title').textContent = cfg.title;
|
||
$('orb-sub').textContent = (d.last_beat
|
||
? d.last_beat.actor + '・' + d.last_beat.ago_minutes + ' 分鐘前' + (d.last_beat.note ? '・' + d.last_beat.note : '')
|
||
: '尚無心跳資料') + (d.light !== 'green' && d.light_reason ? '(' + d.light_reason + ')' : '');
|
||
const done = d.today_done || 0, total = d.today_total || 0;
|
||
$('done-n').textContent = done; $('total-n').textContent = total;
|
||
$('bar-fill').style.width = (total ? Math.round((done / total) * 100) : 0) + '%';
|
||
$('inbox-n').textContent = d.inbox_new || 0;
|
||
// ── 等你的事:來源 + 維護時間攤開講,stale 一定警示 ──
|
||
const wb = $('wait-box'), body = $('wait-body'), wmeta = $('wait-meta');
|
||
const wm = d.waiting_meta || {};
|
||
if (d.waiting && d.waiting.length) {
|
||
wb.classList.add('has');
|
||
body.className = '';
|
||
body.innerHTML = d.waiting.map((w) =>
|
||
'<div class="wait-item"><span class="dm">' + (w.urgency ? esc(w.urgency) : '◆') + '</span><span>' +
|
||
(w.id ? '<b>#' + esc(w.id) + '</b> ' : '') + esc(w.title) + '</span></div>').join('');
|
||
} else {
|
||
wb.classList.remove('has');
|
||
body.className = 'wait-none';
|
||
body.textContent = wm.source === 'none' ? '(管線未接)' : '無,你不用做任何事';
|
||
}
|
||
let metaTxt = '';
|
||
if (wm.source === 'gitea_sprint') {
|
||
metaTxt = '來源:sprint 等leo清單(' + esc((wm.sprint_files || []).join('、')) + ')・清單維護於 ' + humanAge(wm.updated_ago_minutes);
|
||
if (wm.stale) metaTxt += '<br><span class="warn">⚠ 清單超過 2 天沒維護,可能過時</span>';
|
||
} else if (wm.source === 'kbdb_dash_wait') {
|
||
metaTxt = '<span class="warn">⚠ ' + esc(wm.note || 'dash_wait 殘資料') + '・上次寫入 ' + humanAge(wm.updated_ago_minutes) + ',可能過時</span>';
|
||
} else {
|
||
metaTxt = '<span class="warn">管線未接:Gitea sprint 清單與 dash_wait 皆無資料</span>';
|
||
}
|
||
wmeta.innerHTML = metaTxt;
|
||
// ── 今日路線:sprint 任務板優先(來源攤開講);dash_task fallback 沿舊誠實降級 ──
|
||
const rm = d.route_meta || {};
|
||
const today = (d.tasks || []).filter((t) => t.scope === 'today');
|
||
const week = (d.tasks || []).filter((t) => t.scope === 'week');
|
||
if (rm.source === 'gitea_sprint_board') {
|
||
$('route-m').textContent = '來源 sprint 任務板・更新於 ' + humanAge(rm.updated_ago_minutes);
|
||
$('today-list').className = 'route';
|
||
const staleHead = rm.is_today ? '' :
|
||
'<li class="todo"><span class="ic">○</span><span class="muted">⚠ 今日任務板未更新(最後 ' + humanAge(rm.updated_ago_minutes) + ')——以下是板上現況</span></li>';
|
||
$('today-list').innerHTML = staleHead + (today.length
|
||
? today.map(taskLine).join('')
|
||
: '<li class="todo"><span class="ic">○</span><span class="muted">任務板上沒有可解析的事項</span></li>');
|
||
} else if (rm.is_today) {
|
||
$('route-m').textContent = '更新於 ' + humanAge(rm.updated_ago_minutes);
|
||
$('today-list').className = 'route';
|
||
$('today-list').innerHTML = today.length ? today.map(taskLine).join('') : '<li class="todo"><span class="ic">○</span><span class="muted">今日無排定項目</span></li>';
|
||
} else if (today.length) {
|
||
$('route-m').textContent = '最後路線・' + humanAge(rm.updated_ago_minutes) + '寫入';
|
||
$('today-list').className = 'route faded';
|
||
$('today-list').innerHTML =
|
||
'<li class="todo"><span class="ic">○</span><span class="muted">今日尚無路線寫入——以下是 ' + humanAge(rm.updated_ago_minutes) +
|
||
'的殘留路線(sprint 任務板→dashboard 投影管線未接,等leo清單#15 裁決中)</span></li>' + today.map(taskLine).join('');
|
||
} else {
|
||
$('route-m').textContent = '';
|
||
$('today-list').className = 'route';
|
||
$('today-list').innerHTML = '<li class="todo"><span class="ic">○</span><span class="muted">無資料——dash_task 管線未接</span></li>';
|
||
}
|
||
$('week-head').style.display = week.length ? '' : 'none';
|
||
$('week-list').innerHTML = week.map(taskLine).join('');
|
||
// ── 系統狀況 + 總庫規模(全 live,讀不到就標讀不到)──
|
||
const sys = d.system || {}, kb = d.kb || {};
|
||
const rows = [];
|
||
rows.push(sysRow('KBDB 基本盤', sys.kbdb_ok ? '● 正常' : '● 打不通', sys.kbdb_ok ? 'ok' : 'bad'));
|
||
if (sys.embed) {
|
||
rows.push(sys.embed.enabled
|
||
? sysRow('語意嵌入', '● 啟用(已嵌 ' + (sys.embed.embedded ?? '?') + '・待嵌 ' + (sys.embed.pending ?? '?') + ')', 'ok')
|
||
: sysRow('語意嵌入', '○ 停用(已嵌 ' + (sys.embed.embedded ?? '?') + '・待嵌 ' + (sys.embed.pending ?? '?') + ')', 'bad'));
|
||
} else {
|
||
rows.push(sysRow('語意嵌入', '狀態讀不到', 'off'));
|
||
}
|
||
rows.push(sys.graph && sys.graph.ok
|
||
? sysRow('知識圖譜', '● 正常・三元組 ' + (sys.graph.triplets == null ? '?' : sys.graph.triplets), 'ok')
|
||
: sysRow('知識圖譜', '● 打不通', 'bad'));
|
||
rows.push(sysRow('工作流', sys.workflow_total == null ? '讀不到' : sys.workflow_total + ' 條', sys.workflow_total == null ? 'off' : ''));
|
||
// 精耕層 wiki 卡(leo 2026-07-07 裁:14-E 遺產總數 deprecated 不再顯示,只顯示真的新的;
|
||
// 三元組/已嵌入 已各有一列)
|
||
rows.push(sysRow('精耕層 wiki 卡', kb.wiki_card_total == null ? '讀不到' : kb.wiki_card_total + ' 張', kb.wiki_card_total == null ? 'off' : ''));
|
||
$('sys-list').innerHTML = rows.join('');
|
||
$('stamp').innerHTML = '每 60 秒自動刷新・上次 ' + esc(taipeiTimeStr(Date.parse(d.generated_at))) + '(台北)<br>此頁不含機敏內容,免登入';
|
||
} catch (e) {
|
||
$('orb-char').textContent = '?';
|
||
$('orb-title').textContent = '讀不到狀態';
|
||
$('orb-sub').innerHTML = '<span class="err">' + esc(friendlyErr(e)) + '・每 60 秒自動重試</span>';
|
||
}
|
||
}
|
||
load();
|
||
setInterval(load, 60000);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|