Skip to content

Commit 07c3fcc

Browse files
committed
Add auto-fixes for README-04/HTTP-01, relax Fix button guard, reassess spinners, exclude info from AI prompt, show org name as tree root
1 parent 4a0d970 commit 07c3fcc

14 files changed

Lines changed: 405 additions & 59 deletions

File tree

PanoramicData.NugetManagement.Web/Components/Pages/Home.razor

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ else
7474
IconCssClass="(item, _) => item.IconCss"
7575
ToolTip="GetNavItemTooltip"
7676
AllowSelection="true"
77-
ShowRoot="false"
77+
ShowRoot="true"
7878
SelectionChange="OnNavSelectionChanged" />
7979
</div>
8080
</div>
@@ -186,9 +186,9 @@ else
186186
Text="Re-assess"
187187
IconCssClass="fas fa-sync-alt"
188188
CssClass="@GetStepCssClass(WorkflowStep.Reassess)"
189-
IsVisible="@(_currentView is NavView.PackageDetail or NavView.CategoryDetail or NavView.RuleDetail && _selectedRow is not null)"
189+
IsVisible="@(_currentView is NavView.Dashboard or NavView.PackageDetail or NavView.CategoryDetail or NavView.RuleDetail)"
190190
IsEnabled="@(!_isAssessing)"
191-
ToolTip="Re-assess this package"
191+
ToolTip="@(_currentView == NavView.Dashboard ? "Re-assess all repos" : "Re-assess this package")"
192192
Click="OnToolbarClick" />
193193

194194
<PDToolbarButton Key="fix"
@@ -397,6 +397,7 @@ else
397397
private PDFileModal? _fileModal;
398398
private string? _localReposRoot;
399399
private bool _folderSaved;
400+
private bool _includeInfoInAiPrompt;
400401

401402
// === Lifecycle ===
402403
@@ -414,6 +415,7 @@ else
414415
_categories = Enum.GetValues<AssessmentCategory>();
415416
_localReposRoot = RuntimeSettings.LocalReposRoot;
416417
_preferredIdeId = RuntimeSettings.PreferredIdeId;
418+
_includeInfoInAiPrompt = RuntimeSettings.IncludeInfoInAiPrompt;
417419

418420
// Load from cache immediately
419421
var cached = Cache.GetCachedRows();
@@ -624,8 +626,16 @@ else
624626
await FixWithAiAsync();
625627
break;
626628
case "reassess":
627-
await ReassessAllAsync();
628-
break;
629+
if (_currentView == NavView.Dashboard)
630+
{
631+
await ReassessAllReposAsync();
632+
}
633+
else
634+
{
635+
await ReassessAllAsync();
636+
}
637+
638+
break;
629639
case "build":
630640
await BuildAsync();
631641
break;
@@ -1059,12 +1069,27 @@ else
10591069
</select>
10601070
}
10611071
</div>
1072+
<div class="form-group">
1073+
<label>Include info items in AI prompt</label>
1074+
<div style="display: flex; align-items: center; gap: 0.5rem;">
1075+
<input type="checkbox" checked="@_includeInfoInAiPrompt" @onchange="OnIncludeInfoInAiPromptChanged" />
1076+
<span style="color: var(--text-secondary); font-size: 0.85rem;">
1077+
When unchecked, informational (blue) items are excluded from the generated AI prompt.
1078+
</span>
1079+
</div>
1080+
</div>
10621081
</div>
10631082
</div>
10641083
};
10651084

