diff --git a/packaging/README.md b/packaging/README.md index ec2353b..ec06370 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -56,17 +56,15 @@ Optional local zip only: `build_release.ps1 -CreateZip` (not for release upload) 1. Run `NetX-Setup-x.y.z.exe` (admin). 2. **First install:** Database page — choose built-in (offline) or external PostgreSQL (host/port/user/password/db; connection must succeed to continue). -3. **Already installed:** if `%ProgramData%\NetX\.env` already has `NETX_DB_MODE`, Setup shows an **Install mode** page: - - **Update** (default) — skip Database page; stop NetX; replace program files; **keep** ProgramData / DB settings. - - **Reinstall** — same Database wizard as first install (`ApplyDatabaseConfig`); does **not** delete ProgramData (use Uninstall → delete data for a wipe). +3. **Already installed (upgrade):** if `%ProgramData%\NetX\.env` already has `NETX_DB_MODE`, Setup **skips** the Database page, stops running NetX, replaces program files, and **keeps** data / DB settings. Use Start Menu → **Reconfigure database** only when you need to change DB mode. 4. Files → `%ProgramFiles%\NetX\` (program root). 5. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). 6. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI** / **Reconfigure database**. Silent first install (bundled default): `/SILENT /DbMode=bundled` Silent external: `/SILENT /DbMode=external /DbHost=... /DbPort=5432 /DbUser=... /DbPassword=... /DbName=...` -Silent update over existing data: `/VERYSILENT /NORESTART /InstallMode=update` (also `/SkipDbPage=1`) -Silent reinstall (DB wizard via params): `/VERYSILENT /InstallMode=reinstall /DbMode=bundled` (or external `/DbHost`…) — also `/ForceDbPage=1` +Silent upgrade over existing data: `/VERYSILENT /NORESTART` (auto-detects `.env`; or force `/SkipDbPage=1`) +Force DB wizard on an existing install: `/ForceDbPage=1` Optional credential key (reuse encrypted NE passwords from another install): `/CredentialSecretKey=...` — omit to auto-generate. ## Database modes @@ -81,7 +79,7 @@ Existing Windows deploys that only set `NETX_DATABASE_URL` keep working: never s ## Manual update -Preferred: run a newer `NetX-Setup-*.exe` and choose **Update** (or silent `/InstallMode=update`) so ProgramData is kept. Setup (elevated) always relinks `.venv` → `python/runtime` after file copy so tray start works without write access under Program Files. +Preferred: run a newer `NetX-Setup-*.exe` over the same machine (upgrade mode keeps ProgramData). Legacy/dev zip (only if you built with `-CreateZip`): diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index 0a22f55..6c2c9dd 100644 --- a/packaging/_common.ps1 +++ b/packaging/_common.ps1 @@ -404,23 +404,8 @@ function Repair-NetxShippedVenv { "version = $ver", "executable = $rtPyAbs" ) - $desired = ($lines -join "`r`n") - # Skip write when already correct — Users cannot modify Program Files after Setup. - try { - $cur = ([IO.File]::ReadAllText($cfgPath) -replace "`r`n", "`n" -replace "`r", "`n").Trim() - $want = ($desired -replace "`r`n", "`n" -replace "`r", "`n").Trim() - if ($cur -eq $want) { - return $true - } - } catch {} - - try { - Set-Content -LiteralPath $cfgPath -Value $desired -Encoding ascii -ErrorAction Stop - return $true - } catch { - # Non-elevated tray/start: leave cfg as-is; caller may still run if paths happen to work. - return $false - } + Set-Content -LiteralPath $cfgPath -Value ($lines -join "`r`n") -Encoding ascii + return $true } function Test-NetxVenvRunnable { @@ -461,11 +446,7 @@ function Ensure-NetxVenv { $rtPy = Join-Path (Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot) "python.exe" if (Test-Path -LiteralPath $rtPy) { - try { - $null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot - } catch { - # Access denied under Program Files when not elevated — ignore if venv already runs. - } + $null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot } if ((Test-Path -LiteralPath $venvPy) -and -not $ForcePip) { @@ -474,19 +455,11 @@ function Ensure-NetxVenv { } Write-Host "[WARN] Shipped .venv exists but Python cannot start (broken pyvenv.cfg or missing runtime)." -ForegroundColor Yellow if (Test-Path -LiteralPath $rtPy) { - $repaired = $false - try { - $repaired = [bool](Repair-NetxShippedVenv -ProgramRoot $ProgramRoot) - } catch { - $repaired = $false - } - if ($repaired -and (Test-NetxVenvRunnable -VenvPython $venvPy)) { + $null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot + if (Test-NetxVenvRunnable -VenvPython $venvPy) { Write-Host "==> Repaired .venv to use bundled python/runtime" -ForegroundColor Green return $venvPy } - if (-not $repaired) { - throw "venv_needs_admin_repair: pyvenv.cfg still points at the build PC. Re-run Setup (Update) as Administrator, or Start Menu → Reconfigure database once elevated." - } } if (-not $ForcePip) { throw "venv_not_runnable: reinstall NetX Setup (ships python/runtime + .venv) or run repair as admin" diff --git a/packaging/check_update.ps1 b/packaging/check_update.ps1 index 0d45bf9..7e3fd46 100644 --- a/packaging/check_update.ps1 +++ b/packaging/check_update.ps1 @@ -404,22 +404,15 @@ try { $env:NETX_UPDATE_ELEVATED = "1" if ($isSetup) { - # Silent Setup update mode: skip DB wizard, keep ProgramData - # (see installer/netx.iss /InstallMode=update). + # Silent Setup over an existing configured install skips the DB wizard + # (see installer/netx.iss GSkipDbPage) and preserves ProgramData. Write-Host "==> Applying update via silent Setup.exe" - $setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /InstallMode=update" + $setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /SkipDbPage=1" $sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -Wait -PassThru if ($null -eq $sp.ExitCode -or $sp.ExitCode -ne 0) { $code = if ($null -eq $sp.ExitCode) { "null" } else { $sp.ExitCode } throw "setup_update_failed: exit $code" } - # Setup post-install also repairs; belt-and-suspenders when running elevated apply. - $repairPs1 = Join-Path $prog "packaging\repair_venv.ps1" - if (Test-Path -LiteralPath $repairPs1) { - Write-Host "==> Relinking .venv to bundled python/runtime" - & powershell -NoProfile -ExecutionPolicy Bypass -File $repairPs1 ` - -ProgramRoot $prog -DataRoot $data - } Write-Host "==> Restarting NetX after Setup" & (Join-Path $PSScriptRoot "start_netx_app.ps1") ` -ProgramRoot $prog -DataRoot $data -SkipBrowser diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss index 42b836e..7e4a20e 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -5,7 +5,7 @@ #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.12" + #define MyAppVersion "0.4.11" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" @@ -78,12 +78,6 @@ english.DbBundledMissing=Built-in PostgreSQL files are missing from the install english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env. english.ReconfigureDb=Reconfigure database english.UpgradeKeepDb=Existing NetX data found - database settings will be kept (no wizard). -english.ModePageCaption=Install mode -english.ModePageDescription=An existing NetX installation was found. Choose update or reinstall. -english.ModeUpdate=Update existing installation (keep database settings) -english.ModeReinstall=Reinstall and reconfigure database (same as first install) -english.ModeUpdateHint=Replaces program files only. ProgramData\.env and database data are kept. -english.ModeReinstallHint=Shows the Database page again. Does not delete ProgramData unless you uninstall and choose to. chinesesimplified.CreateDesktopIcon=创建桌面快捷方式 chinesesimplified.SetupOptions=安装完成后: chinesesimplified.StartTrayNow=立即启动 NetX 托盘 @@ -113,12 +107,6 @@ chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件 chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。 chinesesimplified.ReconfigureDb=重新配置数据库 chinesesimplified.UpgradeKeepDb=检测到已有 NetX 数据 - 将保留数据库配置(跳过向导)。 -chinesesimplified.ModePageCaption=安装模式 -chinesesimplified.ModePageDescription=检测到已有 NetX 安装。请选择更新或重装。 -chinesesimplified.ModeUpdate=更新现有安装(保留数据库配置) -chinesesimplified.ModeReinstall=重装并重新配置数据库(与首次安装相同) -chinesesimplified.ModeUpdateHint=仅替换程序文件,保留 ProgramData\.env 与数据库数据。 -chinesesimplified.ModeReinstallHint=再次显示数据库页。不会删除 ProgramData(除非卸载时选择删除)。 [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce @@ -215,11 +203,6 @@ var GCredKey: String; GSkipDbPage: Boolean; GIsUpgrade: Boolean; - GInstallMode: String; - ModePage: TWizardPage; - RbModeUpdate: TNewRadioButton; - RbModeReinstall: TNewRadioButton; - LblModeHint: TNewStaticText; GDbConfigured: Boolean; GPgToolsReady: Boolean; GDbConnOk: Boolean; @@ -282,64 +265,6 @@ begin Result := EnvHasDbMode(); end; -procedure SyncSkipDbFromInstallMode(); -begin - GSkipDbPage := (GInstallMode = 'update'); -end; - -procedure ApplyInstallModeFromChoice(); -begin - if (RbModeReinstall <> nil) and RbModeReinstall.Checked then - GInstallMode := 'reinstall' - else if GIsUpgrade then - GInstallMode := 'update' - else - GInstallMode := 'fresh'; - SyncSkipDbFromInstallMode(); -end; - -procedure ModeChoiceClick(Sender: TObject); -begin - ApplyInstallModeFromChoice(); - if LblModeHint = nil then - Exit; - if GInstallMode = 'reinstall' then - LblModeHint.Caption := ExpandConstant('{cm:ModeReinstallHint}') - else - LblModeHint.Caption := ExpandConstant('{cm:ModeUpdateHint}'); -end; - -function ResolveInstallModeFromParams(): Boolean; -{ Returns True if a CLI param forced the mode. } -var - Mode: String; -begin - Result := False; - Mode := LowerCase(Trim(ExpandConstant('{param:InstallMode|}'))); - if (Mode = 'update') or (Mode = 'upgrade') then - begin - GInstallMode := 'update'; - Result := True; - end - else if (Mode = 'reinstall') or (Mode = 'fresh') then - begin - GInstallMode := 'reinstall'; - Result := True; - end - else if ParamIsTruthy('SkipDbPage') then - begin - GInstallMode := 'update'; - Result := True; - end - else if ParamIsTruthy('ForceDbPage') then - begin - GInstallMode := 'reinstall'; - Result := True; - end; - if Result then - SyncSkipDbFromInstallMode(); -end; - procedure StopNetxBeforeFileReplace(); var ResultCode: Integer; @@ -613,62 +538,18 @@ begin GDbConfigured := False; GPgToolsReady := False; GDbConnOk := False; - { Existing install: interactive Mode page (Update vs Reinstall). - Silent: /InstallMode=update|reinstall (or /SkipDbPage=1 /ForceDbPage=1). } + { Upgrade / reinstall over a configured install: skip DB wizard and keep + ProgramData\.env / pgdata. Fresh install still shows the Database page. + Force wizard: /ForceDbPage=1 Force skip: /SkipDbPage=1 } GIsUpgrade := DetectExistingConfiguredInstall(); - ModePage := nil; - RbModeUpdate := nil; - RbModeReinstall := nil; - LblModeHint := nil; - if not ResolveInstallModeFromParams() then - begin - if GIsUpgrade then - GInstallMode := 'update' - else - GInstallMode := 'fresh'; - SyncSkipDbFromInstallMode(); - end; - - if GIsUpgrade and (not WizardSilent) then - begin - ModePage := CreateCustomPage(wpSelectDir, - ExpandConstant('{cm:ModePageCaption}'), - ExpandConstant('{cm:ModePageDescription}')); - - RbModeUpdate := TNewRadioButton.Create(ModePage); - RbModeUpdate.Parent := ModePage.Surface; - RbModeUpdate.Caption := ExpandConstant('{cm:ModeUpdate}'); - RbModeUpdate.Checked := (GInstallMode <> 'reinstall'); - RbModeUpdate.Top := ScaleY(8); - RbModeUpdate.Left := ScaleX(0); - RbModeUpdate.Width := ModePage.SurfaceWidth; - RbModeUpdate.OnClick := @ModeChoiceClick; - - RbModeReinstall := TNewRadioButton.Create(ModePage); - RbModeReinstall.Parent := ModePage.Surface; - RbModeReinstall.Caption := ExpandConstant('{cm:ModeReinstall}'); - RbModeReinstall.Checked := (GInstallMode = 'reinstall'); - RbModeReinstall.Top := RbModeUpdate.Top + ScaleY(28); - RbModeReinstall.Left := ScaleX(0); - RbModeReinstall.Width := ModePage.SurfaceWidth; - RbModeReinstall.OnClick := @ModeChoiceClick; - - LblModeHint := TNewStaticText.Create(ModePage); - LblModeHint.Parent := ModePage.Surface; - LblModeHint.Top := RbModeReinstall.Top + ScaleY(36); - LblModeHint.Left := ScaleX(0); - LblModeHint.Width := ModePage.SurfaceWidth; - LblModeHint.Height := ScaleY(60); - LblModeHint.AutoSize := False; - LblModeHint.WordWrap := True; - ModeChoiceClick(nil); - - DbPage := CreateCustomPage(ModePage.ID, - ExpandConstant('{cm:DbPageCaption}'), - ExpandConstant('{cm:DbPageDescription}')); - end + if ParamIsTruthy('ForceDbPage') then + GSkipDbPage := False + else if ParamIsTruthy('SkipDbPage') then + GSkipDbPage := True else - DbPage := CreateCustomPage(wpSelectDir, + GSkipDbPage := GIsUpgrade; + + DbPage := CreateCustomPage(wpSelectDir, ExpandConstant('{cm:DbPageCaption}'), ExpandConstant('{cm:DbPageDescription}')); @@ -806,12 +687,6 @@ end; function ShouldSkipPage(PageID: Integer): Boolean; begin Result := False; - if (ModePage <> nil) and (PageID = ModePage.ID) then - begin - { Only interactive existing installs show the mode page. } - Result := (not GIsUpgrade) or WizardSilent; - Exit; - end; if (DbPage <> nil) and (PageID = DbPage.ID) then Result := GSkipDbPage; end; @@ -821,11 +696,6 @@ var ErrMsg: String; begin Result := True; - if (ModePage <> nil) and (CurPageID = ModePage.ID) then - begin - ApplyInstallModeFromChoice(); - Exit; - end; { Only the DB page runs validation. Other pages must always proceed (Tasks/Ready used to appear "dead" when CloseApplications hung). } if (DbPage = nil) or (CurPageID <> DbPage.ID) or GSkipDbPage then @@ -866,14 +736,6 @@ var begin Result := ''; NeedsRestart := False; - - if (ModePage <> nil) and (not WizardSilent) then - ApplyInstallModeFromChoice(); - - { Stop tray / API / bundled PG so Program Files can be replaced safely. } - if GSkipDbPage or GIsUpgrade or DirExists(ExpandConstant('{app}\packaging')) then - StopNetxBeforeFileReplace(); - if GSkipDbPage then Exit; @@ -1035,31 +897,12 @@ begin end; end; -procedure RepairNetxVenvAfterInstall(); -var - ResultCode: Integer; - RepairScript: String; - Params: String; -begin - RepairScript := ExpandConstant('{app}\packaging\repair_venv.ps1'); - if not FileExists(RepairScript) then - Exit; - Params := - '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + RepairScript + '"' + - ' -ProgramRoot "' + ExpandConstant('{app}') + '"' + - ' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"'; - Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; - procedure CurStepChanged(CurStep: TSetupStep); var UninstallKey: String; begin if CurStep = ssPostInstall then begin - { Always relink .venv -> local python/runtime (Setup is elevated). } - RepairNetxVenvAfterInstall(); - { Upgrade keeps existing .env / pgdata - do not re-run DB wizard. } if not GSkipDbPage then ApplyDatabaseConfig() diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index 410b5ee..36ac2c8 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -1,11 +1,11 @@ { "channel": "stable", - "latest": "0.4.12", + "latest": "0.4.11", "min_compatible": "0.3.0", - "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.12", + "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.11", "windows": { - "url": "https://github.com/hansjone/netx/releases/download/v0.4.12/NetX-Setup-0.4.12.exe", - "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.12/NetX-Setup-0.4.12.exe", + "url": "https://github.com/hansjone/netx/releases/download/v0.4.11/NetX-Setup-0.4.11.exe", + "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.11/NetX-Setup-0.4.11.exe", "package": "setup", "sha256": "", "size": 0 diff --git a/packaging/repair_venv.ps1 b/packaging/repair_venv.ps1 deleted file mode 100644 index 4172d3a..0000000 --- a/packaging/repair_venv.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -param( - [string]$ProgramRoot = "", - [string]$DataRoot = "" -) - -# Relink shipped .venv to local python/runtime (run elevated after Setup/update). -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$prog = Get-NetxProgramRoot -Override $ProgramRoot -$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot - -Write-Host "==> Repairing .venv under $prog" -if (Repair-NetxShippedVenv -ProgramRoot $prog) { - $venvPy = Join-Path $prog ".venv\Scripts\python.exe" - if (Test-NetxVenvRunnable -VenvPython $venvPy) { - Write-Host "==> .venv OK -> bundled python/runtime" -ForegroundColor Green - Get-Content (Join-Path $prog ".venv\pyvenv.cfg") - exit 0 - } - Write-Host "[ERR] pyvenv.cfg written but venv still not runnable" -ForegroundColor Red - exit 2 -} -Write-Host "[ERR] repair failed (missing runtime/.venv or access denied)" -ForegroundColor Red -exit 1 diff --git a/pyproject.toml b/pyproject.toml index 9263218..4278ff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.12" +version = "0.4.11" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 478d12a..a72a130 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -500,26 +500,13 @@ const en = { runStatusFailed: "Failed", runStatusDoneSec: "Done {{s}}s", runProgress: "{{phase}} · sheet {{i}}/{{n}} · {{sheet}} · {{s}}s elapsed", - runSheetProgress: "sheet {{i}}/{{n}}", - runElapsed: "{{s}}s elapsed", runRowsLoaded: "Loaded {{side}} {{n}} rows", runRowsSqlCount: "DB count {{side}} {{n}} rows", runPersisting: "Wrote {{done}}/{{total}} rows", runEngineSql: "engine SQL", runEnginePython: "engine Python", runEngineNote: "reason {{note}}", - phaseQueued: "Queued", - phaseLoading: "Loading", - phaseSqlCount: "Counting", - phaseComparing: "Comparing", - phasePersisting: "Writing", - phasePersistingFail: "Writing fails", - phasePersistingOk: "Writing passes", - phaseDone: "Done", - phaseFailed: "Failed", - phaseCancelled: "Cancelled", sheetPending: "Pending", - colProgress: "Progress / result", ranWithDuration: "Compare finished ({{s}}s)", unchangedNotStored: "Success rows were counted but not stored. Set “Store success rows” to sample and re-run for spot-check.", unchangedSampleHint: "{{total}} success rows total; browsing a stratified sample of {{listed}}. Search any route for live source lookup. Pass rate uses all {{total}}.", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7369dfd..5247a88 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -499,26 +499,13 @@ const zh = { runStatusFailed: "失败", runStatusDoneSec: "完成 {{s}}s", runProgress: "{{phase}} · 表 {{i}}/{{n}} · {{sheet}} · 已用 {{s}}s", - runSheetProgress: "表 {{i}}/{{n}}", - runElapsed: "已用 {{s}}s", runRowsLoaded: "已加载 {{side}} {{n}} 行", runRowsSqlCount: "库内统计 {{side}} {{n}} 行", runPersisting: "已写入 {{done}}/{{total}} 行", runEngineSql: "引擎 SQL", runEnginePython: "引擎 Python", runEngineNote: "原因 {{note}}", - phaseQueued: "排队中", - phaseLoading: "加载数据", - phaseSqlCount: "库内统计", - phaseComparing: "比对中", - phasePersisting: "写入结果", - phasePersistingFail: "写入失败行", - phasePersistingOk: "写入成功行", - phaseDone: "完成", - phaseFailed: "失败", - phaseCancelled: "已取消", sheetPending: "等待中", - colProgress: "进度 / 结果", ranWithDuration: "比对完成(耗时 {{s}} 秒)", unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。", unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。", diff --git a/web/src/index.css b/web/src/index.css index d3e61d5..d70fc9d 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11700,9 +11700,10 @@ html.login-page--paused .login-page__flare { .bs-cmp-progress { display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 12px; + flex-wrap: wrap; + align-items: baseline; + gap: 8px 14px; + padding: 8px 12px; border-radius: 8px; border: 1px solid rgba(59, 130, 246, 0.35); background: rgba(37, 99, 235, 0.12); @@ -11715,147 +11716,11 @@ html.login-page--paused .login-page__flare { background: rgba(239, 68, 68, 0.12); } -.bs-cmp-progress.is-cancelled { - border-color: rgba(245, 158, 11, 0.4); - background: rgba(245, 158, 11, 0.1); -} - -.bs-cmp-progress__head { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 12px; -} - -.bs-cmp-progress__actions { - margin-left: auto; - display: flex; - align-items: center; - gap: 6px; -} - -.bs-cmp-progress__engine, -.bs-cmp-progress__elapsed { - font-size: 0.8125rem; -} - -.bs-cmp-progress__track, -.bs-cmp-run-prog__track { - height: 6px; - border-radius: 999px; - background: rgba(148, 163, 184, 0.28); - overflow: hidden; -} - -.bs-cmp-progress__fill, -.bs-cmp-run-prog__fill { - height: 100%; - width: 0; - border-radius: inherit; - background: linear-gradient(90deg, #2563eb, #60a5fa); - transition: width 0.35s ease; -} - -.bs-cmp-progress__fill.is-indeterminate, -.bs-cmp-run-prog__fill.is-indeterminate { - width: 36%; - animation: bs-cmp-prog-slide 1.25s ease-in-out infinite; -} - -@keyframes bs-cmp-prog-slide { - 0% { - transform: translateX(-120%); - } - 100% { - transform: translateX(320%); - } -} - -.bs-cmp-progress__meta { - font-size: 0.8125rem; - color: var(--bs-cmp-muted, #94a3b8); - line-height: 1.4; - word-break: break-word; -} - .bs-cmp-progress__msg { flex: 1 1 100%; font-size: 0.8125rem; } -/* Compare batch list (job detail → 对比批次) */ -.bs-cmp-runs { - display: flex; - flex-direction: column; - gap: 10px; - min-height: 260px; -} - -.bs-cmp-runs__toolbar { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: space-between; - gap: 10px 14px; -} - -.bs-cmp-runs__hint { - margin: 0; - flex: 1 1 220px; - line-height: 1.45; -} - -.bs-cmp-runs__table tbody tr.is-selected td { - background: rgba(37, 99, 235, 0.08); -} - -.bs-cmp-runs__elapsed { - margin-top: 4px; - font-size: 0.75rem; - color: var(--muted, #64748b); - font-variant-numeric: tabular-nums; -} - -.bs-cmp-runs__sides { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 2px 6px; - font-size: 0.8125rem; - line-height: 1.35; - max-width: 22rem; -} - -.bs-cmp-runs__arrow { - color: var(--muted, #94a3b8); - flex: 0 0 auto; -} - -.bs-cmp-runs__summary { - font-size: 0.8125rem; - line-height: 1.4; - color: var(--ink, #334155); - max-width: 18rem; -} - -.bs-cmp-run-prog { - display: flex; - flex-direction: column; - gap: 5px; - min-width: 160px; - max-width: 22rem; -} - -.bs-cmp-run-prog__meta { - font-size: 0.75rem; - line-height: 1.35; - color: var(--muted, #64748b); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - /* Native :fullscreen + CSS immersive fallback (class only when FS API blocked) */ .bs-cmp-board.is-fullscreen, .bs-cmp-board:fullscreen { diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 5ed8927..4ebbf6d 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -336,106 +336,6 @@ function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—"; } -type RunProgressInfo = { - phase?: string; - sheet_index?: number; - sheet_total?: number; - sheet_title?: string; - sheet_id?: string; - elapsed_ms?: number; - load_side?: string; - rows_loaded?: number; - engine?: string; - engine_note?: string; - persisted?: number; - persist_total?: number; -}; - -function localizeRunPhase(phase: string | undefined, t: (key: string) => string): string { - const p = String(phase || "").trim().toLowerCase(); - if (!p || p === "…") return "…"; - if (p.startsWith("persisting")) { - if (p.includes("fail")) return t("bizCompare.phasePersistingFail"); - if (p.includes("ok")) return t("bizCompare.phasePersistingOk"); - return t("bizCompare.phasePersisting"); - } - const map: Record = { - queued: "bizCompare.phaseQueued", - loading: "bizCompare.phaseLoading", - sql_count: "bizCompare.phaseSqlCount", - comparing: "bizCompare.phaseComparing", - done: "bizCompare.phaseDone", - failed: "bizCompare.phaseFailed", - cancelled: "bizCompare.phaseCancelled", - }; - const key = map[p]; - return key ? t(key) : String(phase); -} - -function runProgressPercent(prog: RunProgressInfo): number { - const total = Number(prog.sheet_total || 0); - const idx = Number(prog.sheet_index || 0); - if (total <= 0) return 8; - const phase = String(prog.phase || "").toLowerCase(); - if (phase === "queued") return 4; - if (phase === "done") return 100; - let base = Math.max(0, Math.min(idx, total)) / total; - const persistTotal = Number(prog.persist_total || 0); - const persisted = Number(prog.persisted || 0); - if (persistTotal > 0 && phase.startsWith("persisting")) { - const within = Math.min(1, persisted / persistTotal) * (1 / total); - base = Math.max(0, (idx - 1) / total) + within; - } else if (phase === "loading" || phase === "sql_count" || phase === "comparing") { - base = Math.max(0, (idx - 1) / total) + 0.35 / total; - } - return Math.max(4, Math.min(99, Math.round(base * 100))); -} - -function runProgressMetaLine( - prog: RunProgressInfo, - t: (key: string, vars?: Record) => string, -): string { - const parts: string[] = []; - const phase = localizeRunPhase(prog.phase, t); - if (phase && phase !== "…") parts.push(phase); - const total = Number(prog.sheet_total || 0); - if (total > 0) { - parts.push( - t("bizCompare.runSheetProgress", { - i: String(prog.sheet_index || 0), - n: String(total), - }), - ); - } - const title = String(prog.sheet_title || prog.sheet_id || "").trim(); - if (title) parts.push(title); - const rows = Number(prog.rows_loaded || 0); - if (rows > 0) { - const eng = String(prog.engine || "").toLowerCase(); - parts.push( - eng === "sql" - ? t("bizCompare.runRowsSqlCount", { - side: String(prog.load_side || "—"), - n: String(rows), - }) - : t("bizCompare.runRowsLoaded", { - side: String(prog.load_side || "—"), - n: String(rows), - }), - ); - } - const persistTotal = Number(prog.persist_total || 0); - if (String(prog.phase || "").startsWith("persisting") && persistTotal > 0) { - parts.push( - t("bizCompare.runPersisting", { - done: String(prog.persisted || 0), - total: String(persistTotal), - }), - ); - } - return parts.join(" · "); -} - function templateSheets(tpl?: Template | null): MetricSheet[] { if (!tpl) return []; if (tpl.metrics?.length) return tpl.metrics; @@ -2037,7 +1937,25 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage setBusy(false); } }; - const runProgress = (runDetail?.summary?.progress || {}) as RunProgressInfo; + const runProgress = (runDetail?.summary?.progress || {}) as { + phase?: string; + sheet_index?: number; + sheet_total?: number; + sheet_title?: string; + sheet_id?: string; + elapsed_ms?: number; + before_count?: number; + after_count?: number; + diff_rows?: number; + load_side?: string; + rows_loaded?: number; + engine?: string; + engine_note?: string; + persisted?: number; + persist_total?: number; + fail_rows?: number; + ok_rows?: number; + }; const runEngine = String(runProgress.engine || "").toLowerCase(); // Poll active compare runs so the modal can be closed and reopened safely. @@ -3091,8 +3009,10 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage renderJobForm() ) : jobDetailTab === "runs" ? (
-
-

{t("bizCompare.runsHint")}

+
+

+ {t("bizCompare.runsHint")} +

) : ( -
- +
+
- + @@ -3139,11 +3059,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage Number(sum.removed || 0) + Number(sum.changed || 0); const ok = Number(sum.unchanged || 0); const durMs = Number(sum.duration_ms || 0); - const prog = ((r as any).progress || - (r as any).summary?.progress || - {}) as RunProgressInfo; - const pct = active ? runProgressPercent(prog) : 100; - const elapsedSec = Math.round(Number(prog.elapsed_ms || 0) / 1000); + const prog = ((r as any).progress || {}) as { + phase?: string; + sheet_index?: number; + sheet_total?: number; + sheet_title?: string; + rows_loaded?: number; + load_side?: string; + engine?: string; + engine_note?: string; + }; + const eng = String(prog.engine || "").toLowerCase(); const stLabel = st === "running" || st === "queued" ? t("bizCompare.runStatusRunning") @@ -3154,76 +3080,62 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage : t("bizCompare.runStatusDoneSec", { s: String(durMs > 0 ? Math.round(durMs / 1000) : 0), }); - const chipColor = active - ? "accent" - : st === "cancelled" - ? "warning" - : jobChipColor(st); - const meta = active ? runProgressMetaLine(prog, t) : ""; return ( - +
{t("bizCompare.colStatus")} {t("bizCompare.colTime")} {t("bizCompare.sidesTitle")}{t("bizCompare.colProgress")}{t("bizCompare.colResult")} {t("bizCompare.colActions")}
- {stLabel} - {active && elapsedSec > 0 ? ( -
- {t("bizCompare.runElapsed", { s: String(elapsedSec) })} + {stLabel} + {active && (prog.phase || eng) ? ( +
+ {eng === "sql" + ? t("bizCompare.runEngineSql") + : eng === "python" + ? t("bizCompare.runEnginePython") + : ""} + {eng && prog.engine_note + ? ` · ${prog.engine_note}` + : ""} + {prog.phase ? `${eng ? " · " : ""}${prog.phase}` : ""} + {prog.sheet_total + ? ` · ${prog.sheet_index || 0}/${prog.sheet_total}` + : ""} + {prog.sheet_title ? ` · ${prog.sheet_title}` : ""} + {Number(prog.rows_loaded || 0) > 0 + ? eng === "sql" + ? ` · ${prog.load_side || ""} ${prog.rows_loaded}` + : ` · ${prog.load_side || ""} ${prog.rows_loaded}` + : ""} +
+ ) : null} + {(r as any).message ? ( +
+ {String((r as any).message).slice(0, 80)}
) : null}
{when}{when} -
- {sideDeviceName(before)} - {sideCollectTime(before)} - - → - - {sideDeviceName(after)} - {sideCollectTime(after)} -
+ {sideDeviceName(before)} {sideCollectTime(before)} + {" → "} + {sideDeviceName(after)} {sideCollectTime(after)}
- {active ? ( -
-
-
0 - ? { width: `${pct}%` } - : undefined - } - /> -
-
- {meta || "…"} -
-
- ) : ( -
- {st === "failed" || st === "cancelled" - ? String((r as any).message || stLabel).slice(0, 96) - : t("bizCompare.runBatchSummary", { - fail: String(fail), - ok: String(ok), - added: String(Number(sum.added || 0)), - })} -
- )} + {active + ? "…" + : t("bizCompare.runBatchSummary", { + fail: String(fail), + ok: String(ok), + added: String(Number(sum.added || 0)), + })}
-
+
-
-
-
-
0 - ? { width: `${runProgressPercent(runProgress)}%` } - : undefined - } - /> -
-
- {runProgressMetaLine(runProgress, t)} -
+ ) : null} + {runDetail.message ? ( + {String(runDetail.message)} + ) : null}
) : null} {runDetail && runStatus === "failed" ? (
-
- {t("bizCompare.runStatusFailed")} -
-
{String(runDetail.message || "")}
-
- ) : null} - {runDetail && runStatus === "cancelled" ? ( -
-
- {t("bizCompare.runStatusCancelled")} -
-
- {String(runDetail.message || t("bizCompare.runCancelled"))} -
+ {t("bizCompare.runStatusFailed")} + {String(runDetail.message || "")}
) : null} {runDetail ? (