Commit Graph

64 Commits

Author SHA1 Message Date
spouliot 900a52f89d Merge branch 'dev' 2026-04-29 08:11:37 -04:00
spouliot 73df72ab97 Add dismiss button to progress widget completion state
Shows an × button in the top-right of the completion card so users can
permanently hide the widget once they've seen the success message. Dismissal
is stored in localStorage (same pattern as the collapse state) so it persists
across page loads without requiring a DB migration. The widget hides itself
on the next load before any layout is shown, avoiding a flash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:37:53 -04:00
spouliot 45441c1d07 Fix 'Customize your workflow' done signal not detecting deletions
The previous AnyAsync check used global query filters which hide
soft-deleted records. Deleting a lookup sets UpdatedAt on the record
(EF interceptor stamps Modified entities) but the IsDeleted filter
made it invisible to the query. Added ignoreQueryFilters: true with
an explicit CompanyId predicate so soft-deleted lookups are included —
any deletion or edit now correctly marks the step complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:25:30 -04:00
spouliot 64e9abceac Hide team invite step on progress widget for single-user plans
Injects ISubscriptionService into DashboardController and calls
GetUserCountAsync to check the plan's MaxUsers limit. When MaxUsers == 1
the "Bring your crew in" step is omitted from the progress widget entirely,
so solo-plan users aren't prompted to do something their subscription
doesn't allow. Plans with MaxUsers > 1 or unlimited (-1) show the step
as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:22:41 -04:00
spouliot b1337d3b61 Update help docs and AI knowledge base for onboarding overhaul
GettingStarted.cshtml: updated Setup Wizard description from 10-step to
5-step, revised time estimate (5–10 min), added new "After the Wizard"
section explaining the guided activation first-workflow flow and the
progress widget (what it tracks, how the highlight works, when it
disappears, Admin-only visibility).

HelpKnowledgeBase.cs (AI assistant): updated Setup Wizard entry to 5 steps,
replaced old step list (removed steps 5–8, 10), updated new company
quick-start checklist to reference the 5-step wizard and the post-wizard
progress widget. Added new GUIDED ACTIVATION section covering the two
onboarding paths (Quote First / Job First), the Daily Board intro experience,
and how the banner auto-dismisses. Updated Common Workflows to reflect the
new onboarding order. Dashboard section now describes the progress widget
and its six tracked steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:10:59 -04:00
spouliot 8aae30765f Onboarding overhaul: slim wizard, progress widget, guided activation UX
Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.

Dashboard progress widget (new): "Get the most out of your shop" checklist
appears for Company Admins after wizard completion. Tracks six post-setup
activation tasks with dynamic progress badge, motivating subtitle copy,
collapsed-state persistence via localStorage, and a full completion state
("Your shop is fully set up 🎉") that replaces the checklist at 100%.
The next recommended step is highlighted with a solid CTA button and a
subtle blue row tint. Completed steps show encouraging green subtext instead
of just "Done". Widget disappears from controller when AllDone would have
caused a silent vanish — now renders the completion state instead.

Guided activation (Daily Board): rewrote the BoardIntroStep callout to lead
with "This is your shop in real time" and a plain-English description of the
board's purpose. Added a separate InstructionText field to
GuidedActivationCalloutViewModel so the "Move this job to the next stage"
action prompt renders as a distinct bold line with an arrow icon rather than
being buried in the body copy. After the stage change, the confirmation
callout now reads "Nice — your workflow just updated" to reinforce what just
happened before prompting the invoice step.