10661085
// === Dashboard methods ===
10671086
1087+
private void OnIncludeInfoInAiPromptChanged(ChangeEventArgs e)
1088+
{
1089+
_includeInfoInAiPrompt = e.Value is true;
1090+
RuntimeSettings.SetIncludeInfoInAiPrompt(_includeInfoInAiPrompt);
1091+
}
1092+
10681093
private void OnAutoRefreshTimer(object? state)
10691094
{
10701095
_ = InvokeAsync(async () =>
@@ -1230,11 +1255,28 @@ else
12301255
EnsureSelectedRowIsCurrent();
12311256

12321257
_isAssessing = true;
1233-
_progressMessage = $"Assessing {_selectedRow.PackageId}...";
1258+
_progressMessage = $"Checking NuGet listing for {_selectedRow.PackageId}...";
12341259
StateHasChanged();
12351260

12361261
try
12371262
{
1263+
// Check whether the package is still listed on NuGet
1264+
var isListed = await Dashboard.IsPackageListedAsync(_selectedRow.PackageId);
1265+
if (!isListed)
1266+
{
1267+
AppendConsole($"⚠️ {_selectedRow.PackageId} is no longer listed on NuGet — removing from cache.");
1268+
_rows.RemoveAll(r => string.Equals(r.PackageId, _selectedRow.PackageId, StringComparison.OrdinalIgnoreCase));
1269+
Cache.RemoveRow(_selectedRow.PackageId);
1270+
_selectedRow = null;
1271+
_selectedPackageId = null;
1272+
_currentView = NavView.Dashboard;
1273+
await RefreshTableAsync();
1274+
return;
1275+
}
1276+
1277+
_progressMessage = $"Assessing {_selectedRow.PackageId}...";
1278+
StateHasChanged();
1279+
12381280
if (_selectedRow.IsClonedLocally && _selectedRow.LocalPath is not null)
12391281
{
12401282
await Dashboard.AssessLocalRepositoryAsync(_selectedRow);
@@ -1261,6 +1303,72 @@ else
12611303
}
12621304
}
12631305

1306+
private async Task ReassessAllReposAsync()
1307+
{
1308+
_isAssessing = true;
1309+
var assessable = _rows.Where(r => r.RepositoryFullName is not null).ToList();
1310+
AppendConsole($"▶ Re-assessing {assessable.Count} repos...");
1311+
1312+
// Mark all pending rows so the tree shows spinners
1313+
foreach (var r in assessable)
1314+
{
1315+
r.IsReassessing = true;
1316+
}
1317+
1318+
await RefreshTableAsync();
1319+
1320+
try
1321+
{
1322+
var github = CreateGitHubClient();
1323+
1324+
for (var i = 0; i < assessable.Count; i++)
1325+
{
1326+
var row = assessable[i];
1327+
_progressMessage = $"Assessing ({i + 1}/{assessable.Count}): {row.PackageId}...";
1328+
StateHasChanged();
1329+
1330+
try
1331+
{
1332+
if (row.IsClonedLocally && row.LocalPath is not null)
1333+
{
1334+
await Dashboard.AssessLocalRepositoryAsync(row);
1335+
}
1336+
else
1337+
{
1338+
await Dashboard.AssessRepositoryAsync(row, github);
1339+
}
1340+
}
1341+
catch (Exception ex)
1342+
{
1343+
AppendConsole($"❌ {row.PackageId}: {ex.Message}");
1344+
}
1345+
1346+
row.IsReassessing = false;
1347+
await RefreshTableAsync();
1348+
}
1349+
1350+
Cache.Update(_rows);
1351+
_lastRefreshUtc = Cache.LastRefreshUtc;
1352+
AppendConsole($"✅ Re-assessed {assessable.Count} repos.");
1353+
}
1354+
catch (Exception ex)
1355+
{
1356+
AppendConsole($"❌ Re-assess all failed: {ex.Message}");
1357+
}
1358+
finally
1359+
{
1360+
// Ensure all rows are cleared in case of early exit
1361+
foreach (var r in assessable)
1362+
{
1363+
r.IsReassessing = false;
1364+
}
1365+
1366+
_isAssessing = false;
1367+
_progressMessage = string.Empty;
1368+
StateHasChanged();
1369+
}
1370+
}
1371+
12641372
private async Task ReassessCategoryAsync(AssessmentCategory category)
12651373
{
12661374
if (_selectedRow is null)
@@ -1617,7 +1725,7 @@ else
16171725
return;
16181726
}
16191727

1620-
var prompt = DashboardService.GenerateRemediationPrompt(_selectedRow);
1728+
var prompt = DashboardService.GenerateRemediationPrompt(_selectedRow, _includeInfoInAiPrompt);
16211729
if (string.IsNullOrEmpty(prompt))
16221730
{
16231731
AppendConsole("ℹ️ No failing rules to resolve.");
@@ -1641,7 +1749,7 @@ else
16411749
return;
16421750
}
16431751

1644-
var prompt = DashboardService.GenerateCategoryRemediationPrompt(_selectedRow, category);
1752+
var prompt = DashboardService.GenerateCategoryRemediationPrompt(_selectedRow, category, _includeInfoInAiPrompt);
16451753
if (string.IsNullOrEmpty(prompt))
16461754
{
16471755
AppendConsole($"ℹ️ No failing rules in {category}.");
@@ -1672,9 +1780,8 @@ else
16721780
return false;
16731781
}
16741782

1675-
return _selectedRow.IsWorkingTreeClean == true
1676-
&& _selectedRow.CurrentBranch == "main"
1677-
&& _selectedRow.IsSyncedWithOrigin == true;
1783+
return _selectedRow.CurrentBranch == "main"
1784+
&& _selectedRow.IsSyncedWithOrigin != false;
16781785
}
16791786

16801787
private string? GetFixBlockReason()
@@ -1684,16 +1791,6 @@ else
16841791
return "Repository is not cloned locally";
16851792
}
16861793

