Sweep all .cshtml files for encoding corruption; add pre-commit guard

Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII

tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun

.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.

Build clean; 225 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 21:37:10 -04:00
parent 21b39161a3
commit a0bdd2b5b4
252 changed files with 1785 additions and 1633 deletions
@@ -1338,7 +1338,7 @@
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header border-0 bg-body py-3">
<h5 class="mb-0 fw-semibold">Profit & Loss Revenue vs Expenses</h5>
<h5 class="mb-0 fw-semibold">Profit & Loss &mdash; Revenue vs Expenses</h5>
</div>
<div class="card-body">
<canvas id="plChart" height="130"></canvas>
@@ -1604,7 +1604,7 @@
<div class="fw-medium">@color.DisplayLabel</div>
<div class="text-muted small">@color.SKU</div>
</td>
<td class="text-muted small">@(color.Manufacturer ?? "")</td>
<td class="text-muted small">@(color.Manufacturer ?? "&mdash;")</td>
<td class="text-end">
<div>@color.TotalLbsUsed.ToString("N1") lbs</div>
<div class="progress mt-1" style="height:4px; min-width:80px;">
@@ -1749,7 +1749,7 @@
@c.BalanceDue.ToString("C")
</td>
<td class="text-end text-muted">@c.AvgInvoiceValue.ToString("C")</td>
<td class="text-muted small">@(c.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "")</td>
<td class="text-muted small">@(c.LastInvoiceDate?.ToString("MMM d, yyyy") ?? "&mdash;")</td>
<td class="pe-3">
<div class="d-flex align-items-center gap-2">
<div class="progress flex-grow-1" style="height:6px;">
@@ -1810,7 +1810,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-warning">@retAtRisk</div>
<div class="small text-muted">At Risk</div>
<div class="small text-muted">3060 days</div>
<div class="small text-muted">30&ndash;60 days</div>
</div>
</div>
</div>
@@ -1819,7 +1819,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-orange" style="color:#f97316;">@retLapsing</div>
<div class="small text-muted">Lapsing</div>
<div class="small text-muted">6090 days</div>
<div class="small text-muted">60&ndash;90 days</div>
</div>
</div>
</div>
@@ -1896,10 +1896,10 @@
<span class="badge @badgeClass">@r.RetentionStatus</span>
</td>
<td class="text-center small">
@(r.LastJobDate.HasValue ? r.LastJobDate.Value.ToString("MMM d, yyyy") : "")
@(r.LastJobDate.HasValue ? r.LastJobDate.Value.ToString("MMM d, yyyy") : "&mdash;")
</td>
<td class="text-center small">
@(r.DaysSinceLastJob >= 0 ? r.DaysSinceLastJob + "d" : "")
@(r.DaysSinceLastJob >= 0 ? r.DaysSinceLastJob + "d" : "&mdash;")
</td>
<td class="text-end">@r.TotalJobs</td>
<td class="text-end pe-3 fw-semibold">@r.LifetimeRevenue.ToString("C0")</td>
@@ -1954,7 +1954,7 @@
}
else
{
<h3 class="mb-0 fw-bold text-muted"></h3>
<h3 class="mb-0 fw-bold text-muted">&mdash;</h3>
}
</div>
</div>
@@ -2103,7 +2103,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
</tr>
@@ -2128,9 +2128,9 @@
<div class="tab-pane fade" id="invoice-aging-detail-tab">
@{
var iadCurrent = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "Current").ToList();
var iad1to30 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "130 Days").ToList();
var iad31to60 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "3160 Days").ToList();
var iad61to90 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "6190 Days").ToList();
var iad1to30 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "1&ndash;30 Days").ToList();
var iad31to60 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "31&ndash;60 Days").ToList();
var iad61to90 = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "61&ndash;90 Days").ToList();
var iad90plus = Model.InvoiceAgingDetail.Where(i => i.AgingBucket == "90+ Days").ToList();
}
<div class="row g-3 mb-4">
@@ -2147,7 +2147,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold text-warning">@iad1to30.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">130 Days</div>
<div class="small text-muted">1&ndash;30 Days</div>
<div class="small text-muted">@iad1to30.Count invoices</div>
</div>
</div>
@@ -2156,7 +2156,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold" style="color:#f97316;">@iad31to60.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">3160 Days</div>
<div class="small text-muted">31&ndash;60 Days</div>
<div class="small text-muted">@iad31to60.Count invoices</div>
</div>
</div>
@@ -2165,7 +2165,7 @@
<div class="card border-0 shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="fs-5 fw-bold text-danger">@iad61to90.Sum(i => i.BalanceDue).ToString("C0")</div>
<div class="small text-muted">6190 Days</div>
<div class="small text-muted">61&ndash;90 Days</div>
<div class="small text-muted">@iad61to90.Count invoices</div>
</div>
</div>
@@ -2217,9 +2217,9 @@
{
var bucketBadge = inv.AgingBucket switch {
"Current" => "bg-success",
"130 Days" => "bg-warning text-dark",
"3160 Days" => "badge-orange",
"6190 Days" => "bg-danger",
"1&ndash;30 Days" => "bg-warning text-dark",
"31&ndash;60 Days" => "badge-orange",
"61&ndash;90 Days" => "bg-danger",
_ => "bg-danger"
};
<tr>
@@ -2235,7 +2235,7 @@
}
</td>
<td class="small">@inv.InvoiceDate.ToString("MMM d, yyyy")</td>
<td class="small">@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MMM d, yyyy") : "")</td>
<td class="small">@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MMM d, yyyy") : "&mdash;")</td>
<td class="text-end">@inv.Total.ToString("C")</td>
<td class="text-end text-success">@inv.AmountPaid.ToString("C")</td>
<td class="text-end fw-semibold @(inv.BalanceDue > 0 ? "text-danger" : "")">@inv.BalanceDue.ToString("C")</td>
@@ -2250,7 +2250,7 @@
}
</td>
<td class="pe-3">
<span class="badge @bucketBadge" style="@(inv.AgingBucket == "3160 Days" ? "background:#f97316;" : "")">@inv.AgingBucket</span>
<span class="badge @bucketBadge" style="@(inv.AgingBucket == "31&ndash;60 Days" ? "background:#f97316;" : "")">@inv.AgingBucket</span>
</td>
</tr>
}
@@ -2312,7 +2312,7 @@
{
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body border-0 pt-3 pb-2">
<h6 class="mb-0 fw-semibold"><i class="bi bi-bar-chart-fill me-2 text-primary"></i>Top 10 Powders Purchased vs Consumed (lbs)</h6>
<h6 class="mb-0 fw-semibold"><i class="bi bi-bar-chart-fill me-2 text-primary"></i>Top 10 Powders &mdash; Purchased vs Consumed (lbs)</h6>
</div>
<div class="card-body">
<canvas id="powderConsumptionChart" height="100"></canvas>
@@ -2349,7 +2349,7 @@
<div class="small text-muted">@p.ColorCode @(!string.IsNullOrEmpty(p.SKU) ? $"· {p.SKU}" : "")</div>
}
</td>
<td class="small text-muted">@(p.Manufacturer ?? "")</td>
<td class="small text-muted">@(p.Manufacturer ?? "&mdash;")</td>
<td class="text-end">@p.TotalPurchasedLbs.ToString("N1")</td>
<td class="text-end">@p.TotalConsumedLbs.ToString("N1")</td>
<td class="text-end">
@@ -2399,7 +2399,7 @@
<div class="card-body py-3">
<div class="fs-2 fw-bold text-warning">@itLow</div>
<div class="small text-muted">Low</div>
<div class="small text-muted">730 days</div>
<div class="small text-muted">7&ndash;30 days</div>
</div>
</div>
</div>
@@ -3197,7 +3197,7 @@
// ── INVOICE AGING BAR ─────────────────────────────────────────────────────
if (document.getElementById('invoiceAgingChart')) {
@{
var agingLabels = new[] { "Current", "130 Days", "3160 Days", "6190 Days", "90+ Days" };
var agingLabels = new[] { "Current", "1&ndash;30 Days", "31&ndash;60 Days", "61&ndash;90 Days", "90+ Days" };
var agingTotals = agingLabels.Select(b =>
Model.InvoiceAgingDetail.Where(i => i.AgingBucket == b).Sum(i => i.BalanceDue)).ToArray();
}