Full-stack implementation of event invitations allowing users to invite connected
contacts to calendar events. Invitees can respond Going/Tentative/Declined, with
per-occurrence overrides for recurring series. Invited events appear on the invitee's
calendar with a Users icon indicator. LeaveEventDialog replaces delete for invited events.
Backend: Migration 054 (2 tables + notification types), EventInvitation model with
lazy="raise", service layer, dual-router (events + event-invitations), cascade on
disconnect, events/dashboard queries extended with OR for invited events.
Frontend: Types, useEventInvitations hook, InviteeSection (view list + RSVP buttons +
invite search), LeaveEventDialog, event invite toast with 3 response buttons, calendar
eventContent render with Users icon for invited events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1: Recurrence safety — MAX_OCCURRENCES=730 hard cap, adaptive 90-day
horizon for daily events (interval<7), RecurrenceRule cross-field validation,
ID bounds on location_id/calendar_id schemas.
Phase 2: Dashboard correctness — shared calendar events now included in
/dashboard and /upcoming via get_accessible_calendar_ids helper. Project stats
consolidated into single GROUP BY query (saves 1 DB round-trip).
Phase 3: Write performance — bulk db.add_all() for child events, removed
redundant SELECT in this_and_future delete path.
Phase 4: Frontend query efficiency — staleTime: 30_000 on calendar events
query eliminates redundant refetches on mount/view switch. Backend LIMIT 2000
safety guard on events endpoint.
Phase 5: Rate limiting — nginx limit_req zone on /api/events (30r/m) to
prevent DB flooding via recurrence amplification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove migration 054 (duplicate of 035 which already has all 3 indexes,
including a superior partial index for starred events)
- Fix handleEventDidMount indentation and missing semicolons
- Replace eventClassNames arrow function with static UMBRA_EVENT_CLASSES array
- Correct misleading subquery comment in dashboard.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Header mismatch: Removed weekend tint from column headers — the white
overlay replaced the standard header bg (hsl 0 0% 8% / 0.65), creating
a non-flush look. Weekend differentiation now comes from body cells only.
Date format: dayHeaderFormat was applied globally, causing month view
headers to show dates like "Sat 10/1" instead of just "Sat". Moved to
per-view formats: month shows weekday only, week shows weekday + d/m,
day shows full weekday + day + month name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Weekend tint: hsl(0 0% 6%) was lighter than page bg #0a0a0a (imperceptible).
Changed to hsl(0 0% 2%) = #050505 for visible darkening. Added rule for
fc-daygrid-day-frame to paint above FC6 internal layers.
Dot spacing: Reduced padding from 1px 4px to 1px 2px for tighter edge gap.
FOUC fix: Moved umbra-event class from eventDidMount (post-paint) to
eventClassNames (synchronous pre-mount). eventDidMount now only sets
the --event-color CSS custom property.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
eventContent replaces FC's default inner markup including the dot span.
Render a manual fc-daygrid-event-dot with border-color: var(--event-color).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Month timed events: dot + title only, hover reveals translucent card
- Month all-day events: keep translucent fill
- Time right-aligned in month view (ml-auto)
- Week/day view: title on top, time underneath for better scanning
- Remove 2px left accent border from all events
- Set color:'transparent' on FC event data to prevent inline style conflicts
- Recurring repeat icon preserved in all views
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous commit failed to remove inline color props due to CRLF line
endings. FullCalendar was still setting inline styles that override CSS.
calendarColor is now correctly in extendedProps for the eventDidMount callback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 fixes:
- W-01: Add start_period: 30s to backend healthcheck for migration window
- W-03: Narrow .dockerignore *.md to specific files (preserve alembic/README)
Phase 2 fixes:
- C-01: Wrap Argon2id calls in totp.py (disable, regenerate, backup verify,
backup store) — missed in initial AC-2 pass
- S-01: Extract async wrappers (ahash_password, averify_password,
averify_password_with_upgrade) into services/auth.py, refactor all
callers to use them instead of manual run_in_executor boilerplate
- W-01: Fix ntfy dedup regression — commit per category instead of per-user
to preserve dedup records if a later category fails
Phase 4 fixes:
- C-01: Fix optimistic drag-and-drop cache key to include date range
- C-02: Replace toISOString() with format() to avoid UTC date shift in
visible range calculation
- W-02: Initialize visibleRange from current month to eliminate unscoped
first fetch + immediate refetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AW-2: Scope calendar events fetch to visible date range via start/end
query params, leveraging existing backend support
- AW-3: Reduce calendar events poll from 5s to 30s (personal organiser
doesn't need 12 API calls/min)
- AS-4: Gate shared-calendar polling on hasSharedCalendars — saves 12
wasted API calls/min for personal-only users
- AS-2: Lazy-load all route components with React.lazy() — only
AdminPortal was previously lazy, now all 10 routes are code-split
- AS-1: Add Vite manualChunks to split FullCalendar (~400KB), React,
TanStack Query, and UI libs into separate cacheable chunks
- AS-3: Extract clockNow into isolated ClockDisplay memo component —
prevents all 8 dashboard widgets from re-rendering every minute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
S-02: Confirmed drift-3 is used by auth/AmbientBackground — not dead code.
S-03: Extracted noise SVG data URI to module-level NOISE_SVG constant.
S-04: Added will-change: transform to drift orbs for GPU layer promotion.
S-05: Documented the 9 AM snooze default in getMinutesUntilTomorrowMorning.
S-06: Made calendar toolbar bg-card/95 with backdrop-blur-md for better
readability over the transparent FullCalendar grid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mobile view Select had md:hidden on the <select> element, but the
Select component wraps it in a <div> with an absolute ChevronDown icon
that remained visible. Moved md:hidden to a wrapper div so the entire
Select (including the chevron) is hidden on desktop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C-01: Simplify EntityTable sort dropdown to toggle-based (select
column, re-select to flip direction), add aria-label
- W-01: Convert CalendarPage mobile overlay to MobileDetailOverlay
- W-02: Use ref for onClose in MobileDetailOverlay to prevent
listener churn from inline arrow functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- S-01/W-06/S-02/S-04: Extract MobileDetailOverlay shared component
with Escape key, body scroll lock, and ARIA dialog attributes.
Refactored Todos, Reminders, People, Locations, ProjectDetail.
- W-02: Add specificity contract comment to mobile-scale CSS
- W-03: Enforce 10px floor for text-[9px] on mobile
- W-05: Add sort dropdown to EntityTable mobile card view
- S-03: Export MOBILE/DESKTOP breakpoint constants from useMediaQuery,
updated all 8 consumer files to use constants
- S-06: Bump KanbanBoard TouchSensor tolerance from 5 to 8
- S-07: Hover state audit — no action needed, hoverOnlyWhenSupported
in Tailwind config already handles touch devices correctly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add pr-8 to mobile view Select to prevent text clipping under chevron.
Add min-w-0 flex-shrink to calendar title h2 to prevent nav arrow overlap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3a. CalendarSidebar mobile collapse:
- Desktop sidebar + resize handle hidden below lg breakpoint
- Mobile Sheet overlay with PanelLeft toggle in toolbar
- Template selection closes mobile sidebar automatically
3b. KanbanBoard touch support:
- TouchSensor added alongside PointerSensor (200ms delay)
- Column min-width reduced on mobile (160px vs 200px)
- iOS smooth scroll enabled on horizontal container
3c. EntityTable mobile card view:
- mobileCardRender optional prop renders cards instead of table on mobile
- PeoplePage: card with name, category, email, phone
- LocationsPage: card with name, category, address
- TodosPage/RemindersPage use custom list components, not EntityTable
3d. DatePicker mobile bottom sheet:
- Renders as full-width bottom sheet on mobile (< 768px)
- Safe area inset padding for iOS home indicator
- Desktop positioned dropdown unchanged
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All page toolbars now flex-wrap on mobile with min-h instead of fixed h-16
- Segmented button filters (priority, status, view) hidden on mobile, replaced
with compact Select dropdowns
- Search inputs hidden on mobile where CategoryFilterBar already has search
- CategoryFilterBar wraps to full-width row on mobile (order-last)
- Action buttons show icon-only on mobile, full text on md+
- Calendar title hidden on xs screens for space
- Desktop layout completely unchanged (md:flex-nowrap restores original)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace per-mousemove setSidebarWidth() calls (triggering full React re-renders
including FullCalendar) with direct DOM style mutation during drag. React state
is committed only once on mouseup, eliminating all mid-drag re-renders and
localStorage writes that caused the lag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidebar width adjustable via click-and-drag (180–400px range, default 224px).
Width persists to localStorage across sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical:
- C-01: Populate member_count in GET /calendars for shared calendars
- C-02: Differentiate 423 lock errors in drag-drop onError (show lock-specific toast)
- C-03: Add expired lock purge to APScheduler housekeeping job
Warnings:
- W-01: Replace setattr loop with explicit field assignment in update_member
- W-02: Cap sync `since` param to 7 days to prevent unbounded scans
- W-05: Remove cosmetic isShared toggle (is_shared is auto-managed by invite flow)
- W-06: Populate preferred_name in _build_member_response from user model
- W-07: Add releaseMutation to release callback dependency array
Suggestion:
- S-06: Remove unused ConvertToSharedRequest schema
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: the previous approach synced poll data into lockInfo via a useEffect.
When the user selected an event with cached lock data, both the poll-data effect and
the event-change reset effect ran in the same render cycle. The event-change effect
ran second (effects are ordered by definition) and cleared lockInfo to null. On the
next render, viewLockQuery.data hadn't changed (TanStack Query structural sharing
returns same reference), so the poll-data effect never re-fired. Result: lockInfo
stayed null, banner stayed hidden until the next polling interval returned new data.
Fix: derive activeLockInfo directly from viewLockQuery.data (structural sharing
means it's always the latest authoritative value from TanStack Query) with lockInfo
as a fallback for the 423-error path only. Also add refetchIntervalInBackground:true
and refetchOnMount:'always' to ensure polling doesn't pause on tab switch and always
fires a fresh fetch when the component mounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The selectedEventIsShared check used `permissionMap.get(...) !== 'owner'` which
excluded calendar owners from all shared-event behavior (lock polling, lock
acquisition, lock banner display). Replaced with a sharedCalendarIds set that
includes both owned shared calendars (via cal.is_shared) and memberships.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove isEditing guard from viewLockQuery effect so lock banner shows for user B
even after user A transitions into edit mode (fixes banner disappearing)
- Disable Edit button proactively when lockInfo.locked is already known from polling,
preventing the user from even attempting acquireLock when a lock is active
- Fix acquire callback dep array in useEventLock (missing acquireMutation)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 1 (lock banner): Owners bypassed lock acquisition entirely, so no DB lock
was created when an owner edited a shared event. Members polling GET
/shared-calendars/events/{id}/lock correctly saw `locked: false`. Fix: remove
the `myPermission !== 'owner'` guard in handleEditStart so owners also acquire
a temporary 5-min edit lock when editing shared events, making the banner
visible to all other members.
Bug 2 (calendar not found on save): PUT /events/{id} called
_verify_calendar_ownership whenever calendar_id appeared in the payload, even
when it was unchanged. For shared-calendar members this always 404'd because
they don't own the calendar. Fix: add `update_data["calendar_id"] !=
event.calendar_id` to the guard — ownership is only verified when the calendar
is actually being changed (existing M-01 guard handles the move-off-shared case).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Flatten member row to strict single line: avatar | name | umbral name (violet) | pending badge | permission toggle | controls
- Umbral name shown in text-violet-400 for visual differentiation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CalendarForm: max-w-3xl when sharing (was sm:max-w-2xl, overridden by base max-w-xl)
- SharedCalendarSettings: max-w-2xl (was sm:max-w-lg)
- CalendarMemberRow: back to single-line with PermissionToggle inline (less cramped)
- Use unprefixed max-w classes so twMerge properly overrides DialogContent base
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Scope shared calendar polling to CalendarPage only (other consumers no longer poll)
- Add admin sharing stats card (owned/member/invites sent/received) in UserDetailSection
- Fix dual EventDetailPanel mount via JS media query breakpoint (replaces CSS hidden)
- Auto-close panel + toast when shared calendar is removed while viewing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Invite auto-sends at read_only: now stages connection with permission
selector (Read Only / Create Modify / Full Access) before sending
2. Shared calendars missing from event create dropdown: members with
create_modify+ permission now see shared calendars in calendar picker
3. Shared calendar category not showing for owner: owner's shared calendars
now appear under SHARED CALENDARS section with "Owner" badge
4. Event creation not updating calendar: handlePanelClose now invalidates
calendar-events query to ensure FullCalendar refreshes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Types: CalendarPermission, SharedCalendarMembership, CalendarMemberInfo,
CalendarInvite, EventLockInfo. Calendar type extended with is_shared.
Hooks: useCalendars extended with shared calendar polling (5s).
useSharedCalendars for member CRUD, invite responses, color updates.
useConnections cascade invalidation on disconnect.
New components: PermissionBadge, CalendarMemberSearch,
CalendarMemberRow, CalendarMemberList, SharedCalendarSection,
SharedCalendarSettings (non-owner dialog with color, members, leave).
Modified: CalendarForm (sharing toggle, member management for owners),
CalendarSidebar (shared calendars section with localStorage visibility),
CalendarPage (shared calendar ID integration in event filtering),
NotificationToaster (calendar_invite toast with accept/decline),
NotificationsPage (calendar_invite inline actions + type icons).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- EventForm + EventDetailPanel: native <Input type=date|datetime-local> → DatePicker with dynamic mode via all_day toggle
- TodoForm + TodoDetailPanel: merge date + time into single datetime DatePicker, remove separate time input, move recurrence select into 2-col grid beside date picker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The desktop detail panels are pre-mounted (always in DOM, hidden with w-0).
useState(isCreating) only captures the initial value on mount (false), so
when isCreating later becomes true via props, isEditing stays false. The
view-mode branch then runs with a null entity, crashing on property access.
Fix: use (isEditing || isCreating) for all conditionals that gate between
edit/create form and view mode, ensuring the form always renders when
isCreating is true regardless of isEditing state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All three DetailPanel components initialized isEditing=false even
when isCreating=true. The useEffect that flips it to true runs AFTER
the first render, so the view-mode branch executes with todo=null,
crashing on null.priority. Initialize isEditing from isCreating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match TaskDetailPanel gold standard: short fields use grid grid-cols-2
with icon+label headers, full-width fields (description) below the grid.
All grid slots render with "—" fallback to keep alignment consistent.
- Todo: Priority, Category, Due Date, Recurrence in grid
- Reminder: Status, Recurrence, Remind At, Snoozed Until in grid
- Calendar: Calendar, Starred, Start, End, Location, Recurrence in grid
- Task: Add "Updated X ago" footer (was missing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap CategoryFilterBar in flex-1 min-w-0 so search aligns right
- Add first_name, last_name, nickname to People search filter
- Add ring-inset to all header search inputs (People, Todos,
Locations, Reminders, Calendar) to prevent focus ring clipping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace single delayed updateSize() with requestAnimationFrame loop
that continuously resizes FullCalendar throughout the 300ms CSS
transition, eliminating the visual desync between panel and calendar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add animate-fade-in page transitions to all pages
- Persist sidebar collapsed state in localStorage
- Add two-click logout confirmation using useConfirmAction
- Restructure Todos header: replace <select> with pill filters, move search right
- Move Reminders search right-aligned with spacer
- Add event search dropdown + Create Event button to Calendar toolbar
- Add search input to Projects header with name/description filtering
- Fix CategoryFilterBar search focus ring clipping with ring-inset
- Create EventDetailPanel: read-only event view with copyable fields,
recurrence display, edit/delete actions, location name resolution
- Refactor CalendarPage to 55/45 split-panel layout matching People/Locations
- Add mobile overlay panel for calendar event details
- Add navigation state handler for CalendarPage (date/view from dashboard)
- Add navigation state handler for ProjectsPage (status filter from dashboard)
- Make all dashboard widgets navigable: stat cards → pages, week timeline
days → calendar day view, upcoming items → source pages, countdown items
→ calendar, today's events/todos/reminders → respective pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove instant invalid:ring/border from Input component (was showing
red outline on empty required fields before any interaction)
- Add CSS rule: form[data-submitted] input:invalid shows red border
- Add global submit listener in main.tsx that sets data-submitted on forms
- Add required prop to Labels missing asterisks: PersonForm (First Name),
LocationForm (Location Name), CalendarForm (Name), LockScreen
(Username, Password, Confirm Password)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- EventTemplate model: add server_default=func.now() to created_at
to match migration 011 and prevent autogenerate drift
- CalendarPage: use nullish coalescing for template calendar_id
instead of || 0 which produced an invalid falsy ID
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- LocationPicker: skip initial mount effect so dropdown doesn't auto-open
when form loads with an existing location value
- EventForm: separate templateData/templateName props from event prop so
template-based creation shows "Create Event from X Template" title
instead of "Edit Event", and correctly uses Create button + no Delete
- CalendarPage: pass templateName through to EventForm
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Matches the EventForm UI pattern for consistency — same slide-in panel,
same layout structure with scrollable content area and pinned footer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>