Skip to content

fix: Dashboard health scores use pre-computed snapshots in Docker#476

Merged
csharpfritz merged 3 commits intomainfrom
dev
Mar 17, 2026
Merged

fix: Dashboard health scores use pre-computed snapshots in Docker#476
csharpfritz merged 3 commits intomainfrom
dev

Conversation

@csharpfritz
Copy link
Copy Markdown
Collaborator

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:

  • \dev-docs/reference-baselines.json\ → property/event baselines
  • \src/BlazorWebFormsComponents.Test/\ → test detection
  • \docs/\ → documentation detection
  • \ComponentCatalog.cs\ → sample page detection

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

File Change
\HealthSnapshotGenerator.cs\ New — serializes/deserializes health reports as JSON
\ComponentHealthService.cs\ Added snapshot constructor + \IsFromSnapshot\ property
\ServiceCollectionExtensions.cs\ \AddComponentHealthDashboard()\ falls back to \health-snapshot.json\
\GenerateHealthSnapshot/\ New console tool that generates the snapshot
\Dockerfile\ Runs snapshot generator during publish stage
\Generate-HealthSnapshot.ps1\ Wrapper script for local use

Behavior

Environment How it works
Local dev Live reflection + file scanning (unchanged)
Docker/prod Loads pre-computed \health-snapshot.json\ with accurate scores

Verified

  • ✅ All 2,290 tests pass
  • ✅ Snapshot generates 59 component reports with scores ranging 15–100%
  • ✅ Library, sample app, and generator all build cleanly

csharpfritz and others added 3 commits March 17, 2026 10:02
- 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

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.

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"); to System.IO.Path.Join(solutionRoot, "dev-docs", "reference-baselines.json");.
  • On line 56: change System.IO.Path.Combine(solutionRoot, "dev-docs", "tracked-components.json"); to System.IO.Path.Join(solutionRoot, "dev-docs", "tracked-components.json");.
  • On line 69: change System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName); to System.IO.Path.Join(assemblyDir, HealthSnapshotGenerator.SnapshotFileName);.
  • On line 75: change System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName); to System.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.

Suggested changeset 1
src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
--- a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
+++ b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
@@ -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);
         }
 
EOF
@@ -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);
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
{
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

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.

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 AddComponentHealthDashboard method, swap System.IO.Path.Combine for System.IO.Path.Join on the two snapshot path constructions.
  • No additional methods, imports, or definitions are needed.
Suggested changeset 1
src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
--- a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
+++ b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
@@ -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);
         }
 
EOF
@@ -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);
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
{
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

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.

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.

Suggested changeset 1
src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
--- a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
+++ b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs
@@ -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);
         }
 
EOF
@@ -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);
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
@csharpfritz csharpfritz merged commit f1e050d into main Mar 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants