Skip to content

feat: recurring asset reminders (auto-repeat on a cadence) - #2685

Open
carlosvirreira wants to merge 6 commits into
mainfrom
feat/recurring-reminders
Open

feat: recurring asset reminders (auto-repeat on a cadence)#2685
carlosvirreira wants to merge 6 commits into
mainfrom
feat/recurring-reminders

Conversation

@carlosvirreira

@carlosvirreira carlosvirreira commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Closes #2659. Also addresses the long-standing recurrence asks in #483, #1445 and #2520 (calibration / service-interval workflows that today require manually re-creating a reminder after each fire).

What

Reminders can now repeat on a cadence: daily, weekly, every 2 weeks, monthly, every 3 months, every 6 months, or yearly, with an optional end date. One-time reminders are unchanged and stay available to everyone; recurrence is a paid-tier capability (see Gating).

Design: one row per series, alertDateTime = next occurrence

A recurring reminder is the existing AssetReminder row. When it fires, the worker advances alertDateTime in place to the next occurrence and schedules the next job. This keeps every existing surface correct with no forked logic:

  • home "Upcoming reminders" widget and asset-overview card (gte: now filters)
  • advanced asset-index "Upcoming Reminder" column and its CSV export
  • the Pending / Reminder sent status badge
  • the edit guard (active series stays editable) and the cancel-on-delete guard

Fire history remains visible as asset notes (one per fire, now including the next scheduled date).

Scheduling: self-rescheduling chain, no cron

