web-correctness-fixesbreakdown.mdscope.md behind this batch — technical debt, no approval gate
needed. Notes quoted per stub.A batch of concrete defects, ordered by Jamie's stated urgency. The vendor dashboard's
headline tiles are permanently zero (fix "urgently as the top priority", and bring forward the
NRR/GRR definition ruling). A family of Next 16 async-params bugs — masked by
ignoreBuildErrors: true — kills search/pagination on the quotes and invoices lists and makes
the CSAT detail page unloadable; beyond the fix, Jamie wants search checked and standardised
across the whole app. The CSM/SDM/expert dashboards don't refresh after actions because their
revalidations target never-served paths. The setup checklist bypasses the shared data layer.
The quote PDF field should be removed (a future payment gateway owns documents). And a
housekeeping sweep: dead component folders, a committed backup file, inert error boundaries,
and stale links to redirect stubs.
Cross-cutting apps/web correctness; touches vendor reporting (service-journey: reporting),
finances (invoicing step), CSAT, and admin surfaces. One stub reaches into
packages/services (setup-checklist service) and one into the Quote model.
ignoreBuildErrors off — depends-on: nonepdf field and its dead rendering — depends-on: noneAll stubs except 3 are mutually independent; 3 builds on 2 (no point standardising search on top of pages whose params are broken). 1 leads on Jamie's explicit priority; the rest order by user impact.
.icm/intake/vendor-metrics/; stub 1 here fixes the zeroed headline only.access-control-hardening._done/async-params-and-build-errors.mdNext 16 delivers params/searchParams as Promises; six pages treat them as plain objects,
and next.config.ts sets typescript.ignoreBuildErrors: true, which hides exactly this
class of bug. Consequences today: search and pagination on /finances/invoices and
/finances/quotes silently do nothing, and /csat/[id] destructures its id from a Promise so
the CSAT read-only detail page can never load a record. Jamie's notes: "search and pagination
is not working in quotes and invoices" and "CSAT read only detail page never loads, it reads
its url parameter incorrectly."
Await params/searchParams properly in all six affected pages (including the two
Promise.resolve workarounds and the mistyped activity page), then flip
ignoreBuildErrors off and fix whatever else the typecheck surfaces so it stays off.
/finances/quotes and /finances/invoices./csat/[id] loads the record (customer-own form and read-only detail both render).Promise.resolve(searchParams) workarounds are replaced with typed awaits.next.config.ts no longer sets ignoreBuildErrors; the Vercel preview build passes
with type checking on.access-control-hardening/finance-lists-scoping).Evidence (all apps/web): app/(app)/finances/invoices/page.tsx:9-14,
app/(app)/finances/quotes/page.tsx:6-11, app/(app)/csat/[id]/page.tsx:9-12,
app/(app)/activity/[id]/page.tsx:6 (type-only), app/(app)/customer/activity/page.tsx:34-39
and app/(app)/customer/satisfaction/page.tsx:12-17 (workarounds); next.config.ts:7.
Turning the flag off may surface more than six errors — budget for the sweep; typecheck runs
in the factory (CI), not locally, per CLAUDE.md. touches: apps/web (six pages +
next.config.ts + whatever the sweep finds).
_done/dashboard-refresh-after-actions.md/csm/dashboard, /sdm/dashboard and /expert/dashboard have no route-policy entries, so
the proxy redirects them to /csm, /sdm, /expert (which re-export the same pages) — but
their server actions call revalidatePath("/csm/dashboard") etc., invalidating a path that is
never served. Net effect: after a blocker update, outreach send, or work-queue action, the
dashboard shows stale data until a hard reload. Jamie's note: "CSM SDM and expert dashboards
need to refresh after actions are taken."
Make the served path and the revalidated path the same thing: either add policy entries for
the /…/dashboard routes or (simpler) revalidate the role-home paths actually served — one
consistent choice across all three personas — and audit the rest of the app's
revalidatePath targets for the same drift (the setup-checklist one is stub 5's).
/csm, /sdm, /expert role homes still render identically.unstable_cache/revalidateTag to Next 16 use cache — the deferred
"stub 5" cache-tag wiring noted in lib/queries/workspace-cache.ts is a different,
larger change.Evidence: missing policies noted against apps/web/lib/route-policies.ts (the three
dashboards, vs /admin/dashboard etc. which have entries);
apps/web/app/(app)/sdm/dashboard/actions.ts:64, expert/dashboard/actions.ts:60,88,116
(revalidating never-served paths); proxy deny-by-default at apps/web/proxy.ts:101-109.
Follow the web-route skill for policy entries. touches: apps/web (route policies, three
dashboards' actions).
_done/housekeeping-debt.mdCommitted cruft accumulating in apps/web: eight dead component folders with zero references,
a committed .original.bak.txt backup file, ten error.tsx boundaries inside components/
where Next.js can never mount them, and legacy routes that survive only as redirects while
live pages still link to them (double-hop navigation). Jamie's note: "there are eight dead
component folders, a commited backup file, ten error boundaries in places where they can
never run and old routes that survive only as redirects while pages still link to them.
Please fix all this housekeeping debt."
One deletion-and-relink sweep: delete the dead folders and the backup file; move each inert
error.tsx to the owning route segment under app/ where it can actually catch (or delete
it if the segment already has one); update every in-app link and back-link to point at the
canonical route instead of a redirect stub; then remove redirect stubs that nothing links to
any more — keeping the ones that guard genuinely bookmarked/legacy URLs, each with a comment
saying so.
.bak.txt file are gone; build and lint stay
green (factory-verified).error.tsx exists under components/; affected route segments have working
boundaries where the moved files apply./admin/status, /admin/industries,
/products, /services, /integrations, /users invite button, etc.) — links go to
the canonical paths.@sustentus/ui) — real debt, but
a UI-package consolidation with behaviour risk; flag as a future chore, don't fold in here.packages/services (blob helpers etc.) — services-side sweep, separate
chore.Evidence list (all apps/web): dead folders components/admin/{service-detail, user-management-card, setup-completion-card, recent-activity-card, active-users-trend-card, setup-checklist-card, integrations-card} + app/(app)/settings/_components/ admin-tenant-settings-section.tsx; backup
components/dashboard/sdm/backups/expert-strength-row.original.bak.txt; ten inert
error.tsx under components/** (activity-feed, users-table, user-profile, csat-table,
csat-detail, kanban-board, lead-*); stale links: admin/status/[id] → /admin/status,
admin/industries/[id] → /admin/industries, products/[id] → /products,
services/[id] → /services, integrations/[id] → /integrations, /users "Invite user"
→ /settings (should be /admin/users/invite). Also stale docblocks claiming mock data in
components/dashboard/csm/portfolio-panel.tsx:48-51 and
components/dashboard/sdm/area-panel.tsx:144-147 — correct the comments while in there.
touches: apps/web only.
_done/quote-pdf-removal.mdThe quote pdf field promises a document that is never really generated — the docs sell "a
quote document for the customer's records" while the field's default points at a sample URL
from the PDF library's website. Jamie's ruling: "the quote PDF field can be removed, any
existing PDF will be retrievable via the payment gateway we use" (the later payments
project owns documents).
Remove the pdf field from the quote model and every surface that renders or links it, with
a data migration unsetting stored values — following the service-fee removal pattern (write
path and schema in one clean sweep, since the field never carried real documents). Update the
docs' quote page so it stops promising a PDF.
pdf field on the quote model; migration unsets it on existing documents.pdf field — unless Q1 says otherwise.pdf defaults to a princexml sample URL —
db-audit finding #8). Jamie's note names quotes only; recommend removing both in one run,
but flag for confirmation at pickup.Evidence: packages/services/src/db/models/quote.ts (pdf), docs promise in
apps/docs/app/business/service-journey/quote/page.mdx; the removal pattern:
apps/docs/archive/pipeline-runs/service-fee-write-path-removal +
service-fee-schema-and-data-removal (and note
their migration's down() lesson — write an honest down or document why not). Docs update
follows docs-sync. touches: packages/services (quote model + migration), apps/web (quote
surfaces), apps/docs.
Recorded by the intake-easy-features session; these rulings bind Define.
pdf field from the quote AND invoice models, every surface, and the docs, with one migration (db-audit finding #8 folded in)._done/search-and-pagination-standardisation.mdBeyond the two broken finance lists, list surfaces implement search and pagination each their own way — different param names, different service options, regex-based text search in services, per-column paging on the board, offset paging elsewhere — with no shared convention, which is how two lists shipped with dead controls unnoticed. Jamie's note: "it's worth a check across the whole application what search method we're using and how we can maybe improve and standardize it."
Audit every list surface (leads board, proposals, quotes, invoices, users, activity, CSAT,
notifications, skills/products/services/industries, customers) for how search and pagination
are wired; write the standard down (param names, page-size defaults, the service-side
PaginationOpts contract, text-search approach and its index implications); converge the
outliers onto it; and record the convention in CONVENTIONS.md so new lists follow it.
PaginationOpts ↔ query) in CONVENTIONS.md;
the outliers found are converged or explicitly ticketed with a reason.Evidence: packages/services/src/db/services/shared/{paginate,regex}.ts (the existing
primitives), PaginatedResult/PaginationOpts in models/types.ts; known variety across
leadService.findByStatuses (per-column paging), proposalService.findAll,
invoice/quote list opts, notifications (page size 50). touches: apps/web list pages,
packages/services list queries, CONVENTIONS.md.
Recorded by the intake-easy-features session; these rulings bind Define.
_done/setup-checklist-data-layer.mdThe setup-checklist actions are the only place in apps/web that bypass @sustentus/services:
they call connectDB() and register a schemaless model (strict: false) directly with
Mongoose — outside the tenant plugin, the soft-delete plugin, and the demo-reset purge
policy's model catalogue. They also revalidate /setup-checklist (now a redirect stub) and
/admin instead of /admin/settings/setup where the UI lives. Jamie's note: "the setup
checklist actions bypass the shared data layer entirely, fix this."
Give the setup checklist a real model and service in packages/services (tenant-scoped,
plugged in like every other model, classified in the demo-reset purge policy — its drift test
will demand this), move the actions onto it through the standard action pipeline, and point
revalidation at the served path.
connectDB/getModel call remains in apps/web; the checklist reads and writes go
through a @sustentus/services/server service.demo-reset/policy.ts (purged or kept — deliberately)./admin/settings/setup and the admin dashboard
readiness without a reload.SETUP_CHECKLIST_JSON mechanism.Evidence: apps/web/app/(app)/setup-checklist/actions.ts:50-72 (raw Mongoose, wrong
revalidate targets); the setup-checklists collection appears in the db-audit zombie list —
check whether the raw writes are why, and reconcile with .icm/intake/db-audit-findings
before creating a second collection name. Services-layer conventions per
packages/services/AGENTS.md and the services-imports skill. touches: packages/services
(new model+service, purge-policy classification), apps/web (actions).
_done/vendor-headline-tiles.mdThe vendor dashboard assembles its data over a zeroed base object and overrides every section
except headline — so total leads, MRR and delivery rate render 0 for every vendor, always.
The most visible numbers on the vendor's home page are wrong. Jamie's note: "the headline
tiles of the vendor dashboard are permanently zero, the code fills in every section except
that, please fix it urgently as the top priority, and bring forward the pending decision
about the definition of NRR and GRR."
Compute the headline from the same vendor service reads the sections already use, with the claim-surface treatment the dictionary work established (population/period/basis visible, or honestly "not yet reported" where a real value can't be computed — never a silent 0). At the same time, surface the open NRR/GRR ruling: the dashboard shows NRR 109.6 / GRR 92.8 with no definitions behind them, and the vendor-metrics design left "delete or re-scope" undecided — this stub brings that decision forward as Jamie asked.
.icm/intake/vendor-metrics/
owns those; this stub must not quietly implement them..icm/runs/vendor-metrics/02_design/output/stub-reconciliation.md); Jamie to rule at
pickup, and the answer should be recorded there too so the metrics batch unblocks.Evidence: apps/web/app/(app)/vendor/dashboard/empty-data.ts:3-10 ("the seven section
computations land in features 2–8"; headline never overridden),
vendor-dashboard-view.tsx:126-130 (TTV/ARR strip carry no definitions — "the
dashboard-reconciliation stub's job"), vendor/dashboard/page.tsx:136-193 (section
overrides). Vendor reads live in packages/services/src/db/services/vendor/ (2690 LOC);
dictionary/claim surface from metric-dictionary-and-claim-surface (PR #782). Cross-scope:
vendor-metrics/vendor-dashboard-reconciliation — keep the boundary honest. touches:
apps/web/app/(app)/vendor/dashboard/, packages/services/src/db/services/vendor/.
Recorded by the intake-easy-features session; these rulings bind Define.