回應使用者三項需求:
1. 整體 hard timeout 180s → 300s(5 分鐘)
每個 stage 已有 soft timeout 20s 提示機制,整體 budget 不需緊湊。
5 分鐘是「使用者點完一杯咖啡都還沒好」的心理上限。pause 機制
(Stage 1 seed / Stage 2 Python bootstrap / Stage 3 waitHealthy)
仍維持作為「一次性 bootstrap 完全不算 budget」的快速通道。
- 同步更新 i18n 紅 banner 文案 180 → 5 分鐘
- 同步更新 unit tests(HardTimeout 用 -305s,SkipBypass 用 -320s,
PreventsHardTimeout 註解 effective<300s)
2. Stage 6「等待 Web UI 連線」從 6 階段面板隱藏到 header 連線指示燈
Go 端 pipeline 仍保持 6 階段(不動),前端 UI 只顯示 5 階段:
- startup-panel.js: TOTAL_STAGES=5 顯示用,PIPELINE_STAGES=6 內部
state 用。renderStages / paintProgressBar / 進度數字都用 5。
- updateStage 仍會收 stage 6 events 更新內部 state(控 collapse 時機)
但 stage 6 不 paint UI(n > TOTAL_STAGES early return)
- 新增 onConnectionStatusChange listener 機制:stage 6 status 變化
時通知外層
- control-panel.js: setWebUIStatus 把連線狀態 (pending/running/
completed/failed) 渲染到 header 的 meta-webui 指示燈:圓點顏色
+ 文字 (等待連線/已連線/未連線)
- index.html: server-meta 新增 <dd id="meta-webui"> 指示燈位置
- i18n: control.meta.webui / control.webui.{connected,waiting,disconnected}
- style.css: .webui-status::before 圓點 + pulse 動畫 + 顏色對應
state (pending=灰 / running=warning+pulse / connected=success / failed=destructive)
- app.js: 註冊 onConnectionStatusChange listener,初始化呼叫
setWebUIStatus('pending')
3. 全屏 spinner splash 取代「啟動中...」三個字
原本 app 啟動最一開始的「啟動中」狀態只有 header 上三個字很不
明顯,使用者體感像沒反應。改為 DOM ready 時就顯示 fullscreen
spinner overlay,收到第一個 startup:progress event 才隱藏。
- index.html: <div id="boot-splash"> 內含 logo + spinner-lg + 文字
- style.css: .boot-splash position:fixed inset:0 z-index:1000,
.boot-splash.hidden { display:none } 用 class 控制(避免和
[hidden]!important 衝突)
- app.js: hideBootSplash() helper,4 個 hide 觸發點:
(a) 收到 startup:progress event
(b) snapshot 補漏發現 pipeline 已啟動
(c) 收到 startup:error event(即使失敗也要看到錯誤)
(d) handleServerStatus 收到非 idle 狀態(restart wails app
server 還活著的情境)
更新 fix marker 為「d946561+ (5min hard timeout + 5-stage UI + fullscreen splash)」
驗證:
- visiona-local 套件 go build / vet / test -race 全綠
- macOS dmg 163MB 重 build OK
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
200 lines
11 KiB
HTML
200 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>visionA-local · Server Control</title>
|
||
<link rel="icon" type="image/png" href="icon.png">
|
||
<link rel="stylesheet" href="style.css">
|
||
</head>
|
||
<body>
|
||
<!-- 全屏 splash overlay:DOM ready 時就 visible,收到第一個 startup:progress
|
||
event 後 app.js 會把它 hide。給使用者一個明確「app 在啟動中」的視覺回饋。 -->
|
||
<div id="boot-splash" class="boot-splash">
|
||
<img class="boot-splash-logo" src="icon.png" alt="visionA-local">
|
||
<div class="spinner-lg" aria-hidden="true"></div>
|
||
<p class="boot-splash-text" data-i18n="control.status.starting">啟動中...</p>
|
||
</div>
|
||
<div id="app" class="control-panel" data-state="idle">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<img class="brand-logo" src="icon.png" alt="visionA-local">
|
||
<div class="brand-info">
|
||
<h1 class="brand-name">visionA-local</h1>
|
||
<div class="status-line">
|
||
<span class="status-dot" id="status-dot" aria-hidden="true"></span>
|
||
<span class="status-text" id="status-text" role="status" aria-live="polite">Idle</span>
|
||
</div>
|
||
<dl class="server-meta" id="server-meta">
|
||
<div class="meta-item"><dt data-i18n="control.meta.port">Port</dt><dd id="meta-port">—</dd></div>
|
||
<div class="meta-item"><dt data-i18n="control.meta.uptime">Uptime</dt><dd id="meta-uptime">—</dd></div>
|
||
<div class="meta-item"><dt data-i18n="control.meta.pid">PID</dt><dd id="meta-pid">—</dd></div>
|
||
<div class="meta-item"><dt data-i18n="control.meta.webui">Web UI</dt><dd id="meta-webui" class="webui-status" data-state="pending">—</dd></div>
|
||
</dl>
|
||
</div>
|
||
<div class="brand-version">
|
||
<span id="app-version">v0.1.0</span>
|
||
<button class="icon-btn" id="btn-settings" type="button" title="Settings" aria-label="Settings">⚙</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Primary Controls -->
|
||
<section class="primary-controls" aria-label="Server controls">
|
||
<button class="btn btn-primary" id="btn-open-browser" type="button" disabled>
|
||
<span aria-hidden="true">🌐</span>
|
||
<span data-i18n="control.action.openBrowser">Open in Browser</span>
|
||
</button>
|
||
<button class="btn btn-outline" id="btn-start" type="button">
|
||
<span aria-hidden="true">▶</span>
|
||
<span data-i18n="control.action.start">Start</span>
|
||
</button>
|
||
<div class="manage-wrapper">
|
||
<button class="btn btn-outline" id="btn-manage" type="button" disabled aria-haspopup="menu" aria-expanded="false">
|
||
<span data-i18n="control.action.manage">Manage</span> <span aria-hidden="true">▾</span>
|
||
</button>
|
||
<div class="manage-menu" id="manage-menu" role="menu" hidden>
|
||
<button role="menuitem" id="mi-stop" class="menu-item menu-item-danger">
|
||
<span data-i18n="control.action.stopServer">Stop server</span>
|
||
</button>
|
||
<button role="menuitem" id="mi-restart" class="menu-item">
|
||
<span data-i18n="control.action.restartServer">Restart server</span>
|
||
</button>
|
||
<div class="menu-divider"></div>
|
||
<button role="menuitem" id="mi-open-folder" class="menu-item">
|
||
<span data-i18n="control.log.openFolder">Open log folder</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Log controls -->
|
||
<section class="log-controls" aria-label="Log controls">
|
||
<label class="checkbox"><input type="checkbox" id="cb-follow-tail" checked> <span data-i18n="control.log.followTail">Follow tail</span></label>
|
||
<label class="checkbox"><input type="checkbox" id="cb-show-ts" checked> <span data-i18n="control.log.showTimestamps">Show timestamps</span></label>
|
||
<label class="filter-wrapper">
|
||
<span aria-hidden="true">🔍</span>
|
||
<input type="search" id="filter-input" data-i18n-placeholder="control.log.filterPlaceholder" placeholder="Filter..." aria-label="Filter logs">
|
||
</label>
|
||
<select id="level-filter" aria-label="Level filter">
|
||
<option value="">All</option>
|
||
<option value="debug">DEBUG</option>
|
||
<option value="info">INFO</option>
|
||
<option value="warn">WARN</option>
|
||
<option value="error">ERROR</option>
|
||
</select>
|
||
</section>
|
||
|
||
<!-- Startup progress panel (顯示於 Starting state) -->
|
||
<section class="startup-panel" id="startup-panel" role="progressbar" aria-valuemin="0" aria-valuemax="6" aria-valuenow="0" hidden>
|
||
<h2 class="startup-title" id="startup-title" data-i18n="startup.panel.title">Starting visionA-local</h2>
|
||
<div class="stages" id="stages"></div>
|
||
<div class="progress-row">
|
||
<div class="progress-bar" id="progress-bar" aria-hidden="true"></div>
|
||
<span class="progress-text" id="progress-text"></span>
|
||
</div>
|
||
<div class="sr-only" id="startup-live" aria-live="polite" aria-atomic="true"></div>
|
||
|
||
<!-- Error mode -->
|
||
<div class="startup-error" id="startup-error" hidden>
|
||
<h3 class="error-title" data-i18n="startup.error.title">Startup failed</h3>
|
||
<p class="error-desc" id="error-desc"></p>
|
||
<p class="error-stage" id="error-stage"></p>
|
||
<div class="error-actions">
|
||
<button class="btn btn-primary" id="btn-retry" type="button">
|
||
<span aria-hidden="true">🔄</span>
|
||
<span data-i18n="startup.error.retry">Retry</span>
|
||
</button>
|
||
<button class="btn btn-ghost" id="btn-view-log" type="button">
|
||
<span aria-hidden="true">📋</span>
|
||
<span data-i18n="startup.error.viewLog">View Log</span>
|
||
</button>
|
||
<button class="btn btn-ghost" id="btn-report" type="button" disabled title="Coming soon">
|
||
<span aria-hidden="true">🐞</span>
|
||
<span data-i18n="startup.error.report">Report Issue</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Error banner (for runtime server errors) -->
|
||
<section class="error-banner" id="error-banner" role="alert" hidden>
|
||
<span class="banner-icon" aria-hidden="true">⚠</span>
|
||
<div class="banner-content">
|
||
<strong class="banner-title" data-i18n="control.error.title">Server failed to start</strong>
|
||
<p class="banner-desc" id="banner-desc"></p>
|
||
<div class="banner-actions">
|
||
<button class="btn btn-primary btn-sm" id="banner-restart" data-i18n="control.error.restartButton">Restart Server</button>
|
||
<button class="btn btn-ghost btn-sm" id="banner-view" data-i18n="control.error.viewLogDetails">View log details</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Log panel -->
|
||
<section class="log-panel" id="log-panel" aria-label="Log output">
|
||
<output id="log-output" aria-live="polite" aria-atomic="false"></output>
|
||
<button class="jump-latest" id="btn-jump-latest" type="button" hidden data-i18n="control.log.jumpToLatest">Jump to latest</button>
|
||
</section>
|
||
|
||
<!-- Log actions -->
|
||
<section class="log-actions">
|
||
<button class="btn btn-ghost btn-sm" id="btn-clear-log" data-i18n="control.log.clear">Clear</button>
|
||
<button class="btn btn-ghost btn-sm" id="btn-copy-log" data-i18n="control.log.copy">Copy</button>
|
||
<button class="btn btn-ghost btn-sm" id="btn-export-log" data-i18n="control.log.export">Export log</button>
|
||
<button class="btn btn-ghost btn-sm" id="btn-open-folder" data-i18n="control.log.openFolder">Open log folder</button>
|
||
</section>
|
||
|
||
<!-- Footer -->
|
||
<footer class="footer">
|
||
<span class="footer-left" id="footer-lines">Lines: 0 / 2000</span>
|
||
<span class="footer-right" id="footer-warning" data-i18n="control.footer.closeWarning">⚠ Closing this window will stop the server</span>
|
||
</footer>
|
||
|
||
<!-- Settings modal -->
|
||
<div class="modal-backdrop" id="settings-modal" hidden>
|
||
<div class="modal" role="dialog" aria-labelledby="settings-title" aria-modal="true">
|
||
<header class="modal-header">
|
||
<h2 id="settings-title">Settings</h2>
|
||
<button class="icon-btn" id="btn-close-settings" aria-label="Close">✕</button>
|
||
</header>
|
||
<div class="modal-body">
|
||
<label class="setting-row">
|
||
<div class="setting-text">
|
||
<div class="setting-label" data-i18n="settings.autoOpenBrowser.label">Auto-open browser on startup</div>
|
||
<div class="setting-hint" id="auto-open-hint"></div>
|
||
</div>
|
||
<input type="checkbox" id="pref-auto-open">
|
||
</label>
|
||
<label class="setting-row">
|
||
<div class="setting-text">
|
||
<div class="setting-label" data-i18n="settings.language.label">Language</div>
|
||
</div>
|
||
<select id="pref-locale">
|
||
<option value="">Auto</option>
|
||
<option value="zh-TW">繁體中文</option>
|
||
<option value="en">English</option>
|
||
</select>
|
||
</label>
|
||
<div class="about-section">
|
||
<h3 data-i18n="settings.about.title">About</h3>
|
||
<dl class="about-list" id="about-list"></dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Shutdown modal -->
|
||
<div class="modal-backdrop shutdown-modal" id="shutdown-modal" hidden>
|
||
<div class="modal shutdown-dialog" role="alert">
|
||
<div class="spinner-lg" aria-hidden="true"></div>
|
||
<p data-i18n="control.shutdown.stopping">Stopping server…</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div class="toast" id="toast" hidden></div>
|
||
</div>
|
||
|
||
<script type="module" src="app.js"></script>
|
||
</body>
|
||
</html>
|