diff --git a/internal/ui/handler_test.go b/internal/ui/handler_test.go index 932b774..f6fbbb4 100644 --- a/internal/ui/handler_test.go +++ b/internal/ui/handler_test.go @@ -533,6 +533,23 @@ func TestRegisterRoutes_IndexDeployDetailUsesBackendDefaultsAndImmediateClose(t `if (!kvApplied) suggestions.push({ key: 'kv_cache_dtype', value: 'fp8'`, `deploy_started_background`, `deploy_restore_recommended: 'Recommended parameters'`, + `deploy_compat_title: 'Image architecture preflight'`, + `deploy_compat_title: '\u955C\u50CF\u67B6\u6784\u9884\u68C0'`, + `x-text="t('deploy_compat_title')"`, + `handleDeployEngineChange($event)`, + `handleDeployEngineChange(event) {`, + `deployDryRunSeq: 0`, + `const seq = ++this.deployDryRunSeq;`, + `if (seq !== this.deployDryRunSeq) return;`, + `this.deployDetailPlan = { ...this.deployDetailPlan, compatibility: null };`, + `deployEngineRequestValue()`, + `deploySelectedEngineMeta()`, + `deployPlanMatchesSelectedEngine()`, + `deployCompatibilityRows()`, + `typeof c.image_available_in_docker === 'boolean'`, + `typeof c.image_available_in_containerd === 'boolean'`, + `.deploy-compat-grid,`, + `model.detected_arch`, } { if !strings.Contains(body, token) { t.Fatalf("body missing deploy detail token %q", token) @@ -554,6 +571,22 @@ func TestRegisterRoutes_IndexDeployDetailUsesBackendDefaultsAndImmediateClose(t t.Fatalf("deploy detail should close before awaiting deploy.run, body=%s", fnBody) } + start = strings.Index(body, "async refreshDeployDryRun(forceRecommended = false) {") + if start == -1 { + t.Fatal("refreshDeployDryRun not found") + } + end = strings.Index(body[start:], "\n seedDeployDefaultParams()") + if end == -1 { + t.Fatal("could not isolate refreshDeployDryRun body") + } + fnBody = body[start : start+end] + if strings.Contains(fnBody, "if (!modelName || this.deployDetailLoading) return;") { + t.Fatalf("refreshDeployDryRun still drops newer preflights while loading, body=%s", fnBody) + } + if !strings.Contains(fnBody, "const requestEngine = forceRecommended ? '' : this.deployEngineRequestValue();") { + t.Fatalf("refreshDeployDryRun should map selected engine before dry-run, body=%s", fnBody) + } + if strings.Contains(body, "aima_deploy_defaults:") || strings.Contains(body, "localStorage.setItem(this.deployDefaultsKey()") { t.Fatal("deploy defaults should not be stored only in browser localStorage") } diff --git a/internal/ui/static/index.html b/internal/ui/static/index.html index 30d896c..7e321cb 100644 --- a/internal/ui/static/index.html +++ b/internal/ui/static/index.html @@ -2149,6 +2149,70 @@ } .deploy-vram-fill.warn { background: var(--warning); } .deploy-vram-fill.crit { background: var(--error); } +.deploy-compat-card { + padding: 12px; + background: var(--input-bg); + border: 1px solid var(--border-light); + border-radius: var(--radius-sm); +} +.deploy-compat-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} +.deploy-compat-status { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--text); + font-size: 12px; + font-weight: 700; +} +.deploy-compat-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--warning); + box-shadow: 0 0 12px currentColor; +} +.deploy-compat-dot.ok { background: var(--success); color: var(--success); } +.deploy-compat-dot.blocked { background: var(--error); color: var(--error); } +.deploy-compat-dot.repairable { background: var(--warning); color: var(--warning); } +.deploy-compat-dot.unknown, +.deploy-compat-dot.skipped { background: var(--text-tertiary); color: var(--text-tertiary); } +.deploy-compat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border: 1px solid var(--border-light); + border-radius: 10px; + overflow: hidden; +} +.deploy-compat-row { + display: grid; + grid-template-columns: minmax(96px, 0.7fr) minmax(0, 1fr); + gap: 10px; + padding: 8px 10px; + border-right: 1px solid var(--border-light); + border-bottom: 1px solid var(--border-light); +} +.deploy-compat-row:nth-child(2n) { + border-right: none; +} +.deploy-compat-row:nth-last-child(-n + 2) { + border-bottom: none; +} +.deploy-compat-key { + color: var(--text-tertiary); + font-size: 10px; +} +.deploy-compat-value { + color: var(--text); + font-family: var(--font-mono); + font-size: 11px; + overflow-wrap: anywhere; +} .deploy-vram-breakdown { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -3088,6 +3152,7 @@ .deployment-config-table, .deployment-diagnostics-table, .deploy-vram-top, + .deploy-compat-grid, .deploy-form-grid, .deploy-vram-breakdown { grid-template-columns: 1fr; @@ -3104,6 +3169,19 @@ .deployment-diagnostics-row:last-child { border-bottom: none; } + .deploy-compat-head { + align-items: flex-start; + flex-direction: column; + } + .deploy-compat-row { + border-right: none; + } + .deploy-compat-row:nth-last-child(-n + 2) { + border-bottom: 1px solid var(--border-light); + } + .deploy-compat-row:last-child { + border-bottom: none; + } .deploy-param-row { grid-template-columns: 1fr; } @@ -5919,7 +5997,7 @@