Skip to Content

← All archived intake epics

Intake: web-correctness-fixes

breakdown.md

Breakdown: Web correctness fixes — broken pages, dead code, wrong wiring

  • scope-slug: web-correctness-fixes · source: Jamie's platform-audit notes (2026-08-17) + the web-app feature audit; no scope.md behind this batch — technical debt, no approval gate needed. Notes quoted per stub.
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • personas: Vendor, Customer, CSM, SDM, Expert, Admin

What I understood

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.

Where it sits

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.

Build order

  1. vendor-headline-tiles — wire the permanently-zero headline tiles; surface the NRR/GRR ruling — depends-on: none
  2. async-params-and-build-errors — fix the six async-params bugs; turn ignoreBuildErrors off — depends-on: none
  3. search-and-pagination-standardisation — audit search/pagination app-wide; standardise on one pattern — depends-on: async-params-and-build-errors
  4. dashboard-refresh-after-actions — CSM/SDM/expert dashboards refresh after mutations; add their missing route policies — depends-on: none
  5. setup-checklist-data-layer — move the raw-Mongoose setup-checklist access into the services layer; fix its revalidation targets — depends-on: none
  6. quote-pdf-removal — remove the quote pdf field and its dead rendering — depends-on: none
  7. housekeeping-debt — dead components, backup file, inert error boundaries, stale links to redirect stubs — depends-on: none

Parallelizable

All 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.

Out of scope (whole scope)

  • The vendor metrics programme (values behind the KPI dictionary) — its own pending scope, .icm/intake/vendor-metrics/; stub 1 here fixes the zeroed headline only.
  • Payment processing — the manual invoice/quote flow stands (Jamie's note); stub 6 removes the quote PDF on that basis.
  • Access-control scoping of the finance lists — access-control-hardening.

_done/async-params-and-build-errors.md

Stub: Fix the async-params bugs and stop ignoring build errors

  • feature-slug: async-params-and-build-errors
  • scope: web-correctness-fixes
  • personas: Customer, Expert, CSM, SDM, Admin
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 2 of 7

Problem

Next 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."

Proposed change

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.

Acceptance criteria (rough)

  • Search and pagination work on /finances/quotes and /finances/invoices.
  • /csat/[id] loads the record (customer-own form and read-only detail both render).
  • The 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.

Out of scope (this feature)

  • Standardising the search pattern app-wide (stub 3).
  • Viewer-scoping these lists (access-control-hardening/finance-lists-scoping).

Notes for Define

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

Stub: CSM, SDM and expert dashboards refresh after actions

  • feature-slug: dashboard-refresh-after-actions
  • scope: web-correctness-fixes
  • personas: CSM, SDM, Expert
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 4 of 7

Problem

/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."

Proposed change

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).

Acceptance criteria (rough)

  • Resolving a blocker from the CSM dashboard shows the updated queue without a manual reload; same for SDM outreach and expert work-queue actions.
  • Route policy and revalidation targets agree for all three personas (no revalidate-a-redirect anywhere in apps/web after the sweep).
  • The /csm, /sdm, /expert role homes still render identically.

Out of scope (this feature)

  • Migrating from 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.

Notes for Define

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.md

Stub: Housekeeping sweep — dead components, backup file, inert boundaries, stale links

  • feature-slug: housekeeping-debt
  • scope: web-correctness-fixes
  • personas: Admin, CSM, SDM, Expert, Vendor, Customer
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 7 of 7

Problem

Committed 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."

Proposed change

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.

Acceptance criteria (rough)

  • The eight dead component folders and the .bak.txt file are gone; build and lint stay green (factory-verified).
  • No error.tsx exists under components/; affected route segments have working boundaries where the moved files apply.
  • No live page links to a redirect stub (/admin/status, /admin/industries, /products, /services, /integrations, /users invite button, etc.) — links go to the canonical paths.
  • Remaining redirects are enumerated in the PR with a keep-reason each.

Out of scope (this feature)

  • The two coexisting rich-text stacks (Tiptap + Lexical in @sustentus/ui) — real debt, but a UI-package consolidation with behaviour risk; flag as a future chore, don't fold in here.
  • Dead exports inside packages/services (blob helpers etc.) — services-side sweep, separate chore.

Notes for Define

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.md

Stub: Remove the quote PDF field

  • feature-slug: quote-pdf-removal
  • scope: web-correctness-fixes
  • personas: Customer, CSM
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 6 of 7

Problem

The 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).

Proposed change

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.

Acceptance criteria (rough)

  • No pdf field on the quote model; migration unsets it on existing documents.
  • No quote surface (finances lists/detail, workspace quote section, emails) renders a PDF link; nothing 404s or shows an empty affordance.
  • The docs' service-journey quote page no longer promises a PDF document.
  • Invoice surfaces are untouched (see open question).

