- Inventory: location filter dropdown + Print Bin page (line #, name, color, SKU)
- Fix: Prismatic Powders QR scan now extracts manufacturer/SKU/color from URL path
and uses full LookupAsync pipeline instead of relying on page fetch alone
- Fix: iOS Safari 'Login / data Zero KB' download -- add OnRejected HTML response to rate limiter
- Fix: mobile session logout -- ConfigureApplicationCookie with 30-day MaxAge persistent cookie
- Help: new 'Location Filtering & Bin Print' section in Inventory help article
- Help: HelpKnowledgeBase updated with bin filter and print bin details
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file;
QuotesController passes overrideEmail through to NotificationService
- Quotes/Details view: SMS consent display, email/SMS send button state based on consent
- Accounting module: AccountingDisplayHelpers for consistent ledger formatting;
AccountsController + Accounts views improvements; AccountingEnums additions
- Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController
- InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path
(LookupByUrlAsync already has it built in — was double-fetching)
- PdfService: quote/invoice PDF updates
- PricingCalculationService: minor pricing logic fix
- QuoteProfile: mapping updates for new quote fields
- ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PowderCatalogController: Create, Edit, ToggleDiscontinued actions; searchable/filterable/sortable Index with pagination; AiLookup and AiAugmentFromUrl endpoints backed by IInventoryAiLookupService
- New views: Create, Edit, _Form partial (with AI-assisted field population), overhauled Index grid with completeness quality badges and responsive mobile cards
- New ViewModels: PowderCatalogIndexViewModel, PowderCatalogFormViewModel, PowderCatalogListItemViewModel
- AI lookup improvements: SpecificGravity field added to InventoryAiLookupResult; ApplyPowderFallbacks derives CoverageSqFtPerLb from specific gravity when docs omit it; DefaultTransferEfficiency (65%) applied everywhere transfer efficiency is null
- powder-catalog-ai-lookup.js: client-side AI lookup and URL augment wiring for the catalog form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the main AI lookup and catalog search, if CureTemperatureF or
CureTimeMinutes is still null but a TDS URL was found, fetch that page
and ask Claude to extract just the cure schedule.
- IInventoryAiLookupService.FetchTdsCureSpecsAsync: new interface method
- InventoryAiLookupService.FetchTdsCureSpecsAsync: fetches the TDS URL via
the existing FetchPageAsync pipeline (JSON-LD + doc-link extraction, HTML
stripping). If the page is a PDF or unreachable, returns Success=false
silently so no error surfaces in the UI. Otherwise sends a small targeted
prompt that asks only for cureTemperatureF and cureTimeMinutes and uses
MaxTokens=256 so the call is fast and cheap.
- InventoryController.ScanLabel: after catalog lookup, computes the resolved
cure values (catalog preferred over AI result). If either is null and a
TDS URL exists, calls FetchTdsCureSpecsAsync and merges any newly found
values back into aiResult before building the JSON response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QR scanning:
- Run BarcodeDetector and jsQR in parallel — jsQR starts after JSQR_DELAY_MS
(1.5 s) so both decode simultaneously. BarcodeDetector silently returns empty
arrays for some QR variants; running jsQR in parallel via a separate rAF loop
(rafId2) and its own off-screen canvas catches those cases. First decoder to
find anything calls handleQrResult and sets qrFound = true; the other stops.
Price extraction (two bugs):
- ScanLabel: unitPrice was catalogMatch?.UnitPrice ?? 0m, ignoring aiResult
.UnitCostPerLb entirely when no catalog match — changed to fall through to AI result
- AppendOffer: only read JSON-LD "price" field; Shopify AggregateOffer uses
"lowPrice" instead — now checked as fallback so Prismatic Powders prices are found
Camera pre-warm:
- Reverted localStorage approach (caused getUserMedia to fire on every page load,
showing Chrome's "Ask" prompt immediately before user clicked anything)
- Restored Permissions API gate: preWarmCamera only calls getUserMedia when
navigator.permissions.query returns 'granted', never risks a page-load prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LookupByUrlAsync now maps all identity + spec fields from Claude response
(manufacturer, SKU, colorName, description, sdsUrl, tdsUrl, unitCostPerLb, etc.)
Previously only augmenting fields were mapped; Columbia QR path left 80% blank
- Vision scan follow-up: after ScanLabelAsync reads label text, automatically run
LookupAsync using the extracted manufacturer + color/SKU to fill SDS/TDS URLs,
product page, image, description, and any specs not printed on the bag;
label values (cure schedule, SKU) remain authoritative and are never overwritten
- SDS/TDS URL extraction: added ExtractDocumentLinks() that scans anchor tags in
raw HTML before tag-stripping, injects found URLs as [Structured Data] lines so
Claude can read and echo them back in the JSON response; previously all hrefs
were lost with the HTML stripping
- Added SdsUrl/TdsUrl to InventoryAiLookupResult, Claude system prompt JSON schema,
LookupAsync mapping, and ScanLabel response (catalog match ?? aiResult fallback)
- SDS/TDS now also stored on auto-contributed catalog entries
- jsQR inversionAttempts: 'dontInvert' → 'attemptBoth' for better QR detection
under varying label contrast and lighting conditions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Platform PowderCatalogItem table (IPlainRepository, no tenant filter) with
full spec fields: cure temp/time, finish, color families, clear coat flag,
coverage sq ft/lb, transfer efficiency, IsUserContributed
- Two EF migrations: AddPowderCatalogItem + AddPowderCatalogSpecFields
- PowderCatalogController (SuperAdminOnly): import from Prismatic JSON scrape,
Lookup AJAX endpoint (catalog-first, ranked by SKU exact match), stats view
with Tenant Contributed card
- Unified smart Lookup button on inventory Create/Edit: catalog hit fills all
fields via catalogSnapshot pattern; AI augments cure/finish data from product
URL if subscription enabled; catalog miss falls through to AI lookup
- In-browser label scanner (_LabelScanModal): getUserMedia live camera feed,
jsQR auto-detects QR codes in rAF loop; "Scan Label Text" fallback sends
captured frame to Claude vision via /Inventory/ScanLabel
- ScanLabel endpoint handles both QR URL path (LookupByUrlAsync) and vision
path (ScanLabelAsync); auto-inserts unrecognized products as
IsUserContributed=true; returns wasInCatalog/addedToCatalog flags
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When AI Lookup fetches a manufacturer product page, it now extracts the
og:image (Open Graph) meta tag before stripping HTML tags. The image URL
is returned in InventoryAiLookupResult.ImageUrl and automatically shown
as a preview on the Create/Edit form alongside the other filled fields.
The preview includes a Remove button to clear the image, and the Wrong
Match? button clears it along with the other AI-filled fields.
On the inventory Details page a product image card is rendered above the
Stock & Pricing card whenever ImageUrl is set. The field is nullable so
existing records and powders without an image are unaffected.
New field: InventoryItem.ImageUrl (nvarchar, nullable).
Migration: AddInventoryItemImageUrl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>