Skip to content

Commit f1e050d

Browse files
authored
Merge pull request #476 from FritzAndFriends/dev
fix: Dashboard health scores use pre-computed snapshots in Docker
2 parents 92639ff + 68a096b commit f1e050d

File tree

7 files changed

+232
-4
lines changed

7 files changed

+232
-4
lines changed

samples/AfterBlazorServerSide/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ COPY ["Directory.Build.props", "./"]
1212
COPY ["samples/AfterBlazorServerSide/AfterBlazorServerSide.csproj", "samples/AfterBlazorServerSide/"]
1313
COPY ["src/BlazorWebFormsComponents/BlazorWebFormsComponents.csproj", "src/BlazorWebFormsComponents/"]
1414
COPY ["samples/SharedSampleObjects/SharedSampleObjects.csproj", "samples/SharedSampleObjects/"]
15+
COPY ["scripts/GenerateHealthSnapshot/GenerateHealthSnapshot.csproj", "scripts/GenerateHealthSnapshot/"]
1516
RUN dotnet restore "samples/AfterBlazorServerSide/AfterBlazorServerSide.csproj"
17+
RUN dotnet restore "scripts/GenerateHealthSnapshot/GenerateHealthSnapshot.csproj"
1618
COPY . .
1719
# Remove NBGV - not functional in Docker without .git; version is injected via VERSION build arg
1820
RUN sed -i '/<PackageReference Include="Nerdbank.GitVersioning"/,/<\/PackageReference>/d' Directory.Build.props
@@ -21,6 +23,8 @@ RUN dotnet build "AfterBlazorServerSide.csproj" -c Release -o /app/build -p:Vers
2123

2224
FROM build AS publish
2325
RUN dotnet publish "AfterBlazorServerSide.csproj" -c Release -o /app/publish -p:Version=$VERSION -p:InformationalVersion=$VERSION
26+
# Generate health snapshot while the full repo is still available in /src
27+
RUN dotnet run --project /src/scripts/GenerateHealthSnapshot/GenerateHealthSnapshot.csproj -- /src /app/publish/health-snapshot.json
2428

2529
FROM base AS final
2630
WORKDIR /app
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<#
2+
.SYNOPSIS
3+
Generates a health-snapshot.json file containing pre-computed Component Health Dashboard data.
4+
5+
.DESCRIPTION
6+
Builds and runs the GenerateHealthSnapshot console tool, which performs live reflection
7+
and file scanning against the repository to produce a JSON snapshot of all component
8+
health reports. This snapshot can be bundled with published apps so the dashboard
9+
works in environments where the repo filesystem is not available (e.g., Docker).
10+
11+
.PARAMETER OutputPath
12+
Path to write the snapshot file. Defaults to the repo root's health-snapshot.json.
13+
14+
.EXAMPLE
15+
.\Generate-HealthSnapshot.ps1
16+
.\Generate-HealthSnapshot.ps1 -OutputPath ./publish/health-snapshot.json
17+
#>
18+
param(
19+
[string]$OutputPath
20+
)
21+
22+
$ErrorActionPreference = 'Stop'
23+
24+
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
25+
$projectPath = Join-Path $PSScriptRoot 'GenerateHealthSnapshot' 'GenerateHealthSnapshot.csproj'
26+
27+
if (-not $OutputPath) {
28+
$OutputPath = Join-Path $repoRoot 'health-snapshot.json'
29+
}
30+
31+
Write-Host "Building snapshot generator..." -ForegroundColor Cyan
32+
dotnet build $projectPath -c Release --nologo -v quiet
33+
if ($LASTEXITCODE -ne 0) {
34+
Write-Error "Build failed."
35+
exit 1
36+
}
37+
38+
Write-Host "Generating health snapshot..." -ForegroundColor Cyan
39+
dotnet run --project $projectPath -c Release --no-build -- $repoRoot $OutputPath
40+
if ($LASTEXITCODE -ne 0) {
41+
Write-Error "Snapshot generation failed."
42+
exit 1
43+
}
44+
45+
Write-Host "Health snapshot written to: $OutputPath" -ForegroundColor Green
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<!-- Exclude from NBGV versioning to keep this tool simple -->
9+
<NerdbankGitVersioningEnabled>false</NerdbankGitVersioningEnabled>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\BlazorWebFormsComponents\BlazorWebFormsComponents.csproj" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using BlazorWebFormsComponents.Diagnostics;
2+
3+
if (args.Length < 2)
4+
{
5+
Console.Error.WriteLine("Usage: GenerateHealthSnapshot <solutionRoot> <outputPath>");
6+
Console.Error.WriteLine(" solutionRoot: Path to the repository root");
7+
Console.Error.WriteLine(" outputPath: Path to write health-snapshot.json");
8+
return 1;
9+
}
10+
11+
var solutionRoot = args[0];
12+
var outputPath = args[1];
13+
14+
Console.WriteLine($"Solution root: {solutionRoot}");
15+
Console.WriteLine($"Output path: {outputPath}");
16+
17+
var baselinesPath = Path.Combine(solutionRoot, "dev-docs", "reference-baselines.json");
18+
var baselines = ReferenceBaselines.LoadFromFile(baselinesPath);
19+
var service = new ComponentHealthService(baselines, solutionRoot);
20+
21+
HealthSnapshotGenerator.GenerateSnapshot(service, outputPath);
22+
23+
var reports = service.GetAllReports();
24+
Console.WriteLine($"Generated snapshot with {reports.Count} component reports.");
25+
26+
return 0;

