Practitionist / Practitionist/familiarise_web

Post-MVP rails sweep: viewer-zone formatting (34 files) and typed refusals (7 actions, 30 routes) in one bucket

Open
#1,663 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
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.tsx
  • app/dashboard/consultant/[consultantId]/(features)/documents/DocumentsTab.tsx
  • app/dashboard/consultant/[consultantId]/(features)/earnings/EarningsSummaryPanel.tsx
  • app/dashboard/consultant/[consultantId]/(features)/recordings/components/RecordingCard.tsx
  • app/dashboard/consultee/[consulteeId]/(features)/home/HomeTab.tsx
  • app/dashboard/organization/[orgId]/appointments/AppointmentsPageClient.tsx
  • app/dashboard/organization/[orgId]/documents/DocumentsClient.tsx
  • app/dashboard/organization/[orgId]/recordings/RecordingsClient.tsx
  • app/dashboard/organization/[orgId]/reimbursements/page.tsx
  • app/dashboard/staff/[staffId]/(features)/home/HomePageClient.tsx
  • app/explore/experts/[consultantId]/components/SubscriptionPricingToggle.tsx
  • app/form/onboarding/components/experience/CertificationsSection.tsx
  • app/form/onboarding/components/experience/WorkExperienceSection.tsx
  • AppointmentList.tsx
  • AppointmentRow.tsx
  • components/admin/WaitlistManagement.tsx
  • components/appointments/AppointmentCalendar.tsx
  • components/appointments/AppointmentRow.tsx
  • components/appointments/AppointmentSheet.tsx
  • components/appointments/DayGroupHeader.tsx
  • components/appointments/detail/AppointmentDetailClient.tsx
  • components/appointments/detail/AppointmentDocumentsList.tsx
  • components/appointments/detail/RescheduleProposalCard.tsx
  • components/appointments/HeldSlotBadge.tsx
  • components/appointments/NextUpHero.tsx
  • components/appointments/SessionTimeline.tsx
  • components/chat/CustomMessage.tsx
  • components/dashboard/shared/DocumentsPage.tsx
  • components/scheduling/SessionReleasePicker.tsx
  • components/scheduling/UnifiedCalendar.tsx
  • DayGroupHeader.tsx
  • EarningsSummaryPanel.tsx
  • NextUpHero.tsx
  • page.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.ts
  • actions/stream/chat/stream.action.ts
  • actions/stream/chat/user.action.ts
  • actions/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.ts
  • app/api/admin/disputes/[disputeId]/route.ts
  • app/api/admin/erasure-requests/[id]/process/route.ts
  • app/api/admin/maintenance/route.ts
  • app/api/admin/payments/[paymentId]/route.ts
  • app/api/admin/payouts/[id]/route.ts
  • app/api/admin/reconcile-ledgers/route.ts
  • app/api/admin/tds/route.ts
  • app/api/admin/users/[userId]/route.ts
  • app/api/admin/verification/[verificationId]/route.ts
  • app/api/admin/waitlist/broadcast/route.ts
  • app/api/announcements/[id]/route.ts
  • app/api/announcements/route.ts
  • app/api/appointments/[appointmentId]/cancel/preview/route.ts
  • app/api/appointments/[appointmentId]/cancel/route.ts
  • app/api/appointments/[appointmentId]/documents/[documentId]/download/route.ts
  • app/api/appointments/[appointmentId]/documents/[documentId]/route.ts
  • app/api/appointments/[appointmentId]/documents/consultant/route.ts
  • app/api/appointments/[appointmentId]/documents/route.ts
  • app/api/appointments/[appointmentId]/reschedule/route.ts
  • app/api/appointments/[appointmentId]/support/route.ts
  • app/api/bookings/classes/[classId]/route.ts
  • app/api/bookings/classes/[classId]/validate/route.ts
  • app/api/bookings/classes/crud-with-plan/[classId]/route.ts
  • app/api/bookings/classes/crud-with-plan/route.ts
  • app/api/bookings/consultations/[consultationId]/route.ts
  • app/api/bookings/consultations/[consultationId]/validate/route.ts
  • app/api/bookings/consultations/route.ts
  • app/api/bookings/subscriptions/[subscriptionId]/route.ts
  • app/api/bookings/subscriptions/[subscriptionId]/validate/route.ts

Done when

  • No format( / toLocale* on an absolute instant outside lib/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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.