1687-
if (_selectedRow.IsWorkingTreeClean == false)
1688-
{
1689-
return "Working tree has uncommitted changes — commit or stash first";
1690-
}
1691-
1692-
if (_selectedRow.IsWorkingTreeClean is null)
1693-
{
1694-
return "Working tree status is unknown — re-assess or sync first";
1695-
}
1696-
16971794
if (_selectedRow.CurrentBranch != "main")
16981795
{
16991796
return $"Must be on 'main' branch (currently on '{_selectedRow.CurrentBranch ?? "unknown"}')";
@@ -1704,11 +1801,6 @@ else
17041801
return "Local branch is not in sync with origin — run Git Sync first";
17051802
}
17061803

1707-
if (_selectedRow.IsSyncedWithOrigin is null)
1708-
{
1709-
return "Sync status with origin is unknown — run Git Sync first";
1710-
}
1711-
17121804
return null;
17131805
}
17141806

@@ -1918,24 +2010,39 @@ else
19182010
return $"{row.PackageId} — all rules pass ✓ (green = compliant)";
19192011
}
19202012

1921-
return row.TotalErrors > 0
1922-
? $"{row.PackageId} — {row.TotalErrors} error(s), {row.TotalWarnings} warning(s) (red = errors found)"
1923-
: $"{row.PackageId} — {row.TotalWarnings} warning(s) (amber = warnings only)";
2013+
if (row.TotalErrors > 0)
2014+
{
2015+
return $"{row.PackageId} — {row.TotalErrors} error(s), {row.TotalWarnings} warning(s) (red = errors found)";
2016+
}
2017+
2018+
return row.TotalWarnings > 0
2019+
? $"{row.PackageId} — {row.TotalWarnings} warning(s) (amber = warnings only)"
2020+
: $"{row.PackageId} — informational only (blue = info)";
19242021

19252022
case NavView.CategoryDetail:
19262023
if (item.IssueCount == 0)
19272024
{
19282025
return $"{item.Text} — all rules pass";
19292026
}
19302027

1931-
return item.HasErrors
1932-
? $"{item.Text} — {item.IssueCount} issue(s) including errors"
1933-
: $"{item.Text} — {item.IssueCount} warning(s)";
2028+
if (item.HasErrors)
2029+
{
2030+
return $"{item.Text} — {item.IssueCount} issue(s) including errors";
2031+
}
2032+
2033+
return item.HasWarnings
2034+
? $"{item.Text} — {item.IssueCount} warning(s)"
2035+
: $"{item.Text} — {item.IssueCount} informational";
19342036