src/BlazorWebFormsComponents/Diagnostics/ComponentHealthService.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ namespace BlazorWebFormsComponents.Diagnostics
1111
/// <summary>
1212
/// Analyzes BWFC components via reflection and file scanning to produce health reports
1313
/// measuring how completely each Blazor component reproduces its Web Forms original.
14+
/// When the repository filesystem is not available (e.g., in Docker), the service can
15+
/// serve pre-computed reports from a snapshot JSON file.
1416
/// </summary>
1517
public class ComponentHealthService
1618
{
1719
private readonly ReferenceBaselines _baselines;
1820
private readonly string _solutionRoot;
21+
private readonly IReadOnlyList<ComponentHealthReport> _snapshotReports;
1922

2023
private static readonly HashSet<Type> StopTypes = new HashSet<Type>
2124
{
@@ -25,6 +28,12 @@ public class ComponentHealthService
2528
typeof(DataBoundComponent<>)
2629
};
2730

31+
/// <summary>
32+
/// Returns true when this instance is serving pre-computed snapshot data
33+
/// rather than live reflection/file-scanning results.
34+
/// </summary>
35+
public bool IsFromSnapshot => _snapshotReports != null;
36+
2837
// Hardcoded fallback per PRD §3.3 — used when dev-docs/tracked-components.json doesn't exist
2938
private static readonly Dictionary<string, TrackedComponent> DefaultTrackedComponents = new Dictionary<string, TrackedComponent>(StringComparer.OrdinalIgnoreCase)
3039
{
@@ -111,23 +120,42 @@ public class ComponentHealthService
111120
private Dictionary<string, Type> _discoveredTypes;
112121

113122
/// <summary>
114-
/// Creates a new ComponentHealthService.
123+
/// Creates a new ComponentHealthService that performs live reflection and file scanning.
115124
/// </summary>
116125
/// <param name="baselines">Reference baselines loaded from JSON.</param>
117126
/// <param name="solutionRoot">Path to the repository root (for file scanning).</param>
118127
public ComponentHealthService(ReferenceBaselines baselines, string solutionRoot)
119128
{
120129
_baselines = baselines ?? new ReferenceBaselines();
121130
_solutionRoot = solutionRoot ?? "";
131+
_snapshotReports = null;
122132
_trackedComponents = LoadTrackedComponents();
123133
_discoveredTypes = DiscoverComponentTypes();
124134
}
125135

136+
/// <summary>
137+
/// Creates a ComponentHealthService that serves pre-computed snapshot data.
138+
/// Used in environments where the repository filesystem is not available.
139+
/// </summary>
140+
/// <param name="snapshotReports">Pre-computed health reports loaded from a snapshot file.</param>
141+
internal ComponentHealthService(IReadOnlyList<ComponentHealthReport> snapshotReports)
142+
{
143+
_snapshotReports = snapshotReports ?? throw new ArgumentNullException(nameof(snapshotReports));
144+
_baselines = new ReferenceBaselines();
145+
_solutionRoot = "";
146+
_trackedComponents = new Dictionary<string, TrackedComponent>(StringComparer.OrdinalIgnoreCase);
147+
_discoveredTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
148+
}
149+
126150
/// <summary>
127151
/// Gets health reports for all tracked components.
152+
/// When running from a snapshot, returns the pre-computed reports.
128153
/// </summary>
129154
public IReadOnlyList<ComponentHealthReport> GetAllReports()
130155
{
156+
if (_snapshotReports != null)
157+
return _snapshotReports;
158+
131159
var reports = new List<ComponentHealthReport>();
132160
foreach (var kvp in _trackedComponents.OrderBy(k => k.Value.Category).ThenBy(k => k.Key))
133161
{
@@ -142,6 +170,9 @@ public IReadOnlyList<ComponentHealthReport> GetAllReports()
142170
/// </summary>
143171
public ComponentHealthReport GetReport(string componentName)
144172
{
173+
if (_snapshotReports != null)
174+
return _snapshotReports.FirstOrDefault(r => r.Name.Equals(componentName, StringComparison.OrdinalIgnoreCase));
175+
145176
if (!_trackedComponents.TryGetValue(componentName, out var tracked))
146177
return null;
147178
return BuildReport(componentName, tracked);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace BlazorWebFormsComponents.Diagnostics
8+
{
9+
/// <summary>
10+
/// Generates and loads pre-computed health report snapshots.
11+
/// Used to provide accurate health data in environments (e.g., Docker containers)
12+
/// where the repository filesystem is not available.
13+
/// </summary>
14+
public static class HealthSnapshotGenerator
15+
{
16+
/// <summary>
17+
/// The default filename for the health snapshot JSON file.
18+
/// </summary>
19+
public const string SnapshotFileName = "health-snapshot.json";
20+
21+
private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
22+
{
23+
WriteIndented = true,
24+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
25+
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
26+
};
27+
28+
/// <summary>
29+
/// Generates a health snapshot JSON file from the given service.
30+
/// </summary>
31+
/// <param name="service">The health service to snapshot.</param>
32+
/// <param name="outputPath">Full path to write the JSON file.</param>
33+
public static void GenerateSnapshot(ComponentHealthService service, string outputPath)
34+
{
35+
var reports = service.GetAllReports();
36+
var json = JsonSerializer.Serialize(reports, SerializerOptions);
37+
var dir = Path.GetDirectoryName(outputPath);
38+
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
39+
{
40+
Directory.CreateDirectory(dir);
41+
}
42+
File.WriteAllText(outputPath, json);
43+
}
44+
45+
/// <summary>
46+
/// Loads pre-computed health reports from a snapshot JSON file.
47+
/// Returns null if the file does not exist or cannot be parsed.
48+
/// </summary>
49+
/// <param name="snapshotPath">Full path to the snapshot JSON file.</param>
50+
public static IReadOnlyList<ComponentHealthReport>? LoadSnapshot(string snapshotPath)
51+
{
52+
if (!File.Exists(snapshotPath))
53+
return null;
54+
55+
try
56+
{
57+
var json = File.ReadAllText(snapshotPath);
58+
var reports = JsonSerializer.Deserialize<List<ComponentHealthReport>>(json, SerializerOptions);
59+
return reports;
60+
}
61+
catch (JsonException)
62+
{
63+
return null;
64+
}
65+
catch (IOException)
66+
{
67+
return null;
68+
}
69+
}
70+
}
71+
}

src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,51 @@ public static IServiceCollection AddBlazorWebFormsComponents(this IServiceCollec
4444
/// Adds the Component Health Dashboard diagnostic service as a singleton.
4545
/// Loads reference baselines from dev-docs/reference-baselines.json and enables
4646
/// runtime reflection-based health scoring of all tracked components.
47+
/// Falls back to a pre-generated health-snapshot.json when the repository
48+
/// filesystem is not available (e.g., in Docker containers).
4749
/// </summary>
4850
/// <param name="services">The service collection</param>
4951
/// <param name="solutionRoot">Path to the repository root (for file scanning of tests, docs, samples).</param>
5052
/// <returns>The service collection for chaining</returns>
5153
public static IServiceCollection AddComponentHealthDashboard(this IServiceCollection services, string solutionRoot)
5254
{
5355
var baselinesPath = System.IO.Path.Combine(solutionRoot, "dev-docs", "reference-baselines.json");
54-
var baselines = ReferenceBaselines.LoadFromFile(baselinesPath);
55-
var healthService = new ComponentHealthService(baselines, solutionRoot);
56-
services.AddSingleton(healthService);
56+
var trackedPath = System.IO.Path.Combine(solutionRoot, "dev-docs", "tracked-components.json");
57+
58+
// If the repo filesystem exists, use live reflection/file scanning
59+
if (System.IO.File.Exists(baselinesPath) || System.IO.File.Exists(trackedPath))
60+
{
61+
var baselines = ReferenceBaselines.LoadFromFile(baselinesPath);
62+
var healthService = new ComponentHealthService(baselines, solutionRoot);
63+
services.AddSingleton(healthService);
64+
return services;
65+
}
66+
67+
// Fallback: look for a pre-generated snapshot alongside the running assembly
68+
var assemblyDir = System.IO.Path.GetDirectoryName(typeof(ComponentHealthService).Assembly.Location) ?? "";
69+
var snapshotPath = System.IO.Path.Combine(assemblyDir, HealthSnapshotGenerator.SnapshotFileName);
70+
var snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath);
71+
72+
// Also check the app's base directory (common for published apps)
73+
if (snapshotReports == null)
74+
{
75+
snapshotPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, HealthSnapshotGenerator.SnapshotFileName);
76+
snapshotReports = HealthSnapshotGenerator.LoadSnapshot(snapshotPath);
77+
}
78+
79+
if (snapshotReports != null)
80+
{
81+
var healthService = new ComponentHealthService(snapshotReports);
82+
services.AddSingleton(healthService);
83+
}
84+
else
85+
{
86+
// Last resort: use empty baselines (components will show partial health)
87+
var baselines = ReferenceBaselines.LoadFromFile(baselinesPath);
88+
var healthService = new ComponentHealthService(baselines, solutionRoot);
89+
services.AddSingleton(healthService);
90+
}
91+
5792
return services;
5893
}
5994

0 commit comments

Comments
 (0)