All copy passes the "shop owner, not SaaS" test: no technical jargon,
benefit-driven descriptions, natural language throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:10:47 -04:00
spouliot 4d27a378ac Fix QuoteApprovalControllerTests build break after Phase 3 migration
QuoteApprovalController's constructor changed from ApplicationDbContext to
IUnitOfWork in Phase 3, but the test helper was still passing the raw context.
Wrap context in UnitOfWork in CreateController; tests seed/assert through
context directly so the same instance works correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:35:30 -04:00
spouliot 6993c2c462 Fix invoice detail crash after first credit memo or refund is applied
AutoMapper 12+ throws AutoMapperMappingException when mapping a non-empty
collection for which no element type map is registered. Invoice.CreditApplications
and Invoice.Refunds had no CreateMap entries, so the invoice Details view worked
fine until the first credit or refund existed — at that point AutoMapper tried
to map the element type and threw, causing the catch block to redirect to the
invoice list with a generic "failed to load" error.

Fix: mark CreditApplications and Refunds as Ignore() in the Invoice->InvoiceDto
AutoMapper profile. Both collections are already built manually in
BuildInvoiceDtoAsync, matching the existing GiftCertificateRedemptions pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:17:38 -04:00
spouliot 1cb7a8ca4a Phases 3 & 4: Complete data access architecture migration
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers,
routing all data access through IUnitOfWork. Added IPlainRepository<T> for
the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote)
that intentionally don't extend BaseEntity and therefore can't use the
constrained IRepository<T>. Added permanent-exception comments to the 18
controllers that legitimately retain direct DbContext access (Identity infra,
cross-tenant platform ops, bulk streaming exports).

Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup
gate that reflects over every Controller subclass and throws at boot if any
non-exempt controller injects ApplicationDbContext. The app cannot start with
a violation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:17:29 -04:00
spouliot 90bc0d965f Phase 2: Eliminate ApplicationDbContext from domain controllers
Migrated InvoicesController, QuotesController, JobsController, BillsController,
PurchaseOrdersController, and CustomersController to route all data access
through IUnitOfWork typed/generic repositories instead of injecting
ApplicationDbContext directly.

