jim800121chen f5655e38b1 feat(local-tool): hard timeout 5min + Stage 6 隱藏到 header + 全屏 splash
回應使用者三項需求:

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>
2026-04-16 01:23:55 +08:00

200 lines
11 KiB
HTML
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.

<!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 overlayDOM 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>