Compare commits

...

4 Commits

Author SHA1 Message Date
spouliot b7ab85ff92 Merge dev into master: QR scan URL fixes and http scheme failsafe 2026-05-22 17:41:01 -04:00
spouliot 15b070398b 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 <noreply@anthropic.com>
2026-05-22 17:40:14 -04:00
spouliot 14f220347b 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 <noreply@anthropic.com>
2026-05-22 17:38:09 -04:00
spouliot baec0b33f7 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 <noreply@anthropic.com>
2026-05-22 17:26:56 -04:00
4 changed files with 24 additions and 6 deletions
@@ -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
@@ -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 &mdash; 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 : "http://" + url;
}
@section Styles {
@@ -184,7 +189,7 @@
<div class="col-12">
<label class="text-muted small mb-1">Product URL</label>
<p class="mb-0">
<a href="@Model.SpecPageUrl" target="_blank" class="text-decoration-none">
<a href="@SafeUrl(Model.SpecPageUrl)" target="_blank" class="text-decoration-none">
<i class="bi bi-box-arrow-up-right me-1"></i>View on Manufacturer's Web Site
</a>
</p>
@@ -197,13 +202,13 @@
<div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrEmpty(Model.SdsUrl))
{
<a href="@Model.SdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary">
<a href="@SafeUrl(Model.SdsUrl)" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-pdf me-1"></i>Safety Data Sheet
</a>
}
@if (!string.IsNullOrEmpty(Model.TdsUrl))
{
<a href="@Model.TdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary">
<a href="@SafeUrl(Model.TdsUrl)" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-text me-1"></i>Technical Data Sheet
</a>
}
@@ -413,10 +413,15 @@
// ── Helpers ───────────────────────────────────────────────────────────────
function ensureAbsoluteUrl(url) {
if (!url) return url;
return /^https?:\/\//i.test(url) ? url : 'http://' + 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'); }
}
@@ -550,10 +550,15 @@
});
}
function ensureAbsoluteUrl(url) {
if (!url) return url;
return /^https?:\/\//i.test(url) ? url : 'http://' + 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'); }
}