From baec0b33f74a225983e122c545c55f5a76b6deed Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Fri, 22 May 2026 17:26:56 -0400 Subject: [PATCH 1/3] Fix QR scan stripping scheme from product URL LookupAsync builds SpecPageUrl from the ProductUrlTemplate via TryBuildDirectUrl. If the template is stored without a scheme the link is scheme-less and browsers treat it as relative, appending it to the app URL. The scanned QR URL is always fully-qualified and always the correct product page (it came from the manufacturer's bag), so use it unconditionally as SpecPageUrl on the pattern-matched QR path instead of only when SpecPageUrl was null. Co-Authored-By: Claude Sonnet 4.6 --- src/PowderCoating.Web/Controllers/InventoryController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs index 3ae5d49..a703431 100644 --- a/src/PowderCoating.Web/Controllers/InventoryController.cs +++ b/src/PowderCoating.Web/Controllers/InventoryController.cs @@ -946,7 +946,10 @@ public class InventoryController : Controller if (!string.IsNullOrWhiteSpace(urlMfr)) { aiResult = await _aiLookupService.LookupAsync(urlMfr, urlColor, null, urlPart); - if (aiResult.Success && aiResult.SpecPageUrl == null) + // The scanned QR URL is always the authoritative product page link — it came + // directly from the manufacturer's bag and is always fully-qualified. Overwrite + // whatever LookupAsync returned (which may be a scheme-less path from the template). + if (aiResult.Success) aiResult.SpecPageUrl = qrUrl; } else From 14f220347bf8e1e56499cfa3b8d104ace37c572b Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Fri, 22 May 2026 17:38:09 -0400 Subject: [PATCH 2/3] Add scheme failsafe to all inventory URL link buttons If a stored URL is missing http:// or https:// the browser treats it as relative and appends it to the app URL. Guard in three places: - inventory-catalog-lookup.js syncLinkButton: ensureAbsoluteUrl() prepends https:// - inventory-label-scan.js syncLink: same guard for scan-filled URL fields - Details.cshtml SafeUrl() Razor helper on SpecPageUrl, SdsUrl, TdsUrl links Co-Authored-By: Claude Sonnet 4.6 --- src/PowderCoating.Web/Views/Inventory/Details.cshtml | 11 ++++++++--- .../wwwroot/js/inventory-catalog-lookup.js | 7 ++++++- .../wwwroot/js/inventory-label-scan.js | 7 ++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/PowderCoating.Web/Views/Inventory/Details.cshtml b/src/PowderCoating.Web/Views/Inventory/Details.cshtml index f5926ce..23ec7a1 100644 --- a/src/PowderCoating.Web/Views/Inventory/Details.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Details.cshtml @@ -5,6 +5,11 @@ ViewData["PageIcon"] = "bi-box-seam"; ViewData["PageHelpTitle"] = "Inventory Item"; ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds — a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item."; + + string SafeUrl(string? url) => + string.IsNullOrEmpty(url) ? "#" + : (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + ? url : "https://" + url; } @section Styles { @@ -184,7 +189,7 @@

- + View on Manufacturer's Web Site

@@ -197,13 +202,13 @@
@if (!string.IsNullOrEmpty(Model.SdsUrl)) { - + Safety Data Sheet } @if (!string.IsNullOrEmpty(Model.TdsUrl)) { - + Technical Data Sheet } diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js index 7748a31..b2a38e9 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js @@ -413,10 +413,15 @@ // ── Helpers ─────────────────────────────────────────────────────────────── + function ensureAbsoluteUrl(url) { + if (!url) return url; + return /^https?:\/\//i.test(url) ? url : 'https://' + url; + } + function syncLinkButton(inputId, linkId, url) { const link = document.getElementById(linkId); if (!link) return; - if (url) { link.href = url; link.classList.remove('d-none'); } + if (url) { link.href = ensureAbsoluteUrl(url); link.classList.remove('d-none'); } else { link.classList.add('d-none'); } } diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js index 91d9ce2..6406a2e 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js @@ -550,10 +550,15 @@ }); } + function ensureAbsoluteUrl(url) { + if (!url) return url; + return /^https?:\/\//i.test(url) ? url : 'https://' + url; + } + function syncLink(inputId, linkId, url) { const link = document.getElementById(linkId); if (!link) return; - if (url) { link.href = url; link.classList.remove('d-none'); } + if (url) { link.href = ensureAbsoluteUrl(url); link.classList.remove('d-none'); } else { link.classList.add('d-none'); } } From 15b070398bc9e81120a8d6728a8d0bdc39a57597 Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Fri, 22 May 2026 17:40:14 -0400 Subject: [PATCH 3/3] Change URL scheme fallback from https to http Manufacturer product pages are often not on secure connections; http:// is the safer default to avoid connection failures on non-SSL sites. Co-Authored-By: Claude Sonnet 4.6 --- src/PowderCoating.Web/Views/Inventory/Details.cshtml | 2 +- src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js | 2 +- src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PowderCoating.Web/Views/Inventory/Details.cshtml b/src/PowderCoating.Web/Views/Inventory/Details.cshtml index 23ec7a1..8d6a116 100644 --- a/src/PowderCoating.Web/Views/Inventory/Details.cshtml +++ b/src/PowderCoating.Web/Views/Inventory/Details.cshtml @@ -9,7 +9,7 @@ string SafeUrl(string? url) => string.IsNullOrEmpty(url) ? "#" : (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - ? url : "https://" + url; + ? url : "http://" + url; } @section Styles { diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js index b2a38e9..c202de6 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-catalog-lookup.js @@ -415,7 +415,7 @@ function ensureAbsoluteUrl(url) { if (!url) return url; - return /^https?:\/\//i.test(url) ? url : 'https://' + url; + return /^https?:\/\//i.test(url) ? url : 'http://' + url; } function syncLinkButton(inputId, linkId, url) { diff --git a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js index 6406a2e..e41dd14 100644 --- a/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js +++ b/src/PowderCoating.Web/wwwroot/js/inventory-label-scan.js @@ -552,7 +552,7 @@ function ensureAbsoluteUrl(url) { if (!url) return url; - return /^https?:\/\//i.test(url) ? url : 'https://' + url; + return /^https?:\/\//i.test(url) ? url : 'http://' + url; } function syncLink(inputId, linkId, url) {