jim800121chen c649a81d9f fix(local-tool): Windows 首次啟動再修 — waitHealthy pause + shutdown modal hide
續 a209470 修 Windows 乾淨環境啟動問題。使用者回報:
- 紅 banner「伺服器無法啟動 / 啟動時間超過 60 秒」— 即 pipeline total-timeout
- 但上方狀態列顯示「執行中 :3721 PID 8568 uptime 00:00:44」— server 實際活著
- Settings popup 上疊 shutdown-modal「正在停止伺服器…」永遠卡住

三個獨立問題:

1. Stage 3 waitHealthy 在 Windows 首次啟動時,Defender real-time scan
   會延遲 30-60 秒才讓 visiona-local-server.exe 真正 bind port。原本
   30 秒 timeout 可能 stage-failure,且這段等候時間計入 pipeline 60 秒
   total budget。修法:
   (a) healthCheckTimeout 30 秒 → 60 秒
   (b) startServerV2 的 waitHealthy call 在冷啟動時(IsInColdStart)
       包進 Pause/Resume hard timeout — 和 Stage 2 Python bootstrap 同理,
       首次 bootstrap 的 Windows Defender 掃描不該算進日常啟動預算。
       Restart(pipeline 已 ready)維持嚴格計時,不 pause。

2. stopGraceful 只 emit "shutdown:modal-show" 沒有對稱的 hide event,
   前端 popup 顯示後無法關閉(只能等應用重開)。修法:
   (a) stopGraceful 用 defer emit "shutdown:modal-hide"(若曾 show)
   (b) 前端 app.js 加對應 EventsOn listener 把 hidden attribute 設回

3. 配套:cwd bash working dir 會在 session 內持久(system prompt 明說
   "working directory persists between commands"),但 env vars 不持久
   — 非本次 commit 相關,僅自己的 mental note。

驗證:
- visiona-local 套件 go build / vet / test -race 全綠
- macOS dmg 重 build 163MB OK

給 Windows 驗證用的 log 位置:
  %APPDATA%\visiona-local\logs\server.stdout.log     — server 端 log
  %APPDATA%\visiona-local\logs\server.stderr.log     — server 端 panic / 崩潰
  %APPDATA%\visiona-local\logs\wails.log             — Wails app (appLog) 訊息

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:59:20 +08:00

