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
@@ -64,7 +64,7 @@
<div class="alert alert-warning alert-permanent d-flex gap-3 align-items-start mb-3">
<i class="bi bi-exclamation-triangle-fill fs-4 flex-shrink-0 mt-1"></i>
<div>
<strong>Destructive operation this cannot be undone.</strong>
<strong>Destructive operation &mdash; this cannot be undone.</strong>
Purging permanently deletes records from the database. Soft-deleted records are hidden from users but still occupy database space. Use this tool periodically to reclaim space and keep the database clean.
Job photo blobs in Azure Storage are also deleted when purging job photo records.
</div>
@@ -88,8 +88,8 @@
<th style="width:36px"></th>
<th>Entity</th>
<th class="text-end" style="width:90px">Total</th>
<th class="text-end" style="width:100px">030d</th>
<th class="text-end" style="width:100px">3090d</th>
<th class="text-end" style="width:100px">0&ndash;30d</th>
<th class="text-end" style="width:100px">30&ndash;90d</th>
<th class="text-end" style="width:100px">&gt;90d</th>
<th style="width:130px">Oldest</th>
<th style="width:42px">
@@ -114,7 +114,7 @@
}
else
{
<span class="text-muted"></span>
<span class="text-muted">&mdash;</span>
}
</td>
<td class="text-end">
@@ -122,24 +122,24 @@
{
<span class="badge bg-success-subtle text-success">@s.DeletedLast30Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</span> }
</td>
<td class="text-end">
@if (s.Deleted30To90Days > 0)
{
<span class="badge bg-warning-subtle text-warning">@s.Deleted30To90Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</span> }
</td>
<td class="text-end">
@if (s.DeletedOlderThan90Days > 0)
{
<span class="badge bg-danger-subtle text-danger">@s.DeletedOlderThan90Days</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</span> }
</td>
<td class="text-muted">
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")
@(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "&mdash;")
</td>
<td class="text-center">
<input type="checkbox" class="form-check-input entity-select"
@@ -154,7 +154,7 @@
</table>
</div>
<!-- Mobile card view for this group shown on screens < 992px -->
<!-- Mobile card view for this group &mdash; shown on screens < 992px -->
<div class="mobile-card-view">
<div class="px-3 pt-2 pb-1">
<span class="text-muted text-uppercase fw-semibold" style="font-size:0.7rem;letter-spacing:.05em">@group.Key</span>
@@ -169,7 +169,7 @@
</div>
<div class="mobile-card-title">
<h6>@s.Label</h6>
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "")</small>
<small>Oldest: @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "&mdash;")</small>
</div>
</div>
<div class="mobile-card-body">
@@ -180,11 +180,11 @@
{
<span class="badge bg-secondary">@s.Total</span>
}
else { <span class="text-muted"></span> }
else { <span class="text-muted">&mdash;</span> }
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">030d / 3090d / &gt;90d</span>
<span class="mobile-card-label">0&ndash;30d / 30&ndash;90d / &gt;90d</span>
<span class="mobile-card-value">@s.DeletedLast30Days / @s.Deleted30To90Days / @s.DeletedOlderThan90Days</span>
</div>
</div>
@@ -349,7 +349,7 @@
}
previewBtn.disabled = true;
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading';
previewBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading&hellip;';
const days = document.getElementById('olderThanDays').value;
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;