feat: two-tier calendar password protection#40
Conversation
Implements a two-tier password protection model for calendars and integrates verification across server and client. Adds server-side checks that require a password for mutations if a calendar has a passwordHash, and require a password for reads when the calendar is marked locked. Verifies passwords from query params or request bodies and returns 401 on invalid credentials. Ensures calendar existence checks before verification. Updates client flows to use a password cache, automatically append cached passwords to requests, and gracefully handle locked calendars by returning empty datasets on unauthorized responses. Hides or disables UI actions when a calendar requires a password and none is cached, and provides an unlock UX that refetches data on success. Also updates developer instructions and translations to describe the two-tier protection, and standardizes password handling utilities to avoid duplicate prompts and improve user experience.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughServer-side password enforcement added: protected calendars (passwordHash + isLocked) require a provided password for GET (query) and mutations (body). Client caches/supplies passwords, gates UI, and refetches data after unlock; multiple API routes and hooks updated to verify passwords and return 401/404 consistently. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ 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 |
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive two-tier password protection system for calendars, distinguishing between write-only protection (passwordHash set, isLocked=false) and full protection (passwordHash set, isLocked=true). The implementation ensures that calendars with passwords require authentication for mutations (POST/PUT/PATCH/DELETE), while fully locked calendars additionally require passwords for read operations (GET).
Key changes:
- Server-side password verification added to all API routes with consistent two-tier checking (passwordHash for mutations, passwordHash + isLocked for reads)
- Client-side hooks automatically append cached passwords to requests and gracefully handle 401 responses with empty arrays
- UI components conditionally hide or disable actions when calendars require passwords but none is cached, with an unlock UX for write-only protected calendars
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| messages/en.json | Added translation keys for unlock prompts (unlockRequired, unlockRequiredDescription, unlockCalendar) |
| messages/de.json | Added German translations for unlock prompts |
| hooks/useShifts.ts | Auto-appends cached password to shift requests; returns empty array on 401 |
| hooks/usePresets.ts | Auto-appends cached password to preset requests; returns empty array on 401 |
| hooks/useNotes.ts | Auto-appends cached password to note requests; returns empty array on 401 |
| components/sync-notification-dialog.tsx | Adds password to sync log GET/DELETE/PATCH requests |
| components/shifts-list.tsx | Makes onDeleteShift optional to support disabled state |
| components/shift-card.tsx | Makes onDelete optional and conditionally renders delete button |
| components/shift-stats.tsx | Adds password parameter to stats requests with data validation |
| components/preset-selector.tsx | Passes onUnlock handler to preset list |
| components/preset-list.tsx | Shows unlock hint for write-only protected calendars; makes action handlers optional |
| components/calendar-selector.tsx | Hides external sync buttons when password required but not cached |
| components/calendar-grid.tsx | Makes day interaction handlers optional for conditional disabling |
| components/external-sync-manage-dialog.tsx | Adds password to all external sync API calls |
| app/page.tsx | Implements shouldHideUIElements to conditionally disable UI; refetches all data after password success |
| app/api/sync-logs/route.ts | Adds two-tier password verification to GET/PATCH/DELETE routes |
| app/api/shifts/stats/route.ts | Adds read-level password check (passwordHash + isLocked) |
| app/api/shifts/route.ts | Adds read-level check to GET, mutation-level check to POST |
| app/api/shifts/[id]/route.ts | Adds read-level check to GET, mutation-level check to PUT/DELETE |
| app/api/presets/route.ts | Adds read-level check to GET, mutation-level check to POST |
| app/api/presets/[id]/route.ts | Adds GET endpoint with read-level check; PATCH/DELETE have mutation-level check |
| app/api/notes/route.ts | Adds read-level check to GET, mutation-level check to POST |
| app/api/notes/[id]/route.ts | Adds read-level check to GET, mutation-level check to PUT/DELETE |
| app/api/external-syncs/route.ts | Adds read-level check to GET, mutation-level check to POST |
| app/api/external-syncs/[id]/sync/route.ts | Adds mutation-level password check to POST sync trigger |
| app/api/external-syncs/[id]/route.ts | Adds read-level check to GET, mutation-level check to PATCH/DELETE |
| app/api/calendars/[id]/route.ts | Adds read-level password check to GET |
| .github/copilot-instructions.md | Documents two-tier protection system with implementation examples and client flow |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/sync-notification-dialog.tsx (2)
143-160: Implement password dialog flow for 401 responses.DELETE operations should handle 401 responses by showing the PasswordDialog component, allowing users to enter the password and retry the operation. The current generic error toast doesn't guide users to unlock the calendar when password verification fails.
Consider implementing:
const response = await fetch(`/api/sync-logs?calendarId=${calendarId}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); if (response.status === 401) { // Show PasswordDialog, set pendingAction, retry after successful auth } else if (response.ok) { setLogs([]); toast.success(t("syncNotifications.deleteSuccess")); } else { toast.error(t("syncNotifications.deleteError")); }Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."
170-192: Implement password dialog flow for 401 responses.PATCH operations should handle 401 responses by showing the PasswordDialog component, allowing users to enter the password and retry the operation. The current generic error toast doesn't guide users to unlock the calendar when password verification fails.
Consider implementing:
const response = await fetch( `/api/sync-logs?calendarId=${calendarId}&action=markErrorsAsRead`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), } ); if (response.status === 401) { // Show PasswordDialog, set pendingAction, retry after successful auth } else if (response.ok) { await fetchLogs(); toast.success(t("syncNotifications.markedAsRead")); onErrorsMarkedRead?.(); } else { toast.error(t("syncNotifications.markAsReadError")); }Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."
components/external-sync-manage-dialog.tsx (1)
507-566:calendarIddependency missing fromuseCallback.The
saveExternalSyncChangescallback usescalendarId(line 512) but it's not included in the dependency array. This could cause stale closure issues where the wrong password is sent ifcalendarIdchanges while editing.saveExternalSyncChanges, + calendarId, t, ]
🧹 Nitpick comments (7)
components/sync-notification-dialog.tsx (3)
65-75: Consider handling 401 responses with password dialog flow.While the current silent failure approach aligns with gracefully handling locked calendars, users cannot unlock the calendar from this dialog when password verification fails. Consider checking for 401 status and showing the PasswordDialog component to allow password entry and retry, as per coding guidelines.
Example pattern:
const response = await fetch(`/api/sync-logs?${params}`); if (response.status === 401) { // Show password dialog and retry on success } else if (response.ok) { const data = await response.json(); setLogs(data); }Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."
141-141: Remove redundant null check.The ternary operator checking
calendarIdis unnecessary since line 136 already returns early if!calendarId.Apply this diff:
- const password = calendarId ? getCachedPassword(calendarId) : null; + const password = getCachedPassword(calendarId);
168-168: Remove redundant null check.The ternary operator checking
calendarIdis unnecessary since line 164 already returns early if!calendarId.Apply this diff:
- const password = calendarId ? getCachedPassword(calendarId) : null; + const password = getCachedPassword(calendarId);components/shift-stats.tsx (1)
7-7: Password‑aware stats fetch looks good; consider handling non‑OK responses and zero totalsUsing
getCachedPasswordandURLSearchParamsto appendpasswordis consistent with the shared password‑cache utilities and the new server contract. Two small robustness tweaks worth considering:
- Differentiate 401/other HTTP errors and log them explicitly
Right now any non‑OK response is treated as “no stats” (because
data.statswill be missing), which hides server errors and stale data. You can special‑case 401 (locked/invalid password) vs other errors and log the latter:try { const password = getCachedPassword(calendarId); const params = new URLSearchParams({ calendarId, period, date: currentDate.toISOString(), }); if (password) { params.append("password", password); } - const response = await fetch(`/api/shifts/stats?${params}`); - const data = await response.json(); - - // Only set stats if response has the expected structure - if (data.stats && typeof data.stats === "object") { - setStats(data); - } + const response = await fetch(`/api/shifts/stats?${params}`); + + if (!response.ok) { + if (response.status === 401) { + // Locked calendar or invalid password → treat as “no stats” + setStats(null); + } else { + console.error( + "Failed to fetch shift statistics:", + response.status, + response.statusText + ); + } + return; + } + + const data = await response.json(); + + // Only set stats if response has the expected structure + if (data.stats && typeof data.stats === "object") { + setStats(data); + } else { + setStats(null); + } } catch (error) { console.error("Failed to fetch shift statistics:", error); } finally {
- Avoid division by zero when all counts are 0
If
stats.statsexists but all values are0,totalShiftsis0and(count / totalShifts) * 100yieldsInfinity%/NaN%. A simple denominator guard keeps widths valid:- const totalShifts = - stats && stats.stats - ? Object.values(stats.stats).reduce((sum, count) => sum + count, 0) - : 0; + const totalShifts = + stats && stats.stats + ? Object.values(stats.stats).reduce((sum, count) => sum + count, 0) + : 0; + const totalShiftsDenominator = totalShifts || 1; @@ - style={{ - width: `${(count / totalShifts) * 100}%`, - }} + style={{ + width: `${(count / totalShiftsDenominator) * 100}%`, + }}Also applies to: 48-56, 58-64, 76-79
messages/en.json (1)
63-66: English password/unlock strings are consistent with new protection modelThe added
password.unlock*keys clearly describe the locked calendar state and unlock action, and they align with the two‑tier password flow. Just ensure they are referenced in the unlock UI so they don’t become unused per the translation guidelines.components/calendar-grid.tsx (1)
19-21: Optional interaction callbacks are good; consider stopping propagation on note icon clicksMaking
onDayRightClick,onNoteIconClick, andonLongPressoptional and guarding their use (including only setting long‑press timers when provided) makes the grid safer and more reusable.One small UX tweak: clicking the note icon currently still triggers the parent day’s
onClick(since the click bubbles). If the intended behavior is “open notes without toggling shifts,” you can stop propagation:- {dayNote && onNoteIconClick && ( + {dayNote && onNoteIconClick && ( <motion.div className="group/note relative" - onClick={(e) => onNoteIconClick(e, day)} + onClick={(e) => { + e.stopPropagation(); + onNoteIconClick(e, day); + }} title={dayNote.note} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} >Also applies to: 98-104, 119-121, 158-174
app/page.tsx (1)
906-914: UsesetCachedPassword()instead of directlocalStorage.setItem().Per coding guidelines, password-protected calendars should use utilities from
lib/password-cache.ts. Line 908-910 directly accesses localStorage instead of usingsetCachedPassword().+import { + getCachedPassword, + setCachedPassword, + verifyAndCachePassword, +} from "@/lib/password-cache";Then update the form handler:
if (data.valid) { - localStorage.setItem( - `calendar_password_${selectedCalendar}`, - password - ); + setCachedPassword(selectedCalendar, password); setIsCalendarUnlocked(true);Based on learnings, use utilities from lib/password-cache.ts instead of direct localStorage access.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
.github/copilot-instructions.md(1 hunks)app/api/calendars/[id]/route.ts(2 hunks)app/api/external-syncs/[id]/route.ts(5 hunks)app/api/external-syncs/[id]/sync/route.ts(2 hunks)app/api/external-syncs/route.ts(3 hunks)app/api/notes/[id]/route.ts(2 hunks)app/api/notes/route.ts(1 hunks)app/api/presets/[id]/route.ts(1 hunks)app/api/presets/route.ts(1 hunks)app/api/shifts/[id]/route.ts(2 hunks)app/api/shifts/route.ts(4 hunks)app/api/shifts/stats/route.ts(2 hunks)app/api/sync-logs/route.ts(4 hunks)app/page.tsx(8 hunks)components/calendar-grid.tsx(4 hunks)components/calendar-selector.tsx(1 hunks)components/external-sync-manage-dialog.tsx(11 hunks)components/preset-list.tsx(6 hunks)components/preset-selector.tsx(1 hunks)components/shift-card.tsx(2 hunks)components/shift-stats.tsx(3 hunks)components/shifts-list.tsx(1 hunks)components/sync-notification-dialog.tsx(4 hunks)hooks/useNotes.ts(1 hunks)hooks/usePresets.ts(2 hunks)hooks/useShifts.ts(2 hunks)messages/de.json(1 hunks)messages/en.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
app/api/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/api/**/*.ts: In Next.js 16 API routes, use async params destructuring:const { id } = await params;for dynamic routes instead of accessing params directly.
API routes must validate required query parameters and return NextResponse.json with appropriate status codes (e.g., 400 for missing parameters).
All API errors must be logged to console using console.error() for debugging purposes.
Files:
app/api/calendars/[id]/route.tsapp/api/presets/route.tsapp/api/presets/[id]/route.tsapp/api/notes/[id]/route.tsapp/api/notes/route.tsapp/api/external-syncs/[id]/route.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tsapp/api/shifts/route.tsapp/api/shifts/[id]/route.tsapp/api/external-syncs/[id]/sync/route.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.
Files:
app/api/calendars/[id]/route.tsapp/api/presets/route.tscomponents/shift-stats.tsxapp/api/presets/[id]/route.tsapp/api/notes/[id]/route.tshooks/usePresets.tscomponents/calendar-selector.tsxcomponents/sync-notification-dialog.tsxcomponents/shift-card.tsxapp/api/notes/route.tshooks/useNotes.tsapp/api/external-syncs/[id]/route.tscomponents/preset-selector.tsxcomponents/shifts-list.tsxhooks/useShifts.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tscomponents/external-sync-manage-dialog.tsxapp/api/shifts/route.tscomponents/calendar-grid.tsxapp/api/shifts/[id]/route.tscomponents/preset-list.tsxapp/api/external-syncs/[id]/sync/route.tsapp/page.tsx
**/*.{ts,tsx,json}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All code, comments, variable names, and messages must be written in English.
Files:
app/api/calendars/[id]/route.tsapp/api/presets/route.tscomponents/shift-stats.tsxapp/api/presets/[id]/route.tsapp/api/notes/[id]/route.tshooks/usePresets.tscomponents/calendar-selector.tsxcomponents/sync-notification-dialog.tsxmessages/de.jsonmessages/en.jsoncomponents/shift-card.tsxapp/api/notes/route.tshooks/useNotes.tsapp/api/external-syncs/[id]/route.tscomponents/preset-selector.tsxcomponents/shifts-list.tsxhooks/useShifts.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tscomponents/external-sync-manage-dialog.tsxapp/api/shifts/route.tscomponents/calendar-grid.tsxapp/api/shifts/[id]/route.tscomponents/preset-list.tsxapp/api/external-syncs/[id]/sync/route.tsapp/page.tsx
components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
components/**/*.{ts,tsx}: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles.
Components must support real-time updates via Server-Sent Events (SSE). Listen to relevant SSE events and implement silent refresh patterns using fetchData(false) to update without loading states.
Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.
Files:
components/shift-stats.tsxcomponents/calendar-selector.tsxcomponents/sync-notification-dialog.tsxcomponents/shift-card.tsxcomponents/preset-selector.tsxcomponents/shifts-list.tsxcomponents/external-sync-manage-dialog.tsxcomponents/calendar-grid.tsxcomponents/preset-list.tsx
components/**/*[Dd]ialog*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.
Files:
components/sync-notification-dialog.tsxcomponents/external-sync-manage-dialog.tsx
messages/{de,en}.json
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Unused translation keys in messages/{de,en}.json must be removed to keep translation files clean. All translation keys should be actively used in components.
Files:
messages/de.jsonmessages/en.json
app/page.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/page.tsx: Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.
Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.
Files:
app/page.tsx
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Applied to files:
app/api/calendars/[id]/route.tsapp/api/presets/route.tscomponents/shift-stats.tsxapp/api/presets/[id]/route.tsapp/api/notes/[id]/route.tshooks/usePresets.tscomponents/calendar-selector.tsxcomponents/sync-notification-dialog.tsxmessages/en.jsonapp/api/notes/route.tshooks/useNotes.tsapp/api/external-syncs/[id]/route.tscomponents/preset-selector.tsxhooks/useShifts.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tscomponents/external-sync-manage-dialog.tsx.github/copilot-instructions.mdapp/api/shifts/route.tscomponents/calendar-grid.tsxapp/api/shifts/[id]/route.tscomponents/preset-list.tsxapp/api/external-syncs/[id]/sync/route.tsapp/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.
Applied to files:
components/shift-stats.tsxcomponents/sync-notification-dialog.tsxapp/api/external-syncs/[id]/route.tshooks/useShifts.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tscomponents/external-sync-manage-dialog.tsx.github/copilot-instructions.mdapp/api/shifts/route.tsapp/api/external-syncs/[id]/sync/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.
Applied to files:
components/shift-stats.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Applied to files:
components/shift-stats.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Applied to files:
components/shift-stats.tsxhooks/usePresets.tshooks/useNotes.tsapp/api/external-syncs/[id]/route.tshooks/useShifts.tsapp/api/shifts/stats/route.tsapp/api/external-syncs/route.tsapp/api/sync-logs/route.tscomponents/external-sync-manage-dialog.tsx.github/copilot-instructions.mdapp/api/shifts/route.tscomponents/calendar-grid.tsxcomponents/preset-list.tsxapp/api/external-syncs/[id]/sync/route.tsapp/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
Applied to files:
components/shift-stats.tsxcomponents/sync-notification-dialog.tsxcomponents/preset-selector.tsxcomponents/external-sync-manage-dialog.tsx.github/copilot-instructions.mdapp/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*.{ts,tsx} : Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.
Applied to files:
components/calendar-selector.tsxcomponents/shift-card.tsxcomponents/shifts-list.tsx.github/copilot-instructions.mdcomponents/calendar-grid.tsxcomponents/preset-list.tsxapp/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.
Applied to files:
components/calendar-selector.tsxcomponents/calendar-grid.tsxapp/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: When creating new components with dialogs: (1) Create in components/ using shadcn/ui Dialog, (2) Accept props: open, onOpenChange, onSubmit, optional onDelete, (3) Use useTranslations(), (4) Reset local state when open changes to false, (5) Integrate in app/page.tsx.
Applied to files:
components/sync-notification-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to messages/{de,en}.json : Unused translation keys in messages/{de,en}.json must be removed to keep translation files clean. All translation keys should be actively used in components.
Applied to files:
messages/de.jsonmessages/en.json
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Implement cascade delete relationships in database schema: calendars → shifts, shiftPresets, calendarNotes (cascade delete); shiftPresets → shifts (set null on delete).
Applied to files:
components/shift-card.tsxcomponents/shifts-list.tsxapp/api/shifts/stats/route.tsapp/api/shifts/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*[Dd]ialog*.{ts,tsx} : Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.
Applied to files:
components/preset-selector.tsxcomponents/preset-list.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/**/*.ts : Drizzle ORM must be used for all database operations. drizzle.config.ts must be included in Docker runner stage for migrations to work.
Applied to files:
app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database migrations: After schema changes in lib/db/schema.ts, run 'npm run db:generate' followed by 'npm run db:migrate'. Never use db:push - prefer explicit migrations. Migrations must be version-controlled.
Applied to files:
app/api/shifts/stats/route.ts
🧬 Code graph analysis (17)
app/api/calendars/[id]/route.ts (1)
lib/password-utils.ts (1)
verifyPassword(16-23)
app/api/presets/route.ts (3)
lib/db/index.ts (1)
db(19-19)lib/db/schema.ts (1)
calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
components/shift-stats.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
app/api/presets/[id]/route.ts (2)
app/api/calendars/[id]/route.ts (1)
GET(8-53)lib/password-utils.ts (1)
verifyPassword(16-23)
app/api/notes/[id]/route.ts (3)
lib/db/index.ts (1)
db(19-19)lib/db/schema.ts (1)
calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
hooks/usePresets.ts (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
components/calendar-selector.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
components/sync-notification-dialog.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
app/api/notes/route.ts (3)
lib/db/index.ts (1)
db(19-19)lib/db/schema.ts (1)
calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
hooks/useNotes.ts (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
hooks/useShifts.ts (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
app/api/shifts/stats/route.ts (3)
lib/db/index.ts (1)
db(19-19)lib/db/schema.ts (1)
calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
app/api/sync-logs/route.ts (4)
app/api/calendars/[id]/route.ts (1)
GET(8-53)app/api/presets/route.ts (1)
GET(9-58)lib/db/schema.ts (1)
calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
components/external-sync-manage-dialog.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-12)
components/preset-list.tsx (2)
lib/db/schema.ts (1)
calendars(4-18)lib/password-cache.ts (1)
getCachedPassword(9-12)
app/api/external-syncs/[id]/sync/route.ts (2)
lib/db/schema.ts (2)
externalSyncs(20-47)calendars(4-18)lib/password-utils.ts (1)
verifyPassword(16-23)
app/page.tsx (2)
lib/password-cache.ts (1)
getCachedPassword(9-12)lib/db/schema.ts (1)
calendars(4-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: build-dev
🔇 Additional comments (37)
components/calendar-selector.tsx (1)
44-47: Password gating logic for sync buttons is correct.The condition properly gates sync buttons based on the
passwordHashfield fromCalendarWithCount. The implementation correctly usesgetCachedPassword()fromlib/password-cache.tsas required by the coding guidelines, and the logic—hiding sync buttons when a password is required but not cached—aligns with the two-tier password protection model (passwordHashfor mutations,isLockedfor reads).components/sync-notification-dialog.tsx (1)
5-5: LGTM!Correct usage of the password cache utility as per coding guidelines.
app/api/notes/route.ts (1)
22-45: Notes GET password gating aligns with two‑tier protectionFetching the calendar first, returning 404 when missing, and only enforcing
verifyPasswordwhenpasswordHash && isLockedcorrectly implements “read requires password only when locked” and “existence check before auth” for notes.components/shift-card.tsx (1)
12-12: MakingonDeleteoptional and guarding the button is a safe improvementAllowing
onDeleteto be optional and only rendering the delete button when both!shift.externalSyncIdandonDeleteare truthy avoids unsafe calls and correctly disables deletion for externally synced shifts.Also applies to: 52-60
messages/de.json (1)
63-66: German unlock strings match the English semanticsThe added
password.unlock*German translations accurately mirror the English messages for locked calendars and the unlock action, fitting the new two‑tier password UX.app/api/shifts/stats/route.ts (1)
3-8: Stats GET password verification correctly follows the two‑tier modelLoading the calendar first, returning 404 when not found, and only enforcing
verifyPasswordwhenpasswordHash && isLockedcleanly gates statistics behind a password for locked calendars while keeping unlocked ones readable. This is consistent with the wider password protection rules.Also applies to: 10-10, 35-58
.github/copilot-instructions.md (1)
67-133: LGTM! Comprehensive password protection documentation.The two-tier protection model is clearly explained with accurate code examples that match the actual implementation across the API routes. The implementation notes provide valuable guidance for avoiding common pitfalls like double password prompts and ensuring graceful 401 handling.
components/shifts-list.tsx (1)
13-13: LGTM! Optional callback supports password-gated deletion.Making
onDeleteShiftoptional aligns with the password protection model where delete actions may be unavailable until authentication succeeds. This allows the component to render shifts in locked calendars without requiring a delete handler.app/api/notes/[id]/route.ts (1)
15-51: LGTM! Correct implementation of two-tier protection for notes.The password verification correctly checks both
passwordHashandisLockedfor read access. The implementation properly handles the case where the note exists but the calendar doesn't (returns 404 for calendar).Note: The same security consideration about passwords in query parameters applies here (see comment on app/api/presets/route.ts).
hooks/useShifts.ts (2)
16-32: LGTM! Graceful handling of locked calendars.The implementation correctly uses
getCachedPassword()from the password-cache utilities as per coding guidelines. The graceful degradation on non-ok responses (returning empty array) prevents UI crashes when calendars are locked without valid cached passwords.As per coding guidelines: Using utilities from lib/password-cache.ts instead of direct localStorage access.
57-65: LGTM! Password included in shift creation.Correctly retrieves and includes the cached password in the POST request body for write-protected calendars.
hooks/usePresets.ts (1)
18-30: LGTM! Consistent password handling pattern.The implementation mirrors the pattern in
useShifts.ts, correctly usinggetCachedPassword()and gracefully degrading to empty arrays on 401 responses. The comment on line 26 clearly explains the behavior for locked calendars.As per coding guidelines: Using utilities from lib/password-cache.ts for password-protected calendars.
components/preset-selector.tsx (1)
179-179: Clarify the purpose of the empty async function.The
onUnlockprop is passed an empty async functionasync () => {}which doesn't perform any action after successful password verification. This seems incomplete.Is this intentional? Typically after successful unlock, you would want to:
- Refetch presets:
() => onPasswordRequired(async () => { await fetchPresets(); })- Or trigger the parent's preset refresh:
() => onPasswordRequired(onPresetsChange)If
PresetListhandles its own data refresh after unlock, this might be acceptable. Please verify the intended behavior and ensure data is refreshed after successful password entry.app/api/calendars/[id]/route.ts (1)
14-37: LGTM! Correct two-tier protection with efficient order of operations.The implementation correctly checks both
passwordHashandisLockedfor read access. The order of operations is optimal: fetch calendar → verify password → fetch related data. This prevents unnecessary shift queries when password verification fails.Note: The same security consideration about passwords in query parameters applies here (see comment on app/api/presets/route.ts).
app/api/presets/route.ts (1)
21-44: Passwords are transmitted via GET query parameters, which are logged by servers, proxies, and browsers.The implementation correctly validates the calendar exists and verifies the password, but this transmission method carries inherent security risks. This pattern is used consistently across multiple endpoints (shifts, notes, calendars), indicating a deliberate architectural choice. If tighter security is required, consider:
- Requiring POST requests for protected data retrieval
- Using request headers instead of query parameters
- Implementing session-based authentication after initial password verification via POST
app/api/presets/[id]/route.ts (1)
8-58: LGTM! GET handler follows established patterns.The new GET handler correctly implements the two-tier password protection model:
- Fetches preset first to verify existence (404 handling)
- Fetches related calendar to check protection status
- Requires password only when
passwordHash && isLocked(read-tier protection)- Uses query parameter for password (consistent with other GET endpoints)
- Proper error logging and status codes
app/api/external-syncs/route.ts (2)
26-49: LGTM! GET handler password protection is correct.The implementation correctly applies the two-tier model for reads: password is required only when both
passwordHashandisLockedare true. Calendar existence check precedes password verification.
92-114: LGTM! POST handler password protection is correct.Mutations correctly require password whenever
passwordHashexists (regardless ofisLockedstatus), following the PR's two-tier protection model where writes are always protected.components/external-sync-manage-dialog.tsx (2)
99-105: LGTM! Password is correctly appended to fetch requests.The implementation correctly uses
getCachedPassword()from the password-cache utilities (as per coding guidelines) and appends it to query parameters for GET requests.
334-340: LGTM! Sync request includes password in body.The sync request correctly sends the password in the JSON body for the POST mutation, consistent with the server-side expectation.
app/api/external-syncs/[id]/sync/route.ts (1)
347-395: LGTM! Password protection for sync operation is correct.The implementation:
- Reads password from JSON body (appropriate for POST mutation)
- Validates external sync exists before calendar lookup
- Uses mutation-tier protection (
passwordHashonly, not checkingisLocked)- Gracefully handles body parsing failures
app/api/shifts/[id]/route.ts (1)
46-67: LGTM! Password protection for shift GET is correct.The implementation correctly applies the read-tier protection model (
passwordHash && isLocked). While the calendar data could potentially be extracted from the existing join query (lines 32-36), the separate fetch provides cleaner separation of concerns for password verification logic.app/api/sync-logs/route.ts (3)
21-42: LGTM! GET handler implements read-tier protection correctly.Password verification uses the correct two-tier model for reads (
passwordHash && isLocked).
74-108: LGTM! PATCH handler implements mutation-tier protection correctly.Password verification correctly requires password whenever
passwordHashexists, regardless of lock status.
144-178: LGTM! DELETE handler implements mutation-tier protection correctly.Consistent with PATCH handler, correctly requiring password for any protected calendar.
app/api/shifts/route.ts (2)
22-45: LGTM! GET handler implements read-tier protection correctly.The implementation correctly:
- Extracts password from query parameters
- Validates calendar existence with 404 response
- Applies two-tier model (
passwordHash && isLocked) for reads
122-153: LGTM! POST handler implements mutation-tier protection correctly.Password is extracted from request body and verification requires password whenever
passwordHashexists.app/page.tsx (5)
113-127: LGTM! Password is correctly appended to external syncs fetch.Uses
getCachedPassword()utility as per coding guidelines and appends to query parameters for the GET request.
218-228: LGTM! UI gating logic is correct.
shouldHideUIElementscorrectly computes whether the calendar requires a password but none is cached. This drives the conditional rendering of interactive elements.
370-380: LGTM! Comprehensive data refresh after password success.The
handlePasswordSuccessfunction now correctly refetches all relevant data (shifts, presets, notes, external syncs, sync error status) and triggers a stats refresh after successful authentication.
998-1006: LGTM! UI interactions correctly gated.Right-click, note icon click, and long press handlers are conditionally passed as
undefinedwhenshouldHideUIElementsis true, effectively disabling these interactions for locked calendars without a cached password.
1054-1056: LGTM! Delete action correctly hidden.The delete shift handler is passed as
undefinedwhen UI elements should be hidden, preventing unauthorized deletions.components/preset-list.tsx (2)
15-17: LGTM: Props correctly updated for password protection flow.Making
onCreateNewandonManageClickoptional allows the component to gracefully handle locked states by disabling actions when callbacks aren't provided. The newonUnlockprop enables the password entry flow.Also applies to: 29-29
126-136: LGTM: Buttons correctly disabled when callbacks unavailable.The conditional
disabled={!onCreateNew}anddisabled={!onManageClick}properly prevent user interaction when the parent component doesn't provide handlers, which aligns with the password protection requirements.Also applies to: 157-162, 169-177
app/api/external-syncs/[id]/route.ts (3)
19-56: LGTM: Two-tier read protection correctly implemented.The GET handler properly enforces password verification only when
calendar.passwordHash && calendar.isLocked(line 49), allowing reads without password when the calendar is not locked. Calendar existence is validated before password checks, and appropriate status codes (404, 401) are returned.
100-122: LGTM: Mutation password protection correctly enforced.The PATCH handler properly requires password verification when
calendar.passwordHashexists (line 114), regardless ofisLockedstatus. This correctly implements tier-one protection (mutations always need password), with calendar existence validated before password checks.
232-279: LGTM: DELETE handler properly enforces password protection.The DELETE handler correctly implements tier-one protection by requiring password verification when
calendar.passwordHashexists (line 272). The optional body parsing with error handling (lines 236-242) gracefully handles requests without JSON bodies, and proper sequencing ensures calendar existence before password validation.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/page.tsx (2)
909-916: Use password cache utilities instead of direct localStorage access.Per coding guidelines, password-protected calendars should use utilities from
lib/password-cache.tsinstead of direct localStorage access. TheverifyAndCachePassword()function already handles both verification and caching..then((data) => { if (data.valid) { - localStorage.setItem( - `calendar_password_${selectedCalendar}`, - password - ); + // Use password cache utility + import("@/lib/password-cache").then(({ setCachedPassword }) => { + setCachedPassword(selectedCalendar, password); + }); setIsCalendarUnlocked(true); // Trigger data refetch after successful password entry handlePasswordSuccess(password);Alternatively, refactor the entire form submission to use
verifyAndCachePassword()which already exists in scope:onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const password = formData.get("password") as string; if (password && selectedCalendar) { - fetch( - `/api/calendars/${selectedCalendar}/verify-password`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ password }), - } - ) - .then((response) => response.json()) - .then((data) => { - if (data.valid) { - localStorage.setItem( - `calendar_password_${selectedCalendar}`, - password - ); - setIsCalendarUnlocked(true); - // Trigger data refetch after successful password entry - handlePasswordSuccess(password); - } else { - toast.error(t("password.errorIncorrect")); - } - }) - .catch(() => { - toast.error(t("password.errorIncorrect")); - }); + verifyAndCachePassword(selectedCalendar, password) + .then((result) => { + if (result.valid) { + setIsCalendarUnlocked(true); + handlePasswordSuccess(password); + } else { + toast.error(t("password.errorIncorrect")); + } + }) + .catch(() => { + toast.error(t("password.errorIncorrect")); + }); } }}Based on learnings, password cache utilities should be used consistently.
290-317: WraphandlePasswordSuccesswithuseCallbackand add to useEffect dependencies.The function is called at line 292 but missing from the dependency array at line 317. Since
handlePasswordSuccessdepends onrefetchShifts,refetchPresets,refetchNotes,fetchExternalSyncs,fetchSyncErrorStatus, andpendingAction, the effect should re-run when these change. Wrap it withuseCallbackto memoize the function and add it to the dependencies array, following the pattern used forfetchExternalSyncsandfetchSyncErrorStatuselsewhere in this file.
🧹 Nitpick comments (2)
app/page.tsx (2)
119-126: Handle 401 unauthorized responses explicitly.Per the PR objectives, the client should "gracefully handle locked calendars by returning empty datasets on unauthorized responses." Currently, non-200 responses silently fail without distinguishing authorization failures from other errors.
const response = await fetch(`/api/external-syncs?${params}`); if (response.ok) { const data = await response.json(); setExternalSyncs(data); + } else if (response.status === 401) { + // Unauthorized - calendar is locked, clear data gracefully + setExternalSyncs([]); } } catch (error) { console.error("Failed to fetch external syncs:", error); }
146-157: Handle 401 unauthorized responses for sync logs as well.Same issue as external syncs - unauthorized responses should explicitly clear error status rather than silently failing.
const response = await fetch(`/api/sync-logs?${params}`); if (response.ok) { const logs = await response.json(); - // Only show errors that are not read const hasErrors = logs.some( (log: any) => log.status === "error" && !log.isRead ); setHasSyncErrors(hasErrors); + } else if (response.status === 401) { + // Unauthorized - calendar is locked, hide error indicators + setHasSyncErrors(false); } } catch (error) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/page.tsx(8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.
Files:
app/page.tsx
app/page.tsx
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/page.tsx: Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.
Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.
Files:
app/page.tsx
**/*.{ts,tsx,json}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All code, comments, variable names, and messages must be written in English.
Files:
app/page.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*.{ts,tsx} : Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.
Applied to files:
app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.
Applied to files:
app/page.tsx
🧬 Code graph analysis (1)
app/page.tsx (2)
lib/password-cache.ts (1)
getCachedPassword(9-12)lib/db/schema.ts (1)
calendars(4-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-dev
🔇 Additional comments (5)
app/page.tsx (5)
219-230: LGTM!The
shouldHideUIElementsmemo correctly gates UI based on password protection status. UsingpasswordCacheTriggeras a dependency ensures the memo re-evaluates when the password cache changes, working around localStorage not triggering React updates.
371-383: LGTM!The refactored
handlePasswordSuccessproperly refetches all password-protected data after successful authentication and updates thepasswordCacheTriggerto ensure UI gating re-evaluates. UsingPromise.allfor parallel fetches is efficient.
1000-1008: LGTM!Properly gates note-related interactions (right-click, note icon click, long press) when the calendar requires a password but none is cached. Setting handlers to
undefinedis a clean approach.
1014-1043: LGTM!Hiding the note hint block when UI elements are gated is good UX - users won't see instructions for features they can't currently use.
1056-1066: LGTM!Consistent UI gating for shift deletion and the floating action button. This aligns with the PR objective to "hide or disable UI actions when a calendar requires a password and none is cached."
…or calendar access
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/external-sync-manage-dialog.tsx (1)
507-566: MissingcalendarIdinuseCallbackdependency array.The
saveExternalSyncChangescallback usescalendarIdviagetCachedPassword(calendarId)on line 512, butcalendarIdis not included in the dependency array (lines 555-565). This could cause stale closure issues ifcalendarIdchanges while editing.[ editingSync, formName, formUrl, formColor, formDisplayMode, formAutoSyncInterval, fetchSyncs, onSyncComplete, t, + calendarId, ]
🧹 Nitpick comments (2)
components/sync-notification-dialog.tsx (1)
141-147: Password sent correctly in DELETE body, but 401 response isn't handled specially.The password is correctly included in the request body. However, unlike
useShiftsanduseNoteswhich haveonPasswordRequiredcallbacks for 401 responses, this dialog shows a generic error toast on failure.Consider whether sync log deletion should trigger a password prompt on 401, or if the current behavior (generic error) is acceptable since sync logs are secondary data.
hooks/useShifts.ts (1)
56-67: Consider addingonPasswordRequiredcallback for consistency with update/delete.The
createShiftfunction includes the password in the request body but doesn't have anonPasswordRequiredcallback parameter likeupdateShiftanddeleteShiftdo. If the server returns 401 on a protected calendar, the user sees a generic error instead of being prompted to authenticate.If the UI guarantees the calendar is unlocked before allowing shift creation, this may be acceptable. Otherwise, consider adding the callback for consistency:
const createShift = async ( formData: ShiftFormData, + onPasswordRequired?: () => void ) => {And handle 401 responses similarly to
updateShift/deleteShift.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
components/external-sync-manage-dialog.tsx(11 hunks)components/shift-stats.tsx(3 hunks)components/sync-notification-dialog.tsx(4 hunks)hooks/useNotes.ts(3 hunks)hooks/useShifts.ts(4 hunks)lib/password-cache.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- components/shift-stats.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.
Files:
components/sync-notification-dialog.tsxlib/password-cache.tscomponents/external-sync-manage-dialog.tsxhooks/useShifts.tshooks/useNotes.ts
components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
components/**/*.{ts,tsx}: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles.
Components must support real-time updates via Server-Sent Events (SSE). Listen to relevant SSE events and implement silent refresh patterns using fetchData(false) to update without loading states.
Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.
Files:
components/sync-notification-dialog.tsxcomponents/external-sync-manage-dialog.tsx
**/*.{ts,tsx,json}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All code, comments, variable names, and messages must be written in English.
Files:
components/sync-notification-dialog.tsxlib/password-cache.tscomponents/external-sync-manage-dialog.tsxhooks/useShifts.tshooks/useNotes.ts
components/**/*[Dd]ialog*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.
Files:
components/sync-notification-dialog.tsxcomponents/external-sync-manage-dialog.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Applied to files:
components/sync-notification-dialog.tsxlib/password-cache.tscomponents/external-sync-manage-dialog.tsxhooks/useShifts.tshooks/useNotes.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
Applied to files:
components/sync-notification-dialog.tsxcomponents/external-sync-manage-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.
Applied to files:
components/sync-notification-dialog.tsxlib/password-cache.tscomponents/external-sync-manage-dialog.tsxhooks/useShifts.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: When creating new components with dialogs: (1) Create in components/ using shadcn/ui Dialog, (2) Accept props: open, onOpenChange, onSubmit, optional onDelete, (3) Use useTranslations(), (4) Reset local state when open changes to false, (5) Integrate in app/page.tsx.
Applied to files:
components/sync-notification-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Applied to files:
components/external-sync-manage-dialog.tsxhooks/useShifts.tshooks/useNotes.ts
🧬 Code graph analysis (4)
components/sync-notification-dialog.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-14)
components/external-sync-manage-dialog.tsx (1)
lib/password-cache.ts (1)
getCachedPassword(9-14)
hooks/useShifts.ts (1)
lib/password-cache.ts (1)
getCachedPassword(9-14)
hooks/useNotes.ts (1)
lib/password-cache.ts (1)
getCachedPassword(9-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-dev
🔇 Additional comments (15)
lib/password-cache.ts (1)
9-14: LGTM! Signature broadening handles nullable calendar IDs gracefully.The updated signature accepting
string | null | undefinedwith early return for falsy values is a clean approach that:
- Prevents runtime errors when calendarId is not yet available
- Guards against SSR environments where
windowis undefined- Returns
nullconsistently for invalid inputs, aligning with the return typeThis correctly supports the various call sites across hooks and components that may invoke
getCachedPasswordbefore a calendar is selected.components/sync-notification-dialog.tsx (2)
65-71: LGTM! Password correctly appended to GET request via query parameters.The implementation properly:
- Retrieves the cached password after the calendarId guard
- Uses
URLSearchParamsfor clean query string construction- Conditionally appends password only when present
This aligns with the two-tier protection model where read operations on locked calendars require password verification.
168-177: LGTM! Password correctly sent in PATCH body.Consistent pattern with the DELETE handler. The password is properly included in the JSON body for the mark-as-read mutation.
hooks/useShifts.ts (3)
16-33: LGTM! Fetch handles password-protected calendars correctly.The implementation:
- Builds query params with password when available
- Clears shifts on any non-OK response (appropriate for locked calendars returning 401)
- Clears shifts on fetch errors to prevent stale data display
This aligns with the PR objective to "gracefully handle locked calendars by returning empty datasets on unauthorized responses."
94-106: LGTM! Proper 401 handling with password required callback.The implementation correctly:
- Retrieves cached password (returns null if calendarId is undefined)
- Includes password in the PATCH body
- Handles 401 responses by invoking the
onPasswordRequiredcallbackThis follows the coding guidelines pattern for showing
PasswordDialogwhen password verification fails.
134-144: LGTM! Consistent 401 handling pattern.Delete follows the same correct pattern as update: password in body with
onPasswordRequiredcallback for handling authentication failures.hooks/useNotes.ts (3)
16-40: LGTM! Improved error handling addresses previous review feedback.The implementation now correctly:
- Clears notes only on 401/403 (authentication failures for locked calendars)
- Preserves existing notes on other server errors (5xx, etc.)
- Logs errors without clearing data on network failures
This is a significant improvement over the previous version that cleared notes on any non-OK response.
96-102: LGTM! Consistent password handling for update mutation.Follows the established pattern with password in body and proper 401 callback handling.
135-143: LGTM! Consistent password handling for delete mutation.Same correct pattern as update.
components/external-sync-manage-dialog.tsx (6)
99-116: LGTM! Password correctly appended to both sync and log fetches.The implementation properly:
- Retrieves password once after the calendarId guard
- Reuses the same password for both
/api/external-syncsand/api/sync-logsrequests- Uses
URLSearchParamsfor clean query construction
241-258: LGTM! Password included in create request.Password is correctly retrieved and included in the POST body for creating new external syncs.
294-307: LGTM! Password correctly included in update request.
334-340: LGTM! Password sent in sync trigger request.
375-381: LGTM! Password included in delete request.
451-460: LGTM! Password included in visibility toggle request.
Implements a two-tier password protection model for calendars and integrates verification across server and client.
Adds server-side checks that require a password for mutations if a calendar has a passwordHash, and require a password for reads when the calendar is marked locked. Verifies passwords from query params or request bodies and returns 401 on invalid credentials. Ensures calendar existence checks before verification.
Updates client flows to use a password cache, automatically append cached passwords to requests, and gracefully handle locked calendars by returning empty datasets on unauthorized responses. Hides or disables UI actions when a calendar requires a password and none is cached, and provides an unlock UX that refetches data on success.
Also updates developer instructions and translations to describe the two-tier protection, and standardizes password handling utilities to avoid duplicate prompts and improve user experience.
Summary by CodeRabbit
New Features
Bug Fixes / UX
✏️ Tip: You can customize this high-level summary in your review settings.