fix: Dashboard health scores use pre-computed snapshots in Docker#476
fix: Dashboard health scores use pre-computed snapshots in Docker#476csharpfritz merged 3 commits intomainfrom
Conversation
- Replace 9 broken cross-references in AjaxToolkit docs (Panel, ListBox, DropDownList, Image, RequiredFieldValidator) with plain text mentions since those doc pages don't exist yet - Fix broken sample link in Migration/NET-Standard.md - Replace 9 broken screenshot image links in migration test reports (runs 2, 3, 4) with text placeholders - Fix invalid anchor #Migration---Getting-Started in Migration/readme.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The AfterBlazorClientSide sample is not currently deployed. Remove restore, build, publish and artifact steps for client-side from the release workflow, matching the same fix applied to demo.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Component Health Dashboard showed flat 18% for all components when deployed in Docker because the service scanned the repo filesystem (dev-docs/, src/, docs/) which doesn't exist in the container. Changes: - Add HealthSnapshotGenerator to serialize/deserialize health reports - Add snapshot constructor to ComponentHealthService for pre-computed data - Update AddComponentHealthDashboard() to fall back to health-snapshot.json when repo files aren't found (assembly dir, then AppContext.BaseDirectory) - Add GenerateHealthSnapshot console tool (scripts/GenerateHealthSnapshot/) - Update Dockerfile to generate snapshot during publish stage while full repo is still available in /src - Add Generate-HealthSnapshot.ps1 wrapper script Local dev: live reflection + file scanning (unchanged) Docker/prod: pre-computed snapshot with accurate scores (67-100% range) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| var baselines = ReferenceBaselines.LoadFromFile(baselinesPath); | ||
| var healthService = new ComponentHealthService(baselines, solutionRoot); | ||
| services.AddSingleton(healthService); | ||
| return services; |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, this warning is fixed by replacing System.IO.Path.Combine with System.IO.Path.Join in cases where you conceptually want simple path concatenation and do not want later absolute segments to discard earlier ones. Path.Join uses similar separator-handling semantics, but it does not treat an absolute later segment as a signal to drop preceding segments.
For this file, the safest fix that preserves behavior is to update all the Path.Combine calls within AddComponentHealthDashboard to use Path.Join instead. This includes the construction of baselinesPath, trackedPath, the assembly-local snapshotPath, and the base-directory snapshotPath. These are all cases where the intent is "base + relative pieces" and not "allow an absolute later piece to override the base", so Path.Join is a strictly safer choice with no functional loss. No new using directives are required because the code already uses fully qualified System.IO.Path. Concretely:
- On line 55: change
System.IO.Path.Combine(solutionRoot, "dev-docs", "reference-baselines.json");toSystem.IO.Path.Join(solutionRoot, "dev-docs", "reference-baselines.json");. - On line 56: change
System.IO.Path.Combine(solutionRoot, "dev-docs", "tracked-components.json");toSystem.IO.Path.Join(solutionRoot, "dev-docs", "tracked-components.json");. - On line 69: change
System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName);toSystem.IO.Path.Join(assemblyDir, HealthSnapshotGenerator.SnapshotFileName);. - On line 75: change
System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName);toSystem.IO.Path.Join(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName);.
No additional methods, types, or imports are needed; Path.Join is available from .NET Core 2.1+ / .NET Standard 2.1+ in System.IO.
| @@ -52,8 +52,8 @@ | ||
| /// <returns>The service collection for chaining</returns> | ||
| public static IServiceCollection AddComponentHealthDashboard(this IServiceCollection services, string solutionRoot) | ||
| { | ||
| var baselinesPath = System.IO.Path.Combine(solutionRoot, "dev-docs", "reference-baselines.json"); | ||
| var trackedPath = System.IO.Path.Combine(solutionRoot, "dev-docs", "tracked-components.json"); | ||
| var baselinesPath = System.IO.Path.Join(solutionRoot, "dev-docs", "reference-baselines.json"); | ||
| var trackedPath = System.IO.Path.Join(solutionRoot, "dev-docs", "tracked-components.json"); | ||
|
|
||
| // If the repo filesystem exists, use live reflection/file scanning | ||
| if (System.IO.File.Exists(baselinesPath) || System.IO.File.Exists(trackedPath)) | ||
| @@ -66,13 +66,13 @@ | ||
|
|
||
| // Fallback: look for a pre-generated snapshot alongside the running assembly | ||
| var assemblyDir = System.IO.Path.GetDirectoryName(typeof(ComponentHealthService).Assembly.Location) ?? ""; | ||
| var snapshotPath = System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); | ||
| var snapshotPath = System.IO.Path.Join(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); | ||
| var snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
|
|
||
| // Also check the app's base directory (common for published apps) | ||
| if (snapshotReports == null) | ||
| { | ||
| snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotPath = System.IO.Path.Join(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
| } | ||
|
|
| { | ||
| snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
| } |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, to avoid Path.Combine silently discarding earlier segments when a later segment is absolute, use Path.Join when you are combining paths where you expect all components except possibly the first to be relative file/directory names. Path.Join concatenates the segments with directory separators but does not treat later absolute segments as overriding earlier ones.
In this file, the best behavior-preserving change is to replace the two relevant Path.Combine calls with Path.Join:
- At line 69:
var snapshotPath = System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); - At line 75:
snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName);
Path.Join is available in System.IO in modern .NET, and since the code is already using fully qualified System.IO.Path, no new using directives are required. The semantics for the intended case (base directory + file name) remain effectively the same, but we eliminate the risk that a future change making SnapshotFileName absolute will cause the base path to be ignored.
Concretely:
- Edit
src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs. - In the
AddComponentHealthDashboardmethod, swapSystem.IO.Path.CombineforSystem.IO.Path.Joinon the two snapshot path constructions. - No additional methods, imports, or definitions are needed.
| @@ -66,13 +66,13 @@ | ||
|
|
||
| // Fallback: look for a pre-generated snapshot alongside the running assembly | ||
| var assemblyDir = System.IO.Path.GetDirectoryName(typeof(ComponentHealthService).Assembly.Location) ?? ""; | ||
| var snapshotPath = System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); | ||
| var snapshotPath = System.IO.Path.Join(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); | ||
| var snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
|
|
||
| // Also check the app's base directory (common for published apps) | ||
| if (snapshotReports == null) | ||
| { | ||
| snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotPath = System.IO.Path.Join(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
| } | ||
|
|
| { | ||
| var healthService = new ComponentHealthService(snapshotReports); | ||
| services.AddSingleton(healthService); | ||
| } |
Check notice
Code scanning / CodeQL
Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, to avoid Path.Combine silently discarding earlier segments when a later segment is an absolute path, you should either (1) ensure all later segments are relative (not rooted), or (2) switch to Path.Join, which concatenates the segments without treating an absolute later path as resetting the earlier path.
For this specific instance, the intended behavior is to build a path under AppContext.BaseDirectory using HealthSnapshotGenerator.SnapshotFileName. That’s a straightforward “base directory + file name” operation, so replacing System.IO.Path.Combine with System.IO.Path.Join preserves current behavior while eliminating the risk of silently dropping BaseDirectory in the future. No other logic or flow needs to change. The change is local to src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs, line 75, and uses the existing System.IO namespace via fully qualified calls, so no new imports are required.
| @@ -72,7 +72,7 @@ | ||
| // Also check the app's base directory (common for published apps) | ||
| if (snapshotReports == null) | ||
| { | ||
| snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotPath = System.IO.Path.Join(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); | ||
| snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath); | ||
| } | ||
|
|
Problem
The Component Health Dashboard at /dashboard\ showed flat 18% health scores for ALL components when deployed via Docker. This happened because the service scanned the repo filesystem (\dev-docs/, \src/, \docs/) which doesn't exist in the container — only published DLLs are present at /app/.
Root Cause
\ComponentHealthService\ navigates from \ContentRootPath\ up two levels to find the repo root, then scans:
In Docker, none of these paths exist. Only the \Complete\ status dimension contributes (10%/55% = 18%).
Solution
Generate a static health snapshot at Docker build time (when the full repo IS available) and fall back to it at runtime.
Changes
Behavior
Verified