依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。
程式碼變動
- M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
Makefile vendor / installer / bootstrap / CI workflow,-555 行)
- M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
VISIONA_MOCK 環境變數,-528 行)
- M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
LGPL binary,macOS 自 build minimal decoder-only 進 git
(vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
- M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
- M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
- M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
state 視覺、log panel、startup progress panel、Stage 6 manual CTA
pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
- M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
- M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
wsEverConnected 容錯 + Page Visibility)
- M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
- MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
(/ws/system endpoint + notifyShutdownImminent helper)
- M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)
品質
- ~105+ 新 unit test + race detector (-count=2) 全綠
- 10 個 milestone 全部通過 Reviewer 審查
- 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
收錄在 .autoflow/
交付前待處理(M8-10)
- 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
- 三平台 end-to-end build 驗證
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
10 KiB
HTML
192 lines
10 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>
|
|
<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>
|
|
</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>
|