359 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// visionA-local 控制台 main entry
// - 負責 bindings / events 註冊、初始化 UI
// - M8-5 Wails 控制台 UI對齊 Design Spec v2.1
import {
StartServer,
StopServer,
RestartServer,
ForceKillServer,
GetServerStatusV2,
GetRecentLogs,
ClearLogs,
GetSystemInfo,
OpenInBrowser,
RevealLogsFolder,
ExportLog,
GetPreferences,
SetPreferences,
RestartStartupSequence,
} from './wailsjs/go/main/App.js';
import { EventsOn } from './wailsjs/runtime/runtime.js';
import { t, applyI18n, setLocale, getLocale, detectLocale, LOCALES } from './i18n.js';
import {
setServerState,
updateServerMeta,
updatePrimaryControls,
showErrorBanner,
hideErrorBanner,
showToast,
initHeaderClock,
setPrimaryCTAPulse,
STATE_ERROR,
} from './control-panel.js';
import {
showStartupPanel,
hideStartupPanel,
updateStage,
markStageTimeout,
showStartupError,
renderStages,
onManualModeChange,
} from './startup-panel.js';
import {
initLogPanel,
appendLogs,
clearLog,
flashLastError,
applyLogFilter,
} from './log-panel.js';
import { initSettingsPanel, openSettings } from './settings-panel.js';
// ---------- 全域狀態 ----------
const state = {
server: null, // ServerStatusV2
prefs: null, // Preferences
sysInfo: null, // SystemInfo
starting: false, // 啟動進度面板是否顯示
};
// ---------- 初始化 ----------
async function init() {
// 1. 讀 preferences → 決定 locale
try {
state.prefs = await GetPreferences();
} catch (e) {
console.warn('GetPreferences failed:', e);
state.prefs = { autoOpenBrowser: true, locale: '' };
}
const locale = detectLocale(state.prefs && state.prefs.locale);
setLocale(locale);
// 2. 讀系統資訊
try {
state.sysInfo = await GetSystemInfo();
} catch (e) {
console.warn('GetSystemInfo failed:', e);
}
// 3. 套 i18n 到 DOM
applyI18n();
document.title = t('control.title');
if (state.sysInfo && state.sysInfo.appVersion) {
document.getElementById('app-version').textContent = state.sysInfo.appVersion;
}
// 4. render 6 階段 UI skeleton
renderStages();
// 5. 初始化 log panel拉既有 buffer
try {
const existingLogs = await GetRecentLogs(2000);
initLogPanel(existingLogs || []);
} catch (e) {
console.warn('GetRecentLogs failed:', e);
initLogPanel([]);
}
// 6. 綁按鈕 handlers
bindHandlers();
// 7. 訂閱 Wails events
subscribeEvents();
// 8. 初始 state query
try {
const status = await GetServerStatusV2();
handleServerStatus(status);
} catch (e) {
console.warn('GetServerStatusV2 failed:', e);
}
// 9. 初始化 settings panel
initSettingsPanel({
prefs: state.prefs,
sysInfo: state.sysInfo,
getPreferences: GetPreferences,
setPreferences: SetPreferences,
onLocaleChange: (newLoc) => {
setLocale(newLoc || detectLocale(''));
applyI18n();
document.title = t('control.title');
renderStages();
// re-render current state text
if (state.server) setServerState(state.server);
},
});
// 10. header uptime 時鐘
initHeaderClock(() => state.server);
// 11. Manual mode 監聽stage 5 skipped 時引導使用者點 Open in Browser
// 對齊 Design Spec §4.1
onManualModeChange((enabled) => {
setPrimaryCTAPulse(enabled);
});
}
// ---------- 處理 server status ----------
function handleServerStatus(status) {
if (!status) return;
state.server = status;
setServerState(status);
updateServerMeta(status);
updatePrimaryControls(status);
// Error state runtime banner非 startup error
if (status.state === STATE_ERROR && status.lastError && !state.starting) {
showErrorBanner(status.lastError);
} else if (status.state !== STATE_ERROR) {
hideErrorBanner();
}
}
// ---------- 按鈕 handlers ----------
function bindHandlers() {
const $ = (id) => document.getElementById(id);
// Open in Browser
$('btn-open-browser').addEventListener('click', async () => {
try {
await OpenInBrowser('');
} catch (e) {
showToast('Failed to open browser: ' + e);
}
});
// Start
$('btn-start').addEventListener('click', async () => {
try {
await StartServer();
} catch (e) {
showToast('Start failed: ' + e);
}
});
// Manage menu
const manageBtn = $('btn-manage');
const manageMenu = $('manage-menu');
manageBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
const hidden = manageMenu.hasAttribute('hidden');
if (hidden) {
manageMenu.removeAttribute('hidden');
manageBtn.setAttribute('aria-expanded', 'true');
} else {
manageMenu.setAttribute('hidden', '');
manageBtn.setAttribute('aria-expanded', 'false');
}
});
document.addEventListener('click', () => {
if (!manageMenu.hasAttribute('hidden')) {
manageMenu.setAttribute('hidden', '');
manageBtn.setAttribute('aria-expanded', 'false');
}
});
$('mi-stop').addEventListener('click', async () => {
try {
await StopServer();
} catch (e) {
showToast('Stop failed: ' + e);
}
});
$('mi-restart').addEventListener('click', async () => {
try {
await RestartServer();
} catch (e) {
showToast('Restart failed: ' + e);
}
});
$('mi-open-folder').addEventListener('click', async () => {
try {
await RevealLogsFolder();
} catch (e) {
showToast('Open folder failed: ' + e);
}
});
// Log actions
$('btn-clear-log').addEventListener('click', async () => {
try {
await ClearLogs();
clearLog();
showToast(t('control.log.clearToast'));
} catch (e) {
showToast('Clear failed: ' + e);
}
});
$('btn-copy-log').addEventListener('click', async () => {
const output = document.getElementById('log-output');
try {
await navigator.clipboard.writeText(output.innerText || '');
showToast(t('control.log.copied'));
} catch (e) {
showToast('Copy failed: ' + e);
}
});
$('btn-export-log').addEventListener('click', async () => {
try {
const path = await ExportLog();
showToast(t('control.log.exported', { path }));
} catch (e) {
showToast('Export failed: ' + e);
}
});
$('btn-open-folder').addEventListener('click', async () => {
try {
await RevealLogsFolder();
} catch (e) {
showToast('Open folder failed: ' + e);
}
});
// Filter
$('filter-input').addEventListener('input', (e) => applyLogFilter({ text: e.target.value }));
$('level-filter').addEventListener('change', (e) => applyLogFilter({ level: e.target.value }));
// Settings
$('btn-settings').addEventListener('click', openSettings);
// Startup error actions
$('btn-retry').addEventListener('click', async () => {
try {
await RestartStartupSequence();
} catch (e) {
showToast('Retry failed: ' + e);
}
});
$('btn-view-log').addEventListener('click', () => {
hideStartupPanel();
flashLastError();
});
// Report 按鈕 disabledcoming soon
// Error banner
$('banner-restart').addEventListener('click', async () => {
try {
await RestartServer();
} catch (e) {
showToast('Restart failed: ' + e);
}
});
$('banner-view').addEventListener('click', () => {
flashLastError();
});
// 鍵盤快捷鍵⌘F / Ctrl+F focus filter
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
e.preventDefault();
$('filter-input').focus();
}
});
}
// ---------- 訂閱 Wails events ----------
function subscribeEvents() {
// server state / error / recovered
EventsOn('server:state-change', (payload) => handleServerStatus(payload));
EventsOn('server:error', (payload) => {
// Go 端 server_control.go L184/L361 emit payload key 為 "reason"(不是 "error"
if (payload && payload.reason) showErrorBanner(payload.reason);
});
EventsOn('server:recovered', () => {
hideErrorBanner();
showToast('Server recovered');
});
// log stream
EventsOn('log:append', (entries) => {
if (Array.isArray(entries)) appendLogs(entries);
else if (entries) appendLogs([entries]);
});
EventsOn('log:clear', () => clearLog());
// startup pipeline
EventsOn('startup:progress', (ev) => {
if (!state.starting) {
state.starting = true;
showStartupPanel();
}
updateStage(ev);
});
EventsOn('startup:stage-timeout', (ev) => {
markStageTimeout(ev);
});
EventsOn('startup:error', (ev) => {
showStartupError(ev);
});
EventsOn('startup:ready', () => {
state.starting = false;
hideStartupPanel();
});
// shutdown modalM8-4 1 秒後顯示)
EventsOn('shutdown:modal-show', () => {
const m = document.getElementById('shutdown-modal');
if (m) m.removeAttribute('hidden');
});
// stopGraceful 結束時對稱 hide避免 popup 卡住。
EventsOn('shutdown:modal-hide', () => {
const m = document.getElementById('shutdown-modal');
if (m) m.setAttribute('hidden', '');
});
// app level fatal保留相容
EventsOn('app:error', (msg) => {
showErrorBanner(typeof msg === 'string' ? msg : JSON.stringify(msg));
});
}
// ---------- 啟動 ----------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}