Claude/missionary calendar app ti6dr6 - #2764
Conversation
A self-contained React PWA modeled on the Preach My Gospel planner's three-part structure, adapted for personal use: - Goals tab (replaces Key Indicators): weekly targets grouped by category with a week selector, progress ring, and +/- steppers; progress tracked per week. - Planner tab: day timeline (tap to add) and week agenda; events link to a person, with location, notes, and done state. - People tab: general contact tracker with user-defined status labels, search/filter, and a detail page with call/text/email/log-today actions. - More tab: theme, custom status management, and JSON export/import/reset. Data persists locally via localStorage; offline support via a service worker. Built with Vite + React Router (hash routing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
New fifth tab backed by Leaflet + OpenStreetMap (no API key needed): - Drop pins by tapping the map; each pin has an emoji, text label, notes, and an optional linked person. - Tap a pin for an info card with a "Directions" button that opens Google Maps navigation, plus a matching corner directions button. - "My location" control re-centers the map via geolocation. - Pins persist in localStorage alongside the rest of the data and are unlinked automatically when a linked person is deleted; export/import and clear/reset now include pins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
People — reconnect nudges: - Flag contacts not touched within a reminder window (last-contacted, or added date if never contacted). Adds a top "Time to reconnect" banner with one-tap Log, an amber filter chip, per-row tag/dot, and a chip on the contact detail page. Global threshold lives in More; each person can override it with their own cadence. Planner — recurring events: - Events gain a repeat rule (daily/weekly/biweekly/monthly) with an optional end date. Occurrences are expanded for the visible range rather than materialized. Per-occurrence done state (doneDates) and single-day deletion (skipDates) leave the rest of the series intact; a ↻ glyph marks repeating events. Contact "upcoming together" expands recurrences too. Map — pins from a person: - Contact detail gets a Places section listing linked pins and an "Add a place" action that opens the map in placement mode pre-linked to that person; tapping a listed place selects it on the map. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Recurring events now support per-occurrence exceptions in addition to series-level edits: - The event editor shows a "This event / All events" scope toggle for recurring events. "This event" writes a per-occurrence override (title, time, contact, location, notes, or even a different day) keyed by the original recurrence date; "All events" updates the series master. - Occurrences are expanded via expandEventOnDay(), which merges overrides, honors skips, and can relocate a moved occurrence onto its new day. - Edited occurrences detach from the series (kept on series-wide edits) and are marked with a ✎ glyph; unedited ones keep the ↻ glyph. - Contact "upcoming together" uses the same expansion so overrides show through there too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Adds a workflow that builds schedule-app/ and publishes dist/ to GitHub Pages via the Actions artifact flow. Runs on pushes that touch the app (on main or the feature branch) and on manual dispatch. Requires Pages to be enabled once with Source set to "GitHub Actions". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Goals: - Daily goals alongside weekly (Today / This week toggle); daily progress tracked per day, weekly per week. Per-goal reminder times. New goal is now a floating + button. Planner: - Event types with colors (managed in More) plus a per-event color override, applied to day blocks and week chips via a --ev custom property. - Per-event reminders (lead time before start). - Drag an event block on the day timeline to reschedule (pointer-based, with tap-vs-drag threshold; recurring drags write a single-occurrence override). - Event editor opens as a taller sheet with a grip handle and swipe-to-dismiss. People: - Floating + on a person's detail page starts a calendar event pre-linked to them (passed to the Planner via router state). More: - Notifications toggle, event-type management, and a feedback / suggest-a- feature button (email or copy). Branding & polish: - Compass logo mark in every page header and a map badge. - Best-effort local reminder scanner (goals + events) with permission handling; honest about the serverless "only while open" limitation. - Reusable swipe-dismiss Modal, entrance animations, tab/press transitions, and prefers-reduced-motion support. Store: goals migrate weeklyProgress -> progress with a period; events gain typeId/color/reminder; new eventTypes collection; settings.notifications. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
- Rename Compass -> Stewardly across manifest/index.html/SW/README/UI copy; new shield-and-sprout icon and emerald/teal brand color (existing localStorage key kept as-is so nobody's data disappears on upgrade). - Remove the logo badge from the Map page per feedback. - App-wide haptic feedback via one delegated pointerdown listener (tap/ confirm/warn tiers) instead of wiring every button individually. - New Select component: app-styled bottom-sheet dropdown replacing native <select> in People and Map (Planner's selects follow in the next pass). - Contact profile pictures: camera/photo picker, client-side downscaled to a small square JPEG data URL before storage; Avatar component renders the photo or falls back to initials everywhere a contact appears. - Audited light mode across every page/modal — no defects found. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
…view The biggest single change so far — reworks how you interact with events, goals, and people: - New EditorSheet component: full-page (not 3/4) editor used for events, goals, and people. Checkmark save top-right with a pop animation; swipe the grip down (or tap the X) prompts Save/Discard/Cancel when there are unsaved changes, otherwise just closes. - Events: single tap opens a read-only detail view (date, time, repeat, reminder, location + mini map, contact, notes); a pencil button opens the same event in the full editor. Delete-this-day / delete-series moved into the editor's footer. - Day timeline drag now requires a ~500ms press-and-hold to "arm" (haptic + grip-dots indicator) before it moves; a plain tap always opens the detail view instead of nudging the event. Short/accidental presses no longer reschedule anything. - Custom recurrence: a "Custom days" repeat option with a Sun-Sat toggle row (event.repeatDays), on top of the existing daily/weekly/biweekly/ monthly rules. - Month view added alongside Day/Week, with a 6-week grid and per-day event dots; confirmed events schedule fine 4+ months out (no artificial cap). - Multi-select: a "Select" toggle lets you tap multiple occurrences and shift them all by +1 day / +1 week / -1 day from a bottom action bar. - Event location can now drop a temporary pin on an embedded mini map (locLat/locLng) instead of only free-text, surfaced as a Directions link in the detail view. - Fixed a real bug in the new Select component: picking an option closed the sheet but a "ghost" click immediately landed on the now-revealed trigger underneath and reopened it. Select now briefly swallows the trigger's next click right after a selection closes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
New Home tab (now first in the tab bar): - Goal overview bubble with mini daily/weekly progress rings, tapping through to Goals. - Important reminders: goals/tasks/events with a reminder or due date today, merged into one list. - Tasks: simple checkable list with an inline add row; tasks can carry a reminder time (shown on Home and counted in "Important reminders"). - Notes: Google Keep-style masonry grid — free text or a checklist, an optional color, and a pin-to-top toggle. Full-page editor via the same EditorSheet used elsewhere. - A "Pro" bubble (crown icon) in the top-right for non-Pro users, linking to a new pricing page. - Quick-add "+" opens a small sheet for New event / New person / New task / New note; event and contact quick-adds hand off to the Planner/People editors via router state. New Pricing page: - Monthly ($4) / annual ($40) toggle, a feature comparison table, and a "Try Pro" button that flips a local settings.isPro demo flag. Explicitly labeled as a demo — there's no payment processor wired up, so this previews the Pro experience without charging anyone. Real subscriptions need a backend (Stripe or similar) before this can take payments. Store: new tasks/notes collections and settings.isPro/colorScheme. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Wraps up the premium/settings side of the feature request: - 8 color themes (accent-only re-tint over the existing light/dark palette), applied via document.documentElement.dataset.scheme; only "Emerald" (default) is free, the other 7 are Pro-locked with a lock glyph and a tap-through to Pricing. - Real premium gating: the People tab (list + detail) now shows an upgrade prompt instead of contacts when settings.isPro is false; color themes, cloud sync, Google sign-in, and .ics import/export are all gated the same way via a shared requirePro() helper. - Real .ics calendar export/import (src/data/ics.js) — genuine client-side iCalendar generation/parsing, no backend needed, so it actually interoperates with Google/Apple/Outlook calendars. Recurring events export with RRULE where possible. - A large "Upgrade to Stewardly Pro" bubble in More (swaps to an active state once Pro is on), plus a smaller crown bubble on Home — both link to the Pricing page. - Donation button in More, below the Pro bubble — opens an honest dialog explaining no payment link is wired up yet rather than faking one. - Local profile settings (name + photo, device-only) and honestly-labeled Account & Sync stubs (cloud sync toggle, "Sign in with Google") that say plainly they need a backend that doesn't exist in this build. - A "Share event" action on the event detail view (Pro-gated) with the same honest "needs an account backend" messaging. - Slightly more deliberate page-mount transition, applied to every page including the full-bleed Map. README rewritten to describe every tab/feature added across this whole round of work, plus a clear "what's real vs. demo" section for Pro. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
…ings - Map: replace the unreliable Leaflet-mouse-event long-press with native Pointer Events on the map container, and swap the full-width temp-pin card for a small floating action bubble (directions / save / dismiss). - Planner: stop the global haptic delegate from double-firing on event blocks; add a selectTick on each 15-min drag snap and a confirmTick on commit. Add expand/contract timeline zoom controls, persisted in settings. - Fix off-center checkmarks (task list + note checklists) caused by default button padding shrinking the icon's content box; unify note checklist styling with the task list's green accent. - Fix colored notes going unreadable in dark mode by forcing dark text on the (always-light) pastel note swatches. - Pricing: $40/yr -> $35/yr. - Settings: new Calendar section (24h time, week starts Sunday, default event duration/reminder lead, timeline hours, show tasks on timeline, event block opacity) and Map section (contact/custom pin toggles, pin emoji size), plus contact icon size, task-complete animation toggle, clear cache, and remove-all-contacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Normalize margin-bottom spacing across Home/More page blocks and stacked settings toggles, and switch the event block color-intensity slider to a true CSS opacity control gated behind Pro. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
…heet - Daily goals can now repeat on specific weekdays instead of every day; Home page's ring and reminders respect it. - Page transitions are a bit longer with a fuller fade. - Adding/editing a task now opens a full editor sheet (location, due date, notes, reminder) instead of a bare title field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Event detail, modals, and full editor sheets now consume one shared, reference-counted history entry so the back button/gesture closes the topmost overlay (respecting unsaved-changes confirmation) instead of leaving the page behind it. Handles same-tick overlay swaps (e.g. "Delete" replacing an edit sheet with a confirm dialog) and app-level navigate() calls without corrupting the URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
- Tab bar drops text labels (icons only). - Fix the map pin emoji/size slider: the CSS custom property was set as a percentage string but consumed as a unitless number in calc(), silently invalidating the rule; a local re-declaration on .pin-bubble also shadowed the value inherited from the wrapper. Pin bubble size, Leaflet icon bounds, and the caption offset now all scale together. - Best-effort mitigation for toggles losing scroll position: blur switch-like controls shortly after a tap. - Donation button emoji changed to a heart. - New Haptic feedback on/off toggle in Settings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
…ropdowns Two-finger pinch on the day timeline now scales pxPerHour directly (the +/- buttons are gone); the Settings start/end hour steppers are now two dropdown selects instead of +/- steppers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Geocodes the address via OpenStreetMap's Nominatim (no API key) and creates/updates one auto-managed pin per contact, tagged so it's never confused with a pin the user placed by hand; deleting the contact removes the auto pin instead of just unlinking it. Address is now also settable when first adding a contact, not just when editing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
The mark appears as a flat black/white silhouette (matching the resolved theme) and fades out to reveal the app underneath. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
A new button in the event editor swaps its form for a day timeline showing the draft event as a draggable, resizable block (small handles top/bottom) alongside the day's other events for context — drag the block to move it, drag a handle to change start/end, with 15-minute snapping and haptic feedback. "Done" returns to the form with the adjusted time filled in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Home now renders its top bubble row from a settings-driven, ordered list (goals/tasks/notes/upcoming event/people) instead of a hardcoded single Goals bubble. Settings gains a Pro-gated "Customize home screen" section with per-bubble on/off toggles and touch-drag reordering; non-Pro users see a locked preview that links to pricing.
The event editor now hands off to a picking mode on the full Map page instead of an embedded minimap: search an address, tap to drop a pin, or drag it to fine-tune, then confirm to return to the editor. The in-progress event draft is stashed across the navigation (the editor unmounts when leaving /planner) and restored either with the picked coordinates or unchanged if the user cancels.
Press-and-hold a selected event now drags the whole selection together: vertical movement shifts time, horizontal movement shifts day (haptic feedback on each day boundary crossed), with a floating indicator showing the target date/time offset. The move is only committed on release, so the quick-move bar's day-only nudges and this new gesture now share one generalized moveSelected(dayOffset, minOffset) that resolves each selection key against the correct occurrence (handling recurring overrides) instead of assuming a same-day, date-only shift. Also fixes a latent React event-ordering bug this surfaced: a native click firing after a custom pointer gesture can read post-update state and re-fire a toggle the gesture already applied. Guarded with the same suppress-click pattern already used on the Map page.
…pe billing routes New backend/ service (separate from the client-only schedule-app PWA) for turning Stewardly into a real multi-user product: user accounts synced from Clerk, a Postgres-backed User record carrying subscription state mirrored from Stripe, and routes for /api/me, Stripe Checkout/Customer Portal, and signed webhooks from both Clerk and Stripe. Verified against a local Postgres instance: migrations apply, the Prisma client (with the new required driver-adapter pattern) reads/writes correctly, and webhook signature verification reads the raw request body before JSON parsing as both providers require. Not yet wired: frontend Clerk integration, and the actual calendar/contact data sync needed before a server-side AI agent can use someone's history — scoped as follow-up work.
Backend: fix an ESM/CJS interop issue with the generated Prisma client (named import doesn't statically resolve under "type": "module", so default-import-then-destructure instead), move the health check ahead of clerkMiddleware() so it doesn't depend on Clerk being configured, extend the checkout route to accept a plan (monthly/annual) mapped server-side to the matching Stripe price instead of trusting a client-supplied price ID, and add a full setup README (local Postgres, Clerk app + webhook, Stripe product/webhook, Render/Railway deploy). Frontend: add Clerk's React SDK behind a single CLERK_ENABLED flag so the app degrades cleanly when it isn't configured — main.jsx only mounts ClerkProvider when a publishable key exists, and every Clerk-dependent component (SubscriptionSync in App.jsx, the Account section in More, the real Checkout/Portal flow in Pricing) mounts conditionally alongside it, never calling a Clerk hook without a provider. Where Clerk isn't configured, the existing local "demo Pro" toggle keeps working unchanged. Fetched subscription status is written into the existing state.settings.isPro flag rather than threaded through as new state, so none of the app's existing Pro-gating (spread across many files) needed to change. Verified: full regression suite passes against both the unconfigured default build and a build with a fake-but-valid-format Clerk key (confirms the app doesn't hang or crash while Clerk's own network calls fail). Backend routes exercised directly — health check, 401s without a session, 404 on unknown routes, and both webhook endpoints correctly reject bad signatures with 400 rather than crashing. Full Prisma User model CRUD verified against a local Postgres instance. Not yet built: syncing actual calendar/contact/goal data to the backend — everything Pro-gates on the real subscription now, but the data itself still lives only in the browser's localStorage. Needed before any server-side feature (e.g. an AI assistant with access to history) works.
Renamed everywhere: app title, PWA manifest name/short_name (App Store name "Keystone - Daily Planner"), in-app copy, backend README/logs, ICS PRODID, and internal identifiers (service worker cache name, sessionStorage keys, backup filenames). Left the pre-existing localStorage persistence keys (compass.data.v1, compass.firedReminders) untouched, same as the last rebrand — renaming those would reset everyone's local data on next load. New icon: a literal keystone — an arch of blocks locked together by a gold wedge at the apex, on a black badge. Replaces the old shield-and-sprout mark in Logo.jsx (in-app header mark), SplashScreen.jsx (now shows the actual fixed brand colors instead of a theme-adaptive silhouette, since gold isn't something that should invert per theme), and public/icon.svg. Also added the icon sizes/purposes a real submission needs, none of which existed before (there was only a single "any"-sized SVG): - icon-192.png / icon-512.png (any) + icon-512-maskable.png (maskable, generated from a separate full-bleed icon-maskable.svg with extra safe-zone padding so Android's adaptive-icon masking doesn't clip it) - apple-touch-icon.png (180px flat PNG — Safari's SVG apple-touch-icon support is unreliable) - appstore-assets/icon-1024.png — flat 1024px, no rounded corners baked in, for manual upload to App Store Connect (outside public/, not part of the web build) Scope note: only the icon/name changed — the app's UI accent color is still the original teal, not re-themed to gold. Flagged as an open question for the user rather than assumed. Verified: full regression suite passes, manifest/icons all resolve in the built dist/, and the new mark renders correctly at every size from 32px favicon through the 1024px App Store icon.
Re-theme the app's default accent to the black/gold Keystone palette: --accent/--accent-soft in the base :root and dark :root blocks now use gold, with the old teal/emerald default preserved as a selectable Pro-gated scheme alongside the other 7 color options. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
- Event detail sheet: give the category tag a proper bottom margin before the details card (was 0, causing it to touch the card) and align the done-row/section rhythm to the same 14px spacing used throughout the rest of the view. - Settings: normalize the profile row's top margin to match the 8px rhythm used elsewhere in the page. - Day timeline: dragging a single event left/right (past the day-drag threshold) now moves it to the previous/next day, mirroring the existing multi-select group-drag behavior (same rubber-band visual and floating day/time indicator), in addition to the existing vertical drag-to-reschedule-time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Planner: - Dragging a single event left/right now pages the visible day exactly one at a time (was a proportional multi-day jump computed on release), live-navigating the whole timeline so the destination day's actual schedule is visible to drop into, matching the existing multi-select group-drag's day-offset semantics. The dragged event renders as a floating ghost decoupled from whichever day is currently showing, with window-level pointer tracking so paging (which drops the original DOM node from the day's event list) doesn't end the gesture. - Removed the floating day/time bubble that appeared during a single- event drag; the live-paged header already shows the destination day. - Added swipe-left/right to change day (Day view background) and change month (Month view grid), alongside the existing tap/pinch/long-press gestures, with click-suppression for the trailing tap after a swipe. - Fixed a stale-closure bug in step(): it read `cursor` from render-time closure, so repeated calls from a long-lived listener (e.g. the drag gesture's window-level handlers, wired once at arm time) kept recomputing from the original day instead of compounding. Now uses the functional setCursor form so it's correct regardless of which closure invokes it. - Fixed a swipe/click suppression flag that could leak past its own gesture: a swipe that crosses from one element to another never fires a trailing native click (the browser suppresses it), so the flag had no click to reset it and could swallow the next unrelated tap. Now cleared defensively at the start of each new gesture instead. - Event detail sheet: added a bottom margin to the category tag so it no longer touches the details card below it, and aligned the done-row/section spacing to the same rhythm used elsewhere in the view. Settings: - Modal now supports a `fullPage` prop; applied to the profile, status, event-type, and feedback editors (the ones with a text input) so the Save button stays above the on-screen keyboard instead of being covered by it. - Added interactive-widget=resizes-content to the viewport meta so fixed/full-height sheets actually resize above the keyboard. - Fixed the gap between the color-scheme swatches and "Contact icon size" (was relying on the browser's default <p> margin, giving an inconsistent ~13px vs. the page's normal 14px rhythm), and normalized the profile row's margin to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
Planner drag/swipe fixes: - Fixed a stale-closure bug where the window-level pointer listeners (added for live day-paging) always saw dragDy/dragDx as their arm-time value of 0, so releasing a drag never actually saved the new time. Fixed with a ref-forwarding pattern so the listeners always call the latest onMoveP/onUp closure. - Replaced the fixed 70px day-paging threshold with an edge-of-screen hysteresis: dragging an event now only pages a day once the pointer reaches the actual edge of the timeline, one page per approach (dragging back toward center re-arms it), applied to both the single-event drag and the multi-select group drag. The old threshold made it too easy to trigger a day change by accident during an ordinary vertical time-drag. - A quick swipe that starts on an event block (released before the long-press arms) now changes the day too, same as swiping empty timeline space, without interfering with long-press-to-reschedule. - Added a directional slide-in animation when the visible day changes (chevron, swipe, or drag-paging), via a keyed remount of the background/event layer that stays outside the drag ghost's render path so live-paging drags remain smooth. - Added a gold "jump to today" button (Day/Week/Month, stays in mode). - Removed the "Tap to add. Press and hold a block to move it." hint text — a first-time tutorial is the better home for it. Haptics: - The app-wide delegated tap listener fired on literally every button, tab, chip, and link; reserved it for primary/danger actions and toggle switches instead, which was the actual "overwhelming" source reported (most of the 39 call sites remain — they're custom gestures like drag-to-reschedule, not simple button clicks the delegated listener already covers). Contacts: - The People page and person detail page are no longer Pro-gated. Pro now gates person status groups (filter chips, status field, and the Settings management section) and the new contact timeline instead. Updated the pricing page's feature list and upsell copy to match. - Removed the "Log today" quick action. - Added a per-contact Timeline page: a chronological feed of linked calendar events and logged contacts, plus notes with a pin-to-top feature (pinned notes show in their own section above the feed). The feed sorts newest/future-first and lands on "today" when opened, so scrolling down moves toward the past and scrolling up toward the future. New interactions data model (state.interactions) backs the logged-contact entries; notes gained an optional contactId link. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaB8NfLY8nsxGAFk7XbCtw
|
Too many files changed for review (121 files, 80 file limit). Bypass the limit by tagging |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis pull request adds the Keystone schedule application. It includes a React PWA, local scheduling features, Express and Prisma backend services, authentication and billing integrations, shared calendars, assistant tooling, offline support, and GitHub Pages deployment. ChangesKeystone application
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
When every home-screen tile is toggled off, show a small looping desert scene above the placeholder message instead of just blank space: a line-art cactus, a cow skull tilted on the ground, and a tumbleweed that hops and crossfades between two silhouettes (bushy / rolled) as it rolls across every 5s. Drawn stroke-only in currentColor to match the app's existing icon house style, with a prefers-reduced-motion fallback. Also fixes the empty-state block not actually centering on screen — it was centering within an arbitrary 55vh sub-box instead of the real space between the header and tab bar, leaving a large dead gap below it.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Actionable comments posted: 139
🤖 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 @.github/workflows/deploy-schedule-app.yml:
- Line 35: Pin all four GitHub Actions in
.github/workflows/deploy-schedule-app.yml at lines 35, 40, 55, and 70 to
immutable full commit SHA references, replacing their mutable version tags while
preserving the existing actions and workflow behavior.
- Around line 9-11: Restrict the deploy-schedule-app workflow’s on.push.branches
configuration to main only by removing claude/missionary-calendar-app-ti6dr6, so
github-pages deployments cannot originate from the feature branch.
- Around line 18-21: Update the workflow permissions so the top-level block
retains only contents: read, while the deploy job declares pages: write and
id-token: write; keep deployment credentials out of build. Pin checkout,
setup-node, upload-pages-artifact, and deploy-pages to full commit SHAs, and
change the setup-node configuration from Node.js 20 to a supported LTS release.
In `@backend/.gitignore`:
- Line 2: Update the backend ignore pattern so all .env variants, including
.env.local, .env.development, and .env.production, are ignored while preserving
tracking for .env.example.
In `@backend/prisma/schema.prisma`:
- Around line 80-119: Add indexes for the queried foreign-key fields: define
indexes on SharedCalendarMember.userId, SharedEvent.calendarId, and
SharedCalendarInvite.calendarId, then create a new Prisma migration containing
the corresponding PostgreSQL CREATE INDEX statements. Preserve the existing
unique constraint and relation definitions.
- Line 99: Update the invite token field definition to remove the predictable
`@default`(cuid()) generator, then ensure every invite creation path explicitly
assigns a token generated with randomBytes(32).toString('base64url').
In `@backend/README.md`:
- Around line 3-7: Update the introductory data-storage description in the
README to reflect that calendar and contact data is now persisted server-side
through the /api/data endpoints, rather than remaining only in browser
localStorage. Preserve the existing distinction between account/billing
foundations and application-data syncing while removing the outdated claim.
- Line 107: Update the POST /api/billing/checkout entry in the API table to
document the lifetime purchase payload, remove the obsolete monthly and annual
subscription-plan values, and retain the existing response and redirect
behavior.
In `@backend/src/app.js`:
- Around line 39-42: Update the error-handling middleware identified by the
app.use callback to detect JSON parser failures and return HTTP 400 for
malformed JSON, while preserving HTTP 413 for oversized request-body errors;
retain the existing 500 response for all other errors and continue logging each
error explicitly.
- Line 34: Gate the calendarsRoutes mount in backend/src/app.js until the
SharedCalendar schema migration is committed and available in deployments. In
backend/README.md lines 171-177, add the shared-calendar migration and replace
the known-gap text with deployment instructions; both sites require updates.
- Around line 28-29: Add an assistant-specific JSON parser before the generic 8
MB parser in backend/src/app.js:28-29, using a smaller limit for requests
handled by the assistant route. In backend/src/routes/assistant.js:298-307,
update assistantTurn() request validation to reject non-string or oversized
context values and validate the complete request body with the available
schema-based mechanism; preserve valid requests while preventing oversized
context from reaching the handler.
In `@backend/src/db.js`:
- Line 6: Introduce backend/src/env.js with a Zod schema that validates required
DATABASE_URL, STRIPE_SECRET_KEY, and FRONTEND_URL at import time, produces clear
missing-variable errors, and keeps ANTHROPIC_API_KEY optional. In
backend/src/db.js:6-6, backend/src/routes/billing.js:7-7, and
backend/src/routes/billing.js:50-51, replace direct process.env access with the
validated configuration object; update each referenced constructor or URL
interpolation accordingly.
- Around line 1-4: Update the Prisma client imports to use the named
PrismaClient export from the configured generated client output path instead of
destructuring the default `@prisma/client` import. In the database initialization
flow, validate DATABASE_URL before constructing PrismaPg and fail startup with a
clear configuration error when it is missing.
In `@backend/src/middleware/requireUser.js`:
- Around line 19-23: Update the lazy provisioning flow around the
`prisma.user.upsert` in the middleware to handle a `P2002` email conflict by
locating the existing orphaned user with that email and re-pointing it to the
current `clerkId`, preserving the protected-route flow. If reclaiming the
existing row is not the intended behavior, instead return a clear 409 response
and do not repeatedly pass the conflict to `next(err)` as a 500.
In `@backend/src/routes/assistant.js`:
- Around line 43-52: Update rateLimited so recent retains only active-window
timestamps and never appends another timestamp once its length has reached
MAX_PER_WINDOW; preserve the existing hit tracking, cleanup sweep, and boolean
result indicating whether the limit is exceeded.
In `@backend/src/routes/billing.js`:
- Around line 16-24: Update the Stripe customers.create call in the billing flow
to pass a stable idempotency key derived from user.id as its second argument.
Keep the existing customer payload and database update behavior unchanged.
- Around line 28-38: Add rate limiting to the `/checkout` route in
`billingRoutes`, ensuring concurrent checkout requests are throttled before
`ensureStripeCustomer` and Stripe session creation run. Reuse the existing
limiter configuration or middleware pattern from `app.js` or `assistant.js`, and
apply it specifically to this route without changing the purchase validation
behavior.
In `@backend/src/routes/calendars.js`:
- Around line 164-196: Align the three invite routes on one permission model:
update the POST `/:id/invites` route to require owner access, matching the
existing GET and DELETE routes, while preserving the current invite creation
behavior for owners.
- Around line 209-224: Update the invite acceptance handler in the POST
/invites/:token/accept route to compare invite.email with req.dbUser.email
before marking the invite accepted or creating membership. Reject mismatches
with an appropriate client error, while preserving the existing validation and
transaction flow for matching addresses.
- Around line 233-260: Validate calendar event date/time fields in both the POST
and PATCH event handlers before persistence: require date values to match
zero-padded YYYY-MM-DD and start/end values to match HH:MM, rejecting empty or
malformed strings with a 400 response. Also reject events where end is earlier
than start, while preserving partial-update behavior for PATCH fields that are
omitted.
- Around line 74-76: Replace the manual input checks in each write handler with
a dedicated Zod schema per endpoint. Update the calendar create/update handlers
around the name and color parsing to enforce bounded names and valid,
length-limited hex colors; validate invite emails as email addresses before
lowercasing; and bound notes in the relevant handler. Parse request bodies
before processing, preserve generic client-facing validation errors, and avoid
returning raw Zod issue details.
In `@backend/src/routes/data.js`:
- Around line 20-23: Update the global error-handling flow for express.json
parser errors so requests exceeding the 8mb limit return HTTP 413 with a clear
payload-too-large error response instead of 500. Preserve the existing
validation in the data route and handle only the oversized-body parser error
appropriately.
In `@backend/src/routes/webhooksClerk.js`:
- Around line 34-36: In the user deletion flow, replace the caught
prisma.user.delete call with deleteMany using the same clerkId filter, allowing
missing users to remain a successful no-op while letting database and constraint
errors propagate to the outer handler. Remove the empty catch block and its
suppression comment.
In `@backend/src/server.js`:
- Around line 4-13: Update the Stripe price startup validation in
backend/src/server.js, using REQUIRED_ENV and its validation logic, to accept
STRIPE_PRICE_ID_LIFETIME || STRIPE_PRICE_ID consistently with
backend/src/routes/billing.js. Review backend/README.md lines 64-68 and align
the documented variable name or fallback behavior with this implementation.
- Around line 15-18: Update backend/src/server.js lines 15-18 to fail
immediately when missing.length is non-zero, before createApp() is called,
instead of only logging a warning; defer route-module loading with a dynamic
import if necessary to ensure validation runs first. In
backend/src/routes/webhooksStripe.js line 6, make no direct change unless
required to support deferred loading.
In `@schedule-app/public/sw.js`:
- Around line 11-35: Update the service worker’s SHELL/install flow to consume a
build-generated asset manifest containing all required Vite JavaScript and CSS
assets, and precache every manifest entry before activation. In the install
handler, stop swallowing cache.add failures; reject the installation when any
required asset cannot be cached. Also update runtime caching in the fetch
handler to attach each cache.put promise through event.waitUntil().
In `@schedule-app/README.md`:
- Around line 21-42: Update the README’s backend and privacy sections to avoid
claiming the app universally has no backend, account, or cloud services.
Document local-only mode as optional, then separately describe the configured
Clerk authentication and VITE_BACKEND_URL backend integration, including its
data handling based on the implemented behavior. Keep the Pro-toggle and
unsupported-service disclaimers accurate without implying these services are
always absent.
In `@schedule-app/src/App.jsx`:
- Around line 218-222: Update the geolocation watch error callback in the
watchPosition setup to handle PERMISSION_DENIED, POSITION_UNAVAILABLE, and
TIMEOUT explicitly, showing one safe status message while treating unavailable
positions and timeouts as transient. On terminal permission denial, disable
location reminders and clear the stored watch state.
- Around line 128-172: Serialize synchronization around the active effect and
the existing busyRef so debounced pushSyncedData cannot run while the initial
fetchSyncedData pull is pending or while another operation is active. Gate local
pushes until the initial pull settles, preserving the initial import behavior
only after that pull completes. Track the server revision returned by
fetchSyncedData and pass it through pushSyncedData using a conditional write,
rejecting stale updates instead of unconditionally overwriting newer remote
data.
In `@schedule-app/src/components/AddressField.jsx`:
- Around line 97-103: Update the suggestion button rendering in the
suggestions.map flow to support keyboard selection by invoking pick(s) from
onClick, while retaining the pointer behavior through onMouseDown without
selecting twice. Prevent the mousedown default action so the input does not blur
and remove or otherwise guard the duplicate invocation, ensuring mouse and
keyboard activation each call pick once.
In `@schedule-app/src/components/AssistantBubble.jsx`:
- Around line 119-133: Validate that reply.content is an array immediately after
askAssistant returns, before appending it to convo or iterating in the tool-use
path. If invalid, surface a clear error through the existing send error-handling
flow and avoid storing or iterating the malformed content; preserve the current
behavior for valid replies.
- Around line 182-187: Update undoChange to wrap change.undo() in explicit error
handling, using the error-reporting mechanism established in the component when
the undo operation throws. Only set undone: true after undo succeeds, so a
failed operation does not leave the chip showing an available undo state
incorrectly.
- Around line 271-286: Update the AssistantBubble error state and rendering so
the upgrade-button condition uses a dedicated flag or error-code state derived
from err.code === 'upgrade_required', rather than inspecting the error message
text with error.includes('Pro'). Preserve the existing error display and pricing
navigation, while ensuring unrelated backend messages containing “Pro” do not
render the button.
In `@schedule-app/src/components/EditorSheet.jsx`:
- Around line 143-172: Update the confirmClose dialog in EditorSheet to add
role="dialog", aria-modal="true", and an accessible aria-label. Add a ref to the
Cancel button and focus it when the dialog opens, using an effect keyed to
confirmClose; preserve the existing backdrop, Cancel, Discard, and Save
behavior.
- Around line 40-49: Direct body scroll locking in EditorSheet and Select is not
reference-counted, so closing a nested component can unlock scrolling
prematurely. Add the shared scroll-lock helper described in
schedule-app/src/data/scrollLock.js, then update EditorSheet.jsx lines 40-49 and
Select.jsx lines 26-36 to use lockBodyScroll() from their open effects; in
Select, preserve the existing keydown listener cleanup alongside the helper’s
cleanup.
In `@schedule-app/src/components/GroupPicker.jsx`:
- Around line 39-68: Update the new-group flow in create and the awaiting
useEffect to track status ids present before actions.addStatus runs, then select
the status whose id was not in that recorded set once it appears. Stop matching
by label, clear awaiting state after resolving, and ensure it is cleared if no
newly added status can be identified.
In `@schedule-app/src/components/MiniMapPicker.jsx`:
- Around line 18-34: Update the useEffect cleanup in MiniMapPicker to retain the
timeout handle created for map.invalidateSize() and clear it before calling
map.remove(). Ensure the delayed callback cannot invoke invalidateSize on a
removed map, while preserving the existing map initialization and cleanup
behavior.
- Around line 21-26: Update the Leaflet configuration in MiniMapPicker so
attribution is enabled, and configure the OpenStreetMap tile layer’s attribution
to visibly link “OpenStreetMap” to https://www.openstreetmap.org/copyright.
Preserve the existing map view and tile-layer settings.
In `@schedule-app/src/components/Modal.jsx`:
- Around line 13-23: Update the Modal component’s useEffect to depend only on
open while keeping the latest onClose callback in a ref, and have the Escape key
handler invoke that ref. Preserve the drag reset, keydown listener, and body
scroll-lock behavior only across open-state changes so parent renders and inline
onClose identities do not restart the effect.
- Around line 18-22: Update the Modal body-scroll-lock effect to use a shared
reference count for active overlays instead of directly resetting
document.body.style.overflow on every cleanup. Increment the count and set
overflow hidden when Modal mounts, then decrement it on cleanup and restore
scrolling only when no overlay remains, preserving EditorSheet’s lock while an
inner Modal is open.
In `@schedule-app/src/components/ReorderToggleList.jsx`:
- Around line 119-130: Update the drag-handle button in ReorderToggleList to
handle ArrowUp and ArrowDown via onKeyDown, committing a one-slot reorder
through the existing onChange path while preserving pointer dragging. Also
update its aria-label to tell keyboard users that the arrow keys move the row.
In `@schedule-app/src/components/Select.jsx`:
- Around line 62-100: Update the Select component’s trigger and open-sheet
markup: add aria-haspopup="listbox" and an aria-expanded value reflecting open
state to the trigger, move the searchable input outside the element with
role="listbox" while keeping the options inside it, and update
closeFromSelection to return focus to the trigger after closing. Preserve the
existing option selection and search behavior.
In `@schedule-app/src/components/SettingsGroup.jsx`:
- Around line 27-51: Update the SettingsGroup heading structure around the
current settings-group-head div: remove its role="button" and tabIndex, and
introduce a real sibling button as the toggle affordance carrying aria-expanded,
aria-controls, onClick, and keyboard behavior. Keep head content and nested
controls such as the notifications toggle or “+ Add” outside that button,
preserving their independent interaction and the existing onToggle/selectTick
behavior for the new button.
In `@schedule-app/src/components/SmartQuickAdd.jsx`:
- Line 44: Memoize the parsed quick-add result in SmartQuickAdd using useMemo,
with text and state.contacts as dependencies so parseQuickAdd reruns only when
its inputs change; preserve the null result for blank trimmed text and the
existing kindOverride behavior.
In `@schedule-app/src/components/SwipeRow.jsx`:
- Around line 127-140: Make interaction state and available actions accessible
across all three sites: in schedule-app/src/components/SwipeRow.jsx lines
127-140, provide a non-gesture path such as an overflow menu to expose the
configured actions at consuming pages; in
schedule-app/src/pages/ContactsPage.jsx lines 373-377, add
aria-pressed={selectMode ? isSel : undefined} to the row button and
aria-hidden="true" to the selection dot; in
schedule-app/src/pages/GoalHistoryPage.jsx lines 86-107, add role="img" and an
aria-label summarizing the met count out of the total for the heatmap and bar
chart.
- Around line 92-125: Reset suppressClickRef.current when a new pointer gesture
begins, in the gesture-start logic associated with onPointerUp and gestureRef.
This ensures a completed swipe whose click never arrives cannot suppress the
next genuine tap, while preserving the existing onClickCapture clearing
behavior.
In `@schedule-app/src/components/Tutorial.jsx`:
- Line 60: Update the tutorial icon rendering in the step display component to
support the first step’s React-element icon while preserving the existing Icon
lookup for string icon names: render s.icon directly when it is an element, and
use Icon with the name prop for string values.
In `@schedule-app/src/data/api.js`:
- Around line 6-16: Update the request function to create an AbortController
with the configured timeout, pass its signal to fetch, and clear the timeout
after completion. Detect timeout-triggered aborts and throw a clear retryable
error while preserving other fetch errors and existing request behavior.
- Around line 6-26: Update request to validate successful responses with
endpoint-specific Zod schemas before returning parsed JSON. Define schemas for
each supported endpoint, especially /api/data, ensuring fields such as goals
retain their expected array types, and select the appropriate schema based on
path before returning the result; preserve the existing error handling and
status/code propagation.
In `@schedule-app/src/data/assistantContext.js`:
- Around line 102-113: Extract the repeated 40-item limits in
buildAssistantContext into named constants alongside the existing caps, then
replace both hardcoded values with the appropriate constant. Refactor
buildAssistantContext into small builder functions for the People, Calendar,
Tasks, and Places sections, keeping each function under 50 lines and preserving
the current output and ordering.
- Around line 118-119: Update the context assembly around the final lines.slice
in the assistant context builder so truncation cannot silently hide entire
sections: either enforce limits per section or append an explicit truncation
notice identifying omitted content and directing the assistant to use the
relevant tool. Preserve the existing section ordering and ensure the model is
informed whenever output is truncated.
- Around line 35-43: Update buildAssistantContext to derive iso from the
injected now value instead of calling todayISO(), ensuring the displayed date,
weekday, and calendar horizon use the same day. Remove the now-unused todayISO
import.
In `@schedule-app/src/data/assistantTools.js`:
- Around line 365-383: Update timelineEntries to define named constants for the
event window bounds currently represented by -400 and 120, then use those
constants in the day loop. Apply the same date-window filtering to interactions
before adding them to out, so event and interaction samples share identical
horizons for contactInsights cadence calculations.
- Around line 229-235: The swipe action in contactSwipe.js must preserve the
newest lastContacted date by applying the same forward-only comparison used in
assistantTools.js at lines 229-235; update contactSwipe.js lines 27-28 so it
only writes the new date when it is later than the existing value. The
assistantTools.js anchor requires no direct change because it already implements
the required check.
- Around line 333-337: Update the routing call in the surrounding assistant tool
flow to pass stops with the selected start origin removed, while preserving the
existing start selection and header behavior. Ensure optimizeRoute receives only
destinations so the origin is not included as a numbered stop or counted in
plan.totalMeters and plan.endsAt.
In `@schedule-app/src/data/conflicts.js`:
- Around line 75-92: Remove the unreachable `if (b.s < a.e2) continue` and the
subsequent `isNext` calculation from the conflict scan. Keep the overlap
handling in the `timed` iteration unchanged, then allow the existing next-event
logic after this block to run directly when no overlap is found.
In `@schedule-app/src/data/contactInsights.js`:
- Around line 50-110: Split contactInsights into four local helpers: whenInsight
for the dayCounts/parts logic, cadenceInsight for date-gap calculation,
lastConnectedInsight for the latest-connection text, and whereInsight for
location aggregation. Each helper should return an insight object or null, while
contactInsights composes the non-null results and remains under 50 lines.
- Around line 130-133: In the date formatting flow, import the shared toISODate
helper from ./helpers.js, replace calls to the local toISO function with
toISODate, and remove the local toISO implementation.
In `@schedule-app/src/data/contactSwipe.js`:
- Around line 40-64: Replace the fixed hex tones in the swipe action
definitions, including followUp, call, and text, with CSS custom-property
references. Add matching per-theme custom properties to the existing stylesheet
that defines --good, --accent, and --danger, then use those tokens so these
actions respond to theme changes.
In `@schedule-app/src/data/geocode.js`:
- Around line 7-31: Update geocodeOnce and geocodeAddress to use shared
constants for the Nominatim endpoint, request timeout, retry delay, and throttle
timing. Pass an AbortSignal.timeout(...) signal to every fetch attempt, and
centralize request scheduling so geocoding retries and autocomplete requests all
share the same throttle/backoff rather than only the import path. Preserve the
existing empty-query and no-match behavior while ensuring stalled requests
terminate and retry timing respects Nominatim’s rate limit.
In `@schedule-app/src/data/goalHistory.js`:
- Around line 50-51: Rename the longestGoalStreak parameter from lookback to
periods and update its default and the buildGoalHistory call accordingly,
ensuring the surrounding documentation describes the value as a period count
rather than days.
In `@schedule-app/src/data/helpers.js`:
- Around line 44-47: Update weekKey to always derive the key from a
Monday-anchored week start, independent of the mutable sundayWeekStart setting;
preserve sundayWeekStart only for display-oriented startOfWeek behavior. Ensure
goalKey and all weekly goal progress, frozen-key, streak, history, and recap
lookups continue using stable Monday-based keys.
- Around line 287-305: Prevent both goal walkers from looping indefinitely on
invalid repeat-day values by adding and exporting a shared weekdayList(days)
normalizer in schedule-app/src/data/helpers.js, converting entries with Number
and retaining only integer values from 0 through 6, then building repeatDays
with it in the helpers walker at lines 287-305. In
schedule-app/src/data/goalHistory.js lines 24-28, import weekdayList and apply
it when creating repeatDays so the loops at lines 28 and 43 use the validated
list.
- Around line 167-171: Update daysSince to use Math.round instead of Math.floor
when converting the local-midnight millisecond difference into whole days,
preserving the Infinity result for missing dates and correcting calculations
across DST transitions.
In `@schedule-app/src/data/ics.js`:
- Around line 51-52: Update the DESCRIPTION construction in the notesParts flow
to join entries with a real newline rather than the currently double-escaped
separator, then continue passing the result through escapeText so it produces
the proper ICS line-break escape.
- Around line 38-56: Update the ICS export flow to fold every generated content
line to the RFC 5545 75-octet limit before joining the lines, including long
SUMMARY, LOCATION, and DESCRIPTION values. Apply folding after all event lines
are assembled and preserve CRLF joining and existing escaping.
- Around line 103-117: Update finishDate and the DTSTART/DTEND handling in the
ICS parser to validate externally supplied date and time components, preserve
DTEND’s date when present, and distinguish all-day DATE values from timed
events. Before the parsed event is pushed, validate that the end is not earlier
than the start and fall back to a sane end value for invalid, missing,
overnight, or zero-length ranges so actions.addEvent never receives an invalid
interval.
In `@schedule-app/src/data/image.js`:
- Around line 11-21: Wrap the full img.onload processing in image-loading code
with try/catch so every canvas-related failure settles the Promise via reject
instead of leaving it pending. Validate the result of canvas.getContext('2d')
before calling drawImage, preserve successful JPEG resolution, and ensure zero
dimensions or any other callback exception are propagated explicitly.
In `@schedule-app/src/data/maps.js`:
- Around line 98-121: Update openWithNativeFallback so blur does not cancel the
fallback while the document remains visible: remove the blur listener and its
corresponding cleanup, or only invoke cancel when document.visibilityState is
hidden. Preserve pagehide and visibilitychange handling and the existing timer
fallback behavior.
- Around line 82-93: Update tryNativeScheme to replace the hardcoded 600ms delay
with a named constant, consistent with FALLBACK_MS, and ensure the iframe is
removed when the handoff cancellation path runs as well as when the timer fires.
Preserve the existing native-scheme navigation behavior while guaranteeing
cleanup in both paths.
In `@schedule-app/src/data/nlSearch.js`:
- Around line 66-73: Update the weekday parsing in the search flow around wdRe
and nextWeekday so the optional “next” prefix is captured and explicitly selects
next-week behavior. Reuse the existing weekdayNextWeek helper from smartParse.js
for prefixed expressions, while retaining nextWeekday for plain weekday names,
and export that helper if needed.
- Around line 87-92: Update the contact-matching logic in the search function to
skip contacts whose trimmed name is empty before evaluating substring matches,
especially the f.includes branch. Preserve matching for contacts with non-empty
names and the existing null fallback.
In `@schedule-app/src/data/notifications.js`:
- Around line 92-119: Update the reminder scan around the event and task loops
to compare full date-time values rather than same-day minute values, allowing
lead times to trigger on the preceding date. Evaluate each next event occurrence
and task due timestamp, remove the same-day restriction that excludes prior-date
task reminders, and preserve the existing fire keys and reminder messaging. Add
coverage for an event and task occurring shortly after midnight with leads that
trigger the previous day.
In `@schedule-app/src/data/nudges.js`:
- Around line 123-137: Update computeNudges so each goal’s computeGoalStreak
result is calculated once and reused by the milestone loop. Preserve the
existing daily-goal streak computation and exclude weekly goals from milestone
processing, avoiding the second full walk through goals.
- Around line 15-140: Split computeNudges into separate dateNudge, streakNudge,
followUpNudge, overdueNudge, and milestoneNudge helpers, moving each rule block
and its scratch state into the corresponding builder. Have computeNudges compose
the builders in the existing priority order, omit null or absent results, and
retain the final nudges.slice(0, 3) cap.
- Around line 104-112: Normalize contact date fields at the data boundary used
by IMPORT_DATA and cloud sync so createdAt and lastContacted are stored as
YYYY-MM-DD before computeNudges invokes daysSince. Alternatively, update
fromISODate to safely accept full ISO timestamps while preserving existing
date-only handling, ensuring overdue contacts are not skipped.
In `@schedule-app/src/data/reconnect.js`:
- Around line 33-56: Update makeOverdueCheck to precompute a
latest-interaction-date index by contact ID once when creating the predicate,
using a single pass over interactions and retaining the latest date without
per-contact filtering, allocation, or sorting. Remove the predicate’s dependency
on hasContactHistory, use the index for history validation and the fallback
date, and preserve hasContactHistory as an exported helper for other callers.
In `@schedule-app/src/data/routePlanner.js`:
- Around line 187-190: Update formatDistance to avoid hardcoded distance-unit
behavior by introducing a named conversion constant and using the configured
units preference from settings when available. Return kilometres for metric
users and miles for imperial users, while preserving an appropriate
short-distance fallback and existing formatting behavior.
- Around line 26-47: Update optimizeRoute’s input boundary to validate every
stop’s lat and lng as finite numbers before any filtering, sorting, or distance
calculation; reject invalid stops immediately with a clear error identifying the
missing or invalid coordinates. Apply the same validation to start if it is
externally supplied, and keep valid-stop routing behavior unchanged.
- Around line 202-216: Update buildGoogleMapsUrl to enforce Google Maps limits:
allow at most 3 waypoints on mobile and 9 otherwise, and ensure the serialized
URL does not exceed 2,048 characters. When orderedStops exceeds these limits,
either split the route into supported segments or truncate it and expose a
visible warning to the user; preserve the existing URL generation for routes
within limits.
In `@schedule-app/src/data/smartParse.js`:
- Around line 447-451: Optimize findContact by avoiding per-candidate RegExp
construction during each parseQuickAdd call. Build reusable matching data once
for the current contact list, or use a lowercase word set for boundary-safe
matching, and reuse it while iterating candidates; preserve the existing
candidate-length ordering and return behavior.
- Around line 196-206: Update numeric-date handling in the parser around the
numeric match so month/day interpretation follows an explicit locale setting, or
reject ambiguous dates where both components are 12 or less when no order is
configured. Ensure accepted values are passed to calendarDate in the selected
order and ambiguous inputs are not converted into a confident incorrect date.
- Around line 97-386: Split parseQuickAdd into focused helpers matching the
numbered stages: stripRecurrence, stripReminder, stripDuration,
stripExplicitDate, stripRelativeDay, stripTimes, stripLocation, and linkContact.
Have each helper accept the current remaining text plus required context and
return the updated text with its stage-specific values, while preserving stage
order and behavior; keep parseQuickAdd as orchestration and ensure each function
stays under 50 lines.
- Around line 402-420: Validate that constructed dates preserve the requested
month in both calendarDate and dayOfMonth; reject impossible month/day
combinations such as February 30 or April 31 by returning null before applying
next-month rollover logic, while preserving valid date behavior.
In `@schedule-app/src/data/store.jsx`:
- Around line 376-377: Update the IMPORT_DATA handling in the store reducer to
validate and normalize the complete imported backup schema before merging it
into state, including goals, events, and settings shapes. Reject invalid
action.data with a clear error instead of updating state, while preserving valid
backup values and seed defaults where appropriate. Reuse the project’s existing
schema-validation utilities if available.
- Around line 153-155: Update the JSON parsing error path in the persisted-state
loader so the catch block does not silently replace unreadable localStorage data
with makeSeed(). Preserve the original persisted value, explicitly surface a
recovery or export option to the user, and only reset to seed data after that
recovery path is handled; ensure the persistence effect cannot overwrite the
unreadable value automatically.
In `@schedule-app/src/data/templates.js`:
- Around line 44-52: Harden makeTemplate by normalizing a missing name before
trimming, while preserving the existing fallback labels. In instantiate, replace
the duplicated date construction using the T00:00:00 literal with the shared
fromISODate helper from the data helpers module for both date conversions.
In `@schedule-app/src/data/toast.jsx`:
- Around line 64-68: The continuous gesture updates create new state objects
unnecessarily. In schedule-app/src/data/toast.jsx lines 64-68, move drag state
and pointer-move handling from ToastProvider into a ToastCard child to isolate
re-renders, and memoize showToast with useCallback so the context value remains
stable. In schedule-app/src/data/useEdgeFade.js lines 13-21, change the fade
setter to a functional update that returns prev when left and right are
unchanged.
- Around line 28-30: In the toast provider component, add unmount cleanup for
timerRef, closeTimerRef, and flyTimerRef using the existing React effect
pattern. Clear each non-null timer and reset its ref so pending callbacks cannot
call setToast or setClosing after unmount.
- Around line 57-63: Update onPointerDown to clear closeTimerRef in addition to
timerRef before starting the drag, and reset the closing state if needed so
grabbing a toast during the close animation cancels its pending removal.
Preserve the existing toast-action bypass and drag initialization behavior.
In `@schedule-app/src/data/useBackDismiss.js`:
- Around line 47-57: Update the popstate handling and overlay lifecycle in
useBackDismiss so history remains armed whenever any overlay is still registered
after the top overlay closes. Ensure unregistering a nested overlay re-arms the
entry when openCount remains greater than zero, while preserving the existing
behavior for fully closing the final overlay and for onBack returning true.
- Around line 27-42: The route-change branch in maybePop must remove the stale
overlay history entry instead of only clearing pushedForApp and returning.
Replace the hash-mismatch cleanup with a history operation that closes or
replaces the overlay entry while preserving the destination route state, and do
not require event.state.__overlay because the destination is the underlying
route.
In `@schedule-app/src/data/useDeleteContact.js`:
- Around line 20-27: Refactor the snapshot construction in the delete flow
around the list.map callback to pre-group state.events, state.pins,
state.interactions, and state.notes by contactId in single passes before
iterating contacts. Have each snapshot read its affectedEvents, removedPins,
unlinkedPins, affectedInteractions, and affectedNotes from the corresponding
groups while preserving the existing pin-source partitioning and empty-array
defaults.
- Around line 32-41: Update useDeleteContactWithUndo to snapshot tasks deleted
through followUpContactId and restore them in the showToast undo callback
alongside contacts, events, pins, interactions, and notes. Use the existing task
action and task snapshot data, preserving each task’s followUpContactId when
restoring.
In `@schedule-app/src/data/useEdgeFade.js`:
- Around line 7-12: Update useEdgeFade to document that the caller-provided deps
array must retain a fixed length across renders, and remove the need for callers
to trigger updates when the element’s content size changes by observing el with
ResizeObserver alongside the existing resize handling. Ensure observer setup
occurs only when ref.current exists and is properly disconnected during effect
cleanup, while preserving the existing fade recalculation behavior.
In `@schedule-app/src/data/useSmartAdd.js`:
- Around line 17-21: Apply shared boundary validation across all four sites: in
schedule-app/src/data/useSmartAdd.js lines 17-21, validate parsed.title and
reject unsupported kind values before choosing the event/task path; in
schedule-app/src/data/maps.js lines 61-69, allow only http: and https: URLs in
openInNewContext; in schedule-app/src/data/image.js lines 4-7, require an
image/* MIME type and enforce the maximum byte size before readAsDataURL; in
schedule-app/src/data/useDeleteContact.js lines 16-29, filter null or id-less
list entries before accessing c.id. Use Zod where appropriate and fail fast with
clear validation messages.
- Around line 61-65: Harden plusMinutes against malformed hhmm input by
validating the parsed time before arithmetic and returning the established safe
fallback instead of producing “NaN:NaN”. Replace hardcoded minute limits with
named constants, and ensure the calculated end time is always later than the
start time when clamping near midnight, preserving valid HH:MM output and
preventing zero-length events.
In `@schedule-app/src/data/vcard.js`:
- Around line 65-74: Update the property-key normalization in the vCard parsing
loop before the comparisons in the `TEL`/`EMAIL`/`ADR` branches so an optional
group prefix such as `ITEM1.` is removed, leaving `TEL` for matching. Preserve
parameter stripping and existing handling for ungrouped properties.
In `@schedule-app/src/main.jsx`:
- Around line 35-41: Update notifications.js around notify() to use
navigator.serviceWorker.getRegistration(), checking registration?.active before
attempting service-worker notification delivery and falling back to new
Notification(...) when no active worker exists; preserve the existing
active-worker path. In schedule-app/src/main.jsx around the service-worker
registration listener, replace the silent catch with an explicit status or
telemetry handling path for registration failures.
In `@schedule-app/src/pages/ContactDetailPage.jsx`:
- Around line 255-259: Update the follow-up draft construction in the contact
follow-up button handler and the effect that initializes the draft so
followUp.note always defaults to an empty string when absent. Ensure the save
logic around the note trim operation and the textarea binding receive a defined
string while preserving existing note values.
- Around line 91-105: Move the initialEditJsonRef useRef call above the if
(!contact) early return in ContactDetailPage, keeping it unconditionally invoked
on every render. Preserve the existing missing-contact UI and all other hook
behavior unchanged.
In `@schedule-app/src/pages/ContactsPage.jsx`:
- Around line 446-519: Extract the self-contained add-person form currently
rendered inside the EditorSheet in ContactsPage into a ContactForm component,
accepting value and onChange props and preserving the existing fields,
AvatarPicker, GroupPicker, and AddressField behavior. Replace the inline form
with ContactForm and update ContactDetailPage’s near-identical editor to reuse
it, keeping save and draft state management in each page.
- Around line 64-73: Update bulkExport so the temporary anchor is attached to
the document before triggering a.click(), then defer URL.revokeObjectURL until
after the browser has started the download (using the existing asynchronous
scheduling approach), and clean up the anchor afterward.
- Around line 126-133: Update the ContactsPage search filter and sort to safely
handle contacts with missing names, using an empty-string fallback before
lowercasing or comparing names. Also normalize potentially non-string tag values
in the tag search, such as via String(t || ''), while preserving the existing
matching and alphabetical ordering behavior.
- Around line 373-377: Update the contact-row button in the ContactsPage render
to expose its selection state through an appropriate ARIA attribute when
selectMode is active, using isSel as the source of truth. Keep navigation
behavior unchanged outside select mode and ensure assistive technology can
distinguish selected from unselected contacts.
- Around line 205-246: Handle FileReader failures in the import flow by adding a
reader.onerror callback alongside reader.onload. Display the same clear
vCard-read failure alert used for parsing errors, while preserving the existing
successful read and parsing behavior.
- Around line 18-22: Remove the named re-export of reconnectDaysOf and
makeOverdueCheck from ContactsPage.jsx, while retaining their direct import from
../data/reconnect.js for local use.
- Around line 104-114: Update the isOverdue useMemo in ContactsPage around
makeOverdueCheck to depend only on state.settings?.reconnectRemindersEnabled,
state.settings?.reconnectDays, and state.interactions, while preserving the
existing predicate behavior and overdue list memoization.
- Around line 235-241: Update the background geocoding IIFE around
syncContactAddressPin to support cancellation on component unmount and verify
each contact still exists in current state before dispatching its pin. Pass or
check the cancellation signal through the lookup flow, stop processing when
cancelled, and skip contacts removed or cleared before their result is
dispatched.
In `@schedule-app/src/pages/ContactTimelinePage.jsx`:
- Around line 1-16: The ContactTimelinePage module exceeds the 800-line limit
and should be split by feature. Extract the bulk-selection logic around
stageMasters, moveSelected, moveSelectedToDate, and deleteSelected; extract the
modal set; and move TimelineEntry into independent modules, then update
ContactTimelinePage imports and composition while preserving existing behavior.
- Around line 56-77: Refactor the entries useMemo around the event-processing
loop to iterate each state.events item once and generate only its occurrences
within the WINDOW_DAYS date range, instead of calling expandEventOnDay for every
event on every day. Preserve contact filtering, entry shape, interaction
handling, sorting, and memo dependencies.
- Around line 815-841: Clear the pending long-press timer when the TimelineEntry
component unmounts by adding a cleanup effect alongside the timer refs and
handlers. Reuse the existing cancel function or equivalent cleanup logic so the
timeout is cleared and related press state is reset before unmount; import
useEffect if needed.
In `@schedule-app/src/pages/GoalHistoryPage.jsx`:
- Around line 54-56: Update the GoalHistoryPage completion-rate calculation
around computeGoalStreak, metCount, and rate so history excludes periods before
the goal’s creation date, using the goal-history construction in
buildGoalHistory as the source of the bounded window. Also revise windowLabel
usage in the percentage caption and chart section so the displayed text
accurately describes the metric and does not misleadingly label the unbounded
period range as the completion-rate denominator.
- Around line 86-107: Update the daily heatmap and non-daily bar-chart
containers in the GoalHistoryPage render to include role="img" and an aria-label
summarizing the displayed history window, such as the number of met entries out
of the total. Keep the existing visual cells, bars, and title details unchanged.
In `@schedule-app/src/pages/GoalsPage.jsx`:
- Around line 217-221: Update the press-start handling for the goal buttons to
clear suppressClickRef.current before processing a new press, preventing a stale
hold suppression from affecting the next tap. Use the existing
hold/pointer-start handler associated with endHold, while preserving the current
onClick suppression behavior for completed repeating holds.
- Around line 271-290: Update openEdit and saveGoal to default missing category
and unit to empty strings before they reach controlled inputs or .trim() calls.
In saveGoal, construct persisted goal data from the explicit payload fields and
avoid spreading ...editing into updateGoal, so reminderOn and reminderTime are
not stored alongside reminder.
In `@schedule-app/src/pages/HomePage.jsx`:
- Around line 72-1036: Reduce both oversized page components below the 800-line
cap by extracting feature-specific code: in schedule-app/src/pages/HomePage.jsx
lines 72-1036, move the note editor sheet, task editor sheet, and home-block
renderers into schedule-app/src/pages/home/ and replace the renderTaskRow
closure with a TaskRow component receiving explicit props; in
schedule-app/src/pages/MapPage.jsx lines 29-904, move the pin editor sheet,
location-picking flow, and Leaflet marker effects into
schedule-app/src/pages/map/ modules such as PinEditorSheet.jsx,
PickLocationOverlay.jsx, and a useMapMarkers hook, preserving existing behavior
and wiring.
- Around line 468-487: Update openEditNote to construct editing state with
defaults for all optional note fields, matching the pattern used by
openEditTask. Ensure title and body are always strings and checklist, color, and
pinned receive safe defaults before saveNote or controlled inputs consume the
state; preserve stored values when present.
In `@schedule-app/src/pages/MapPage.jsx`:
- Around line 446-452: Update locateMe and the related error path around the
code at lines 479–489 to replace every alert call with the existing showToast
from useToast, preserving the current user-friendly error messages and
triggering the toast for unavailable geolocation, location failures, and the
additional reported error.
- Around line 110-118: Remove the render-phase `wasEditingRef` and
`setInitialEditingJson` logic, and introduce a `beginEdit` helper that
establishes the JSON baseline before assigning `editing`. Replace the three
`setEditing({...})` calls at the edit-entry points with `beginEdit({...})`,
while preserving `setEditing(null)` for all close paths so `dirty` compares
against the correct untouched draft.
- Around line 479-489: Update searchPickLocation to wrap the geocodeAddress(q)
await and subsequent result handling in try/catch/finally, ensuring
setPickSearching(false) always executes even when geocoding rejects. In the
catch path, show an appropriate user-facing error message instead of allowing
the async event-handler rejection to escape, while preserving the existing
successful location-selection flow.
- Around line 317-350: Track both timeout handles in the map setup effect:
retain the existing saveViewTimer handle and store the invalidateSize timeout
separately. In the cleanup returned by the effect, clear both timers before
calling map.remove(), preventing callbacks from running after unmount.
- Around line 379-392: Sanitize mapEmojiSize at its source before the
pin-rendering loop: coerce it to a finite number and clamp it between
module-level bounds defined alongside LONG_PRESS_MS. Use this validated numeric
value for grow, the --emoji-size style interpolation, and icon dimensions,
ensuring invalid or malicious settings cannot produce NaN or inject markup.
In `@schedule-app/src/pages/MorePage.jsx`:
- Line 137: Change the Event colors entry in the settings-group definitions to
use a unique group id instead of g16, while leaving the Feedback entry’s g16
unchanged. Ensure the corresponding grp call for Event colors uses the same new
id so accordion state, search results, and aria-controls remain isolated.
- Around line 1-34: Refactor MorePage so it retains only the settings index,
search, and layout responsibilities, moving each settings section into separate
components under a settings/ folder: AppearanceSettings, CalendarSettings,
PeopleSettings, MapSettings, NotificationSettings, and DataSettings. Update
MorePage to import and render these components while preserving existing
behavior and shared state/actions.
- Around line 756-771: Update the opacity slider block around the event settings
controls: render a real button or equivalent keyboard-accessible control for
non-Pro users that navigates to pricing, while keeping the range input enabled
and editable only for Pro users. Remove the unreachable non-Pro requirePro path
from the slider’s onChange handling, preserving selectTick and settings updates
for Pro users.
- Around line 302-310: Update exportData so URL.revokeObjectURL(url) runs
asynchronously after the anchor click, rather than in the same synchronous tick;
preserve the existing blob creation and download behavior.
In `@schedule-app/src/pages/PricingPage.jsx`:
- Around line 102-126: Introduce a shared error helper that logs the raw backend
error while mapping known error codes to fixed user-facing messages. In
schedule-app/src/pages/PricingPage.jsx lines 102-126, update handleUpgrade and
handleManage to use the helper instead of exposing err.message; in
schedule-app/src/pages/SharedCalendarDetailPage.jsx lines 50-62, update load
with a fixed load-failure message; and in lines 91-137, update sendInvite,
removeMember, saveEvent, and removeEvent with fixed action-specific messages.
Ensure no raw server-controlled error text reaches rendered state.
- Line 21: The pricing feature row’s “Color themes (22, incl. pastels)” text is
inconsistent with the 17 COLOR_SCHEMES and 6 PASTEL_SCHEMES entries used by
MorePage. Update the label to reflect the actual combined count, or derive it
from those scheme collections so it stays synchronized.
In `@schedule-app/src/pages/RoutePlannerPage.jsx`:
- Around line 125-151: Wrap the body of planRoute in try/finally so
setPlanning(false) always executes when findStart or optimizeRoute fails,
including malformed stop.start values. Add a catch using the page’s existing
user-facing error mechanism to report the failure, then preserve the current
successful route setup and early-return behavior.
- Around line 70-77: Replace the one-time lazy initializer for selected in
RoutePlannerPage with a post-hydration effect that runs when pins first becomes
non-empty, derives overdue contact IDs using contactById and overdueCheck, and
seeds the selection. Add useRef and guard the effect so initialization occurs
only once while preserving any existing selection afterward.
In `@schedule-app/src/pages/SearchPage.jsx`:
- Around line 174-185: Remove nearestEventDate(ev) from the SearchPage event-row
render and use the event date already computed in the results memo, such as
ev.date, for display. Keep nearestEventDate exclusively in openEvent so
recurrence scanning does not run during keystroke-driven renders.
In `@schedule-app/src/pages/SharedCalendarDetailPage.jsx`:
- Around line 64-75: Reserve the full-page error branch in
SharedCalendarDetailPage for load failures only. Introduce or reuse an
actionError state, update the catch blocks in sendInvite, removeMember,
saveEvent, and removeEvent to call setActionError instead of setError, and
render actionError as an inline banner within the normal calendar layout so
failed actions preserve the page and unsaved editor input.
- Around line 114-128: Update saveEvent in SharedCalendarDetailPage to validate
that the event end time is strictly after the start time before calling
addSharedEvent or updateSharedEvent. Route this validation failure through the
existing inline action-error state, while preserving the current required-field
checks and API error handling.
- Around line 50-62: Update the useEffect surrounding load so each effect run
maintains a cancellation flag, sets it during cleanup, and prevents stale load
responses from calling setInfo or setError after id changes. Keep the existing
fetchCalendar behavior and dependency on id.
In `@schedule-app/src/pages/SharedCalendarJoinPage.jsx`:
- Around line 55-56: The error branch in SharedCalendarJoinPage must stop
rendering the raw error value: replace it with a fixed user-facing message and
log the original error for diagnostics. Add a retry button beside the message
that re-invokes the existing join/request flow.
- Around line 35-41: Update the SharedCalendarJoinPage authentication flow to
use isLoaded from useAuth: gate the acceptCalendarInvite effect and the sign-in
panel render condition so loading does not appear signed out. In the effect’s
success handler, validate that the API response contains a valid calendarId
before calling navigate; otherwise set an appropriate error and do not construct
a URL containing undefined.
In `@schedule-app/src/pages/SharedCalendarsPage.jsx`:
- Around line 55-63: Update the shared-calendar load path in load so failures
assign a non-null empty calendars value, preventing the page from remaining in
the Loading state, and replace err.message with the same generic user-safe error
message used by saveNew. In the error rendering near the calendars status, add a
“Try again” button that invokes load().
- Around line 70-80: Update saveNew to track an in-flight save with a pending
state: set it before createCalendar begins, clear it in finally, and pass that
state to EditorSheet through saveDisabled. Preserve the existing validation,
success, and error handling while preventing repeated Save clicks during the
request.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f72a9417-b498-4dd7-9d19-606a4b1e8670
⛔ Files ignored due to path filters (9)
backend/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/appstore-assets/icon-1024.pngis excluded by!**/*.pngschedule-app/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/public/apple-touch-icon.pngis excluded by!**/*.pngschedule-app/public/icon-192.pngis excluded by!**/*.pngschedule-app/public/icon-512-maskable.pngis excluded by!**/*.pngschedule-app/public/icon-512.pngis excluded by!**/*.pngschedule-app/public/icon-maskable.svgis excluded by!**/*.svgschedule-app/public/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (110)
.github/workflows/deploy-schedule-app.yml.gitignorebackend/.env.examplebackend/.gitignorebackend/README.mdbackend/package.jsonbackend/prisma.config.mjsbackend/prisma/migrations/20260722012026_init/migration.sqlbackend/prisma/migrations/20260722062956_add_user_data/migration.sqlbackend/prisma/migrations/20260727010000_lifetime_purchase/migration.sqlbackend/prisma/migrations/migration_lock.tomlbackend/prisma/schema.prismabackend/src/app.jsbackend/src/db.jsbackend/src/middleware/requireUser.jsbackend/src/routes/assistant.jsbackend/src/routes/billing.jsbackend/src/routes/calendars.jsbackend/src/routes/data.jsbackend/src/routes/me.jsbackend/src/routes/webhooksClerk.jsbackend/src/routes/webhooksStripe.jsbackend/src/server.jsschedule-app/.env.exampleschedule-app/README.mdschedule-app/index.htmlschedule-app/package.jsonschedule-app/public/manifest.webmanifestschedule-app/public/sw.jsschedule-app/src/App.jsxschedule-app/src/components/AddressField.jsxschedule-app/src/components/AnimatedNumber.jsxschedule-app/src/components/AssistantBubble.jsxschedule-app/src/components/Avatar.jsxschedule-app/src/components/Checkbox.jsxschedule-app/src/components/EditorSheet.jsxschedule-app/src/components/ExpandableFab.jsxschedule-app/src/components/GroupPicker.jsxschedule-app/src/components/Icon.jsxschedule-app/src/components/Logo.jsxschedule-app/src/components/MilestoneCelebration.jsxschedule-app/src/components/MiniMapPicker.jsxschedule-app/src/components/Modal.jsxschedule-app/src/components/ReorderToggleList.jsxschedule-app/src/components/Select.jsxschedule-app/src/components/SettingsGroup.jsxschedule-app/src/components/SettingsSection.jsxschedule-app/src/components/SmartQuickAdd.jsxschedule-app/src/components/SwipeRow.jsxschedule-app/src/components/SwipeToDelete.jsxschedule-app/src/components/TabBar.jsxschedule-app/src/components/Tutorial.jsxschedule-app/src/data/aiConfig.jsschedule-app/src/data/api.jsschedule-app/src/data/assistantContext.jsschedule-app/src/data/assistantTools.jsschedule-app/src/data/clerkConfig.jsschedule-app/src/data/conflicts.jsschedule-app/src/data/contactDates.jsschedule-app/src/data/contactInsights.jsschedule-app/src/data/contactSwipe.jsschedule-app/src/data/geocode.jsschedule-app/src/data/goalHistory.jsschedule-app/src/data/haptics.jsschedule-app/src/data/helpers.jsschedule-app/src/data/homeBlocks.jsschedule-app/src/data/ics.jsschedule-app/src/data/image.jsschedule-app/src/data/mapStyles.jsschedule-app/src/data/maps.jsschedule-app/src/data/nlSearch.jsschedule-app/src/data/notifications.jsschedule-app/src/data/nudges.jsschedule-app/src/data/pinLabel.jsschedule-app/src/data/quickAdd.jsschedule-app/src/data/reconnect.jsschedule-app/src/data/routePlanner.jsschedule-app/src/data/routePlannerConfig.jsschedule-app/src/data/seed.jsschedule-app/src/data/smartParse.jsschedule-app/src/data/store.jsxschedule-app/src/data/syncStatus.jsschedule-app/src/data/tabs.jsschedule-app/src/data/templates.jsschedule-app/src/data/toast.jsxschedule-app/src/data/useBackDismiss.jsschedule-app/src/data/useCountUp.jsschedule-app/src/data/useDeleteContact.jsschedule-app/src/data/useEdgeFade.jsschedule-app/src/data/useSmartAdd.jsschedule-app/src/data/vcard.jsschedule-app/src/data/weeklyRecap.jsschedule-app/src/main.jsxschedule-app/src/pages/ContactDetailPage.jsxschedule-app/src/pages/ContactTimelinePage.jsxschedule-app/src/pages/ContactsPage.jsxschedule-app/src/pages/GoalHistoryPage.jsxschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/HomePage.jsxschedule-app/src/pages/MapPage.jsxschedule-app/src/pages/MorePage.jsxschedule-app/src/pages/PlannerPage.jsxschedule-app/src/pages/PricingPage.jsxschedule-app/src/pages/RoutePlannerPage.jsxschedule-app/src/pages/SearchPage.jsxschedule-app/src/pages/SharedCalendarDetailPage.jsxschedule-app/src/pages/SharedCalendarJoinPage.jsxschedule-app/src/pages/SharedCalendarsPage.jsxschedule-app/src/styles.cssschedule-app/vite.config.js
| branches: | ||
| - main | ||
| - claude/missionary-calendar-app-ti6dr6 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
gh api "repos/${repo}/environments/github-pages" \
--jq '{deployment_branch_policy, protection_rules}'Repository: affaan-m/ECC
Length of output: 308
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/deploy-schedule-app.yml
printf '%s\n' '--- environment names ---'
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
gh api "repos/${repo}/environments" --paginate \
--jq '.environments[] | {name, deployment_branch_policy, protection_rules}'
printf '%s\n' '--- workflow runs for the feature branch ---'
gh api "repos/${repo}/actions/workflows/deploy-schedule-app.yml/runs?branch=claude/missionary-calendar-app-ti6dr6&per_page=10" \
--jq '.workflow_runs[] | {id, event, head_branch, status, conclusion, environment: .name, created_at}'
printf '%s\n' '--- repository Pages configuration ---'
gh api "repos/${repo}/pages" \
--jq '{build_type, source, html_url, status}' 2>&1 || trueRepository: affaan-m/ECC
Length of output: 2671
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="$(gh repo view --json nameWithOwner,defaultBranchRef --jq '{repo:.nameWithOwner,default:.defaultBranchRef.name}')"
printf '%s\n' "$repo"
name="$(printf '%s' "$repo" | jq -r .repo)"
default="$(printf '%s' "$repo" | jq -r .default)"
printf '%s\n' '--- workflows ---'
gh api "repos/${name}/actions/workflows" \
--jq '.workflows[] | {id, name, path, state}'
printf '%s\n' '--- workflow file on default branch ---'
gh api "repos/${name}/contents/.github/workflows/deploy-schedule-app.yml?ref=${default}" \
--jq '{path, sha, html_url}' 2>&1 || true
printf '%s\n' '--- Pages configuration ---'
gh api "repos/${name}/pages" \
--jq '{build_type, source, status, html_url}' 2>&1 || true
printf '%s\n' '--- environments ---'
gh api "repos/${name}/environments" --paginate \
--jq '.environments[] | {name, deployment_branch_policy, protection_rules}'Repository: affaan-m/ECC
Length of output: 3803
🌐 Web query:
GitHub Actions environments deployment branch policy environment does not exist auto-created deploy-pages github-pages
💡 Result:
When you run a GitHub Actions workflow that references an environment (such as github-pages) that does not yet exist, GitHub automatically creates the environment for you [1][2][3]. Key details regarding this behavior and deployment branch policies include: Environment Creation: If a workflow references a non-existent environment, it is created automatically upon the first run of that workflow [1][3]. Protection Rules: - Implicit Creation: If the environment is created as part of an implicit Pages build (e.g., from a branch or folder source), the source branch is automatically added as a deployment branch protection rule [1][3]. - Manual/Explicit Creation: If you create the environment via a workflow file or manually, it will not have any protection rules or secrets configured by default [1][3]. Deployment Policy Errors: You may encounter errors like "Invalid deployment branch" or "Branch is not allowed to deploy" if your repository's Pages source settings or environment protection rules are misaligned [4][5]. For example, if your Pages source is set to a specific branch, but your workflow attempts to deploy from a different branch without that branch being explicitly allowed in the github-pages environment protection rules, the deployment will fail [4][5]. To resolve deployment branch policy issues: 1. Navigate to your repository's Settings > Environments. 2. Select the github-pages environment (or the one you are targeting). 3. Review the Deployment branches section. Ensure that your intended deployment branches are included under Selected branches and tags or that the policy is set to All branches if you do not require restrictions [1][6][4]. 4. Verify your Pages source settings under Settings > Pages to ensure they align with your deployment strategy (if using Custom Workflows, it is recommended to set the source to GitHub Actions) [2][5].
Citations:
- 1: https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment
- 2: https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site
- 3: https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments
- 4: https://latchkey.dev/learn/github-actions/deploy-pages-deployment-failed-environment
- 5: Invalid deployment branch and no branch protection rules set in the environment actions/deploy-pages#33
- 6: https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments
Restrict Pages deployments to main
Once Pages is enabled, this trigger allows claude/missionary-calendar-app-ti6dr6 to publish to the same Pages site. Configure github-pages to allow only main, or remove this branch from on.push.branches. GitHub creates a referenced environment without branch protection by default.
🤖 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 @.github/workflows/deploy-schedule-app.yml around lines 9 - 11, Restrict the
deploy-schedule-app workflow’s on.push.branches configuration to main only by
removing claude/missionary-calendar-app-ti6dr6, so github-pages deployments
cannot originate from the feature branch.
Source: Path instructions
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/deploy-schedule-app.yml
printf '%s\n' '--- related package metadata ---'
if [ -f schedule-app/package.json ]; then
cat -n schedule-app/package.json
fi
printf '%s\n' '--- action references ---'
rg -n 'uses:|node-version|environment:|permissions:|npm (ci|run build)' .github/workflows/deploy-schedule-app.ymlRepository: affaan-m/ECC
Length of output: 3394
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Pages environment policy ---'
curl -fsSL https://api.github.com/repos/affaan-m/ECC/environments/github-pages \
-H 'Accept: application/vnd.github+json' |
jq '{name, protection_rules, deployment_branch_policy}'
printf '%s\n' '--- workflow and lockfile tracking ---'
git ls-files .github/workflows/deploy-schedule-app.yml schedule-app/package-lock.json
printf '%s\n' '--- workflow-level versus job-level permissions ---'
python3 - <<'PY'
from pathlib import Path
import re
text = Path(".github/workflows/deploy-schedule-app.yml").read_text()
workflow = re.search(r"(?ms)^permissions:\n(.*?)(?=^\S|\Z)", text)
build = re.search(r"(?ms)^ build:\n(.*?)(?=^ deploy:|\Z)", text)
deploy = re.search(r"(?ms)^ deploy:\n(.*)", text)
print("workflow_permissions:", workflow.group(1).strip().splitlines() if workflow else None)
print("build_has_job_permissions:", bool(build and re.search(r"(?m)^ permissions:", build.group(1))))
print("deploy_has_job_permissions:", bool(deploy and re.search(r"(?m)^ permissions:", deploy.group(1))))
PYRepository: affaan-m/ECC
Length of output: 543
🌐 Web query:
Node.js release schedule Node.js 20 end of life April 2026 official
💡 Result:
Node.js 20 (codenamed "Iron") reached its official end-of-life (EOL) on April 30, 2026 [1][2]. As of today, August 11, 2026, this version is no longer supported by the Node.js project, meaning it does not receive security patches, bug fixes, or any other updates [3][4]. Users still running applications on Node.js 20 are advised to migrate to a currently supported version of Node.js to ensure the security and stability of their environments [3][5].
Citations:
- 1: https://github.com/nodejs/Release/blob/main/README.md
- 2: https://github.com/nodejs/release?tab=readme-ov-file
- 3: https://nodejs.org/en/about/eol
- 4: https://endoflife.ai/nodejs/20
- 5: https://nodejs.org/en/blog/announcements/node-18-eol-support
🌐 Web query:
GitHub Actions job-level permissions workflow-level permissions GitHub Pages deployment pages write id-token write official documentation
💡 Result:
In GitHub Actions, the permissions key is used to configure the access levels granted to the GITHUB_TOKEN for either an entire workflow or individual jobs [1][2][3]. Workflow-Level vs. Job-Level Permissions The permissions setting can be defined at the top level of a workflow file, in which case it applies to all jobs in the workflow [1][2][4]. Alternatively, it can be defined within a specific job, which overrides any workflow-level settings for that job [2][4][5]. When defining permissions for a job, you must explicitly declare all required permissions, as job-level configurations do not merge with workflow-level settings; they entirely replace them [4]. GitHub Pages Deployment Requirements To successfully deploy to GitHub Pages, the job performing the deployment must be granted specific write permissions to the GITHUB_TOKEN [6][7][8]. Specifically, you must configure the following [6][7]: pages: write: This grants the GITHUB_TOKEN the necessary access to create a deployment via the GitHub API [6][9]. id-token: write: This allows the workflow to request an OpenID Connect (OIDC) token, which is used to verify that the deployment request originates from an authorized source (i.e., a GitHub workflow running in a specific repository and environment) [6][9][10]. If these permissions are omitted, you will typically encounter a 403 error, such as "Resource not accessible by integration" or "missing 'pages: write' permission," when the deployment step executes [8]. Configuration Example The following snippet demonstrates how to apply these permissions at the job level [6][7]: jobs: deploy: runs-on: ubuntu-latest permissions: pages: write # Required for deployment id-token: write # Required for OIDC verification steps: - name: Deploy to GitHub Pages uses: actions/deploy-pages@v4
Citations:
- 1: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax?use_case=
- 2: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- 3: https://github.com/github/docs/blob/main/content/actions/tutorials/authenticate-with-github_token.md
- 4: https://adaptive-enforcement-lab.com/secure/github-actions-security/token-permissions/job-scoping/
- 5: https://adaptive-enforcement-lab.com/secure/github-actions-security/token-permissions/
- 6: https://github.com/actions/deploy-pages
- 7: https://github.com/actions/deploy-pages/blob/main/README.md
- 8: https://latchkey.dev/learn/github-actions/github-actions-pages-permission-write-denied
- 9: More clarity on id-token: write actions/deploy-pages#329
- 10: https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-cloud-providers
Scope deployment permissions to the deploy job and harden the workflow.
- Keep only
contents: readat workflow level. Addpages: writeandid-token: writeunderdeploy.permissions. Thebuildjob runsnpm ciand must not inherit deployment credentials. - Pin
actions/checkout,actions/setup-node,actions/upload-pages-artifact, andactions/deploy-pagesto full commit SHAs. - Replace Node.js 20 with a supported LTS release.
🧰 Tools
🪛 zizmor (1.29.0)
[error] 20-20: overly broad permissions (excessive-permissions): pages: write is overly broad at the workflow level
(excessive-permissions)
[error] 21-21: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level
(excessive-permissions)
[warning] 20-20: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
🤖 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 @.github/workflows/deploy-schedule-app.yml around lines 18 - 21, Update the
workflow permissions so the top-level block retains only contents: read, while
the deploy job declares pages: write and id-token: write; keep deployment
credentials out of build. Pin checkout, setup-node, upload-pages-artifact, and
deploy-pages to full commit SHAs, and change the setup-node configuration from
Node.js 20 to a supported LTS release.
Sources: Path instructions, Linters/SAST tools
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for action in \
actions/checkout@v4 \
actions/setup-node@v4 \
actions/upload-pages-artifact@v3 \
actions/deploy-pages@v4
do
repo="${action%@*}"
tag="${action#*@}"
sha="$(git ls-remote "https://github.com/${repo}.git" "refs/tags/${tag}^{}" \
| awk 'NR == 1 { print $1 }')"
sha="${sha:-$(git ls-remote "https://github.com/${repo}.git" "refs/tags/${tag}" \
| awk 'NR == 1 { print $1 }')}"
printf '%s@%s # %s\n' "$repo" "$sha" "$tag"
doneRepository: affaan-m/ECC
Length of output: 421
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".github/workflows/deploy-schedule-app.yml"
sed -n '25,78p' "$file"
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/deploy-schedule-app.yml")
text = p.read_text()
expected = [
"actions/checkout@v4",
"actions/setup-node@v4",
"actions/upload-pages-artifact@v3",
"actions/deploy-pages@v4",
]
for ref in expected:
print(f"{ref}: {text.count(ref)} occurrence(s)")
PY
if command -v gh >/dev/null 2>&1; then
for item in \
"actions/checkout v4 11d5960a326750d5838078e36cf38b85af677262" \
"actions/setup-node v4 49933ea5288caeca8642d1e84afbd3f7d6820020" \
"actions/upload-pages-artifact v3 56afc609e74202658d3ffba0e8f6dda462b719fa" \
"actions/deploy-pages v4 d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e"
do
set -- $item
repo="$1/$2"
tag="$3"
sha="$4"
printf '%s tag: ' "$repo"
gh api "repos/$repo/git/ref/tags/$tag" --jq '.object.type + " " + .object.sha'
printf '%s commit: ' "$repo"
gh api "repos/$repo/commits/$sha" --jq '.sha'
done
else
echo "gh is not available; no API commit-resolution check was run" >&2
fiRepository: affaan-m/ECC
Length of output: 1403
Pin all four actions to full commit SHAs.
Replace the mutable version tags at lines 35, 40, 55, and 70 with immutable commit SHA references.
🧰 Tools
🪛 zizmor (1.29.0)
[error] 35-35: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
📍 Affects 1 file
.github/workflows/deploy-schedule-app.yml#L35-L35(this comment).github/workflows/deploy-schedule-app.yml#L40-L40.github/workflows/deploy-schedule-app.yml#L55-L55.github/workflows/deploy-schedule-app.yml#L70-L70
🤖 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 @.github/workflows/deploy-schedule-app.yml at line 35, Pin all four GitHub
Actions in .github/workflows/deploy-schedule-app.yml at lines 35, 40, 55, and 70
to immutable full commit SHA references, replacing their mutable version tags
while preserving the existing actions and workflow behavior.
Sources: Path instructions, Linters/SAST tools
| @@ -0,0 +1,3 @@ | |||
| node_modules/ | |||
| .env | |||
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Ignore all .env variants, not only .env.
The pattern .env does not match .env.local, .env.development, or .env.production. A developer who creates one of those files can commit live Clerk, Stripe, and Anthropic credentials. Keep .env.example tracked.
🔒 Proposed fix
node_modules/
-.env
+.env
+.env.*
+!.env.example
generated/As per coding guidelines: "NEVER hardcode secrets in source code - ALWAYS use environment variables or a secret manager".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .env | |
| node_modules/ | |
| .env | |
| .env.* | |
| !.env.example | |
| generated/ |
🤖 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 `@backend/.gitignore` at line 2, Update the backend ignore pattern so all .env
variants, including .env.local, .env.development, and .env.production, are
ignored while preserving tracking for .env.example.
Source: Coding guidelines
| model SharedCalendarMember { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| userId String | ||
| user User @relation(fields: [userId], references: [id], onDelete: Cascade) | ||
| role String @default("editor") // owner | editor | viewer | ||
| joinedAt DateTime @default(now()) | ||
|
|
||
| @@unique([calendarId, userId]) | ||
| } | ||
|
|
||
| // A pending invite by email, accepted via a tokenized link — the invited | ||
| // person doesn't need to exist as a User yet when the invite is created. | ||
| model SharedCalendarInvite { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| email String | ||
| token String @unique @default(cuid()) | ||
| role String @default("editor") | ||
| invitedBy String | ||
| createdAt DateTime @default(now()) | ||
| expiresAt DateTime | ||
| acceptedAt DateTime? | ||
| } | ||
|
|
||
| model SharedEvent { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| title String | ||
| date String // YYYY-MM-DD | ||
| start String // HH:MM | ||
| end String // HH:MM | ||
| notes String @default("") | ||
| createdBy String | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Add indexes on the foreign-key columns used by the calendar queries.
Prisma does not create indexes for relation scalar fields on PostgreSQL. Three supplied queries scan:
backend/src/routes/calendars.jslines 49-57 filtersSharedCalendarMemberbyuserIdonly. The@@unique([calendarId, userId])index is not usable becausecalendarIdis the leading column.backend/src/routes/calendars.jslines 100-103 filtersSharedEventbycalendarId.backend/src/routes/calendars.jslines 166-169 filtersSharedCalendarInvitebycalendarId.
Cascade deletes on calendar removal scan the same tables.
⚡ Proposed indexes
role String `@default`("editor") // owner | editor | viewer
joinedAt DateTime `@default`(now())
@@unique([calendarId, userId])
+ @@index([userId])
} expiresAt DateTime
acceptedAt DateTime?
+
+ @@index([calendarId])
} createdBy String
createdAt DateTime `@default`(now())
updatedAt DateTime `@updatedAt`
+
+ @@index([calendarId, date])
}Add the matching CREATE INDEX statements in a new migration.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| model SharedCalendarMember { | |
| id String @id @default(cuid()) | |
| calendarId String | |
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | |
| userId String | |
| user User @relation(fields: [userId], references: [id], onDelete: Cascade) | |
| role String @default("editor") // owner | editor | viewer | |
| joinedAt DateTime @default(now()) | |
| @@unique([calendarId, userId]) | |
| } | |
| // A pending invite by email, accepted via a tokenized link — the invited | |
| // person doesn't need to exist as a User yet when the invite is created. | |
| model SharedCalendarInvite { | |
| id String @id @default(cuid()) | |
| calendarId String | |
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | |
| email String | |
| token String @unique @default(cuid()) | |
| role String @default("editor") | |
| invitedBy String | |
| createdAt DateTime @default(now()) | |
| expiresAt DateTime | |
| acceptedAt DateTime? | |
| } | |
| model SharedEvent { | |
| id String @id @default(cuid()) | |
| calendarId String | |
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | |
| title String | |
| date String // YYYY-MM-DD | |
| start String // HH:MM | |
| end String // HH:MM | |
| notes String @default("") | |
| createdBy String | |
| createdAt DateTime @default(now()) | |
| updatedAt DateTime @updatedAt | |
| } | |
| model SharedCalendarMember { | |
| id String `@id` `@default`(cuid()) | |
| calendarId String | |
| calendar SharedCalendar `@relation`(fields: [calendarId], references: [id], onDelete: Cascade) | |
| userId String | |
| user User `@relation`(fields: [userId], references: [id], onDelete: Cascade) | |
| role String `@default`("editor") // owner | editor | viewer | |
| joinedAt DateTime `@default`(now()) | |
| @@unique([calendarId, userId]) | |
| @@index([userId]) | |
| } | |
| // A pending invite by email, accepted via a tokenized link — the invited | |
| // person doesn't need to exist as a User yet when the invite is created. | |
| model SharedCalendarInvite { | |
| id String `@id` `@default`(cuid()) | |
| calendarId String | |
| calendar SharedCalendar `@relation`(fields: [calendarId], references: [id], onDelete: Cascade) | |
| email String | |
| token String `@unique` `@default`(cuid()) | |
| role String `@default`("editor") | |
| invitedBy String | |
| createdAt DateTime `@default`(now()) | |
| expiresAt DateTime | |
| acceptedAt DateTime? | |
| @@index([calendarId]) | |
| } | |
| model SharedEvent { | |
| id String `@id` `@default`(cuid()) | |
| calendarId String | |
| calendar SharedCalendar `@relation`(fields: [calendarId], references: [id], onDelete: Cascade) | |
| title String | |
| date String // YYYY-MM-DD | |
| start String // HH:MM | |
| end String // HH:MM | |
| notes String `@default`("") | |
| createdBy String | |
| createdAt DateTime `@default`(now()) | |
| updatedAt DateTime `@updatedAt` | |
| @@index([calendarId, date]) | |
| } |
🤖 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 `@backend/prisma/schema.prisma` around lines 80 - 119, Add indexes for the
queried foreign-key fields: define indexes on SharedCalendarMember.userId,
SharedEvent.calendarId, and SharedCalendarInvite.calendarId, then create a new
Prisma migration containing the corresponding PostgreSQL CREATE INDEX
statements. Preserve the existing unique constraint and relation definitions.
| const [editing, setEditing] = useState(null); | ||
| const [initialEditingJson, setInitialEditingJson] = useState(''); | ||
| const wasEditingRef = useRef(false); | ||
| if (editing && !wasEditingRef.current) { | ||
| wasEditingRef.current = true; | ||
| setInitialEditingJson(JSON.stringify(editing)); | ||
| } else if (!editing && wasEditingRef.current) { | ||
| wasEditingRef.current = false; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Render-phase ref mutation makes dirty wrong under StrictMode.
Lines 113-118 mutate wasEditingRef.current during render and call setInitialEditingJson during render. React may render a component, discard the result, and render again. StrictMode in development does exactly that. On the discarded render the ref flips to true; on the retained render the condition is already false, so setInitialEditingJson never runs and initialEditingJson stays ''.
dirty at Line 821 then compares the draft against '' and reports unsaved changes for a pin the user never touched. EditorSheet shows the "You have unsaved changes" dialog on every close.
Set the baseline at the three points where editing is assigned instead.
🐛 Proposed fix
- const [initialEditingJson, setInitialEditingJson] = useState('');
- const wasEditingRef = useRef(false);
- if (editing && !wasEditingRef.current) {
- wasEditingRef.current = true;
- setInitialEditingJson(JSON.stringify(editing));
- } else if (!editing && wasEditingRef.current) {
- wasEditingRef.current = false;
- }
+ const [initialEditingJson, setInitialEditingJson] = useState('');
+ // Single entry point for opening the pin editor, so the dirty baseline is
+ // always captured from the same draft that goes into state.
+ const beginEdit = (draft) => {
+ setInitialEditingJson(JSON.stringify(draft));
+ setEditing(draft);
+ };Then replace setEditing({...}) with beginEdit({...}) at Line 154, Line 699, and Line 763. Keep setEditing(null) for the close paths.
🧰 Tools
🪛 ast-grep (0.45.1)
[error] 114-114: React's useState should not be directly called
Context: setInitialEditingJson(JSON.stringify(editing))
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(usestate-direct-usage)
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 110 - 118, Remove the
render-phase `wasEditingRef` and `setInitialEditingJson` logic, and introduce a
`beginEdit` helper that establishes the JSON baseline before assigning
`editing`. Replace the three `setEditing({...})` calls at the edit-entry points
with `beginEdit({...})`, while preserving `setEditing(null)` for all close paths
so `dirty` compares against the correct untouched draft.
Source: Linters/SAST tools
| let saveViewTimer = null; | ||
| map.on('moveend zoomend', () => { | ||
| if (pickModeRef.current) return; | ||
| clearTimeout(saveViewTimer); | ||
| saveViewTimer = setTimeout(() => { | ||
| const c = map.getCenter(); | ||
| actions.setSettings({ mapLastView: { lat: c.lat, lng: c.lng, zoom: map.getZoom() } }); | ||
| }, 500); | ||
| }); | ||
| mapRef.current = map; | ||
|
|
||
| // Native Pointer Events directly on the DOM element (see note above the | ||
| // handler definitions) — passive since we never call preventDefault. | ||
| const el = containerRef.current; | ||
| const onDown = (e) => handlersRef.current.onPressStart?.(e); | ||
| const onMove = (e) => handlersRef.current.onPressMove?.(e); | ||
| const onUp = () => handlersRef.current.onPressEnd?.(); | ||
| el.addEventListener('pointerdown', onDown, { passive: true }); | ||
| el.addEventListener('pointermove', onMove, { passive: true }); | ||
| el.addEventListener('pointerup', onUp, { passive: true }); | ||
| el.addEventListener('pointercancel', onUp, { passive: true }); | ||
|
|
||
| // Leaflet needs a nudge once the tab's layout settles. | ||
| setTimeout(() => map.invalidateSize(), 120); | ||
|
|
||
| return () => { | ||
| el.removeEventListener('pointerdown', onDown); | ||
| el.removeEventListener('pointermove', onMove); | ||
| el.removeEventListener('pointerup', onUp); | ||
| el.removeEventListener('pointercancel', onUp); | ||
| map.remove(); | ||
| mapRef.current = null; | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Two timers survive unmount.
Line 321 schedules saveViewTimer and Line 340 schedules the invalidateSize call. Neither is cleared in the cleanup at Lines 342-349.
Consequences:
- The
invalidateSizecallback runs up to 120 ms aftermap.remove(). Leaflet nulls_containerinremove(), so the call throwsTypeError. - The
saveViewTimercallback runs up to 500 ms after unmount and dispatchessetSettings, writing a map view for a page that is gone.
🐛 Proposed fix
// Leaflet needs a nudge once the tab's layout settles.
- setTimeout(() => map.invalidateSize(), 120);
+ const sizeTimer = setTimeout(() => map.invalidateSize(), 120);
return () => {
+ clearTimeout(sizeTimer);
+ clearTimeout(saveViewTimer);
el.removeEventListener('pointerdown', onDown);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let saveViewTimer = null; | |
| map.on('moveend zoomend', () => { | |
| if (pickModeRef.current) return; | |
| clearTimeout(saveViewTimer); | |
| saveViewTimer = setTimeout(() => { | |
| const c = map.getCenter(); | |
| actions.setSettings({ mapLastView: { lat: c.lat, lng: c.lng, zoom: map.getZoom() } }); | |
| }, 500); | |
| }); | |
| mapRef.current = map; | |
| // Native Pointer Events directly on the DOM element (see note above the | |
| // handler definitions) — passive since we never call preventDefault. | |
| const el = containerRef.current; | |
| const onDown = (e) => handlersRef.current.onPressStart?.(e); | |
| const onMove = (e) => handlersRef.current.onPressMove?.(e); | |
| const onUp = () => handlersRef.current.onPressEnd?.(); | |
| el.addEventListener('pointerdown', onDown, { passive: true }); | |
| el.addEventListener('pointermove', onMove, { passive: true }); | |
| el.addEventListener('pointerup', onUp, { passive: true }); | |
| el.addEventListener('pointercancel', onUp, { passive: true }); | |
| // Leaflet needs a nudge once the tab's layout settles. | |
| setTimeout(() => map.invalidateSize(), 120); | |
| return () => { | |
| el.removeEventListener('pointerdown', onDown); | |
| el.removeEventListener('pointermove', onMove); | |
| el.removeEventListener('pointerup', onUp); | |
| el.removeEventListener('pointercancel', onUp); | |
| map.remove(); | |
| mapRef.current = null; | |
| }; | |
| }, []); | |
| let saveViewTimer = null; | |
| map.on('moveend zoomend', () => { | |
| if (pickModeRef.current) return; | |
| clearTimeout(saveViewTimer); | |
| saveViewTimer = setTimeout(() => { | |
| const c = map.getCenter(); | |
| actions.setSettings({ mapLastView: { lat: c.lat, lng: c.lng, zoom: map.getZoom() } }); | |
| }, 500); | |
| }); | |
| mapRef.current = map; | |
| // Native Pointer Events directly on the DOM element (see note above the | |
| // handler definitions) — passive since we never call preventDefault. | |
| const el = containerRef.current; | |
| const onDown = (e) => handlersRef.current.onPressStart?.(e); | |
| const onMove = (e) => handlersRef.current.onPressMove?.(e); | |
| const onUp = () => handlersRef.current.onPressEnd?.(); | |
| el.addEventListener('pointerdown', onDown, { passive: true }); | |
| el.addEventListener('pointermove', onMove, { passive: true }); | |
| el.addEventListener('pointerup', onUp, { passive: true }); | |
| el.addEventListener('pointercancel', onUp, { passive: true }); | |
| // Leaflet needs a nudge once the tab's layout settles. | |
| const sizeTimer = setTimeout(() => map.invalidateSize(), 120); | |
| return () => { | |
| clearTimeout(sizeTimer); | |
| clearTimeout(saveViewTimer); | |
| el.removeEventListener('pointerdown', onDown); | |
| el.removeEventListener('pointermove', onMove); | |
| el.removeEventListener('pointerup', onUp); | |
| el.removeEventListener('pointercancel', onUp); | |
| map.remove(); | |
| mapRef.current = 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 `@schedule-app/src/pages/MapPage.jsx` around lines 317 - 350, Track both
timeout handles in the map setup effect: retain the existing saveViewTimer
handle and store the invalidateSize timeout separately. In the cleanup returned
by the effect, clear both timers before calling map.remove(), preventing
callbacks from running after unmount.
| const locateMe = () => { | ||
| if (!navigator.geolocation) return alert('Location is not available in this browser.'); | ||
| navigator.geolocation.getCurrentPosition( | ||
| (pos) => mapRef.current?.setView([pos.coords.latitude, pos.coords.longitude], 15), | ||
| () => alert('Could not get your location. Check your browser permissions.') | ||
| ); | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Replace alert() with the existing toast.
Lines 447, 450, and 485 report errors with alert(). The page already holds showToast from useToast() (Line 32) and uses it at Line 800. alert() blocks the main thread, cannot be styled, and is inconsistent with every other error surface in the app.
♻️ Proposed refactor
const locateMe = () => {
- if (!navigator.geolocation) return alert('Location is not available in this browser.');
+ if (!navigator.geolocation) return showToast('Location is not available in this browser.');
navigator.geolocation.getCurrentPosition(
(pos) => mapRef.current?.setView([pos.coords.latitude, pos.coords.longitude], 15),
- () => alert('Could not get your location. Check your browser permissions.')
+ () => showToast('Could not get your location. Check your browser permissions.')
);
};Apply the same change at Line 485.
As per coding guidelines: "Provide user-friendly error messages in UI-facing code".
Also applies to: 479-489
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 446 - 452, Update locateMe
and the related error path around the code at lines 479–489 to replace every
alert call with the existing showToast from useToast, preserving the current
user-friendly error messages and triggering the toast for unavailable
geolocation, location failures, and the additional reported error.
Source: Coding guidelines
| const searchPickLocation = async () => { | ||
| const q = pickQuery.trim(); | ||
| if (!q || pickSearching) return; | ||
| setPickSearching(true); | ||
| const hit = await geocodeAddress(q); | ||
| setPickSearching(false); | ||
| if (!hit) return alert("Couldn't find that address."); | ||
| setPickLatLng(hit); | ||
| mapRef.current?.setView([hit.lat, hit.lng], 16); | ||
| selectTick(); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
searchPickLocation swallows failures from geocodeAddress.
Line 483 awaits geocodeAddress(q) with no try/catch. If the helper rejects — a network failure, or a non-JSON response from the tile provider — the rejection escapes an async event handler. setPickSearching(false) at Line 484 never runs, so the Search button stays disabled for the rest of the session and the user gets no message.
🛡️ Proposed fix
const searchPickLocation = async () => {
const q = pickQuery.trim();
if (!q || pickSearching) return;
setPickSearching(true);
- const hit = await geocodeAddress(q);
- setPickSearching(false);
- if (!hit) return alert("Couldn't find that address.");
- setPickLatLng(hit);
- mapRef.current?.setView([hit.lat, hit.lng], 16);
- selectTick();
+ try {
+ const hit = await geocodeAddress(q);
+ if (!hit) {
+ showToast("Couldn't find that address.");
+ return;
+ }
+ setPickLatLng(hit);
+ mapRef.current?.setView([hit.lat, hit.lng], 16);
+ selectTick();
+ } catch {
+ showToast("Address search is unavailable right now.");
+ } finally {
+ setPickSearching(false);
+ }
};As per coding guidelines: "Handle errors explicitly at every level and never silently swallow errors" and "Use async/await with try-catch for error handling in TypeScript/JavaScript".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const searchPickLocation = async () => { | |
| const q = pickQuery.trim(); | |
| if (!q || pickSearching) return; | |
| setPickSearching(true); | |
| const hit = await geocodeAddress(q); | |
| setPickSearching(false); | |
| if (!hit) return alert("Couldn't find that address."); | |
| setPickLatLng(hit); | |
| mapRef.current?.setView([hit.lat, hit.lng], 16); | |
| selectTick(); | |
| }; | |
| const searchPickLocation = async () => { | |
| const q = pickQuery.trim(); | |
| if (!q || pickSearching) return; | |
| setPickSearching(true); | |
| try { | |
| const hit = await geocodeAddress(q); | |
| if (!hit) { | |
| showToast("Couldn't find that address."); | |
| return; | |
| } | |
| setPickLatLng(hit); | |
| mapRef.current?.setView([hit.lat, hit.lng], 16); | |
| selectTick(); | |
| } catch { | |
| showToast("Address search is unavailable right now."); | |
| } finally { | |
| setPickSearching(false); | |
| } | |
| }; |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 485-485: Avoid using the initial state variable in setState
Context: setPickLatLng(hit)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 479 - 489, Update
searchPickLocation to wrap the geocodeAddress(q) await and subsequent result
handling in try/catch/finally, ensuring setPickSearching(false) always executes
even when geocoding rejects. In the catch path, show an appropriate user-facing
error message instead of allowing the async event-handler rejection to escape,
while preserving the existing successful location-selection flow.
Source: Coding guidelines
| const load = async () => { | ||
| setError(''); | ||
| try { | ||
| const { calendars: list } = await fetchCalendars(getToken); | ||
| setCalendars(list); | ||
| } catch (err) { | ||
| setError(err.message); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
A failed load leaves the page on "Loading…" forever, and the raw error text is shown to the user.
Two defects in the same path:
loadsetserrorbut never setscalendars.calendarsstaysnull, so Line 110 keeps rendering "Loading…" underneath the error message. The user sees a permanent loading state with no retry.- Line 61 assigns
err.messagestraight to state, and Line 109 renders it. That message comes from the backend or fromfetch. It can contain request URLs, internal identifiers, or server exception text.
🛡️ Proposed fix
const load = async () => {
setError('');
try {
const { calendars: list } = await fetchCalendars(getToken);
setCalendars(list);
} catch (err) {
- setError(err.message);
+ setCalendars([]);
+ setError("Couldn't load your shared calendars. Check your connection and try again.");
}
};Apply the same generic message in saveNew at Line 78. Add a "Try again" button next to the error that calls load().
As per coding guidelines: "Ensure error messages do not leak sensitive data in responses" and "Error messages must be scrubbed of sensitive internals".
Also applies to: 109-111
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 58-58: Avoid using the initial state variable in setState
Context: setCalendars(list)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 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 `@schedule-app/src/pages/SharedCalendarsPage.jsx` around lines 55 - 63, Update
the shared-calendar load path in load so failures assign a non-null empty
calendars value, preventing the page from remaining in the Loading state, and
replace err.message with the same generic user-safe error message used by
saveNew. In the error rendering near the calendars status, add a “Try again”
button that invokes load().
Source: Coding guidelines
| const reply = await askAssistant( | ||
| getTokenRef.current, | ||
| convo, | ||
| buildAssistantContext(stateRef.current) | ||
| ); | ||
| // The assistant message goes back into the conversation exactly as | ||
| // it came, thinking blocks and all — dropping those breaks the next | ||
| // request when there are tool results to return alongside them. | ||
| convo = [...convo, { role: 'assistant', content: reply.content }]; | ||
| setMessages(convo); | ||
| if (reply.stop_reason !== 'tool_use') return; | ||
|
|
||
| const results = []; | ||
| for (const block of reply.content) { | ||
| if (block.type !== 'tool_use') continue; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Validate the assistant reply shape before iterating.
reply.content comes from the backend. Line 127 stores it directly and line 132 iterates it. If the server returns a non-array content, the for...of throws. The error reaches send's catch, so the user sees a generic failure instead of a clear message. Guard the shape at this boundary.
🛡️ Proposed guard
const reply = await askAssistant(
getTokenRef.current,
convo,
buildAssistantContext(stateRef.current)
);
+ if (!Array.isArray(reply?.content)) {
+ setError("The assistant sent something unexpected — try again.");
+ return;
+ }As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const reply = await askAssistant( | |
| getTokenRef.current, | |
| convo, | |
| buildAssistantContext(stateRef.current) | |
| ); | |
| // The assistant message goes back into the conversation exactly as | |
| // it came, thinking blocks and all — dropping those breaks the next | |
| // request when there are tool results to return alongside them. | |
| convo = [...convo, { role: 'assistant', content: reply.content }]; | |
| setMessages(convo); | |
| if (reply.stop_reason !== 'tool_use') return; | |
| const results = []; | |
| for (const block of reply.content) { | |
| if (block.type !== 'tool_use') continue; | |
| const reply = await askAssistant( | |
| getTokenRef.current, | |
| convo, | |
| buildAssistantContext(stateRef.current) | |
| ); | |
| if (!Array.isArray(reply?.content)) { | |
| setError("The assistant sent something unexpected — try again."); | |
| return; | |
| } | |
| // The assistant message goes back into the conversation exactly as | |
| // it came, thinking blocks and all — dropping those breaks the next | |
| // request when there are tool results to return alongside them. | |
| convo = [...convo, { role: 'assistant', content: reply.content }]; | |
| setMessages(convo); | |
| if (reply.stop_reason !== 'tool_use') return; | |
| const results = []; | |
| for (const block of reply.content) { | |
| if (block.type !== 'tool_use') continue; |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 127-127: Avoid using the initial state variable in setState
Context: setMessages(convo)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 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 `@schedule-app/src/components/AssistantBubble.jsx` around lines 119 - 133,
Validate that reply.content is an array immediately after askAssistant returns,
before appending it to convo or iterating in the tool-use path. If invalid,
surface a clear error through the existing send error-handling flow and avoid
storing or iterating the malformed content; preserve the current behavior for
valid replies.
Source: Coding guidelines
| const undoChange = (id) => { | ||
| const change = changes[id]; | ||
| if (!change || change.undone) return; | ||
| change.undo(); | ||
| setChanges((prev) => ({ ...prev, [id]: { ...prev[id], undone: true } })); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Wrap change.undo() in error handling.
undo is supplied by runAssistantTool. If it throws, the click handler rejects silently and the chip still shows "Undo". Catch the failure and report it.
🛡️ Proposed fix
const undoChange = (id) => {
const change = changes[id];
if (!change || change.undone) return;
- change.undo();
- setChanges((prev) => ({ ...prev, [id]: { ...prev[id], undone: true } }));
+ try {
+ change.undo();
+ } catch {
+ warnTick();
+ setError("That couldn't be undone.");
+ return;
+ }
+ setChanges((prev) => ({ ...prev, [id]: { ...prev[id], undone: true } }));
};As per coding guidelines: "Handle errors explicitly at every level; never swallow errors silently".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const undoChange = (id) => { | |
| const change = changes[id]; | |
| if (!change || change.undone) return; | |
| change.undo(); | |
| setChanges((prev) => ({ ...prev, [id]: { ...prev[id], undone: true } })); | |
| }; | |
| const undoChange = (id) => { | |
| const change = changes[id]; | |
| if (!change || change.undone) return; | |
| try { | |
| change.undo(); | |
| } catch { | |
| warnTick(); | |
| setError("That couldn't be undone."); | |
| return; | |
| } | |
| setChanges((prev) => ({ ...prev, [id]: { ...prev[id], undone: true } })); | |
| }; |
🤖 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 `@schedule-app/src/components/AssistantBubble.jsx` around lines 182 - 187,
Update undoChange to wrap change.undo() in explicit error handling, using the
error-reporting mechanism established in the component when the undo operation
throws. Only set undone: true after undo succeeds, so a failed operation does
not leave the chip showing an available undo state incorrectly.
Source: Coding guidelines
| {error && ( | ||
| <div className="assistant-error"> | ||
| <Icon name="warning" size={15} /> {error} | ||
| {error.includes('Pro') && ( | ||
| <button | ||
| className="btn btn-ghost small" | ||
| onClick={() => { | ||
| setOpen(false); | ||
| navigate('/pricing'); | ||
| }} | ||
| > | ||
| See Pro | ||
| </button> | ||
| )} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Do not branch UI on error message text.
Line 274 tests error.includes('Pro'). Line 172 sets that text from err.code === 'upgrade_required'. Any other backend message containing the word "Pro" also renders the upgrade button. Track the cause in state instead.
♻️ Proposed refactor
- const [error, setError] = useState('');
+ const [error, setError] = useState('');
+ const [needsUpgrade, setNeedsUpgrade] = useState(false);- if (err.code === 'upgrade_required') setError('The assistant is part of Pro.');
+ if (err.code === 'upgrade_required') {
+ setNeedsUpgrade(true);
+ setError('The assistant is part of Pro.');
+ }- {error.includes('Pro') && (
+ {needsUpgrade && (🤖 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 `@schedule-app/src/components/AssistantBubble.jsx` around lines 271 - 286,
Update the AssistantBubble error state and rendering so the upgrade-button
condition uses a dedicated flag or error-code state derived from err.code ===
'upgrade_required', rather than inspecting the error message text with
error.includes('Pro'). Preserve the existing error display and pricing
navigation, while ensuring unrelated backend messages containing “Pro” do not
render the button.
| const awaiting = useRef(null); | ||
| useEffect(() => { | ||
| if (!awaiting.current) return; | ||
| const made = state.statuses.find((s) => s.label === awaiting.current); | ||
| if (made) { | ||
| awaiting.current = null; | ||
| onChange(made.id); | ||
| } | ||
| }, [state.statuses, onChange]); | ||
|
|
||
| const options = [ | ||
| { value: '', label: 'No group' }, | ||
| ...state.statuses.map((s) => ({ value: s.id, label: s.label, color: s.color })), | ||
| { value: NEW, label: '+ New group…' }, | ||
| ]; | ||
|
|
||
| const openNew = () => { | ||
| // Offer a colour nothing else is using, so a new group is | ||
| // distinguishable at a glance instead of matching an existing one. | ||
| const used = new Set(state.statuses.map((s) => s.color)); | ||
| setDraft({ label: '', color: PRESET_COLORS.find((c) => !used.has(c)) || PRESET_COLORS[0] }); | ||
| }; | ||
|
|
||
| const create = () => { | ||
| const label = draft.label.trim(); | ||
| if (!label) return; | ||
| awaiting.current = label; | ||
| actions.addStatus({ label, color: draft.color }); | ||
| setDraft(null); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Match the new group by id diff, not by label.
create() does not prevent duplicate labels. If a group named "Family" already exists and the user creates another "Family", state.statuses.find((s) => s.label === awaiting.current) returns the pre-existing group. onChange then selects the wrong group id, and the newly created group stays unused. A stale awaiting.current also survives if no match ever appears.
Record the known ids before the create call and select the id that is new.
🐛 Proposed fix
- const awaiting = useRef(null);
+ const awaiting = useRef(null); // { label, knownIds: Set<string> } | null
useEffect(() => {
if (!awaiting.current) return;
- const made = state.statuses.find((s) => s.label === awaiting.current);
+ const { label, knownIds } = awaiting.current;
+ const made = state.statuses.find((s) => s.label === label && !knownIds.has(s.id));
if (made) {
awaiting.current = null;
onChange(made.id);
}
}, [state.statuses, onChange]);
@@
const create = () => {
const label = draft.label.trim();
if (!label) return;
- awaiting.current = label;
+ awaiting.current = { label, knownIds: new Set(state.statuses.map((s) => s.id)) };
actions.addStatus({ label, color: draft.color });
setDraft(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 `@schedule-app/src/components/GroupPicker.jsx` around lines 39 - 68, Update the
new-group flow in create and the awaiting useEffect to track status ids present
before actions.addStatus runs, then select the status whose id was not in that
recorded set once it appears. Stop matching by label, clear awaiting state after
resolving, and ensure it is cleared if no newly added status can be identified.
| useEffect(() => { | ||
| const map = L.map(containerRef.current, { | ||
| zoomControl: false, | ||
| attributionControl: false, | ||
| dragging: true, | ||
| tap: true, | ||
| }); | ||
| map.setView(lat != null ? [lat, lng] : DEFAULT_VIEW, lat != null ? 15 : 12); | ||
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); | ||
| map.on('click', (e) => onPickRef.current(e.latlng.lat, e.latlng.lng)); | ||
| mapRef.current = map; | ||
| setTimeout(() => map.invalidateSize(), 60); | ||
| return () => { | ||
| map.remove(); | ||
| mapRef.current = null; | ||
| }; | ||
| }, []); // eslint-disable-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Clear the invalidateSize timer in the cleanup function.
The cleanup runs map.remove() but leaves the 60 ms timer pending. If the form sheet closes within 60 ms, the callback runs map.invalidateSize() on a removed map. Leaflet clears container and pane internals in remove(), so this call throws.
🐛 Proposed fix
mapRef.current = map;
- setTimeout(() => map.invalidateSize(), 60);
+ const sizeTimer = setTimeout(() => map.invalidateSize(), 60);
return () => {
+ clearTimeout(sizeTimer);
map.remove();
mapRef.current = null;
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const map = L.map(containerRef.current, { | |
| zoomControl: false, | |
| attributionControl: false, | |
| dragging: true, | |
| tap: true, | |
| }); | |
| map.setView(lat != null ? [lat, lng] : DEFAULT_VIEW, lat != null ? 15 : 12); | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); | |
| map.on('click', (e) => onPickRef.current(e.latlng.lat, e.latlng.lng)); | |
| mapRef.current = map; | |
| setTimeout(() => map.invalidateSize(), 60); | |
| return () => { | |
| map.remove(); | |
| mapRef.current = null; | |
| }; | |
| }, []); // eslint-disable-line react-hooks/exhaustive-deps | |
| useEffect(() => { | |
| const map = L.map(containerRef.current, { | |
| zoomControl: false, | |
| attributionControl: false, | |
| dragging: true, | |
| tap: true, | |
| }); | |
| map.setView(lat != null ? [lat, lng] : DEFAULT_VIEW, lat != null ? 15 : 12); | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); | |
| map.on('click', (e) => onPickRef.current(e.latlng.lat, e.latlng.lng)); | |
| mapRef.current = map; | |
| const sizeTimer = setTimeout(() => map.invalidateSize(), 60); | |
| return () => { | |
| clearTimeout(sizeTimer); | |
| map.remove(); | |
| mapRef.current = null; | |
| }; | |
| }, []); // eslint-disable-line react-hooks/exhaustive-deps |
🤖 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 `@schedule-app/src/components/MiniMapPicker.jsx` around lines 18 - 34, Update
the useEffect cleanup in MiniMapPicker to retain the timeout handle created for
map.invalidateSize() and clear it before calling map.remove(). Ensure the
delayed callback cannot invoke invalidateSize on a removed map, while preserving
the existing map initialization and cleanup behavior.
| if (error) { | ||
| return ( | ||
| <div className="page"> | ||
| <header className="page-head"> | ||
| <button className="back-btn" onClick={() => navigate('/shared-calendars')}> | ||
| ‹ Shared calendars | ||
| </button> | ||
| </header> | ||
| <p className="muted center-pad">{error}</p> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
An action failure replaces the entire page.
error is a single state value shared by load, sendInvite, removeMember, saveEvent, and removeEvent. Line 64 returns the error-only page whenever error is non-empty. A failed invite or a failed event save therefore unmounts the calendar, the member list, and the open editor. The user loses unsaved input and has no retry path.
Reserve the full-page state for load failures, and show action failures inline.
🐛 Proposed fix
const [error, setError] = useState('');
+ const [actionError, setActionError] = useState(''); const load = async () => {
setError('');Change every action handler catch to setActionError(...), and render actionError as a banner inside the normal layout.
🤖 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 `@schedule-app/src/pages/SharedCalendarDetailPage.jsx` around lines 64 - 75,
Reserve the full-page error branch in SharedCalendarDetailPage for load failures
only. Introduce or reuse an actionError state, update the catch blocks in
sendInvite, removeMember, saveEvent, and removeEvent to call setActionError
instead of setError, and render actionError as an inline banner within the
normal calendar layout so failed actions preserve the page and unsaved editor
input.
| const saveEvent = async () => { | ||
| const { title, date, start, end, notes } = editingEvent; | ||
| if (!title.trim() || !date || !start || !end) return; | ||
| try { | ||
| if (editingEvent.id) { | ||
| await updateSharedEvent(getToken, id, editingEvent.id, { title, date, start, end, notes }); | ||
| } else { | ||
| await addSharedEvent(getToken, id, { title, date, start, end, notes }); | ||
| } | ||
| setEditingEvent(null); | ||
| load(); | ||
| } catch (err) { | ||
| setError(err.message); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate that the event end time is after the start time.
Line 116 checks presence only. A user can save an event with end earlier than start. The rendered row on line 198-201 shows only the start, so the invalid record is not visible until something downstream computes a duration.
🐛 Proposed fix
- if (!title.trim() || !date || !start || !end) return;
+ if (!title.trim() || !date || !start || !end) return;
+ if (end <= start) {
+ setError('The end time must be after the start time.');
+ return;
+ }Use the inline action-error state from the previous comment rather than setError.
As per coding guidelines: "Validate all user input before processing; use schema-based validation where available".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const saveEvent = async () => { | |
| const { title, date, start, end, notes } = editingEvent; | |
| if (!title.trim() || !date || !start || !end) return; | |
| try { | |
| if (editingEvent.id) { | |
| await updateSharedEvent(getToken, id, editingEvent.id, { title, date, start, end, notes }); | |
| } else { | |
| await addSharedEvent(getToken, id, { title, date, start, end, notes }); | |
| } | |
| setEditingEvent(null); | |
| load(); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| }; | |
| const saveEvent = async () => { | |
| const { title, date, start, end, notes } = editingEvent; | |
| if (!title.trim() || !date || !start || !end) return; | |
| if (end <= start) { | |
| setError('The end time must be after the start time.'); | |
| return; | |
| } | |
| try { | |
| if (editingEvent.id) { | |
| await updateSharedEvent(getToken, id, editingEvent.id, { title, date, start, end, notes }); | |
| } else { | |
| await addSharedEvent(getToken, id, { title, date, start, end, notes }); | |
| } | |
| setEditingEvent(null); | |
| load(); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| }; |
🤖 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 `@schedule-app/src/pages/SharedCalendarDetailPage.jsx` around lines 114 - 128,
Update saveEvent in SharedCalendarDetailPage to validate that the event end time
is strictly after the start time before calling addSharedEvent or
updateSharedEvent. Route this validation failure through the existing inline
action-error state, while preserving the current required-field checks and API
error handling.
Source: Coding guidelines
| useEffect(() => { | ||
| if (!isSignedIn) return; | ||
| acceptCalendarInvite(getToken, token) | ||
| .then(({ calendarId }) => navigate(`/shared-calendars/${calendarId}`, { replace: true })) | ||
| .catch((err) => setError(err.message)); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isSignedIn]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Wait for isLoaded, and validate calendarId before navigating.
Two defects in this effect.
First, useAuth() also returns isLoaded. Until Clerk resolves, isSignedIn is undefined. The render on line 48 treats that as signed out, so a signed-in user who opens an invite link sees the "Sign in to accept this invite" panel before the accept starts. Gate on isLoaded.
Second, ({ calendarId }) destructures the backend response with no check. If the field is absent, navigate produces the path /shared-calendars/undefined.
🐛 Proposed fix
- const { isSignedIn, getToken } = useAuth();
+ const { isLoaded, isSignedIn, getToken } = useAuth();
@@
useEffect(() => {
- if (!isSignedIn) return;
+ if (!isLoaded || !isSignedIn) return;
acceptCalendarInvite(getToken, token)
- .then(({ calendarId }) => navigate(`/shared-calendars/${calendarId}`, { replace: true }))
- .catch((err) => setError(err.message));
+ .then((res) => {
+ if (!res?.calendarId) throw new Error('That invite could not be accepted.');
+ navigate(`/shared-calendars/${res.calendarId}`, { replace: true });
+ })
+ .catch(() => setError('That invite could not be accepted. Ask for a new link.'));
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isSignedIn]);
+ }, [isLoaded, isSignedIn]);Update the render condition on line 48 to account for isLoaded as well, so the sign-in panel does not appear during the loading phase.
As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate".
🤖 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 `@schedule-app/src/pages/SharedCalendarJoinPage.jsx` around lines 35 - 41,
Update the SharedCalendarJoinPage authentication flow to use isLoaded from
useAuth: gate the acceptCalendarInvite effect and the sign-in panel render
condition so loading does not appear signed out. In the effect’s success
handler, validate that the API response contains a valid calendarId before
calling navigate; otherwise set an appropriate error and do not construct a URL
containing undefined.
Source: Coding guidelines
| ) : error ? ( | ||
| <p className="muted center-pad">{error}</p> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Do not render the raw backend error message.
Line 39 stores err.message and line 56 renders it. request() in schedule-app/src/data/api.js builds that message from the server response, so backend detail reaches the user interface unfiltered.
Show a fixed user-facing message and log the raw error for diagnostics. The fix in the previous comment already applies this.
The page also offers no way to retry after an error. Add a retry button next to the message.
As per coding guidelines: "Error messages must be scrubbed of sensitive internals".
🤖 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 `@schedule-app/src/pages/SharedCalendarJoinPage.jsx` around lines 55 - 56, The
error branch in SharedCalendarJoinPage must stop rendering the raw error value:
replace it with a fixed user-facing message and log the original error for
diagnostics. Add a retry button beside the message that re-invokes the existing
join/request flow.
Source: Coding guidelines
| const saveNew = async () => { | ||
| const name = adding.name.trim(); | ||
| if (!name) return; | ||
| try { | ||
| await createCalendar(getToken, name); | ||
| setAdding(null); | ||
| load(); | ||
| } catch (err) { | ||
| setError(err.message); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether the calendars POST route requires `color`.
fd -t f 'calendars.js' -x cat -n {} \;
echo '--- api.js calendar helpers ---'
rg -n -C3 'createCalendar|fetchCalendars' schedule-app/src/data/api.js
echo '--- prisma Calendar model ---'
fd -t f 'schema.prisma' -x rg -n -A15 'model Calendar' {} \;Repository: affaan-m/ECC
Length of output: 12964
🏁 Script executed:
#!/bin/bash
set -e
echo '--- SharedCalendarsPage.jsx ---'
cat -n schedule-app/src/pages/SharedCalendarsPage.jsx | sed -n '1,180p'
echo '--- EditorSheet definition and uses ---'
rg -n -C8 'function EditorSheet|const EditorSheet|export .*EditorSheet|<EditorSheet' schedule-app/src
echo '--- createCalendar payload behavior ---'
python3 - <<'PY'
import json
payload = {"name": "Example", "color": None}
print(json.dumps(payload))
print(json.dumps({"name": "Example", "color": None}).replace("null", "<undefined is represented by omitted JS property>"))
PYRepository: affaan-m/ECC
Length of output: 23056
🏁 Script executed:
#!/bin/bash
set -e
echo '--- SharedCalendarsPage.jsx ---'
cat -n schedule-app/src/pages/SharedCalendarsPage.jsx | sed -n '1,180p'
echo '--- EditorSheet definition and uses ---'
rg -n -C8 'function EditorSheet|const EditorSheet|export .*EditorSheet|<EditorSheet' schedule-app/src
echo '--- createCalendar payload behavior ---'
node - <<'JS'
const payload = { name: 'Example', color: undefined };
console.log(JSON.stringify(payload));
JSRepository: affaan-m/ECC
Length of output: 22960
🏁 Script executed:
#!/bin/bash
set -e
echo '--- EditorSheet.jsx ---'
cat -n schedule-app/src/components/EditorSheet.jsx | sed -n '1,180p'
echo '--- backend create-calendar contract ---'
rg -n -C4 "router\.post\('/'|const color|sharedCalendar\.create" schedule-app/backend schedule-app/server schedule-app -g '*.js' -g '*.prisma' 2>/dev/null || trueRepository: affaan-m/ECC
Length of output: 8184
Disable Save while saveNew is in flight. EditorSheet invokes onSave on every click, and this page does not pass saveDisabled or track a pending state. Repeated clicks can create duplicate calendars. Set a pending flag before the request and clear it in finally.
🤖 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 `@schedule-app/src/pages/SharedCalendarsPage.jsx` around lines 70 - 80, Update
saveNew to track an in-flight save with a pending state: set it before
createCalendar begins, clear it in finally, and pass that state to EditorSheet
through saveDisabled. Preserve the existing validation, success, and error
handling while preventing repeated Save clicks during the request.
Redesign the desert scene shown when every home tile is hidden to read closer to Chrome's offline dino game: drop the cow skull, add a small cluster of hollow blocky cacti plus a lone one further off, a bumpy ground line, and a tumbleweed (styled after a tangled-coil reference icon, twigs and all) that blows left to right on a 5s loop. The sky shows a filled sun in light mode and a filled crescent moon in dark mode, swapped purely in CSS off the existing html[data-theme] attribute. (The first crescent path had a geometrically invalid second arc — its radius couldn't span the given chord, so the browser silently rescaled it to match the outer arc and the crescent collapsed to nothing. Swapped in a known-good two-circle crescent path.) Also: the placeholder message below the scene now cycles through the list in order (once per visit, persisted in settings.emptyHomeMsgIndex) instead of picking randomly, so users actually see all of them.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Bring the scene's motion in line with the rest of the app's timing: - The whole scene now fades/settles in on mount (cubic-bezier(0.16, 0.8, 0.24, 1), the same ease used for page transitions) instead of just popping in fully opaque. - The tumbleweed's spin switched from flat linear to an eased curve, and gained a bounce layer with squash/stretch as it rolls, so it reads as a lively hop rather than a mechanically spinning circle. Matches the springy/eased feel used elsewhere (milestone pops, sheet slides) rather than introducing a new animation vocabulary.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
- Enlarge the cacti and ground them more convincingly: bigger trunks/ arms, tighter corner rounding so the base doesn't look like it's floating over the sand, and ground-line "dune" bumps repositioned under each cactus. - Give the sun 8 rays; the dark-mode crescent moon is unaffected. - Move the small lone cactus after the tumbleweed's clip group in DOM order so it now renders on top — the tumbleweed visibly rolls behind it as it crosses. - Add two small wind-scratch marks (styled after a reference wind-sketch icon) that trail ~20-45px behind the tumbleweed's own position throughout the crossing, fading in/out with it. - Rework the tumbleweed's motion into one 6s cycle instead of an infinite 5s loop: a 3s crossing with an eased, slow emergence from the left and two small bounces, then it goes fully invisible behind a clip-path "wall" at each edge of the floor (x: 4..196) for 3s before the next one starts — replacing the old always-visible infinite roll.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Actionable comments posted: 89
🤖 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 `@backend/package.json`:
- Around line 9-15: Add backend test tooling and a test script in package.json,
using an appropriate runner and coverage configuration that supports unit,
integration, and end-to-end tests. Add coverage enforcement for at least 80%,
and provide tests covering the authentication, billing, webhook, and sync
routes, including critical user flows.
In `@backend/prisma.config.mjs`:
- Around line 1-8: Update the datasource.url value in the defineConfig
configuration to use Prisma’s env helper with the DATABASE_URL key instead of
process.env.DATABASE_URL, while preserving the existing schema and
migrations.path settings.
In `@backend/prisma/migrations/20260722012026_init/migration.sql`:
- Around line 10-12: Update backend/prisma/schema.prisma by adding
`@db.Timestamptz`(3) to User.currentPeriodEnd, User.createdAt, User.updatedAt and
UserData.createdAt and UserData.updatedAt, then regenerate the migrations; do
not edit generated SQL directly. Apply the change for
backend/prisma/migrations/20260722012026_init/migration.sql lines 10-12 and
backend/prisma/migrations/20260722062956_add_user_data/migration.sql lines 6-7.
In `@backend/README.md`:
- Around line 171-177: Commit a Prisma migration for the shared-calendar schema
models SharedCalendar, SharedCalendarMember, SharedCalendarInvite, and
SharedEvent so npm run db:deploy creates their tables. Validate the migration
against an empty database and update the README’s migration status to reflect
that the migration is committed before enabling the calendar routes.
In `@backend/src/app.js`:
- Line 15: Validate FRONTEND_URL during startup in backend/src/app.js before
configuring CORS, throwing when it is unset, and pass the validated value
directly to cors({ origin }) without a permissive fallback. In
backend/src/routes/billing.js lines 50-51 and 69, continue using the validated
FRONTEND_URL for Stripe success_url, cancel_url, and billing-portal return_url;
these sites require no separate fallback logic.
- Around line 31-35: Apply a shared rate limiter in the app setup before
mounting the /api/me, /api/billing, /api/data, and /api/calendars routers,
ensuring every public API endpoint is covered while preserving the assistant
router’s existing limiter behavior. Reuse the project’s established
rate-limiting middleware or configuration rather than adding per-route limits.
In `@backend/src/routes/assistant.js`:
- Around line 298-307: Update the assistant request handler around messages
validation to define and apply a Zod schema before forwarding data to Anthropic.
Require each message to have an allowed role and supported content shape, reject
malformed entries and unknown keys with a specific 400 response, and preserve
the existing empty-array, count, and serialized-size limits.
In `@backend/src/routes/billing.js`:
- Around line 7-12: Validate that STRIPE_SECRET_KEY is present before
constructing the Stripe client at module load, and fail startup immediately with
a clear configuration error when it is missing. Update the initialization around
the stripe symbol while preserving normal construction when the secret is
configured.
In `@backend/src/routes/calendars.js`:
- Around line 209-214: Apply both per-user and per-IP rate limiting to the
router.post invite-accept endpoint before its handler logic, using the project’s
existing limiter middleware or conventions. Preserve the existing invite
validation and combine this protection with the required email-binding check for
the authenticated user before accepting the invite.
- Around line 92-114: The GET calendar handler currently exposes every member’s
raw email to viewers; update the members mapping in the `router.get('/:id')`
handler to return a display name or masked address for non-owner roles while
preserving owner access to raw emails. Keep the member list visible to all roles
and align the response with the owner-only exposure used by the invite-list
flow.
In `@backend/src/routes/data.js`:
- Around line 19-29: Replace the shallow object check in the PUT handler with
Zod validation for the sync blob. Define a top-level schema covering goals,
events, contacts, pins, tasks, notes, interactions, templates, statuses, and
settings, reject unknown keys and invalid nested shapes, and persist only the
parsed validated value in prisma.userData.upsert; return the existing 400
response for validation failures.
In `@backend/src/routes/webhooksClerk.js`:
- Around line 19-32: Extend the webhook switch to handle both user.created and
user.updated through the existing Clerk user upsert flow, keeping the email
selection logic synchronized with the latest primary address. When no email is
available, log an explicit error and fail the webhook request instead of
silently returning success, so Clerk can retry and the missing provisioning is
visible.
In `@backend/src/routes/webhooksStripe.js`:
- Around line 33-48: Guard the webhook grant flow before constructing or
executing updateMany: when both obj.client_reference_id and obj.customer are
absent, do not perform any user update. Keep the existing client-reference
preference and customer fallback for valid identifiers, and correct the nearby
idempotency comment to describe lifetimePurchasedAt: null as the effective retry
guard rather than attributing it to a unique lifetimeSessionId constraint.
- Around line 62-64: Update the currentPeriodEnd mapping in the subscription
webhook handler to read obj.items?.data?.[0]?.current_period_end when
obj.current_period_end is absent, then convert the selected Unix timestamp to a
Date and retain null when neither value exists.
In `@schedule-app/index.html`:
- Around line 5-8: Update the viewport meta declaration in the HTML head to
remove only the maximum-scale=1.0 restriction, preserving viewport-fit=cover,
interactive-widget=resizes-content, and the remaining viewport settings so
pinch-zoom remains available.
- Around line 36-41: In schedule-app/src/data/store.jsx, add a concise comment
next to STORAGE_KEY documenting that schedule-app/index.html is a second reader
of compass.data.v1 and settings.theme. Do not change the bootstrap reader or
storage behavior.
In `@schedule-app/package.json`:
- Around line 12-22: Upgrade the dependency versions in package.json by moving
vite from the unsupported Vite 5 range to a supported current major and
upgrading react from 18.3.1 to React 19.2.8; keep the existing React tooling and
dependency declarations consistent with the new React major.
- Around line 7-22: Add `@playwright/test` and a test script to
schedule-app/package.json, then create the tracked Playwright configuration and
specs for the schedule-app’s core flows. Update deploy-schedule-app.yml to
install the required Playwright browsers and run the test script after the
application build, preserving the existing CI steps.
In `@schedule-app/public/sw.js`:
- Line 28: Add service-worker environment configuration for public/sw.js,
preferably through an ESLint override targeting that file and enabling
globals.serviceworker; alternatively, add the eslint-env serviceworker directive
at the top of sw.js so self and caches are recognized without suppressing other
lint errors.
- Around line 96-111: In schedule-app/public/sw.js at lines 96-111, update the
fetch handler’s cache write inside the fetch response flow to run through
event.waitUntil, ensuring the caches.open(CACHE).then((cache) =>
cache.put(request, copy)) promise extends the event lifetime. Apply the same
event.waitUntil wrapping to the shell cache write at schedule-app/public/sw.js
lines 78-82 for caches.put('./index.html', copy); no other behavior changes are
needed.
In `@schedule-app/README.md`:
- Around line 16-17: Update the README sections covering features, pricing,
deployment/backend status, account model, and pages list to reflect the actual
local-first app with optional Clerk sign-in, backend sync, Stripe billing,
shared calendars, and assistant support. Remove obsolete claims about no
backend, demo-only Pro subscriptions, unconnected OAuth/sync, and the donation
button; add the omitted Search, Goal history, Route planner, and Shared
calendars pages.
- Around line 70-77: Remove the blank line separating the map and reminders
blockquotes in the README so they remain consecutive blockquote lines and
satisfy markdownlint MD028.
In `@schedule-app/src/App.jsx`:
- Around line 36-232: Extract the headless components SubscriptionSync,
DataSync, and ArrivalWatch from App.jsx into src/data/SubscriptionSync.jsx,
src/data/DataSync.jsx, and src/data/ArrivalWatch.jsx respectively. Move each
component’s related constants and imports, export the components, and update
App.jsx to import and render them while preserving their existing behavior and
lifecycle logic.
- Around line 195-229: Replace the empty geolocation error callback in
schedule-app/src/App.jsx lines 195-229 with a handler that logs the error and
informs the user when permission is denied, while preserving the non-fatal
watchPosition flow. In schedule-app/src/main.jsx lines 36-42, replace the empty
service-worker registration catch handler with logging of the failure reason and
keep registration failure non-fatal; update the related geolocation logic around
the useEffect callback and service-worker registration call.
- Around line 275-286: Move the HAPTIC_KINDS and MOVE_CANCEL_PX constant
declarations from the component body to module scope, outside the component.
Keep their values and the existing effect behavior unchanged while ensuring the
constants are not recreated on each render.
- Around line 150-172: Gate the debounced push effect in App.jsx behind a primed
state that becomes true when the initial pull settles, whether it succeeds or
fails. Update the pull effect to set primed in its completion path, and have the
push effect return without scheduling a timer until primed is true; preserve
existing skipNextPushRef handling and allow pushing after an initial pull
failure.
- Around line 412-417: Update the replay-tour useEffect to clear the route state
via React Router navigation: when location.state?.replayTour is true, call
navigate(location, { replace: true, state: null }) instead of
window.history.replaceState. Ensure navigate is available in the component and
included in the effect dependencies so React Router updates location and
generates the required location.key.
In `@schedule-app/src/components/AddressField.jsx`:
- Line 19: Update the DEBOUNCE_MS constant in AddressField to a delay of at
least 1000 milliseconds, ensuring geocoding requests cannot be triggered less
than one second apart.
In `@schedule-app/src/components/AssistantBubble.jsx`:
- Line 176: Update the error handling in the AssistantBubble component so
unknown errors no longer render err.message from /api/assistant; display a fixed
user-safe fallback message instead, while retaining the raw error text only for
logging.
In `@schedule-app/src/components/Avatar.jsx`:
- Around line 36-38: Update the catch block in the Avatar component to bind the
caught error, log its details for diagnostics, and replace the blocking alert
with the existing toast mechanism from the app’s toast system. Preserve the
user-friendly failure message while ensuring the error is explicitly handled
rather than discarded.
- Around line 45-48: The avatar picker button needs an accessible name when the
photo image has an empty alt and the badge icon is hidden from assistive
technology. Update the button in the Avatar picker rendering to provide an
appropriate accessible label, preserving the existing busy state and visual
content.
In `@schedule-app/src/components/Modal.jsx`:
- Around line 25-75: Update the Modal component’s open/close behavior around the
role="dialog" element to move focus into the dialog when open, trap Tab and
Shift+Tab within its focusable descendants, and restore focus to the element
that was active before opening after close. Use refs and an effect or equivalent
lifecycle handling, preserve the existing close and dialog interactions, and
handle cases where no focusable child exists by focusing the dialog itself.
In `@schedule-app/src/components/SwipeRow.jsx`:
- Around line 83-103: The onPointerUp handler uses stale render-closure dx
instead of the latest gesture offset. In the onPointerMove logic, store clamped
on gestureRef.current, then have onPointerUp use that stored offset for
actionFor selection and slideAway direction while preserving the existing armed
and dragging checks.
In `@schedule-app/src/data/api.js`:
- Around line 56-76: Update the request paths in fetchCalendar, renameCalendar,
deleteCalendar, removeCalendarMember, fetchCalendarInvites, inviteToCalendar,
revokeCalendarInvite, acceptCalendarInvite, addSharedEvent, updateSharedEvent,
and deleteSharedEvent to wrap every interpolated id, memberId, eventId,
inviteId, and token value with encodeURIComponent before constructing the URL.
- Around line 6-27: Update the request function to enforce a finite timeout for
fetch calls using an AbortController, aborting the request when the timeout
expires so callers do not remain pending indefinitely. Ensure the timer is
cleared when fetch settles and preserve existing response parsing and
error-status handling.
In `@schedule-app/src/data/assistantContext.js`:
- Around line 35-120: Refactor buildAssistantContext into four helpers:
peopleLines(state), calendarLines(state, iso), taskLines(state), and
placeLines(state), moving each corresponding section’s line construction into
its helper while preserving existing output and ordering. Concatenate the helper
results in buildAssistantContext, then apply MAX_LINES truncation only to the
combined lines before joining and returning the context.
- Around line 113-114: Update the pin formatting loop in the assistant context
builder to include only each pin’s label, retaining “Dropped pin” as the
fallback; remove the p.address value from the generated payload so addresses can
be retrieved through the appropriate tool.
- Around line 118-119: Update the final truncation logic in the assistant
context builder around MAX_LINES so content removed by lines.slice is explicitly
indicated, or raise MAX_LINES above the combined section-cap budget; preserve
the existing section formatting and ensure the output signals when sections were
omitted rather than implying no tasks or places exist.
- Around line 35-43: Update buildAssistantContext to derive iso from the
injected now value instead of calling todayISO(), ensuring the header and
downstream date-sensitive context use the same clock. Remove todayISO from the
imports if it is no longer referenced.
In `@schedule-app/src/data/assistantTools.js`:
- Around line 365-385: Update timelineEntries to avoid the fixed 521-day outer
loop: iterate each contact event in theirs once, derive its occurrences within
the current date window from the event’s recurrence rule, and append those
occurrences with the existing event metadata. Preserve the interaction handling
and final date sort, and reuse existing recurrence-expansion helpers where
available.
- Around line 310-315: Guard the contact lookup in the `pinStops.filter`
callback before calling `isOverdue`, matching the existing `c &&` pattern used
nearby. Ensure pins whose `contactId` has no entry in `contactById` are excluded
without invoking `isOverdue` with undefined.
In `@schedule-app/src/data/conflicts.js`:
- Around line 45-58: Update eventCoords to validate event.locLat/event.locLng
and pin.lat/pin.lng with Number.isFinite instead of typeof number checks,
preserving the existing coordinate lookup and null fallback behavior.
- Around line 87-92: In the conflict-pair loop, remove the unreachable `if (b.s
< a.e2) continue` guard and the `isNext` calculation/check around `timed.some`.
Keep the existing sorted traversal and overlap handling unchanged so each
non-overlapping pair is handled directly as the next-event candidate.
In `@schedule-app/src/data/contactDates.js`:
- Around line 116-140: Update contactDatesInMonth to handle Feb 29 rollover
consistently with nextAnnualOccurrence: when constructing occISO for a non-leap
year, preserve the occurrence as a valid March 1 date so the emitted entry is
assigned to the month represented by nextDate rather than being filtered or
labeled as February. Keep normal dates unchanged and ensure month filtering uses
the normalized occurrence month.
In `@schedule-app/src/data/contactSwipe.js`:
- Around line 25-30: In the contact swipe action’s run handler, normalize
contact.name to a safe display-name value before the interaction/contact updates
and showToast call. Use that normalized value when constructing the toast text
so contacts without a name do not trigger split on undefined, while preserving
the existing first-name display for named contacts.
In `@schedule-app/src/data/geocode.js`:
- Around line 11-14: Validate converted latitude and longitude values with
Number.isFinite before returning a geocode hit, and apply the same
finite-coordinate filter in suggestAddresses. In
schedule-app/src/data/geocode.js lines 11-14, reject hits with invalid
coordinates; in schedule-app/src/data/conflicts.js lines 45-58, replace both
typeof checks in eventCoords with Number.isFinite so invalid stored pins return
null.
- Around line 7-10: Add a shared request-timeout mechanism for both Nominatim
calls by applying it in geocodeOnce and combining the timeout signal with the
caller signal in suggestAddresses. Use AbortSignal.any when available, with a
manual AbortController fallback for older Safari/iOS, and preserve existing
response handling and caller cancellation behavior.
In `@schedule-app/src/data/goalHistory.js`:
- Around line 50-51: Update the caller of longestGoalStreak, particularly
GoalHistoryPage, to pass a lookback matching its displayed “Last 12 weeks”
window instead of relying on the 365-period default. Keep the existing
longestGoalStreak calculation unchanged unless the wider scan is explicitly
intended and documented.
In `@schedule-app/src/data/helpers.js`:
- Around line 296-305: Add the shared normalizeRepeatDays accessor in
schedule-app/src/data/helpers.js, filtering repeat-day values to unique integer
weekdays 0–6. In schedule-app/src/data/helpers.js lines 296-305, use it instead
of goal.repeatDays || [] so both backward walks terminate; in
schedule-app/src/data/goalHistory.js lines 24-28, import and use the same
accessor so its seek and step loops are likewise bounded by validated data.
- Around line 426-449: Refactor occursOn to delegate recurrence evaluation to
the existing matchesRule implementation, retaining only the skipDates exclusion
before or around that delegation. Remove the duplicated date bounds, diff
calculation, and daily/weekly/biweekly/monthly/custom switch from occursOn,
while preserving its behavior for non-repeating events and skipped dates.
- Around line 442-443: Update the monthly recurrence checks in the monthly case
and the corresponding logic near the second occurrence to apply the same
end-of-month clamping behavior documented by addMonths, so events anchored on
days 29–31 occur on the last valid day of shorter months. Preserve the existing
exact-day behavior for dates that exist in the target month.
- Around line 203-213: Update the weekly key-generation flow around weekKey()
and goalKey() so persistence keys always derive from the ISO-8601 Monday,
independent of sundayWeekStart. Keep sundayWeekStart available for calendar
rendering and navigation, but ensure weekly progress and frozenKeys continue
reading existing entries after the preference changes.
In `@schedule-app/src/data/ics.js`:
- Around line 80-119: Update finishDate and the event parsing flow to validate
imported calendar values: reject events with impossible dates or times,
including invalid DTSTART/DTEND values, before they reach actions.addEvent.
Track whether DTSTART is all-day and ensure its end defaults appropriately, and
when a parsed DTEND is not later than the event start (including a later
calendar day represented only by its time), clamp cur.end to cur.start. Preserve
valid event parsing and prevent invalid events from being pushed at END:VEVENT.
- Around line 51-52: Update the DESCRIPTION construction in the notesParts flow
to join entries with an actual newline character rather than the literal
backslash-n sequence, then continue passing the result through escapeText for
ICS escaping.
In `@schedule-app/src/data/image.js`:
- Around line 4-6: Update fileToAvatarDataUrl to validate the external file
before constructing or using FileReader: require an image MIME type and enforce
an appropriate maximum file size, rejecting immediately with clear error
messages when either check fails. Preserve the existing image-processing flow
for valid files.
- Around line 11-21: Update the img.onload handler in the image-loading function
to explicitly catch failures from canvas.getContext, drawImage, or toDataURL and
reject the promise with the original error. Handle a null 2D context as an
error, preserving the existing successful JPEG conversion behavior; define and
reuse module-scope OUTPUT_TYPE and OUTPUT_QUALITY constants for the toDataURL
call.
In `@schedule-app/src/data/nlSearch.js`:
- Around line 58-63: Validate the parsed offset in the `stripMatch` handling
before calling `addDays`: require `n` to be a safe integer within the supported
date-offset range, including the multiplied week value. For invalid values,
preserve the phrase in `keywords` or return the established clear parse-failure
result, and never pass invalid or overflowing values to `addDays`.
In `@schedule-app/src/data/notifications.js`:
- Around line 92-119: Update the event and task reminder loops to calculate
trigger and occurrence/due values as full date-times rather than comparing
time-of-day minutes only. Scan items whose trigger falls within the active
notification window, including next-day occurrences/tasks for pre-midnight
reminders, and retain deduplication keys while preventing notifications after
the intended occurrence time.
In `@schedule-app/src/data/reconnect.js`:
- Around line 45-53: Use one latest-contact-date rule across the affected sites:
in schedule-app/src/data/reconnect.js lines 45-53, derive the date from the
newest valid value among contact.lastContacted and all interaction dates; in
schedule-app/src/data/nudges.js lines 101-120, reuse that overdue rule without
counting createdAt as contact history; and in schedule-app/src/data/seed.js
lines 43-51, set lastContacted to the newest seeded interaction date.
In `@schedule-app/src/data/routePlanner.js`:
- Around line 187-190: Update formatDistance to use the existing
display-preference module-state pattern from setUse24hFormat, adding a
setter/state value for the preferred distance unit and rendering meters or miles
accordingly. Replace the hardcoded conversion and thresholds with named
constants or configuration, and ensure callers can configure the preference
before formatting.
- Around line 62-80: Update the inner loop in the route-planning flow around
nearestIdx and takeNearestFree so the already selected nearest candidate is
reused when the detour is accepted, avoiding a second nearestIdx computation
over the unchanged free list. Preserve the existing break conditions and anchor
ordering.
- Around line 202-216: Update buildGoogleMapsUrl to limit intermediate waypoints
to the supported Google Maps maximum, using the mobile-safe cap of 3, and make
the destination the final included stop. Return the generated URL together with
truncation metadata indicating how many ordered stops were omitted, so
RoutePlannerPage.jsx can report link coverage while preserving the empty-route
behavior.
In `@schedule-app/src/data/smartParse.js`:
- Around line 196-206: The numeric date branch in the date parsing flow must not
unconditionally interpret numeric[1] as the month. Update the handling around
numeric and calendarDate to use the configured locale’s date ordering, or reject
ambiguous two-part dates while continuing to accept unambiguous inputs; preserve
explicit monthDay and dayMonth parsing.
- Around line 402-412: Update calendarDate to reject monthIdx values above 11 in
addition to negative values, returning null before constructing any Date.
Preserve the existing day and year handling for valid month indices.
In `@schedule-app/src/data/store.jsx`:
- Around line 376-377: Update the IMPORT_DATA branch in the reducer to validate
and normalize action.data before merging it into state, reusing the same per-key
validation/merge logic as loadState. Preserve seeded defaults whenever imported
fields such as events or contacts are missing or have invalid non-array values.
- Around line 19-24: Update loadState and its JSON.parse error handler to
explicitly report parse failures, preserve the unreadable raw localStorage value
under a separate backup key, and only then fall back to makeSeed(). Ensure the
backup operation does not prevent the fallback if it fails, and retain the
existing successful-load behavior.
- Around line 380-393: Update the CLEAR_DATA branch in the reducer to derive its
returned state from makeSeed(), preserving only state.statuses and
state.settings from the existing state. Remove the manually listed collection
properties so newly added seed collections are included automatically.
In `@schedule-app/src/data/tabs.js`:
- Around line 32-35: Update the fallback branch in the tab-processing logic to
avoid mutating the object returned by out.find for the Home tab. Return or
construct a copied Home entry with enabled set to true, while preserving the
existing entries and behavior when Home is absent or another tab is enabled.
In `@schedule-app/src/data/templates.js`:
- Around line 44-50: Update makeTemplate so the stored blocks are cloned at
creation time: create a new array and a new object for each block before
assigning blocks to the returned template, preventing later caller mutations
from affecting the saved template.
In `@schedule-app/src/data/toast.jsx`:
- Around line 106-109: Replace the onPointerCancel={onPointerUp} wiring in the
toast pointer handlers with a dedicated cancel handler that clears dragStartRef,
resets drag, and restarts the dismissal timer without dismissing the toast. Keep
onPointerUp unchanged for genuine pointer releases.
- Around line 24-30: Update ToastProvider to clear timerRef, closeTimerRef, and
flyTimerRef in an unmount cleanup. Add a separate onPointerCancel path that
resets or terminates the drag without evaluating dismissal thresholds or calling
confirmTick(), ensuring cancelled gestures never dismiss the toast.
In `@schedule-app/src/data/useBackDismiss.js`:
- Around line 44-71: Update ensureListener and register so nested overlays keep
browser back interception armed: after handling popstate and invoking the top
entry’s onBack, call pushEntry whenever overlays remain, including when the top
overlay closes, while preserving the existing stillOpen handling; in register,
arm the shared entry on any transition from an unarmed state to at least one
open overlay rather than only when openCount equals one.
In `@schedule-app/src/data/useDeleteContact.js`:
- Around line 32-41: Update the undo callback in showToast to capture and use
only identifiers for affected events, unlinked pins, and affected notes, then
read each current record from the live store at undo time and create a new
object changing only contactId before dispatching the corresponding update
action. Preserve snapshot replay for removedPins and affectedInteractions, which
are genuinely deleted, and avoid mutating existing records.
- Around line 20-27: Update the snapshot construction in
useDeleteContactWithUndo to capture follow-up tasks whose followUpContactId
matches each contact’s id, then include those tasks in the undo restore flow
alongside the existing affected events, pins, interactions, and notes. Ensure
DELETE_CONTACT’s removed mirrored tasks are restored with the contact.
In `@schedule-app/src/data/vcard.js`:
- Around line 6-7: Update schedule-app/src/data/vcard.js lines 6-7 in
unescapeText to use one left-to-right escape-sequence parser, preventing escaped
backslashes from being reinterpreted as escaped n, comma, or semicolon
sequences. In schedule-app/src/data/vcard.js lines 37-47, update the N and ADR
parsing to split only on unescaped semicolons, then apply unescapeText to each
resulting field.
In `@schedule-app/src/main.jsx`:
- Around line 14-19: Update the clerkConfig module to export
CLERK_PUBLISHABLE_KEY from the environment variable and derive CLERK_ENABLED
from it, then import and use CLERK_PUBLISHABLE_KEY in the Root component’s
ClerkProvider instead of reading import.meta.env directly.
In `@schedule-app/src/pages/ContactDetailPage.jsx`:
- Line 66: Update the overdue calculation around makeOverdueCheck so it does not
use a pointless useMemo whose result is immediately invoked and whose state
dependency changes on every store update. Prefer removing useMemo and calling
makeOverdueCheck directly, unless the function’s required state inputs can be
identified and used as narrower dependencies.
- Around line 293-295: Derive a safe first-name value once near the top of the
render, after the early return, using an empty fallback when contact.name is
missing; then replace all four contact.name.split(' ')[0] usages in the
follow-up text and Done handler with that derived value.
In `@schedule-app/src/pages/ContactsPage.jsx`:
- Around line 206-244: Replace all three alert() calls in the reader.onload
handler with the existing showToast helper already used in the page. Preserve
the no-contacts and import-success messages, and provide user-friendly
actionable wording for the unreadable-file failure while keeping the existing
control flow.
In `@schedule-app/src/pages/HomePage.jsx`:
- Around line 505-510: Update the empty-home message rotation in the HomePage
effect to store the incremented emptyHomeMsgIndex in local component state or a
module-level variable instead of calling actions.setSettings. Preserve the
existing modulo rotation and isEmptyHome-triggered behavior, while ensuring the
cosmetic index never reaches persisted or cloud-synced settings.
- Around line 302-315: Seed all optional task, note, and pin fields in the
addTask, addNote, and addPin action creators, then remove reliance on
page-specific partial defaults. In schedule-app/src/pages/HomePage.jsx lines
302-315, add the title default in openEditTask and guard s.text in saveTask; in
schedule-app/src/pages/MapPage.jsx lines 458-467, guard editing.label and
editing.notes in savePin and default both at the edit entry point; in
schedule-app/src/pages/HomePage.jsx lines 474-477, default title, body, color,
and pinned in openEditNote. Preserve existing values while falling back safely
for missing imported or synced fields.
- Around line 302-315: Update openEditTask to default title to an empty string,
alongside the other normalized task fields, so editing tasks without a title
remains safe. Also update the corresponding actions.addTask normalization for
s.text to default it to an empty string before any trim or save logic uses it.
In `@schedule-app/src/pages/MapPage.jsx`:
- Around line 458-467: Update savePin to safely trim label and notes using
empty-string fallbacks, matching the existing emoji handling, so incomplete
imported or restored pins can be saved. Also update the edit-pin initialization
near the spread of the stored pin to default label and notes to empty strings,
keeping the controlled inputs consistently defined.
In `@schedule-app/src/pages/MorePage.jsx`:
- Around line 290-294: Update the copy branch in submitFeedback to handle
clipboard failures explicitly: only call setFeedback(null) after writeText
resolves successfully, and preserve the feedback draft when navigator.clipboard
is unavailable or the copy promise rejects. Replace the empty rejection handler
with user-visible failure handling consistent with the page.
In `@schedule-app/src/pages/PricingPage.jsx`:
- Around line 102-126: Validate the checkout and billing-portal URLs in
handleUpgrade and handleManage before assigning window.location.href: require a
present, well-formed URL and allow only the expected trusted billing
origin/protocol, otherwise surface an error and reset busy without navigating.
Add the same backendConfigured() guard used by handleUpgrade to handleManage,
preserving the existing sign-in and error flows.
In `@schedule-app/src/pages/RoutePlannerPage.jsx`:
- Around line 253-258: Update the full-route Google Maps link in the
RoutePlannerPage route-rendering block to remove the origin from the waypoint
list when route.startedFromStop is set. Preserve the existing route.stops list
for normal routes, while ensuring started-from-stop routes pass only the
remaining stops to buildGoogleMapsUrl.
In `@schedule-app/src/pages/SearchPage.jsx`:
- Around line 143-150: Give the search input in the SearchPage component a
persistent accessible name by associating it with a visible label or an
explicitly linked screen-reader-only label; retain the existing placeholder and
search behavior.
- Line 48: Update the contact-name lookup used by the state.events filter around
contactName so contacts are indexed once per memoized computation, rather than
calling state.contacts.find for every event. Build a reusable id-to-name map
from state.contacts and have contactName perform constant-time lookups while
preserving the existing empty-string fallback.
In `@schedule-app/src/pages/SharedCalendarDetailPage.jsx`:
- Around line 91-101: Update sendInvite to validate the trimmed email with the
existing schema or email validity check before calling inviteToCalendar, route
validation failures through the inline action-error state, and preserve the
existing success flow. Add an inviting in-flight guard and bind it to the Create
invite button via disabled={inviting} so repeated submissions are blocked.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 12ef22b6-7ccb-4211-b543-81246c9c2960
⛔ Files ignored due to path filters (9)
backend/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/appstore-assets/icon-1024.pngis excluded by!**/*.pngschedule-app/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/public/apple-touch-icon.pngis excluded by!**/*.pngschedule-app/public/icon-192.pngis excluded by!**/*.pngschedule-app/public/icon-512-maskable.pngis excluded by!**/*.pngschedule-app/public/icon-512.pngis excluded by!**/*.pngschedule-app/public/icon-maskable.svgis excluded by!**/*.svgschedule-app/public/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (111)
.github/workflows/deploy-schedule-app.yml.gitignorebackend/.env.examplebackend/.gitignorebackend/README.mdbackend/package.jsonbackend/prisma.config.mjsbackend/prisma/migrations/20260722012026_init/migration.sqlbackend/prisma/migrations/20260722062956_add_user_data/migration.sqlbackend/prisma/migrations/20260727010000_lifetime_purchase/migration.sqlbackend/prisma/migrations/migration_lock.tomlbackend/prisma/schema.prismabackend/src/app.jsbackend/src/db.jsbackend/src/middleware/requireUser.jsbackend/src/routes/assistant.jsbackend/src/routes/billing.jsbackend/src/routes/calendars.jsbackend/src/routes/data.jsbackend/src/routes/me.jsbackend/src/routes/webhooksClerk.jsbackend/src/routes/webhooksStripe.jsbackend/src/server.jsschedule-app/.env.exampleschedule-app/README.mdschedule-app/index.htmlschedule-app/package.jsonschedule-app/public/manifest.webmanifestschedule-app/public/sw.jsschedule-app/src/App.jsxschedule-app/src/components/AddressField.jsxschedule-app/src/components/AnimatedNumber.jsxschedule-app/src/components/AssistantBubble.jsxschedule-app/src/components/Avatar.jsxschedule-app/src/components/Checkbox.jsxschedule-app/src/components/DesertEmptyState.jsxschedule-app/src/components/EditorSheet.jsxschedule-app/src/components/ExpandableFab.jsxschedule-app/src/components/GroupPicker.jsxschedule-app/src/components/Icon.jsxschedule-app/src/components/Logo.jsxschedule-app/src/components/MilestoneCelebration.jsxschedule-app/src/components/MiniMapPicker.jsxschedule-app/src/components/Modal.jsxschedule-app/src/components/ReorderToggleList.jsxschedule-app/src/components/Select.jsxschedule-app/src/components/SettingsGroup.jsxschedule-app/src/components/SettingsSection.jsxschedule-app/src/components/SmartQuickAdd.jsxschedule-app/src/components/SwipeRow.jsxschedule-app/src/components/SwipeToDelete.jsxschedule-app/src/components/TabBar.jsxschedule-app/src/components/Tutorial.jsxschedule-app/src/data/aiConfig.jsschedule-app/src/data/api.jsschedule-app/src/data/assistantContext.jsschedule-app/src/data/assistantTools.jsschedule-app/src/data/clerkConfig.jsschedule-app/src/data/conflicts.jsschedule-app/src/data/contactDates.jsschedule-app/src/data/contactInsights.jsschedule-app/src/data/contactSwipe.jsschedule-app/src/data/geocode.jsschedule-app/src/data/goalHistory.jsschedule-app/src/data/haptics.jsschedule-app/src/data/helpers.jsschedule-app/src/data/homeBlocks.jsschedule-app/src/data/ics.jsschedule-app/src/data/image.jsschedule-app/src/data/mapStyles.jsschedule-app/src/data/maps.jsschedule-app/src/data/nlSearch.jsschedule-app/src/data/notifications.jsschedule-app/src/data/nudges.jsschedule-app/src/data/pinLabel.jsschedule-app/src/data/quickAdd.jsschedule-app/src/data/reconnect.jsschedule-app/src/data/routePlanner.jsschedule-app/src/data/routePlannerConfig.jsschedule-app/src/data/seed.jsschedule-app/src/data/smartParse.jsschedule-app/src/data/store.jsxschedule-app/src/data/syncStatus.jsschedule-app/src/data/tabs.jsschedule-app/src/data/templates.jsschedule-app/src/data/toast.jsxschedule-app/src/data/useBackDismiss.jsschedule-app/src/data/useCountUp.jsschedule-app/src/data/useDeleteContact.jsschedule-app/src/data/useEdgeFade.jsschedule-app/src/data/useSmartAdd.jsschedule-app/src/data/vcard.jsschedule-app/src/data/weeklyRecap.jsschedule-app/src/main.jsxschedule-app/src/pages/ContactDetailPage.jsxschedule-app/src/pages/ContactTimelinePage.jsxschedule-app/src/pages/ContactsPage.jsxschedule-app/src/pages/GoalHistoryPage.jsxschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/HomePage.jsxschedule-app/src/pages/MapPage.jsxschedule-app/src/pages/MorePage.jsxschedule-app/src/pages/PlannerPage.jsxschedule-app/src/pages/PricingPage.jsxschedule-app/src/pages/RoutePlannerPage.jsxschedule-app/src/pages/SearchPage.jsxschedule-app/src/pages/SharedCalendarDetailPage.jsxschedule-app/src/pages/SharedCalendarJoinPage.jsxschedule-app/src/pages/SharedCalendarsPage.jsxschedule-app/src/styles.cssschedule-app/vite.config.js
| "scripts": { | ||
| "dev": "node --watch src/server.js", | ||
| "start": "node src/server.js", | ||
| "db:generate": "prisma generate --config prisma.config.mjs", | ||
| "db:migrate": "prisma migrate dev --config prisma.config.mjs", | ||
| "db:deploy": "prisma migrate deploy --config prisma.config.mjs" | ||
| }, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Add a test script and test tooling for the backend.
scripts has no test entry, and devDependencies contains only prisma. The backend exposes authentication, billing, webhook, and sync routes. Those paths carry the highest risk in this stack and currently have no automated coverage.
As per coding guidelines: "Maintain at least 80% test coverage and provide unit, integration, and end-to-end tests, including critical user flows."
🤖 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 `@backend/package.json` around lines 9 - 15, Add backend test tooling and a
test script in package.json, using an appropriate runner and coverage
configuration that supports unit, integration, and end-to-end tests. Add
coverage enforcement for at least 80%, and provide tests covering the
authentication, billing, webhook, and sync routes, including critical user
flows.
Source: Coding guidelines
| import 'dotenv/config'; | ||
| import { defineConfig } from 'prisma/config'; | ||
|
|
||
| export default defineConfig({ | ||
| schema: 'prisma/schema.prisma', | ||
| migrations: { path: 'prisma/migrations' }, | ||
| datasource: { url: process.env.DATABASE_URL }, | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Prisma 7 prisma.config.ts defineConfig export from prisma/config datasource url
💡 Result:
In Prisma 7, database connection configuration has moved from the prisma/schema.prisma file to the prisma.config.ts file [1][2][3]. To configure the datasource URL, you must use the defineConfig helper exported from the prisma/config module in your prisma.config.ts file, which should be located at the root of your project [4][1][5]. Example Configuration To define the URL, import env from prisma/config to safely access your environment variables, ensuring that your environment is loaded (e.g., by importing dotenv/config at the top of the file) [4][2]: import "dotenv/config"; import { defineConfig, env } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { url: env("DATABASE_URL"), }, }); Key Changes and Best Practices 1. Removal of URL from Schema: In Prisma 7, the url field is no longer defined within the datasource block of the schema.prisma file [2][3]. 2. CLI Integration: Prisma CLI commands (such as prisma migrate or prisma generate) now read the connection URL directly from this prisma.config.ts file [1][6][3]. 3. Environment Variables: You should use the env helper provided by prisma/config [4][1]. While this helper provides type safety, it does not replace the need to load your environment variables using a package like dotenv [1][2]. 4. Migration and Direct Connections: If you previously used directUrl for migrations, you should now pass this connection string directly into the url field in prisma.config.ts, as the CLI uses this value for migration operations [1][5][6]. 5. Shadow Databases: If required, you can also define a shadowDatabaseUrl within the same datasource object in your configuration [1][5].
Citations:
- 1: https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7
- 2: https://www.prisma.io/docs/orm/v6/reference/prisma-config-reference
- 3: https://docs.prisma.io/docs/orm/reference/prisma-schema-reference
- 4: https://www.prisma.io/docs/orm/reference/prisma-config-reference
- 5: https://github.com/prisma/docs/blob/main/apps/docs/content/docs/guides/upgrade-prisma-orm/v7.mdx
- 6: https://www.prisma.io/docs/v6/orm/prisma-client/setup-and-configuration/databases-connections
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- package manifests ---'
find . -maxdepth 3 -type f \( -name 'package.json' -o -name 'pnpm-lock.yaml' -o -name 'package-lock.json' -o -name 'yarn.lock' \) -print
printf '%s\n' '--- Prisma references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' 'prisma|DATABASE_URL|migrations\s*:|datasource\s*:' . | head -240
printf '%s\n' '--- config file ---'
cat -n backend/prisma.config.mjsRepository: affaan-m/ECC
Length of output: 29304
🌐 Web query:
Prisma 7.9.0 prisma/config defineConfig datasource url migrations path env helper official documentation
💡 Result:
In Prisma 7.9.0, the prisma.config.ts file is the central location for managing project-wide configurations, including datasource connections and migration paths [1][2][3]. The defineConfig helper from prisma/config is used to structure this configuration [4][5]. The env helper, also imported from prisma/config, is designed to safely retrieve environment variables [4][6]. It verifies that the specified variable exists and is not empty, throwing an error if it is missing [6]. This eliminates the need for manual assertions or boilerplate code when accessing sensitive values like DATABASE_URL [4][6]. Below is an example of how to use defineConfig with the env helper in your prisma.config.ts file: import "dotenv/config"; import { defineConfig, env } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", migrations: { path: "prisma/migrations", }, datasource: { url: env("DATABASE_URL"), }, }); Key implementation notes: - Database Connection: Starting with Prisma 7, the url field for your datasource is defined in prisma.config.ts rather than directly in the schema.prisma file [1][3]. - Environment Safety: If a specific environment variable is not guaranteed to exist (such as in certain CI/CD pipelines during type-check runs), it is recommended to bypass the env helper and access process.env.DATABASE_URL directly with a fallback, such as process.env.DATABASE_URL ?? '' [1]. - Migration Path: The migrations.path property allows you to explicitly define the directory where your migration files are stored [4][5]. For full reference, see the official Prisma configuration documentation [4][5].
Citations:
- 1: https://www.prisma.io/docs/orm/v6/reference/prisma-config-reference
- 2: https://www.prisma.io/docs/prisma-postgres/from-the-cli
- 3: https://docs.prisma.io/docs/orm/reference/prisma-schema-reference
- 4: https://www.prisma.io/docs/orm/reference/prisma-config-reference
- 5: https://docs.prisma.io/docs/orm/reference/prisma-config-reference
- 6: feat(config): add
envhelper prisma/orm#28305
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Prisma package exports ---'
node - <<'JS'
const fs = require('fs');
const path = require('path');
const pkgPath = path.resolve('backend/node_modules/prisma/package.json');
const configPkgPath = path.resolve('backend/node_modules/@prisma/config/package.json');
for (const file of [pkgPath, configPkgPath]) {
console.log(`FILE ${file}`);
if (!fs.existsSync(file)) {
console.log('MISSING');
continue;
}
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
console.log(JSON.stringify({
name: pkg.name,
version: pkg.version,
exports: pkg.exports,
main: pkg.main,
module: pkg.module
}, null, 2));
}
JS
printf '%s\n' '--- config implementation references ---'
rg -n --glob '!*.map' 'defineConfig|function env|const env|migrations|datasource' \
backend/node_modules/@prisma/config backend/node_modules/prisma 2>/dev/null | head -160Repository: affaan-m/ECC
Length of output: 376
🌐 Web query:
site:prisma.io/docs/orm/reference/prisma-config-reference Prisma 7.9.0 env DATABASE_URL migrations path datasource url
💡 Result:
In Prisma ORM version 7, configuration of the database connection and migration behavior has been updated. Database URL Configuration In the schema.prisma file, the datasource block uses the url property to define the connection string. It is standard practice to use an environment variable to set this value, typically named DATABASE_URL. datasource db { provider = "postgresql" url = env("DATABASE_URL") } Environment Variables In Prisma ORM v7, environment variables from.env files are not loaded automatically by the Prisma CLI. You must explicitly load them in your prisma.config.ts file, which is generated by the prisma init command [1][2]. The recommended approach is to use dotenv: import 'dotenv/config'; // Your prisma.config.ts configuration here Prisma Configuration Updates Several properties in prisma.config.ts have been removed or simplified in version 7 [1][2]: - datasource.directUrl: This property has been removed in favor of using a single url property [1][2]. - adapter and engine: These properties have been removed [1][2]. - Migrations: Migrations for driver adapters now work automatically without needing additional configuration in prisma.config.ts [1][2]. Migrations Path The Prisma CLI resolves paths based on the location of your schema.prisma file and the execution context. While specific documentation on overriding the migrations directory path via environment variables is limited in the provided sources, the Prisma CLI generally expects a migrations folder relative to the schema file location [1][2]. Ensure your environment is set up to handle the necessary path resolution if you are working in a non-standard file structure [1][2].
Citations:
- 1: https://www.prisma.io/docs/orm/reference/prisma-config-reference
- 2: https://docs.prisma.io/docs/orm/reference/prisma-config-reference
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Prisma package exports ---'
node - <<'JS'
const fs = require('fs');
const path = require('path');
const files = [
path.resolve('backend/node_modules/prisma/package.json'),
path.resolve('backend/node_modules/@prisma/config/package.json'),
];
for (const file of files) {
console.log(`FILE ${file}`);
if (!fs.existsSync(file)) {
console.log('MISSING');
continue;
}
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
console.log(JSON.stringify({
name: pkg.name,
version: pkg.version,
exports: pkg.exports,
main: pkg.main,
module: pkg.module,
}, null, 2));
}
JS
printf '%s\n' '--- config implementation references ---'
rg -n --glob '!*.map' 'defineConfig|function env|const env|migrations|datasource' \
backend/node_modules/@prisma/config backend/node_modules/prisma 2>/dev/null | head -160Repository: affaan-m/ECC
Length of output: 376
Use Prisma’s env helper for DATABASE_URL.
Replace process.env.DATABASE_URL with env('DATABASE_URL') so Prisma fails during config loading when the variable is missing. defineConfig from prisma/config, datasource.url, and migrations.path are valid in Prisma 7.9.0.
🤖 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 `@backend/prisma.config.mjs` around lines 1 - 8, Update the datasource.url
value in the defineConfig configuration to use Prisma’s env helper with the
DATABASE_URL key instead of process.env.DATABASE_URL, while preserving the
existing schema and migrations.path settings.
Source: Coding guidelines
| "currentPeriodEnd" TIMESTAMP(3), | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updatedAt" TIMESTAMP(3) NOT NULL, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Every DateTime column in the schema generates TIMESTAMP(3) and drops the UTC offset. The shared root cause is that no DateTime field in backend/prisma/schema.prisma carries a @db.Timestamptz attribute. Both migrations are generated output, so fix the schema and regenerate rather than editing the SQL.
backend/prisma/migrations/20260722012026_init/migration.sql#L10-L12: regenerate after adding@db.Timestamptz(3)to thecurrentPeriodEnd,createdAt, andupdatedAtfields of theUsermodel.backend/prisma/migrations/20260722062956_add_user_data/migration.sql#L6-L7: regenerate after adding@db.Timestamptz(3)to thecreatedAtandupdatedAtfields of theUserDatamodel.
🧰 Tools
🪛 Squawk (2.61.0)
[warning] 10-10: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 11-11: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 12-12: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
📍 Affects 2 files
backend/prisma/migrations/20260722012026_init/migration.sql#L10-L12(this comment)backend/prisma/migrations/20260722062956_add_user_data/migration.sql#L6-L7
🤖 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 `@backend/prisma/migrations/20260722012026_init/migration.sql` around lines 10
- 12, Update backend/prisma/schema.prisma by adding `@db.Timestamptz`(3) to
User.currentPeriodEnd, User.createdAt, User.updatedAt and UserData.createdAt and
UserData.updatedAt, then regenerate the migrations; do not edit generated SQL
directly. Apply the change for
backend/prisma/migrations/20260722012026_init/migration.sql lines 10-12 and
backend/prisma/migrations/20260722062956_add_user_data/migration.sql lines 6-7.
Source: Linters/SAST tools
| - **Shared calendars are scaffolded but not migrated.** The | ||
| `SharedCalendar` / `SharedCalendarMember` / `SharedCalendarInvite` / | ||
| `SharedEvent` models exist in `prisma/schema.prisma` and the routes in | ||
| `src/routes/calendars.js` are wired up, but no migration has been run | ||
| against any database — run `npm run db:migrate` (creates one against | ||
| your local dev DB) or `npm run db:deploy` (applies pending migrations, | ||
| for CI/production) once you're ready to turn this on. Invites don't send |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Commit a shared-calendar migration before enabling these routes.
This README states that the shared-calendar models and routes exist but no migration creates their tables. A fresh deployment runs npm run db:deploy, which only applies committed migrations. The calendar endpoints will fail against that database.
Generate and commit the migration with the schema change. Validate it against an empty database before release.
🤖 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 `@backend/README.md` around lines 171 - 177, Commit a Prisma migration for the
shared-calendar schema models SharedCalendar, SharedCalendarMember,
SharedCalendarInvite, and SharedEvent so npm run db:deploy creates their tables.
Validate the migration against an empty database and update the README’s
migration status to reflect that the migration is committed before enabling the
calendar routes.
| <meta | ||
| name="viewport" | ||
| content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
maximum-scale=1.0 blocks pinch-zoom.
Mobile Safari and several Android browsers honour maximum-scale=1.0 and disable pinch-zoom. Users with low vision cannot enlarge the planner grid, contact rows, or map labels. This fails WCAG 2.1 SC 1.4.4.
viewport-fit=cover and interactive-widget=resizes-content deliver the notch and keyboard behaviour on their own. Remove only the scale lock.
🛠️ Proposed fix
<meta
name="viewport"
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" | |
| /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" | |
| /> |
🤖 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 `@schedule-app/index.html` around lines 5 - 8, Update the viewport meta
declaration in the HTML head to remove only the maximum-scale=1.0 restriction,
preserving viewport-fit=cover, interactive-widget=resizes-content, and the
remaining viewport settings so pinch-zoom remains available.
| showToast(label, 'Undo', () => { | ||
| for (const s of snapshots) { | ||
| actions.addContact(s.contact); | ||
| s.affectedEvents.forEach((e) => actions.updateEvent({ ...e, contactId: s.contact.id })); | ||
| s.unlinkedPins.forEach((p) => actions.updatePin({ ...p, contactId: s.contact.id })); | ||
| s.removedPins.forEach((p) => actions.addPin(p)); | ||
| s.affectedInteractions.forEach((i) => actions.addInteraction(i)); | ||
| s.affectedNotes.forEach((n) => actions.updateNote({ ...n, contactId: s.contact.id })); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Undo overwrites edits made after the delete.
The undo callback runs when the user taps the toast, which can be several seconds after the delete. Lines 35, 36, and 39 pass the whole snapshot object to updateEvent, updatePin, and updateNote. Those actions replace the record.
The delete only unlinks these records; they stay in the store and stay editable. If the user edits the title of an unlinked event, then taps Undo, the snapshot overwrites that edit. This is a lost update, and the user has no second undo.
Restore only the contactId link, and read the current record at undo time.
As per coding guidelines: "Always create new objects and return new copies with changes applied; never mutate existing objects."
🐛 Proposed fix
Capture only the identifiers, then re-link against live state:
+import { useStore, useActions } from './store.jsx';
+import { useToast } from './toast.jsx';
...
export function useDeleteContactWithUndo() {
const { state } = useStore();
const actions = useActions();
const showToast = useToast();
+ // The undo runs later than the delete, so it must re-read live records
+ // rather than replay a stale copy of them.
+ const stateRef = useRef(state);
+ stateRef.current = state; const snapshots = list.map((c) => ({
contact: c,
- affectedEvents: state.events.filter((e) => e.contactId === c.id),
+ affectedEventIds: state.events.filter((e) => e.contactId === c.id).map((e) => e.id),
removedPins: (state.pins || []).filter((p) => p.contactId === c.id && p.source === 'contact-address'),
- unlinkedPins: (state.pins || []).filter((p) => p.contactId === c.id && p.source !== 'contact-address'),
+ unlinkedPinIds: (state.pins || []).filter((p) => p.contactId === c.id && p.source !== 'contact-address').map((p) => p.id),
affectedInteractions: (state.interactions || []).filter((i) => i.contactId === c.id),
- affectedNotes: state.notes.filter((n) => n.contactId === c.id),
+ affectedNoteIds: state.notes.filter((n) => n.contactId === c.id).map((n) => n.id),
})); showToast(label, 'Undo', () => {
+ const live = stateRef.current;
+ const relink = (collection, ids, update, contactId) => {
+ for (const id of ids) {
+ const current = (live[collection] || []).find((r) => r.id === id);
+ if (current) update({ ...current, contactId });
+ }
+ };
for (const s of snapshots) {
actions.addContact(s.contact);
- s.affectedEvents.forEach((e) => actions.updateEvent({ ...e, contactId: s.contact.id }));
- s.unlinkedPins.forEach((p) => actions.updatePin({ ...p, contactId: s.contact.id }));
+ relink('events', s.affectedEventIds, actions.updateEvent, s.contact.id);
+ relink('pins', s.unlinkedPinIds, actions.updatePin, s.contact.id);
s.removedPins.forEach((p) => actions.addPin(p));
s.affectedInteractions.forEach((i) => actions.addInteraction(i));
- s.affectedNotes.forEach((n) => actions.updateNote({ ...n, contactId: s.contact.id }));
+ relink('notes', s.affectedNoteIds, actions.updateNote, s.contact.id);
}
});removedPins and affectedInteractions are genuinely deleted, so replaying those copies stays correct.
🤖 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 `@schedule-app/src/data/useDeleteContact.js` around lines 32 - 41, Update the
undo callback in showToast to capture and use only identifiers for affected
events, unlinked pins, and affected notes, then read each current record from
the live store at undo time and create a new object changing only contactId
before dispatching the corresponding update action. Preserve snapshot replay for
removedPins and affectedInteractions, which are genuinely deleted, and avoid
mutating existing records.
Source: Coding guidelines
| function unescapeText(s = '') { | ||
| return s.replace(/\\n/g, '\n').replace(/\\,/g, ',').replace(/\\;/g, ';').replace(/\\\\/g, '\\'); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Parse vCard escape sequences before interpreting delimiters. A literal escaped backslash followed by n, ,, or ; is decoded incorrectly by the chained replacements. N and ADR also split escaped semicolons as field boundaries.
schedule-app/src/data/vcard.js#L6-L7: replace chained replacements with a single escape-sequence parser.schedule-app/src/data/vcard.js#L37-L47: splitNandADRonly on unescaped semicolons, then decode each field.
📍 Affects 1 file
schedule-app/src/data/vcard.js#L6-L7(this comment)schedule-app/src/data/vcard.js#L37-L47
🤖 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 `@schedule-app/src/data/vcard.js` around lines 6 - 7, Update
schedule-app/src/data/vcard.js lines 6-7 in unescapeText to use one
left-to-right escape-sequence parser, preventing escaped backslashes from being
reinterpreted as escaped n, comma, or semicolon sequences. In
schedule-app/src/data/vcard.js lines 37-47, update the N and ADR parsing to
split only on unescaped semicolons, then apply unescapeText to each resulting
field.
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [location.key, id]); | ||
| const reconnectDays = state.settings?.reconnectDays ?? 30; | ||
| const over = useMemo(() => makeOverdueCheck(state), [state])(contact); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
This useMemo provides no memoization.
The dependency is the whole state object, so makeOverdueCheck re-runs on every store change. The result is then invoked immediately, so the memoized value never outlives the render either way.
Either drop the useMemo or narrow the dependencies to what makeOverdueCheck reads.
🤖 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 `@schedule-app/src/pages/ContactDetailPage.jsx` at line 66, Update the overdue
calculation around makeOverdueCheck so it does not use a pointless useMemo whose
result is immediately invoked and whose state dependency changes on every store
update. Prefer removing useMemo and calling makeOverdueCheck directly, unless
the function’s required state inputs can be identified and used as narrower
dependencies.
| <p className="muted small"> | ||
| Nothing promised. Add one when you tell {contact.name.split(' ')[0]} you'll be in touch. | ||
| </p> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
contact.name.split(' ') crashes the page for a contact with no name.
Line 294 runs on every render of the follow-up section. Lines 317 and 339 do the same, and line 286 does it inside the Done handler.
The file already documents this exact class of data. Lines 118-120 state that "a contact that arrived by vCard import (or from an older save) can be missing keys entirely, and a bare .trim() on one of those threw and made the person uneditable." schedule-app/src/pages/ContactsPage.jsx line 212 copies name: c.name straight from parseVCard output with no fallback, so a vCard without an FN field produces exactly such a contact.
The result is worse than the .trim() case it describes. The whole detail page throws during render and shows nothing.
Add one derived first-name value near the top of the render and use it at all four sites.
🐛 Proposed fix
Derive it once, after the early return:
const isPro = !!state.settings?.isPro;
+ // Imported contacts can arrive with no name at all (see saveEdit below),
+ // and every "…with {first name}" string below renders unconditionally.
+ const firstName = (contact.name || '').split(' ')[0] || 'them';Then replace the four call sites:
- Nothing promised. Add one when you tell {contact.name.split(' ')[0]} you'll be in touch.
+ Nothing promised. Add one when you tell {firstName} you'll be in touch.- <p className="muted">No places pinned yet. Add {contact.name.split(' ')[0]}'s home or a spot you meet.</p>
+ <p className="muted">No places pinned yet. Add {firstName}'s home or a spot you meet.</p>- <p className="muted">Nothing scheduled. Add an event in the Planner and link {contact.name.split(' ')[0]}.</p>
+ <p className="muted">Nothing scheduled. Add an event in the Planner and link {firstName}.</p>- showToast(`Marked as followed up with ${contact.name.split(' ')[0]}.`);
+ showToast(`Marked as followed up with ${firstName}.`);Fixing parseVCard to always emit a name is the durable fix. This guard stops the crash today.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <p className="muted small"> | |
| Nothing promised. Add one when you tell {contact.name.split(' ')[0]} you'll be in touch. | |
| </p> | |
| const isPro = !!state.settings?.isPro; | |
| // Imported contacts can arrive with no name at all (see saveEdit below), | |
| // and every "…with {first name}" string below renders unconditionally. | |
| const firstName = (contact.name || '').split(' ')[0] || 'them'; | |
| <p className="muted small"> | |
| Nothing promised. Add one when you tell {firstName} you'll be in touch. | |
| </p> | |
| <p className="muted">No places pinned yet. Add {firstName}'s home or a spot you meet.</p> | |
| <p className="muted">Nothing scheduled. Add an event in the Planner and link {firstName}.</p> | |
| showToast(`Marked as followed up with ${firstName}.`); |
🤖 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 `@schedule-app/src/pages/ContactDetailPage.jsx` around lines 293 - 295, Derive
a safe first-name value once near the top of the render, after the early return,
using an empty fallback when contact.name is missing; then replace all four
contact.name.split(' ')[0] usages in the follow-up text and Done handler with
that derived value.
| reader.onload = () => { | ||
| try { | ||
| const imported = parseVCard(reader.result); | ||
| if (imported.length === 0) return alert('No contacts found in that file.'); | ||
| const newContacts = imported.map((c) => ({ | ||
| id: uid('c'), | ||
| name: c.name, | ||
| phone: c.phone, | ||
| email: c.email, | ||
| address: c.address, | ||
| photo: '', | ||
| statusId: state.statuses[0]?.id || '', | ||
| tags: [], | ||
| notes: c.notes, | ||
| lastContacted: '', | ||
| createdAt: todayISO(), | ||
| })); | ||
| newContacts.forEach((contact) => actions.addContact(contact)); | ||
| alert(`Imported ${newContacts.length} contact${newContacts.length === 1 ? '' : 's'}.`); | ||
| // Geocoding pins one at a time in the background, a beat apart — | ||
| // Nominatim's public endpoint enforces roughly one request per | ||
| // second, and firing every contact's lookup at once (the previous | ||
| // behaviour) meant only the first one or two ever got served; the | ||
| // rest were silently rate-limited, and a failed geocode is | ||
| // deliberately a no-op rather than an error (see geocodeAddress), | ||
| // so importing a dozen contacts quietly auto-pinned just one of | ||
| // them with nothing on screen to explain why. Sequenced with a gap | ||
| // between requests, every address actually gets geocoded — just | ||
| // progressively rather than all at once. | ||
| (async () => { | ||
| for (const contact of newContacts) { | ||
| if (!contact.address) continue; | ||
| await syncContactAddressPin(contact, state, actions); | ||
| await new Promise((r) => setTimeout(r, 1100)); | ||
| } | ||
| })(); | ||
| } catch { | ||
| alert('That file could not be read as a vCard (.vcf) file.'); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Replace alert() with the toast already wired into this page.
Lines 209, 224, and 243 call alert(). alert() blocks the main thread, renders browser chrome that breaks the installed-PWA look, and cannot be styled or dismissed by a gesture.
showToast is already imported on line 15 and used on line 82. The success case on line 224 is a plain confirmation and maps directly. The two failure cases need a message the user can act on.
As per coding guidelines: "Provide user-friendly error messages in UI-facing code."
♻️ Proposed change
const imported = parseVCard(reader.result);
- if (imported.length === 0) return alert('No contacts found in that file.');
+ if (imported.length === 0) {
+ showToast('No contacts found in that file.');
+ return;
+ } newContacts.forEach((contact) => actions.addContact(contact));
- alert(`Imported ${newContacts.length} contact${newContacts.length === 1 ? '' : 's'}.`);
+ showToast(`Imported ${newContacts.length} contact${newContacts.length === 1 ? '' : 's'}.`); } catch {
- alert('That file could not be read as a vCard (.vcf) file.');
+ showToast('That file is not a readable vCard (.vcf).');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| reader.onload = () => { | |
| try { | |
| const imported = parseVCard(reader.result); | |
| if (imported.length === 0) return alert('No contacts found in that file.'); | |
| const newContacts = imported.map((c) => ({ | |
| id: uid('c'), | |
| name: c.name, | |
| phone: c.phone, | |
| email: c.email, | |
| address: c.address, | |
| photo: '', | |
| statusId: state.statuses[0]?.id || '', | |
| tags: [], | |
| notes: c.notes, | |
| lastContacted: '', | |
| createdAt: todayISO(), | |
| })); | |
| newContacts.forEach((contact) => actions.addContact(contact)); | |
| alert(`Imported ${newContacts.length} contact${newContacts.length === 1 ? '' : 's'}.`); | |
| // Geocoding pins one at a time in the background, a beat apart — | |
| // Nominatim's public endpoint enforces roughly one request per | |
| // second, and firing every contact's lookup at once (the previous | |
| // behaviour) meant only the first one or two ever got served; the | |
| // rest were silently rate-limited, and a failed geocode is | |
| // deliberately a no-op rather than an error (see geocodeAddress), | |
| // so importing a dozen contacts quietly auto-pinned just one of | |
| // them with nothing on screen to explain why. Sequenced with a gap | |
| // between requests, every address actually gets geocoded — just | |
| // progressively rather than all at once. | |
| (async () => { | |
| for (const contact of newContacts) { | |
| if (!contact.address) continue; | |
| await syncContactAddressPin(contact, state, actions); | |
| await new Promise((r) => setTimeout(r, 1100)); | |
| } | |
| })(); | |
| } catch { | |
| alert('That file could not be read as a vCard (.vcf) file.'); | |
| } | |
| reader.onload = () => { | |
| try { | |
| const imported = parseVCard(reader.result); | |
| if (imported.length === 0) { | |
| showToast('No contacts found in that file.'); | |
| return; | |
| } | |
| const newContacts = imported.map((c) => ({ | |
| id: uid('c'), | |
| name: c.name, | |
| phone: c.phone, | |
| email: c.email, | |
| address: c.address, | |
| photo: '', | |
| statusId: state.statuses[0]?.id || '', | |
| tags: [], | |
| notes: c.notes, | |
| lastContacted: '', | |
| createdAt: todayISO(), | |
| })); | |
| newContacts.forEach((contact) => actions.addContact(contact)); | |
| showToast(`Imported ${newContacts.length} contact${newContacts.length === 1 ? '' : 's'}.`); | |
| // Geocoding pins one at a time in the background, a beat apart — | |
| // Nominatim's public endpoint enforces roughly one request per | |
| // second, and firing every contact's lookup at once (the previous | |
| // behaviour) meant only the first one or two ever got served; the | |
| // rest were silently rate-limited, and a failed geocode is | |
| // deliberately a no-op rather than an error (see geocodeAddress), | |
| // so importing a dozen contacts quietly auto-pinned just one of | |
| // them with nothing on screen to explain why. Sequenced with a gap | |
| // between requests, every address actually gets geocoded — just | |
| // progressively rather than all at once. | |
| (async () => { | |
| for (const contact of newContacts) { | |
| if (!contact.address) continue; | |
| await syncContactAddressPin(contact, state, actions); | |
| await new Promise((r) => setTimeout(r, 1100)); | |
| } | |
| })(); | |
| } catch { | |
| showToast('That file is not a readable vCard (.vcf).'); | |
| } |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 238-238: Avoid using the initial state variable in setState
Context: setTimeout(r, 1100)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 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 `@schedule-app/src/pages/ContactsPage.jsx` around lines 206 - 244, Replace all
three alert() calls in the reader.onload handler with the existing showToast
helper already used in the page. Preserve the no-contacts and import-success
messages, and provide user-friendly actionable wording for the unreadable-file
failure while keeping the existing control flow.
Source: Coding guidelines
There was a problem hiding this comment.
Actionable comments posted: 48
🤖 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 `@backend/prisma/migrations/20260727010000_lifetime_purchase/migration.sql`:
- Line 8: Update the User.lifetimePurchasedAt field in schema.prisma from a
timezone-unaware DateTime mapping to optional `@db.Timestamptz`(3), then
regenerate the Prisma migration so the generated SQL reflects the timezone-aware
type; do not edit migration.sql directly.
In `@backend/prisma/schema.prisma`:
- Around line 63-119: Add and commit a Prisma migration that creates the
SharedCalendar, SharedCalendarMember, SharedCalendarInvite, and SharedEvent
tables with their fields, defaults, relations, unique constraint, indexes, and
cascade-delete behavior matching the schema models. Ensure the migration is
included in the deployable migrations so db:deploy creates all shared-calendar
tables.
In `@backend/README.md`:
- Around line 23-28: Update the PostgreSQL setup examples in the README to
require a user-provided password via an environment variable or prompt instead
of the hardcoded keystone_dev credential, and change the Docker port mapping to
bind explicitly to 127.0.0.1:5432:5432. Keep the database and user names
unchanged.
In `@backend/src/app.js`:
- Around line 31-35: Update the router mounting section in app.js to create and
apply an express-rate-limit middleware before the /api/me, /api/billing,
/api/data, /api/calendars, and /api/assistant mounts. Configure the limiter to
identify authenticated requests by Clerk user id and fall back to the client IP,
ensuring all public API endpoints share this protection while preserving the
existing router handlers.
- Line 15: Update the CORS configuration in app.js to remove the true fallback
and require FRONTEND_URL, throwing an error during startup when it is absent.
Configure the origin check to allow the required frontend URL plus any
explicitly configured additional origins, without permitting arbitrary request
origins.
In `@backend/src/routes/assistant.js`:
- Around line 27-28: Update the assistant configuration around MODEL to read
ANTHROPIC_MODEL from configuration instead of hardcoding claude-opus-5, validate
that setting during startup, and preserve the existing model usage. Add
ANTHROPIC_MODEL to backend/.env.example with the appropriate default or
required-value documentation.
In `@backend/src/routes/calendars.js`:
- Line 179: Update the role validation in the calendar invite flow around the
role assignment so only 'viewer' and 'editor' are accepted; reject any other
supplied value, including missing or differently cased roles, with a clear
validation error before creating the invite or reporting success.
In `@backend/src/routes/me.js`:
- Around line 6-16: Extract the shared Pro entitlement rule into
backend/src/entitlements.js, exporting PRO_STATUSES and isPro(user). In
backend/src/routes/me.js lines 6-16, remove the local constant and use the
imported helper; in backend/src/routes/assistant.js lines 62-63, remove both
local definitions and import the same helper so both routes evaluate
entitlements identically.
In `@backend/src/routes/webhooksClerk.js`:
- Around line 19-32: Extend the switch in the webhook handler to route
user.updated events through the existing user.created email-resolution and
prisma.user.upsert logic, ensuring the User.email value is refreshed when the
primary email changes. Preserve the current create/update behavior and existing
error handling, including unique-email conflicts.
In `@backend/src/routes/webhooksStripe.js`:
- Around line 34-48: Validate the identifier selection before calling
prisma.user.updateMany: when client_reference_id is absent, reject or skip the
webhook if obj.customer is null or otherwise missing, rather than constructing a
stripeCustomerId null filter. Only execute the lifetime grant when either a
valid client_reference_id or customer identifier resolves, preserving the
existing lifetimePurchasedAt guard and idempotent update behavior.
- Around line 62-64: Update the currentPeriodEnd mapping in the Stripe webhook
handler to fall back to obj.items.data[0].current_period_end when
obj.current_period_end is absent, while preserving null when neither value
exists. Explicitly use the first subscription item as the period source for
multi-item subscriptions.
In `@backend/src/server.js`:
- Line 24: Replace the production console.log in the server startup flow with
the existing server logging mechanism, emitting the startup message with port
passed as structured context. Preserve the current startup event and port value
while removing the direct console call.
In `@schedule-app/public/sw.js`:
- Around line 78-80: Update both cache.put calls in the fetch handler at
schedule-app/public/sw.js:78-80 and schedule-app/public/sw.js:102-105 to run
through event.waitUntil(). Catch cache-write failures so they do not reject or
replace the successful network response, while preserving the existing response
flow.
In `@schedule-app/src/App.jsx`:
- Around line 128-172: Gate the debounced push effect on an initialPullDone flag
so no push timer is scheduled until the first pull succeeds. Reset
initialPullDone when active becomes false, and set it after every successful
pull, including retry pulls, so a later success unlocks pushing. Update the pull
and push effects’ dependencies as needed while preserving existing sync
behavior.
In `@schedule-app/src/components/AssistantBubble.jsx`:
- Around line 135-148: Wrap the per-block runAssistantTool call in the
tool-processing loop with explicit error handling so a thrown tool error does
not abort runLoop or send. Preserve already-collected results and append a
tool_result for the failing block using its block.id, error details in content,
and is_error: true; only apply confirmTick/setChanges when the tool succeeds.
In `@schedule-app/src/components/MiniMapPicker.jsx`:
- Line 27: Guard the callback invocation in the map click handler by using
optional chaining on onPickRef.current, while preserving the existing latitude
and longitude arguments.
- Around line 21-26: Update the Leaflet map initialization in MiniMapPicker to
enable attributionControl and configure the OpenStreetMap tile layer with
visible attribution linking to https://www.openstreetmap.org/copyright; keep the
existing tile source and map behavior unchanged.
In `@schedule-app/src/components/SwipeRow.jsx`:
- Around line 86-90: Update the transition logic around nowArmed and g.armed so
the tick fires only when crossing from disarmed to armed, not when retreating
below ARM_THRESHOLD_PX. Select the tick pattern based on action.destructive,
using the destructive pattern only for destructive actions and the appropriate
non-destructive pattern otherwise.
In `@schedule-app/src/data/api.js`:
- Around line 17-27: Update the shared request helper to handle successful empty
responses, including 204 No Content, without attempting to parse them as JSON;
preserve JSON parsing for responses containing a body. Add an AbortSignal-based
timeout to the fetch call used by request, ensuring stalled requests reject
after the configured timeout so existing callers can handle the failure.
In `@schedule-app/src/data/assistantTools.js`:
- Around line 176-177: Update the end-time calculation in create_event so an
event starting at 23:59 cannot produce an end equal to start; clamp or reject
the start to preserve a positive minimum duration and the same validation
behavior as supplied end values.
- Around line 32-34: Add an isRealDate helper beside DATE_RE that validates both
the YYYY-MM-DD format and actual calendar validity, then replace
DATE_RE.test(...) checks with isRealDate(...) in list_schedule, create_event,
create_task, log_interaction, set_follow_up, and plan_route. Keep rejecting
impossible dates before they reach the action handlers or fromISODate.
In `@schedule-app/src/data/helpers.js`:
- Around line 286-305: Normalize goal.repeatDays to integer weekday values from
0 through 6 before constructing isScheduled in computeGoalStreak, treating the
normalized empty list as unrestricted; apply the same normalization in
buildGoalHistory so longestGoalStreak is protected as well. Update
schedule-app/src/data/helpers.js lines 286-305 and
schedule-app/src/data/goalHistory.js lines 24-44; both sites require the same
change.
- Around line 426-497: Refactor occursOn to delegate the recurrence calculation
to matchesRule, retaining only the skipDates exclusion in occursOn. Move
matchesRule above occursOn for readable declaration order, and preserve
identical behavior for all repeat types, date bounds, and non-recurring events.
In `@schedule-app/src/data/ics.js`:
- Around line 62-67: Defer revocation of the object URL created in the download
flow until the next task. Replace the immediate URL.revokeObjectURL call after
a.click() with a zero-delay setTimeout callback, preserving the existing url
value and download behavior.
In `@schedule-app/src/data/maps.js`:
- Around line 61-69: Update openInNewContext to validate the URL scheme before
assigning a.href or triggering the synthetic click, allowing only http and https
URLs and returning immediately for all other schemes. Keep this guard at the
shared openInNewContext choke point so both webTarget and mapsLinkProps
consumers are protected.
In `@schedule-app/src/data/notifications.js`:
- Around line 96-99: Update the notification reminder loop to use
expandEventOnDay(e, today) instead of matchesRule and the master event fields.
Iterate the returned occurrences, skip when none are returned, and calculate
each reminder’s trigger from occ.start so per-occurrence start-date overrides
and skipDates are honored.
In `@schedule-app/src/data/routePlanner.js`:
- Around line 100-111: The route-planning flow must exclude saved pins whose lat
or lng is missing or non-numeric before optimization. Add shared coordinate
validation for pins handled by addPin, updatePin, IMPORT_DATA, and loadState,
and apply the same filtering in the plan_route UI path before
nearestIdx/distanceMeters processing; preserve valid pins and prevent invalid
coordinates from producing NaN distances.
In `@schedule-app/src/data/seed.js`:
- Around line 374-386: Extract the complete default settings object into an
exported constant in makeSeed within schedule-app/src/data/seed.js:374-386, and
have makeSeed return a copy of it. In schedule-app/src/data/store.jsx:376-379,
import that constant and use it as the base for loadState instead of the
duplicated literal; ensure RESET_DATA and IMPORT_DATA preserve the same settings
shape as a fresh load.
In `@schedule-app/src/data/smartParse.js`:
- Around line 97-386: Refactor parseQuickAdd into the nine requested stage
helpers: extractRecurrence, extractReminder, extractDuration,
extractExplicitDate, extractRelativeDay, extractTimes, extractLocation,
extractContact, and cleanTitle. Have each accept the current remaining text plus
required context and return remaining alongside its extracted fields, preserving
the existing stage order and behavior, including cross-stage weekday/time and
duration/end-time logic; keep parseQuickAdd under 50 lines and use it only to
compose the helpers and build the result.
In `@schedule-app/src/data/store.jsx`:
- Around line 407-413: Update the useEffect persistence catch block to handle
localStorage write failures explicitly instead of silently ignoring them. Report
the first failure through the existing toast channel, or log the error if no
suitable toast mechanism is available, while preventing repeated failures from
generating duplicate notifications.
- Around line 376-377: Normalize or validate action.data in the IMPORT_DATA
reducer case before merging it with makeSeed(), ensuring events and contacts are
arrays and settings is a valid object; discard or replace invalid values with
the seed defaults so malformed backup data cannot reach state or localStorage.
In `@schedule-app/src/data/syncStatus.js`:
- Around line 18-26: Replace the useEffect-based subscription in useSyncStatus
with useSyncExternalStore, wiring the existing listeners collection and current
snapshot into the hook’s subscribe and getSnapshot arguments. Preserve listener
cleanup and synchronous catch-up behavior through the subscription
implementation, while continuing to return the current sync status.
In `@schedule-app/src/data/templates.js`:
- Around line 90-94: Update templateSummary so it safely defaults t.blocks to an
empty array before calculating the block count, preserving the existing
singular/plural and week-summary formatting.
In `@schedule-app/src/data/toast.jsx`:
- Around line 47-55: Reset dragStartRef.current in showToast alongside the
existing drag state reset, before setting the replacement toast, so a pointerup
from the previous gesture cannot apply its delta to the new toast.
In `@schedule-app/src/data/useBackDismiss.js`:
- Around line 44-57: Update ensureListener so the popstate handler re-arms the
history entry whenever any overlays remain after top.onBack(), rather than only
when that callback returns stillOpen. Coordinate this with maybePop so
unregistering the closing overlay does not immediately consume the newly
re-armed entry.
In `@schedule-app/src/data/useSmartAdd.js`:
- Around line 61-65: Update plusMinutes to validate that the parsed hour and
minute values are numeric before calculating the end time; when validation
fails, return the original hhmm start-time string instead of producing
"NaN:NaN", while preserving the existing clamping and formatting for valid
input.
- Around line 17-21: Validate parsed.title at the start of the returned callback
before constructing the event or task record, using the available schema-based
validation and rejecting or applying the established default when it is missing
or empty. Store the validated result in title, then use title in both the event
and task branches and in the success toast instead of parsed.title.
In `@schedule-app/src/main.jsx`:
- Around line 36-41: Update the service-worker registration catch handler in the
load event to report the registration error through the project’s existing
error-reporting mechanism, or display a non-blocking status message, instead of
silently ignoring it. Preserve the current progressive-enhancement behavior and
production-only registration flow.
In `@schedule-app/src/pages/ContactDetailPage.jsx`:
- Around line 49-54: Move the startEdit declaration and its initialEditJsonRef
dependency above the early-return guard in the component, so startEdit is
initialized before the useEffect can execute. Preserve the existing effect
behavior and guard conditions.
In `@schedule-app/src/pages/GoalsPage.jsx`:
- Around line 292-304: Update stepDay and stepWeek to reset manualNavRef.current
to false when their calculated resulting day or week matches the current day or
week; otherwise keep it true. Preserve the existing navigation updates and
ensure returning to today re-enables the midnight resync behavior.
In `@schedule-app/src/pages/HomePage.jsx`:
- Around line 505-510: The empty-home effect currently persists
emptyHomeMsgIndex through actions.setSettings on each empty visit. Update the
HomePage empty-state message selection around isEmptyHome and emptyHomeMsgIndex
to use local component state or a mount-scoped useMemo instead, removing the
persisted-settings write while retaining message rotation or selection behavior.
- Around line 302-315: Default all optional editor fields when opening persisted
records. In schedule-app/src/pages/HomePage.jsx lines 302-315, update
openEditTask’s defaults object to include title. In
schedule-app/src/pages/HomePage.jsx lines 474-477, update openEditNote to build
defaults for title, body, color, and pinned, then use that same object for both
setEditingNote and initialNoteJson.
In `@schedule-app/src/pages/MapPage.jsx`:
- Around line 236-252: In the initialization useEffect, replace the direct
window.history.replaceState call with React Router’s navigate using the current
location.pathname, replace: true, and state: null. Preserve the existing
intent-handling logic and ensure the router’s history metadata remains intact.
In `@schedule-app/src/pages/MorePage.jsx`:
- Around line 1029-1046: Update the Feedback SettingsGroup in the MorePage
component to use a unique group ID instead of g16. Ensure the replacement does
not match the existing Event colors group, preserving independent search and
open-state behavior.
- Around line 312-326: Update importData to validate the parsed JSON against a
Zod schema matching the expected Keystone backup shape before calling
actions.importData, including validating fields such as goals and contacts as
arrays with appropriate item structures. Treat schema failures the same as parse
failures and show a clear backup-validation error through the existing alert
path; only import data after successful validation.
In `@schedule-app/src/pages/PricingPage.jsx`:
- Around line 110-112: Update the error handling around the catch blocks in the
PricingPage component to set a fixed user-facing error message instead of
err.message, including the corresponding block around lines 122-124. Retain the
raw error details only for logging and ensure the rendered message is always
scrubbed of backend or provider-specific information.
- Around line 107-113: Validate the external checkout response before navigation
in the checkout handler containing startCheckout, requiring a non-empty url
before assigning window.location.href; otherwise surface an error and reset busy
state. Apply the same validation to handleManage, preserving successful
navigation only for valid URLs and ensuring invalid responses cannot navigate to
/undefined or leave the UI busy.
In `@schedule-app/src/pages/SharedCalendarDetailPage.jsx`:
- Around line 91-101: Update sendInvite to validate inviteEmail as a properly
formatted email, not merely a non-empty trimmed string, before calling
inviteToCalendar. Reuse the project’s existing schema or email-validation
utility if available; otherwise apply the established email validation pattern
and setError with the validation message while returning early.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ba156c4b-dccb-4369-8a28-447fa111fa57
⛔ Files ignored due to path filters (9)
backend/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/appstore-assets/icon-1024.pngis excluded by!**/*.pngschedule-app/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/public/apple-touch-icon.pngis excluded by!**/*.pngschedule-app/public/icon-192.pngis excluded by!**/*.pngschedule-app/public/icon-512-maskable.pngis excluded by!**/*.pngschedule-app/public/icon-512.pngis excluded by!**/*.pngschedule-app/public/icon-maskable.svgis excluded by!**/*.svgschedule-app/public/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (111)
.github/workflows/deploy-schedule-app.yml.gitignorebackend/.env.examplebackend/.gitignorebackend/README.mdbackend/package.jsonbackend/prisma.config.mjsbackend/prisma/migrations/20260722012026_init/migration.sqlbackend/prisma/migrations/20260722062956_add_user_data/migration.sqlbackend/prisma/migrations/20260727010000_lifetime_purchase/migration.sqlbackend/prisma/migrations/migration_lock.tomlbackend/prisma/schema.prismabackend/src/app.jsbackend/src/db.jsbackend/src/middleware/requireUser.jsbackend/src/routes/assistant.jsbackend/src/routes/billing.jsbackend/src/routes/calendars.jsbackend/src/routes/data.jsbackend/src/routes/me.jsbackend/src/routes/webhooksClerk.jsbackend/src/routes/webhooksStripe.jsbackend/src/server.jsschedule-app/.env.exampleschedule-app/README.mdschedule-app/index.htmlschedule-app/package.jsonschedule-app/public/manifest.webmanifestschedule-app/public/sw.jsschedule-app/src/App.jsxschedule-app/src/components/AddressField.jsxschedule-app/src/components/AnimatedNumber.jsxschedule-app/src/components/AssistantBubble.jsxschedule-app/src/components/Avatar.jsxschedule-app/src/components/Checkbox.jsxschedule-app/src/components/DesertEmptyState.jsxschedule-app/src/components/EditorSheet.jsxschedule-app/src/components/ExpandableFab.jsxschedule-app/src/components/GroupPicker.jsxschedule-app/src/components/Icon.jsxschedule-app/src/components/Logo.jsxschedule-app/src/components/MilestoneCelebration.jsxschedule-app/src/components/MiniMapPicker.jsxschedule-app/src/components/Modal.jsxschedule-app/src/components/ReorderToggleList.jsxschedule-app/src/components/Select.jsxschedule-app/src/components/SettingsGroup.jsxschedule-app/src/components/SettingsSection.jsxschedule-app/src/components/SmartQuickAdd.jsxschedule-app/src/components/SwipeRow.jsxschedule-app/src/components/SwipeToDelete.jsxschedule-app/src/components/TabBar.jsxschedule-app/src/components/Tutorial.jsxschedule-app/src/data/aiConfig.jsschedule-app/src/data/api.jsschedule-app/src/data/assistantContext.jsschedule-app/src/data/assistantTools.jsschedule-app/src/data/clerkConfig.jsschedule-app/src/data/conflicts.jsschedule-app/src/data/contactDates.jsschedule-app/src/data/contactInsights.jsschedule-app/src/data/contactSwipe.jsschedule-app/src/data/geocode.jsschedule-app/src/data/goalHistory.jsschedule-app/src/data/haptics.jsschedule-app/src/data/helpers.jsschedule-app/src/data/homeBlocks.jsschedule-app/src/data/ics.jsschedule-app/src/data/image.jsschedule-app/src/data/mapStyles.jsschedule-app/src/data/maps.jsschedule-app/src/data/nlSearch.jsschedule-app/src/data/notifications.jsschedule-app/src/data/nudges.jsschedule-app/src/data/pinLabel.jsschedule-app/src/data/quickAdd.jsschedule-app/src/data/reconnect.jsschedule-app/src/data/routePlanner.jsschedule-app/src/data/routePlannerConfig.jsschedule-app/src/data/seed.jsschedule-app/src/data/smartParse.jsschedule-app/src/data/store.jsxschedule-app/src/data/syncStatus.jsschedule-app/src/data/tabs.jsschedule-app/src/data/templates.jsschedule-app/src/data/toast.jsxschedule-app/src/data/useBackDismiss.jsschedule-app/src/data/useCountUp.jsschedule-app/src/data/useDeleteContact.jsschedule-app/src/data/useEdgeFade.jsschedule-app/src/data/useSmartAdd.jsschedule-app/src/data/vcard.jsschedule-app/src/data/weeklyRecap.jsschedule-app/src/main.jsxschedule-app/src/pages/ContactDetailPage.jsxschedule-app/src/pages/ContactTimelinePage.jsxschedule-app/src/pages/ContactsPage.jsxschedule-app/src/pages/GoalHistoryPage.jsxschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/HomePage.jsxschedule-app/src/pages/MapPage.jsxschedule-app/src/pages/MorePage.jsxschedule-app/src/pages/PlannerPage.jsxschedule-app/src/pages/PricingPage.jsxschedule-app/src/pages/RoutePlannerPage.jsxschedule-app/src/pages/SearchPage.jsxschedule-app/src/pages/SharedCalendarDetailPage.jsxschedule-app/src/pages/SharedCalendarJoinPage.jsxschedule-app/src/pages/SharedCalendarsPage.jsxschedule-app/src/styles.cssschedule-app/vite.config.js
| -- cancel through the billing portal; nothing new writes to them. | ||
|
|
||
| -- AlterTable | ||
| ALTER TABLE "User" ADD COLUMN "lifetimePurchasedAt" TIMESTAMP(3); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 2 'lifetimePurchasedAt|Timestamptz' \
backend/prisma/schema.prisma \
backend/prisma/migrationsRepository: affaan-m/ECC
Length of output: 1447
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Prisma datasource and User model ---'
sed -n '1,45p' backend/prisma/schema.prisma
printf '%s\n' '--- DateTime mappings in schema and migrations ---'
rg -n -C 2 '`@db`\.(Timestamp|Timestamptz)|DateTime|TIMESTAMP' \
backend/prisma/schema.prisma \
backend/prisma/migrations
printf '%s\n' '--- Application reads and writes ---'
rg -n -C 3 'lifetimePurchasedAt|lifetimeSessionId' \
--glob '!backend/prisma/migrations/**' \
--glob '!backend/prisma/schema.prisma' .Repository: affaan-m/ECC
Length of output: 10046
🌐 Web query:
Prisma PostgreSQL DateTime default timestamp(3) @db.Timestamptz(3) official documentation
💡 Result:
In Prisma ORM, the default mapping for the DateTime scalar type on PostgreSQL is timestamp(3) [1][2][3]. To use timestamptz(3) (timestamp with time zone) instead of the default timestamp(3), you must explicitly define the native database type attribute @db.Timestamptz(3) in your Prisma schema [1][4][3]. Example usage: model Example { id String @id @default(uuid) createdAt DateTime @default(now) @db.Timestamptz(3) } Key technical points: - Default Behavior: Prisma maps the DateTime type to timestamp(3) by default in PostgreSQL [1][2]. - Customization: You can refine this by adding the @db.Timestamptz(x) attribute, where x represents the precision (e.g., 3) [1][3]. - @default(now): The now function is used to set the default current time [5]. When used with @db.Timestamptz(3), the database will store the value as a timestamp with time zone at the specified precision [6]. For authoritative documentation, refer to the Prisma Schema Reference and the Prisma Type System guides, which detail the native database type attributes [1][2][3].
Citations:
- 1: https://www.prisma.io/docs/orm/v6/reference/prisma-schema-reference
- 2: https://www.prisma.io/docs/orm/v6/prisma-client/type-safety/prisma-type-system
- 3: https://www.prisma.io/docs/orm/reference/prisma-schema-reference
- 4: "Timestamp with timezone" (
Timestamptz) asDateTimetype mapping for Postgres prisma/orm#8986 - 5: https://www.prisma.io/docs/orm/prisma-schema/data-model/models
- 6: @default(now()) applying unexpected datetime with @prisma/adapter-pg prisma/orm#27590
🌐 Web query:
PostgreSQL timestamp without time zone timestamptz stores UTC instant official documentation
💡 Result:
In PostgreSQL, the behavior of these two data types differs significantly regarding time zone interpretation and storage [1][2]: TIMESTAMP WITHOUT TIME ZONE (or simply TIMESTAMP) This type stores only the date and time fields exactly as provided in the input [1][3]. It does not store or track any time zone information [4][5]. PostgreSQL will silently ignore any time zone indication provided in the input string for this type [1][3]. Because it contains no time zone context, it is not treated as a UTC instant by the database; rather, it is a "wall clock" time that remains constant regardless of the database's time zone settings [6]. TIMESTAMP WITH TIME ZONE (or TIMESTAMPTZ) This type stores a UTC instant [1][6]. When you insert a value into a TIMESTAMPTZ column, PostgreSQL converts the input to UTC using the session's current TimeZone setting (if no explicit offset is provided) or the explicit offset provided in the input string [1][3]. Internally, it is stored as a UTC value, and the original time zone or offset is discarded [1][3][6]. Upon retrieval, PostgreSQL converts the stored UTC value back into the time zone specified by the current session's TimeZone parameter for display [4][6][7]. In summary, only TIMESTAMPTZ stores a UTC instant, while TIMESTAMP stores a literal date/time value without any associated time zone or UTC conversion logic [1][4][6].
Citations:
- 1: https://www.postgresql.org/docs/current/datatype-datetime.html
- 2: https://www.postgresql.org/docs/19/datatype-datetime.html
- 3: https://www.postgresql.org/docs/12/datatype-datetime.html
- 4: https://stackoverflow.com/questions/65143816/how-does-postgresql-store-datetime-types-internally
- 5: https://stackoverflow.com/questions/27299234/how-do-i-tell-postgres-a-timestamp-within-a-column-is-utc
- 6: https://stackoverflow.com/questions/5876218/difference-between-timestamps-with-without-time-zone-in-postgresql
- 7: https://www.postgresql.org/message-id/F8E84F0F56445B4CB39E019EF67DACBA401726%40exchsrvr.winemantech.com
Store lifetimePurchasedAt as a timezone-aware timestamp.
Change User.lifetimePurchasedAt to DateTime? @db.Timestamptz(3) in backend/prisma/schema.prisma, then regenerate the migration. Do not edit generated SQL directly.
🧰 Tools
🪛 Squawk (2.61.0)
[warning] 8-8: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
🤖 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 `@backend/prisma/migrations/20260727010000_lifetime_purchase/migration.sql` at
line 8, Update the User.lifetimePurchasedAt field in schema.prisma from a
timezone-unaware DateTime mapping to optional `@db.Timestamptz`(3), then
regenerate the Prisma migration so the generated SQL reflects the timezone-aware
type; do not edit migration.sql directly.
Source: Linters/SAST tools
| model SharedCalendar { | ||
| id String @id @default(cuid()) | ||
| name String | ||
| color String @default("#0f8f72") | ||
| ownerId String | ||
| owner User @relation("OwnedCalendars", fields: [ownerId], references: [id], onDelete: Cascade) | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| members SharedCalendarMember[] | ||
| invites SharedCalendarInvite[] | ||
| events SharedEvent[] | ||
| } | ||
|
|
||
| // Everyone who can see a calendar, including the owner (added as an | ||
| // "owner" member at creation time) — so "which calendars can I see" is one | ||
| // query against this table instead of a union with SharedCalendar.ownerId. | ||
| model SharedCalendarMember { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| userId String | ||
| user User @relation(fields: [userId], references: [id], onDelete: Cascade) | ||
| role String @default("editor") // owner | editor | viewer | ||
| joinedAt DateTime @default(now()) | ||
|
|
||
| @@unique([calendarId, userId]) | ||
| } | ||
|
|
||
| // A pending invite by email, accepted via a tokenized link — the invited | ||
| // person doesn't need to exist as a User yet when the invite is created. | ||
| model SharedCalendarInvite { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| email String | ||
| token String @unique @default(cuid()) | ||
| role String @default("editor") | ||
| invitedBy String | ||
| createdAt DateTime @default(now()) | ||
| expiresAt DateTime | ||
| acceptedAt DateTime? | ||
| } | ||
|
|
||
| model SharedEvent { | ||
| id String @id @default(cuid()) | ||
| calendarId String | ||
| calendar SharedCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) | ||
| title String | ||
| date String // YYYY-MM-DD | ||
| start String // HH:MM | ||
| end String // HH:MM | ||
| notes String @default("") | ||
| createdBy String | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Add a migration for the shared-calendar models.
The supplied migrations do not create SharedCalendar, SharedCalendarMember, SharedCalendarInvite, or SharedEvent. A database deployed with db:deploy will not contain these tables, so shared-calendar requests will fail.
Create and commit a Prisma migration for these models before release.
🤖 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 `@backend/prisma/schema.prisma` around lines 63 - 119, Add and commit a Prisma
migration that creates the SharedCalendar, SharedCalendarMember,
SharedCalendarInvite, and SharedEvent tables with their fields, defaults,
relations, unique constraint, indexes, and cascade-delete behavior matching the
schema models. Ensure the migration is included in the deployable migrations so
db:deploy creates all shared-calendar tables.
| ```bash | ||
| sudo -u postgres psql -c "CREATE USER keystone WITH PASSWORD 'keystone_dev' CREATEDB;" | ||
| sudo -u postgres psql -c "CREATE DATABASE keystone_dev OWNER keystone;" | ||
| ``` | ||
|
|
||
| Or use Docker: `docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=keystone_dev -e POSTGRES_USER=keystone -e POSTGRES_DB=keystone_dev postgres:16`. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not publish PostgreSQL with a documented password.
Lines 23-28 use keystone_dev as a fixed database password. The Docker command also publishes port 5432 on all host interfaces by default. This can expose a database with known credentials to the local network. (docs.docker.com)
Require a user-provided local password and bind the development database to 127.0.0.1:5432:5432.
As per coding guidelines: "Never hardcode API keys, passwords, tokens, or other secrets; use environment variables or a secret manager."
🤖 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 `@backend/README.md` around lines 23 - 28, Update the PostgreSQL setup examples
in the README to require a user-provided password via an environment variable or
prompt instead of the hardcoded keystone_dev credential, and change the Docker
port mapping to bind explicitly to 127.0.0.1:5432:5432. Keep the database and
user names unchanged.
Source: Coding guidelines
| export function createApp() { | ||
| const app = express(); | ||
|
|
||
| app.use(cors({ origin: process.env.FRONTEND_URL || true })); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove the permissive CORS fallback.
origin: process.env.FRONTEND_URL || true reflects any requesting Origin when FRONTEND_URL is unset. backend/src/server.js lines 16-18 only log a warning for a missing FRONTEND_URL, so the process still starts and serves every origin. Any website can then issue cross-origin calls to /api/* in a signed-in user's browser.
Require the value and fail startup when it is absent. Accept an explicit allowlist for extra origins.
🔒 Proposed fix
- app.use(cors({ origin: process.env.FRONTEND_URL || true }));
+ const allowedOrigins = (process.env.FRONTEND_URL || '')
+ .split(',')
+ .map((o) => o.trim())
+ .filter(Boolean);
+ if (allowedOrigins.length === 0) {
+ throw new Error('FRONTEND_URL is required: refusing to start with a wildcard CORS origin');
+ }
+ app.use(cors({ origin: allowedOrigins }));As per coding guidelines: "Throw an error when required environment variables are not configured to fail fast and ensure security prerequisites are met".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| app.use(cors({ origin: process.env.FRONTEND_URL || true })); | |
| const allowedOrigins = (process.env.FRONTEND_URL || '') | |
| .split(',') | |
| .map((o) => o.trim()) | |
| .filter(Boolean); | |
| if (allowedOrigins.length === 0) { | |
| throw new Error('FRONTEND_URL is required: refusing to start with a wildcard CORS origin'); | |
| } | |
| app.use(cors({ origin: allowedOrigins })); |
🤖 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 `@backend/src/app.js` at line 15, Update the CORS configuration in app.js to
remove the true fallback and require FRONTEND_URL, throwing an error during
startup when it is absent. Configure the origin check to allow the required
frontend URL plus any explicitly configured additional origins, without
permitting arbitrary request origins.
Source: Coding guidelines
| app.use('/api/me', meRoutes); | ||
| app.use('/api/billing', billingRoutes); | ||
| app.use('/api/data', dataRoutes); | ||
| app.use('/api/calendars', calendarsRoutes); | ||
| app.use('/api/assistant', assistantRoutes); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add rate limiting at the router mount point.
None of these five routers has a limiter. assistant.js throttles inside its own handler, and that limiter covers only /api/assistant. /api/data accepts 8 MB PUT bodies, /api/calendars performs unbounded writes, and /api/billing creates Stripe sessions. The billing gap was raised on an earlier commit; the root cause is the absent app-wide limiter here.
Mount express-rate-limit before the routers, keyed on the Clerk user id with an IP fallback.
As per coding guidelines: "Rate limiting must be applied to all public endpoints".
🤖 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 `@backend/src/app.js` around lines 31 - 35, Update the router mounting section
in app.js to create and apply an express-rate-limit middleware before the
/api/me, /api/billing, /api/data, /api/calendars, and /api/assistant mounts.
Configure the limiter to identify authenticated requests by Clerk user id and
fall back to the client IP, ensuring all public API endpoints share this
protection while preserving the existing router handlers.
Source: Coding guidelines
| const openEditTask = (t) => { | ||
| const d = { | ||
| ...t, | ||
| notes: t.notes || '', | ||
| location: t.location || '', | ||
| dueDate: t.dueDate || '', | ||
| dueTime: t.dueTime || '', | ||
| reminderOffsets: t.reminderOffsets || [], | ||
| repeat: t.repeat || 'none', | ||
| subtasks: t.subtasks || [], | ||
| }; | ||
| setEditingTask(d); | ||
| initialTaskJson.current = JSON.stringify(d); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Editor-open helpers do not default stored record fields. openEditTask and openEditNote copy persisted records into editor state without defaulting every optional field. Later code calls .trim() on those fields and binds them to controlled inputs, so a record missing a field throws or switches an input to uncontrolled.
schedule-app/src/pages/HomePage.jsx#L302-L315: addtitle: t.title || ''to the defaults object inopenEditTask.schedule-app/src/pages/HomePage.jsx#L474-L477: build a defaults object inopenEditNotefortitle,body,color, andpinned, and store that object in bothsetEditingNoteandinitialNoteJson.
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 312-312: Avoid using the initial state variable in setState
Context: setEditingTask(d)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
📍 Affects 1 file
schedule-app/src/pages/HomePage.jsx#L302-L315(this comment)schedule-app/src/pages/HomePage.jsx#L474-L477
🤖 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 `@schedule-app/src/pages/HomePage.jsx` around lines 302 - 315, Default all
optional editor fields when opening persisted records. In
schedule-app/src/pages/HomePage.jsx lines 302-315, update openEditTask’s
defaults object to include title. In schedule-app/src/pages/HomePage.jsx lines
474-477, update openEditNote to build defaults for title, body, color, and
pinned, then use that same object for both setEditingNote and initialNoteJson.
| useEffect(() => { | ||
| const st = location.state; | ||
| if (!st) return; | ||
| if (st.placeForContact) { | ||
| setPendingContact(st.placeForContact); | ||
| setPlacing(true); | ||
| } | ||
| if (st.selectPin) setSelectedId(st.selectPin); | ||
| if (st.picking) { | ||
| setPickMode(true); | ||
| setPickReturnTo(st.returnTo || '/planner'); | ||
| if (st.initialLat != null && st.initialLng != null) { | ||
| setPickLatLng({ lat: st.initialLat, lng: st.initialLng }); | ||
| } | ||
| } | ||
| window.history.replaceState({}, ''); // consume so it doesn't retrigger | ||
| }, []); // eslint-disable-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
React Router v6 history.state usr key idx replaceState overwrite
💡 Result:
React Router v6 manages the browser's history state by wrapping the native History API [1][2][3]. When you navigate using React Router, it stores its own internal metadata alongside your custom data within the window.history.state object [1][4]. Internal Structure The state object used by React Router typically contains the following properties [1][5]: - usr: This property holds the state you provide when calling navigate(path, { state }) or using a Link component [1][6]. This is the only part of the history state exposed via the useLocation hook [4][6]. - key: A unique string identifier for the history entry [1][5]. - idx: An integer representing the index of the entry in the navigation stack, which React Router uses to manage back/forward navigation [1][5]. Navigation and Overwrites React Router's navigation methods (like navigate or the Link component) are designed to replace or push new entries into the history stack [7][8][9]. - When you perform a navigation, React Router generates a new history state object that includes updated idx, key, and usr properties [1][2]. - If you call navigation methods multiple times in the same tick of the event loop, the final call will effectively dictate the location state, as each call operates on the current location context [10]. - Using { replace: true } in your navigation options will cause React Router to use history.replaceState internally, replacing the current entry rather than pushing a new one, which prevents the stack from growing unnecessarily [7][11]. Direct Manipulation Risks React Router strongly discourages direct manipulation of window.history.state [6][3]. Because React Router relies on the internal structure (idx, key, usr) to track navigation, manually calling history.replaceState or history.pushState without preserving or correctly calculating these values can break the router's ability to track history, lead to incorrect back/forward behavior, or cause state synchronization bugs [1][6][5]. If you must manipulate state directly—for example, in a testing environment—you are responsible for maintaining the integrity of the idx and key fields to ensure they align with what React Router expects [6][5].
Citations:
- 1: https://github.com/remix-run/react-router/blob/05180441b118d26da4df94bacc2211923d87e4c1/packages/react-router/lib/router/history.ts
- 2: https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/history.ts
- 3: https://reactrouter.com/6.28.0/start/concepts
- 4: https://github.com/jwworth/til/blob/master/react/clear-react-routers-location-state-from-the-console.md
- 5: [Bug]: Not able to manually manipulate route state outside of react router remix-run/react-router#10428
- 6: Replace history state without redirection remix-run/react-router#11415
- 7: https://reactrouter.com/6.28.1/hooks/use-navigate
- 8: https://reactrouter.com/docs/en/en/v6/hooks/use-navigate
- 9: https://github.com/remix-run/react-router/blob/18ae5d189e7ce529e00669ec797d7fa445274a33/packages/react-router/lib/router/history.ts
- 10: [Bug]: Sequential calls to
navigatefromuseNavigateoverwrite each-other's location remix-run/react-router#9638 - 11: https://stackoverflow.com/questions/73931578/replace-true-in-react-router-v6-doesnt-replace-last-entry-in-the-history
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(MapPage\.jsx|App\.jsx|package\.json)$' || true
printf '%s\n' '--- MapPage.jsx relevant symbols and lines ---'
mapfile -t maps < <(git ls-files | grep 'schedule-app/src/pages/MapPage.jsx$')
for f in "${maps[@]}"; do
printf '\nFILE: %s\n' "$f"
rg -n -C 8 'useEffect|history\.replaceState|useNavigate|navigate|location|setPendingContact|setPickMode' "$f" || true
done
printf '%s\n' '--- App.jsx relevant symbols and lines ---'
mapfile -t apps < <(git ls-files | grep 'schedule-app/src/App.jsx$')
for f in "${apps[@]}"; do
printf '\nFILE: %s\n' "$f"
rg -n -C 8 'location\.key|useLocation|scroll|useEffect|BrowserRouter|Router' "$f" || true
done
printf '%s\n' '--- router package version ---'
mapfile -t manifests < <(git ls-files | grep -E '(^|/)schedule-app/package\.json$|(^|/)package\.json$')
for f in "${manifests[@]}"; do
printf '\nFILE: %s\n' "$f"
rg -n -C 2 '"react-router-dom"|"react-router"' "$f" || true
doneRepository: affaan-m/ECC
Length of output: 416
🏁 Script executed:
#!/bin/bash
set -eu
mapfile -t maps < <(git ls-files | grep 'schedule-app/src/pages/MapPage.jsx$')
for f in "${maps[@]}"; do
printf '--- %s size ---\n' "$f"
wc -l "$f"
printf '--- %s imports and effect ---\n' "$f"
sed -n '1,80p' "$f"
sed -n '220,265p' "$f"
done
mapfile -t apps < <(git ls-files | grep 'schedule-app/src/App.jsx$')
for f in "${apps[@]}"; do
printf '--- %s ---\n' "$f"
sed -n '1,180p' "$f"
done
printf '%s\n' '--- all MapPage navigation calls ---'
rg -n -C 3 'navigate\(|<Navigate|useNavigate|history\.' schedule-app/src 2>/dev/null || trueRepository: affaan-m/ECC
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- MapPage.jsx imports and relevant section ---'
wc -l schedule-app/src/pages/MapPage.jsx
sed -n '1,90p' schedule-app/src/pages/MapPage.jsx
sed -n '220,265p' schedule-app/src/pages/MapPage.jsx
printf '%s\n' '--- App.jsx ---'
sed -n '1,180p' schedule-app/src/App.jsx
printf '%s\n' '--- package versions ---'
grep -n -C 2 '"react-router-dom"\|"react-router"' schedule-app/package.json package.json 2>/dev/null || true
printf '%s\n' '--- MapPage navigation and history usage ---'
grep -RIn -E -C 3 'navigate\(|<Navigate|useNavigate|history\.' schedule-app/src 2>/dev/null || trueRepository: affaan-m/ECC
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- MapPage router hooks ---'
grep -n -C 4 -E 'const location|const navigate|useLocation|useNavigate' schedule-app/src/pages/MapPage.jsx
printf '%s\n' '--- App location and scroll handling ---'
grep -n -C 12 -E 'useLocation|location\.key|scrollTo|scroll' schedule-app/src/App.jsx
printf '%s\n' '--- App route tree ---'
grep -n -A90 -B5 '<Routes' schedule-app/src/App.jsx
printf '%s\n' '--- precise history-state clearing sites ---'
grep -RIn -E -C 5 'window\.history\.replaceState\(\{\}, ['\"']' schedule-app/src/pages/MapPage.jsx schedule-app/src/pages/ContactDetailPage.jsx schedule-app/src/pages/PlannerPage.jsx schedule-app/src/pages/ContactsPage.jsx 2>/dev/null || true
printf '%s\n' '--- MapPage call sites ---'
grep -RIn -E -C 4 'navigate\(['\"']\/map|to=.*/map' schedule-app/src 2>/dev/null || trueRepository: affaan-m/ECC
Length of output: 11179
🌐 Web query:
React Router 6.26.2 history.ts getIndex state.idx replaceState direct history.replaceState missing idx key POP
💡 Result:
In React Router 6.26.2 (and other 6.x versions using the internal history wrapper), the idx property in window.history.state is an internal mechanism used by the router to track the position of the current location in the navigation stack [1][2][3]. React Router maintains this idx value to support navigation features like navigate(-1) [2]. The router expects this property to exist in the history.state object [1][3]. When you manually call window.history.replaceState or window.history.pushState directly, you bypass the router's internal state management [3][4]. If these manual calls do not include the idx key, the internal getIndex() function—which retrieves the current index from history.state—may return undefined or null [1][5]. When React Router detects an idx of null during initialization (or navigation), it typically attempts to recover by setting idx to 0 using globalHistory.replaceState [1][5]. However, manually overwriting the state without including the expected structure (which includes idx, key, and usr) will cause inconsistencies with the router's internal state tracking [3][4]. Key points regarding this behavior: - Internal usage: idx is an internal implementation detail of the React Router history wrapper and is not intended to be a public API [2][6]. Reliance on it is discouraged, as it can change or be removed in future versions without notice [2][6]. - Direct manipulation: If you must manually interact with the History API, you should preserve the existing history.state and ensure the idx property remains correctly formatted to avoid breaking the router's navigation logic [3][4]. The expected structure for the state object is: { idx: number, key: string, usr: any } [3]. - POP actions: A POP action occurs when the browser's back or forward buttons are used [7][8]. The router calculates the navigation delta by comparing the previous idx with the new idx retrieved from history.state [1][5]. If the state is corrupted or missing the idx key, this calculation can fail or result in unexpected navigation behavior [1][2]. If you are encountering missing idx keys, ensure that any manual history modifications are correctly merging the existing state rather than replacing it entirely [4].{top_results: [1][5][2][3][4]}
Citations:
- 1: https://github.com/remix-run/react-router/blob/caaae653/packages/react-router/lib/router/history.ts
- 2: [Bug]: state.idx returns NaN sometimes remix-run/react-router#10964
- 3: [Bug]: Not able to manually manipulate route state outside of react router remix-run/react-router#10428
- 4: Replace history state without redirection remix-run/react-router#11415
- 5: https://github.com/remix-run/react-router/blob/3927a948a6f0ad77358338eede8690ea91612715/packages/react-router/lib/router/history.ts
- 6: [Bug]: idx dissapeared from window.history.state in 6.4.0 remix-run/react-router#9273
- 7: https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/history.ts
- 8: https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router@6.30.3/docs/start/concepts.md
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- MapPage effect context ---'
sed -n '92,135p' schedule-app/src/pages/MapPage.jsx
sed -n '238,258p' schedule-app/src/pages/MapPage.jsx
printf '%s\n' '--- location.key usage in schedule-app ---'
grep -RIn -E -C 3 'location\.key|location\.state|useLocation\(' schedule-app/src --include='*.jsx' --include='*.js' | head -n 260Repository: affaan-m/ECC
Length of output: 17055
🏁 Script executed:
#!/bin/bash
set -u
url='https://raw.githubusercontent.com/remix-run/react-router/react-router%406.26.2/packages/react-router/lib/router/history.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
if curl -fsSL "$url" -o "$tmp"; then
printf '%s\n' '--- 6.26.2 history implementation ---'
grep -n -A35 -B12 -E 'function getIndex|let index|popstate|handlePop|v5Compat|globalHistory' "$tmp" | head -n 260
else
printf '%s\n' 'Unable to fetch the pinned source URL'
fi
printf '%s\n' '--- exact package metadata ---'
sed -n '1,35p' schedule-app/package.jsonRepository: affaan-m/ECC
Length of output: 797
🏁 Script executed:
#!/bin/bash
set -u
for url in \
'https://raw.githubusercontent.com/remix-run/react-router/v6.26.2/packages/react-router/lib/router/history.ts' \
'https://raw.githubusercontent.com/remix-run/react-router/refs/tags/react-router%406.26.2/packages/react-router/lib/router/history.ts' \
'https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/history.ts'
do
printf '\n--- %s ---\n' "$url"
if curl -fsSL "$url" | grep -n -A35 -B12 -E 'function getIndex|let index|handlePop|popstate|globalHistory' | head -n 220; then
break
fi
doneRepository: affaan-m/ECC
Length of output: 317
🌐 Web query:
site:github.com/remix-run/react-router "function getIndex" "Cannot determine the next location"
💡 Result:
The phrase "Cannot determine the next location" does not appear as a standard error message in public React Router documentation or widespread developer reports. It is likely a custom error message defined within a specific application's codebase rather than a built-in library error [1][2][3]. However, the context of your query suggests it relates to navigation handling, specifically functions like getIndex used in internal browser history management [3][4]. Technical Context: 1. Internal History Management: React Router historically used an internal idx property on window.history.state to track history position [1][2][3]. Accessing this or functions like getIndex() often leads to issues because it is an undocumented, internal API that the library maintainers explicitly advise against relying on [1][2]. 2. Troubleshooting Similar Errors: If you are seeing errors related to navigation state, location, or "next location," they often stem from: - Improper Component Nesting: Using hooks like useLocation or useNavigate in components that are not wrapped in a proper Router provider (e.g., BrowserRouter) [5][6][7]. - Stale/Missing State: Attempting to access history state that has not been initialized or was manipulated directly via window.history.pushState or replaceState by external code [2][3]. - Race Conditions: Asynchronous updates to location, particularly when using features like useBlocker or transitions, where the "next" state might not yet be synchronized with the browser's actual history stack [8][9]. If you are encountering this error, search your project's codebase for the string "Cannot determine the next location" to identify where it is thrown. It is likely a custom check added by a developer to validate navigation logic before calling a router's navigation method [3].
Citations:
- 1: [Bug]: state.idx returns NaN sometimes remix-run/react-router#10964
- 2: [Bug]: idx dissapeared from window.history.state in 6.4.0 remix-run/react-router#9273
- 3: https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/history.ts
- 4: https://github.com/remix-run/react-router/blob/caaae653/packages/react-router/lib/router/history.ts
- 5: Cannot read property 'location' of undefined at useLocation remix-run/react-router#7015
- 6: [Bug]:
useLocation()can crash whenuseContext(RouterContext)returns undefined remix-run/react-router#8104 - 7: Cannot read properties of undefined (reading 'location') remix-run/react-router#9035
- 8: [Bug]: useBlocker does have wrong
pathnameincurrentLocationandnextLocationremix-run/react-router#10549 - 9: Issues with v7_startTransition and triggering useEffect from current location remix-run/react-router#12546
🌐 Web query:
React Router history.ts "idx" "handlePop" "getIndex" source
💡 Result:
In React Router, the idx property and the getIndex and handlePop functions are internal mechanics within history.ts used to track the application's position within the browser's history stack [1][2][3]. The getIndex function retrieves the current index from the browser's history.state object [1][3]. React Router injects an idx property into the state of history entries to maintain a predictable stack position, which is essential for determining the direction of navigation (e.g., forward or backward) [1][2]. When the user navigates using the browser's back or forward buttons, a popstate event is triggered, which invokes handlePop [1][4][3]. The handlePop function performs the following steps [1][3]: 1. Sets the navigation action to Action.Pop [1][3]. 2. Retrieves the nextIndex using getIndex() [1][3]. 3. Calculates the delta by subtracting the previous index from the new one (i.e., nextIndex - index) to determine how many steps the user moved in the history stack [1][3]. 4. Updates the internal index reference and notifies any active listeners of the change [1][3]. If the index is null (which can happen upon initial page load), React Router initializes it to 0 using replaceState to ensure subsequent navigations can be tracked accurately [1][2]. Manipulating history.pushState or replaceState directly outside of React Router can cause these internal indices to become desynchronized, often resulting in navigation bugs [1].
Citations:
- 1: https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/history.ts
- 2: https://github.com/remix-run/react-router/blob/546b57e605aa7e1f5c100f94b147dd5d76515026/packages/react-router/lib/router/history.ts
- 3: https://code.swecha.org/Priyanka_07/foundit/-/blob/main/node_modules/react-router/dist/production/chunk-OSYEOCBT.mjs?ref_type=heads
- 4: https://unpkg.smartaccess.io/browse/react-router@7.0.1/dist/development/chunk-WFMEQ3WA.mjs
🏁 Script executed:
#!/bin/bash
set -u
url='https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/history.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -fsSL "$url" -o "$tmp" || exit 0
printf '%s\n' '--- source markers ---'
grep -n -E 'idx|getIndex|handlePop|popstate|replaceState' "$tmp" | head -n 100
printf '%s\n' '--- history implementation excerpt ---'
sed -n '230,430p' "$tmp" | head -n 220Repository: affaan-m/ECC
Length of output: 6763
🏁 Script executed:
#!/bin/bash
set -u
url='https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/history.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -fsSL "$url" -o "$tmp" || exit 0
sed -n '630,745p' "$tmp"Repository: affaan-m/ECC
Length of output: 3777
🏁 Script executed:
#!/bin/bash
set -u
for url in \
'https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/router.ts' \
'https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/history.ts'
do
tmp=$(mktemp)
curl -fsSL "$url" -o "$tmp" || { rm -f "$tmp"; continue; }
printf '\n--- %s delta handling ---\n' "$url"
grep -n -E -C 8 'delta|location\.key|history\.go|POP' "$tmp" | grep -E 'delta|history\.go|POP|location\.key' | head -n 220
rm -f "$tmp"
doneRepository: affaan-m/ECC
Length of output: 2543
🏁 Script executed:
#!/bin/bash
set -u
url='https://raw.githubusercontent.com/remix-run/react-router/main/packages/react-router/lib/router/router.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -fsSL "$url" -o "$tmp" || exit 0
sed -n '1285,1360p' "$tmp"Repository: affaan-m/ECC
Length of output: 3137
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- blocker usage ---'
grep -RIn -E 'useBlocker|usePrompt|blocker|shouldBlockNavigation' schedule-app/src schedule-app/package.json 2>/dev/null | head -n 120
printf '%s\n' '--- location.key-dependent application effects ---'
grep -RIn -E -C 5 'location\.key' schedule-app/src --include='*.jsx' --include='*.js' 2>/dev/nullRepository: affaan-m/ECC
Length of output: 4827
Preserve React Router history state
window.history.replaceState({}, '') removes React Router's usr, key, and idx fields. A later POP to this entry produces a missing index and a default location key. Use navigate(location.pathname, { replace: true, state: null }) to consume the intent while preserving router metadata.
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 236 - 252, In the
initialization useEffect, replace the direct window.history.replaceState call
with React Router’s navigate using the current location.pathname, replace: true,
and state: null. Preserve the existing intent-handling logic and ensure the
router’s history metadata remains intact.
| const importData = (e) => { | ||
| const file = e.target.files?.[0]; | ||
| if (!file) return; | ||
| const reader = new FileReader(); | ||
| reader.onload = () => { | ||
| try { | ||
| const data = JSON.parse(reader.result); | ||
| actions.importData(data); | ||
| } catch { | ||
| alert('That file could not be read as a Keystone backup.'); | ||
| } | ||
| }; | ||
| reader.readAsText(file); | ||
| e.target.value = ''; | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
importData sends unvalidated file content straight into the store.
Line 318 parses the file and Line 319 passes the result to actions.importData with no shape check. A syntactically valid JSON file — for example "hello", [], or an object whose goals is a string — passes the try block and replaces application state. Downstream readers such as state.goals.length on Line 343 and state.contacts.filter(...) on Line 823 then throw, which breaks the page for the user with no way back except clearing storage.
Validate the parsed object against a schema before importing, and report a clear failure message when validation fails.
🛡️ Proposed fix
+import { z } from 'zod';
+
+const BackupSchema = z.object({
+ goals: z.array(z.object({}).passthrough()).default([]),
+ events: z.array(z.object({}).passthrough()).default([]),
+ contacts: z.array(z.object({}).passthrough()).default([]),
+ statuses: z.array(z.object({}).passthrough()).default([]),
+ settings: z.object({}).passthrough().default({}),
+}); reader.onload = () => {
- try {
- const data = JSON.parse(reader.result);
- actions.importData(data);
- } catch {
- alert('That file could not be read as a Keystone backup.');
- }
+ const parsed = BackupSchema.safeParse(
+ (() => {
+ try {
+ return JSON.parse(reader.result);
+ } catch {
+ return null;
+ }
+ })()
+ );
+ if (!parsed.success) {
+ alert('That file could not be read as a Keystone backup.');
+ return;
+ }
+ actions.importData(parsed.data);
};As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate" and "Use Zod for schema-based input validation in TypeScript/JavaScript".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const importData = (e) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| try { | |
| const data = JSON.parse(reader.result); | |
| actions.importData(data); | |
| } catch { | |
| alert('That file could not be read as a Keystone backup.'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| e.target.value = ''; | |
| }; | |
| import { z } from 'zod'; | |
| const BackupSchema = z.object({ | |
| goals: z.array(z.object({}).passthrough()).default([]), | |
| events: z.array(z.object({}).passthrough()).default([]), | |
| contacts: z.array(z.object({}).passthrough()).default([]), | |
| statuses: z.array(z.object({}).passthrough()).default([]), | |
| settings: z.object({}).passthrough().default({}), | |
| }); | |
| const importData = (e) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const parsed = BackupSchema.safeParse( | |
| (() => { | |
| try { | |
| return JSON.parse(reader.result); | |
| } catch { | |
| return null; | |
| } | |
| })() | |
| ); | |
| if (!parsed.success) { | |
| alert('That file could not be read as a Keystone backup.'); | |
| return; | |
| } | |
| actions.importData(parsed.data); | |
| }; | |
| reader.readAsText(file); | |
| e.target.value = ''; | |
| }; |
🤖 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 `@schedule-app/src/pages/MorePage.jsx` around lines 312 - 326, Update
importData to validate the parsed JSON against a Zod schema matching the
expected Keystone backup shape before calling actions.importData, including
validating fields such as goals and contacts as arrays with appropriate item
structures. Treat schema failures the same as parse failures and show a clear
backup-validation error through the existing alert path; only import data after
successful validation.
Source: Coding guidelines
| <SettingsGroup {...grp('g16')}> | ||
| <span className="detail-label">Feedback</span> | ||
| <p className="muted small">Have an idea or found a bug? I'd love to hear it.</p> | ||
| <div className="stack-btns"> | ||
| <button className="btn btn-ghost full" onClick={() => setFeedback('')}> | ||
| <Icon name="lightbulb" /> Send feedback / suggest a feature | ||
| </button> | ||
| {/* The tour narrates the Home screen, so it goes there rather | ||
| than playing on top of Settings. App.jsx owns the Tutorial for | ||
| both the first run and this replay, so there's one code path. */} | ||
| <button | ||
| className="btn btn-ghost full" | ||
| onClick={() => navigate('/', { state: { replayTour: true } })} | ||
| > | ||
| <Icon name="play" /> Replay the tour | ||
| </button> | ||
| </div> | ||
| </SettingsGroup> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: List settings group ids and grp() call sites, then report duplicates.
rg -n "id: '" schedule-app/src/pages/MorePage.jsx
echo '--- grp call sites ---'
rg -no "grp\('[^']+'\)" schedule-app/src/pages/MorePage.jsx | sort
echo '--- duplicate grp ids ---'
rg -o "grp\('[^']+'\)" schedule-app/src/pages/MorePage.jsx | sort | uniq -dRepository: affaan-m/ECC
Length of output: 2679
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '90,185p' schedule-app/src/pages/MorePage.jsx
echo '--- grp definition and usages ---'
rg -n -C 8 "function grp|const grp|grp\s*=" schedule-app/src/pages/MorePage.jsx
echo '--- g16 references ---'
rg -n -C 4 "g16" schedule-app/src/pages/MorePage.jsxRepository: affaan-m/ECC
Length of output: 6811
Use a unique settings-group ID for Feedback. g16 is assigned to both Event colors and Feedback, so search and open-state handling can target both groups.
🤖 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 `@schedule-app/src/pages/MorePage.jsx` around lines 1029 - 1046, Update the
Feedback SettingsGroup in the MorePage component to use a unique group ID
instead of g16. Ensure the replacement does not match the existing Event colors
group, preserving independent search and open-state behavior.
There was a problem hiding this comment.
Actionable comments posted: 43
♻️ Duplicate comments (10)
.github/workflows/deploy-schedule-app.yml (3)
35-35: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winPin all four actions to full commit SHAs.
Lines 35, 40, 55, and 70 reference mutable tags. A tag can be repointed to a different commit, which gives the referenced action code the workflow token. Replace each tag with the resolved commit SHA and keep the tag as a trailing comment.
Also applies to: 40-40, 55-55, 70-70
🤖 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 @.github/workflows/deploy-schedule-app.yml at line 35, Update the four action references at the workflow steps using actions/checkout and the other actions on lines 40, 55, and 70 to use their full resolved commit SHAs instead of mutable tags, preserving each original tag as a trailing comment.Sources: Path instructions, Linters/SAST tools
9-11: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRemove the feature branch from the Pages deploy trigger.
Line 11 lets
claude/missionary-calendar-app-ti6dr6publish to the same Pages site asmain. GitHub creates a referenced environment with no branch protection by default, so the feature branch overwrites the production site on every push.🔒 Proposed fix
branches: - main - - claude/missionary-calendar-app-ti6dr6As per path instructions: "Treat workflow changes as security-sensitive."
🤖 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 @.github/workflows/deploy-schedule-app.yml around lines 9 - 11, Remove the claude/missionary-calendar-app-ti6dr6 entry from the branches list in the Pages deployment workflow, leaving main as the only deploy trigger so feature-branch pushes cannot publish to the production site.Source: Path instructions
18-21: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winScope the deploy permissions to the
deployjob, and replace Node 20.
pages: writeandid-token: writesit at the workflow level, so thebuildjob that runsnpm cion third-party packages inherits the Pages deployment credentials. Keep onlycontents: readat the workflow level and declare the write scopes underdeploy.permissions.- Node.js 20 reached end of life on 2026-04-30 and receives no security patches. Move to a supported LTS release.
🔒 Proposed fix
-# Least-privilege token, plus the scopes the Pages deploy needs. +# Least-privilege token. The Pages write scopes live on the deploy job only. permissions: contents: read - pages: write - id-token: write- name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22'deploy: name: Deploy needs: build runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + # Required to create the Pages deployment. + pages: write + # Required for the OIDC token that authenticates the deployment. + id-token: writeAs per path instructions: "Flag unpinned third-party actions, broad write permissions".
Also applies to: 42-42
🤖 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 @.github/workflows/deploy-schedule-app.yml around lines 18 - 21, Restrict workflow-level permissions to contents: read, and add pages: write plus id-token: write to the deploy job’s permissions block so build does not inherit deployment credentials. Update the workflow’s Node.js 20 setup to a supported LTS release, and pin any third-party actions in the workflow to immutable versions.Sources: Path instructions, Linters/SAST tools
backend/src/routes/calendars.js (3)
233-260: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate
date,start, andendbefore persisting.The POST handler checks truthiness only. The PATCH loop accepts any string, including
''. Line 102 orders events withorderBy: { date: 'asc' }, which is a lexicographic sort over a text column. A non-YYYY-MM-DDvalue corrupts event ordering for every member of the calendar, and the corruption persists.🛡️ Proposed fix
+const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; + +function validateEventFields({ date, start, end }) { + if (date !== undefined && !DATE_RE.test(date)) return 'date must be YYYY-MM-DD'; + if (start !== undefined && !TIME_RE.test(start)) return 'start must be HH:MM'; + if (end !== undefined && !TIME_RE.test(end)) return 'end must be HH:MM'; + if (start !== undefined && end !== undefined && end < start) return 'end must not precede start'; + return 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 `@backend/src/routes/calendars.js` around lines 233 - 260, Update the POST handler and PATCH flow for shared events to validate date, start, and end against the required non-empty YYYY-MM-DD/date-time formats before calling prisma.sharedEvent.create or update. Reject invalid or empty values with the existing 400 validation response, while preserving valid field updates and preventing malformed values from being persisted.
209-224: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick winBind invite acceptance to the invited email address.
The handler still accepts the invite for any authenticated caller that presents the token. It never compares
invite.emailwithreq.dbUser.email. Anyone who obtains a forwarded token joins the private calendar with the invited role.🔒 Proposed fix
if (invite.expiresAt < new Date()) return res.status(400).json({ error: 'Invite expired' }); + if (invite.email !== req.dbUser.email.trim().toLowerCase()) { + return res.status(403).json({ error: 'This invite was sent to a different email address' }); + }🤖 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 `@backend/src/routes/calendars.js` around lines 209 - 224, Update the /invites/:token/accept handler to compare invite.email with req.dbUser.email before running the acceptance transaction, rejecting mismatches without creating membership or marking the invite accepted. Preserve the existing success and validation behavior for the invited user.
164-196: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winAlign the permission model across the three invite routes.
Line 176 lets any editor create an invite. Line 164 and line 196 restrict listing and revoking to the owner. An editor can grant access and then cannot see or revoke that invite. Pick one model.
🤖 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 `@backend/src/routes/calendars.js` around lines 164 - 196, Align the middleware permissions for the three invite routes: update the create route near the POST handler to use the same owner-level requirement as the GET and DELETE handlers, or consistently relax all three to the editor-level requirement. Apply the chosen model uniformly across listing, creation, and revocation without changing their invite behavior.schedule-app/public/sw.js (1)
78-80: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winKeep the cache writes alive with
event.waitUntil().Both
cache.put()calls run outside therespondWith()promise chain. The service worker can terminate before either write completes, so offline availability is unreliable. Attach both writes toevent.waitUntil()and swallow cache-write failures without replacing the successful network response.Also applies to: 102-105
🤖 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 `@schedule-app/public/sw.js` around lines 78 - 80, Update the fetch handler’s cache-write logic around the response clone and both cache.put calls to register each write with event.waitUntil(). Keep the successful network response returned through respondWith(), and catch or otherwise swallow cache-write failures so they cannot replace that response.schedule-app/src/pages/PricingPage.jsx (1)
21-21: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe color-theme count does not match the implementation.
The row states 22 themes.
schedule-app/src/pages/MorePage.jsxdefines 17 entries inCOLOR_SCHEMESand 6 inPASTEL_SCHEMES, which is 23. Derive the count from those collections, or correct the text.🤖 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 `@schedule-app/src/pages/PricingPage.jsx` at line 21, Update the pricing feature label near the color-theme entry to match the 23 themes defined by COLOR_SCHEMES and PASTEL_SCHEMES, or derive the displayed count directly from those collections so it stays synchronized.schedule-app/index.html (1)
5-8: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
maximum-scale=1.0blocks pinch-zoom.Mobile Safari and several Android browsers honor
maximum-scale=1.0and disable pinch-zoom. Users with low vision cannot enlarge the planner grid, contact rows, or map labels. This fails WCAG 2.1 SC 1.4.4.viewport-fit=coverandinteractive-widget=resizes-contentwork without the scale lock.🛠️ Proposed fix
- content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" + content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"🤖 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 `@schedule-app/index.html` around lines 5 - 8, Remove the maximum-scale=1.0 restriction from the viewport meta tag while preserving width, initial-scale, viewport-fit, and interactive-widget settings so users can pinch-zoom the planner interface.schedule-app/src/App.jsx (1)
128-172: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftBlock pushes until the first pull completes.
When
activebecomes true, line 134 starts the pull and the push effect starts its 2500 ms timer in parallel. If the pull takes longer thanPUSH_DEBOUNCE_MS, the push overwrites the remote snapshot with stale local state. The sync is last-write-wins, so the remote data is lost.Gate the push effect on an
initialPullDoneflag. Reset the flag whenactivebecomes false. Set it after every successful pull, so a retry unlocks pushes after an initial failure.🤖 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 `@schedule-app/src/App.jsx` around lines 128 - 172, Gate the debounced push effect on an initialPullDone flag so it cannot schedule pushSyncedData before the first pull succeeds. Reset the flag when active becomes false, and set it after every successful pull so a later successful retry unlocks pushes; update the existing pull completion flow and push effect while preserving current cleanup and error handling.
🤖 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 `@backend/README.md`:
- Around line 40-41: Update the startup flow in server.js around REQUIRED_ENV
validation so missing required configuration prevents unavailable routes from
serving traffic: exit before app.listen() when required variables are absent,
while preserving the unauthenticated /api/health response of {"ok":true} if
health must remain available before configuration.
In `@backend/src/routes/assistant.js`:
- Around line 298-307: Update the request validation around the messages check
in the assistant route to apply schema-based validation to every message before
assistantTurn forwards it to Anthropic. Restrict role to user or assistant and
content to either a string or the supported block-object shapes emitted by the
app; reject invalid or nested unsupported structures with a clear 400 response
before any API call. Preserve the existing non-empty, count, and body-size
limits.
In `@backend/src/routes/calendars.js`:
- Around line 73-90: Apply express-rate-limit middleware to the calendars router
mount in app.js so all calendar endpoints, including POST /, POST /:id/events,
and other public routes, are rate limited. Add a stricter route-specific limiter
to the POST /:id/invites handler, reusing the project’s established rate-limit
configuration conventions where available.
- Around line 128-135: Add a committed Prisma migration defining the
SharedCalendar, SharedCalendarMember, SharedCalendarInvite, and SharedEvent
tables with all required columns, indexes, constraints, and cascading foreign
keys from the Prisma schema. Ensure the migration is included in the deployable
migration history so prisma migrate deploy creates these tables before the route
using prisma.sharedCalendar.delete is invoked.
In `@backend/src/routes/data.js`:
- Around line 19-33: Update the router.put handler and pushSyncedData/DataSync
flow to include a base version with each sync push, then perform the userData
update atomically only when the stored version matches that base. Return HTTP
409 on a version mismatch without replacing server or local data, and make
pushSyncedData handle 409 by preserving the local offline edits for conflict
resolution rather than overwriting them.
In `@schedule-app/package.json`:
- Around line 7-11: Add `@playwright/test` to devDependencies and extend the
package scripts with test commands covering unit tests, Playwright regression
tests, and coverage reporting. Ensure the commands invoke the repository’s
configured test runners and support the stated minimum 80% coverage requirement.
- Around line 12-22: Update the react-router-dom dependency in package.json to
version 7.18.0 or later, then regenerate the lockfile so it resolves to the
patched release and no longer uses affected 6.x versions.
In `@schedule-app/public/sw.js`:
- Around line 1-12: Declare the service-worker lint environment for public/sw.js
so ESLint recognizes the runtime globals self and caches. Add the serviceworker
eslint environment directive at the file top or configure the equivalent
file-specific override, without changing the service-worker logic.
- Around line 39-59: Update both the activate handler’s cache cleanup and the
keystone:reset branch in the message handler to operate only on cache keys
beginning with the keystone- prefix. Preserve retaining CACHE during activation,
and preserve deleting all Keystone caches before unregistering without touching
unrelated origin caches.
In `@schedule-app/src/App.jsx`:
- Around line 234-255: Extract the independent side effects from App() into
named hooks under schedule-app/src/data/, including the reminder scanner and the
delegated haptics, blur-on-tap, scroll-reset, sticky-header, tutorial-gate,
theme-applier, and display-preference effects. Preserve each effect’s existing
behavior and cleanup while composing the hooks from App() so the component is
reduced to state, navigation, and rendering logic.
- Around line 218-222: Replace the empty error callback in
navigator.geolocation.watchPosition within App.jsx#L218-L222 with a handler that
logs the error and displays a user-friendly message when err.code ===
err.PERMISSION_DENIED. Also replace the empty catch handler for
navigator.serviceWorker.register in main.jsx#L36-L42 with logging of the
registration error; both sites require direct changes.
- Around line 412-417: Update the replayTour handling in the useEffect to clear
the route state through React Router by calling navigate with location.pathname,
replace: true, and state: null; remove the direct window.history.replaceState
call and ensure navigate is available to the effect.
In `@schedule-app/src/components/Avatar.jsx`:
- Around line 28-35: Validate the selected file in handleFile before calling
fileToAvatarDataUrl: reject unsupported image MIME types and files exceeding a
named maximum size, then show a fixed user-safe error and stop processing.
Preserve the existing empty-selection handling and only set busy state after
validation succeeds.
In `@schedule-app/src/components/EditorSheet.jsx`:
- Around line 86-103: Update onPointerUp to accept the pointer event and
calculate the final downward offset from e.clientY and startY.current instead of
the potentially stale dragY state, then dismiss when that offset exceeds
DISMISS_THRESHOLD. Add or update onPointerCancel to only reset dragging and drag
position without calling requestClose.
In `@schedule-app/src/components/MiniMapPicker.jsx`:
- Around line 25-26: Update MiniMapPicker’s map initialization and marker path
to use a shared coordinate validator that accepts only finite latitude/longitude
values within their valid bounds. Replace the lat-only check before map.setView
with this validator, fall back to DEFAULT_VIEW when validation fails, and skip
marker creation and panning for invalid coordinates.
In `@schedule-app/src/components/Modal.jsx`:
- Around line 37-42: Update onPointerUp to accept the pointer event and
calculate the release distance from e.clientY and startY rather than rendered
dragY before deciding whether to call onClose. Keep the normal release path
resetting dragging and dragY, and introduce a separate onPointerCancel handler
that resets dragging, startY, and dragY without dismissing the modal.
In `@schedule-app/src/components/ReorderToggleList.jsx`:
- Around line 69-80: Update onMove to avoid mutating the existing drag
descriptor: create a new descriptor based on g with the calculated to value,
assign it to dragRef.current, and use that updated descriptor for subsequent
state handling while preserving the existing selectTick behavior.
In `@schedule-app/src/components/SmartQuickAdd.jsx`:
- Around line 39-150: Refactor SmartQuickAdd so it remains responsible only for
modal state, parsing, reset/close behavior, and submit orchestration. Extract
the chips/parsed-preview construction and preview JSX, including the type
selector, into focused helper functions or components, passing only the required
parsed data and handlers while preserving current rendering and behavior.
In `@schedule-app/src/components/SwipeRow.jsx`:
- Around line 76-89: Update the gesture state transitions in the pointer-move
logic around gestureRef.current so they no longer mutate the existing gesture
object. When setting dragging and when updating armed state, create and assign a
new gesture object that preserves the other gesture fields while changing only
the relevant property.
In `@schedule-app/src/components/Tutorial.jsx`:
- Around line 54-83: Add modal behavior to the tutorial component containing the
`tutorial-backdrop` and `tutorial-card`: apply `role="dialog"`,
`aria-modal="true"`, and an accessible label to the dialog, focus the dialog or
an appropriate control on open, restore focus to the previously focused element
on close, and close via Escape by invoking `onDone`. Keep keyboard focus
contained within the active tutorial until it closes.
In `@schedule-app/src/data/api.js`:
- Around line 56-76: Update the URL construction in fetchCalendar,
renameCalendar, deleteCalendar, removeCalendarMember, fetchCalendarInvites,
inviteToCalendar, revokeCalendarInvite, acceptCalendarInvite, addSharedEvent,
updateSharedEvent, and deleteSharedEvent to wrap every interpolated path
parameter—id, memberId, inviteId, token, and eventId—with encodeURIComponent
before insertion, preserving the existing endpoints and request options.
In `@schedule-app/src/data/goalHistory.js`:
- Around line 17-45: Validate and normalize goal.repeatDays before defining or
using isScheduled in buildGoalHistory: retain only numeric weekday values in the
inclusive 0–6 range, and treat an empty or entirely invalid result as
unscheduled-day filtering disabled. Ensure both scheduling loops terminate even
when persisted repeatDays contains invalid data.
In `@schedule-app/src/data/ics.js`:
- Around line 100-102: Update the iCalendar parsing logic around the SUMMARY,
LOCATION, and DESCRIPTION assignments to add one shared text-decoding helper
that reverses all escapes produced by escapeText, including backslashes, commas,
semicolons, and newlines. Apply that decoder consistently to each persisted text
field instead of maintaining separate replace chains.
In `@schedule-app/src/data/notifications.js`:
- Around line 39-40: Update the catch block in the notification operation to
return an explicit failure result instead of silently ignoring the exception.
Preserve the caller-facing abstraction by exposing a controlled failure value
rather than browser internals, while allowing the caller to retain
observability.
- Around line 74-79: Update the fire function so it does not mutate the existing
fired Set via fired.add(key); instead, create a new Set containing the existing
entries and key, then assign that new Set to fired while preserving the current
notification and changed behavior.
In `@schedule-app/src/data/nudges.js`:
- Around line 29-135: Replace every in-place nudges.push call in the
nudge-building function with immutable array reassignment using spread
operations, including the date, streak, follow-up, overdue-contact, and
milestone nudge branches. Preserve the existing ordering and conditional
behavior while ensuring each added nudge produces a new nudges array.
In `@schedule-app/src/data/smartParse.js`:
- Around line 196-206: Update the numeric-date handling in the parser around the
numeric match and calendarDate so a first component above 12 is not passed as a
month index; interpret that form as day/month or reject it. Also harden
calendarDate to reject month indexes outside 0–11, while preserving valid
numeric month/day parsing.
In `@schedule-app/src/data/store.jsx`:
- Around line 376-377: Update the IMPORT_DATA handling in the reducer to pass
action.data through a new sanitizeImport helper before merging with makeSeed().
Implement sanitizeImport in a small module beside seed.js using the existing Zod
dependency; retain only recognized state keys and reject imports where contacts,
statuses, goals, or other collection fields are not arrays, failing fast with a
clear validation error before dispatch commits state.
- Around line 145-156: Update the data-loading function around the parsed
collections so goals, events, tasks, notes, interactions, and templates are
validated/coerced to arrays before any .map or related processing, preserving
stored data when external shapes are invalid. Retain the outer catch only for
JSON parsing failures, and log that failure explicitly before returning
makeSeed(); do not silently discard data for collection shape mismatches.
In `@schedule-app/src/data/vcard.js`:
- Around line 56-58: Update the vCard parsing loop to use trimmed content only
for blank-line detection, while preserving the original raw line for property
parsing and unescapeText processing. Ensure intentional leading and trailing
whitespace in values such as NOTE and FN remains unchanged.
In `@schedule-app/src/data/weeklyRecap.js`:
- Around line 34-38: Update the contactsReconnected filter and tasksCompleted
reduce in the weekly recap calculation to use elapsedDays.includes(...) instead
of days.includes(...), ensuring only dates through the current day are counted
while preserving the existing fallback and aggregation behavior.
In `@schedule-app/src/pages/ContactDetailPage.jsx`:
- Around line 117-146: Update saveEdit so the syncContactAddressPin call has
explicit rejection handling: attach a catch handler that reports any address-pin
synchronization failure to the user through the existing error-reporting
mechanism, without silently swallowing the error. Keep the current update flow
and conditional address-change check unchanged.
In `@schedule-app/src/pages/ContactsPage.jsx`:
- Around line 235-241: Update the asynchronous loop around syncContactAddressPin
in ContactsPage so it tracks cancellation and stops before processing or syncing
contacts after unmount. Trigger cancellation from the component cleanup and
ensure pending delays cannot resume geocoding or add contact-address pins once
ContactsPage is unmounted.
In `@schedule-app/src/pages/GoalsPage.jsx`:
- Around line 429-449: Update applyDelta to stop and clear the active
hold/repeat when a decrement cannot change the value, specifically when the goal
progress has reached its lower bound. Move the pointer-release and cancellation
handlers from the disabled decrement button to a non-disabled stepper wrapper so
endHold and clearHold still run when the button becomes disabled; preserve
normal click suppression and repeat behavior.
In `@schedule-app/src/pages/HomePage.jsx`:
- Around line 184-200: Replace all four window.history.replaceState calls in
schedule-app/src/pages/HomePage.jsx lines 184-200 with router-aware
navigate(location.pathname, { replace: true, state: null }) calls, using the
existing navigation setup. Apply the same replacement in the quickNewContact
effect at schedule-app/src/pages/ContactsPage.jsx lines 158-163 so only the
transient state is cleared while React Router history metadata is preserved.
In `@schedule-app/src/pages/MapPage.jsx`:
- Line 874: Update the contacts sort in the pin editor options to handle
contacts with missing names by comparing fallback empty strings for both
entries. Preserve the existing alphabetical ordering and mapping behavior while
preventing localeCompare from receiving undefined values.
- Around line 458-475: Update savePin to safely normalize editing.label and
editing.notes before calling trim, using empty-string fallbacks for missing
values. Also guard the coordinate display around editing.lat.toFixed in the
editor so restored pins without coordinates do not throw, while preserving the
existing formatted output when coordinates are present.
In `@schedule-app/src/pages/MorePage.jsx`:
- Around line 817-828: Rename the status object parameter in the
state.statuses.map callback from s to a descriptive identifier such as status,
and update its id, color, label, and statusId-related references within the
callback accordingly; leave the outer settings alias unchanged.
- Around line 822-823: Replace the per-status state.contacts.filter call in
MorePage’s status rendering with a useMemo-derived count map built from
state.contacts, then read each status count by s.id. Recompute the map only when
the contacts collection changes and preserve the existing count display
behavior.
In `@schedule-app/src/pages/PricingPage.jsx`:
- Around line 102-126: Validate that the checkout and billing-portal responses
contain a non-empty URL before assigning window.location.href in handleUpgrade
and handleManage. Treat missing or null URLs as errors, route them through the
existing catch handling, and surface a generic user-facing message instead of
raw backend error details; ensure busy is cleared on this failure path.
In `@schedule-app/src/pages/SharedCalendarDetailPage.jsx`:
- Around line 114-116: Update saveEvent to safely handle a missing title by
using the same optional title guard as the validation at line 248, and add
validation that the event end time is after its start time. Keep valid event
saving unchanged while returning early for missing required values or an invalid
time range.
In `@schedule-app/src/pages/SharedCalendarsPage.jsx`:
- Around line 57-59: Validate and normalize the `calendars` value in the fetch
flow before calling `setCalendars` in `SharedCalendarsPage`: store the response
array when valid, otherwise store an empty array. Preserve the existing
rendering behavior while ensuring missing or null API fields cannot reach the
`calendars.length` access.
In `@schedule-app/vite.config.js`:
- Around line 9-12: Update the Vite server configuration to make LAN binding
opt-in: gate the host: true setting behind an environment variable and leave
server.fs.strict enabled. Preserve the existing port configuration and use the
project’s established environment-variable access pattern if available.
---
Duplicate comments:
In @.github/workflows/deploy-schedule-app.yml:
- Line 35: Update the four action references at the workflow steps using
actions/checkout and the other actions on lines 40, 55, and 70 to use their full
resolved commit SHAs instead of mutable tags, preserving each original tag as a
trailing comment.
- Around line 9-11: Remove the claude/missionary-calendar-app-ti6dr6 entry from
the branches list in the Pages deployment workflow, leaving main as the only
deploy trigger so feature-branch pushes cannot publish to the production site.
- Around line 18-21: Restrict workflow-level permissions to contents: read, and
add pages: write plus id-token: write to the deploy job’s permissions block so
build does not inherit deployment credentials. Update the workflow’s Node.js 20
setup to a supported LTS release, and pin any third-party actions in the
workflow to immutable versions.
In `@backend/src/routes/calendars.js`:
- Around line 233-260: Update the POST handler and PATCH flow for shared events
to validate date, start, and end against the required non-empty
YYYY-MM-DD/date-time formats before calling prisma.sharedEvent.create or update.
Reject invalid or empty values with the existing 400 validation response, while
preserving valid field updates and preventing malformed values from being
persisted.
- Around line 209-224: Update the /invites/:token/accept handler to compare
invite.email with req.dbUser.email before running the acceptance transaction,
rejecting mismatches without creating membership or marking the invite accepted.
Preserve the existing success and validation behavior for the invited user.
- Around line 164-196: Align the middleware permissions for the three invite
routes: update the create route near the POST handler to use the same
owner-level requirement as the GET and DELETE handlers, or consistently relax
all three to the editor-level requirement. Apply the chosen model uniformly
across listing, creation, and revocation without changing their invite behavior.
In `@schedule-app/index.html`:
- Around line 5-8: Remove the maximum-scale=1.0 restriction from the viewport
meta tag while preserving width, initial-scale, viewport-fit, and
interactive-widget settings so users can pinch-zoom the planner interface.
In `@schedule-app/public/sw.js`:
- Around line 78-80: Update the fetch handler’s cache-write logic around the
response clone and both cache.put calls to register each write with
event.waitUntil(). Keep the successful network response returned through
respondWith(), and catch or otherwise swallow cache-write failures so they
cannot replace that response.
In `@schedule-app/src/App.jsx`:
- Around line 128-172: Gate the debounced push effect on an initialPullDone flag
so it cannot schedule pushSyncedData before the first pull succeeds. Reset the
flag when active becomes false, and set it after every successful pull so a
later successful retry unlocks pushes; update the existing pull completion flow
and push effect while preserving current cleanup and error handling.
In `@schedule-app/src/pages/PricingPage.jsx`:
- Line 21: Update the pricing feature label near the color-theme entry to match
the 23 themes defined by COLOR_SCHEMES and PASTEL_SCHEMES, or derive the
displayed count directly from those collections so it stays synchronized.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2909c411-6ed3-4a2e-ba2b-afeadac2fea1
⛔ Files ignored due to path filters (9)
backend/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/appstore-assets/icon-1024.pngis excluded by!**/*.pngschedule-app/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/public/apple-touch-icon.pngis excluded by!**/*.pngschedule-app/public/icon-192.pngis excluded by!**/*.pngschedule-app/public/icon-512-maskable.pngis excluded by!**/*.pngschedule-app/public/icon-512.pngis excluded by!**/*.pngschedule-app/public/icon-maskable.svgis excluded by!**/*.svgschedule-app/public/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (111)
.github/workflows/deploy-schedule-app.yml.gitignorebackend/.env.examplebackend/.gitignorebackend/README.mdbackend/package.jsonbackend/prisma.config.mjsbackend/prisma/migrations/20260722012026_init/migration.sqlbackend/prisma/migrations/20260722062956_add_user_data/migration.sqlbackend/prisma/migrations/20260727010000_lifetime_purchase/migration.sqlbackend/prisma/migrations/migration_lock.tomlbackend/prisma/schema.prismabackend/src/app.jsbackend/src/db.jsbackend/src/middleware/requireUser.jsbackend/src/routes/assistant.jsbackend/src/routes/billing.jsbackend/src/routes/calendars.jsbackend/src/routes/data.jsbackend/src/routes/me.jsbackend/src/routes/webhooksClerk.jsbackend/src/routes/webhooksStripe.jsbackend/src/server.jsschedule-app/.env.exampleschedule-app/README.mdschedule-app/index.htmlschedule-app/package.jsonschedule-app/public/manifest.webmanifestschedule-app/public/sw.jsschedule-app/src/App.jsxschedule-app/src/components/AddressField.jsxschedule-app/src/components/AnimatedNumber.jsxschedule-app/src/components/AssistantBubble.jsxschedule-app/src/components/Avatar.jsxschedule-app/src/components/Checkbox.jsxschedule-app/src/components/DesertEmptyState.jsxschedule-app/src/components/EditorSheet.jsxschedule-app/src/components/ExpandableFab.jsxschedule-app/src/components/GroupPicker.jsxschedule-app/src/components/Icon.jsxschedule-app/src/components/Logo.jsxschedule-app/src/components/MilestoneCelebration.jsxschedule-app/src/components/MiniMapPicker.jsxschedule-app/src/components/Modal.jsxschedule-app/src/components/ReorderToggleList.jsxschedule-app/src/components/Select.jsxschedule-app/src/components/SettingsGroup.jsxschedule-app/src/components/SettingsSection.jsxschedule-app/src/components/SmartQuickAdd.jsxschedule-app/src/components/SwipeRow.jsxschedule-app/src/components/SwipeToDelete.jsxschedule-app/src/components/TabBar.jsxschedule-app/src/components/Tutorial.jsxschedule-app/src/data/aiConfig.jsschedule-app/src/data/api.jsschedule-app/src/data/assistantContext.jsschedule-app/src/data/assistantTools.jsschedule-app/src/data/clerkConfig.jsschedule-app/src/data/conflicts.jsschedule-app/src/data/contactDates.jsschedule-app/src/data/contactInsights.jsschedule-app/src/data/contactSwipe.jsschedule-app/src/data/geocode.jsschedule-app/src/data/goalHistory.jsschedule-app/src/data/haptics.jsschedule-app/src/data/helpers.jsschedule-app/src/data/homeBlocks.jsschedule-app/src/data/ics.jsschedule-app/src/data/image.jsschedule-app/src/data/mapStyles.jsschedule-app/src/data/maps.jsschedule-app/src/data/nlSearch.jsschedule-app/src/data/notifications.jsschedule-app/src/data/nudges.jsschedule-app/src/data/pinLabel.jsschedule-app/src/data/quickAdd.jsschedule-app/src/data/reconnect.jsschedule-app/src/data/routePlanner.jsschedule-app/src/data/routePlannerConfig.jsschedule-app/src/data/seed.jsschedule-app/src/data/smartParse.jsschedule-app/src/data/store.jsxschedule-app/src/data/syncStatus.jsschedule-app/src/data/tabs.jsschedule-app/src/data/templates.jsschedule-app/src/data/toast.jsxschedule-app/src/data/useBackDismiss.jsschedule-app/src/data/useCountUp.jsschedule-app/src/data/useDeleteContact.jsschedule-app/src/data/useEdgeFade.jsschedule-app/src/data/useSmartAdd.jsschedule-app/src/data/vcard.jsschedule-app/src/data/weeklyRecap.jsschedule-app/src/main.jsxschedule-app/src/pages/ContactDetailPage.jsxschedule-app/src/pages/ContactTimelinePage.jsxschedule-app/src/pages/ContactsPage.jsxschedule-app/src/pages/GoalHistoryPage.jsxschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/HomePage.jsxschedule-app/src/pages/MapPage.jsxschedule-app/src/pages/MorePage.jsxschedule-app/src/pages/PlannerPage.jsxschedule-app/src/pages/PricingPage.jsxschedule-app/src/pages/RoutePlannerPage.jsxschedule-app/src/pages/SearchPage.jsxschedule-app/src/pages/SharedCalendarDetailPage.jsxschedule-app/src/pages/SharedCalendarJoinPage.jsxschedule-app/src/pages/SharedCalendarsPage.jsxschedule-app/src/styles.cssschedule-app/vite.config.js
| `GET /api/health` should return `{"ok":true}` even before Clerk/Stripe are | ||
| configured — auth-gated routes will 500 until those env vars are real. |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline backend/src/app.js --items all
ast-grep outline backend/src/server.js --items all
rg -n -C 4 \
'process\.env|CLERK_|STRIPE_|ANTHROPIC_|DATABASE_URL|listen\(|throw new Error' \
backend/src/app.js backend/src/server.jsRepository: affaan-m/ECC
Length of output: 2677
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- backend/src/server.js ---'
cat -n backend/src/server.js
printf '%s\n' '--- backend/src/app.js ---'
cat -n backend/src/app.js
printf '%s\n' '--- route configuration usage ---'
rg -n -C 3 \
'CLERK_|STRIPE_|process\.env|clerkMiddleware|auth\(|requireAuth|new Stripe|DATABASE_URL' \
backend/src backend/package.jsonRepository: affaan-m/ECC
Length of output: 9259
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- package metadata ---'
cat -n backend/package.json
rg -n -C 2 \
'requireAuth|auth\(|clerkMiddleware|STRIPE_SECRET_KEY|STRIPE_PRICE_ID|CLERK_SECRET_KEY|CLERK_PUBLISHABLE_KEY' \
backend/src backend/package-lock.json backend/pnpm-lock.yaml backend/yarn.lock 2>/dev/null || true
printf '%s\n' '--- route entry points ---'
for file in backend/src/routes/me.js backend/src/routes/billing.js backend/src/routes/data.js backend/src/routes/calendars.js backend/src/routes/assistant.js; do
if [ -f "$file" ]; then
echo "--- $file ---"
cat -n "$file"
fi
doneRepository: affaan-m/ECC
Length of output: 37845
Fail fast when required route configuration is missing.
backend/src/server.js only logs a warning and still calls app.listen() when REQUIRED_ENV is incomplete. Exit before accepting traffic when required variables are missing, or disable unavailable routes with a controlled response.
🤖 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 `@backend/README.md` around lines 40 - 41, Update the startup flow in server.js
around REQUIRED_ENV validation so missing required configuration prevents
unavailable routes from serving traffic: exit before app.listen() when required
variables are absent, while preserving the unauthenticated /api/health response
of {"ok":true} if health must remain available before configuration.
Source: Coding guidelines
| const { messages, context } = req.body || {}; | ||
| if (!Array.isArray(messages) || messages.length === 0) { | ||
| return res.status(400).json({ error: 'Request body must be { messages: [...] }' }); | ||
| } | ||
| if (messages.length > MAX_MESSAGES) { | ||
| return res.status(400).json({ error: 'Conversation too long — start a new one.', code: 'too_long' }); | ||
| } | ||
| if (JSON.stringify(messages).length > MAX_BODY_CHARS) { | ||
| return res.status(400).json({ error: 'Conversation too long — start a new one.', code: 'too_long' }); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate the shape of each message before forwarding it to Anthropic.
Lines 299-307 check only that messages is a non-empty array within size limits. Element shape is never checked. assistantTurn passes the array straight to messages.stream. A client can post arbitrary objects, arbitrary role values, or nested content blocks that the app never produces. The failure surfaces as a generic BadRequestError after a paid round trip to the API.
Validate with a schema at the boundary: role restricted to user or assistant, and content restricted to a string or an array of the block shapes this app emits.
As per coding guidelines: "Validate all external and user input at system boundaries using schema-based validation; fail fast with clear messages and never trust external data."
🤖 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 `@backend/src/routes/assistant.js` around lines 298 - 307, Update the request
validation around the messages check in the assistant route to apply
schema-based validation to every message before assistantTurn forwards it to
Anthropic. Restrict role to user or assistant and content to either a string or
the supported block-object shapes emitted by the app; reject invalid or nested
unsupported structures with a clear 400 response before any API call. Preserve
the existing non-empty, count, and body-size limits.
Source: Coding guidelines
| router.post('/', requireUser, async (req, res, next) => { | ||
| const name = (req.body?.name || '').trim(); | ||
| if (!name) return res.status(400).json({ error: 'name is required' }); | ||
| const color = typeof req.body?.color === 'string' ? req.body.color : undefined; | ||
| try { | ||
| const calendar = await prisma.sharedCalendar.create({ | ||
| data: { | ||
| name, | ||
| ...(color ? { color } : {}), | ||
| ownerId: req.dbUser.id, | ||
| members: { create: { userId: req.dbUser.id, role: 'owner' } }, | ||
| }, | ||
| }); | ||
| res.status(201).json({ calendar }); | ||
| } catch (err) { | ||
| next(err); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Add rate limiting to the write endpoints.
No route in this file is rate limited, and backend/src/app.js lines 12-45 mount no rate-limit middleware. POST /:id/invites is the sharpest case: an authenticated editor can create unbounded invite rows, each holding an email address and a long-lived token. POST / and POST /:id/events allow unbounded row creation as well.
Apply a limiter such as express-rate-limit to the mounted router in backend/src/app.js, and a tighter limit on the invite route.
As per coding guidelines: "Implement rate limiting on all endpoints to prevent abuse" and "Rate limiting must be applied to all public endpoints".
Also applies to: 176-194
🤖 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 `@backend/src/routes/calendars.js` around lines 73 - 90, Apply
express-rate-limit middleware to the calendars router mount in app.js so all
calendar endpoints, including POST /, POST /:id/events, and other public routes,
are rate limited. Add a stricter route-specific limiter to the POST /:id/invites
handler, reusing the project’s established rate-limit configuration conventions
where available.
Source: Coding guidelines
| router.delete('/:id', requireUser, requireMember, requireOwner, async (req, res, next) => { | ||
| try { | ||
| await prisma.sharedCalendar.delete({ where: { id: req.calendar.id } }); | ||
| res.json({ ok: true }); | ||
| } catch (err) { | ||
| next(err); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the relation delete behavior for shared calendar children.
rg -n -C 3 'SharedCalendarMember|SharedCalendarInvite|SharedEvent|onDelete' backend/prisma/schema.prismaRepository: affaan-m/ECC
Length of output: 3062
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Prisma datasource and calendar relations ---'
sed -n '1,125p' backend/prisma/schema.prisma
printf '%s\n' '--- Calendar foreign keys in migrations ---'
find backend/prisma -type f -path '*/migrations/*' -name '*.sql' -print0 |
xargs -0 rg -n -C 2 'SharedCalendar|SharedEvent|SharedCalendarInvite|SharedCalendarMember|ON DELETE CASCADE|onDelete'Repository: affaan-m/ECC
Length of output: 5204
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
schema = Path("backend/prisma/schema.prisma").read_text()
models = {}
for match in re.finditer(r"model\s+(\w+)\s*\{(.*?)\n\}", schema, re.S):
models[match.group(1)] = match.group(2)
for model in ("SharedCalendarMember", "SharedCalendarInvite", "SharedEvent"):
body = models.get(model, "")
relation = re.search(
r"calendar\s+SharedCalendar\s+@relation\([^)]*onDelete:\s*(\w+)[^)]*\)",
body,
)
print(f"{model}: calendar delete action = {relation.group(1) if relation else 'not declared'}")
migration_files = sorted(Path("backend/prisma/migrations").glob("*/migration.sql"))
print(f"migration files: {len(migration_files)}")
for path in migration_files:
text = path.read_text()
calendar_fk_lines = [
line.strip()
for line in text.splitlines()
if any(name in line for name in (
"SharedCalendarMember",
"SharedCalendarInvite",
"SharedEvent",
))
and "FOREIGN KEY" in line
]
if calendar_fk_lines:
print(path)
print("\n".join(calendar_fk_lines))
PYRepository: affaan-m/ECC
Length of output: 325
🏁 Script executed:
#!/bin/bash
set -eu
find backend/prisma/migrations -maxdepth 2 -type f -name 'migration.sql' -print |
sort |
while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
rg -n -C 2 'SharedCalendar|SharedEvent|Invite|Member|FOREIGN KEY|ON DELETE' "$file" || true
doneRepository: affaan-m/ECC
Length of output: 551
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Prisma and deployment commands ---'
rg -n -C 2 'prisma (migrate|db|generate)|migrate deploy|db push|DATABASE_URL' \
backend package.json .github Dockerfile* docker-compose* 2>/dev/null || true
printf '%s\n' '--- Initial migration contents ---'
cat backend/prisma/migrations/20260722012026_init/migration.sql
printf '%s\n' '--- Migration directory files ---'
find backend/prisma -maxdepth 3 -type f -print | sortRepository: affaan-m/ECC
Length of output: 3198
Add and apply a shared-calendar migration.
The committed migrations do not create SharedCalendar, SharedCalendarMember, SharedCalendarInvite, or SharedEvent, or their cascading foreign keys. prisma migrate deploy therefore leaves this delete route unusable on a database created from these migrations.
🤖 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 `@backend/src/routes/calendars.js` around lines 128 - 135, Add a committed
Prisma migration defining the SharedCalendar, SharedCalendarMember,
SharedCalendarInvite, and SharedEvent tables with all required columns, indexes,
constraints, and cascading foreign keys from the Prisma schema. Ensure the
migration is included in the deployable migration history so prisma migrate
deploy creates these tables before the route using prisma.sharedCalendar.delete
is invoked.
| router.put('/', requireUser, async (req, res, next) => { | ||
| const { data } = req.body || {}; | ||
| if (!data || typeof data !== 'object' || Array.isArray(data)) { | ||
| return res.status(400).json({ error: 'Request body must be { data: <object> }' }); | ||
| } | ||
| try { | ||
| const row = await prisma.userData.upsert({ | ||
| where: { userId: req.dbUser.id }, | ||
| update: { data }, | ||
| create: { userId: req.dbUser.id, data }, | ||
| }); | ||
| res.json({ updatedAt: row.updatedAt }); | ||
| } catch (err) { | ||
| next(err); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect the frontend sync client for conflict handling.
fd -t f 'api.js' schedule-app/src --exec cat -n {}
rg -n -C 6 'api/data|putData|syncData|updatedAt' schedule-app/src --type=js --type=jsxRepository: affaan-m/ECC
Length of output: 5120
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- backend data route ---'
fd -t f . backend | rg '(^|/)data\.js$|schema|prisma'
for f in $(fd -t f 'data.js' backend); do
cat -n "$f"
done
printf '%s\n' '--- data API client and callers ---'
fd -t f . schedule-app/src | sort | head -200
rg -n -C 8 'fetchSyncedData|pushSyncedData|/api/data|updatedAt|localStorage' schedule-app/src -g '*.js' -g '*.jsx' -g '*.ts' -g '*.tsx' || true
printf '%s\n' '--- route registration and model definition ---'
rg -n -C 5 'dataRouter|userData|updatedAt|router\.use' backend -g '*.js' -g '*.prisma' -g '*.json'Repository: affaan-m/ECC
Length of output: 26815
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- sync implementation ---'
sed -n '80,180p' schedule-app/src/App.jsx
printf '%s\n' '--- Prisma version and update semantics references ---'
rg -n '"`@prisma/client`"|"prisma"' backend/package.json package.json 2>/dev/null || true
rg -n -C 3 'upsert\(|updateMany\(|findUnique\(' backend/src -g '*.js'
printf '%s\n' '--- deterministic sync contract probe ---'
python3 - <<'PY'
from pathlib import Path
api = Path("schedule-app/src/data/api.js").read_text()
app = Path("schedule-app/src/App.jsx").read_text()
print("push request includes baseUpdatedAt:", "baseUpdatedAt" in api[api.find("pushSyncedData"):api.find("// The assistant")])
print("pull stores server updatedAt:", "seenAtRef.current = updatedAt" in app)
print("push passes seenAtRef:", "seenAtRef.current" in app[app.find("pushSyncedData"):])
print("PUT performs conditional write:", "updateMany" in Path("backend/src/routes/data.js").read_text())
PYRepository: affaan-m/ECC
Length of output: 14335
Prevent silent loss of offline edits during sync.
DataSync imports newer server data before pushing, and pushSyncedData sends no base version. If offline edits must survive, add atomic optimistic concurrency to PUT /api/data and handle 409 without overwriting local state.
🤖 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 `@backend/src/routes/data.js` around lines 19 - 33, Update the router.put
handler and pushSyncedData/DataSync flow to include a base version with each
sync push, then perform the userData update atomically only when the stored
version matches that base. Return HTTP 409 on a version mismatch without
replacing server or local data, and make pushSyncedData handle 409 by preserving
the local offline edits for conflict resolution rather than overwriting them.
| {state.statuses.map((s) => ( | ||
| <li key={s.id}> | ||
| <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...s }))}> | ||
| <span className="swatch" style={{ background: s.color }} /> | ||
| <span>{s.label}</span> | ||
| <span className="muted count-tag"> | ||
| {state.contacts.filter((c) => c.statusId === s.id).length} | ||
| </span> | ||
| </button> | ||
| </li> | ||
| ))} | ||
| {state.statuses.length === 0 && <li className="muted small">No statuses yet.</li>} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Rename the map parameter so it does not shadow the s settings alias.
Line 360 binds s to state.settings. Line 817 rebinds s to a status object for the whole callback body. The current reads on Lines 819-823 all target status fields, so behavior is correct today. The hazard is the next edit: anyone adding a settings read inside this callback silently gets a status object instead.
♻️ Proposed refactor
- {state.statuses.map((s) => (
- <li key={s.id}>
- <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...s }))}>
- <span className="swatch" style={{ background: s.color }} />
- <span>{s.label}</span>
+ {state.statuses.map((status) => (
+ <li key={status.id}>
+ <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...status }))}>
+ <span className="swatch" style={{ background: status.color }} />
+ <span>{status.label}</span>
<span className="muted count-tag">
- {state.contacts.filter((c) => c.statusId === s.id).length}
+ {state.contacts.filter((c) => c.statusId === status.id).length}
</span>As per coding guidelines: "Use readable, well-named identifiers in all code".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {state.statuses.map((s) => ( | |
| <li key={s.id}> | |
| <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...s }))}> | |
| <span className="swatch" style={{ background: s.color }} /> | |
| <span>{s.label}</span> | |
| <span className="muted count-tag"> | |
| {state.contacts.filter((c) => c.statusId === s.id).length} | |
| </span> | |
| </button> | |
| </li> | |
| ))} | |
| {state.statuses.length === 0 && <li className="muted small">No statuses yet.</li>} | |
| {state.statuses.map((status) => ( | |
| <li key={status.id}> | |
| <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...status }))}> | |
| <span className="swatch" style={{ background: status.color }} /> | |
| <span>{status.label}</span> | |
| <span className="muted count-tag"> | |
| {state.contacts.filter((c) => c.statusId === status.id).length} | |
| </span> | |
| </button> | |
| </li> | |
| ))} | |
| {state.statuses.length === 0 && <li className="muted small">No statuses yet.</li>} |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 818-824: A list component should have a key to prevent re-rendering
Context: <button className="status-item" onClick={() => requirePro(() => setEditingStatus({ ...s }))}>
<span className="swatch" style={{ background: s.color }} />
{s.label}
{state.contacts.filter((c) => c.statusId === s.id).length}
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
[warning] 819-819: A list component should have a key to prevent re-rendering
Context: <span className="swatch" style={{ background: s.color }} />
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
[warning] 820-820: A list component should have a key to prevent re-rendering
Context: {s.label}
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
[warning] 821-823: A list component should have a key to prevent re-rendering
Context:
{state.contacts.filter((c) => c.statusId === s.id).length}
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
🤖 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 `@schedule-app/src/pages/MorePage.jsx` around lines 817 - 828, Rename the
status object parameter in the state.statuses.map callback from s to a
descriptive identifier such as status, and update its id, color, label, and
statusId-related references within the callback accordingly; leave the outer
settings alias unchanged.
Source: Coding guidelines
| <span className="muted count-tag"> | ||
| {state.contacts.filter((c) => c.statusId === s.id).length} |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
state.contacts.filter(...) runs once per status on every render.
Line 823 scans the full contacts array inside the status map. The cost is statuses × contacts per render, and this page re-renders on every settings change and on the 30-second sync tick. Build one count map with useMemo and read from it.
♻️ Proposed refactor
+ const statusCounts = useMemo(() => {
+ const counts = new Map();
+ for (const c of state.contacts) {
+ if (c.statusId) counts.set(c.statusId, (counts.get(c.statusId) || 0) + 1);
+ }
+ return counts;
+ }, [state.contacts]); <span className="muted count-tag">
- {state.contacts.filter((c) => c.statusId === s.id).length}
+ {statusCounts.get(s.id) || 0}
</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="muted count-tag"> | |
| {state.contacts.filter((c) => c.statusId === s.id).length} | |
| const statusCounts = useMemo(() => { | |
| const counts = new Map(); | |
| for (const c of state.contacts) { | |
| if (c.statusId) { | |
| counts.set(c.statusId, (counts.get(c.statusId) || 0) + 1); | |
| } | |
| } | |
| return counts; | |
| }, [state.contacts]); | |
| <span className="muted count-tag"> | |
| {statusCounts.get(s.id) || 0} | |
| </span> |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 821-823: A list component should have a key to prevent re-rendering
Context:
{state.contacts.filter((c) => c.statusId === s.id).length}
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
🤖 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 `@schedule-app/src/pages/MorePage.jsx` around lines 822 - 823, Replace the
per-status state.contacts.filter call in MorePage’s status rendering with a
useMemo-derived count map built from state.contacts, then read each status count
by s.id. Recompute the map only when the contacts collection changes and
preserve the existing count display behavior.
| const handleUpgrade = async () => { | ||
| if (!isSignedIn) return clerk.openSignIn(); | ||
| if (!backendConfigured()) return setError('Billing isn’t connected yet.'); | ||
| setError(''); | ||
| setBusy(true); | ||
| try { | ||
| const { url } = await startCheckout(getToken); | ||
| window.location.href = url; | ||
| } catch (err) { | ||
| setError(err.message); | ||
| setBusy(false); | ||
| } | ||
| }; | ||
|
|
||
| const handleManage = async () => { | ||
| setError(''); | ||
| setBusy(true); | ||
| try { | ||
| const { url } = await openBillingPortal(getToken); | ||
| window.location.href = url; | ||
| } catch (err) { | ||
| setError(err.message); | ||
| setBusy(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate the checkout response before navigating.
Both handlers destructure url from the API response and assign it to window.location.href without a check. If the backend returns a body without url, or a body whose url is null, the browser navigates to the string "undefined" or "null" relative to the current origin. The user lands on the app's catch-all redirect with no error, and busy stays true because the failure path never runs.
Validate the field, and keep the surfaced message generic rather than echoing the raw backend error.
🛡️ Proposed fix
try {
const { url } = await startCheckout(getToken);
+ if (typeof url !== 'string' || !url) {
+ throw new Error('Checkout is unavailable right now. Please try again.');
+ }
window.location.href = url;
} catch (err) {
setError(err.message);
setBusy(false);
}Apply the same guard in handleManage around the openBillingPortal response.
As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate".
🤖 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 `@schedule-app/src/pages/PricingPage.jsx` around lines 102 - 126, Validate that
the checkout and billing-portal responses contain a non-empty URL before
assigning window.location.href in handleUpgrade and handleManage. Treat missing
or null URLs as errors, route them through the existing catch handling, and
surface a generic user-facing message instead of raw backend error details;
ensure busy is cleared on this failure path.
Source: Coding guidelines
| try { | ||
| const { calendars: list } = await fetchCalendars(getToken); | ||
| setCalendars(list); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
An unexpected response body crashes the page.
Line 58 destructures calendars from the response and Line 59 stores it without a shape check. If the backend omits the key or returns null, calendars becomes undefined. The calendars === null guard on Line 110 is then false, and Line 112 reads calendars.length on undefined. The page throws during render, and the user has no way back.
Normalize the response to an array before storing it.
🛡️ Proposed fix
- const { calendars: list } = await fetchCalendars(getToken);
- setCalendars(list);
+ const { calendars: list } = (await fetchCalendars(getToken)) || {};
+ setCalendars(Array.isArray(list) ? list : []);As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const { calendars: list } = await fetchCalendars(getToken); | |
| setCalendars(list); | |
| try { | |
| const { calendars: list } = (await fetchCalendars(getToken)) || {}; | |
| setCalendars(Array.isArray(list) ? list : []); |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 58-58: Avoid using the initial state variable in setState
Context: setCalendars(list)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 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 `@schedule-app/src/pages/SharedCalendarsPage.jsx` around lines 57 - 59,
Validate and normalize the `calendars` value in the fetch flow before calling
`setCalendars` in `SharedCalendarsPage`: store the response array when valid,
otherwise store an empty array. Preserve the existing rendering behavior while
ensuring missing or null API fields cannot reach the `calendars.length` access.
Source: Coding guidelines
| server: { | ||
| host: true, | ||
| port: 5173, | ||
| }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Resolve the installed Vite version and check for dev-server advisories.
set -euo pipefail
fd -t f 'package-lock.json' schedule-app --max-depth 1 --exec jq -r '.packages["node_modules/vite"].version // "not in lockfile"'
gh api graphql -f query='
{
securityVulnerabilities(first: 20, ecosystem: NPM, package: "vite") {
nodes { advisory { summary severity publishedAt } vulnerableVersionRange firstPatchedVersion { identifier } }
}
}'Repository: affaan-m/ECC
Length of output: 4937
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- vite config ---'
cat -n schedule-app/vite.config.js
printf '%s\n' '--- package metadata ---'
cat -n schedule-app/package.json
printf '%s\n' '--- Vite-related configuration and host usage ---'
rg -n -C 3 'host:|server:|vite|5173|--host|--port' schedule-app \
-g '!node_modules' -g '!dist' -g '!build'Repository: affaan-m/ECC
Length of output: 20184
🌐 Web query:
Vite 5 server.host true listens on all addresses official documentation server.fs.strict dev server security
💡 Result:
In Vite, configuring server.host to true (or using --host) instructs the development server to listen on all available network addresses (0.0.0.0), which makes the server accessible via the local network and public IP addresses [1][2][3]. Regarding security and file system access: 1. server.fs.strict: By default, this option is set to true [4][5]. It restricts the Vite dev server from serving files outside of the project root or defined workspace, preventing potential unauthorized file access [4]. When enabled, attempts to access files outside of allowed directories that are not imported by allowed files result in a 403 Forbidden response [4][5]. 2. File Access Configuration: - server.fs.allow: This option allows you to explicitly define directories (or files) that the dev server is permitted to serve [4]. - server.fs.deny: This is a blocklist for sensitive files (e.g.,.env,.git, *.crt, *.pem) [4]. It has higher priority than server.fs.allow [4]. 3. Security Considerations: Exposing the dev server to the network (e.g., using --host or server.host: true) expands the attack surface. It is critical to rely on server.fs.strict to limit file exposure [4]. Users should be aware that security vulnerabilities, such as past issues where query parameters could bypass server.fs.deny restrictions, emphasize the importance of keeping Vite updated and not exposing development servers on untrusted networks [6].
Citations:
- 1: https://vite.dev/config/server-options
- 2: https://github.com/vitejs/vite/blob/main/docs/config/server-options.md
- 3: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/http.ts
- 4: https://github.com/vitejs/vite/blob/b089c2ba/docs/config/server-options.md
- 5: https://github.com/vitejs/vite/blob/130ef31b/docs/config/server-options.md
- 6: GHSA-v2wj-q39q-566r
Make LAN binding opt-in. host: true listens on all interfaces and exposes the development server to every reachable device. The lockfile pins Vite 5.4.21, which is patched for the cited Vite 5.x server.fs.deny bypasses, but it does not remove the exposure from default network binding. Gate host: true behind an environment variable and keep server.fs.strict enabled.
🤖 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 `@schedule-app/vite.config.js` around lines 9 - 12, Update the Vite server
configuration to make LAN binding opt-in: gate the host: true setting behind an
environment variable and leave server.fs.strict enabled. Preserve the existing
port configuration and use the project’s established environment-variable access
pattern if available.
| const numeric = remaining.match(/\b(?:on\s+)?(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\b/); | ||
| const ordinal = remaining.match(/\b(?:on\s+)?the\s+(\d{1,2})(?:st|nd|rd|th)\b/i); | ||
| if (monthDay) { | ||
| date = calendarDate(monthIndex(monthDay[1]), Number(monthDay[2]), monthDay[3], now); | ||
| remaining = remaining.replace(monthDay[0], ' '); | ||
| } else if (dayMonth) { | ||
| date = calendarDate(monthIndex(dayMonth[2]), Number(dayMonth[1]), dayMonth[3], now); | ||
| remaining = remaining.replace(dayMonth[0], ' '); | ||
| } else if (numeric) { | ||
| date = calendarDate(Number(numeric[1]) - 1, Number(numeric[2]), numeric[3], now); | ||
| remaining = remaining.replace(numeric[0], ' '); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
A numeric date with a first number above 12 produces a date years away.
Line 205 passes Number(numeric[1]) - 1 as the month index without a range check. calendarDate on line 403 only rejects monthIdx < 0, so 25/12 becomes new Date(year, 24, 12), which rolls forward to December two years later. The quick-add preview then shows a plausible but wrong date, and the event is saved with it.
Reject an out-of-range month, or read the value as day/month when the first number exceeds 12.
🐛 Proposed fix
} else if (numeric) {
- date = calendarDate(Number(numeric[1]) - 1, Number(numeric[2]), numeric[3], now);
- remaining = remaining.replace(numeric[0], ' ');
+ const a = Number(numeric[1]);
+ const b = Number(numeric[2]);
+ // A first number above 12 can only be a day, so read it as day/month.
+ date = a > 12
+ ? calendarDate(b - 1, a, numeric[3], now)
+ : calendarDate(a - 1, b, numeric[3], now);
+ if (date) remaining = remaining.replace(numeric[0], ' ');
} else if (ordinal) {Also harden calendarDate on line 403:
- if (monthIdx < 0 || day < 1 || day > 31) return null;
+ if (monthIdx < 0 || monthIdx > 11 || day < 1 || day > 31) return null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const numeric = remaining.match(/\b(?:on\s+)?(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\b/); | |
| const ordinal = remaining.match(/\b(?:on\s+)?the\s+(\d{1,2})(?:st|nd|rd|th)\b/i); | |
| if (monthDay) { | |
| date = calendarDate(monthIndex(monthDay[1]), Number(monthDay[2]), monthDay[3], now); | |
| remaining = remaining.replace(monthDay[0], ' '); | |
| } else if (dayMonth) { | |
| date = calendarDate(monthIndex(dayMonth[2]), Number(dayMonth[1]), dayMonth[3], now); | |
| remaining = remaining.replace(dayMonth[0], ' '); | |
| } else if (numeric) { | |
| date = calendarDate(Number(numeric[1]) - 1, Number(numeric[2]), numeric[3], now); | |
| remaining = remaining.replace(numeric[0], ' '); | |
| const numeric = remaining.match(/\b(?:on\s+)?(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\b/); | |
| const ordinal = remaining.match(/\b(?:on\s+)?the\s+(\d{1,2})(?:st|nd|rd|th)\b/i); | |
| if (monthDay) { | |
| date = calendarDate(monthIndex(monthDay[1]), Number(monthDay[2]), monthDay[3], now); | |
| remaining = remaining.replace(monthDay[0], ' '); | |
| } else if (dayMonth) { | |
| date = calendarDate(monthIndex(dayMonth[2]), Number(dayMonth[1]), dayMonth[3], now); | |
| remaining = remaining.replace(dayMonth[0], ' '); | |
| } else if (numeric) { | |
| const a = Number(numeric[1]); | |
| const b = Number(numeric[2]); | |
| // A first number above 12 can only be a day, so read it as day/month. | |
| date = a > 12 | |
| ? calendarDate(b - 1, a, numeric[3], now) | |
| : calendarDate(a - 1, b, numeric[3], now); | |
| if (date) remaining = remaining.replace(numeric[0], ' '); | |
| } else if (ordinal) { |
🤖 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 `@schedule-app/src/data/smartParse.js` around lines 196 - 206, Update the
numeric-date handling in the parser around the numeric match and calendarDate so
a first component above 12 is not passed as a month index; interpret that form
as day/month or reject it. Also harden calendarDate to reject month indexes
outside 0–11, while preserving valid numeric month/day parsing.
| <div className="stepper"> | ||
| <button | ||
| className="step-btn" | ||
| data-haptic="select" | ||
| onPointerDown={() => startHold(g.id, key, -1)} | ||
| onPointerUp={() => endHold(g.id, -1)} | ||
| onPointerLeave={() => clearHold(`${g.id}:-1`)} | ||
| onPointerCancel={() => clearHold(`${g.id}:-1`)} | ||
| onClick={() => { | ||
| const holdKey = `${g.id}:-1`; | ||
| if (suppressClickRef.current === holdKey) { | ||
| suppressClickRef.current = null; | ||
| return; | ||
| } | ||
| applyDelta(g.id, key, -1); | ||
| }} | ||
| disabled={value <= 0} | ||
| aria-label={`Decrease ${g.title}`} | ||
| > | ||
| − | ||
| </button> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
The auto-repeat interval keeps running when the decrement button becomes disabled mid-hold.
Line 445 disables the button when value <= 0. A hold that reaches repeat mode decrements every 150 ms until value hits 0. React then re-renders the button as disabled, and the browser stops dispatching pointer events to it. onPointerUp and onPointerLeave never fire, so endHold and clearHold never run. The interval on line 214 keeps calling applyDelta, which keeps dispatching setGoalProgress and firing selectTick until the page unmounts.
Stop the repeat inside applyDelta when the value cannot change, and attach the pointer-release handlers where a disabled child cannot swallow them.
🛡️ Proposed fix
const applyDelta = (goalId, periodKey, delta, { repeatTick = false } = {}) => {
const g = stateRef.current.goals.find((x) => x.id === goalId);
if (!g) return;
const current = g.progress?.[periodKey] || 0;
+ // A hold that runs the value into the floor disables its own button, so
+ // no pointerup ever reaches it to stop the interval. Stop it here.
+ if (delta < 0 && current === 0) {
+ clearHold(`${goalId}:${delta}`);
+ return;
+ }
const wasDone = current >= g.target;🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 429-448: A list component should have a key to prevent re-rendering
Context: <button
className="step-btn"
data-haptic="select"
onPointerDown={() => startHold(g.id, key, -1)}
onPointerUp={() => endHold(g.id, -1)}
onPointerLeave={() => clearHold(${g.id}:-1)}
onPointerCancel={() => clearHold(${g.id}:-1)}
onClick={() => {
const holdKey = ${g.id}:-1;
if (suppressClickRef.current === holdKey) {
suppressClickRef.current = null;
return;
}
applyDelta(g.id, key, -1);
}}
disabled={value <= 0}
aria-label={Decrease ${g.title}}
>
−
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
🤖 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 `@schedule-app/src/pages/GoalsPage.jsx` around lines 429 - 449, Update
applyDelta to stop and clear the active hold/repeat when a decrement cannot
change the value, specifically when the goal progress has reached its lower
bound. Move the pointer-release and cancellation handlers from the disabled
decrement button to a non-disabled stepper wrapper so endHold and clearHold
still run when the button becomes disabled; preserve normal click suppression
and repeat behavior.
| const saveEvent = async () => { | ||
| const { title, date, start, end, notes } = editingEvent; | ||
| if (!title.trim() || !date || !start || !end) return; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
saveEvent dereferences title without the guard used elsewhere.
Line 248 uses editingEvent?.title?.trim(), which treats a missing title as possible. Line 116 calls title.trim() directly on a value spread from the server response in openEditEvent. An event record without title throws TypeError on save.
Note: this line was also flagged for missing end-after-start validation; apply both changes together.
🛡️ Proposed fix
- if (!title.trim() || !date || !start || !end) return;
+ if (!(title || '').trim() || !date || !start || !end) return;🤖 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 `@schedule-app/src/pages/SharedCalendarDetailPage.jsx` around lines 114 - 116,
Update saveEvent to safely handle a missing title by using the same optional
title guard as the validation at line 248, and add validation that the event end
time is after its start time. Keep valid event saving unchanged while returning
early for missing required values or an invalid time range.
There was a problem hiding this comment.
Actionable comments posted: 24
♻️ Duplicate comments (2)
backend/README.md (2)
24-28: 🔒 Security & Privacy | 🟠 MajorRemove the documented fixed database password.
The commands expose
keystone_devas a known password. The Docker command also publishes port5432on all host interfaces by default.Require a user-provided password and bind Docker to
127.0.0.1:5432:5432.As per coding guidelines: “Never hardcode API keys, passwords, tokens, or other secrets; use environment variables or a secret manager.”
🤖 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 `@backend/README.md` around lines 24 - 28, Update the PostgreSQL setup commands in the README to require a user-provided password instead of hardcoding keystone_dev, using an environment-variable or interactive placeholder consistently for both native and Docker instructions. Change the Docker port mapping to bind only to 127.0.0.1:5432:5432 while preserving the existing database and user setup.Source: Coding guidelines
171-177: 🗄️ Data Integrity & Integration | 🟠 MajorCommit the shared-calendar migration before enabling these routes.
npm run db:deployapplies only committed migrations. The README states that/api/calendarsis wired, but no migration createsSharedCalendar,SharedCalendarMember,SharedCalendarInvite, orSharedEvent.A fresh database can fail when these endpoints query missing tables. Add and validate the migration before exposing the routes.
🤖 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 `@backend/README.md` around lines 171 - 177, Commit and validate a Prisma migration creating the SharedCalendar, SharedCalendarMember, SharedCalendarInvite, and SharedEvent tables before enabling the calendar routes in src/routes/calendars.js. Ensure the migration is included in the repository and works with npm run db:deploy on a fresh database, then update the README status to reflect that the routes are backed by the migration.
🤖 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 `@backend/src/routes/assistant.js`:
- Around line 240-247: Update assistantTurn to call anthropic() at the start and
throw a named configuration error when it returns null, before accessing the
client for the message request. Preserve the existing route behavior and
streaming flow for configured callers.
- Around line 253-256: Update the system message construction in the assistant
route so the digest text block is included only when digest is non-empty. Always
preserve the SYSTEM_STATIC block, while omitting the second block when optional
context produces an empty digest.
In `@backend/src/routes/calendars.js`:
- Around line 100-104: Update the calendar event query in the surrounding
handler to accept and validate a query-string date range, apply the resulting
date filter, and limit results with a fixed maximum using Prisma’s take option.
Preserve the existing ascending date ordering and reject invalid or unsafe range
values before calling sharedEvent.findMany.
In `@backend/src/routes/me.js`:
- Around line 13-23: Wrap the object returned by the `/api/me` handler in the
standard `{ success: true, data }` envelope while preserving all existing user
fields. Update the `/api/me` consumers in `schedule-app/src/App.jsx` to access
values through `me.data.*`, and revise the endpoint contract in
`backend/README.md` to document the new response shape.
In `@schedule-app/index.html`:
- Around line 87-96: Update the repair cleanup chain to call
navigator.serviceWorker.getRegistration() and unregister only that registration,
then restrict cache deletion to keys beginning with the keystone- prefix before
calling caches.delete. Preserve the existing done completion handling and avoid
removing unrelated origin caches.
In `@schedule-app/public/sw.js`:
- Around line 77-81: Update the fetch handler’s response caching so the
cache.put('./index.html', copy) call runs only when res.ok is true, preventing
HTTP error responses from replacing the app shell. Preserve the existing
event.waitUntil() handling and return the original response unchanged.
In `@schedule-app/src/components/Avatar.jsx`:
- Around line 33-35: Update the avatar change flow around fileToAvatarDataUrl
and onChange to handle localStorage quota exhaustion caused by storing avatar
data in the complete state, either by surfacing a clear user-facing error or by
enforcing a bounded avatar-storage strategy. Do not silently suppress failed
persistence; preserve successful avatar updates and notify the user when storage
cannot accommodate the data.
In `@schedule-app/src/components/TabBar.jsx`:
- Around line 9-16: Update the ICONS lookup in TabBar.jsx to include a valid
icon entry for every tab defined by TAB_TYPES, including any newly added tab
types. Preserve the existing lookup approach and ensure each TabBar icon
receives a defined value.
In `@schedule-app/src/data/contactInsights.js`:
- Around line 104-107: Update the place insight threshold in the logic using
topPlace to reuse the existing MIN_OCCURRENCES constant instead of the hardcoded
2, so “Most often at” is emitted only when the documented minimum of three
occurrences is met.
In `@schedule-app/src/data/geocode.js`:
- Around line 78-83: Update the picked-coordinate validation in the geocode flow
to use Number.isFinite for both contact.addressLat and contact.addressLng
instead of typeof checks. Preserve the existing fallback to
geocodeAddress(address) when either coordinate is invalid, including NaN.
In `@schedule-app/src/data/nlSearch.js`:
- Around line 58-64: Validate the relative offset in stripDateRange before
addDays: require n and any week-to-day multiplication to be finite safe integers
within the supported range, otherwise leave the phrase in keywords. In
schedule-app/src/data/nlSearch.js lines 58-64, apply this to the relative-date
branch; in schedule-app/src/data/helpers.js lines 457-464, bound the day walk
and stop when toISODate(addDays(iso, 1)) fails to advance.
In `@schedule-app/src/data/seed.js`:
- Around line 374-386: Move the complete default settings object currently
defined in loadState into a shared export in seed.js, and have makeSeed use that
object instead of the partial inline settings literal. Update loadState to
spread the shared defaults when merging persisted state, preserving any stored
setting overrides so RESET_DATA and IMPORT_DATA receive the same complete
settings schema.
In `@schedule-app/src/data/templates.js`:
- Around line 58-63: Update instantiate and templateSummary to validate the
external template.blocks value before mapping, defaulting missing or invalid
blocks to an empty array while preserving valid block data.
In `@schedule-app/src/data/useEdgeFade.js`:
- Around line 13-19: Update the setFade call in update to use the previous fade
state and return it unchanged when the computed left and right values already
match; otherwise return the new fade object. Preserve the existing scroll
threshold calculations and state behavior.
- Around line 20-28: Update the dependency handling in useEdgeFade so the effect
tracks the conditional container state derived from the task’s dueDate and
dueTime, not only the task identity. Ensure changes to either value rerun the
setup and cleanup, attaching listeners to the current element and removing them
from any detached element.
In `@schedule-app/src/data/vcard.js`:
- Around line 65-74: Update the property-key parsing in the vCard parser so
grouped keys such as item1.TEL and item1.EMAIL have their group prefix removed
before matching against FN, N, TEL, EMAIL, ADR, and NOTE. Preserve parameter
stripping and uppercase normalization, and leave the existing unescape and
field-assignment behavior unchanged. Do not expand scope to quoted-printable
decoding unless the implementation already defines that format as supported.
In `@schedule-app/src/pages/ContactsPage.jsx`:
- Around line 106-114: Update the makeOverdueCheck useMemo in ContactsPage to
pass the two reconnect settings and state.interactions as separate inputs, and
change its dependency list from the whole state object to those values. Ensure
the overdue memo uses the memoized check while avoiding invalidation from
unrelated root-state updates.
In `@schedule-app/src/pages/GoalsPage.jsx`:
- Around line 675-744: Extract the self-contained WeekPace component into
schedule-app/src/components/WeekPace.jsx, preserving its existing props,
rendering, handlers, and pace behavior. Export it from the new file, add the
required imports there, and update GoalsPage.jsx to import and use the extracted
component while removing its local definition.
- Around line 162-163: Remove the render-time assignment to stateRef.current
near the stateRef declaration. Either synchronize stateRef.current = state in an
effect, or update applyDelta to read the current goal directly from the store,
preserving the existing applyDelta behavior without mutating refs during render.
In `@schedule-app/src/pages/MapPage.jsx`:
- Around line 1-29: Reduce MapPage below the 800-line limit by extracting
cohesive logic into separate modules: move the pin editor sheet,
location-picking flow, inline SVG icon components, and Leaflet setup effect from
MapPage into focused components/hooks or utility modules. Update MapPage imports
and usage while preserving existing behavior and interfaces.
- Around line 53-63: Update toggleFilter so it computes the toggled filter value
before calling setFilters, keeps the updater pure by only applying that value,
and performs the relevant actions.setSettings dispatch outside the updater for
contacts and custom filters.
In `@schedule-app/src/pages/MorePage.jsx`:
- Around line 291-294: Update the rejection handler on
navigator.clipboard.writeText in the feedback-copy flow to explicitly report
failures with a controlled user-facing error message, directing the user to use
“Email it” or retry; do not leave the handler empty or silently swallow the
error.
In `@schedule-app/src/pages/SharedCalendarDetailPage.jsx`:
- Line 72: Update the error rendering in SharedCalendarDetailPage so the raw
fetchCalendar error message is never displayed directly. Map recognized server
error codes to approved user-facing messages, and use a generic sanitized
fallback for unknown or missing errors while preserving the existing error-state
UI.
In `@schedule-app/src/pages/SharedCalendarJoinPage.jsx`:
- Around line 29-37: Encode the route token before passing it to
acceptCalendarInvite in the SharedCalendarJoinPage useEffect, ensuring decoded
values such as “/” or “..” remain a single URL path segment while preserving the
existing invite acceptance flow.
---
Duplicate comments:
In `@backend/README.md`:
- Around line 24-28: Update the PostgreSQL setup commands in the README to
require a user-provided password instead of hardcoding keystone_dev, using an
environment-variable or interactive placeholder consistently for both native and
Docker instructions. Change the Docker port mapping to bind only to
127.0.0.1:5432:5432 while preserving the existing database and user setup.
- Around line 171-177: Commit and validate a Prisma migration creating the
SharedCalendar, SharedCalendarMember, SharedCalendarInvite, and SharedEvent
tables before enabling the calendar routes in src/routes/calendars.js. Ensure
the migration is included in the repository and works with npm run db:deploy on
a fresh database, then update the README status to reflect that the routes are
backed by the migration.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 12ef22b6-7ccb-4211-b543-81246c9c2960
⛔ Files ignored due to path filters (9)
backend/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/appstore-assets/icon-1024.pngis excluded by!**/*.pngschedule-app/package-lock.jsonis excluded by!**/package-lock.jsonschedule-app/public/apple-touch-icon.pngis excluded by!**/*.pngschedule-app/public/icon-192.pngis excluded by!**/*.pngschedule-app/public/icon-512-maskable.pngis excluded by!**/*.pngschedule-app/public/icon-512.pngis excluded by!**/*.pngschedule-app/public/icon-maskable.svgis excluded by!**/*.svgschedule-app/public/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (111)
.github/workflows/deploy-schedule-app.yml.gitignorebackend/.env.examplebackend/.gitignorebackend/README.mdbackend/package.jsonbackend/prisma.config.mjsbackend/prisma/migrations/20260722012026_init/migration.sqlbackend/prisma/migrations/20260722062956_add_user_data/migration.sqlbackend/prisma/migrations/20260727010000_lifetime_purchase/migration.sqlbackend/prisma/migrations/migration_lock.tomlbackend/prisma/schema.prismabackend/src/app.jsbackend/src/db.jsbackend/src/middleware/requireUser.jsbackend/src/routes/assistant.jsbackend/src/routes/billing.jsbackend/src/routes/calendars.jsbackend/src/routes/data.jsbackend/src/routes/me.jsbackend/src/routes/webhooksClerk.jsbackend/src/routes/webhooksStripe.jsbackend/src/server.jsschedule-app/.env.exampleschedule-app/README.mdschedule-app/index.htmlschedule-app/package.jsonschedule-app/public/manifest.webmanifestschedule-app/public/sw.jsschedule-app/src/App.jsxschedule-app/src/components/AddressField.jsxschedule-app/src/components/AnimatedNumber.jsxschedule-app/src/components/AssistantBubble.jsxschedule-app/src/components/Avatar.jsxschedule-app/src/components/Checkbox.jsxschedule-app/src/components/DesertEmptyState.jsxschedule-app/src/components/EditorSheet.jsxschedule-app/src/components/ExpandableFab.jsxschedule-app/src/components/GroupPicker.jsxschedule-app/src/components/Icon.jsxschedule-app/src/components/Logo.jsxschedule-app/src/components/MilestoneCelebration.jsxschedule-app/src/components/MiniMapPicker.jsxschedule-app/src/components/Modal.jsxschedule-app/src/components/ReorderToggleList.jsxschedule-app/src/components/Select.jsxschedule-app/src/components/SettingsGroup.jsxschedule-app/src/components/SettingsSection.jsxschedule-app/src/components/SmartQuickAdd.jsxschedule-app/src/components/SwipeRow.jsxschedule-app/src/components/SwipeToDelete.jsxschedule-app/src/components/TabBar.jsxschedule-app/src/components/Tutorial.jsxschedule-app/src/data/aiConfig.jsschedule-app/src/data/api.jsschedule-app/src/data/assistantContext.jsschedule-app/src/data/assistantTools.jsschedule-app/src/data/clerkConfig.jsschedule-app/src/data/conflicts.jsschedule-app/src/data/contactDates.jsschedule-app/src/data/contactInsights.jsschedule-app/src/data/contactSwipe.jsschedule-app/src/data/geocode.jsschedule-app/src/data/goalHistory.jsschedule-app/src/data/haptics.jsschedule-app/src/data/helpers.jsschedule-app/src/data/homeBlocks.jsschedule-app/src/data/ics.jsschedule-app/src/data/image.jsschedule-app/src/data/mapStyles.jsschedule-app/src/data/maps.jsschedule-app/src/data/nlSearch.jsschedule-app/src/data/notifications.jsschedule-app/src/data/nudges.jsschedule-app/src/data/pinLabel.jsschedule-app/src/data/quickAdd.jsschedule-app/src/data/reconnect.jsschedule-app/src/data/routePlanner.jsschedule-app/src/data/routePlannerConfig.jsschedule-app/src/data/seed.jsschedule-app/src/data/smartParse.jsschedule-app/src/data/store.jsxschedule-app/src/data/syncStatus.jsschedule-app/src/data/tabs.jsschedule-app/src/data/templates.jsschedule-app/src/data/toast.jsxschedule-app/src/data/useBackDismiss.jsschedule-app/src/data/useCountUp.jsschedule-app/src/data/useDeleteContact.jsschedule-app/src/data/useEdgeFade.jsschedule-app/src/data/useSmartAdd.jsschedule-app/src/data/vcard.jsschedule-app/src/data/weeklyRecap.jsschedule-app/src/main.jsxschedule-app/src/pages/ContactDetailPage.jsxschedule-app/src/pages/ContactTimelinePage.jsxschedule-app/src/pages/ContactsPage.jsxschedule-app/src/pages/GoalHistoryPage.jsxschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/HomePage.jsxschedule-app/src/pages/MapPage.jsxschedule-app/src/pages/MorePage.jsxschedule-app/src/pages/PlannerPage.jsxschedule-app/src/pages/PricingPage.jsxschedule-app/src/pages/RoutePlannerPage.jsxschedule-app/src/pages/SearchPage.jsxschedule-app/src/pages/SharedCalendarDetailPage.jsxschedule-app/src/pages/SharedCalendarJoinPage.jsxschedule-app/src/pages/SharedCalendarsPage.jsxschedule-app/src/styles.cssschedule-app/vite.config.js
| export async function assistantTurn({ messages, context }) { | ||
| const digest = typeof context === 'string' ? context.slice(0, MAX_CONTEXT_CHARS) : ''; | ||
|
|
||
| // Streamed rather than a plain create(): with adaptive thinking a hard | ||
| // question can spend a while before the first token, which is exactly the | ||
| // shape of request that trips a non-streaming timeout. The client still | ||
| // gets one JSON reply — finalMessage() reassembles it here. | ||
| const message = await anthropic() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
Guard anthropic() inside assistantTurn.
assistantTurn is exported for direct use without the route. Line 247 dereferences anthropic(), which returns null when ANTHROPIC_API_KEY is unset. The call then throws TypeError: Cannot read properties of null. The route checks first, but a direct caller does not.
Throw a named configuration error at the top of the function instead.
🤖 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 `@backend/src/routes/assistant.js` around lines 240 - 247, Update assistantTurn
to call anthropic() at the start and throw a named configuration error when it
returns null, before accessing the client for the message request. Preserve the
existing route behavior and streaming flow for configured callers.
| system: [ | ||
| { type: 'text', text: SYSTEM_STATIC, cache_control: { type: 'ephemeral' } }, | ||
| { type: 'text', text: digest }, | ||
| ], |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Anthropic Messages API system text block empty string invalid_request_error non-empty text content blocks
💡 Result:
The Anthropic Messages API enforces a strict validation rule requiring all text content blocks to contain non-empty text [1][2]. When a request contains a text content block that is empty (e.g., {"type": "text", "text": ""}), whitespace-only, or missing content, the API will return an invalid_request_error with a message typically stating "messages: text content blocks must be non-empty" [1][2][3]. This error commonly occurs in the following scenarios: 1. Tool-based workflows or multi-agent systems where intermediate processing or streaming logic generates empty text segments [1][4][5]. 2. Storing and re-sending conversation history where empty blocks were previously recorded (e.g., streaming gaps or placeholder messages) [3][5]. 3. Client-side SDKs or wrappers that fail to sanitize empty inputs before forwarding them to the API [1][6][4]. To resolve this, developers should implement client-side sanitization to filter out or strip empty text content blocks from the content list before submitting the request to the Messages API [6][4][3]. Replacing empty content with a single space (" ") is a common workaround adopted by various SDKs to satisfy the validation requirement [6]. Regarding the system parameter: The API accepts either a string or an array of TextBlockParam [7][8]. While not explicitly labeled as an "empty string error," the system field can also trigger generic invalid_request_error responses if it fails internal validation, such as specific undocumented requirements for certain authentication tokens (e.g., OAuth tokens requiring a specific identity string for non-Haiku models) [9]. As a best practice, always ensure that both the system field and message content contain meaningful, non-empty data before invocation [2][10].
Citations:
- 1: ChatAnthropic fails to handle empty HumanMessage content, leading to BadRequestError (400) from Anthropic API langchain-ai/langchain#35081
- 2: https://portkey.ai/error-library/input-validation-error-10487
- 3: Sanitize empty text content blocks on /v1/messages BerriAI/litellm#27832
- 4: [Bug]: Empty Text Content Blocks Cause Errors with Anthropic API in Tool Calling vercel/ai#5576
- 5: Empty text content blocks in stored messages cause Anthropic API rejection mastra-ai/mastra#12553
- 6: fix(anthropic): handle empty message content gracefully langchain-ai/langchain#35504
- 7: https://platform.claude.com/docs/en/api/cli/messages/create
- 8: https://github.com/anthropics/anthropic-sdk-python/blob/49d639a6/src/anthropic/types/message_create_params.py
- 9: OAuth tokens require undocumented system prompt validation for non-Haiku models — returns generic 400 anthropics/claude-code#40515
- 10: https://theneuralbase.com/anthropic-api/learn/beginner/invalidrequesterror-malformed-messages/
🏁 Script executed:
#!/bin/bash
set -eu
file="backend/src/routes/assistant.js"
printf '%s\n' '--- target lines ---'
sed -n '220,340p' "$file"
printf '%s\n' '--- relevant symbols and call sites ---'
rg -n -C 3 "digest|context|system:|messages\\.create|That conversation confused" "$file"Repository: affaan-m/ECC
Length of output: 7836
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- assistantTurn callers and tests ---'
rg -n -C 4 "assistantTurn|context:|context\\b" backend test tests frontend 2>/dev/null || true
printf '%s\n' '--- Anthropic dependency declarations ---'
rg -n -C 2 '"`@anthropic-ai/sdk`"|"anthropic"' package.json package-lock.json yarn.lock pnpm-lock.yaml backend/package.json 2>/dev/null || true
printf '%s\n' '--- deterministic request-shape probe ---'
python3 - <<'PY'
MAX_CONTEXT_CHARS = 10
def request_system(context):
digest = context[:MAX_CONTEXT_CHARS] if isinstance(context, str) else ''
return [
{'type': 'text', 'text': 'static'},
{'type': 'text', 'text': digest},
]
for label, context in [('absent', None), ('empty string', ''), ('non-empty', 'digest')]:
blocks = request_system(context)
print(label, [(b['type'], repr(b['text'])) for b in blocks])
PYRepository: affaan-m/ECC
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- documented request contract ---'
sed -n '138,151p' backend/README.md
printf '%s\n' '--- client construction of the assistant request ---'
rg -n -C 5 --glob '!tests/**' --glob '!node_modules/**' \
"api/assistant|/assistant|context:" backend frontend 2>/dev/null | head -n 160
printf '%s\n' '--- dependency declaration ---'
for f in package.json backend/package.json package-lock.json yarn.lock pnpm-lock.yaml; do
if [ -f "$f" ]; then
printf '%s\n' "--- $f ---"
rg -n -C 2 '"`@anthropic-ai/sdk`"|"anthropic"' "$f" | head -n 40 || true
fi
doneRepository: affaan-m/ECC
Length of output: 5743
Omit the second system block when digest is empty.
context is optional, so first turns can set digest to ''. Anthropic rejects the resulting empty text block with a 400 invalid_request_error.
🤖 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 `@backend/src/routes/assistant.js` around lines 253 - 256, Update the system
message construction in the assistant route so the digest text block is included
only when digest is non-empty. Always preserve the SYSTEM_STATIC block, while
omitting the second block when optional context produces an empty digest.
| prisma.sharedEvent.findMany({ | ||
| where: { calendarId: req.calendar.id }, | ||
| orderBy: { date: 'asc' }, | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Bound the event query.
findMany returns every row for the calendar with no take or date filter. A calendar accumulates events without limit, and this handler runs on every calendar open. Response size and query cost grow without bound.
Accept a date range from the query string, validate it, and apply take with a fixed maximum.
🤖 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 `@backend/src/routes/calendars.js` around lines 100 - 104, Update the calendar
event query in the surrounding handler to accept and validate a query-string
date range, apply the resulting date filter, and limit results with a fixed
maximum using Prisma’s take option. Preserve the existing ascending date
ordering and reject invalid or unsafe range values before calling
sharedEvent.findMany.
| res.json({ | ||
| id: u.id, | ||
| email: u.email, | ||
| isPro: isLifetime || PRO_STATUSES.has(u.subscriptionStatus), | ||
| isLifetime, | ||
| lifetimePurchasedAt: u.lifetimePurchasedAt, | ||
| // Only set for pre-switch subscribers — the frontend uses this to decide | ||
| // whether to offer the "manage billing" escape hatch at all. | ||
| subscriptionStatus: u.subscriptionStatus, | ||
| currentPeriodEnd: u.currentPeriodEnd, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- route files and relevant symbols ---'
fd -t f . | rg '(^|/)(backend/src/routes/me\.js|backend/src|frontend|src)/|package\.json$' | head -200
printf '%s\n' '--- backend/src/routes/me.js ---'
cat -n backend/src/routes/me.js
printf '%s\n' '--- API response conventions and /me consumers ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' \
'ApiResponse|success:\s*true|res\.json\(|/me\b|subscriptionStatus|lifetimePurchasedAt|currentPeriodEnd|isLifetime|isPro' . | head -300Repository: affaan-m/ECC
Length of output: 33135
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- frontend API helper and /me state update ---'
cat -n schedule-app/src/data/api.js
sed -n '1,75p' schedule-app/src/App.jsx
printf '%s\n' '--- all direct fetchMe and /api/me consumers ---'
rg -n -C 4 --glob '!node_modules' --glob '!dist' --glob '!build' \
'fetchMe|request\([^)]*api/me|/api/me' schedule-app backend tests
printf '%s\n' '--- backend README API contract ---'
sed -n '90,112p' backend/README.md
printf '%s\n' '--- package test/config context ---'
cat backend/package.json
cat schedule-app/package.jsonRepository: affaan-m/ECC
Length of output: 16209
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
route = Path("backend/src/routes/me.js").read_text()
app = Path("schedule-app/src/App.jsx").read_text()
readme = Path("backend/README.md").read_text()
# The route currently serializes profile fields directly under res.json().
assert re.search(r"res\.json\(\{\s*id:\s*u\.id", route), "The route shape is not the reported top-level shape"
# The only discovered frontend /me consumer reads the profile fields directly.
for field in ("isPro", "isLifetime", "subscriptionStatus"):
assert f"me.{field}" in app, f"App.jsx does not consume me.{field}"
# The documented contract also describes the unwrapped shape.
assert "{ id, email, isPro, subscriptionStatus, currentPeriodEnd }" in readme
print("route currently returns an unwrapped profile")
print("App.jsx currently consumes me.isPro, me.isLifetime, and me.subscriptionStatus")
print("backend/README.md documents the same unwrapped contract")
print("wrapping data requires the App.jsx consumer and README contract to change together")
PYRepository: affaan-m/ECC
Length of output: 412
Wrap the /api/me response in the standard envelope.
Return { success: true, data }. Update schedule-app/src/App.jsx to read me.data.*, and update the /api/me contract in backend/README.md.
🤖 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 `@backend/src/routes/me.js` around lines 13 - 23, Wrap the object returned by
the `/api/me` handler in the standard `{ success: true, data }` envelope while
preserving all existing user fields. Update the `/api/me` consumers in
`schedule-app/src/App.jsx` to access values through `me.data.*`, and revise the
endpoint contract in `backend/README.md` to document the new response shape.
Source: Coding guidelines
| navigator.serviceWorker.getRegistrations() | ||
| .then(function (regs) { | ||
| return Promise.all(regs.map(function (r) { return r.unregister(); })); | ||
| }) | ||
| .then(function () { | ||
| return window.caches ? caches.keys().then(function (keys) { | ||
| return Promise.all(keys.map(function (k) { return caches.delete(k); })); | ||
| }) : null; | ||
| }) | ||
| .then(done, done); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Limit repair cleanup to Keystone resources.
caches.keys() returns cache keys for the complete origin. A repair on a GitHub Pages project site can delete offline caches for other projects on the same <owner>.github.io origin.
Use navigator.serviceWorker.getRegistration() instead of getRegistrations(). Delete only cache keys with a Keystone prefix, such as keystone-.
🤖 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 `@schedule-app/index.html` around lines 87 - 96, Update the repair cleanup
chain to call navigator.serviceWorker.getRegistration() and unregister only that
registration, then restrict cache deletion to keys beginning with the keystone-
prefix before calling caches.delete. Preserve the existing done completion
handling and avoid removing unrelated origin caches.
| import { useEffect, useMemo, useRef, useState } from 'react'; | ||
| import { Link, useLocation, useNavigate } from 'react-router-dom'; | ||
| import L from 'leaflet'; | ||
| import 'leaflet/dist/leaflet.css'; | ||
| import { useStore, useActions } from '../data/store.jsx'; | ||
| import { useToast } from '../data/toast.jsx'; | ||
| import EditorSheet from '../components/EditorSheet.jsx'; | ||
| import Select from '../components/Select.jsx'; | ||
| import Modal from '../components/Modal.jsx'; | ||
| import { todayISO, expandEventOnDay, formatTime } from '../data/helpers.js'; | ||
| import { confirmTick, selectTick } from '../data/haptics.js'; | ||
| import { geocodeAddress } from '../data/geocode.js'; | ||
| import { directionsTarget, openMaps } from '../data/maps.js'; | ||
| import { resolveMapStyle, MAP_STYLE_OPTIONS } from '../data/mapStyles.js'; | ||
| import { eventPinIdentity } from '../data/pinLabel.js'; | ||
| import { ROUTE_PLANNER_ENABLED } from '../data/routePlannerConfig.js'; | ||
| import Icon from '../components/Icon.jsx'; | ||
| import AddressField from '../components/AddressField.jsx'; | ||
|
|
||
| const LONG_PRESS_MS = 500; | ||
| const LONG_PRESS_TOLERANCE_PX = 18; // generous — real fingers drift more than a mouse | ||
|
|
||
| const QUICK_EMOJI = ['📍', '🏠', '💼', '☕', '🍽️', '🏋️', '🛒', '🏥', '🎓', '⛪', '🌳', '❤️', '⭐', '🎉']; | ||
| const DEFAULT_VIEW = [37.7749, -122.4194]; | ||
|
|
||
| const escapeHtml = (s = '') => | ||
| s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | ||
|
|
||
| export default function MapPage() { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
The file is 954 lines and exceeds the 800-line maximum.
Extract cohesive units into separate modules. Candidates: the pin editor sheet (Lines 818-901), the location-picking flow (Lines 122-133, 419-437, 479-494, 608-651), the inline SVG icon components (Lines 906-953), and the Leaflet setup effect (Lines 262-350).
As per coding guidelines: "Keep files under 800 lines".
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 26-26: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(manual-sanitization)
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 1 - 29, Reduce MapPage below
the 800-line limit by extracting cohesive logic into separate modules: move the
pin editor sheet, location-picking flow, inline SVG icon components, and Leaflet
setup effect from MapPage into focused components/hooks or utility modules.
Update MapPage imports and usage while preserving existing behavior and
interfaces.
Source: Coding guidelines
| const toggleFilter = (k) => { | ||
| selectTick(); | ||
| setFilters((f) => { | ||
| const next = { ...f, [k]: !f[k] }; | ||
| // Mirror the two that have a persisted settings equivalent, so the | ||
| // map and Settings never disagree about what's showing. | ||
| if (k === 'contacts') actions.setSettings({ mapShowContactPins: next.contacts }); | ||
| if (k === 'custom') actions.setSettings({ mapShowCustomPins: next.custom }); | ||
| return next; | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
toggleFilter dispatches a settings write inside a state updater.
Lines 59-60 call actions.setSettings inside the setFilters updater. A state updater must be pure. React invokes updaters twice under StrictMode and may re-invoke them during concurrent rendering, so the settings dispatch runs more than once per tap.
Compute the next value outside the updater and dispatch there.
🐛 Proposed fix
const toggleFilter = (k) => {
selectTick();
- setFilters((f) => {
- const next = { ...f, [k]: !f[k] };
- // Mirror the two that have a persisted settings equivalent, so the
- // map and Settings never disagree about what's showing.
- if (k === 'contacts') actions.setSettings({ mapShowContactPins: next.contacts });
- if (k === 'custom') actions.setSettings({ mapShowCustomPins: next.custom });
- return next;
- });
+ const next = { ...filters, [k]: !filters[k] };
+ setFilters(next);
+ // Mirror the two that have a persisted settings equivalent, so the
+ // map and Settings never disagree about what's showing.
+ if (k === 'contacts') actions.setSettings({ mapShowContactPins: next.contacts });
+ if (k === 'custom') actions.setSettings({ mapShowCustomPins: next.custom });
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const toggleFilter = (k) => { | |
| selectTick(); | |
| setFilters((f) => { | |
| const next = { ...f, [k]: !f[k] }; | |
| // Mirror the two that have a persisted settings equivalent, so the | |
| // map and Settings never disagree about what's showing. | |
| if (k === 'contacts') actions.setSettings({ mapShowContactPins: next.contacts }); | |
| if (k === 'custom') actions.setSettings({ mapShowCustomPins: next.custom }); | |
| return next; | |
| }); | |
| }; | |
| const toggleFilter = (k) => { | |
| selectTick(); | |
| const next = { ...filters, [k]: !filters[k] }; | |
| setFilters(next); | |
| // Mirror the two that have a persisted settings equivalent, so the | |
| // map and Settings never disagree about what's showing. | |
| if (k === 'contacts') actions.setSettings({ mapShowContactPins: next.contacts }); | |
| if (k === 'custom') actions.setSettings({ mapShowCustomPins: next.custom }); | |
| }; |
🤖 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 `@schedule-app/src/pages/MapPage.jsx` around lines 53 - 63, Update toggleFilter
so it computes the toggled filter value before calling setFilters, keeps the
updater pure by only applying that value, and performs the relevant
actions.setSettings dispatch outside the updater for contacts and custom
filters.
| navigator.clipboard?.writeText(text).then( | ||
| () => alert('Feedback copied to your clipboard.'), | ||
| () => {} | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Report clipboard write failures.
The rejection handler silently ignores a failed clipboard write. Show a controlled error message so the user can use “Email it” or retry.
As per coding guidelines: “Always handle errors explicitly at every level and never silently swallow errors.”
Proposed fix
navigator.clipboard?.writeText(text).then(
() => alert('Feedback copied to your clipboard.'),
- () => {}
+ () => alert('Could not copy feedback. Use “Email it” instead.')
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| navigator.clipboard?.writeText(text).then( | |
| () => alert('Feedback copied to your clipboard.'), | |
| () => {} | |
| ); | |
| navigator.clipboard?.writeText(text).then( | |
| () => alert('Feedback copied to your clipboard.'), | |
| () => alert('Could not copy feedback. Use “Email it” instead.') | |
| ); |
🤖 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 `@schedule-app/src/pages/MorePage.jsx` around lines 291 - 294, Update the
rejection handler on navigator.clipboard.writeText in the feedback-copy flow to
explicitly report failures with a controlled user-facing error message,
directing the user to use “Email it” or retry; do not leave the handler empty or
silently swallow the error.
Source: Coding guidelines
| ‹ Shared calendars | ||
| </button> | ||
| </header> | ||
| <p className="muted center-pad">{error}</p> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Raw server error text reaches the UI.
Line 72 renders err.message from fetchCalendar directly. The value originates from the backend response, so an unhandled server error can expose internal details to the user. Map known error codes to user-facing text, and use a generic fallback for everything else.
As per coding guidelines: "Error messages must be scrubbed of sensitive internals".
🤖 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 `@schedule-app/src/pages/SharedCalendarDetailPage.jsx` at line 72, Update the
error rendering in SharedCalendarDetailPage so the raw fetchCalendar error
message is never displayed directly. Map recognized server error codes to
approved user-facing messages, and use a generic sanitized fallback for unknown
or missing errors while preserving the existing error-state UI.
Source: Coding guidelines
| const { token } = useParams(); | ||
| const navigate = useNavigate(); | ||
| const { isSignedIn, getToken } = useAuth(); | ||
| const clerk = useClerk(); | ||
| const [error, setError] = useState(''); | ||
|
|
||
| useEffect(() => { | ||
| if (!isSignedIn) return; | ||
| acceptCalendarInvite(getToken, token) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Encode token before it becomes a URL path segment.
useParams returns the decoded route value. acceptCalendarInvite in schedule-app/src/data/api.js line 69 interpolates it directly into /api/calendars/invites/${token}/accept. A token that decodes to / or .. changes the request path. Encode the value at this call site, or in acceptCalendarInvite.
As per coding guidelines: "Never trust external data (API responses, file content, query params); always validate".
🛡️ Proposed fix
- acceptCalendarInvite(getToken, token)
+ acceptCalendarInvite(getToken, encodeURIComponent(token))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { token } = useParams(); | |
| const navigate = useNavigate(); | |
| const { isSignedIn, getToken } = useAuth(); | |
| const clerk = useClerk(); | |
| const [error, setError] = useState(''); | |
| useEffect(() => { | |
| if (!isSignedIn) return; | |
| acceptCalendarInvite(getToken, token) | |
| const { token } = useParams(); | |
| const navigate = useNavigate(); | |
| const { isSignedIn, getToken } = useAuth(); | |
| const clerk = useClerk(); | |
| const [error, setError] = useState(''); | |
| useEffect(() => { | |
| if (!isSignedIn) return; | |
| acceptCalendarInvite(getToken, encodeURIComponent(token)) |
🤖 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 `@schedule-app/src/pages/SharedCalendarJoinPage.jsx` around lines 29 - 37,
Encode the route token before passing it to acceptCalendarInvite in the
SharedCalendarJoinPage useEffect, ensuring decoded values such as “/” or “..”
remain a single URL path segment while preserving the existing invite acceptance
flow.
Source: Coding guidelines
Drop the desert scene entirely (DesertEmptyState.jsx and its CSS) along with the .page-fill/.home-empty-state wrapper it needed for layout. When every home tile is hidden this now just shows the cycling message as plain centered text, same as before the animation was added.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
…d hook Planner's cursor and Goals' day/weekStart each independently hit and fixed the same bug: state seeded once at mount via useState(() => todayISO()) never advances on its own, so a session backgrounded across a real midnight (routine for a PWA, which gets suspended/resumed rather than reloaded) silently drifts a day stale. Both fixes ended up as near- identical copies of the same visibilitychange/focus/interval listener plus a manualNavRef guard to avoid clobbering deliberate navigation. Pulled that boilerplate into useTodayResync (schedule-app/src/data/) so a third page hitting this doesn't have to rediscover and re-verify the same fix from scratch — it now owns the listener/interval/guard, while each page still supplies its own resync callback and keeps its existing manualNavRef.current = true/false call sites unchanged. Verified with Playwright using page.clock to cross a simulated midnight without reloading: both pages' displayed day still advances on resync, and a deliberate step forward still survives a resync tick immediately after (the manual-nav guard). node tests/run-all.js: 3123/3124 (the one failure is the pre-existing, unrelated hooks.test.js flake).
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
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 `@schedule-app/src/data/useTodayResync.js`:
- Line 26: Define a module-scoped constant named DEFAULT_RESYNC_INTERVAL_MS with
the existing 60000-millisecond value, then update useTodayResync’s intervalMs
default parameter to reference that constant.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1e40d050-cf81-4cea-99f8-f6e2aa0df45a
📒 Files selected for processing (3)
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsxschedule-app/src/pages/PlannerPage.jsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (16)
**/*.{js,ts,jsx,tsx,py,java,cs,go,rb,php,scala,kt}
📄 CodeRabbit inference engine (.cursor/rules/common-coding-style.md)
**/*.{js,ts,jsx,tsx,py,java,cs,go,rb,php,scala,kt}: Always create new objects, never mutate existing ones. Use immutable patterns to prevent hidden side effects and enable safe concurrency
Organize code into many small files (200-400 lines typical, 800 lines max) organized by feature/domain rather than by type
Always handle errors explicitly at every level and never silently swallow errors
Always validate all user input before processing at system boundaries
Use schema-based validation where available
Fail fast with clear error messages when validation fails
Never trust external data (API responses, user input, file content)
Ensure code is readable and well-named
Keep functions small (less than 50 lines)
Keep files focused (less than 800 lines)
Avoid deep nesting (more than 4 levels)
Do not use hardcoded values; use constants or configuration instead
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/{server,backend,api,src}/**/*.{ts,tsx,js,py,java,cs,go,rb,php,scala,kt}
📄 CodeRabbit inference engine (.cursor/rules/common-coding-style.md)
Log detailed error context on the server side
Files:
schedule-app/src/data/useTodayResync.js
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,swift,kt,rs,c,cpp,h,hpp}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
No hardcoded secrets (API keys, passwords, tokens) - validate before any commit
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php}: All user inputs must be validated
Enable CSRF protection on all state-changing endpoints
Verify authentication and authorization for all protected endpoints
Implement rate limiting on all endpoints to prevent abuse
Ensure error messages do not leak sensitive data in responses
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,sql}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
Use parameterized queries to prevent SQL injection
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx,html,php,java,cs,rb,go}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
Implement XSS prevention by sanitizing HTML output
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,swift,kt,rs,c,cpp,h,hpp,properties,yml,yaml,json,env,config}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
NEVER hardcode secrets in source code - ALWAYS use environment variables or a secret manager
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/typescript-coding-style.md)
**/*.{ts,tsx,js,jsx}: Use spread operator for immutable updates in TypeScript/JavaScript instead of direct mutation
Use async/await with try-catch for error handling in TypeScript/JavaScript
Use Zod for schema-based input validation in TypeScript/JavaScript
No console.log statements in production code; use proper logging libraries instead
**/*.{ts,tsx,js,jsx}: Auto-format JavaScript/TypeScript files using Prettier after edit
Warn aboutconsole.logstatements in edited files
Check all modified files forconsole.logstatements before session ends
**/*.{ts,tsx,js,jsx}: Use the ApiResponse interface pattern with generic type parameter:interface ApiResponse<T> { success: boolean; data?: T; error?: string; meta?: { total: number; page: number; limit: number; } }
Implement custom React hooks following the pattern: export a named function with use prefix, generic type parameters, and proper useEffect cleanup for side effects
**/*.{ts,tsx,js,jsx}: Never hardcode secrets; always use environment variables for sensitive credentials like API keys
Throw an error when required environment variables are not configured to fail fast and ensure security prerequisites are metUse Playwright as the E2E testing framework for critical user flows in TypeScript/JavaScript
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{js,ts,jsx,tsx}: Always create new objects and never mutate in place; return new copies instead
Keep files between 200–400 lines typical, with a maximum of 800 lines
Extract helpers when a file exceeds 200 lines
Handle errors explicitly at every level; never swallow errors silently
Validate all user input before processing; use schema-based validation where available
Never trust external data (API responses, file content, query params); always validate
All user inputs must be validated and sanitized
Error messages must be scrubbed of sensitive internals
Use readable, well-named identifiers in all code
Keep functions under 50 lines
Keep files under 800 lines
Avoid nesting deeper than 4 levels
Implement comprehensive error handling in all code
Do not hardcode values; use constants or environment configuration instead
Do not use in-place mutation; always return new objects or state
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,jsx,tsx,json,env*}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Do not hardcode secrets, API keys, passwords, or tokens
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{js,ts}: Use parameterized queries for all database writes (no string interpolation)
Auth/authz must be checked server-side for every sensitive path
Rate limiting must be applied to all public endpoints
Files:
schedule-app/src/data/useTodayResync.js
**/*.{jsx,tsx,js,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
HTML output must be sanitized where applicable
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/*.{js,ts,env*}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Required environment variables must be validated at startup
Files:
schedule-app/src/data/useTodayResync.js
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Delegate complex, domain-specific, architectural, security-sensitive, review, build, and testing tasks to the appropriate specialized agent; use parallel execution for independent operations.
For new features and bug fixes, follow TDD: write a failing test first, implement the minimum solution, then refactor and verify coverage.
Maintain at least 80% test coverage and provide unit, integration, and end-to-end tests, including critical user flows.
Never compromise security: validate all inputs, prevent SQL injection with parameterized queries, sanitize HTML against XSS, enable CSRF protection, verify authentication and authorization, rate-limit endpoints, and avoid leaking sensitive data in errors.
Never hardcode API keys, passwords, tokens, or other secrets; use environment variables or a secret manager, validate required secrets at startup, and rotate exposed secrets immediately.
If a security issue is found, stop, use the security-reviewer agent, fix critical issues, rotate exposed secrets, and review the codebase for similar issues.
Always create new objects and return new copies with changes applied; never mutate existing objects.
Organize code into many small, focused files by feature or domain rather than by type; target 200–400 lines and keep files below 800 lines where practical.
Handle errors at every level, show user-friendly messages in UI code, log detailed context server-side, and never silently swallow errors.
Validate all external and user input at system boundaries using schema-based validation; fail fast with clear messages and never trust external data.
Keep functions under 50 lines, files focused and under 800 lines, avoid nesting deeper than four levels, avoid hardcoded values, and use readable, well-named identifiers.
Plan complex features before implementation, identifying dependencies and risks and breaking work into phases.
After modifying code, run code review immediately and address critical and high-severity issues.
Store personal ...
Files:
schedule-app/src/data/useTodayResync.jsschedule-app/src/pages/GoalsPage.jsx
**/{src,app}/**/*.{jsx,tsx,vue,html}
📄 CodeRabbit inference engine (.cursor/rules/common-coding-style.md)
Provide user-friendly error messages in UI-facing code
Files:
schedule-app/src/pages/GoalsPage.jsx
{**/*.tsx,**/*.jsx,**/components/**}
📄 CodeRabbit inference engine (CLAUDE.md)
When working on React files (*.tsx, *.jsx, components/**), use react-patterns and react-testing skills. Invoke
/react-review,/react-build,/react-testcommands for React-specific work.
Files:
schedule-app/src/pages/GoalsPage.jsx
🪛 ast-grep (0.45.1)
schedule-app/src/data/useTodayResync.js
[warning] 38-38: Avoid using the initial state variable in setState
Context: setInterval(resync, intervalMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🔇 Additional comments (2)
schedule-app/src/pages/GoalsPage.jsx (2)
116-121: Re-enable resync when navigation returns to the current period.
useTodayResyncskips resync whilemanualNavRef.currentistrue.stepDayandstepWeekset that flag unconditionally. If the user navigates back to today or the current week, automatic resync remains disabled.Set the flag from the calculated next day or week value.
9-9: LGTM!Also applies to: 108-115
| // the phone) but aren't guaranteed on every platform — a screen simply | ||
| // timing out and back on doesn't reliably fire either — so a low-cost | ||
| // interval check backs them up rather than depending on any one event. | ||
| export function useTodayResync(onResync, intervalMs = 60000) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Replace the interval literal with a named default.
Line 26 embeds the resync policy as 60000. Define DEFAULT_RESYNC_INTERVAL_MS at module scope and use it as the default value.
Proposed change
+const DEFAULT_RESYNC_INTERVAL_MS = 60_000;
+
-export function useTodayResync(onResync, intervalMs = 60000) {
+export function useTodayResync(onResync, intervalMs = DEFAULT_RESYNC_INTERVAL_MS) {As per coding guidelines, “Do not use hardcoded values; use constants or configuration instead.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function useTodayResync(onResync, intervalMs = 60000) { | |
| const DEFAULT_RESYNC_INTERVAL_MS = 60_000; | |
| export function useTodayResync(onResync, intervalMs = DEFAULT_RESYNC_INTERVAL_MS) { |
🤖 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 `@schedule-app/src/data/useTodayResync.js` at line 26, Define a module-scoped
constant named DEFAULT_RESYNC_INTERVAL_MS with the existing 60000-millisecond
value, then update useTodayResync’s intervalMs default parameter to reference
that constant.
Source: Coding guidelines
The SharedCalendar/SharedCalendarMember/SharedCalendarInvite/SharedEvent models existed in schema.prisma and the routes in calendars.js were fully wired up, but no migration had ever been generated for them at all (not just unapplied — the migration file itself didn't exist), so there was nothing for `db:migrate`/`db:deploy` to run. Generated prisma/migrations/20260811161029_shared_calendars by diffing schema.prisma against a throwaway local Postgres already at the last checked-in migration. Verified the full route lifecycle directly against it: create a calendar (owner auto-added as a member) -> list -> invite -> accept (and re-accept is idempotent) -> add a shared event -> delete the calendar and confirm members/invites/events cascade away while the User rows survive. All 9 checks passed. Updated the README's "Known gaps" entry to match: the migration now exists and is verified, it just still needs `npm run db:migrate` (dev) or `npm run db:deploy` (prod) run against a real database, same as the lifetime-purchase gap above it. Left the "invites don't send email" gap as-is — that needs a provider decision (which service, whose API key) rather than a migration.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
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 `@backend/prisma/migrations/20260811161029_shared_calendars/migration.sql`:
- Around line 55-59: Update the SharedCalendarMember model in schema.prisma to
add an index on userId, then add the corresponding non-unique migration index
for SharedCalendarMember.userId alongside the existing indexes. Preserve the
existing unique calendarId/userId constraint.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0c90c3d0-03fe-4602-8549-f47e3b918d16
📒 Files selected for processing (2)
backend/README.mdbackend/prisma/migrations/20260811161029_shared_calendars/migration.sql
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,jsx,tsx,py,java,cs,rb,go,php,sql}
📄 CodeRabbit inference engine (.cursor/rules/common-security.md)
Use parameterized queries to prevent SQL injection
Files:
backend/prisma/migrations/20260811161029_shared_calendars/migration.sql
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Delegate complex, domain-specific, architectural, security-sensitive, review, build, and testing tasks to the appropriate specialized agent; use parallel execution for independent operations.
For new features and bug fixes, follow TDD: write a failing test first, implement the minimum solution, then refactor and verify coverage.
Maintain at least 80% test coverage and provide unit, integration, and end-to-end tests, including critical user flows.
Never compromise security: validate all inputs, prevent SQL injection with parameterized queries, sanitize HTML against XSS, enable CSRF protection, verify authentication and authorization, rate-limit endpoints, and avoid leaking sensitive data in errors.
Never hardcode API keys, passwords, tokens, or other secrets; use environment variables or a secret manager, validate required secrets at startup, and rotate exposed secrets immediately.
If a security issue is found, stop, use the security-reviewer agent, fix critical issues, rotate exposed secrets, and review the codebase for similar issues.
Always create new objects and return new copies with changes applied; never mutate existing objects.
Organize code into many small, focused files by feature or domain rather than by type; target 200–400 lines and keep files below 800 lines where practical.
Handle errors at every level, show user-friendly messages in UI code, log detailed context server-side, and never silently swallow errors.
Validate all external and user input at system boundaries using schema-based validation; fail fast with clear messages and never trust external data.
Keep functions under 50 lines, files focused and under 800 lines, avoid nesting deeper than four levels, avoid hardcoded values, and use readable, well-named identifiers.
Plan complex features before implementation, identifying dependencies and risks and breaking work into phases.
After modifying code, run code review immediately and address critical and high-severity issues.
Store personal ...
Files:
backend/prisma/migrations/20260811161029_shared_calendars/migration.sqlbackend/README.md
🪛 Squawk (2.61.0)
backend/prisma/migrations/20260811161029_shared_calendars/migration.sql
[warning] 7-7: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 8-8: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 19-19: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 32-32: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 33-33: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 34-34: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 49-49: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
[warning] 50-50: When Postgres stores a datetime in a timestamp field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time. Use timestamptz instead of timestamp for your column type.
(prefer-timestamp-tz)
🔇 Additional comments (2)
backend/README.md (1)
171-183: LGTM!backend/prisma/migrations/20260811161029_shared_calendars/migration.sql (1)
1-53: LGTM!Also applies to: 61-74
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "SharedCalendarMember_calendarId_userId_key" ON "SharedCalendarMember"("calendarId", "userId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "SharedCalendarInvite_token_key" ON "SharedCalendarInvite"("token"); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Add an index for member visibility lookups.
The visibility design queries SharedCalendarMember by userId. The existing (calendarId, userId) index cannot efficiently serve that predicate. Calendar listing will scan the membership table as it grows.
Add @@index([userId]) to SharedCalendarMember in backend/prisma/schema.prisma, then add the matching migration index.
Proposed migration change
CREATE UNIQUE INDEX "SharedCalendarMember_calendarId_userId_key" ON "SharedCalendarMember"("calendarId", "userId");
+
+-- CreateIndex
+CREATE INDEX "SharedCalendarMember_userId_idx" ON "SharedCalendarMember"("userId");🤖 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 `@backend/prisma/migrations/20260811161029_shared_calendars/migration.sql`
around lines 55 - 59, Update the SharedCalendarMember model in schema.prisma to
add an index on userId, then add the corresponding non-unique migration index
for SharedCalendarMember.userId alongside the existing indexes. Preserve the
existing unique calendarId/userId constraint.
|
Thank you for the time you put into this submission. This is a standalone missionary calendar application with its own backend, frontend, database migrations, billing, and deployment workflow, while this repository is specifically for the ECC agent-workflow plugin. Because the application is unrelated to ECC’s runtime or distribution surfaces, it is outside the project’s scope and we are going to close this PR. If you intended to contribute to ECC in the future, we would be glad to review a focused change tied to an ECC skill, agent, hook, installer, or documented workflow. |
What Changed
Why This Change
Testing Done
node tests/run-all.js)Type of Change
fix:Bug fixfeat:New featurerefactor:Code refactoringdocs:Documentationtest:Testschore:Maintenance/toolingci:CI/CD changesSecurity & Quality Checklist
If you changed dependencies or
package.json(bin/files/ deps)yarn install --mode=update-lockfileand committed theyarn.lockchange. CI runs Yarn in hardened mode on public PRs and fails if the lockfile would be modified, so an out of dateyarn.lockbreaks the build even when nothing else is wrong.If you added a skill, command, agent, hook, or CLI tool
package.json(binandfiles),manifests/install-components.json,manifests/install-modules.json, andagent.yamlnpm run catalog:sync) and command registry (npm run command-registry:write)README.md,COMMANDS-QUICK-REF.md,docs/COMMAND-AGENT-MAP.md)tests/scripts/npm-publish-surface.test.js).agents/skills/<name>/plusagents/openai.yaml; the Codex frontmatter validator allows onlyname,description,metadata,license,allowed-tools, so drop keys likeversionfrom that copy)npm test)Documentation