fix(console): 真實性稽核修正——時區統一台北、精耕層數字、今日完成接 sprint 任務板

稽核輪(fix/console-truth-audit,總管派工+leo 三裁決):

B1 時區:console 全站日期/時間顯示統一 Asia/Taipei(新 lib/taipei-time.ts,
   server 判定與頁面 client JS 同一套、等價性單測互鎖)。原本用瀏覽器本地
   getter——換裝置就換日期;憑證頁改顯「日期+時分(台北)」:raw 1783349834
   =台北 2026-07-06 22:57,7/6 顯示是真的,缺的是時分與固定時區。

B2 總庫數字(leo 裁:14-E 遺產 deprecated 不再顯示):總庫搜尋/設定頁頭部
   統計改讀新 GET /console/kb-scale-data(精耕層 wiki 卡/三元組/已嵌入+最近
   寫入,全 live、limit=1/stats 聚合不掃庫);搜尋仍搜全庫、結果 wiki_card
   加「精耕」綠標;dashboard-data 的 entries 計數補 owner_id(原混租戶:
   459,137 vs leo 458,732)。

B3 設定頁:vectorize 文案改具體事實(config kbdb_embed+acr update,#32
   教訓)+live 狀態;新增 MCP token TTL 誠實佔位(GET /console/settings-data,
   可調功能=Arcrun#19,不假裝能遠端改)。

C 今日完成/今日路線接 sprint 任務板(leo 拍板):比照 #36 等leo清單模式,
   同一輪 Gitea fetch(90s CF cache 共用)解析「## 任務板」checkbox;「今日
   完成」只認「完成(今天台北日)」標記,[x] 無日期不冒領;快取存原始
   completed_days,跨台北午夜不誤算;板檔今天沒 commit → 頁面誠實標「今日
   任務板未更新」;dash_task 降 fallback、燈號判定不變。parser 以 live
   07a/07b 檔實測(標題抽取棄「首個粗體」——實讀發現會抽到完成註記)。

驗證:tsc exit 0;vitest 82/83(1 失敗=pre-existing executor「不存在的
零件」,stash 於 main 複驗同樣紅);新增 taipei-time 10 案例(含跨日/跨年/
client-server 等價/leo 實測 epoch)+任務板 parser 7 案例;兩頁 inline JS
渲染後抽 <script> node --check 全過(mistakes #24 手法)。
This commit is contained in:
cloud-worker
2026-07-07 12:25:31 +00:00
parent ef779e3ef6
commit 5690d61438
9 changed files with 660 additions and 138 deletions
+85 -37
View File
@@ -15,7 +15,8 @@
* - 駕駛艙:GET /console/dashboard-data(免登入聚合端點,console-dashboard.ts
* - 總庫搜尋:GET /kbdb/searchmode=semantic 未開 Vectorize 會誠實降級 + capability_hint
* chips 用 entry_type——KBDB 真能篩的欄位,不編造「電腦筆記/手機筆記」等不存在的分類);
* 規模感 total 來自 GET /kbdb/entries?limit=1 的誠實 total 欄位
* 頭部統計來自 GET /console/kb-scale-data(精耕層 wiki 卡/三元組/已嵌入——leo 2026-07-07
* 裁:45.8 萬 14-E 搬遷遺產 deprecated 不再當招牌數字;搜尋本身仍搜全庫)
* - 卡片詳頁:GET /kbdb/entries/:id;關聯視圖走 GET /kbdb/graph/neighbors/:name
* cypher 代轉 kbdb-graph-pluginkbdb-proxy.ts;查無 triplet → 「尚無關聯資料」)
* - 工作流:GET /webhooks/namedlist+ POST /webhooks/named/:name/trigger(手動觸發)
@@ -24,7 +25,16 @@
* + PUT /credentials/:name(整筆替換);刪除後端未接 D1(T9 範圍)→ 標「即將開通」
* - 收件匣:GET /console/inbox-data(需 session——訊息原文屬機敏,計數才免登入)
* - 設定:vectorize 狀態探測(讀 /kbdb/search 回應的 mode/capability_hint,無新端點;
* 開關本體需部署端啟用 kbdb_embed → 標「即將開通」)+ /console/setup/reset 換帳密 + 系統資訊
* 開關本體需部署端 config kbdb_embed + acr update → 誠實文案講清楚、不假裝能遠端開)
* + MCP token TTL 誠實佔位(/console/settings-data;可調功能=Arcrun#19
* + /console/setup/reset 換帳密 + 系統資訊(精耕層規模)
*
* 2026-07-07 fix/console-truth-audit(總管稽核派工 + leo 裁決):
* 1. 全站日期/時間顯示統一 Asia/Taipeilib/taipei-time.ts,與駕駛艙 #36 台北日判定同套;
* 原本用瀏覽器本地 getter,換裝置就換日期;憑證頁加顯時分——7/6 22:57 建立不再被誤讀)。
* 2. 總庫搜尋/設定頁頭部統計改精耕層(458,732 是真數字——cypher 與 base 一手同值實證——
* 但 leo 裁 14-E 遺產 deprecated 不再顯示)。
* 3. 駕駛艙「今日完成/今日路線」接 sprint 任務板真資料(dashboard-data 端,同 #36 模式)。
*
* 認證模型沿 v1console-auth.ts):單一管理員 email+passwordsession token 在 localStorage
* 實際打 /kbdb/* 用後端回的固定租戶字串(CONSOLE_TENANT)。駕駛艙免登入版在 /console/dashboard。
@@ -42,6 +52,7 @@
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { TAIPEI_CLIENT_JS } from '../lib/taipei-time';
export const consoleRouter = new Hono<{ Bindings: Bindings }>();
@@ -331,7 +342,7 @@ function renderConsoleHtml(registryBase: string): string {
</div>
<div>
<div style="display:flex;justify-content:space-between;align-items:baseline;margin:24px 0 10px">
<span class="subhead">今日路線</span><span style="font-size:13px;color:rgba(var(--ink-rgb),.4)">狀態即時同步</span>
<span class="subhead">今日路線</span><span id="ck-route-src" style="font-size:13px;color:rgba(var(--ink-rgb),.4)">狀態即時同步</span>
</div>
<ul class="route" id="ck-today"><li class="todo"><span class="ic">○</span><span class="muted">載入中…</span></li></ul>
<div class="subhead" id="ck-weekhead" style="display:none;margin:18px 0 10px">本週</div>
@@ -431,10 +442,17 @@ function renderConsoleHtml(registryBase: string): string {
<div style="font-size:17px;font-weight:600">語意搜尋(vectorize</div>
<div id="st-vec" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">狀態偵測中…</div>
</div>
<div class="switch" id="st-vec-switch" title="開關即將開通"><i></i></div>
<div class="switch" id="st-vec-switch" title="開關不能遠端改,只如實顯示狀態"><i></i></div>
</div>
<div style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
誠實提示:這裡的開關<b style="color:var(--amber)">即將開通</b>——啟用/停用需在部署端設定(acr config 開 kbdb_embed 後重新部署),Console 目前只如實顯示狀態,不假裝能遠端開啟。
誠實提示:開關真相=部署端 <code style="font-size:12.5px">~/.arcrun/config.yaml</code> 的 <code style="font-size:12.5px">kbdb_embed: true</code> <code style="font-size:12.5px">acr update</code> 重部署(#32 教訓:wrangler 直推改的形態 config 要同步)。Console 只如實顯示狀態,不假裝能遠端開啟。目前狀態:<b style="color:var(--amber)" id="st-vec-state">偵測中…</b>
</div>
</div>
<div class="panel">
<div style="font-size:17px;font-weight:600">MCP token 有效期(TTL</div>
<div id="st-mcp-ttl" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">讀取中…</div>
<div style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
誠實佔位:此頁<b style="color:var(--amber)">還不能改</b>這個值——可調功能=Arcrun#19(設定頁改→存 KBDB→發 token 時讀)。實作前要調整請在部署端 env <code style="font-size:12.5px">MCP_TOKEN_TTL</code>mcp worker)設定。
</div>
</div>
<div class="panel">
@@ -480,6 +498,9 @@ function renderConsoleHtml(registryBase: string): string {
<script>
(function () {
'use strict';
// 台北時間 helperlib/taipei-time.ts 注入,與駕駛艙 #36 台北日判定同一套)——
// console 所有日期/時間顯示固定 Asia/Taipei,不隨看的裝置時區漂移
${TAIPEI_CLIENT_JS}
var REGISTRY_BASE = ${JSON.stringify(registryBase)};
var $ = function (id) { return document.getElementById(id); };
@@ -487,7 +508,6 @@ function renderConsoleHtml(registryBase: string): string {
var S = {
token: localStorage.getItem('arcrun_console_session') || '',
tenant: '',
total: null, // KBDB 總筆數(誠實 total
chip: '', // entry_type filter'' = 全部)
semantic: false,
cardId: '',
@@ -545,19 +565,16 @@ function renderConsoleHtml(registryBase: string): string {
var ms = Date.parse(s);
return isNaN(ms) ? null : ms;
}
// 顯示一律台北時間(bug 修正:原本用瀏覽器本地 getFullYear/getHours——顯示結果
// 隨看的裝置時區漂移,且與後端「台北日」判定不同套;憑證頁 7/6 22:57(台北)建立的
// credential 在非 +8 裝置上會顯示成另一天)
function fmtDate(v) {
var ms = parseAtMs(v);
if (ms == null) return '';
var d = new Date(ms);
var p = function (n) { return String(n).padStart(2, '0'); };
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
return ms == null ? '' : taipeiDateStr(ms);
}
function fmtDateTime(v) {
var ms = parseAtMs(v);
if (ms == null) return '';
var d = new Date(ms);
var p = function (n) { return String(n).padStart(2, '0'); };
return fmtDate(v) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
return ms == null ? '' : taipeiDateTimeStr(ms);
}
// ── 極簡 Markdown 渲染(先全 escape 再上格式,防 XSS)──
@@ -707,8 +724,8 @@ function renderConsoleHtml(registryBase: string): string {
return Math.round(m / 1440) + ' 天前';
}
function loadCockpit() {
var now = new Date();
$('ck-date').textContent = CNUM[now.getMonth() + 1] + '月' + cnDay(now.getDate()) + '日';
var nowTpe = taipeiMonthDay(Date.now()); // 頁首日期=台北日,不吃裝置時區
$('ck-date').textContent = CNUM[nowTpe.month] + '月' + cnDay(nowTpe.day) + '日';
fetch('/console/dashboard-data')
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function (d) {
@@ -749,18 +766,27 @@ function renderConsoleHtml(registryBase: string): string {
} else {
wmeta.textContent = '';
}
// 今日路線:非今日寫入=誠實降級為「最後路線(N 天前)」,不假裝是今天的
// 今日路線:sprint 任務板優先(來源與更新時間攤開講);dash_task fallback 沿舊誠實降級
var rm = d.route_meta || {};
var today = (d.tasks || []).filter(function (t) { return t.scope === 'today'; });
var week = (d.tasks || []).filter(function (t) { return t.scope === 'week'; });
if (rm.is_today || !today.length) {
if (rm.source === 'gitea_sprint_board') {
$('ck-route-src').textContent = '來源 sprint 任務板・更新於 ' + ckAge(rm.updated_ago_minutes);
var staleHead = rm.is_today ? '' :
'<li class="todo"><span class="ic">○</span><span class="muted">⚠ 今日任務板未更新(最後 ' + ckAge(rm.updated_ago_minutes) + ')——以下是板上現況</span></li>';
$('ck-today').innerHTML = staleHead + (today.length
? today.map(taskLine).join('')
: '<li class="todo"><span class="ic">○</span><span class="muted">任務板上沒有可解析的事項</span></li>');
} else if (rm.is_today || !today.length) {
$('ck-route-src').textContent = '來源 dash_task';
$('ck-today').innerHTML = today.length ? today.map(taskLine).join('') : '<li class="todo"><span class="ic">○</span><span class="muted">今日無排定項目——dash_task 今天沒有寫入</span></li>';
} else {
$('ck-route-src').textContent = '來源 dash_task';
$('ck-today').innerHTML = '<li class="todo"><span class="ic">○</span><span class="muted">今日尚無路線寫入——以下是 ' + ckAge(rm.updated_ago_minutes) + '的殘留路線</span></li>' + today.map(taskLine).join('');
}
$('ck-weekhead').style.display = week.length ? '' : 'none';
$('ck-week').innerHTML = week.map(taskLine).join('');
$('ck-stamp').textContent = '每 60 秒自動刷新・上次 ' + new Date(d.generated_at).toLocaleTimeString('zh-TW', { hour12: false });
$('ck-stamp').textContent = '每 60 秒自動刷新・上次 ' + taipeiTimeStr(Date.parse(d.generated_at)) + '(台北)';
})
.catch(function (e) {
$('ck-char').textContent = '';
@@ -785,7 +811,7 @@ function renderConsoleHtml(registryBase: string): string {
if (searchInited) { return; }
searchInited = true;
renderChips();
ensureTotal();
loadScale();
}
function renderChips() {
$('se-chips').innerHTML = CHIPS.map(function (c, i) {
@@ -799,15 +825,18 @@ function renderConsoleHtml(registryBase: string): string {
renderChips();
if ($('se-q').value.trim()) doSearch();
});
function ensureTotal() {
fetch('/kbdb/entries?limit=1', { headers: apiHeaders() })
// 頭部統計=精耕層 live 數字(leo 2026-07-07 裁:45.8 萬 14-E 搬遷遺產已 deprecated、
// 之後要刪——不再拿遺產總數撐場面,只顯示真的新的)。搜尋本身仍搜全庫,資料不藏。
function loadScale() {
fetch('/console/kb-scale-data')
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (d && typeof d.total === 'number') {
S.total = d.total;
$('se-scale').textContent = '總庫 ' + d.total.toLocaleString() + ' 筆';
$('se-q').placeholder = '搜尋 ' + d.total.toLocaleString() + ' 筆知識…';
}
if (!d) return;
var n = function (v) { return v == null ? '' : v.toLocaleString(); };
var parts = ['wiki 卡 ' + n(d.wiki_card_total), '三元組 ' + n(d.triplets_total), '已嵌入 ' + n(d.embedded)];
var latest = d.wiki_card_latest_ago_minutes;
$('se-scale').textContent = '精耕層 ' + parts.join('・') +
(latest != null && latest >= 0 ? '・最近寫入 ' + ckAge(latest) : '');
})
.catch(function () { /* 規模感拿不到不擋搜尋 */ });
}
@@ -848,17 +877,21 @@ function renderConsoleHtml(registryBase: string): string {
$('se-banner').innerHTML = '<div class="honest" style="margin-top:18px"><div class="h">語意搜尋尚未啟用</div><div class="b">語意搜尋用「意思」找資料,不是字面比對。<br>' + esc(d.capability_hint || '部署端尚未開啟 Vectorize——不會假裝有語意結果,以下是關鍵字結果。') + '</div></div>';
}
var entries = d.entries || [];
var scale = S.total != null ? '於 ' + S.total.toLocaleString() + ' 筆中' : '';
$('se-count').textContent = scale + '命中 ' + entries.length + ' 筆・模式 ' + (d.mode || 'keyword');
$('se-count').textContent = '命中 ' + entries.length + ' 筆・模式 ' + (d.mode || 'keyword') + '・搜尋範圍=全庫(含 14-E 遺產)';
if (!entries.length) {
$('se-results').innerHTML = '<div class="muted" style="padding:30px 10px;text-align:center;grid-column:1/-1">找不到「' + esc(q) + '」——換個關鍵字試試。</div>';
return;
}
$('se-results').innerHTML = entries.map(function (e) {
// 精耕層(wiki_card)綠標突顯;其餘照 entry_type 原樣(不誤標——block/value/note
// 大宗是 14-E 遺產,但 entry_type 本身不足以斷言,僅精耕層正向標記)
var typeTag = e.entry_type === 'wiki_card'
? '<span class="tag green">wiki_card・精耕</span>'
: '<span class="tag">' + esc(e.entry_type || 'entry') + '</span>';
return '<div class="kcard" data-card="' + esc(e.id) + '">' +
'<div class="kt">' + esc(entryTitle(e)) + '</div>' +
'<div class="ks">' + esc(entrySnippet(e)) + '</div>' +
'<div class="km"><span class="tag">' + esc(e.entry_type || 'entry') + '</span>' +
'<div class="km">' + typeTag +
(e.source ? '<span class="dim">' + esc(e.source) + '</span>' : '') +
'<span class="dim mono" style="margin-left:auto">' + esc(fmtDate(e.created_at)) + '</span></div></div>';
}).join('');
@@ -1158,8 +1191,8 @@ function renderConsoleHtml(registryBase: string): string {
'<div class="rowhead" data-credtoggle="' + esc(c.name) + '">' +
'<span class="avatar">' + esc(initial) + '</span>' +
'<div style="min-width:0"><div style="font-size:16.5px;font-weight:600;word-break:break-all">' + esc(c.name) + '</div>' +
'<div style="margin-top:2px;font-size:13.5px;color:rgba(var(--ink-rgb),.5)">' + esc(c.service || '未填服務') + '・建立於 ' + esc(fmtDate(c.created_at) || '—') + '</div></div>' +
'<div style="margin-left:auto;text-align:right;font-size:12.5px;color:rgba(var(--ink-rgb),.4);flex:none">最後使用<br>' + esc(c.last_used_at ? fmtDate(c.last_used_at) : '尚未記錄') + '</div></div>' +
'<div style="margin-top:2px;font-size:13.5px;color:rgba(var(--ink-rgb),.5)">' + esc(c.service || '未填服務') + '・建立於 ' + esc(fmtDateTime(c.created_at) || '—') + '(台北)</div></div>' +
'<div style="margin-left:auto;text-align:right;font-size:12.5px;color:rgba(var(--ink-rgb),.4);flex:none">最後使用<br>' + esc(c.last_used_at ? fmtDateTime(c.last_used_at) : '尚未記錄') + '</div></div>' +
(open
? '<div class="rowbody">' +
'<div class="maskline" style="margin-top:14px"><span class="mono" style="font-size:16px;letter-spacing:.3em;color:rgba(var(--ink-rgb),.4)">••••••••••••••••</span><span style="margin-left:auto;font-size:12.5px;color:rgba(var(--ink-rgb),.4)">已加密・不可回看</span></div>' +
@@ -1257,21 +1290,36 @@ function renderConsoleHtml(registryBase: string): string {
$('st-vec').textContent = on
? '已啟用・語意搜尋可用(搜尋頁切「語意」)'
: '未啟用・' + (d.capability_hint || '部署端尚未開啟 Vectorizekbdb_embed');
$('st-vec-state').textContent = on ? '已啟用(live 探測 mode=semantic' : '未啟用(live 探測降級 keyword';
})
.catch(function () { $('st-vec').textContent = '狀態偵測失敗(KBDB 不可達)'; });
// 系統資訊:worker 自報 + 誠實 total
.catch(function () {
$('st-vec').textContent = '狀態偵測失敗(KBDB 不可達)';
$('st-vec-state').textContent = '偵測失敗';
});
// MCP token TTL(誠實佔位:只顯示目前生效值,不假裝能改)
fetch('/console/settings-data')
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d) { $('st-mcp-ttl').textContent = '讀取失敗'; return; }
var days = Math.round(d.mcp_token_ttl_seconds / 86400 * 10) / 10;
$('st-mcp-ttl').textContent = '目前生效:' + d.mcp_token_ttl_seconds.toLocaleString() + ' 秒(約 ' + days + ' 天)・' +
(d.mcp_token_ttl_source === 'env' ? '來自部署端 env MCP_TOKEN_TTL' : '未設 env,用預設值 30 天');
})
.catch(function () { $('st-mcp-ttl').textContent = '讀取失敗'; });
// 系統資訊:worker 自報 + 精耕層規模(14-E 遺產總數不再顯示,leo 2026-07-07 裁)
Promise.allSettled([
fetch('/').then(function (r) { return r.json(); }),
fetch('/kbdb/entries?limit=1', { headers: apiHeaders() }).then(function (r) { return r.ok ? r.json() : null; })
fetch('/console/kb-scale-data').then(function (r) { return r.ok ? r.json() : null; })
]).then(function (rs) {
var svc = rs[0].status === 'fulfilled' ? rs[0].value : {};
var kb = rs[1].status === 'fulfilled' ? rs[1].value : null;
var n = function (v) { return v == null ? '' : v.toLocaleString(); };
var rows = '';
rows += '<div class="kvline"><span class="muted">服務</span><span class="mono" style="font-size:14px">' + esc(svc.service || 'arcrun-cypher-executor') + '</span></div>';
rows += '<div class="kvline"><span class="muted">版本</span><span class="mono" style="color:var(--amber)">' + esc(svc.version || '—') + '</span></div>';
rows += '<div class="kvline"><span class="muted">部署位置</span><span class="mono" style="font-size:14px">' + esc(location.host) + '</span></div>';
rows += '<div class="kvline"><span class="muted">租戶</span><span class="mono">' + esc(S.tenant) + '</span></div>';
rows += '<div class="kvline"><span class="muted">總庫規模</span><span class="mono" style="color:var(--amber)">' + (kb && typeof kb.total === 'number' ? kb.total.toLocaleString() + ' 筆' : '讀取失敗') + '</span></div>';
rows += '<div class="kvline"><span class="muted">精耕層</span><span class="mono" style="color:var(--amber)">' + (kb ? 'wiki 卡 ' + n(kb.wiki_card_total) + '・三元組 ' + n(kb.triplets_total) + '・已嵌入 ' + n(kb.embedded) : '讀取失敗') + '</span></div>';
$('st-info').innerHTML = rows;
});
}
@@ -1297,7 +1345,7 @@ function renderConsoleHtml(registryBase: string): string {
.catch(function (e) { st.innerHTML = '<span class="err">請求失敗:' + esc(friendlyErr(e)) + '</span>'; });
});
$('st-vec-switch').addEventListener('click', function () {
toast('開關即將開通——目前需在部署端啟用 kbdb_embed');
toast('開關不能遠端改——部署端 config.yaml 開 kbdb_embed 後 acr update 重部署');
});
$('st-logout').addEventListener('click', function () {
var t = S.token;