New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync,
GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository
(GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync
with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync.

All EF complex include chains relocated into repository methods; controllers now
call named query methods rather than composing raw IQueryable chains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:20:39 -04:00
spouliot 80b0e547cc Phase 1: Introduce typed repository interfaces and report service stubs
Six IUnitOfWork properties upgraded from generic IRepository<T> to domain-specific
typed interfaces (IJobRepository, IQuoteRepository, IInvoiceRepository,
ICustomerRepository, IBillRepository, IPurchaseOrderRepository). Each backed by a
concrete typed repository that encapsulates complex include chains previously
inlined in controllers.

Also adds IFinancialReportService and IOperationalReportService stub implementations
(NotImplementedException placeholders) to Application.Interfaces and Infrastructure.Services,
registered in Program.cs. These are the migration targets for ReportsController's
aggregate query methods in Phase 2.

No controller behaviour changed in this commit — all callers still compile because
typed interfaces extend IRepository<T>.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:54:10 -04:00
spouliot 92dc3ebd08 Add data access architecture spec and enforce rules in CLAUDE.md
Defines the target architecture for eliminating direct ApplicationDbContext
injection from controllers. Documents the three-tier model (generic repo,
typed domain repos, read services), the 6 typed repository interfaces to
build, the 2 reporting service interfaces to build, permanent exceptions,
and the 4-phase migration roadmap with per-controller checklist.

CLAUDE.md updated with the hard rule and tier quick-reference so every
session and every team member sees the constraint immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:35:16 -04:00
spouliot 5631d1d57a Add WarningPermanent toast type and upgrade invoice failure notifications
Email delivery failures and PDF generation errors now show a permanent
warning/error toast that requires manual dismissal, so users cannot
accidentally miss critical action-blocking feedback.

- ToastHelper: WarningPermanent TempData key + Warning/WarningPermanent
  extension methods on both ITempDataDictionary and Controller
- SetNotificationResultToast: NotificationStatus.Failed now uses
  ToastWarningPermanent (previously auto-dismissed in 5 s)
- InvoicesController.Send: TempData["Warning"] → TempData["WarningPermanent"]
  when PDF generation or email dispatch fails
- InvoicesController.DownloadPdf: TempData["Error"] → TempData["ErrorPermanent"]
  with the actual exception message so root cause is visible
- _Layout.cshtml: WarningPermanent hidden div
- toast-notifications.js: WarningPermanent handler (timeOut: 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:02:16 -04:00
spouliot cad728ba66 Fix passkey login tracking, add email opt-out UI guards, and add Quick/Full quote mode toggle
- PasskeyController: set LastLoginDate on passkey sign-in so Company Health
  and audit pages show accurate last-login times (was always showing 'Never')
- Jobs/Index status modal: disable 'Notify customer' email toggle and show
  warning when customer has notifications turned off; CustomerNotifyByEmail
  added to JobListDto + JobProfile mapping + data-customer-notify attribute
- Quotes/Create: disable 'Send quote via email' checkbox with 'Notifications
  off' badge when selected customer has email opt-out; ViewBag.CustomerEmailOptOutIds
  added alongside existing CustomerTaxExemptIds pattern
- Quotes/Create: Quick Quote / Full Quote segmented toggle at top of form;
  hides non-essential fields (dates, notes, tags, oven, discount, photos) in
  Quick mode; selection persisted in localStorage
- InvoicesController Send action: improved error logging and user-facing
  warning when PDF generation or email dispatch fails after status is saved
- item-wizard.js: guard item restoration with try/catch; ensure writeHiddenFields
  always runs on form submit via capture-phase listener
- Help docs and AI knowledge base updated for all new features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 13:32:34 -04:00
spouliot 0ea192d55b Harden legacy file paths and Twilio webhook validation 2026-04-26 18:14:16 -04:00
spouliot 8491b308eb Add admin email wizard and logging 2026-04-26 17:01:09 -04:00
spouliot 404ab3c45d Add unit tests for LedgerService, AccountBalanceService, DepositsController, and GiftCertificatesController
181 tests total passing. Covers all 9 LedgerService transaction sources, debit/credit
balance mechanics, AR/AP movements, date range filtering, running balance computation,
and future-dated opening balance exclusion. Also covers deposit recording/deletion,
gift certificate lifecycle (issue, void, lazy expiry), and account balance recalculation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:48:34 -04:00
spouliot 90a01571e3 Merge dev into master
- AI Catalog Price Check (Haiku model, rate limiting, progress bar, quarterly limit)
- Three-layer feature gating for AI Catalog Price Check (platform/plan/company)
- Passkey biometric login improvements (enrollment prompt, RPID fix, dismiss option)
- Company admin navigation consolidation (Subscription & Features button)
- Unit tests for 9 new services/controllers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 10:35:29 -04:00
spouliot a4b8ae611a Add passkey prompt dismissal and consolidate company admin navigation
- Add "Don't ask me again" to passkey enrollment prompt (PasskeyPromptDismissed
  field on ApplicationUser; DismissPrompt POST action; migration applied)
- Add Subscription & Features button to Companies/Index btn-group and
  Companies/Edit header for direct navigation to SubscriptionManagement/Manage
- Add Edit Company back-link on SubscriptionManagement/Manage
- Remove duplicate AI Features section from Companies/Edit (managed exclusively
  via Subscription & Features page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 10:34:50 -04:00
spouliot 3899860c1f Fix PlatformSettings insert collision in AddAiCatalogPriceCheckGating migration
Replace InsertData (hardcoded ID 9) with raw IF NOT EXISTS SQL so the
migration is safe on environments where ID 9 is already taken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 09:30:28 -04:00
spouliot f03a198e79 Make AiCatalogPriceCheckEnabled a plan-override toggle
Company-level toggle now grants access regardless of plan tier, checked
before the plan gate. Useful for enabling the feature on individual
Pro/Basic companies without upgrading their plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 08:41:27 -04:00
spouliot cb7bbc37bd Add three-layer feature gating for AI Catalog Price Check
Adds platform-level, plan-level (Enterprise only), and per-company
toggles for the AI Catalog Price Check feature. Includes:
- Company.AiCatalogPriceCheckEnabled per-company flag
- SubscriptionPlanConfig.AllowAiCatalogPriceCheck plan-level flag
- PlatformSetting 'AiCatalogPriceCheckEnabled' global kill switch
- IPlatformSettingsService.GetBoolAsync helper
- ISubscriptionService.CanUseAiCatalogPriceCheckAsync
- UI controls in Companies/Edit, PlatformSubscription/Edit+Index,
  and SubscriptionManagement/Manage
- Migration AddAiCatalogPriceCheckGating applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 08:29:51 -04:00
spouliot fa9fa76231 Document AI Catalog Price Check in knowledge base and help article
- HelpKnowledgeBase: full AI price check section (verdicts, confidence,
  category paths, run limit, how to use, common questions)
- Inventory help article: new 'AI Catalog Price Check' section with
  verdicts, step-by-step instructions, caveats, and on-page nav link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:55:05 -04:00
spouliot 4128c15bbb Remove 'Claude' brand references from all user-facing views
Replace with generic 'AI', 'AI agent', or 'AI system' throughout.
Keeps the underlying vendor implementation details off the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:51:36 -04:00
spouliot 6d9111b448 Rename button and add explanatory blurb to AI price check page
- Button: 'Run/Re-run Price Check' -> 'Analyze Catalog with AI'
- Add info card explaining what the analysis does, verdict meanings,
  and the disclaimer to verify operating costs before running

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:49:49 -04:00
spouliot 37c95192ca Enforce quarterly run limit on AI price check
- GET: sets ViewBag.NextRunAvailable if last run was within 90 days;
  view disables the button and shows the next eligible date
- POST: returns early with a warning if called before the 90-day window

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:48:39 -04:00
spouliot 03c10a3d77 Recalibrate progress bar to 27s/batch based on observed run time
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:45:36 -04:00
spouliot ff79c39e83 Switch to sequential batching to eliminate rate limit hits
1 concurrent + 20s pacing = ~3 batches/min × 2k tokens = 6k TPM,
safely under the 8k output TPM limit. Progress estimate updated to 22s/batch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:27:10 -04:00
spouliot 2d25f6db2b Add proactive inter-batch pacing to avoid rate limit hits
Rather than relying on reactive 65s retries, each semaphore slot is held
for at least MinBatchIntervalSeconds (20s). With 2 concurrent slots that
limits throughput to ~3 batches/min × ~2k tokens = ~6k output TPM,
safely under the 8k/min limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:01:22 -04:00
spouliot 47f186384f Increase progress bar estimate to account for rate-limit retry waits
25s per wave (was 10s) gives headroom for occasional 65s pauses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:55:17 -04:00
spouliot 26b8244422 Reduce to 2 concurrent batches to avoid Haiku output TPM bursting
3 concurrent batches hit the rate limit simultaneously then retry in
unison, causing repeated 429s. 2 concurrent keeps output rate lower.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:54:32 -04:00
spouliot 7b902d90a2 Restore 3 concurrent batches with Haiku; recalibrate progress bar
Haiku has generous rate limits so parallelism is safe again. Retry
logic catches any 429s. Progress estimate updated to ~8s per wave.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:49:53 -04:00
spouliot f05e16a826 Switch AI price check to Haiku for cost and speed
Testing Haiku 4.5 for catalog price analysis — structured JSON output
with explicit rules is well within its capabilities. Revert to Sonnet
if result quality is insufficient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:41:50 -04:00
spouliot 97d47dbd1c Fix progress bar timing for sequential batch processing
Was still estimating based on 3 concurrent waves (old model).
Sequential mode runs ~18s per batch, so 500 items ≈ 6 minutes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:13:39 -04:00
spouliot 7407d1cd96 Fix rate limit errors in AI price check
Tier 1 Anthropic accounts are capped at 8,000 output tokens/minute on
Sonnet. 3 concurrent batches burst well past that, causing 429s.

- MaxConcurrentBatches: 3 → 1 (sequential prevents burst)
- Add retry: on rate_limit_error, wait 65s then retry up to 3 times
  so the per-minute window resets before the next attempt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:54:30 -04:00
spouliot 740238a939 Drop description field from AI price check user prompt
Item name + category path give Claude sufficient context for surface area
estimation. Descriptions add input tokens without meaningfully improving
verdict quality.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:44:25 -04:00
spouliot 560a2c76b8 Add full category path to AI price check for coating-type context
- Skip $0-priced items (placeholders/category headers) in RunAiPriceCheck
- Build full category path (e.g. "Cerakote > Firearms") via BuildCategoryPath
  so Claude receives coating-type context — Cerakote pricing differs significantly
  from standard powder coat
- Update AI system prompt to instruct Claude to use the category path when
  determining process type, equipment, cure times, and market rates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:35:41 -04:00
spouliot 19cc03ad1c Parallelize AI price check batches, increase batch size to 25
500-item catalog was making 50 sequential API calls, causing progressive rate-limit
throttling (explains "super slow towards the end") and ~$3 in credits.

- BatchSize: 10 → 25 (word limits are in place; 25 items × ~80 tokens ≈ 2000
  output tokens, well within MaxTokens=8192 — the original truncation cause)
- Run up to 3 batches concurrently via SemaphoreSlim(3) — independent API calls
  with no shared state, so no growing context issue
- For a 500-item catalog: 50 sequential calls → 20 calls in ~7 parallel waves,
  roughly 4× faster and 60% cheaper
- Dropped unused `costs` param from AnalyzeBatchAsync (system prompt has all costs)
- JS progress timing updated to reflect parallel waves

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:27:07 -04:00
spouliot 9370fcdd8f Reduce batch size to 10 and tighten AI price check prompt
Still seeing stubs despite MaxTokens=8192 — smaller batches and explicit
word limits in the prompt eliminate any remaining truncation risk.

- BatchSize: 15 → 10 (~1200 output tokens per batch vs. potential 3000+)
- Prompt: added 20-word cap on assumptions, 25-word cap on reasoning
- Prompt: strengthened "nothing before or after the '['" instruction
- Error log: now includes item IDs and first 300 chars of raw response
  so the next failure tells us exactly what Claude returned
- JS timing: updated batch divisor from 25 → 10 to match actual batch size

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:57:23 -04:00
spouliot 2c4c1a6846 Fix AI price check truncation and JSON parse errors
Root cause: MaxTokens=4096 was too low — 25 items at ~250 tokens each hit the
limit mid-array (logged error showed Path: $[17]).

- MaxTokens: 4096 → 8192
- BatchSize: 25 → 15 items (keeps each response well under the limit)
- StripJsonFences → ExtractJsonArray: now also handles prose before/after the
  JSON array, and recovers truncated responses by finding the last complete
  object and closing the array — so partial batches return whatever Claude
  finished rather than nothing
- GET action: added try-catch around ResultsJson deserialization so a bad DB
  row shows a friendly "re-run" warning instead of a raw error page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:45:53 -04:00
spouliot c9324ee0b0 Fix catalog-price-check.js served from wrong wwwroot
File was written to repo-root wwwroot/ instead of
src/PowderCoating.Web/wwwroot/ — causing a 404 and MIME type refusal.
Moved to the correct location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:33:14 -04:00
spouliot 9943c11571 Add progress overlay to AI Catalog Price Check
Shows a modal overlay with animated progress bar and batch-aware status messages
while Claude is analyzing. Progress animates in two phases: ease-out to ~85%
over the estimated duration, then a slow crawl to 99% so it never falsely
"completes" before the server responds.

- Overlay driven by CSS (hidden until .active added by JS)
- Item count passed from controller as data-item-count on the run button
- Batch count derived from item count (batches of 25) to show accurate
  "Analyzing batch N of M…" messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:27:08 -04:00
spouliot 360edace72 Fix EnrollPrompt page layout squished on desktop
The auth panel CSS (brand panel gradient, form panel flex centering, feature list,
subtext color) was only defined in Login.cshtml's @section Styles — not in the
shared auth layout. EnrollPrompt used the same class names but had no styles behind
them, so the two-column layout collapsed. Added matching styles in EnrollPrompt's
own @section Styles block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:16:40 -04:00
spouliot 54f444d981 Add AI Catalog Price Check feature
Claude reviews every active catalog item against the shop's own operating costs
and returns a per-item verdict (below-cost / thin-margin / high / ok) with a
suggested price range, cost floor, and assumptions.

- New entity: CatalogPriceCheckReport (JSON blob, archived per company)
- New service: IAiCatalogPriceCheckService / AiCatalogPriceCheckService
  batches items 25 at a time to stay within model context limits
- Two new controller actions: GET AiPriceCheck (view report) + POST RunAiPriceCheck
- AiPriceCheck view: summary cards (counts by verdict), color-coded item cards
  with Edit Price link, assumptions detail, and loading spinner on submit
- AI Price Check button added to catalog Index header
- Migration AddCatalogPriceCheckReport applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:41:56 -04:00
spouliot dbe4170986 Add unit tests for 9 new services/controllers and expand existing test coverage
116 tests passing: JobPhotoService, MeasurementConversionService, PlatformSettingsService,
QuoteApprovalController, QuotePhotoService, ShopCapabilityCalculator, StorageMigrationService,
TenantContext, UsageQuotaController — plus expanded PricingCalculation, Registration, and
Subscription tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:27:30 -04:00
spouliot edce8e8c4a Move passkey enrollment prompt to post-login dedicated page
After password login, users are routed through /Passkey/EnrollPrompt
before reaching the dashboard. The page shows an Enable / Maybe later
choice using the auth layout for a clean full-screen experience.
Users who already have a passkey are skipped past instantly.

Removes the floating bottom-right card from _Layout — the dedicated
page is a better UX touchpoint (one moment, right after login, rather
than a floating card on every page).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:41:01 -04:00
spouliot 92f71f62d0 Fix iOS passkey enrollment sheet appearing on password form submit
Switch passkeySupported() from isConditionalMediationAvailable() to
isUserVerifyingPlatformAuthenticatorAvailable(). The conditional API
signals to iOS 17/18 that the page wants autofill passkey interception,
causing Safari to show its own native enrollment bottom sheet when the
password Sign In button is clicked. The platform authenticator check
simply asks if the device has biometrics, with no UI side-effects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:56:17 -04:00
spouliot c71332740e Fix passkey RPID mismatch across environments
Derive ServerDomain and Origin from the incoming HTTP request instead of
appsettings.json, so WebAuthn works on localhost, dev, and production
without any environment-specific configuration. Removed IFido2 from DI
and the Fido2 appsettings block — PasskeyController instantiates Fido2
per-request via BuildFido2().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:49:45 -04:00
spouliot edc599a1a2 Clean up TODO list and remove stale deploy_migration.sql
Completed items removed from TODO: AI catalog price check, catalog item
images, AI company lookup. deploy_migration.sql replaced by the
versioned scripts/042426_deploy_migration.sql.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:12:31 -04:00
spouliot 90a5a028ad Update docs and AI assistant for passkey biometric login
- HelpKnowledgeBase: passkey entry under USER PROFILE section with
  full how-it-works detail (setup, login flow, browser requirements,
  account-lock enforcement, per-device management)
- UserProfile help article: new Passkeys & Biometrics section between
  Two-Factor Auth and Appearance, with setup steps, login steps,
  browser compatibility note, and lost-device warning
- TOC nav link added to UserProfile article sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:08:46 -04:00