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
@@ -329,7 +329,7 @@
width: 1.5rem;
}
/* ── Nav mode: FOUC prevention CSS wins before JS runs ─────── */
/* ── Nav mode: FOUC prevention &mdash; CSS wins before JS runs ─────── */
html[data-nav-mode="ops"] [data-nav="fin"] { display: none !important; }
html[data-nav-mode="fin"] [data-nav="ops"] { display: none !important; }
@@ -810,12 +810,12 @@
text-align: center;
}
/* Sidebar themes hardcoded solids, NOT token-referenced, so they never flip with surface */
/* Sidebar themes &mdash; hardcoded solids, NOT token-referenced, so they never flip with surface */
:root,
[data-sidebar="ink"],
[data-sidebar="ocean"],
[data-sidebar="slate"],
[data-sidebar="midnight"] { --sidebar-bg: #1A1A1C; } /* graphite default */
[data-sidebar="midnight"] { --sidebar-bg: #1A1A1C; } /* graphite &mdash; default */
[data-sidebar="navy"] { --sidebar-bg: #0D1B2E; } /* deep navy */
[data-sidebar="forest"],
[data-sidebar="emerald"] { --sidebar-bg: #0D2116; } /* deep forest */
@@ -834,12 +834,12 @@
/* Profile photo avatar */
.user-avatar-img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; cursor: pointer; }
/* Theme-aware overrides make Bootstrap light-only utilities respond to data-bs-theme */
/* Theme-aware overrides &mdash; make Bootstrap light-only utilities respond to data-bs-theme */
.bg-white { background-color: var(--bs-body-bg) !important; }
.bg-light { background-color: var(--bs-tertiary-bg) !important; color: var(--bs-body-color) !important; }
.card-footer { background-color: var(--bs-body-bg) !important; border-color: var(--bs-border-color) !important; }
.table-light { --bs-table-bg: var(--bs-tertiary-bg); --bs-table-color: var(--bs-body-color); --bs-table-border-color: var(--bs-border-color); }
/* Table contextual row colors use hardcoded light values in Bootstrap make them theme-aware */
/* Table contextual row colors use hardcoded light values in Bootstrap &mdash; make them theme-aware */
[data-bs-theme="dark"] tr.table-warning > td,
[data-bs-theme="dark"] tr.table-warning > th {
background-color: rgba(255, 193, 7, 0.15) !important;
@@ -896,7 +896,7 @@
[data-bs-theme="dark"] .powder-combo-dropdown { background: var(--bs-body-bg) !important; border-color: var(--bs-border-color) !important; color: var(--bs-body-color) !important; }
[data-bs-theme="dark"] .powder-opt { color: var(--bs-body-color) !important; }
/* Tom Select dark mode
/* Tom Select &mdash; dark mode
The bootstrap5 theme hardcodes color:#343a40 on .ts-dropdown which
cascades to every option row making them near-invisible on dark bg. */
[data-bs-theme="dark"] .ts-control {
@@ -949,7 +949,7 @@
display:flex;align-items:center;justify-content:center;gap:0.5rem;
font-size:0.78rem;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;">
<i class="bi bi-exclamation-triangle-fill"></i>
@_envName Environment Not Production
@_envName Environment &mdash; Not Production
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
}
@@ -962,7 +962,7 @@
{
<div id="tempdata-error-message" style="display:none;">@TempData["Error"]</div>
}
@* Permanent variants displayed with no auto-dismiss timeout *@
@* Permanent variants &mdash; displayed with no auto-dismiss timeout *@
@if (TempData["SuccessPermanent"] != null)
{
<div id="tempdata-success-permanent-message" style="display:none;">@TempData["SuccessPermanent"]</div>
@@ -1041,11 +1041,11 @@
<nav class="sidebar-nav">
@if (User.Identity?.IsAuthenticated == true)
{
@* ⌘K scaffold wire to command palette later *@
@* ⌘K scaffold &mdash; wire to command palette later *@
<div class="sidebar-search-wrap" style="position:relative">
<i class="bi bi-search sidebar-search-icon"></i>
<input type="search" class="sidebar-search-input" id="cmdKInput"
placeholder="Quick search" autocomplete="off"
placeholder="Quick search&hellip;" autocomplete="off"
title="Search (⌘K / Ctrl+K)" />
<span class="sidebar-search-kbd">⌘K</span>
<div class="sidebar-search-results" id="cmdKResults"
@@ -1393,7 +1393,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
{
<div style="background:#ffc107;color:#000;padding:7px 16px;text-align:center;font-weight:500;font-size:.9rem;z-index:9999;position:relative;">
<i class="bi bi-eye-fill me-2"></i>
<strong>Impersonating:</strong> @_impersonatingName &nbsp;&nbsp;
<strong>Impersonating:</strong> @_impersonatingName &nbsp;&mdash;&nbsp;
All data is scoped to this company.
<form method="post" asp-controller="Companies" asp-action="StopImpersonating" class="d-inline ms-2">
@Html.AntiForgeryToken()
@@ -1865,8 +1865,8 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
const isDelete = data.eventType === 'Deleted';
const msg = isDelete
? `<strong>${data.jobNumber}</strong> ${data.detail} <small class="d-block text-muted">by ${data.changedBy}</small>`
: `<a href="${jobLink}" class="text-white fw-bold">${data.jobNumber}</a> ${data.detail} <small class="d-block text-muted">by ${data.changedBy}</small>`;
? `<strong>${data.jobNumber}</strong> &mdash; ${data.detail} <small class="d-block text-muted">by ${data.changedBy}</small>`
: `<a href="${jobLink}" class="text-white fw-bold">${data.jobNumber}</a> &mdash; ${data.detail} <small class="d-block text-muted">by ${data.changedBy}</small>`;
toastr.info(msg, `<i class="bi ${icon} me-1"></i> Job Updated`, {
timeOut: 8000, extendedTimeOut: 2000, closeButton: true, enableHtml: true
@@ -1999,7 +1999,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
viewBtn.href = '#';
viewBtn.classList.add('d-none');
}
// Mark as read immediately decrements the badge but keeps item in the list
// Mark as read immediately &mdash; decrements the badge but keeps item in the list
markRead(n.id, el);
// Close the bell dropdown before opening the modal
const dropdown = bootstrap.Dropdown.getInstance(document.getElementById('notifBellBtn'));
@@ -2086,7 +2086,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('notifDetailModal');
// Dismiss remove from list and close (already marked read when modal opened)
// Dismiss &mdash; remove from list and close (already marked read when modal opened)
document.getElementById('notifDetailDismissBtn')?.addEventListener('click', () => {
if (activeItem) {
activeItem.el.remove();
@@ -2096,7 +2096,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
bootstrap.Modal.getInstance(modal)?.hide();
});
// View remove from list then navigate (already marked read when modal opened)
// View &mdash; remove from list then navigate (already marked read when modal opened)
document.getElementById('notifDetailViewBtn')?.addEventListener('click', () => {
if (activeItem) {
activeItem.el.remove();
@@ -2161,7 +2161,7 @@ var hasReports = _isAdminOrManager || User.HasClaim("Permission", "ViewReports")
<div class="modal-body" id="quickAddModalBody">
<div class="d-flex align-items-center justify-content-center py-5">
<div class="spinner-border text-primary me-3" role="status"></div>
<span class="text-muted">Loading</span>
<span class="text-muted">Loading&hellip;</span>
</div>
</div>
<div class="modal-footer d-none" id="quickAddModalErrors">