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
@@ -20,7 +20,7 @@
<a tabindex="0" class="help-icon ms-1" role="button"
data-bs-toggle="popover" data-bs-placement="right"
data-bs-title="Quote Statuses"
data-bs-content="&lt;strong&gt;Draft&lt;/strong&gt; saved but not yet sent. Edit freely.&lt;br&gt;&lt;strong&gt;Sent&lt;/strong&gt; delivered to the customer, awaiting response.&lt;br&gt;&lt;strong&gt;Approved&lt;/strong&gt; customer accepted. You can convert this to a Job.&lt;br&gt;&lt;strong&gt;Rejected&lt;/strong&gt; customer declined.&lt;br&gt;&lt;strong&gt;Expired&lt;/strong&gt; validity period has passed. Edit to extend it.&lt;br&gt;&lt;strong&gt;Converted&lt;/strong&gt; a job has been created from this quote.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;">
data-bs-content="&lt;strong&gt;Draft&lt;/strong&gt; &mdash; saved but not yet sent. Edit freely.&lt;br&gt;&lt;strong&gt;Sent&lt;/strong&gt; &mdash; delivered to the customer, awaiting response.&lt;br&gt;&lt;strong&gt;Approved&lt;/strong&gt; &mdash; customer accepted. You can convert this to a Job.&lt;br&gt;&lt;strong&gt;Rejected&lt;/strong&gt; &mdash; customer declined.&lt;br&gt;&lt;strong&gt;Expired&lt;/strong&gt; &mdash; validity period has passed. Edit to extend it.&lt;br&gt;&lt;strong&gt;Converted&lt;/strong&gt; &mdash; a job has been created from this quote.&lt;br&gt;&lt;br&gt;&lt;a href='/Help/Quotes#quote-statuses' target='_blank'&gt;Learn more →&lt;/a&gt;">
<i class="bi bi-question-circle"></i>
</a>
</p>
@@ -334,7 +334,7 @@
}
else
{
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder must be purchased before coating">
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder &mdash; must be purchased before coating">
<i class="bi bi-cart me-1"></i>ORDER: @coat.PowderToOrder?.ToString("0.##") lbs
</span>
}
@@ -438,7 +438,7 @@
}
else
{
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder must be purchased before coating">
<span class="badge bg-warning text-dark" style="font-size: 0.7em;" title="Custom powder &mdash; must be purchased before coating">
<i class="bi bi-cart me-1"></i>ORDER: @coat.PowderToOrder?.ToString("0.##") lbs
</span>
}
@@ -463,7 +463,7 @@
<br />
<small class="ms-3">
• <strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong>
<span class="text-muted"> @ps.EstimatedMinutes min</span>
<span class="text-muted">&mdash; @ps.EstimatedMinutes min</span>
</small>
}
}
@@ -665,7 +665,7 @@
{
<div class="mb-1">
<strong>@(ps.PrepServiceName ?? $"Service #{ps.PrepServiceId}")</strong>
<span class="text-muted"> @ps.EstimatedMinutes min</span>
<span class="text-muted">&mdash; @ps.EstimatedMinutes min</span>
</div>
}
</span>
@@ -753,7 +753,7 @@
</div>
<div id="photoUploadProgress" class="d-none mt-2">
<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" style="width:100%"></div></div>
<small class="text-muted">Uploading</small>
<small class="text-muted">Uploading&hellip;</small>
</div>
</div>
</div>
@@ -1110,7 +1110,7 @@
var allCatalog = Model.QuoteItems != null && Model.QuoteItems.All(i => i.CatalogItemId.HasValue);
}
@* ── SECTION 1: Item Cost Breakdown ─────────────────────── *@
@* -- SECTION 1: Item Cost Breakdown ----------------------- *@
<div class="mb-3">
<div class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.05em;">
<i class="bi bi-boxes me-1"></i>Item Costs
@@ -1140,7 +1140,7 @@
}
else if (allCatalog)
{
<div class="text-muted small fst-italic">All items use fixed catalog pricing no per-category cost split available.</div>
<div class="text-muted small fst-italic">All items use fixed catalog pricing &mdash; no per-category cost split available.</div>
}
else
{
@@ -1152,7 +1152,7 @@
</div>
</div>
@* ── SECTION 2: Quote-Level Additions ───────────────────── *@
@* -- SECTION 2: Quote-Level Additions --------------------- *@
@if (pb.OvenBatchCost > 0 || pb.FacilityOverheadCost > 0 || pb.ShopSuppliesAmount > 0 || pb.OverheadCosts > 0)
{
<div class="mb-3">
@@ -1190,7 +1190,7 @@
</div>
}
@* ── SECTION 3: Final Calculation ────────────────────────── *@
@* -- SECTION 3: Final Calculation -------------------------- *@
<div class="mb-2">
<div class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.05em;">
<i class="bi bi-receipt me-1"></i>Final Calculation
@@ -1245,13 +1245,13 @@
var priceBeforeDiscount = pb.Total + pb.DiscountAmount;
var marginBeforeDiscount = priceBeforeDiscount > 0 ? ((priceBeforeDiscount - totalDirectCost) / priceBeforeDiscount * 100m) : 0m;
<div class="small text-muted">
Without discount: @marginBeforeDiscount.ToString("F1")% discount cost you @((marginBeforeDiscount - effectiveMargin).ToString("F1")) margin points
Without discount: @marginBeforeDiscount.ToString("F1")% &mdash; discount cost you @((marginBeforeDiscount - effectiveMargin).ToString("F1")) margin points
</div>
}
}
</div>
@* ── SECTION 4: Per-Item Cost Breakdown ─────────────────── *@
@* -- SECTION 4: Per-Item Cost Breakdown ------------------- *@
@{
var hasItemCostData = Model.QuoteItems != null && Model.QuoteItems.Any(i =>
i.ItemMaterialCost > 0 || i.ItemLaborCost > 0 || i.ItemEquipmentCost > 0);
@@ -1273,13 +1273,13 @@
var tier = ViewBag.PricingTier as PowderCoating.Core.Entities.PricingTier;
<div class="alert alert-success alert-permanent py-2 px-3 mb-3 small">
<i class="bi bi-tag me-1"></i>
<strong>Pricing Tier:</strong> @tier.TierName @tier.DiscountPercent% discount applied to this customer
<strong>Pricing Tier:</strong> @tier.TierName &mdash; @tier.DiscountPercent% discount applied to this customer
</div>
}
@if (!hasItemCostData)
{
<div class="text-muted small fst-italic mb-3">Per-item cost detail not available re-save this quote to capture the breakdown.</div>
<div class="text-muted small fst-italic mb-3">Per-item cost detail not available &mdash; re-save this quote to capture the breakdown.</div>
}
@* Per-item breakdown cards *@
@@ -1427,7 +1427,7 @@
<div class="d-flex justify-content-between text-muted ps-2 border-start border-2 mb-1">
<div>
@coat.CoatName
@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> @coat.ColorName</text> }
@if (!string.IsNullOrEmpty(coat.ColorName)) { <text> &mdash; @coat.ColorName</text> }
@if (!string.IsNullOrEmpty(coat.ColorCode)) { <text> (@coat.ColorCode)</text> }
@if (coat.InventoryItemId.HasValue)
{
@@ -1467,11 +1467,11 @@
}
else if (item.CatalogItemId.HasValue && !item.Coats.Any())
{
<div class="px-3 py-2 text-muted fst-italic">Catalog fixed price no cost split available.</div>
<div class="px-3 py-2 text-muted fst-italic">Catalog fixed price &mdash; no cost split available.</div>
}
else if (item.IsAiItem)
{
<div class="px-3 py-2 text-muted fst-italic">AI-estimated price cost breakdown not applicable.</div>
<div class="px-3 py-2 text-muted fst-italic">AI-estimated price &mdash; cost breakdown not applicable.</div>
}
</div>
}
@@ -1480,7 +1480,7 @@
@if (hasItemCostData)
{
<div class="d-flex justify-content-between fw-semibold border-top pt-2 mt-1 small">
<span class="text-muted">All items material / labor / equipment / total</span>
<span class="text-muted">All items &mdash; material / labor / equipment / total</span>
<span>@pb.MaterialCosts.ToString("C") / @pb.LaborCosts.ToString("C") / @pb.EquipmentCosts.ToString("C") / @pb.ItemsSubtotal.ToString("C")</span>
</div>
}
@@ -1506,7 +1506,7 @@
</tr>
<tr>
<td class="ps-0">Markup</td>
<td class="text-end pe-0">@dbgCosts.GeneralMarkupPercentage.ToString("F1")% applied to material costs</td>
<td class="text-end pe-0">@dbgCosts.GeneralMarkupPercentage.ToString("F1")% &mdash; applied to material costs</td>
</tr>
<tr>
<td class="ps-0">Additional coat charge</td>
@@ -1598,7 +1598,7 @@
@if (!detHasMobile && !detHasEmail && !detProspectHasPhone && string.IsNullOrWhiteSpace(Model.ProspectEmail))
{
<div class="alert alert-warning alert-permanent py-1 px-2 small">
<i class="bi bi-exclamation-triangle me-1"></i>No email or mobile number on file update the customer record to send this quote electronically.
<i class="bi bi-exclamation-triangle me-1"></i>No email or mobile number on file &mdash; update the customer record to send this quote electronically.
</div>
}
@if (detHasMobile && !detHasSmsConsent)
@@ -1607,7 +1607,7 @@
}
@if (detProspectHasPhone && !Model.ProspectSmsConsent)
{
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent not recorded edit the quote to enable SMS for this prospect.</div>
<div class="text-muted small"><i class="bi bi-phone-slash me-1"></i>SMS consent not recorded &mdash; edit the quote to enable SMS for this prospect.</div>
}
@if (!Model.ConvertedToJobId.HasValue)
{
@@ -2412,7 +2412,7 @@
<td class="small">${escHtml(n.type.replace(/([A-Z])/g, ' $1').trim())}</td>
<td class="small"><i class="bi ${channelIcon} me-1"></i>${escHtml(n.channel)}</td>
<td class="small">${escHtml(n.recipientName)}<br><span class="text-muted">${escHtml(n.recipient)}</span></td>
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted"></span>'}</td>
<td class="small">${n.subject ? escHtml(n.subject) : '<span class="text-muted">&mdash;</span>'}</td>
<td><span class="badge bg-${statusClass}">${escHtml(n.status)}</span></td>
</tr>${errorRow}`;
}).join('');
@@ -2449,7 +2449,7 @@
}
}
// ── Deposits ─────────────────────────────────────────────────────────
// -- Deposits ---------------------------------------------------------
function getAntiForgeryToken() {
return document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
}
@@ -2466,7 +2466,7 @@
}
if (errEl) errEl.classList.add('d-none');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving'; }
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving&hellip;'; }
const params = new URLSearchParams(new FormData(form));
try {