noScheduling: true is untouched. Recurrence is a self-rescheduling sendAfter chain, the same in-house pattern the audit reminder chain (24h → 4h → 1h → overdue) and the booking checkin → overdue hop already use, hardened with:

  • singletonKey + retryLimit 3 on all recurring jobs (one-shot jobs keep today's exact semantics)
  • compare-and-swap advance (updateMany guarded on the old alertDateTime) so concurrent workers/reconciliation/edits can never double-advance (relevant under bluegreen deploys where two machines briefly poll)
  • advance-before-notify ordering: a retried job can never double-email
  • stale-job guard: a job superseded by a newer schedule is skipped; this also closes a pre-existing double-fire window when an edit's cancel silently failed
  • orphan recovery on retry: if the advance committed but scheduling the next job failed transiently, the retry re-arms it
  • boot reconciliation: at every deploy/boot, series whose chain died (crash, jobs archived during long downtime) are re-armed. 1-hour grace so healthy in-flight fires are never hijacked; per-row fault isolation so one bad row can't block the sweep

pg-boss 9 retention is safe for far-future occurrences: keepUntil anchors to startAfter, so a job scheduled months out is not reaped (verified in the vendored source).

Timezone: the cadence is wall-clock stable in the IANA zone captured from client hints at create/edit (luxon; DST-correct). Month-end anniversaries clamp (Jan 31 → Feb 28 and the advanced date becomes the new anchor) — intended v1 behavior, covered by tests. Catch-up policy: occurrences missed during downtime are skipped, never bursted.

Gating

  • One-time reminders remain free.
  • Recurrence requires the new canUseRecurringReminders tier flag (TierLimit default false, enabled for tier_1/tier_2; CustomTierLimit default true), following the canHideShelfBranding precedent, asserted server-side at both mutation entry points.
  • Edits only assert when recurrence is being added or changed vs the stored row, so a downgraded workspace can still edit other fields, turn recurrence off, or delete.
  • Downgrade behavior: the series pauses at its next fire (that occurrence still notifies; nothing further is scheduled; a note explains why). Upgrading + editing re-arms it.
  • Self-host (ENABLE_PREMIUM_FEATURES=false): everything enabled, per the existing convention.
  • Free-tier UI: the Repeat select renders locked with an upgrade link (workspace edit-form precedent); hidden inputs carry the stored cadence so plain edits round-trip safely.

Migrations

Two migrations mirroring the canImportNRM / canHideShelfBranding pair: enum + 4 nullable AssetReminder columns + both tier-table booleans, then the UPDATE enabling paid tiers. Verified on a clean Postgres 16: prisma migrate deploy applies cleanly and migrate status reports zero drift; a live-Prisma script additionally exercised the reconcile query and the CAS advance against the real database.

Drive-by fixes (each on a line this change already touches)

  • createAssetReminder now runs assertAssetsBelongToOrg before creating (org-scope-user-supplied-ids rule: create paths too)
  • reminders pagination totalPages divided by the raw per_page param → Infinity when absent; now uses the cookie-resolved perPage
  • asset-overview reminders card ordered desc (showed the two furthest-future reminders instead of the next two)
  • reminder schema's future-check evaluated new Date() once at module load; now validated at parse time

Deliberate non-goals (v1)

  • Duplicating an asset does not copy its reminders (kept as-is by design)
  • No per-occurrence editing (edit = edit the series), no calendar surface, no mobile surface (reminders don't exist on mobile today), no maintenance module (separate scope)
  • recordEvent/ActivityAction wiring deferred: no reminder events exist today; matching existing behavior

Tests

First tests for the asset-reminder module (44 new): recurrence math (DST both directions, month-end clamp, catch-up skip, end-date termination, invalid-zone fallback, interval clamp), chain advance/reconcile (CAS, grace window, per-row isolation, tier pause, fail-open), worker handler (advance-before-notify order, stale-job guard, orphan re-arm, rethrow path, one-shot unchanged), form schema parsing (empty endsAt, missing repeat, past dates), and the endsAt timezone round-trip. pnpm webapp:validate green (3073 tests).

Verification honesty

Statically verified + live-DB-verified as above. Not click-through-verified in a browser: the shared dev/QA database cannot take this migration (it is on a diverged schema), so reviewers should expect to exercise the dialog in their own environment. The UI changes follow existing primitives (Radix Select, locked-control precedent, DateS).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added recurring asset reminders (daily–yearly) with timezone-aware repeat settings and optional “Ends on”.
    • Recurrence now shows across the experience: lists, cards, upcoming reminders, advanced asset tooltips, and reminder emails (including next occurrence).
  • Bug Fixes
    • Improved recurring reminders’ delivery and edit behavior, including correct “Pending”/editable status across series state and safer retries.
  • Chores
    • Added recurring schema, client/server validation, and expanded automated test coverage; enabled recurring reminders for eligible paid tiers.

Reminders can now repeat on a cadence (daily, weekly, every 2 weeks, monthly,
every 3/6 months, yearly) with an optional end date. Closes #2659; also
addresses the recurring asks in #483, #1445 and #2520.

Design: one AssetReminder row per series. The worker advances alertDateTime to
the next occurrence in place on each fire, so every upcoming surface (home
widget, overview card, advanced-index column, CSV export) and the existing
status/edit/cancel guards stay correct without forked logic. Scheduling stays
on pg-boss sendAfter one-shots (noScheduling untouched) as a self-rescheduling
chain, the same pattern the audit reminder chain already uses, hardened with:

- singletonKey dedupe + retryLimit 3 on all recurring jobs
- compare-and-swap row advance (safe under bluegreen double-workers)
- stale-job guard in the worker (also closes the pre-existing double-fire
  window after a failed cancel on edit)
- orphan-recovery on retry when the advance committed but scheduling failed
- boot-time reconciliation (1h grace, per-row fault isolation) as the no-cron
  safety net for dead chains

Recurrence math is wall-clock stable in the timezone captured from client
hints at create/edit (luxon; DST-correct, month-end clamps Jan 31 -> Feb 28).
Catch-up policy: occurrences missed during downtime are skipped, never
bursted.

Gating: one-shot reminders stay free. Recurrence requires the new
canUseRecurringReminders tier flag (TierLimit false by default, enabled for
tier_1/tier_2, CustomTierLimit true), asserted server-side at both mutation
entry points. Edits only assert when recurrence is added or changed, so
downgraded workspaces can still edit other fields, turn recurrence off or
delete. If a workspace loses the capability, the series pauses at its next
fire (notifies, does not reschedule) and can be resumed after upgrading.

Migrations follow the canImportNRM/canHideShelfBranding two-file precedent
and were verified with prisma migrate deploy + migrate status (zero drift)
against a clean Postgres 16.

Drive-by fixes on touched lines: org-scope validation (assertAssetsBelongToOrg)
in createAssetReminder per the org-scope-user-supplied-ids rule; reminders
pagination totalPages divided by the raw per_page param (Infinity without it);
asset-overview reminder card ordered desc (showed furthest-future instead of
next); reminder schema future-check evaluated new Date() at module load.

Deliberate non-goals for v1: duplicating an asset does not copy its reminders,
no per-occurrence editing, no calendar surface, no mobile surface (reminders
do not exist on mobile today), no maintenance module (own scope).

Tests: recurrence math (DST both directions, month-end, catch-up, end date),
chain advance/reconcile, worker handler (order, guards, retry paths), form
schema parsing (empty endsAt, missing repeat, past dates), and the endsAt
timezone round-trip. First tests for the asset-reminder module.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🩺 React Doctor — webapp

Findings on the files changed by this PR:

  • 0 errors
  • 1 warning — advisory
⚠️ 1 warnings (click to expand)
  • react-doctor/no-derived-useState (1)
    • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx:179

Run locally with pnpm webapp:doctor for a full scan, or cd apps/webapp && pnpm exec react-doctor . --diff for the same diff-only view.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4744100e01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/webapp/app/modules/asset-reminder/chain.server.ts
Comment thread apps/webapp/app/modules/asset-reminder/utils.server.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/modules/asset-reminder/service.server.ts (1)

309-335: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Set explicit 4xx status for intentional edit blocks.

These ShelfErrors use cause: null without status, so they default to 500 even though they are expected user/business-rule denials.

Proposed fix
       throw new ShelfError({
         cause: null,
         message: "Edit is not allowed for this reminder.",
         label: "Asset Reminder",
         additionalData: { id },
         shouldBeCaptured: false,
+        status: 403,
       });
         throw new ShelfError({
           cause: null,
           title: "Not allowed",
           message:
             "Recurring reminders are not available on your workspace's current plan. Please upgrade your subscription to unlock this feature, or set a one-time reminder instead.",
           label: "Tier",
           additionalData: { id, organizationId },
           shouldBeCaptured: false,
+          status: 403,
         });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/service.server.ts` around lines 309 -
335, The intentional edit blocks in service.server.ts are throwing ShelfError
from the reminder edit flow without an explicit client-error status, so they
default to a 500. Update the ShelfError instances in the reminder validation
logic around the edit restrictions to set an explicit 4xx status (for example,
400/403 as appropriate) while keeping the existing message, label, and
additionalData fields. Use the surrounding reminder edit checks and the
recurring-reminder branch to locate the two throws.
🧹 Nitpick comments (12)
apps/webapp/app/modules/asset-reminder/service.server.ts (1)

36-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add JSDoc for the changed exported service APIs.

createAssetReminder and editAssetReminder now expose recurrence/tier behavior; document parameters, return value, and ShelfError cases. As per coding guidelines, “every exported function, component, and type must have a JSDoc comment describing parameters, return values, and thrown errors.”

Also applies to: 271-293

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/service.server.ts` around lines 36 -
56, The exported service APIs in createAssetReminder and editAssetReminder need
JSDoc comments. Add docs above each function describing the parameters
(including recurrence/tier-related inputs), the return value, and the ShelfError
cases they can throw; make sure the comments cover the updated behavior in the
asset-reminder service module and match the project’s exported-function
documentation rule.

Source: Coding guidelines

apps/webapp/app/routes/_layout+/assets.$assetId.tsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required file/action JSDoc.

The file starts with an import, and the exported action handler is undocumented. As per coding guidelines, every TypeScript file must start with a JSDoc purpose block and every exported function must have JSDoc.

Also applies to: 272-272

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/routes/_layout`+/assets.$assetId.tsx at line 1, The route
module currently starts with an import and the exported action handler lacks
documentation; add a JSDoc purpose block at the top of this TypeScript file and
a JSDoc comment for the exported action function in assets.$assetId.tsx. Use the
existing action symbol and module entrypoint as anchors, and ensure the docs
describe the file’s purpose and the handler’s behavior without changing logic.

Source: Coding guidelines

apps/webapp/app/routes/_layout+/assets.$assetId.reminders.tsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required route JSDoc.

This file starts with an import and the exported loader/action handlers are undocumented. As per coding guidelines, every TypeScript file must start with a JSDoc purpose block and every exported function must have JSDoc.

Also applies to: 23-23, 74-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/routes/_layout`+/assets.$assetId.reminders.tsx at line 1, The
route module is missing the required JSDoc documentation, so add a file-level
purpose block at the top of the asset reminders route and JSDoc comments for
each exported handler in this module, especially the loader and action
functions. Update the documentation near the loader/action symbols so they
describe their purpose and align with the project’s TypeScript JSDoc convention.

Source: Coding guidelines

apps/webapp/app/routes/_layout+/reminders._index.tsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required route JSDoc.

This file starts with an import and the exported loader/action handlers are undocumented. As per coding guidelines, every TypeScript file must start with a JSDoc purpose block and every exported function must have JSDoc.

Also applies to: 23-23, 74-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/routes/_layout`+/reminders._index.tsx at line 1, The route
module is missing the required JSDoc purpose block at the top of the file, and
the exported `loader` and `action` handlers are undocumented. Add a brief
file-level JSDoc describing the route purpose before the imports, then add JSDoc
comments for each exported function (`loader` and `action`) in this route
module, following the existing coding guidelines.

Source: Coding guidelines

apps/webapp/app/modules/asset-reminder/recurrence.ts (2)

82-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add full @param/@returns JSDoc to these exported helpers.

isRecurringReminder, repeatValueFromRecurrence, describeRecurrence, and resolveRecurrenceZone each have only a one-line or partial doc comment, missing @param/@returns tags (unlike getNextOccurrence below, which is fully documented).

As per coding guidelines, "every exported function, component, and type must have a JSDoc comment describing parameters, return values, and thrown errors."

Also applies to: 89-93, 120-143, 145-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/recurrence.ts` around lines 82 - 87,
Add full JSDoc to the exported helpers in recurrence.ts: isRecurringReminder,
repeatValueFromRecurrence, describeRecurrence, and resolveRecurrenceZone should
each document their parameters and return values with `@param` and `@returns` tags,
matching the style already used by getNextOccurrence. Keep the existing symbols
and behavior unchanged; only expand the comments so the exported function docs
are complete and consistent.

Source: Coding guidelines


94-106: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Fallback to "monthly" risks silently rewriting stored cadence for non-preset data.

The comment on this function explicitly anticipates "future custom cadences" as unit+interval data that won't match any preset. When that happens, this fallback returns "monthly" regardless of the actual stored unit/interval, so the edit dialog would show "Monthly" and a no-op save would silently change the reminder's real cadence.

♻️ Proposed fix: fall back to same-unit preset instead of a fixed one
   const match = Object.entries(REMINDER_REPEAT_PRESETS).find(
     ([, preset]) =>
       preset.unit === reminder.recurrenceUnit &&
       preset.interval === reminder.recurrenceInterval
   );
-
-  return (match?.[0] as ReminderRepeatValue) ?? "monthly";
+  if (match) return match[0] as ReminderRepeatValue;
+
+  // No exact preset match (custom cadence). Falling back to a fixed preset
+  // like "monthly" would silently rewrite the stored cadence on a no-op
+  // save — pick the closest same-unit preset so at least the unit survives.
+  const sameUnitFallback = Object.entries(REMINDER_REPEAT_PRESETS).find(
+    ([, preset]) => preset.unit === reminder.recurrenceUnit
+  );
+  return (sameUnitFallback?.[0] as ReminderRepeatValue) ?? "monthly";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/recurrence.ts` around lines 94 - 106,
The fallback in repeatValueFromRecurrence currently returns a fixed "monthly"
value for any non-preset recurrence, which can misrepresent future custom
cadences and silently rewrite stored data. Update repeatValueFromRecurrence to
preserve the same recurrenceUnit/recurrenceInterval semantics by deriving a
fallback from the reminder’s actual unit rather than hardcoding "monthly", and
keep the preset lookup via REMINDER_REPEAT_PRESETS for known values. Ensure the
edit dialog’s value mapping remains stable for custom cadence data in
AssetReminder recurrence handling.
apps/webapp/app/utils/subscription.server.ts (1)

432-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add @param docs for organizationId and organizations.

The JSDoc documents @throws but omits parameter descriptions.

As per coding guidelines, "every exported function, component, and type must have a JSDoc comment describing parameters, return values, and thrown errors."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/utils/subscription.server.ts` around lines 432 - 468, The
exported assertUserCanUseRecurringReminders function already documents the throw
behavior, but it is missing JSDoc `@param` entries for organizationId and
organizations. Update the existing JSDoc above
assertUserCanUseRecurringReminders to describe both parameters clearly, keeping
the current `@throws` note intact and matching the project rule that exported
functions document parameters, return values, and errors.

Source: Coding guidelines

apps/webapp/app/components/assets/asset-reminder-cards.tsx (1)

58-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cache describeRecurrence(reminder) instead of calling it twice.

Sibling components (reminders-table.tsx, advanced-asset-columns.tsx) compute the recurrence label once and reuse it; here it's invoked twice per reminder row.

♻️ Proposed fix
       {reminders.map((reminder) => {
         const slicedTeamMembers = reminder.teamMembers.slice(0, 10);
         const remainingTeamMembers =
           reminder.teamMembers.length - slicedTeamMembers.length;
         const isAlreadySent = new Date() > new Date(reminder.alertDateTime);
+        const recurrenceLabel = describeRecurrence(reminder);
 
         return (
           <div key={reminder.id} className="border-b px-4 py-3">
@@
             <p className="mb-2">
               <DateS date={reminder.alertDateTime} includeTime />
             </p>
-            {describeRecurrence(reminder) ? (
+            {recurrenceLabel ? (
               <p className="mb-2 flex items-center gap-1 text-xs text-gray-500">
                 <RepeatIcon className="size-3" aria-hidden="true" />
-                {describeRecurrence(reminder)}
+                {recurrenceLabel}
               </p>
             ) : null}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/components/assets/asset-reminder-cards.tsx` around lines 58 -
63, The recurrence label in asset-reminder-cards.tsx is computed twice by
calling describeRecurrence(reminder) in the conditional and again when
rendering. Update the AssetReminderCards rendering logic to store the result in
a local variable and reuse it for both the truthy check and the displayed text,
following the same pattern used in reminders-table.tsx and
advanced-asset-columns.tsx.
apps/webapp/app/entry.server.tsx (1)

85-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: mixed logging conventions for reconciliation results.

The success path uses console.log while the failure path right below uses Logger.error(new ShelfError(...)). For consistency and structured observability (searchable/filterable boot logs), consider routing the success message through Logger.info as well.

🔧 Suggested tweak
       .then(({ scanned, rearmed }) => {
         if (scanned > 0) {
-          console.log(
+          Logger.info(
             `Recurring reminders reconciled: ${rearmed}/${scanned} chains re-armed`
           );
         }
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/entry.server.tsx` around lines 85 - 109, The reconciliation
success path in entry.server.tsx is using console.log while the failure path
uses Logger.error with ShelfError, so make the logging consistent. Update the
success branch in reconcileRecurringReminders to use Logger.info with the same
“Recurring reminders reconciled” message instead of console.log, keeping the
existing structured logging pattern alongside Logger.error and ShelfError.
apps/webapp/app/modules/asset-reminder/chain.server.ts (1)

190-241: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Reconciliation candidate set has no upper bound and can grow unboundedly for permanently tier-paused chains.

Two related scalability gaps in reconcileRecurringReminders:

  1. findMany has no take/pagination limit — as the number of recurring reminders grows, boot-time reconciliation performs an ever-larger unbounded query and then processes rows sequentially in a for loop (one DB round-trip per row via advanceRecurringReminder), adding to boot latency on every deploy.
  2. When advanceRecurringReminder returns paused: true (org lost tier access), alertDateTime is never advanced and recurrenceUnit/recurrenceEndsAt are left untouched. That row will keep matching this query's where clause (recurrenceUnit: { not: null }, alertDateTime: { lt: deadBefore }, endsAt null/future) on every subsequent boot indefinitely, re-running the organization.findUnique + getUserTierLimit lookup each time with no way to short-circuit until the org re-upgrades or the row is edited.

Given "Shelf deploys frequently" (per the file's own doc comment), paused chains accumulating over time will make this scan progressively more expensive at every deploy.

Consider adding a take limit with cursor/pagination for the candidate scan, and consider persisting an explicit "paused" signal (or bumping alertDateTime forward on pause) so downgraded chains stop being rescanned on every boot until something changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/chain.server.ts` around lines 190 -
241, `reconcileRecurringReminders` currently scans an unbounded candidate set
and will keep rescaning permanently paused recurring reminders on every boot.
Add pagination/`take` (with cursor or batching) to the
`db.assetReminder.findMany` query and process batches incrementally so boot-time
reconciliation stays bounded. Also update the `advanceRecurringReminder` paused
path so paused chains are marked in a way that excludes them from the existing
`where` clause in future runs, preventing repeated
`organization.findUnique`/tier checks until the reminder is changed or
reactivated.
apps/webapp/app/modules/asset-reminder/emails.tsx (1)

53-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing JSDoc on exported, signature-changed function.

assetAlertEmailText gained a new nextOccurrence parameter but has no JSDoc block describing parameters/returns. Per repo guidelines, every exported function must document its parameters and return value.

As per coding guidelines: "every exported function, component, and type must have a JSDoc comment describing parameters, return values, and thrown errors."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/emails.tsx` around lines 53 - 61, The
exported assetAlertEmailText function is missing the required JSDoc block after
its signature changed to include nextOccurrence. Add a JSDoc comment above
assetAlertEmailText documenting all parameters in AssetAlertEmailProps,
including nextOccurrence, and describing the returned email text; keep the
documentation aligned with the function’s behavior and update any return/throws
notes if applicable.

Source: Coding guidelines

apps/webapp/app/modules/asset-reminder/worker.server.test.ts (1)

1-286: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid coverage of ordering/staleness/re-arm branches — nicely done.

Mocks are all annotated with // why: comments and only mock external boundaries (db, scheduler, email, note) plus the already-separately-tested advanceRecurringReminder, consistent with repo test conventions.

One gap worth adding: a test for the "series naturally ended" outcome (advanceRecurringReminder resolving { next: null, advanced: true, paused: false }) — see the related comment on worker.server.ts (lines 121-170) about that branch currently being indistinguishable from a one-shot fire.

As per path instructions: "Every mock in tests must be accompanied by a // why: comment explaining the reason for mocking."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/worker.server.test.ts` around lines 1
- 286, Add a test in the REMINDER worker handler suite for the “series naturally
ended” path where advanceRecurringReminder resolves to { next: null, advanced:
true, paused: false }. This branch is currently not covered and should verify
the handler treats it distinctly from a one-shot reminder by asserting the
expected note/log behavior and that no re-scheduling occurs; use the existing
handler/JOB setup plus the mocked advanceRecurringReminder, sendEmail, and
createNote symbols to locate the flow.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx`:
- Around line 142-143: The repeat selection in SetOrEditReminderDialog is only
initialized once via useState(initialRepeat), so reopening the dialog or
switching to a different reminder can leave stale cadence state. Update the
state in the dialog component when the dialog opens or when reminder changes,
using the existing SetOrEditReminderDialog and repeat/setRepeat state so the
selected value always reflects the current reminder instead of the previous
session.
- Around line 58-72: The future-date validation in setOrEditReminderSchema is
using z.coerce.date() input time, which can disagree with the client-hint
timezone used later for scheduling. Move the “Please select a date in the
future” check into the timezone-aware parse/validation path for alertDateTime,
and compare against the resolved instant rather than Date.now() on the raw
coerced date. Use the existing setOrEditReminderSchema and alertDateTime
validation block as the fix point.

In `@apps/webapp/app/modules/asset-reminder/service.server.ts`:
- Around line 319-349: The recurring-reminder downgrade gate in
service.server.ts only compares recurrence unit, interval, and endsAt, but it
ignores recurrenceTimezone even though assetReminder.update writes it. Update
the recurrenceChanged check in the recurrence validation block to include
recurrence.timezone versus reminder.recurrenceTimezone so timezone-only changes
are also blocked for plans without recurring reminders.

In `@apps/webapp/app/modules/asset-reminder/utils.server.ts`:
- Around line 48-77: The recurrence builder in the asset reminder helper is
overwriting the stored series timezone with the current request’s client hint,
which can shift existing reminders and trigger unintended recurrence changes
during edits. Update the logic in the reminder parsing/helper path to prefer the
persisted recurrence timezone passed through the form/action (for example, the
existing recurrenceTimezone) when editing an existing series, and only fall back
to the client hint-derived zone for new reminders or when no stored timezone
exists. Make sure the recurrence object assembled in this helper preserves the
original timezone instead of always using getHints(request) and
IANAZone.isValidZone(...).

In `@apps/webapp/app/modules/asset-reminder/worker.server.ts`:
- Around line 217-231: The next-occurrence date formatting in the asset reminder
worker duplicates the same Luxon formatting logic used in the email recurrence
helper, so extract it into the shared recurrence module. Move the
DateTime.fromJSDate(...).setZone(...).toFormat("d LLL yyyy, HH:mm") logic into a
reusable formatRecurrenceDateTime(date, timezone) helper in ./recurrence, then
update worker.server.ts and emails.tsx to call that helper instead of formatting
inline. Keep the existing zone resolution behavior aligned with the
recurrence-related symbols already in use, such as resolveRecurrenceZone and
recurrenceLine.
- Around line 121-170: The recurring reminder flow in worker.server.ts only
distinguishes nextOccurrence and seriesPaused, so a naturally ending series is
treated like a generic one-shot reminder. Update advanceRecurringReminder to
return an explicit “series ended” signal, then consume that result in the
recurring branch here (alongside nextOccurrence and seriesPaused) so the final
fire is handled with dedicated ended messaging instead of falling through to the
default text.

In `@apps/webapp/app/routes/_layout`+/assets.$assetId.tsx:
- Around line 379-383: The recurring-reminders tier check is surfacing as a 500
because `assertUserCanUseRecurringReminders` throws a `ShelfError` without an
explicit status. Update that utility so the denial path includes `status: 403`
on the `ShelfError`, and keep the route in `assets.$assetId.tsx` unchanged
except to rely on the corrected helper behavior.

---

Outside diff comments:
In `@apps/webapp/app/modules/asset-reminder/service.server.ts`:
- Around line 309-335: The intentional edit blocks in service.server.ts are
throwing ShelfError from the reminder edit flow without an explicit client-error
status, so they default to a 500. Update the ShelfError instances in the
reminder validation logic around the edit restrictions to set an explicit 4xx
status (for example, 400/403 as appropriate) while keeping the existing message,
label, and additionalData fields. Use the surrounding reminder edit checks and
the recurring-reminder branch to locate the two throws.

---

Nitpick comments:
In `@apps/webapp/app/components/assets/asset-reminder-cards.tsx`:
- Around line 58-63: The recurrence label in asset-reminder-cards.tsx is
computed twice by calling describeRecurrence(reminder) in the conditional and
again when rendering. Update the AssetReminderCards rendering logic to store the
result in a local variable and reuse it for both the truthy check and the
displayed text, following the same pattern used in reminders-table.tsx and
advanced-asset-columns.tsx.

In `@apps/webapp/app/entry.server.tsx`:
- Around line 85-109: The reconciliation success path in entry.server.tsx is
using console.log while the failure path uses Logger.error with ShelfError, so
make the logging consistent. Update the success branch in
reconcileRecurringReminders to use Logger.info with the same “Recurring
reminders reconciled” message instead of console.log, keeping the existing
structured logging pattern alongside Logger.error and ShelfError.

In `@apps/webapp/app/modules/asset-reminder/chain.server.ts`:
- Around line 190-241: `reconcileRecurringReminders` currently scans an
unbounded candidate set and will keep rescaning permanently paused recurring
reminders on every boot. Add pagination/`take` (with cursor or batching) to the
`db.assetReminder.findMany` query and process batches incrementally so boot-time
reconciliation stays bounded. Also update the `advanceRecurringReminder` paused
path so paused chains are marked in a way that excludes them from the existing
`where` clause in future runs, preventing repeated
`organization.findUnique`/tier checks until the reminder is changed or
reactivated.

In `@apps/webapp/app/modules/asset-reminder/emails.tsx`:
- Around line 53-61: The exported assetAlertEmailText function is missing the
required JSDoc block after its signature changed to include nextOccurrence. Add
a JSDoc comment above assetAlertEmailText documenting all parameters in
AssetAlertEmailProps, including nextOccurrence, and describing the returned
email text; keep the documentation aligned with the function’s behavior and
update any return/throws notes if applicable.

In `@apps/webapp/app/modules/asset-reminder/recurrence.ts`:
- Around line 82-87: Add full JSDoc to the exported helpers in recurrence.ts:
isRecurringReminder, repeatValueFromRecurrence, describeRecurrence, and
resolveRecurrenceZone should each document their parameters and return values
with `@param` and `@returns` tags, matching the style already used by
getNextOccurrence. Keep the existing symbols and behavior unchanged; only expand
the comments so the exported function docs are complete and consistent.
- Around line 94-106: The fallback in repeatValueFromRecurrence currently
returns a fixed "monthly" value for any non-preset recurrence, which can
misrepresent future custom cadences and silently rewrite stored data. Update
repeatValueFromRecurrence to preserve the same recurrenceUnit/recurrenceInterval
semantics by deriving a fallback from the reminder’s actual unit rather than
hardcoding "monthly", and keep the preset lookup via REMINDER_REPEAT_PRESETS for
known values. Ensure the edit dialog’s value mapping remains stable for custom
cadence data in AssetReminder recurrence handling.

In `@apps/webapp/app/modules/asset-reminder/service.server.ts`:
- Around line 36-56: The exported service APIs in createAssetReminder and
editAssetReminder need JSDoc comments. Add docs above each function describing
the parameters (including recurrence/tier-related inputs), the return value, and
the ShelfError cases they can throw; make sure the comments cover the updated
behavior in the asset-reminder service module and match the project’s
exported-function documentation rule.

In `@apps/webapp/app/modules/asset-reminder/worker.server.test.ts`:
- Around line 1-286: Add a test in the REMINDER worker handler suite for the
“series naturally ended” path where advanceRecurringReminder resolves to { next:
null, advanced: true, paused: false }. This branch is currently not covered and
should verify the handler treats it distinctly from a one-shot reminder by
asserting the expected note/log behavior and that no re-scheduling occurs; use
the existing handler/JOB setup plus the mocked advanceRecurringReminder,
sendEmail, and createNote symbols to locate the flow.

In `@apps/webapp/app/routes/_layout`+/assets.$assetId.reminders.tsx:
- Line 1: The route module is missing the required JSDoc documentation, so add a
file-level purpose block at the top of the asset reminders route and JSDoc
comments for each exported handler in this module, especially the loader and
action functions. Update the documentation near the loader/action symbols so
they describe their purpose and align with the project’s TypeScript JSDoc
convention.

In `@apps/webapp/app/routes/_layout`+/assets.$assetId.tsx:
- Line 1: The route module currently starts with an import and the exported
action handler lacks documentation; add a JSDoc purpose block at the top of this
TypeScript file and a JSDoc comment for the exported action function in
assets.$assetId.tsx. Use the existing action symbol and module entrypoint as
anchors, and ensure the docs describe the file’s purpose and the handler’s
behavior without changing logic.

In `@apps/webapp/app/routes/_layout`+/reminders._index.tsx:
- Line 1: The route module is missing the required JSDoc purpose block at the
top of the file, and the exported `loader` and `action` handlers are
undocumented. Add a brief file-level JSDoc describing the route purpose before
the imports, then add JSDoc comments for each exported function (`loader` and
`action`) in this route module, following the existing coding guidelines.

In `@apps/webapp/app/utils/subscription.server.ts`:
- Around line 432-468: The exported assertUserCanUseRecurringReminders function
already documents the throw behavior, but it is missing JSDoc `@param` entries for
organizationId and organizations. Update the existing JSDoc above
assertUserCanUseRecurringReminders to describe both parameters clearly, keeping
the current `@throws` note intact and matching the project rule that exported
functions document parameters, return values, and errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 802fe815-60cf-4989-8c5a-d373c11a2e41

📥 Commits

Reviewing files that changed from the base of the PR and between e0c977e and 4744100.

📒 Files selected for processing (29)
  • apps/webapp/app/components/asset-reminder/actions-dropdown.tsx
  • apps/webapp/app/components/asset-reminder/reminders-table.tsx
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.schema.test.ts
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx
  • apps/webapp/app/components/assets/asset-reminder-cards.tsx
  • apps/webapp/app/components/assets/assets-index/advanced-asset-columns.tsx
  • apps/webapp/app/components/home/upcoming-reminders.tsx
  • apps/webapp/app/entry.server.tsx
  • apps/webapp/app/modules/asset-reminder/chain.server.test.ts
  • apps/webapp/app/modules/asset-reminder/chain.server.ts
  • apps/webapp/app/modules/asset-reminder/emails.tsx
  • apps/webapp/app/modules/asset-reminder/recurrence.test.ts
  • apps/webapp/app/modules/asset-reminder/recurrence.ts
  • apps/webapp/app/modules/asset-reminder/scheduler.server.ts
  • apps/webapp/app/modules/asset-reminder/service.server.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.test.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.ts
  • apps/webapp/app/modules/asset-reminder/worker.server.test.ts
  • apps/webapp/app/modules/asset-reminder/worker.server.ts
  • apps/webapp/app/modules/asset/query.server.ts
  • apps/webapp/app/modules/asset/service.server.ts
  • apps/webapp/app/modules/asset/types.ts
  • apps/webapp/app/routes/_layout+/assets.$assetId.reminders.tsx
  • apps/webapp/app/routes/_layout+/assets.$assetId.tsx
  • apps/webapp/app/routes/_layout+/reminders._index.tsx
  • apps/webapp/app/utils/subscription.server.ts
  • packages/database/prisma/migrations/20260702120000_add_recurring_reminders/migration.sql
  • packages/database/prisma/migrations/20260702120100_enable_recurring_reminders_for_paid_tiers/migration.sql
  • packages/database/prisma/schema.prisma

Comment thread apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx Outdated
Comment thread apps/webapp/app/modules/asset-reminder/service.server.ts Outdated
Comment thread apps/webapp/app/modules/asset-reminder/utils.server.ts
Comment thread apps/webapp/app/modules/asset-reminder/worker.server.ts
Comment thread apps/webapp/app/modules/asset-reminder/worker.server.ts
Comment thread apps/webapp/app/routes/_layout+/assets.$assetId.tsx
Codex + CodeRabbit review of #2685:

- Preserve the series' stored recurrence timezone on edits: a plain edit
  (message/recipients) from another timezone no longer re-anchors the
  schedule or shifts occurrences across mismatched DST boundaries. The
  submitted end date's calendar day is re-anchored (end-of-day) into the
  stored zone via a new rebaseEndOfDayToZone helper. Because the zone is
  never taken from the request for an existing series, it also cannot be
  mutated by downgraded workspaces (closes the tier-gate omission on the
  timezone field).
- Reconciliation orphan recovery: when the CAS advance commits but
  scheduling the next occurrence throws, the per-row catch now re-arms the
  row's current occurrence best-effort (singleton-deduped) instead of
  leaving the series unmatched by the dead-chain query until it goes stale.
- Move the future-date check off the shared zod schema for SERVER parses:
  z.coerce.date() reads the raw datetime-local string in the process zone
  and wrongly rejected valid future times for users west of UTC. The client
  schema keeps the check (browser zone is the user's zone); the server now
  enforces it in resolveReminderPayloadDates against the client-hint-resolved
  instant, surfacing a field-level 400.
- Return 403 (not 500) when the tier gate denies recurrence (create + edit).
- Explicit 'last occurrence' note when a series ends at its end date, so the
  final fire is distinguishable from a one-shot in the asset activity.
- Reset the Repeat selection when the dialog reopens (stale cadence could be
  resubmitted after cancel).
- Extract shared occurrence formatting into recurrence.ts
  (formatOccurrenceInZone), deduplicating worker note + email copy.
- JSDoc for the changed service/subscription APIs; cache a duplicated
  describeRecurrence call.

Tests: 10 new/updated (timezone preservation round-trip, server-vs-client
schema split, resolved-instant validation, reconcile orphan re-arm, ended
note).
@carlosvirreira

carlosvirreira commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

AI-review processing log (all rounds):

  • Round 1 (processed in b78c877): all 9 inline findings from @coderabbitai and Codex fixed, or resolved in-thread with a stronger alternative where noted.
  • Round 2 (5687666): all 6 findings fixed — zone-safe endsAt ordering (client calendar-day compare + authoritative zone-resolved server check), locked-state error surfacing, next-fire-date moves gated for downgraded workspaces, DB CHECK constraint for the cadence invariant (re-verified on a clean Postgres 16), totalPages/take, label contrast.
  • Round 3 (21e6635): role="alert" on the locked-state error for assistive tech.
  • Round 4: no findings.
  • Round 5 (1168579 + 3b71a1b): React Doctor warnings addressed by extracting ReminderRecurrenceFields + the adjust-state-during-render pattern; JSDoc nitpick on the extracted exports addressed.

16/16 review threads resolved, no open findings. Ready for human review.

(This comment was originally the round-1 summary; consolidated into the full log after a tooling slip edited it in place.)

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@carlosvirreira Thanks for the detailed summary and for closing the loop on all 9 inline findings plus the disposition of the 12 nitpicks. The rationale for the deferred items is reasonable:

  • Skipping file JSDoc on the three pre-existing, barely-touched routes to keep the diff focused makes sense.
  • The "monthly" fallback in repeatValueFromRecurrence being unreachable via v1 write paths (since unit/interval are always derived from the fixed preset enum server-side) is a fair explanation, especially with it documented in the JSDoc.
  • Bounding the reconciliation candidate set by real-world reminder volume for now, with a take+pagination follow-up if boot timings ever show it, is a pragmatic call.
  • Separating the one-line boot summary (info) from ShelfError-based error logging is consistent with the rest of the module's conventions.

Nice work getting pnpm webapp:validate green with the additional coverage this round. No further action needed from me on these threads.


✏️ Learnings added
Learnt from: carlosvirreira
URL: https://github.com/Shelf-nu/shelf.nu/pull/2685

Timestamp: 2026-07-02T16:42:25.285Z
Learning: In the shelf.nu codebase's recurring-reminders feature (AssetReminder), `repeatValueFromRecurrence` falls back to "monthly" for non-preset recurrence data, but this is unreachable through v1 write paths because unit/interval are always derived server-side from a fixed preset enum; this fallback is documented as edit-dialog-only in the function's JSDoc. `describeRecurrence` is the function that renders non-preset recurrence data faithfully wherever it's displayed.

Learnt from: carlosvirreira
URL: https://github.com/Shelf-nu/shelf.nu/pull/2685

Timestamp: 2026-07-02T16:42:25.285Z
Learning: In the shelf.nu codebase's recurring-reminders boot reconciliation logic, the candidate set for tier-paused chains is not bounded (no `take`/pagination), but this is considered acceptable in practice because the AssetReminder table is small and reconciliation runs once at boot; a `take` cap plus pagination is the agreed follow-up if boot timings ever regress. Paused rows are intentionally skipped without logging, and the reconciliation module uses a one-line info-level boot summary while errors go through `ShelfError`, consistent with the rest of the module's logging conventions.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53f392b2-edbe-45f2-b00b-aa0e247ddd12

📥 Commits

Reviewing files that changed from the base of the PR and between 1168579 and 3b71a1b.

📒 Files selected for processing (1)
  • apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx

Walkthrough

Adds recurring reminder support across schema, scheduling, worker processing, edit/create flows, email content, tier gating, and reminder list/UI surfaces.

Changes

Recurring Asset Reminders

Layer / File(s) Summary
Database schema and migrations for recurrence and tier flags
packages/database/prisma/schema.prisma, packages/database/prisma/migrations/20260702120000_add_recurring_reminders/*, packages/database/prisma/migrations/20260702120100_enable_recurring_reminders_for_paid_tiers/*
Adds recurrence columns and enum support to AssetReminder, plus tier capability flags and a paid-tier enablement migration.
Recurrence utility module and tests
apps/webapp/app/modules/asset-reminder/recurrence.ts, apps/webapp/app/modules/asset-reminder/recurrence.test.ts
Implements repeat presets, timezone helpers, and next-occurrence calculation with tests for cadence, DST, month-end, and cutoff behavior.
Recurring reminder tier gating utilities
apps/webapp/app/utils/subscription.server.ts
Adds recurring-reminder capability checks and a 403 guard for organizations without the tier flag.
Scheduler job options and reminder payload updates
apps/webapp/app/modules/asset-reminder/scheduler.server.ts, apps/webapp/app/modules/asset/query.server.ts, apps/webapp/app/modules/asset/types.ts, apps/webapp/app/modules/asset/service.server.ts
Adds recurring job options, changes cancellation behavior for recurring reminders, and widens reminder payloads to include recurrence fields.
Recurring chain advance and boot-time reconciliation
apps/webapp/app/modules/asset-reminder/chain.server.ts, apps/webapp/app/modules/asset-reminder/chain.server.test.ts, apps/webapp/app/entry.server.tsx
Implements recurring advancement and reconciliation logic, wires reconciliation into startup, and covers the chain behavior with tests.
Reminder creation and editing with recurrence persistence
apps/webapp/app/modules/asset-reminder/service.server.ts
Persists recurrence on create/edit, enforces edit gating, re-anchors end dates, updates pagination, and reorders overview reminders.
Worker job handling for recurring reminders
apps/webapp/app/modules/asset-reminder/worker.server.ts, apps/webapp/app/modules/asset-reminder/worker.server.test.ts
Adds stale-job protection, recurring advance handling, retry signaling, note content updates, and worker test coverage.
Recurrence content in reminder emails
apps/webapp/app/modules/asset-reminder/emails.tsx
Adds next-occurrence rendering to text and HTML reminder emails.
Form payload resolution for recurrence and timezone
apps/webapp/app/modules/asset-reminder/utils.server.ts, apps/webapp/app/modules/asset-reminder/utils.server.test.ts
Adds server-side parsing for reminder dates and recurrence, with timezone and validation regression tests.
Set/edit reminder dialog recurrence UI and schema
apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx, apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.schema.test.ts
Refactors reminder schemas for recurrence, adds gated repeat/end-date controls, and verifies schema parsing behavior.
Recurrence display in reminder lists and asset views
apps/webapp/app/components/asset-reminder/actions-dropdown.tsx, apps/webapp/app/components/asset-reminder/reminders-table.tsx, apps/webapp/app/components/assets/asset-reminder-cards.tsx, apps/webapp/app/components/assets/assets-index/advanced-asset-columns.tsx, apps/webapp/app/components/home/upcoming-reminders.tsx
Shows recurrence labels and icons and makes recurring reminders editable in reminder list and asset surfaces.
Route loaders/actions wiring tier limit and organizations
apps/webapp/app/routes/_layout+/assets.$assetId.reminders.tsx, apps/webapp/app/routes/_layout+/assets.$assetId.tsx, apps/webapp/app/routes/_layout+/reminders._index.tsx
Fetches tier limits, computes recurrence capability flags, passes organizations through permission resolution, and gates creation through the routes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: DonKoko

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding recurring asset reminders.
Linked Issues check ✅ Passed The changes implement recurring reminders, cadence editing/stopping, self-rescheduling, tier gating, and continued notifications required by #2659.
Out of Scope Changes check ✅ Passed The added tests, schema updates, scheduler changes, and UI updates all support recurring reminders and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/recurring-reminders

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/modules/asset-reminder/scheduler.server.ts (1)

46-87: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Guard the dedupe path before updating activeSchedulerReference. sendAfter() can return null for an already-queued singleton job, and writing that straight to the row can clear a live reference and make later edit/delete cancellation skip the still-queued job. Resolve the existing job by singletonKey (pg-boss findJobs) or avoid overwriting the row with null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/scheduler.server.ts` around lines 46 -
87, The dedupe path in scheduleAssetReminder is overwriting
activeSchedulerReference with null when scheduler.sendAfter returns null, which
can clear a live job reference. Update scheduleAssetReminder to detect the
singletonKey dedupe case before db.assetReminder.update, and either look up the
existing pg-boss job (for example via findJobs) to persist its real reference or
skip updating activeSchedulerReference when no new job reference is returned.
Keep the fix focused in scheduleAssetReminder and the sendAfter result handling.
🧹 Nitpick comments (7)
apps/webapp/app/modules/asset-reminder/recurrence.ts (1)

33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add JSDoc for the exported type.

ReminderRepeatValue is exported but has no direct JSDoc block.

As per coding guidelines, **/*.{ts,tsx} exported types must have a JSDoc comment.

Proposed documentation addition
+/** Repeat selector values supported by the reminder dialog. */
 export type ReminderRepeatValue = (typeof REMINDER_REPEAT_VALUES)[number];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/recurrence.ts` at line 33, Add a JSDoc
block for the exported type ReminderRepeatValue in recurrence.ts. Document what
the type represents by placing the comment directly above the export type
declaration so it satisfies the exported-types documentation guideline.

Source: Coding guidelines

apps/webapp/app/entry.server.tsx (1)

92-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Logger.info instead of console.log for the reconciliation summary.

The rest of the boot sequence (and this same catch block) uses Logger. A raw console.log here bypasses whatever structured-logging/observability pipeline Logger feeds into.

🧹 Suggested tweak
-        if (scanned > 0) {
-          console.log(
-            `Recurring reminders reconciled: ${rearmed}/${scanned} chains re-armed`
-          );
-        }
+        if (scanned > 0) {
+          Logger.info(
+            `Recurring reminders reconciled: ${rearmed}/${scanned} chains re-armed`
+          );
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/entry.server.tsx` around lines 92 - 98, The reconciliation
summary in entry.server.tsx is using a raw console.log inside the then handler
for the recurring reminders reconciliation. Replace that call with Logger.info
so the summary follows the same structured logging path as the rest of the boot
sequence; use the existing Logger symbol in this flow and keep the same message
content/context when logging scanned and rearmed counts.
apps/webapp/app/modules/asset-reminder/chain.server.ts (1)

182-286: 🧹 Nitpick | 🔵 Trivial

Boot-only reconciliation leaves long recovery windows between deploys.

reconcileRecurringReminders only runs once at boot, per the module docstring's "no cron by design" rationale. If a chain dies and the next deploy is far off, the series stays dead until then. Given this is explicitly documented as an accepted tradeoff, consider whether a lightweight periodic trigger (e.g. every N hours in addition to boot) would meaningfully shrink the exposure window for infrequently-deployed environments, without reintroducing the cron infrastructure being avoided.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/chain.server.ts` around lines 182 -
286, `reconcileRecurringReminders` is boot-only, so dead recurring reminder
chains can stay unrecovered until the next deploy; consider adding a lightweight
periodic trigger in addition to the boot sweep. Update the scheduling/entry path
around `reconcileRecurringReminders` so it still runs at boot but also executes
every N hours in environments with infrequent deploys, while preserving the
existing no-cron, fault-isolated behavior and the current
`advanceRecurringReminder`/`scheduleAssetReminder` flow.
apps/webapp/app/modules/asset-reminder/worker.server.ts (1)

244-244: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Final/paused occurrence emails don't tell recipients the series stopped.

nextOccurrence is the only recurrence signal passed to assetAlertEmailHtmlString/assetAlertEmailText. When the series ends (seriesEnded) or is paused by a downgrade (seriesPaused), nextOccurrence is null, so recurrenceLine() in emails.tsx returns null and the outgoing email is silent about it — only the internal asset-activity note (Line 229/231) mentions it. Recipients of what they know is a recurring reminder get no signal that no further reminders are coming, which is important for the downgrade-pause case especially (they may need to re-enable the plan or recreate the reminder manually).

Consider threading seriesEnded/seriesPaused through to the email props alongside nextOccurrence so recurrenceLine can render an appropriate closing line.

♻️ Sketch of the change
-          nextOccurrence,
+          nextOccurrence,
+          seriesEnded,
+          seriesPaused,
         });

(mirrored for the assetAlertEmailText call at Line 257, plus extending AssetAlertEmailProps/recurrenceLine in emails.tsx to consume the new flags.)

Also applies to: 257-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/worker.server.ts` at line 244, The
recurring reminder emails are missing the “series ended/paused” status because
only nextOccurrence is passed into assetAlertEmailHtmlString and
assetAlertEmailText, so recurrenceLine in emails.tsx has no way to render a
closing message. Thread seriesEnded and seriesPaused through the
worker.server.ts email payloads alongside nextOccurrence, then update
AssetAlertEmailProps and recurrenceLine in emails.tsx to use those flags and
emit an appropriate final/paused recurrence line.
apps/webapp/app/modules/asset-reminder/emails.tsx (1)

18-18: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

See linked comment on worker.server.ts (Lines 244, 257).

recurrenceLine correctly falls back to null when nextOccurrence is absent, but that also means it can't render an "ended"/"paused" message even though the reminder itself is flagged as recurring — the caller never passes that context through. This is the natural place to accept it once worker.server.ts threads the extra state.

Also applies to: 27-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/emails.tsx` at line 18, The recurrence
email rendering path is missing the reminder state needed to show an “ended” or
“paused” message when there is no next occurrence. Update the recurrence-related
helper/rendering in emails.tsx (including the recurrenceLine logic that uses
describeRecurrence and formatOccurrenceInZone) to accept the extra state passed
from worker.server.ts, and use it to render the appropriate fallback message
instead of returning null whenever nextOccurrence is absent.
apps/webapp/app/modules/asset-reminder/utils.server.test.ts (1)

40-68: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Round-trip test doesn't exercise editing from a different timezone than the original series.

Both calls in this test use requestWithZone(zone) with the identical zone, so it validates self-consistency within one timezone but doesn't cover the scenario raised previously (editing an existing recurring series while the current request's client-hint zone differs from the series' stored zone). Given resolveReminderPayloadDates has no parameter for a stored zone (see companion comment in utils.server.ts), a test asserting the cross-zone-edit behavior would help confirm whether that concern is actually resolved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/utils.server.test.ts` around lines 40
- 68, The round-trip test in dialogEndsAtDefault and resolveReminderPayloadDates
only checks the same timezone twice, so it misses the cross-zone edit case.
Update the test to simulate editing an existing recurring series whose stored
end date was created in a different zone than the current requestWithZone zone,
then verify the displayed date and re-submitted getTime() remain stable across
that mismatch. Use the existing helpers dialogEndsAtDefault,
resolveReminderPayloadDates, requestWithZone, and formData to cover the original
stored zone versus the current client-hint zone.
apps/webapp/app/modules/asset-reminder/utils.server.ts (1)

89-93: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Invalid endsAt input silently becomes "no end date" instead of a validation error.

If rawEndsAt fails to parse (malformed string), the code falls back to null silently rather than surfacing a field-level validation error like the alertDateTime check does. A user who mistypes the end date could unknowingly create a never-ending recurrence.

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/modules/asset-reminder/utils.server.ts` around lines 89 - 93,
The `endsAt` parsing in `utils.server.ts` currently turns any malformed
`rawEndsAt` into `null`, which hides invalid user input. Update the validation
flow in the same place as the `alertDateTime` check so `DateTime.fromFormat(...,
"yyyy-MM-dd", { zone })` is verified before constructing the date, and surface a
field-level validation error when parsing fails instead of treating it as “no
end date.” Ensure the logic around `endsAt` and the existing recurrence
validation path uses a clear parse-failure branch rather than falling back
silently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx`:
- Around line 312-320: The disabled “Repeat” label in
set-or-edit-reminder-dialog.tsx uses text-gray-400 via the
canUseRecurringReminders ternary, which does not meet contrast requirements.
Update the label styling in the “Repeat” label block to use a darker gray such
as text-gray-500 or another color that achieves at least 4.5:1 contrast while
preserving the enabled/disabled state distinction.
- Around line 344-407: The edit flow in SetOrEditReminderDialog is validating
hidden recurrence data even when canUseRecurringReminders is false, which can
block submission without any visible error. Update the validation/rendering
around endsAtOrderingRefinement and the endsAt input so downgraded workspaces
either skip that refinement for hidden recurring fields or surface the error on
a visible control in set-or-edit-reminder-dialog.tsx, using the existing
zo.errors.endsAt(), validationErrors?.endsAt, and canUseRecurringReminders /
initialRepeat / endsAtDefault logic to keep the form submittable.
- Around line 48-70: The endsAt validation in endsAtOrderingRefinement is
comparing UTC-parsed dates against a local alertDateTime, which can reject valid
same-day recurring reminders for some time zones. Move this ordering check out
of the shared base reminder schema path and into the zone-resolved reminder
payload path used after localization, or make the comparison zone-aware using
the resolved reminder date/time values in setReminderSchema and
setReminderServerSchema.

In `@apps/webapp/app/modules/asset-reminder/service.server.ts`:
- Around line 261-263: The `totalPages` calculation in `getAssetReminders` is
still using `perPage`, but the query actually paginates with the clamped `take`
value, so the page count can be wrong when the cookie-resolved page size is
invalid. Update the `Math.ceil(totalReminders / ...)` logic to use `take` (the
effective page size used by the query) and keep the surrounding pagination flow
in sync with `perPage`, `perPageParam`, and `take`.
- Around line 368-375: The recurrence-change guard in
asset-reminder/service.server.ts only compares recurrence unit/interval/endsAt,
so downgraded recurring reminders can still change schedule by moving the next
fire time. Update the recurrenceChanged logic in the create/update flow around
effectiveRecurrence and isRecurringReminder to also compare the current
next-occurrence value derived from alertDateTime against the effective
recurrence’s next fire time, and treat that as a recurrence change when
canUseRecurringReminders is false.

In
`@packages/database/prisma/migrations/20260702120000_add_recurring_reminders/migration.sql`:
- Around line 8-11: The migration for AssetReminder currently allows invalid
recurrence cadence states because recurrenceUnit and recurrenceInterval can be
saved independently or with a non-positive interval. Add a database check
constraint in this migration that enforces the invariant: both cadence fields
are null, or both are non-null with recurrenceInterval > 0; use the
AssetReminder table and the recurrenceUnit/recurrenceInterval columns as the key
symbols to update.

---

Outside diff comments:
In `@apps/webapp/app/modules/asset-reminder/scheduler.server.ts`:
- Around line 46-87: The dedupe path in scheduleAssetReminder is overwriting
activeSchedulerReference with null when scheduler.sendAfter returns null, which
can clear a live job reference. Update scheduleAssetReminder to detect the
singletonKey dedupe case before db.assetReminder.update, and either look up the
existing pg-boss job (for example via findJobs) to persist its real reference or
skip updating activeSchedulerReference when no new job reference is returned.
Keep the fix focused in scheduleAssetReminder and the sendAfter result handling.

---

Nitpick comments:
In `@apps/webapp/app/entry.server.tsx`:
- Around line 92-98: The reconciliation summary in entry.server.tsx is using a
raw console.log inside the then handler for the recurring reminders
reconciliation. Replace that call with Logger.info so the summary follows the
same structured logging path as the rest of the boot sequence; use the existing
Logger symbol in this flow and keep the same message content/context when
logging scanned and rearmed counts.

In `@apps/webapp/app/modules/asset-reminder/chain.server.ts`:
- Around line 182-286: `reconcileRecurringReminders` is boot-only, so dead
recurring reminder chains can stay unrecovered until the next deploy; consider
adding a lightweight periodic trigger in addition to the boot sweep. Update the
scheduling/entry path around `reconcileRecurringReminders` so it still runs at
boot but also executes every N hours in environments with infrequent deploys,
while preserving the existing no-cron, fault-isolated behavior and the current
`advanceRecurringReminder`/`scheduleAssetReminder` flow.

In `@apps/webapp/app/modules/asset-reminder/emails.tsx`:
- Line 18: The recurrence email rendering path is missing the reminder state
needed to show an “ended” or “paused” message when there is no next occurrence.
Update the recurrence-related helper/rendering in emails.tsx (including the
recurrenceLine logic that uses describeRecurrence and formatOccurrenceInZone) to
accept the extra state passed from worker.server.ts, and use it to render the
appropriate fallback message instead of returning null whenever nextOccurrence
is absent.

In `@apps/webapp/app/modules/asset-reminder/recurrence.ts`:
- Line 33: Add a JSDoc block for the exported type ReminderRepeatValue in
recurrence.ts. Document what the type represents by placing the comment directly
above the export type declaration so it satisfies the exported-types
documentation guideline.

In `@apps/webapp/app/modules/asset-reminder/utils.server.test.ts`:
- Around line 40-68: The round-trip test in dialogEndsAtDefault and
resolveReminderPayloadDates only checks the same timezone twice, so it misses
the cross-zone edit case. Update the test to simulate editing an existing
recurring series whose stored end date was created in a different zone than the
current requestWithZone zone, then verify the displayed date and re-submitted
getTime() remain stable across that mismatch. Use the existing helpers
dialogEndsAtDefault, resolveReminderPayloadDates, requestWithZone, and formData
to cover the original stored zone versus the current client-hint zone.

In `@apps/webapp/app/modules/asset-reminder/utils.server.ts`:
- Around line 89-93: The `endsAt` parsing in `utils.server.ts` currently turns
any malformed `rawEndsAt` into `null`, which hides invalid user input. Update
the validation flow in the same place as the `alertDateTime` check so
`DateTime.fromFormat(..., "yyyy-MM-dd", { zone })` is verified before
constructing the date, and surface a field-level validation error when parsing
fails instead of treating it as “no end date.” Ensure the logic around `endsAt`
and the existing recurrence validation path uses a clear parse-failure branch
rather than falling back silently.

In `@apps/webapp/app/modules/asset-reminder/worker.server.ts`:
- Line 244: The recurring reminder emails are missing the “series ended/paused”
status because only nextOccurrence is passed into assetAlertEmailHtmlString and
assetAlertEmailText, so recurrenceLine in emails.tsx has no way to render a
closing message. Thread seriesEnded and seriesPaused through the
worker.server.ts email payloads alongside nextOccurrence, then update
AssetAlertEmailProps and recurrenceLine in emails.tsx to use those flags and
emit an appropriate final/paused recurrence line.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a5230a2-7709-4b73-9e03-d5eb66aad3aa

📥 Commits

Reviewing files that changed from the base of the PR and between e0c977e and b78c877.

📒 Files selected for processing (29)
  • apps/webapp/app/components/asset-reminder/actions-dropdown.tsx
  • apps/webapp/app/components/asset-reminder/reminders-table.tsx
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.schema.test.ts
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx
  • apps/webapp/app/components/assets/asset-reminder-cards.tsx
  • apps/webapp/app/components/assets/assets-index/advanced-asset-columns.tsx
  • apps/webapp/app/components/home/upcoming-reminders.tsx
  • apps/webapp/app/entry.server.tsx
  • apps/webapp/app/modules/asset-reminder/chain.server.test.ts
  • apps/webapp/app/modules/asset-reminder/chain.server.ts
  • apps/webapp/app/modules/asset-reminder/emails.tsx
  • apps/webapp/app/modules/asset-reminder/recurrence.test.ts
  • apps/webapp/app/modules/asset-reminder/recurrence.ts
  • apps/webapp/app/modules/asset-reminder/scheduler.server.ts
  • apps/webapp/app/modules/asset-reminder/service.server.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.test.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.ts
  • apps/webapp/app/modules/asset-reminder/worker.server.test.ts
  • apps/webapp/app/modules/asset-reminder/worker.server.ts
  • apps/webapp/app/modules/asset/query.server.ts
  • apps/webapp/app/modules/asset/service.server.ts
  • apps/webapp/app/modules/asset/types.ts
  • apps/webapp/app/routes/_layout+/assets.$assetId.reminders.tsx
  • apps/webapp/app/routes/_layout+/assets.$assetId.tsx
  • apps/webapp/app/routes/_layout+/reminders._index.tsx
  • apps/webapp/app/utils/subscription.server.ts
  • packages/database/prisma/migrations/20260702120000_add_recurring_reminders/migration.sql
  • packages/database/prisma/migrations/20260702120100_enable_recurring_reminders_for_paid_tiers/migration.sql
  • packages/database/prisma/schema.prisma

Comment thread apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx Outdated
Comment thread apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx Outdated
Comment thread apps/webapp/app/modules/asset-reminder/service.server.ts Outdated
Comment thread apps/webapp/app/modules/asset-reminder/service.server.ts
- Zone-safe endsAt ordering: the shared refinement compared a UTC-midnight
  type=date coercion against a local datetime-local coercion and wrongly
  rejected valid same-day evening reminders for users west of UTC. The client
  schema now compares CALENDAR DAYS; the authoritative check runs in
  resolveReminderPayloadDates on the zone-resolved instants (field-level 400).
  Server schemas carry no date refinements at all now.
- Surface endsAt validation errors in the locked (downgraded) dialog state,
  where the Ends-on input does not render but its hidden value still
  validates — previously the submit could block with no visible cause.
- Gate next-fire-date moves for downgraded recurring reminders: moving
  alertDateTime re-anchors the series, so it now counts as a recurrence
  change (message/recipient edits round-trip the prefilled date unchanged
  and still pass).
- DB CHECK constraint for the cadence invariant (both cadence fields null,
  or both present with a positive interval), amended into the unreleased
  migration; re-verified on a clean Postgres 16 (deploy + drift + violation
  probes both rejected).
- totalPages divides by the clamped page size the query actually used.
- Disabled Repeat label: text-gray-500 for WCAG AA contrast.

Tests: 6 new/updated (calendar-day ordering, same-day west-of-UTC pass,
resolved ordering 400, server-schema no-refinement contract).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx`:
- Around line 398-405: The locked-state endsAt validation message in
SetOrEditReminderDialog is not announced to assistive technologies because it is
rendered outside any input association. Update the error paragraph in the locked
branch to use an alert/live region, and ensure the endsAt message remains
reachable for screen readers even when the “Ends on” field is not rendered. Keep
the change localized to SetOrEditReminderDialog and the endsAt error rendering
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc15e371-3d67-460c-8bd3-db6a81262e44

📥 Commits

Reviewing files that changed from the base of the PR and between b78c877 and 5687666.

📒 Files selected for processing (6)
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.schema.test.ts
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx
  • apps/webapp/app/modules/asset-reminder/service.server.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.test.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.ts
  • packages/database/prisma/migrations/20260702120000_add_recurring_reminders/migration.sql
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.schema.test.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.test.ts
  • apps/webapp/app/modules/asset-reminder/utils.server.ts
  • apps/webapp/app/modules/asset-reminder/service.server.ts

Comment thread apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx Outdated
Carlos Virreira added 2 commits July 2, 2026 19:20
role="alert" on the downgraded-dialog error paragraph: it isn't tied to a
rendered input (the Ends-on field is hidden in that state), so screen
readers otherwise get no notification when a submit is blocked.
- Extract ReminderRecurrenceFields (Repeat select, locked/upgrade state,
  Ends-on input) into its own component (no-giant-component).
- Replace the reset-on-open effect with the adjust-state-during-render
  pattern (no-effect-event-handler); the reset now applies in the same
  render pass. The pattern's prop-seeded useState trips no-derived-useState,
  documented as an accepted residual per CLAUDE.md ('repeat' is user-editable
  state that must reset on open, not derivable state).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx (1)

25-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing JSDoc on exported type/component.

ReminderRecurrenceFieldsProps and the default-exported ReminderRecurrenceFields component lack a JSDoc block describing their purpose/parameters/return value (only per-field inline comments exist on the type). The file-level doc at the top explains the file's purpose but doesn't substitute for per-entity docs.

As per coding guidelines, "every exported function, component, and type must have a JSDoc comment describing parameters, return values, and thrown errors."

📝 Proposed doc addition
+/**
+ * Props for {`@link` ReminderRecurrenceFields}.
+ */
 type ReminderRecurrenceFieldsProps = {
   /** Whether the workspace tier includes recurring reminders. */
   canUseRecurringReminders: boolean;
   ...
 };
 ...
+/**
+ * Renders the Repeat select and optional "Ends on" date input for the
+ * set/edit reminder dialog, locking the controls with an upgrade nudge
+ * when the workspace tier lacks recurring reminders.
+ *
+ * `@param` props - See {`@link` ReminderRecurrenceFieldsProps}.
+ * `@returns` The recurrence form fields fragment.
+ */
 export default function ReminderRecurrenceFields({

Also applies to: 57-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx`
around lines 25 - 42, Add JSDoc blocks for both exported symbols: the
ReminderRecurrenceFieldsProps type and the default-exported
ReminderRecurrenceFields component. Describe the props/purpose for
ReminderRecurrenceFieldsProps and document the component’s role plus its
parameters/returned JSX in ReminderRecurrenceFields, keeping the existing inline
field comments intact.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx`:
- Around line 25-42: Add JSDoc blocks for both exported symbols: the
ReminderRecurrenceFieldsProps type and the default-exported
ReminderRecurrenceFields component. Describe the props/purpose for
ReminderRecurrenceFieldsProps and document the component’s role plus its
parameters/returned JSX in ReminderRecurrenceFields, keeping the existing inline
field comments intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 306a99ab-e82b-460f-b23c-1fb7428b8676

📥 Commits

Reviewing files that changed from the base of the PR and between 21e6635 and 1168579.

📒 Files selected for processing (2)
  • apps/webapp/app/components/asset-reminder/reminder-recurrence-fields.tsx
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/components/asset-reminder/set-or-edit-reminder-dialog.tsx

Addresses the CodeRabbit nitpick on the extracted component: exported
symbols require JSDoc per the repo documentation guidelines.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@carlosvirreira Thanks for consolidating the full processing log across all five rounds into one place — that's a clean record for anyone reviewing the thread history later. All 16 threads being resolved with no open findings lines up with what I saw on my end. Ready for human review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recurring asset reminders (auto-repeat on a cadence)

1 participant