Out of scope (this feature)

  • Building real document generation — the payments project owns it.
  • The invoice pdf field — unless Q1 says otherwise.

Open questions

  • Q1: The invoice model has the same disease (its 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.

Notes for Define

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.

Answers from Jamie — interrogation 2026-08-17

Recorded by the intake-easy-features session; these rulings bind Define.

  • Q1 (invoice pdf field): remove both — this run removes the dead 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.md

Stub: One search-and-pagination pattern across the app

  • feature-slug: search-and-pagination-standardisation
  • scope: web-correctness-fixes
  • personas: Admin, CSM, SDM, Expert, Vendor, Customer
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: async-params-and-build-errors
  • sequence: 3 of 7

Problem

Beyond 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."

Proposed change

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.

Acceptance criteria (rough)

  • An inventory of list surfaces and their current search/paging wiring exists in the run (the audit output), with each classified conforming/outlier.
  • Every list with a search box actually filters, and every pager actually pages — verified per surface.
  • One documented convention (URL params ↔ PaginationOpts ↔ query) in CONVENTIONS.md; the outliers found are converged or explicitly ticketed with a reason.
  • Text search performance is sane: any regex search over an unindexed field on a hot collection is flagged with its index need (coordinate with the db-audit intake for anything destructive).

Out of scope (this feature)

  • Full-text search infrastructure (Atlas Search etc.) — improvement notes only, unless the audit proves current search unusable.
  • Access-control scoping (the other batch).

Open questions

  • Q1: Case-insensitive regex vs MongoDB text indexes vs Atlas Search — the audit should end with a recommendation for Jamie rather than a silent choice.

Notes for Define

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.

Answers from Jamie — interrogation 2026-08-17

Recorded by the intake-easy-features session; these rulings bind Define.

  • Q1 (search backend, preliminary steer): MongoDB text indexes — no new infrastructure; the audit validates per-collection fit and may still argue otherwise with evidence.

_done/setup-checklist-data-layer.md

Stub: Move the setup checklist onto the shared data layer

  • feature-slug: setup-checklist-data-layer
  • scope: web-correctness-fixes
  • personas: Admin
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 5 of 7

Problem

The 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."

Proposed change

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.

Acceptance criteria (rough)

  • No connectDB/getModel call remains in apps/web; the checklist reads and writes go through a @sustentus/services/server service.
  • The model carries the standard plugins (tenant scoping included) and is classified in demo-reset/policy.ts (purged or kept — deliberately).
  • Toggling a checklist item updates /admin/settings/setup and the admin dashboard readiness without a reload.
  • Existing checklist data still reads correctly (migration if the raw documents need shaping).

Out of scope (this feature)

  • Changing checklist content or the SETUP_CHECKLIST_JSON mechanism.
  • The wider revalidation-drift sweep (stub 4 owns it).

Notes for Define

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.md

Stub: Fix the vendor dashboard's permanently-zero headline tiles

  • feature-slug: vendor-headline-tiles
  • scope: web-correctness-fixes
  • personas: Vendor
  • initiative: Refine the bridge / objective: Q2-2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 1 of 7

Problem

The 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."

Proposed change

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.

Acceptance criteria (rough)

  • A vendor with real activity sees non-zero, correct headline figures; a vendor with none sees an honest empty/"not yet reported" state, not 0s presented as fact.
  • Each headline figure states its period and basis (consistent with the KPI dictionary's claim surface).
  • The NRR/GRR ruling is applied: the figures are either removed or given real definitions — no undefined numbers remain on the dashboard.
  • No other dashboard section regresses.

Out of scope (this feature)

  • The vendor metrics programme's remaining families (M1–M10) — .icm/intake/vendor-metrics/ owns those; this stub must not quietly implement them.
  • New headline KPIs.

Open questions

  • Q1: The NRR/GRR ruling itself — delete from the dashboard, or define and keep? This is one of the four decisions blocking the vendor-metrics batch (.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.
  • Q2: What exactly is "MRR" for a vendor here given there's no billing — active service revenue proxy, or drop the tile until vendor-metrics M8 lands? Recommend ruling rather than inventing a definition in a fix stub.

Notes for Define

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/.

Answers from Jamie — interrogation 2026-08-17

Recorded by the intake-easy-features session; these rulings bind Define.

  • Q1 (NRR/GRR): define and keep — write real NRR/GRR definitions now and keep the tiles honest (also recorded against the vendor-metrics batch's stub-reconciliation so it unblocks).
  • Q2 (vendor MRR): show it from active service revenue — a monthly-normalised figure from active engagements' contracted value, labelled honestly, until the money-states stub defines the real revenue reads.