Added explicit CompanyId == companyId predicates to every tenant-scoped
query in 22 controllers so cross-tenant data leakage is impossible even
if EF Core global query filters are bypassed or misconfigured.
Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true
for SuperAdmins with no CompanyId claim (break-glass accounts) and when
no HTTP context is present (background services, unit tests), resolving
225 unit test failures that stemmed from the global filter blocking all
in-memory test data.
New MultiTenantIsolationTests class (8 tests) verifies the explicit
predicate layer independently of the global query filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Start Intake button only shows when company has an active kiosk token
- Remote Link button renamed to "Send Intake Link" for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DashboardReadService no longer loads full entity lists and filters in memory.
All job panels (today/overdue/in-progress) now execute targeted COUNT + capped
SELECT queries in SQL. AR aging buckets, powder order lines, bill totals, and
active-customer counts are all aggregated at the DB level. The SuperAdmin action
previously loaded every company row to compute plan distribution and alert lists;
it now delegates to a new GetSuperAdminDashboardDataAsync() that uses SQL GROUP BY
and projections instead.
DashboardIndexData record updated to carry pre-sliced counts and capped lists so
the controller only does lightweight DTO projection. DashboardPowderOrderLineData
replaces the deep Job→JobItem→Coat Include chains with a single flat coat query
projected in SQL. OnlineUserMiddleware switches its per-user throttle from a
static ConcurrentDictionary (grows forever) to IMemoryCache with a 60-second
sliding expiry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Progress widget dismiss now POSTs to Dashboard/DismissProgressWidget, writing
GuidedActivationDismissedAt to the DB so the widget stays hidden across devices
and cache clears (localStorage alone wasn't enough). BuildShopProgressWidgetAsync
suppresses the widget server-side when AllDone + dismissed.
New SuperAdmin page at /OnboardingProgress shows the activation funnel across
all tenant companies: wizard status, chosen path, milestone progress bar, key
dates (first job/quote, first invoice, workflow completed, widget dismissed),
and a status badge (Not Started / In Progress / Complete / Dismissed). Nav link
added under Users & Activity in the Platform Management sidebar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>