Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a58b68f
docs: add design spec for repository compliance overview redesign and…
davidkallesen May 2, 2026
4a2d9fb
docs: add three-phase implementation plans for compliance overview re…
davidkallesen May 2, 2026
0a34e03
feat(compliance): add RepositoryComplianceSummary domain DTOs
davidkallesen May 3, 2026
3788c54
feat(compliance): add GetComplianceSummary client with two-tier cache
davidkallesen May 3, 2026
e335702
feat(compliance): add ComplianceHealth helper (TDD)
davidkallesen May 3, 2026
982664d
feat(compliance): add ComplianceFilterState and ComplianceFilterEngin…
davidkallesen May 3, 2026
6db541b
feat(compliance): add ComplianceStatusChip primitive
davidkallesen May 3, 2026
093f2e9
feat(compliance): add ComplianceKpiStrip with org-level percentage tiles
davidkallesen May 3, 2026
898d247
feat(compliance): add ComplianceFilterBar with search and dropdowns
davidkallesen May 3, 2026
1417f34
feat(compliance): add ComplianceDashboardRowDetail expanded panel
davidkallesen May 3, 2026
d5e8dba
feat(compliance): add ComplianceDashboardTable with 12 signal columns
davidkallesen May 3, 2026
a2986ef
feat(compliance): add ComplianceCardsGrid reusing repo-card styles
davidkallesen May 3, 2026
d86809b
feat(compliance): rewrite RepositoryComplianceOverview with hybrid la…
davidkallesen May 3, 2026
facacbd
style(compliance): KPI strip, sticky filter bar, row-detail, link
davidkallesen May 3, 2026
8eb7f96
feat(insights): add InsightsKpiTiles for org-level percentages
davidkallesen May 3, 2026
263469c
feat(insights): add InsightsHealthByCategoryChart stacked-bar
davidkallesen May 3, 2026
2056626
feat(insights): add InsightsAdoptionCharts for TFM, analyzer, languag…
davidkallesen May 3, 2026
95d7b72
feat(insights): add InsightsActionList grouping repos by category wit…
davidkallesen May 3, 2026
c5e68b7
feat(insights): add /support/repository-insights page
davidkallesen May 3, 2026
9249699
feat(insights): add Repository insights to maintenance menu
davidkallesen May 3, 2026
5e5d0de
style(insights): add insights tile styling
davidkallesen May 3, 2026
41b54d8
style(menu): align Maintenance section header with menu items
davidkallesen May 3, 2026
e240f83
feat(compliance): wrap editorconfig R/S/T in chip and skip .NET signa…
davidkallesen May 3, 2026
ef91c6d
chore: remove orphaned compliance UI components from old overview
davidkallesen May 3, 2026
1e36e57
chore: remove CodingRules domain code superseded by compliance-summary
davidkallesen May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/AtcWeb.Domain/AtcApi/AtcApiGitHubRepositoryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class AtcApiGitHubRepositoryClient
private static readonly SemaphoreSlim SemaphorePaths = new(1, 1);
private static readonly SemaphoreSlim SemaphoreFiles = new(1, 1);
private static readonly SemaphoreSlim SemaphoreIssues = new(1, 1);
private static readonly SemaphoreSlim SemaphoreCompliance = new(1, 1);

