Many addresses resolve to just the road in Nominatim (no house_number
in response). Now extracts the leading number from the user's original
search query and prepends it to the road name, so "123 Adelaide Terrace"
stays as "123 Adelaide Terrace" instead of just "Adelaide Terrace".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use addressdetails=1 to get structured address components and build
the name as "123 Example St" instead of splitting display_name on
the first comma (which isolated the house number from the road).
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>
- 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>
The notifications table CHECK constraint did not include project_invite,
project_invite_accepted, project_invite_rejected, or task_assigned.
This caused 500 errors on invite_members and assign_users_to_task
because create_notification violated ck_notifications_type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The invite_members handler called create_notification with type="project_invite", which
is not in the ck_notifications_type CHECK constraint. The db.flush() inside the handler
flushed both the ProjectMember and Notification INSERTs atomically, causing a
CheckViolationError → 500. Added "project_invite", "project_invite_accepted",
"project_invite_rejected" to the model tuple and migration 060 drops/recreates the
constraint to include them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both endpoints accessed ORM object IDs after db.commit(), which
expires all loaded objects in async SQLAlchemy. Added db.flush()
before commit to assign IDs while objects are still live.
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>
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>
SQLAlchemy 2.0's select().union_all() returns a CompoundSelect which
cannot chain another .union_all(). Use the standalone union_all()
function to combine all three queries.
Co-Authored-By: Claude Opus 4.6 <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>
Invitees no longer see the event owner's calendar name/color,
preventing minor information disclosure (CWE-200).
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>
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>
Starred events should appear in the countdown widget regardless of how
far in the future they are. The _not_parent_template filter still
excludes recurring parent templates while allowing starred children.
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>
Migration 054: three indexes on calendar_events table:
- (calendar_id, start_datetime) for range queries
- (parent_event_id) for recurrence bulk operations
- (calendar_id, is_starred, start_datetime) for starred widget
Dashboard: replaced correlated subquery with single materialized
list fetch for user_calendar_ids in both /dashboard and /upcoming
handlers — eliminates 2 redundant subquery evaluations per request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Starred events query had no upper date bound — a starred recurring
event would fill all 5 countdown slots with successive occurrences
beyond the user's configured range. Now capped to upcoming_cutoff_dt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert hasSharingRef from useRef to useState in useCalendars so
refetchInterval reacts immediately when sharing is detected (P-01)
- Add extra="forbid" to ReorderItem schema to prevent mass-assignment (S-03)
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-1: Add composite index on calendar_members(user_id, status) for the
hot shared-calendar polling query
- AS-6: Add composite index on ntfy_sent(user_id, sent_at) for dedup lookups
- AW-5: Combine get_user_permission into single LEFT JOIN query instead of
2 sequential queries (called twice per event edit)
- AC-5: Batch cascade_on_disconnect — single GROUP BY + bulk UPDATE instead
of N per-calendar checks when a connection is severed
- AW-6: Collapse admin dashboard 5 COUNT queries into single conditional
aggregation using COUNT().filter()
- AC-3: Cache get_current_settings in request.state to avoid redundant
queries when multiple dependencies need settings in the same request
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AC-1: Merge get_current_user into single JOIN query (session + user in
one round-trip instead of two sequential queries per request)
- AC-2: Wrap all Argon2id hash/verify calls in run_in_executor to avoid
blocking the async event loop (~150ms per operation)
- AW-7: Add connection pool config (pool_size=10, pool_pre_ping=True,
pool_recycle=1800) to prevent connection exhaustion under load
- AC-4: Batch-fetch tasks in reorder_tasks with IN clause instead of
N sequential queries during Kanban drag operations
- AW-4: Bulk NtfySent inserts with single commit per user instead of
per-notification commits in the dispatch job
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gate dashboard rendering on isLockResolved to prevent content flash
before lock state is known. Remove animate-fade-in from LockOverlay
so it renders instantly. Always write accent color to localStorage
(even default cyan) to prevent theme flash on reload. Resolve lock
state on auth query error to avoid permanent blank screen. Lift
mobileOpen state above lock gate to survive lock/unlock cycles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical: Lock state was purely React useState — refreshing the page reset it.
Now persisted server-side via is_locked/locked_at columns on user_sessions.
POST /auth/lock sets the flag, /auth/verify-password clears it, and
GET /auth/status returns is_locked so the frontend initializes correctly.
UI: Cache accent color in localStorage and apply via inline script in
index.html before React hydrates to eliminate the cyan flash on load.
UI: Increase TanStack Query gcTime from 5min to 30min so page data
survives component unmount/remount across tab switches without skeleton.
UI: Move Projects nav onClick from the icon element to the full-width
container div so the entire row is clickable when the sidebar is collapsed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: Add 30-day lower bound on overdue todo/reminder queries to
prevent fetching entire history.
C-02: Remove dead include_past query param — past-event filtering
is handled client-side.
W-01: Add onError toast handlers to all three inline mutations.
W-02: Snooze dropdown opens upward (bottom-full) to avoid clipping
inside the ScrollArea overflow container.
S-06: Clamp getMinutesUntilTomorrowMorning() to max 1440 to stay
within ReminderSnooze schema bounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: Include overdue todos and snoozed reminders in /upcoming response,
add end_datetime/snoozed_until/is_overdue fields, widen snooze schema to
accept 1-1440 minutes for 1h/3h/tomorrow options.
Frontend: Full UpcomingWidget rewrite with sticky day separators (Today
highlighted in accent), collapsible groups, past-event toggle, focus mode
(Today + Tomorrow), color-coded left borders, compact type pills, relative
time for today's items, item count badge, and inline quick actions (complete
todo, snooze/dismiss reminder on hover). Card fills available height with
no dead space. DashboardPage always renders widget (no duplicate empty state).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: sync_birthday_to_contacts now accepts (share_birthday, date_of_birth)
directly — no internal re-query, no stale-read risk with autoflush.
W-01: Eliminated redundant User/Settings SELECTs inside the service.
W-02: Removed scalar_one() on User query (no longer queries internally).
W-03: Settings router only syncs when share_birthday value actually changes.
S-02: Added logger.info with rowcount for observability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user updates their date of birth or toggles share_birthday,
all linked Person records (where linked_user_id matches) are updated.
If share_birthday is off, the birthday is cleared on linked records.
Virtual birthday events auto-reflect the change on next calendar poll.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _build_member_response helper tried to access member.user.preferred_name
but User model has no preferred_name field (it's on Settings). With lazy="raise"
this caused a 500 on GET /shared-calendars/{id}/members. Reverted to None —
the list_members endpoint already patches preferred_name from Settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-03: invite_member now verifies the target user has a reciprocal
UserConnection row before sending the invite.
W-04: CalendarMember relationships changed from lazy="selectin" to
lazy="raise". All queries that access .user, .calendar, or .inviter
already use explicit selectinload() — verified across all routers
and services.
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>
SC-01: unlock_event now verifies caller has access to the calendar before
revealing lock state. Previously any authenticated user could probe event
existence via 404/204/403 response differences.
SC-02: acquire_lock no longer overwrites permanent locks. If the owner holds
a permanent lock and clicks Edit, the existing lock is returned as-is instead
of being downgraded to a 5-minute temporary lock.
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>
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>