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>
Add dark-themed FullCalendar "+more" popover with CSS X close button
(replaces broken font icon). Add pr-8 to all mobile Select dropdowns
to prevent text clipping under chevron. Normalize header gap to
gap-2 md:gap-4 across all page headers for tighter mobile layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale down all content text on mobile via .mobile-scale CSS class (excludes
navbar/UMBRA title). Hide calendar event times in month view (Google Calendar
style). Restructure CategoryFilterBar so categories display on a separate row
when toggled instead of being hidden behind the search bar. Reduce dashboard
widget density with hidden badges and tighter spacing on small screens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Page titles: text-xl on mobile, text-2xl on desktop (7 pages)
- Stat cards: reduce padding/gap on mobile, hide icons below sm (3 pages)
- TodoItem: two-line layout on mobile (title row + metadata row)
- ReminderItem: same two-line treatment
- FullCalendar: smaller event font/padding on mobile via CSS media query
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The import was added but the sensors config replacement failed silently
due to line ending mismatch. TouchSensor now properly registered with
200ms delay / 5px tolerance alongside PointerSensor.
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>
- EntityTable: add useMediaQuery hook, mobileCardRender prop, and mobile card path
that replaces the table on screens <768px when a renderer is provided
- PeoplePage: add mobileCardRender showing name, category, email, phone
- LocationsPage: add mobileCardRender showing name, category, address
Note: TodosPage and RemindersPage use custom list components (TodoList,
ReminderList), not EntityTable directly — no changes needed there.
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>
The onBlur handler captured the stale dateOfBirth value from before
onChange updated state, causing the equality guard to silently abort
the save. Fixed by saving inline in onChange with the fresh value
and removing onBlur/onKeyDown from the DatePicker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
M-01: Unify registration error messages to prevent username/email
enumeration — both now return the same generic message.
Bug fix: Contact detail panel header and initials now use shared_fields
for umbral contacts instead of stale Person record first_name/last_name.
The table list already did this via sf() helper; the detail panel header
and getPersonInitialsName were bypassing it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-02: Purge accepted connection requests after 90 days (rejected/cancelled stay at 30)
W-04: Rename shadowed `type` parameter to `notification_type` with alias
W-05: Extract notification type string literals to constants in connection service
W-06: Match notification list polling interval to unread count (15s when visible)
W-07: Add filter_to_shareable defence-in-depth gate on resolve_shared_profile output
W-03: Verified false positive — no double person lookup exists in accept flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: ConnectionRequestCard now uses local isResponding state instead of
shared hook boolean, so accepting one card doesn't disable all others.
C-03: detach_umbral_contact no longer wipes person data (name, email,
phone, etc.) when a connection is severed. The person becomes a standard
contact with all data preserved, preventing data loss for pre-existing
contacts that were linked to connections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
location /api/connections/request was a prefix match, unintentionally
rate-limiting /api/connections/requests/incoming, /outgoing, and
/requests/{id}/respond at 3r/m. This caused the accept button on toast
notifications to get 429'd when clicked immediately — the incoming
requests poll + outgoing poll had already exhausted the rate limit.
Changed to exact match (= /api/connections/request) so only the send
endpoint is rate-limited, not the entire /requests/* tree.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The W-03 QA fix (removing refetchIntervalInBackground) broke toast
notifications when the receiver tab is hidden (e.g. user switches to
sender's tab). Without background polling, unread count never updates,
NotificationToaster never detects new notifications, and no toast fires.
Restored with explanatory comment documenting the dependency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W-01: Wrap accept flow db.commit() in IntegrityError handler (409)
- W-03: Remove refetchIntervalInBackground from unread count polling
- W-04: detach_umbral_contact now clears all shared fields on Person
- W-05: sf() callers no longer fall through via ?? to stale local data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Toast accept/reject now calls markRead on the corresponding notification
so it clears from unread in the notifications tab. Uses markReadRef to
avoid stale closure in Sonner toast callbacks. Covers both success and
409 (already resolved) paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previous fix (2139ea8) caused regression: staleTime:0 nuked cache on
every mount, isLoadingIncoming disabled buttons during cold-cache fetch.
Changes:
- Remove staleTime:0 (keep refetchOnMount:'always' for background refresh)
- Revert button gate to pendingRequestIds.has() only (no is_read gate)
- Remove isLoadingIncoming from disabled prop and spinner condition
- Add 409-as-success handling to ConnectionRequestCard (People tab)
- Use axios.isAxiosError() instead of err:any in all 3 catch blocks
- Add markRead() call to 409 branch so notifications clear properly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- incomingQuery: staleTime:0 + refetchOnMount:'always' so pending
requests are always fresh when components mount (was inheriting
5-min global staleTime, causing empty pendingRequestIds on nav)
- NotificationsPage: show Accept button while incoming data loads
(was hidden during async gap); disable with spinner until ready
- Both toast and page: treat 409 as success ("already accepted")
instead of showing error (fixes race when both fire respond)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Toast accept button captured a stale `respond` reference from the
Sonner closure. Use respondRef pattern so clicks always dispatch
through the current mutation. Backend respond endpoint now catches
unhandled exceptions and returns proper JSON with detail field
instead of plain-text 500s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sendRequestMutation and cancelMutation were awaiting query invalidation
in onSuccess, which blocked mutateAsync until 3+ network refetches
completed (~1s+ delay). This caused noticeable lag on send/link/cancel
operations. Now all mutations use sync fire-and-forget invalidation,
matching respondMutation's pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Toast buttons are static Sonner elements that can't bind React state,
so clicks had no visual feedback and allowed duplicate mutations.
Now: dismiss custom toast immediately, show loading toast, and block
concurrent clicks via a ref-based Set guard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Toast now uses the same respond() from useConnections hook instead
of raw api.put, making both accept surfaces share identical code.
Also made respondMutation.onSuccess fire-and-forget to prevent
invalidation errors from surfacing as mutation failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate API error handling from query invalidation in
NotificationToaster's handleConnectionRespond so a failed refetch
doesn't surface as "Failed to respond" after a successful accept.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: ['connections', 'incoming'] query has no polling, so
pendingRequestIds stays empty until manually refetched. Accept buttons
on NotificationsPage are gated by this set.
- NotificationsPage: detect connection_request notifications not in
pendingRequestIds and eagerly invalidate incoming requests query
- NotificationToaster: invalidate ['connections', 'incoming'] when
connection_request notifications arrive, so accept buttons are
ready on all surfaces immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Await all query invalidations in respondMutation/cancelMutation
onSuccess so UI has fresh data before mutation promise resolves
- Use deterministic toast IDs (connection-request-{id}) for Sonner
toasts so they can be dismissed from any accept surface
- Dismiss stale connection toasts in respondMutation.onSuccess
- Fix handleCancel setting resolved before API call completes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inject umbral_name into shared_fields for umbral contacts (always visible)
- Show @umbralname subtitle in detail panel header
- Add preferred_name to panel fields with synced label for umbral contacts
- Add Link button on standard contacts to tie to umbral user via connection request
- Migration 046: person_id FK on connection_requests with index
- Validate person_id ownership on send, re-validate + convert on accept
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 1: _to_settings_response() was missing share_first_name and
share_last_name — the response always returned false (Pydantic default),
causing the frontend to sync toggles back to off after save.
Bug 2: Table column renderers read from stale Person record fields.
Added sf() helper that overlays shared_fields for umbral contacts,
applied to name, phone, email, role, and birthday columns. The table
now shows live shared profile data matching the detail panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete person now severs the bidirectional connection when the person
is an umbral contact — removes both UserConnection rows and detaches
the counterpart's Person record. Fixes "Already connected" error
when trying to reconnect after deleting an umbral contact.
New PUT /people/{id}/unlink endpoint converts an umbral contact to a
standard contact (detaches linked fields) while also severing the
bidirectional connection, keeping the Person in the contact list.
Frontend: EntityDetailPanel gains extraActions prop. PeoplePage renders
an "Unlink" button in the panel footer for umbral contacts. Delete
mutation now also invalidates connections query.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notifications: enable refetchIntervalInBackground on unread count
query so notifications appear in background tabs without requiring
a tab switch to trigger refetchOnWindowFocus.
Name sharing: add share_first_name and share_last_name to the full
sharing pipeline — migration 045, Settings model/schema, SHAREABLE_FIELDS,
resolve_shared_profile, create_person_from_connection (now populates
first_name + last_name + computed display name), SharingOverrideUpdate,
frontend types and SettingsPage toggles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>