From 1df2d76c9af41a1314ae0f47eb6ed7ed75c79404 Mon Sep 17 00:00:00 2001 From: win gutmann Date: Thu, 26 Mar 2026 00:29:58 -0400 Subject: [PATCH] fix: deploy DLL safeguard and replay scope badge redesign - Trim deploy DLL list to only plugin-unique assemblies, preventing overwrites of SimHub's own System.*/Microsoft.* DLLs that break its web server and plugin loader via binding-redirect mismatches - Add version-conflict guard in Copy-DeployDlls that blocks deploys when assembly versions differ from SimHub's existing copies - Replace replay scope toggle buttons with auto-detected read-only badges (full/partial) that reflect plugin session detection state - Auto-detect replay scope from IsReplaySessionCompleted() at preflight start instead of requiring manual dashboard toggle Co-Authored-By: Claude Sonnet 4.6 --- deploy.ps1 | 35 +++++++++++++++- .../data-capture-suite.html | 41 +++++++------------ .../SimStewardPlugin.DataCaptureSuite.cs | 3 ++ 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/deploy.ps1 b/deploy.ps1 index 55fefc4..407121d 100644 --- a/deploy.ps1 +++ b/deploy.ps1 @@ -102,13 +102,27 @@ Push-LokiEvent 'deploy_started' 'INFO' 'Deploy script started' @{ loki_url = $env:SIMSTEWARD_LOKI_URL } +# ── Plugin DLLs to deploy ──────────────────────────────────────────────────── +# ONLY list DLLs that SimHub does NOT ship. Overwriting SimHub's own assemblies +# (System.*, Microsoft.*, Newtonsoft.Json, etc.) breaks its web server and +# plugin loader via binding-redirect mismatches. See docs/DEPLOY-DLLS.md. $PluginDlls = @( "SimSteward.Plugin.dll", "Fleck.dll", - "Newtonsoft.Json.dll", "IRSDKSharper.dll", "YamlDotNet.dll", - "Sentry.dll" + "Sentry.dll", + "System.Text.Json.dll", + # OpenTelemetry + gRPC + "OpenTelemetry.dll", + "OpenTelemetry.Api.dll", + "OpenTelemetry.Api.ProviderBuilderExtensions.dll", + "OpenTelemetry.Exporter.OpenTelemetryProtocol.dll", + "Google.Protobuf.dll", + "Grpc.Core.dll", + "Grpc.Core.Api.dll", + "grpc_csharp_ext.x64.dll", + "grpc_csharp_ext.x86.dll" ) function Read-PluginDllProductVersion { @@ -317,10 +331,27 @@ Push-LokiEvent 'deploy_dlls_cleaned' 'INFO' "Removed $($deletedDlls.Count) exist } # ── 3. Copy build files to target location ────────────────────────────────── +# Guard: refuse to overwrite a SimHub-shipped DLL if its assembly version differs +# from ours. This prevents breaking SimHub's binding redirects. function Copy-DeployDlls { foreach ($d in $PluginDlls) { $src = Join-Path $outDir $d if (-not (Test-Path $src)) { throw "Build output missing: $src" } + $existing = Join-Path $SimHubPath $d + if ((Test-Path $existing) -and $d -notlike "SimSteward.*") { + try { + $ourVer = [System.Reflection.AssemblyName]::GetAssemblyName((Resolve-Path $src).Path).Version.ToString() + $theirVer = [System.Reflection.AssemblyName]::GetAssemblyName((Resolve-Path $existing).Path).Version.ToString() + if ($ourVer -ne $theirVer) { + Push-LokiEvent 'deploy_dll_version_conflict' 'ERROR' "Refusing to overwrite $d (SimHub=$theirVer, ours=$ourVer)" @{ + dll = $d; simhub_version = $theirVer; our_version = $ourVer + } + Write-Error "BLOCKED: $d already in SimHub with version $theirVer (ours is $ourVer). Overwriting would break SimHub. Remove $d from `$PluginDlls or match the version." + } + } catch [System.BadImageFormatException] { + # native DLL (e.g. grpc_csharp_ext) — no managed assembly version, skip check + } + } Copy-Item $src $SimHubPath -Force } } diff --git a/src/SimSteward.Dashboard/data-capture-suite.html b/src/SimSteward.Dashboard/data-capture-suite.html index e239650..c2e4dda 100644 --- a/src/SimSteward.Dashboard/data-capture-suite.html +++ b/src/SimSteward.Dashboard/data-capture-suite.html @@ -96,9 +96,8 @@ .pc-name { font-size: 0.82rem; flex: 1; } .pc-level { font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; } .pc-detail { font-size: 0.72rem; color: var(--muted); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.scope-btn { transition: opacity 0.2s, border-color 0.2s; } -.scope-btn:not(.active) { opacity: 0.35; } -.scope-btn.active { border-color: var(--accent); color: var(--accent); } +.scope-badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 0.76rem; font-weight: 600; border: 1px solid var(--border); color: var(--muted); opacity: 0.35; transition: opacity 0.3s, border-color 0.3s, color 0.3s, background 0.3s; cursor: default; } +.scope-badge.active { opacity: 1; border-color: var(--accent); color: var(--accent); background: rgba(99,144,255,0.08); } @keyframes spin { to { transform: rotate(360deg); } } .pc-spinner { display: inline-block; width: 10px; height: 10px; border: 2px solid var(--accent); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } @@ -277,8 +276,6 @@ SimHub Grafana Replay - Full replay - @@ -287,8 +284,8 @@
2 Pre-test Conditions
Replay scope: - - + Full replay + Partial replay
@@ -788,16 +785,8 @@ pf.correlationId ? pf.correlationId.slice(0, 8) + '\u2026' : '\u2014'; document.getElementById('lbl-preflight-level').textContent = pf.level || 0; - // Scope toggle sync - const scopeFull = document.getElementById('btn-scope-full'); - const scopePartial = document.getElementById('btn-scope-partial'); - if (pf.replayScope === 'partial') { - scopeFull.classList.remove('active'); - scopePartial.classList.add('active'); - } else { - scopeFull.classList.add('active'); - scopePartial.classList.remove('active'); - } + // Scope badge sync — reflects what the app detected + updateScopeBadge(pf.replayScope === 'partial' ? 'partial' : 'full'); // Button text const btn = document.getElementById('btn-preflight'); @@ -824,12 +813,10 @@ } } -function setScope(scope) { - _replayScope = scope; - document.getElementById('btn-scope-full').classList.toggle('active', scope === 'full'); - document.getElementById('btn-scope-partial').classList.toggle('active', scope === 'partial'); - send({ action: 'data_capture_suite', arg: 'preflight_scope:' + scope }); - send({ action: 'log', event: 'dashboard_ui_event', element_id: 'btn-scope-' + scope, event_type: 'click', message: 'Set replay scope: ' + scope }); +function updateScopeBadge(scope) { + if (scope) _replayScope = scope; + document.getElementById('badge-scope-full').classList.toggle('active', scope === 'full'); + document.getElementById('badge-scope-partial').classList.toggle('active', scope === 'partial'); } function runPreflight() { @@ -1042,8 +1029,9 @@ setSig('sig-simhub', diag.simHubHttpListening); setSig('sig-grafana', diag.grafanaConfigured); setSig('sig-replay', replay); - setSig('sig-full', diag.replaySessionCompleted); - document.getElementById('sig-partial-wrap').hidden = !(replay && !diag.replaySessionCompleted); + + // Replay scope badge — auto-detected from session data + updateScopeBadge(replay ? (diag.replaySessionCompleted ? 'full' : 'partial') : null); // Preflight / Precondition card const pf = msg.preflight; @@ -1054,11 +1042,10 @@ if (pf && pf.phase === 'complete') { setSig('sig-grafana', pf.grafanaOk); setSig('sig-simhub', pf.simHubOk); - setSig('sig-full', pf.checkeredOk); } const preflightPassed = pf && pf.allPassed; - const ready = _wsOk && _pluginSeen && diag.simHubHttpListening && diag.grafanaConfigured && replay && diag.replaySessionCompleted && preflightPassed; + const ready = _wsOk && _pluginSeen && diag.simHubHttpListening && diag.grafanaConfigured && replay && preflightPassed; // Guard banner const guard = document.getElementById('guard-banner'); diff --git a/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs b/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs index b461523..1175f9c 100644 --- a/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs +++ b/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs @@ -328,6 +328,9 @@ private void BeginPreflight() if (_preflightSnapshot.MiniTests == null || _preflightLevel == 0) _preflightSnapshot.MiniTests = BuildPreflightMiniTests(); + // Auto-detect replay scope from session data + _preflightReplayScope = IsReplaySessionCompleted() ? "full" : "partial"; + _preflightSnapshot.Phase = "running"; _preflightSnapshot.CorrelationId = _preflightCorrelationId; _preflightSnapshot.ReplayScope = _preflightReplayScope;