Post-MVP rails sweep: viewer-zone formatting (34 files) and typed refusals (7 actions, 30 routes) in one bucket
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 42/100
- Issue 类型
- 重构
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- next.js, react, typescript
- 领域
- api, backend, frontend, full-stack, observability
调研方向
从 lib/time/viewer-zone.ts、lib/errors/refusal.ts,以及 tests/time/viewer-zone.test.ts 和 tests/errors/refusal-rails.test.ts 中现有的 rails 开始。审查列出的 dashboard/components 文件和 actions/routes,在应用既定模式之前验证每个候选项。运行两个 pin tests 和指定的 grep checks;完成意味着两轮扫描满足所有已声明的条件,且不添加逐文件测试。
由索引模型根据 Issue 内容生成。
描述
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.
- 主要语言
- TypeScript
- 星标
- 4
- 派生
- 4
- 平均合并
- 8 小时 53 分钟
- 30 天内合并 PR
- 232
环境准备
我们还没有检查这个项目的环境配置文件。先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
Practitionist/familiarise_web 的其他 Issue
-
infrastructure launch: scale production
难度 2/5 半天 新手友好度 72/100
Practitionist/familiarise_web#1772 ·
维护者通常 1 天内回复
-
reliability tech-debt
难度 2/5 1-3 小时 新手友好度 88/100
Practitionist/familiarise_web#1630 ·
维护者通常 1 天内回复
-
moderation
难度 2/5 1-3 小时 新手友好度 76/100
Practitionist/familiarise_web#1604 ·
维护者通常 1 天内回复
-
Enterprise launch: post-mvp notifications tech-debt
难度 2/5 1-3 小时 新手友好度 84/100
Practitionist/familiarise_web#1452 ·
维护者通常 1 天内回复
-
bug dashboard Enterprise launch: post-mvp
难度 2/5 1-3 小时 新手友好度 75/100
Practitionist/familiarise_web#1445 ·
维护者通常 1 天内回复
查看 Practitionist/familiarise_web 的全部 Issue
相似的 Issue
-
area/frontend good first issue kind/cooldown
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 85/100
voidzero-dev/oxc-angular-compiler#511 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
langchain-ai/deepagentsjs#898 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 92/100
anomalyco/models.dev#8509 · 2 条评论 ·
维护者通常 1 天内回复
-
bug documentation P2 UI/UX
难度 2/5 1-3 小时 新手友好度 85/100
维护者通常 1 天内回复