jim800121chen d946561362 fix(local-tool): Stage 順序亂跳修復 + 移除秒數顯示
兩個問題一次修:

1. Stage 順序亂跳 — 「Stage 1 等待中、Stage 2 完成、Stage 3 進行中」
   根因:Wails Webview JS load 需 1-3 秒(Windows 乾淨環境更慢),這段
   期間 Go 的 Pipeline.Start 已經 emit Stage 1 running event 甚至跑完
   Stage 1 / Stage 2,但前端 EventsOn 還沒掛上去,events 全被丟掉。前端
   接到的第一個 event 可能是 Stage 2 completed 或 Stage 3 running,
   stages[1].status 仍是初始 pending 值,UI 顯示亂序。

   修法:
   - 新增 Go binding GetStartupSnapshot() 回傳 pipeline 當前所有 stages
     狀態(含 current / startedAt / status)。
   - 前端 init 流程在 subscribeEvents 後立即拉一次 snapshot,補上漏掉
     的 stage 狀態。
   - updateStage 加 monotonic 模式:snapshot 補漏時不會用較舊狀態覆蓋
     已收到的較新狀態(避免 race condition 倒退)。
   - status 優先級 STAGE_STATUS_RANK = pending<running<{skipped,failed}<completed

2. 進度條已等待秒數顯示錯誤 — 「進度 3 / 6 · 已等待 20 秒」
   根因:pause 機制讓 elapsed 計算失準(pause 期間 wall clock 仍走但
   stages[i].startedAt 沒重設,會顯示明顯比真實還久的數字)。使用者
   覺得不需要顯示秒數。
   修法:
   - paintProgressBar 移除 elapsedText 邏輯,永遠顯示 progressLabel
   - i18n 文案移除 {elapsed} placeholder(zh-TW + en):
     stage.1.detail.seedSlow / stage.3.detail.waitHealth /
     stage.3.detail.waitHealthSlow 都改為固定文案
   - Go 端 emit 仍會傳 elapsed(waitProgress callback 不變),但前端
     i18n template 不再用該變數,自然就不顯示

驗證:
- visiona-local 套件 go build / vet / test -race 全綠
- macOS dmg 163MB 重 build OK
- Wails bindings 自動 regen 含 GetStartupSnapshot

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

161 lines
4.3 KiB
TypeScript
Executable File

export namespace main {
export class LogLine {
ts: number;
stream: string;
line: string;
level?: string;
static createFrom(source: any = {}) {
return new LogLine(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ts = source["ts"];
this.stream = source["stream"];
this.line = source["line"];
this.level = source["level"];
}
}
export class Preferences {
autoOpenBrowser: boolean;
locale?: string;
logRingSize?: number;
static createFrom(source: any = {}) {
return new Preferences(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.autoOpenBrowser = source["autoOpenBrowser"];
this.locale = source["locale"];
this.logRingSize = source["logRingSize"];
}
}
export class ServerStatus {
running: boolean;
port: number;
url: string;
pid: number;
pythonBin: string;
pythonMode: string;
lastError?: string;
static createFrom(source: any = {}) {
return new ServerStatus(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.running = source["running"];
this.port = source["port"];
this.url = source["url"];
this.pid = source["pid"];
this.pythonBin = source["pythonBin"];
this.pythonMode = source["pythonMode"];
this.lastError = source["lastError"];
}
}
export class ServerStatusV2 {
state: string;
port?: number;
url?: string;
pid?: number;
pythonBin?: string;
pythonMode?: string;
startedAt?: number;
lastError?: string;
static createFrom(source: any = {}) {
return new ServerStatusV2(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.state = source["state"];
this.port = source["port"];
this.url = source["url"];
this.pid = source["pid"];
this.pythonBin = source["pythonBin"];
this.pythonMode = source["pythonMode"];
this.startedAt = source["startedAt"];
this.lastError = source["lastError"];
}
}
export class StartupStageSnapshot {
stage: number;
status: string;
startedAt: number;
static createFrom(source: any = {}) {
return new StartupStageSnapshot(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.stage = source["stage"];
this.status = source["status"];
this.startedAt = source["startedAt"];
}
}
export class StartupSnapshot {
current: number;
totalStages: number;
stages: StartupStageSnapshot[];
static createFrom(source: any = {}) {
return new StartupSnapshot(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.current = source["current"];
this.totalStages = source["totalStages"];
this.stages = this.convertValues(source["stages"], StartupStageSnapshot);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class SystemInfo {
appVersion: string;
buildTime: string;
dataDir: string;
logsDir: string;
platform: string;
static createFrom(source: any = {}) {
return new SystemInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.appVersion = source["appVersion"];
this.buildTime = source["buildTime"];
this.dataDir = source["dataDir"];
this.logsDir = source["logsDir"];
this.platform = source["platform"];
}
}
}