Practitionist / Practitionist/familiarise_web
Post-MVP rails sweep: viewer-zone formatting (34 files) and typed refusals (7 actions, 30 routes) in one bucket
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4
- Forks
- 4
- Avg merge
- 9h 12m
- Merged PRs (30d)
- 238
Description
Why one issue
Two rails landed on 2026-09-15 and each was applied only to the sites that were visibly failing, on purpose (the Stream MVP is the priority): the typed Refusal (#1655, lib/errors/refusal.ts, docs/errors/01-refusals.md) and the viewer-zone formatting rail (#1657, lib/time/viewer-zone.ts, docs/booking/06-booking-lifecycle.md "Times are rendered in the viewer's zone"). This issue is the single bucket for finishing both sweeps after the MVP, so neither turns into a trickle of one-file PRs. Both are mechanical against an existing rail; both are Sonnet-tier work; both should ship as ONE PR each (or one combined), not per file.
Sweep A — bare date formatting on absolute instants (hydration + wrong-zone risk)
Every file below formats a Date with date-fns format(), toLocaleDateString, or a local-zone getter. On the server that is UTC and in the browser it is the user's zone, so any of them that server-renders can throw React #418 and re-render, and any of them shows the wrong wall-clock time to a user whose zone is not the runtime's. Replace with formatInViewerZone / formatForViewer and thread the zone from getViewerZone() (RSC) or useViewerZone() (client-only), exactly as AppointmentRow/NextUpHero do on #1657. The relative labels (getProximityLabel, CountdownBadge, DayGroupHeader's isToday, AppointmentList.dayKey) are the same bug at day boundaries and belong here too.
app/dashboard/consultant/[consultantId]/(features)/analytics/AnalyticsPageClient.tsxapp/dashboard/consultant/[consultantId]/(features)/documents/DocumentsTab.tsxapp/dashboard/consultant/[consultantId]/(features)/earnings/EarningsSummaryPanel.tsxapp/dashboard/consultant/[consultantId]/(features)/recordings/components/RecordingCard.tsxapp/dashboard/consultee/[consulteeId]/(features)/home/HomeTab.tsxapp/dashboard/organization/[orgId]/appointments/AppointmentsPageClient.tsxapp/dashboard/organization/[orgId]/documents/DocumentsClient.tsxapp/dashboard/organization/[orgId]/recordings/RecordingsClient.tsxapp/dashboard/organization/[orgId]/reimbursements/page.tsxapp/dashboard/staff/[staffId]/(features)/home/HomePageClient.tsxapp/explore/experts/[consultantId]/components/SubscriptionPricingToggle.tsxapp/form/onboarding/components/experience/CertificationsSection.tsxapp/form/onboarding/components/experience/WorkExperienceSection.tsxAppointmentList.tsxAppointmentRow.tsxcomponents/admin/WaitlistManagement.tsxcomponents/appointments/AppointmentCalendar.tsxcomponents/appointments/AppointmentRow.tsxcomponents/appointments/AppointmentSheet.tsxcomponents/appointments/DayGroupHeader.tsxcomponents/appointments/detail/AppointmentDetailClient.tsxcomponents/appointments/detail/AppointmentDocumentsList.tsxcomponents/appointments/detail/RescheduleProposalCard.tsxcomponents/appointments/HeldSlotBadge.tsxcomponents/appointments/NextUpHero.tsxcomponents/appointments/SessionTimeline.tsxcomponents/chat/CustomMessage.tsxcomponents/dashboard/shared/DocumentsPage.tsxcomponents/scheduling/SessionReleasePicker.tsxcomponents/scheduling/UnifiedCalendar.tsxDayGroupHeader.tsxEarningsSummaryPanel.tsxNextUpHero.tsxpage.tsx
Sweep B — refusals still thrown as plain Errors
Server actions that throw new Error("Unauthorized …") are captured by Next's onRequestError; routes that answer a hand-built 4xx while also calling Sentry.captureException produce an error-level issue for a modelled "no". Convert each to a Refusal answered through apiError (routes) or returned as an ActionResult (actions), with a userMessage for the toast and a devMessage for the log — the shape #1655 established. Verify each candidate against the code before converting; some 4xx sites are genuine faults and should stay captured.
Server actions (count of refusal-shaped throws):
actions/stream/chat/event-channel.action.tsactions/stream/chat/stream.action.tsactions/stream/chat/user.action.tsactions/stream/chat/member.action.ts
Route handlers that both capture to Sentry and answer a 4xx (candidates, not verdicts):
app/api/admin/compliance/tds-return/route.tsapp/api/admin/disputes/[disputeId]/route.tsapp/api/admin/erasure-requests/[id]/process/route.tsapp/api/admin/maintenance/route.tsapp/api/admin/payments/[paymentId]/route.tsapp/api/admin/payouts/[id]/route.tsapp/api/admin/reconcile-ledgers/route.tsapp/api/admin/tds/route.tsapp/api/admin/users/[userId]/route.tsapp/api/admin/verification/[verificationId]/route.tsapp/api/admin/waitlist/broadcast/route.tsapp/api/announcements/[id]/route.tsapp/api/announcements/route.tsapp/api/appointments/[appointmentId]/cancel/preview/route.tsapp/api/appointments/[appointmentId]/cancel/route.tsapp/api/appointments/[appointmentId]/documents/[documentId]/download/route.tsapp/api/appointments/[appointmentId]/documents/[documentId]/route.tsapp/api/appointments/[appointmentId]/documents/consultant/route.tsapp/api/appointments/[appointmentId]/documents/route.tsapp/api/appointments/[appointmentId]/reschedule/route.tsapp/api/appointments/[appointmentId]/support/route.tsapp/api/bookings/classes/[classId]/route.tsapp/api/bookings/classes/[classId]/validate/route.tsapp/api/bookings/classes/crud-with-plan/[classId]/route.tsapp/api/bookings/classes/crud-with-plan/route.tsapp/api/bookings/consultations/[consultationId]/route.tsapp/api/bookings/consultations/[consultationId]/validate/route.tsapp/api/bookings/consultations/route.tsapp/api/bookings/subscriptions/[subscriptionId]/route.tsapp/api/bookings/subscriptions/[subscriptionId]/validate/route.ts
Done when
- No
format(/toLocale*on an absolute instant outsidelib/time/;grep -rln 'from "date-fns"' --include=*.tsx components app | xargs grep -l "format("returns only files that format durations or already pass a zone. - No server action throws a refusal-shaped
Error; no route answers a 4xx for a modelled refusal while also capturing it. - One pin per rail already exists (
__tests__/time/viewer-zone.test.ts,__tests__/errors/refusal-rails.test.ts); do not add per-file tests.
Part of the post-MVP hardening; see #1645 (the sweeps-treat-permanent-as-transient lesson), #1655, #1657.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/time/viewer-zone.ts, lib/errors/refusal.ts, and the existing rails in tests/time/viewer-zone.test.ts and tests/errors/refusal-rails.test.ts. Audit the listed dashboard/components files and actions/routes, verifying each candidate before applying the established patterns. Run both pin tests and the specified grep checks; done means the two sweeps meet every stated condition without adding per-file tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, react, typescript
- Domain
- api, backend, frontend, full-stack, observability
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 42/100