// visionA-local 控制台 main entry // - 負責 bindings / events 註冊、初始化 UI // - M8-5 Wails 控制台 UI(對齊 Design Spec v2.1) import { StartServer, StopServer, RestartServer, ForceKillServer, GetServerStatusV2, GetStartupSnapshot, 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, collapseStartupPanel, expandStartupPanel, resetStartupPanel, updateStage, updateStageDetail, 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(); // 7.5. 補上前端 init 完成前 Go 端已 emit 但被丟掉的 startup events。 // Wails Webview JS load 完成需 1-3 秒(Windows 乾淨環境更慢),這段 // 期間 Go 的 Pipeline.Start 已經跑完 emit Stage 1 running、可能也 // 跑完 Stage 1 complete + Stage 2 running 等多個 events,前端 EventsOn // 還沒掛上去就被丟掉。拉一次 snapshot 把這些 stage 狀態補回來, // 避免 UI 顯示「Stage 1 等待中、Stage 2 完成」這種亂序畫面。 try { const snapshot = await GetStartupSnapshot(); if (snapshot && snapshot.current > 0) { // pipeline 還在進行中或已完成 → 顯示 panel + 用 monotonic 模式 // 補上各 stage 狀態(已收到較新狀態的 stage 不會被倒退) state.starting = true; showStartupPanel(); for (const s of snapshot.stages || []) { updateStage( { stage: s.stage, status: s.status, startedAt: s.startedAt }, { monotonic: true }, ); } // 已 ready (current=7) → 直接 collapse 面板 if (snapshot.current > snapshot.totalStages) { state.starting = false; collapseStartupPanel(); } } } catch (e) { console.warn('GetStartupSnapshot failed:', e); } // 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 { resetStartupPanel(); 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 panel collapsed bar:點任何位置都展開(看歷史紀錄) const startupPanelEl = $('startup-panel'); if (startupPanelEl) { startupPanelEl.addEventListener('click', () => { if (startupPanelEl.getAttribute('data-collapsed') === 'true') { expandStartupPanel(); } }); } // Startup error actions — Retry 會把面板重置展開 $('btn-retry').addEventListener('click', async () => { try { resetStartupPanel(); await RestartStartupSequence(); } catch (e) { showToast('Retry failed: ' + e); } }); $('btn-view-log').addEventListener('click', () => { hideStartupPanel(); flashLastError(); }); // Report 按鈕 disabled(coming soon) // Error banner — Restart Server 會重置展開(讓使用者看新一輪 6 階段) $('banner-restart').addEventListener('click', async () => { try { resetStartupPanel(); 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); }); // Stage 細步 detail(例如 Stage 3 的 spawn / waitHealth / waitHealthSlow) EventsOn('startup:stage-detail', (ev) => { updateStageDetail(ev); }); EventsOn('startup:error', (ev) => { showStartupError(ev); }); EventsOn('startup:ready', () => { state.starting = false; collapseStartupPanel(); }); // shutdown modal(M8-4 1 秒後顯示) // 用 watchdog 做 safety net:popup show 後最多顯示 15 秒,即使 Go 端 // 沒 emit hide(stopGraceful 卡死、Process.Wait 阻塞等邊界情境)前端也 // 會自己 hide,避免使用者卡在無法關閉的 popup。 let shutdownModalWatchdog = null; const hideShutdownModal = () => { const m = document.getElementById('shutdown-modal'); if (m) m.setAttribute('hidden', ''); if (shutdownModalWatchdog) { clearTimeout(shutdownModalWatchdog); shutdownModalWatchdog = null; } }; EventsOn('shutdown:modal-show', () => { const m = document.getElementById('shutdown-modal'); if (m) m.removeAttribute('hidden'); if (shutdownModalWatchdog) clearTimeout(shutdownModalWatchdog); shutdownModalWatchdog = setTimeout(() => { hideShutdownModal(); showToast('伺服器停止耗時過久,popup 已自動關閉。請用工作管理員確認 server 是否已結束。'); }, 15000); }); // stopGraceful 結束時對稱 hide。 EventsOn('shutdown:modal-hide', hideShutdownModal); // 點 backdrop 空白處可手動關閉 popup(Escape hatch) const shutdownBackdrop = document.getElementById('shutdown-modal'); if (shutdownBackdrop) { shutdownBackdrop.addEventListener('click', (e) => { if (e.target === shutdownBackdrop) hideShutdownModal(); }); } // Esc 鍵也可以關 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const m = document.getElementById('shutdown-modal'); if (m && !m.hasAttribute('hidden')) hideShutdownModal(); } }); // 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(); }