19352037
case NavView.RuleDetail:
1936-
return item.HasErrors
1937-
? $"{item.Text} — error"
1938-
: $"{item.Text} — warning";
2038+
if (item.HasErrors)
2039+
{
2040+
return $"{item.Text} — error";
2041+
}
2042+
2043+
return item.HasWarnings
2044+
? $"{item.Text} — warning"
2045+
: $"{item.Text} — informational";
19392046

19402047
default:
19412048
return item.Text;
@@ -1954,9 +2061,14 @@ else
19542061
return "fas fa-cube text-success";
19552062
}
19562063

1957-
return row.TotalErrors > 0
1958-
? "fas fa-cube text-danger"
1959-
: "fas fa-cube text-warning";
2064+
if (row.TotalErrors > 0)
2065+
{
2066+
return "fas fa-cube text-danger";
2067+
}
2068+
2069+
return row.TotalWarnings > 0
2070+
? "fas fa-cube text-warning"
2071+
: "fas fa-cube text-info";
19602072
}
19612073

19622074
private bool HasAutoRemediableFailures(AssessmentCategory category)

PanoramicData.NugetManagement.Web/Models/NavItem.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public class NavItem
6262
/// Whether this subtree has any errors (not just warnings).
6363
/// </summary>
6464
public bool HasErrors { get; init; }
65+
66+
/// <summary>
67+
/// Whether this subtree has any warnings (not just info).
68+
/// </summary>
69+
public bool HasWarnings { get; init; }
6570
}
6671

6772
/// <summary>

PanoramicData.NugetManagement.Web/Models/PackageDashboardRow.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public class PackageDashboardRow
6363
/// </summary>
6464
public RepoAssessment? Assessment { get; set; }
6565

66+
/// <summary>
67+
/// Whether this row is currently being reassessed.
68+
/// Used to show a spinner in the tree while awaiting reassessment.
69+
/// </summary>
70+
public bool IsReassessing { get; set; }
71+
6672
/// <summary>
6773
/// Issue counts grouped by category.
6874
/// </summary>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace PanoramicData.NugetManagement.Web.Remediations.HttpClient;
2+
3+
/// <summary>Adds the expected HTTP client package to Directory.Packages.props.</summary>
4+
public sealed class ExpectedHttpClientPackageRemediation : DataDrivenRemediation
5+
{
6+
/// <inheritdoc />
7+
public override string RuleId => "HTTP-01";
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace PanoramicData.NugetManagement.Web.Remediations.ReadmeBadges;
2+
3+
/// <summary>Prepends license badge to README.md.</summary>
4+
public sealed class LicenseBadgeRemediation : DataDrivenRemediation
5+
{
6+
/// <inheritdoc />
7+
public override string RuleId => "README-04";
8+
}

PanoramicData.NugetManagement.Web/Services/DashboardCacheService.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,38 @@ public void NotifyRowUpdated()
134134
SaveToDisk();
135135
}
136136

137+
/// <summary>
138+
/// Removes a row by package ID from the cache and persists to disk.
139+
/// Returns true if the row was found and removed.
140+
/// </summary>
141+
public bool RemoveRow(string packageId)
142+
{
143+
bool removed;
144+
lock (_lock)
145+
{
146+
if (_cachedRows is null)
147+
{
148+
return false;
149+
}
150+
151+
removed = _cachedRows.RemoveAll(r =>
152+
string.Equals(r.PackageId, packageId, StringComparison.OrdinalIgnoreCase)) > 0;
153+
154+
if (removed)
155+
{
156+
_lastRefreshUtc = DateTimeOffset.UtcNow;
157+
}
158+
}
159+
160+
if (removed)
161+
{
162+
_logger.LogInformation("Removed de-listed package '{PackageId}' from cache", packageId);
163+
SaveToDisk();
164+
}
165+
166+
return removed;
167+
}
168+
137169
private void SaveToDisk()
138170
{
139171
try

0 commit comments

Comments
 (0)