public AtcApiGitHubRepositoryClient(
HttpClient httpClient,
Expand Down Expand Up @@ -99,6 +100,55 @@ public AtcApiGitHubRepositoryClient(
: (IsSuccessful: true, gitHubRepository: repository);
}

public async Task<(bool IsSuccessful, List<RepositoryComplianceSummary> Summaries)> GetComplianceSummary(
CancellationToken cancellationToken = default)
{
const string cacheKey = CacheConstants.CacheKeyComplianceSummary;
if (memoryCache.TryGetValue(cacheKey, out List<RepositoryComplianceSummary> data))
{
return (IsSuccessful: true, data!);
}

var browserCached = await browserCache.GetAsync<List<RepositoryComplianceSummary>>(cacheKey);
if (browserCached is not null)
{
memoryCache.Set(cacheKey, browserCached, CacheConstants.AbsoluteExpirationRelativeToNow);
return (IsSuccessful: true, browserCached);
}

await SemaphoreCompliance.WaitAsync(cancellationToken);

try
{
const string url = $"{BaseAddress}/compliance-summary";

var responseMessage = await httpClient.GetAsync(url, cancellationToken);
if (!responseMessage.IsSuccessStatusCode)
{
return (IsSuccessful: false, []);
}

var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
var result = JsonSerializer.Deserialize<List<RepositoryComplianceSummary>>(content, JsonSerializerOptionsFactory.Create());
if (result is null)
{
return (IsSuccessful: false, []);
}

memoryCache.Set(cacheKey, result, CacheConstants.AbsoluteExpirationRelativeToNow);
await browserCache.SetAsync(cacheKey, result);
return (IsSuccessful: true, result);
}
catch
{
return (IsSuccessful: false, []);
}
finally
{
SemaphoreCompliance.Release();
}
}

public async Task<(bool IsSuccessful, List<DotnetNugetPackageMetadataBase> DotnetNugetPackagesMetadata)> GetLatestNugetPackageVersionsUsed(
CancellationToken cancellationToken = default)
{
Expand Down
10 changes: 10 additions & 0 deletions src/AtcWeb.Domain/AtcApi/Models/Compliance/AnalyzerPackageRef.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class AnalyzerPackageRef
{
public string PackageId { get; init; } = string.Empty;

public string Version { get; init; } = string.Empty;

public bool IsLatest { get; init; }
}
22 changes: 22 additions & 0 deletions src/AtcWeb.Domain/AtcApi/Models/Compliance/EditorConfigStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class EditorConfigStatus
{
public bool RootPresent { get; set; }

public bool RootIsLatest { get; set; }

public string? RootVersion { get; set; }

public bool SrcPresent { get; set; }

public bool SrcIsLatest { get; set; }

public string? SrcVersion { get; set; }

public bool TestPresent { get; set; }

public bool TestIsLatest { get; set; }

public string? TestVersion { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class RepositoryComplianceDetail
{
public List<string> SrcFrameworks { get; init; } = [];

public List<string> TestFrameworks { get; init; } = [];

public List<string> SampleFrameworks { get; init; } = [];

public List<AnalyzerPackageRef> AnalyzerPackages { get; init; } = [];

public List<string> SuppressedRulesRoot { get; init; } = [];

public List<string> SuppressedRulesSrc { get; init; } = [];

public List<string> SuppressedRulesTest { get; init; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class RepositoryComplianceSignals
{
public bool HasGoodReadme { get; set; }

public bool LicenseIsMit { get; set; }

public bool HomepageIsAtcWeb { get; set; }

public EditorConfigStatus EditorConfigStatus { get; init; } = new();

public bool UpdaterPresent { get; set; }

public bool UpdaterTargetIsLatest { get; set; }

public string? UpdaterProjectTarget { get; set; }

public bool GlobalLangVersionIsLatest { get; set; }

public string? GlobalLangVersion { get; set; }

public bool GlobalTargetFrameworkIsLatest { get; set; }

public string? GlobalTargetFramework { get; set; }

public XunitV3Status XunitV3Status { get; set; } = XunitV3Status.NotApplicable;

public WorkflowsStatus WorkflowsStatus { get; init; } = new();

public bool ReleasePleasePresent { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class RepositoryComplianceSummary
{
public string Name { get; init; } = string.Empty;

public string? Language { get; init; }

public string? Description { get; init; }

public string? Homepage { get; init; }

public string? LicenseKey { get; init; }

public string? DefaultBranch { get; init; }

public List<string> Topics { get; init; } = [];

public int StargazersCount { get; init; }

public int ForksCount { get; init; }

public int OpenIssuesCount { get; init; }

public DateTimeOffset? PushedAt { get; init; }

public DateTimeOffset UpdatedAt { get; init; }

public DateTimeOffset CreatedAt { get; init; }

public DateTimeOffset? OldestOpenIssueAt { get; init; }

public DateTimeOffset? NewestOpenIssueAt { get; init; }

public RepositoryComplianceSignals Signals { get; init; } = new();

public RepositoryComplianceDetail Detail { get; init; } = new();
}
16 changes: 16 additions & 0 deletions src/AtcWeb.Domain/AtcApi/Models/Compliance/WorkflowsStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public sealed class WorkflowsStatus
{
public List<string> Actions { get; init; } = [];

public List<string> DotnetVersions { get; init; } = [];

public bool CheckoutIsLatest { get; set; }

public bool SetupDotnetIsLatest { get; set; }

public bool HasJavaSetup { get; set; }

public bool DotnetVersionIsLatest { get; set; }
}
8 changes: 8 additions & 0 deletions src/AtcWeb.Domain/AtcApi/Models/Compliance/XunitV3Status.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AtcWeb.Domain.AtcApi.Models.Compliance;

public enum XunitV3Status
{
Yes,
No,
NotApplicable,
}
62 changes: 62 additions & 0 deletions src/AtcWeb.Domain/Compliance/ComplianceFilterEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace AtcWeb.Domain.Compliance;

public static class ComplianceFilterEngine
{
public static IReadOnlyList<RepositoryComplianceSummary> Apply(
IEnumerable<RepositoryComplianceSummary> source,
ComplianceFilterState state)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(state);

var query = source.AsEnumerable();

if (!string.IsNullOrWhiteSpace(state.SearchText))
{
var needle = state.SearchText.Trim();
query = query.Where(s =>
s.Name.Contains(needle, StringComparison.OrdinalIgnoreCase) ||
(s.Description ?? string.Empty).Contains(needle, StringComparison.OrdinalIgnoreCase));
}

if (!string.IsNullOrWhiteSpace(state.Language))
{
query = query.Where(s => string.Equals(s.Language, state.Language, StringComparison.Ordinal));
}

if (state.Health is { } targetHealth)
{
query = query.Where(s => ComplianceHealth.Compute(s) == targetHealth);
}

if (state.FailingSignals.Count > 0)
{
query = query.Where(s => state.FailingSignals.All(key => SignalIsFailing(s, key)));
}

return query.OrderBy(s => s.Name, StringComparer.Ordinal).ToList();
}

private static bool SignalIsFailing(
RepositoryComplianceSummary s,
string key)
=> key switch
{
"ReadmeMissing" => !s.Signals.HasGoodReadme,
"LicenseWrong" => !s.Signals.LicenseIsMit,
"HomepageWrong" => !s.Signals.HomepageIsAtcWeb,
"EditorConfigBehind" => !s.Signals.EditorConfigStatus.RootIsLatest
|| !s.Signals.EditorConfigStatus.SrcIsLatest
|| !s.Signals.EditorConfigStatus.TestIsLatest,
"UpdaterBehind" => !s.Signals.UpdaterPresent || !s.Signals.UpdaterTargetIsLatest,
"LangVersionBehind" => !s.Signals.GlobalLangVersionIsLatest,
"TfmBehind" => !s.Signals.GlobalTargetFrameworkIsLatest,
"XunitNotV3" => s.Signals.XunitV3Status == XunitV3Status.No,
"WorkflowsBehind" => !s.Signals.WorkflowsStatus.CheckoutIsLatest
|| !s.Signals.WorkflowsStatus.SetupDotnetIsLatest
|| !s.Signals.WorkflowsStatus.DotnetVersionIsLatest
|| s.Signals.WorkflowsStatus.HasJavaSetup,
"NoReleasePlease" => !s.Signals.ReleasePleasePresent,
_ => false,
};
}
14 changes: 14 additions & 0 deletions src/AtcWeb.Domain/Compliance/ComplianceFilterState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AtcWeb.Domain.Compliance;

public sealed class ComplianceFilterState
{
public string? SearchText { get; set; }

public string? Language { get; set; }

public string? Category { get; set; }

public HealthStatus? Health { get; set; }

public List<string> FailingSignals { get; set; } = [];
}
83 changes: 83 additions & 0 deletions src/AtcWeb.Domain/Compliance/ComplianceHealth.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace AtcWeb.Domain.Compliance;

public enum HealthStatus
{
Ok,
Warning,
Error,
}

public static class ComplianceHealth
{
public static HealthStatus Compute(RepositoryComplianceSummary summary)

Check failure on line 12 in src/AtcWeb.Domain/Compliance/ComplianceHealth.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=atc-net_atc-net.github.io&issues=AZ3uODvpWu1Ao8WwM1yo&open=AZ3uODvpWu1Ao8WwM1yo&pullRequest=33
{
ArgumentNullException.ThrowIfNull(summary);

var s = summary.Signals;
var isDotnet = string.Equals(summary.Language, "C#", StringComparison.Ordinal);

if (!s.LicenseIsMit)
{
return HealthStatus.Error;
}

if (s.WorkflowsStatus.HasJavaSetup)
{
return HealthStatus.Error;
}

if (isDotnet && !s.GlobalTargetFrameworkIsLatest && !string.IsNullOrEmpty(s.GlobalTargetFramework))
{
return HealthStatus.Error;
}

if (!s.HasGoodReadme)
{
return HealthStatus.Warning;
}

if (!s.HomepageIsAtcWeb)
{
return HealthStatus.Warning;
}

if (isDotnet)
{
if (!s.UpdaterPresent || !s.UpdaterTargetIsLatest)
{
return HealthStatus.Warning;
}

if (!s.EditorConfigStatus.RootIsLatest ||
!s.EditorConfigStatus.SrcIsLatest ||
!s.EditorConfigStatus.TestIsLatest)
{
return HealthStatus.Warning;
}

if (!s.GlobalLangVersionIsLatest)
{
return HealthStatus.Warning;
}

if (s.XunitV3Status == XunitV3Status.No)
{
return HealthStatus.Warning;
}

if (!s.WorkflowsStatus.CheckoutIsLatest ||
!s.WorkflowsStatus.SetupDotnetIsLatest ||
!s.WorkflowsStatus.DotnetVersionIsLatest)
{
return HealthStatus.Warning;
}

if (!s.ReleasePleasePresent)
{
return HealthStatus.Warning;
}
}

return HealthStatus.Ok;
}
}
2 changes: 2 additions & 0 deletions src/AtcWeb.Domain/GitHub/CacheConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public static class CacheConstants

public const string CacheKeyNugetPackageVersions = "NugetPackageVersions";

public const string CacheKeyComplianceSummary = "ComplianceSummary";

public static readonly TimeSpan SlidingExpiration = TimeSpan.FromHours(6);

public static readonly TimeSpan AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
Expand Down
Loading
Loading