The view-only banner checked canEdit (hardcoded to isOwner) instead
of canEditTasks (which includes create_modify members). Editors saw
the banner incorrectly. Removed stale canEdit variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adding lazy='raise' to relationships with cascade='all, delete-orphan'
broke db.delete() — SQLAlchemy tried to lazy-load related objects for
Python-side cascade but lazy='raise' blocked it with MissingGreenlet.
Fix: Add passive_deletes=True to subtasks, comments, assignments, tasks,
and members relationships. This tells SQLAlchemy to defer cascade to
PostgreSQL's ondelete=CASCADE FK constraint instead of loading objects
in Python. Both the FK and ORM cascade are now aligned.
Also added onError handler to deleteTaskMutation so failures are visible
via toast instead of failing silently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P-01: Clamp delta poll since param to max 24h in the past (projects +
calendars) to prevent expensive full-table scans from malicious timestamps.
P-02: Validate individual user_id elements in ProjectMemberInvite and
TaskAssignmentCreate with Annotated[int, Field(ge=1, le=2147483647)].
P-04: Only enable delta polling for shared projects (member_count > 0).
Solo projects skip the 5s poll entirely.
P-05: Remove fragile 200ms onBlur timeout in ProjectShareSheet search.
The onMouseDown preventDefault on dropdown items already prevents blur
from firing before click registers.
P-06/S-04: Replace manual dict construction in model_validators with
__table__.columns iteration so new fields are auto-included.
S-01: Replace bare except in ProjectResponse.compute_member_count with
logger.debug to surface errors in development.
S-03: Consolidate cascade_projects_on_disconnect from 2 project ID
queries into 1 using IN clause with both user IDs.
S-05: Send version in toggleTaskMutation, updateTaskStatusMutation,
and toggleSubtaskMutation for full optimistic locking coverage. Handle
409 with refresh toast.
S-07: Replace window.location.href with React Router navigateRef in
task_assigned toast for client-side navigation.
S-08: Already fixed in previous commit (subtask comment selectinload).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Perf-1: Eliminate duplicate permission query on task update.
get_effective_task_permission now returns (effective, project_level)
tuple so the SEC-P02 allowlist check reuses the project-level
permission from the first call instead of querying again.
Perf-2: Memoize member permission lookup in ProjectDetail. Replace
3 inline acceptedMembers.find() calls with useMemo-derived
myPermission and canEditTasks.
S-06: Pass members/currentUserId/ownerId/canAssign to mobile
TaskDetailPanel (was missing — AssignmentPicker never appeared on
mobile).
S-08: Add missing selectinload(TaskComment.user) to subtask comments
chain in _task_load_options. Subtask comment author_name was always
null.
W-01: useDeltaPoll stores queryKeyToInvalidate in a ref to prevent
infinite re-render if caller passes inline array literal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root causes of the jitter:
1. No DragOverlay — card transformed in-place via translate(), causing
parent column layout reflow as siblings shifted around the gap.
2. transition-all on cards fought with drag transforms on slow moves.
3. closestCorners collision bounced rapidly between column boundaries.
Fixes:
- DragOverlay renders a floating copy of the card above everything,
with a subtle 2deg rotation and shadow for visual feedback.
- Original card becomes a ghost placeholder (accent border, 40% opacity)
so the column layout stays stable during drag.
- Switched to closestCenter collision detection (less boundary bounce).
- Increased PointerSensor distance from 5px to 8px to reduce accidental
drag activation.
- Removed transition-all from card styles (no more CSS vs drag fight).
- dropAnimation: null for instant snap on release.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskDetailPanel now shows an interactive AssignmentPicker (click to
open dropdown, select members, remove with X) when the user has
create_modify permission or is the owner. Read-only users see static
chips. Owner is included as a synthetic entry in the picker so they
can self-assign. Both assign and unassign mutations invalidate the
project query for immediate UI refresh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TaskRow: Show 'unassigned' label (muted) instead of invisible dash
so the assigned column is always visible in the task list.
- TaskDetailPanel: Replace old person_id dropdown with assignment chips
showing avatar + name for each assignee. Unassigned shows muted text
instead of a dash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TaskRow: Replace tiny avatar-only display with proper assigned column
showing avatar + name (single assignee) or avatar + "N people" (multi).
Hidden on mobile, right-aligned, 96px width matching other columns.
- Load options: Chain selectinload(ProjectTaskAssignment.user) so the
user relationship is available for serialization.
- TaskAssignmentResponse: Add model_validator to resolve user_name from
eagerly loaded user relationship (same pattern as TaskCommentResponse).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add member_count to ProjectResponse via model_validator (computed from
eagerly loaded members relationship). Shows on ProjectCard for both
owners ("2 members") and shared users ("Shared with you").
- Fix share button badge positioning (add relative class).
- Add dedicated showTaskAssignedToast with blue ClipboardList icon,
"View Project" action button, and 15s duration.
- Wire task_assigned into both initial-load and new-notification toast
dispatch flows in NotificationToaster.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
W-04: Add showProjectInviteToast with Accept/Decline buttons in
NotificationToaster, matching the connection/calendar/event invite
toast pattern. Wired into both initial-load and new-notification flows.
W-06: Delete rejected ProjectMember rows on rejection instead of
accumulating them with status='rejected'. Prevents indefinite growth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enables multi-user project collaboration mirroring the shared calendar
pattern. Includes ProjectMember model with permission levels, task
assignment with auto-membership, optimistic locking, field allowlist
for assignees, disconnect cascade, delta polling for projects and
calendars, and full frontend integration with share sheet, assignment
picker, permission gating, and notification handling.
Migrations: 057 (indexes + version + comment user_id), 058
(project_members), 059 (project_task_assignments)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C-01: Use func.count() for invitation cap instead of loading all rows
C-02: Remove unused display_calendar_id from EventInvitationResponse
F-01: Add field allowlist for invited editors (blocks is_starred,
recurrence_rule, calendar_id mutations)
W-02: Memoize existingInviteeIds Set in EventDetailPanel
W-03: Block per-occurrence overrides on declined/pending invitations
S-01: Make can_modify non-optional in EventInvitation TypeScript type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds has_active_invitees flag to the events GET response. The Users icon
now appears on the owner's calendar view when an event has accepted or
tentative invitees, giving visual feedback that the event is actively
shared. Single batch query with set lookup — no N+1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the AW-3 optimization that increased polling from 5s to 30s.
The faster interval is needed for shared calendar edits and invited
editor changes to appear promptly on other users' views.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allows event owners to grant individual invitees edit permission via a
toggle in the invitee list. Invited editors can modify event details
(title, description, time, location) but cannot change calendars, manage
invitees, delete events, or bulk-edit recurring series (scope restricted
to "this" only). The can_modify flag resets on decline to prevent silent
re-grant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C-01: Remove nginx rate limit on event invitations endpoint — was
blocking GET (invitee list) on rapid event switching. Backend
already caps at 20 invitations per event with connection validation.
C-02: respondingRef uses string prefixes (conn-, cal-, event-) instead
of fragile numeric offsets (+100000/+200000) to prevent collisions.
W-01: get_accessible_event_scope combined into single UNION ALL query
(3 DB round-trips → 1) for calendar IDs + invitation IDs.
W-02: Dashboard and upcoming endpoints now include is_invited,
invitation_status, and display_calendar_id on event items.
W-04: LeaveEventDialog closes on error (.finally) instead of staying
open when mutation rejects.
S-01: Migration 055 FK constraint gets explicit name for consistency.
S-02: InviteSearch dropdown dismisses on blur (150ms delay for clicks).
S-03: Display calendar picker shows only owned calendars, not shared.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
eventDidMount only fires once when FullCalendar first mounts a DOM element.
When event data refetches with a new calendarColor, the existing DOM element
is reused and --event-color CSS variable stays stale.
Fix: renderEventContent now uses a ref callback (syncColor) to walk up to
the parent .umbra-event element and update --event-color on every render,
ensuring background, hover, and dot colors reflect the current calendar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add py-1 to Select to prevent text clipping at h-8 height
- Use refetchQueries instead of invalidateQueries for calendar-events
after display calendar update to ensure immediate visual refresh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C-02: flush invitations before creating notifications so invitation_id
is available in notification data; eliminates extra pending fetch
- C-03: skip RSVP notification when status hasn't changed
- C-01: add defensive comments on update/delete endpoints
- W-01: add ge=1, le=2147483647 per-element validation on user_ids
- W-04: deduplicate invited_event_ids query via get_invited_event_ids()
- W-06: replace Python False with sa_false() in or_() clauses
- Frontend: extract resolveInvitationId helper, prefer data.invitation_id
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shared-calendar removal guard checks allCalendarIds, which only
contains the user's own + shared calendars. Invited events belong to
the inviter's calendar, triggering a false positive. Skip the check
for invited events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invited events belong to the inviter's calendar, which doesn't exist
in the invitee's calendar list. The visibleCalendarIds filter was
removing them. Now invited events bypass this filter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NotificationsPage: Going/Maybe/Decline buttons for event_invite notifications
- NotificationsPage: event_invite icon mapping, eager-refetch, click-to-calendar nav
- NotificationToaster: toast actionable unread notifications on first load (max 3)
so users see pending invites/requests when they sign in
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
W-01: Consolidate get_accessible_calendar_ids to single UNION query
instead of two separate DB round-trips.
W-02: Document that nginx rate limit on /api/events applies to all
methods (30r/m generous enough for GET polling at 2r/m).
W-03: Add weekly rule validation for consistency with other rule types.
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>
FC applies its own weekend background to header <th> elements too.
Force weekend header cells to use the same hsl(0 0% 8% / 0.65) as
weekday headers with !important to override FC's built-in styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After 10+ attempts, semi-transparent HSL values on near-black backgrounds
produce visible teal artifacts in Firefox due to compositor divergence.
Weekday/weekend frames now use identical --fc-neutral-bg-color. FC's own
weekend td background is neutralised with transparent !important.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hsl(0 0% 10% / 0.65) was visibly too bright vs weekday hsl(0 0% 8% / 0.65)
in Firefox. Reduced to hsl(0 0% 9% / 0.65) — 1% bump, subtle but present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Firefox composites rgba(255,255,255,0.05) differently against the
fc-daygrid-day-frame's --fc-neutral-bg-color background, producing a
visible mismatch. Switched to absolute HSL values that match the base
pattern:
- Month frame: hsl(0 0% 10% / 0.65) — same alpha as neutral-bg but
slightly lighter (10% vs 8% lightness)
- Timegrid cols: hsl(0 0% 5.5%) — slightly above page bg (3.9%)
Cross-browser consistent since no alpha compositing is needed.
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>
autoprefixer was silently stripping color-mix() during the PostCSS
build pipeline, causing the weekend tint background rules to produce
no output in the deployed CSS bundle. Replaced the three weekend
tint color-mix() calls with equivalent rgba(255,255,255,0.05) which
autoprefixer passes through unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FC6 renders an fc-daygrid-day-frame div inside every daygrid td, painted
with --fc-neutral-bg-color (hsl 0 0% 8% / 0.65). This opaque-ish layer sits
on top of the td background, completely hiding any rgba white overlay applied
to the td itself. Previous attempts set the tint on the td — it was never
visible because the frame covered it.
Fix: apply 5% white color-mix overlay directly to fc-daygrid-day-frame for
month view, and !important on fc-timegrid-col for week/day view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both the <td> and its child fc-daygrid-day-frame had the 3% white overlay,
causing the frame area to compound to ~6% while td edges stayed at 3%.
This created an uneven "not flush" pattern. The td rule alone is sufficient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RCA finding: grayscale tints are imperceptible on near-black (#0a0a0a)
backgrounds. Deltas of 3-5 RGB units fall below human JND threshold
and OLED panels can clip them to identical output via gamma compression.
Changed from hsl(0 0% 5%) to hsl(0 0% 100% / 0.03) — a semi-transparent
white overlay that composites additively for visible contrast.
See .claude/context/RCA/rca-calendarbg.md for full investigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Weekend bg raised from hsl(0 0% 2%) to hsl(0 0% 5%) across all 4 rules
(day cells, col headers, timegrid cols, daygrid-day-frame) so the tint is
visually distinct against the #0a0a0a page background
- Reduced .fc-daygrid-event-dot margin from default 4px each side to
0 2px 0 0 on umbra dot events, tightening the gap between dot and title
Co-Authored-By: Claude Sonnet 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>
- index.css: add explicit .fc-col-header-cell.fc-day-sat/sun rules with
!important to override the generic header background, and cover
.fc-timegrid-col weekend cells so the tint reaches all views
- CalendarPage.tsx: render .fc-daygrid-event-dot manually in the timed
month-view eventContent branch — FC's eventContent hook replaces the
entire default inner content including the dot span, so the CSS target
had nothing to paint
Co-Authored-By: Claude Sonnet 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>