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>
data-bs-content="This information appears on every customer-facing document — quotes, invoices, and PDFs. Keep the company name, address, and email accurate so customers see the right details. The <strong>Primary Contact Email</strong> is used as the reply-to address on all outgoing notifications.<br><br><a href='/Help/Settings#company-information' target='_blank'>Learn more →</a>">
data-bs-content="This information appears on every customer-facing document — quotes, invoices, and PDFs. Keep the company name, address, and email accurate so customers see the right details. The <strong>Primary Contact Email</strong> is used as the reply-to address on all outgoing notifications.<br><br><a href='/Help/Settings#company-information' target='_blank'>Learn more →</a>">
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. <strong>New quotes use the current rates</strong> — changing a rate here does not retroactively reprice existing quotes.<br><br><a href='/Help/Settings#pricing-configuration' target='_blank'>Learn more →</a>">
data-bs-content="These are the rates the quoting engine uses to price every job automatically. Set them to your real shop costs and the system will produce accurate quotes without manual calculation. <strong>New quotes use the current rates</strong> — changing a rate here does not retroactively reprice existing quotes.<br><br><a href='/Help/Settings#pricing-configuration' target='_blank'>Learn more →</a>">
data-bs-content="<strong>Standard Labor Rate</strong> is the baseline $/hr for all coating work — sandblasting and masking are multiplied from this. <strong>Powder Coating Cost/sq ft</strong> is the fallback material rate used when you don't select a specific powder inventory item on a quote item. <strong>Additional Coat Labor</strong> is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
data-bs-content="<strong>Standard Labor Rate</strong> is the baseline $/hr for all coating work — sandblasting and masking are multiplied from this. <strong>Powder Coating Cost/sq ft</strong> is the fallback material rate used when you don't select a specific powder inventory item on a quote item. <strong>Additional Coat Labor</strong> is the percentage of the base labor cost charged for each coat after the first (e.g. 30% means a 2nd coat adds 30% more labor).">
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The <strong>Default Oven Rate</strong> is used on quotes where no named oven is chosen — add individual shop ovens below if you have multiple ovens with different capacities and costs.">
data-bs-content="The hourly cost of running each piece of equipment, including energy and depreciation. These are added to quote items based on the prep services selected. The <strong>Default Oven Rate</strong> is used on quotes where no named oven is chosen — add individual shop ovens below if you have multiple ovens with different capacities and costs.">
data-bs-content="<strong>Markup mode</strong> adds a % on top of material costs only (labor and equipment pass through at cost). <strong>Margin mode</strong> targets a gross margin % of the total selling price — e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. <strong>Shop Minimum</strong> sets a floor price for any job.">
data-bs-content="<strong>Markup mode</strong> adds a % on top of material costs only (labor and equipment pass through at cost). <strong>Margin mode</strong> targets a gross margin % of the total selling price — e.g. 30% margin on a $100 cost base gives a $142.86 price. Note: margin % and markup % are not the same number. <strong>Shop Minimum</strong> sets a floor price for any job.">
data-bs-content="A percentage added to the price of <strong>calculated items</strong> based on how intricate the part is. When adding an item in a quote, staff select a complexity level — the system then applies this multiplier to account for the extra time and care needed. <em>Simple</em> = 0% (flat panels, basic shapes). <em>Extreme</em> = highly detailed, tight recesses, masking-intensive parts.">
data-bs-content="A percentage added to the price of <strong>calculated items</strong> based on how intricate the part is. When adding an item in a quote, staff select a complexity level — the system then applies this multiplier to account for the extra time and care needed. <em>Simple</em> = 0% (flat panels, basic shapes). <em>Extreme</em> = highly detailed, tight recesses, masking-intensive parts.">
data-bs-content="Describe your shop's specialties, typical items, and pricing style in plain language. This text is injected into the AI's system prompt every time a photo quote is analysed — the more specific you are, the better calibrated the estimates will be for your business. You can also describe anything the AI tends to get wrong. <br><br><strong>Additionally</strong>, the AI automatically learns from quotes your team accepted without overriding — those become calibration examples that improve accuracy over time.">
data-bs-content="Describe your shop's specialties, typical items, and pricing style in plain language. This text is injected into the AI's system prompt every time a photo quote is analysed — the more specific you are, the better calibrated the estimates will be for your business. You can also describe anything the AI tends to get wrong. <br><br><strong>Additionally</strong>, the AI automatically learns from quotes your team accepted without overriding — those become calibration examples that improve accuracy over time.">
placeholder="Examples: • We specialise in automotive restoration — wheels, frames, suspension brackets, and roll cages are our bread and butter. • Our customers expect premium pricing. We rarely work on items over 20 sqft. • Most items come to us already stripped; sandblasting adds roughly 15 min per item on average. • We use a 2-stage cure cycle — pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)</textarea>
placeholder="Examples: • We specialise in automotive restoration — wheels, frames, suspension brackets, and roll cages are our bread and butter. • Our customers expect premium pricing. We rarely work on items over 20 sqft. • Most items come to us already stripped; sandblasting adds roughly 15 min per item on average. • We use a 2-stage cure cycle — pre-heat 10 min, coat, cure 20 min at 400°F.">@(Model.OperatingCosts?.AiContextProfile)</textarea>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted">Plain language — write it as if briefing a new estimator on your shop.</small>
<small class="text-muted">Plain language — write it as if briefing a new estimator on your shop.</small>
title="Build a suggested profile from your existing settings — ovens, workers, inventory categories, and rates">
title="Build a suggested profile from your existing settings — ovens, workers, inventory categories, and rates">
<i class="bi bi-stars me-1"></i> Generate from my settings
</button>
<span id="aiProfileStatus" class="small"></span>
@@ -843,9 +843,9 @@
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-lightbulb text-warning me-1"></i> How AI Learning Works</h6>
<p class="small mb-2"><strong>Layer 1 — Pricing config:</strong> Your operating costs (labor, equipment, markup) are always injected automatically.</p>
<p class="small mb-2"><strong>Layer 2 — Your shop profile:</strong> The description you write here is added to every AI analysis, guiding estimates toward your typical work.</p>
<p class="small mb-0"><strong>Layer 3 — Automatic learning:</strong> Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.</p>
<p class="small mb-2"><strong>Layer 1 — Pricing config:</strong> Your operating costs (labor, equipment, markup) are always injected automatically.</p>
<p class="small mb-2"><strong>Layer 2 — Your shop profile:</strong> The description you write here is added to every AI analysis, guiding estimates toward your typical work.</p>
<p class="small mb-0"><strong>Layer 3 — Automatic learning:</strong> Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.</p>
data-bs-content="The prefix is combined with a date stamp and sequence number to form record IDs — for example prefix <strong>QT</strong> produces <em>QT-2603-0042</em>. Change the prefix to match your preferred numbering convention. Changing it only affects <strong>new</strong> records; existing numbers are not renamed.">
data-bs-content="The prefix is combined with a date stamp and sequence number to form record IDs — for example prefix <strong>QT</strong> produces <em>QT-2603-0042</em>. Change the prefix to match your preferred numbering convention. Changing it only affects <strong>new</strong> records; existing numbers are not renamed.">
data-bs-content="Controls how jobs are created and flow through your shop. <strong>Require Customer PO</strong> enforces that a PO number is entered before a job can be saved — useful for commercial accounts. <strong>Allow Customer Approval</strong> enables the approval step in the job workflow — when a quote is approved, the job moves to an Approved status before work begins.">
data-bs-content="Controls how jobs are created and flow through your shop. <strong>Require Customer PO</strong> enforces that a PO number is entered before a job can be saved — useful for commercial accounts. <strong>Allow Customer Approval</strong> enables the approval step in the job workflow — when a quote is approved, the job moves to an Approved status before work begins.">
data-bs-content="Controls which events send emails to your team and customers. Set the <strong>From Email Address</strong> to a domain you control — using a domain-verified address prevents emails landing in spam. If left blank, the system default address is used. Turn off notification types you don't need to avoid inbox noise.">
data-bs-content="Controls which events send emails to your team and customers. Set the <strong>From Email Address</strong> to a domain you control — using a domain-verified address prevents emails landing in spam. If left blank, the system default address is used. Turn off notification types you don't need to avoid inbox noise.">
data-bs-content="Customise the subject and body of every automated email sent by the system — job status updates, quote approvals, invoice reminders, and more. Templates use <strong>{{placeholder}}</strong> tokens that are replaced with live data when the email is sent. Click <strong>Edit</strong> on any row to modify it; use <strong>Reset to Default</strong> to restore the original wording at any time.<br><br>Changes take effect immediately — the next triggered notification will use the updated template.">
data-bs-content="Customise the subject and body of every automated email sent by the system — job status updates, quote approvals, invoice reminders, and more. Templates use <strong>{{placeholder}}</strong> tokens that are replaced with live data when the email is sent. Click <strong>Edit</strong> on any row to modify it; use <strong>Reset to Default</strong> to restore the original wording at any time.<br><br>Changes take effect immediately — the next triggered notification will use the updated template.">
data-bs-content="Controls how long records are kept. Most businesses set quote and job retention to <strong>7 years</strong> to satisfy tax and audit requirements. <strong>Deleted record retention</strong> is the grace period after a soft-delete before the record is permanently purged — useful if someone accidentally deletes something.">
data-bs-content="Controls how long records are kept. Most businesses set quote and job retention to <strong>7 years</strong> to satisfy tax and audit requirements. <strong>Deleted record retention</strong> is the grace period after a soft-delete before the record is permanently purged — useful if someone accidentally deletes something.">
data-bs-content="Lookups are the dropdown options that appear throughout the app — job statuses, priorities, quote statuses, and more. You can rename labels, change colours, and reorder them to match your shop's terminology. <strong>Status codes</strong> drive workflow logic and should not be changed unless you understand the impact.">
data-bs-content="Lookups are the dropdown options that appear throughout the app — job statuses, priorities, quote statuses, and more. You can rename labels, change colours, and reorder them to match your shop's terminology. <strong>Status codes</strong> drive workflow logic and should not be changed unless you understand the impact.">
placeholder="e.g. *Products must be picked up within 5 days of notification of completion or a storage fee may apply."
>@(Model.Preferences?.WoTerms ?? "")</textarea>
<div class="form-text">Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.</div>
<div class="form-text">Printed in italic at the bottom of every blank work order. Supports plain text — use * or ** for visual emphasis.</div>
</div>
<div class="d-flex gap-2 align-items-center">
@@ -2168,7 +2168,7 @@
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="smsTermsModalLabel">
<i class="bi bi-phone me-2"></i>SMS Notifications — Terms of Service
<i class="bi bi-phone me-2"></i>SMS Notifications — Terms of Service
</h5>
</div>
<div class="modal-body">
@@ -2178,9 +2178,9 @@
</div>
<h6 class="fw-bold">1. Prior Express Written Consent Required</h6>
<p class="text-muted small">You <strong>must obtain clear, documented consent</strong> from each customer before sending them any SMS message. This means each customer must have explicitly agreed — in writing or through a recorded digital interaction — that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.</p>
<p class="text-muted small">You <strong>must obtain clear, documented consent</strong> from each customer before sending them any SMS message. This means each customer must have explicitly agreed — in writing or through a recorded digital interaction — that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.</p>
<h6 class="fw-bold">2. Federal Law Governs SMS — Fines Are Real</h6>
<h6 class="fw-bold">2. Federal Law Governs SMS — Fines Are Real</h6>
<p class="text-muted small">The <strong>Telephone Consumer Protection Act (TCPA)</strong>, enforced by the Federal Communications Commission (FCC), imposes fines of <strong>$500 to $1,500 per individual message</strong> sent without proper authorization. These fines apply per text, not per customer. A single campaign to 100 unconsented recipients could result in exposure of $50,000 to $150,000. The FCC and private plaintiffs both actively pursue TCPA violations.</p>
<h6 class="fw-bold">3. Opt-Out Requests Must Be Honored Immediately</h6>
@@ -2189,7 +2189,7 @@
<h6 class="fw-bold">4. Message Rates & Content Restrictions</h6>
<p class="text-muted small">Every message sent must include your business name and an opt-out reminder (e.g., "Reply STOP to opt out"). Messages must be directly relevant to the service the customer consented to receive and must not contain solicitations, promotions, or third-party offers unless the customer has separately consented to those.</p>
<h6 class="fw-bold">5. Your Responsibility — Not Ours</h6>
<h6 class="fw-bold">5. Your Responsibility — Not Ours</h6>
<p class="text-muted small">Powder Coating Logix provides this feature as a communication tool only. <strong>We are not responsible for how you use it.</strong> You agree that your company is solely responsible for obtaining proper consent, maintaining records of that consent, honoring opt-outs, and ensuring all outbound messages comply with the TCPA, FCC regulations, and any applicable state laws. You agree to indemnify and hold Powder Coating Logix harmless from any claims, fines, or damages arising from your company's use of SMS.</p>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.