兩個問題一次修:
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>
161 lines
4.3 KiB
TypeScript
Executable File
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"];
|
|
}
|
|
}
|
|
|
|
}
|
|
|