Merge dev into master: QR scan URL fixes and http scheme failsafe

This commit is contained in:
2026-05-22 17:41:01 -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 — 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'); }
}