443 Commits

Author SHA1 Message Date
fc1f8d5514 Fix passkey registration: use correct py_webauthn credential parsers
RegistrationCredential and AuthenticationCredential are plain dataclasses,
not Pydantic models — model_validate_json() does not exist on them.
Replace with parse_registration_credential_json() and
parse_authentication_credential_json() from webauthn.helpers, which
correctly parse the camelCase JSON from @simplewebauthn/browser and
convert base64url fields to bytes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:40:26 +08:00
57d400c6de Update .env.example and README.md for passkey authentication
- .env.example: Add WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, WEBAUTHN_ORIGIN,
  ENVIRONMENT, and UMBRA_URL with documentation comments
- README.md: Full rewrite — remove outdated PIN/bcrypt references, document
  current auth stack (Argon2id + TOTP + passkeys), all 17 API route groups,
  security features, and Docker deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:25:27 +08:00
9234880648 Fix SyntaxError: reorder delete_passkey params
Move `request: Request` (no default) before parameters with defaults
to fix 'parameter without default follows parameter with default'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:05:31 +08:00
53101d1401 Action deferred review items: TOTP lockout consolidation + toast nav
W-04: Replace inline lockout logic in totp.py (3 occurrences of
manual failed_login_count/locked_until manipulation) with shared
session service calls: check_account_lockout, record_failed_login,
record_successful_login. Also fix TOTP replay prevention to use
flush() not commit() for atomicity with session creation.

S-1: Add "Set up" action button to the post-login passkey prompt
toast, navigating to /settings?tab=security (already supported by
SettingsPage search params).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:02:59 +08:00
ab84c7bc53 Fix review findings: transaction atomicity, perf, and UI polish
Backend fixes:
- session.py: record_failed/successful_login use flush() not commit()
  — callers own transaction boundary (BUG-2 atomicity fix)
- auth.py: Add explicit commits after record_failed_login where callers
  raise immediately; add commit before TOTP mfa_token return path
- passkeys.py: JOIN credential+user lookup in login/complete (W-1 perf)
- passkeys.py: Move mfa_enforce_pending clear before main commit (S-2)
- passkeys.py: Add Path(ge=1, le=2147483647) on DELETE endpoint (BUG-3)
- auth.py: Switch has_passkeys from COUNT to EXISTS with LIMIT 1 (W-2)
- passkey.py: Add single-worker nonce cache comment (H-1)

Frontend fixes:
- PasskeySection: emerald→green badge colors (W-3 palette)
- PasskeySection: text-[11px]/text-[10px]→text-xs (W-4 a11y minimum)
- PasskeySection: Scope deleteMutation.isPending to per-item (W-5)
- nginx.conf: Permissions-Policy publickey-credentials use (self) (H-2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:59:59 +08:00
51d98173a6 Phase 3: Post-login passkey prompt toast
Show a one-time toast suggesting passkey setup after login when:
- User has no passkeys registered
- Browser supports WebAuthn
- Prompt hasn't been shown this session (sessionStorage gate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:51:06 +08:00
cc460df5d4 Phase 2: Add passkey frontend UI
New files:
- PasskeySection.tsx: Passkey management in Settings > Security with
  registration ceremony (password -> browser prompt -> name), credential
  list, two-click delete with password confirmation

Changes:
- types/index.ts: PasskeyCredential type, has_passkeys on AuthStatus
- api.ts: 401 interceptor exclusions for passkey login endpoints
- useAuth.ts: passkeyLoginMutation with dynamic import of
  @simplewebauthn/browser (~45KB saved from initial bundle)
- LockScreen.tsx: "Sign in with a passkey" button (browser feature
  detection, not per-user), Fingerprint icon, error handling
- SecurityTab.tsx: PasskeySection between Auto-lock and TOTP
- package.json: Add @simplewebauthn/browser ^10.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:50:06 +08:00
e8e3f62ff8 Phase 1: Add passkey (WebAuthn/FIDO2) backend
New files:
- models/passkey_credential.py: PasskeyCredential model with indexed credential_id
- alembic 061: Create passkey_credentials table
- services/passkey.py: Challenge token management (itsdangerous + nonce replay
  protection) and py_webauthn wrappers for registration/authentication
- routers/passkeys.py: 6 endpoints (register begin/complete, login begin/complete,
  list, delete) with full security hardening

Changes:
- config.py: WEBAUTHN_RP_ID, RP_NAME, ORIGIN, CHALLENGE_TTL settings
- main.py: Mount passkey router, add CSRF exemptions for login endpoints
- auth.py: Add has_passkeys to /auth/status response
- nginx.conf: Rate limiting on all passkey endpoints, Permissions-Policy
  updated for publickey-credentials-get/create
- requirements.txt: Add webauthn>=2.1.0

Security: password re-entry for registration (V-02), single-use nonce
challenges (V-01), constant-time login/begin (V-03), shared lockout
counter, generic 401 errors, audit logging on all events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:46:00 +08:00
eebb34aa77 Phase 0: Consolidate session creation into shared service
Extract _create_db_session, _set_session_cookie, _check_account_lockout,
_record_failed_login, and _record_successful_login from auth.py into
services/session.py. Update totp.py to use shared service instead of
its duplicate _create_full_session (which lacked session cap enforcement).

Also fixes:
- auth/status N+1 query (2 sequential queries -> single JOIN)
- Rename verify_password route to verify_password_endpoint (shadow fix)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:40:46 +08:00
c5a309f4a1 Merge feature/mini-calendar: compact date navigator in sidebar
Adds MiniCalendar component with independent month browsing, click-to-navigate,
today/selected highlights, firstDayOfWeek support, navKey selection clearing,
aria-labels, and mobile sheet auto-close. QA reviewed — 0 critical findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:12:50 +08:00
0ba920f8e1 Fix issues from QA review: stale closure, aria-labels, mobile sheet close
W-01: Use functional updater in handleDayClick to remove displayedMonth
      from dependency array, eliminating stale closure risk
S-02: Add aria-label with full date string to day buttons for screen readers
S-04: Close mobile sidebar sheet when clicking a date in mini calendar,
      matching existing onUseTemplate behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:12:16 +08:00
68337b12a0 Fix: clear mini-cal selection on Today click even when month unchanged
datesSet fires but currentDate stays the same value when already on
the current month, so the useEffect didn't re-run. Added navKey counter
that increments on every datesSet call — MiniCalendar watches it in a
separate useEffect to reliably clear selectedDate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:14:00 +08:00
bda02039a6 Clear mini calendar selection on main calendar navigation
Clicking Today/prev/next on the toolbar now clears the selected day
in the mini calendar, so only the today highlight remains visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:08:56 +08:00
2d76ecf869 Fix 1st-of-month highlight bug and restore Calendars header
selectedDate now only set by user clicks in mini calendar, not by
external currentDate prop (which is always 1st of displayed month
from FullCalendar's view.currentStart). Restore h-16 "Calendars"
header above mini calendar for consistent top-of-page alignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 20:03:22 +08:00
b939843249 Fix review findings: safe date parsing, useCallback discipline, dead class cleanup
W-01: Wrap handlePrev/handleNext/handleDayClick in useCallback
W-02: Use date-fns parse() instead of new Date() for timezone-safe parsing
W-03: Change default firstDayOfWeek from 1 to 0 to match CalendarPage
S-01: Use format(day, 'yyyy-MM-dd') as React key instead of toISOString()
S-02: Remove dead Tailwind color classes overridden by inline styles
Perf: Guard setSelectedDate with comparison to skip no-op re-renders
Perf: Memoize selectedDateObj via useMemo to avoid re-parsing each render

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:48:32 +08:00
a5ac047b0b Add mini monthly calendar to sidebar for quick date navigation
New MiniCalendar component with independent month browsing, today/selected
highlights, firstDayOfWeek support, and month sync with main calendar.
Replaces old "Calendars" header with the mini-cal + "MY CALENDARS" heading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:42:00 +08:00
1daec977ba Merge feature/event-panel-ux: scroll bleed fix, auto-grow description, compact layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:14:52 +08:00
bb39888d2e Fix issues from QA review: invited editor payload, auto-resize perf, resize-y conflict
C-01: Strip is_starred/recurrence_rule from payload for invited editors
      (not in backend allowlist → would 403). Hide Star checkbox from
      invited editor edit mode entirely.

W-01: Wrap auto-resize in requestAnimationFrame to batch with paint
      cycle and avoid forced reflow on every keystroke.

S-01: Add comment documenting belt-and-suspenders scroll prevention.

S-02: Remove resize-y from textarea (conflicts with auto-grow which
      resets height on keystroke, overriding manual resize).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:13:31 +08:00
43322db5ff Disable month-scroll wheel navigation when event panel is open
Prevents accidental month changes (and lost edits) while scrolling
anywhere on the calendar page with the detail panel visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:02:30 +08:00
11f42ef91e Fix description textarea resize: remove max-height cap blocking drag
max-h-[200px] CSS and the 200px JS cap both prevented the resize
handle from expanding the textarea. Removed both constraints so
auto-grow and manual resize work without ceiling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 18:58:19 +08:00
a78fb495a2 Improve event panel UX: fix scroll bleed, auto-grow description, compact layout
P0 - Scroll bleed: onWheel stopPropagation on panel root prevents
     wheel events from navigating calendar months while editing.

P1 - Description textarea: auto-grows with content (min 80px, max
     200px), manually resizable via resize-y handle. Applied to both
     EventDetailPanel and EventForm.

P2 - Space utilization: moved All Day checkbox inline above date row,
     combined Recurrence + Star into a 2-col row, description now
     fills remaining vertical space with flex-1.

P3 - Removed duplicate footer Save/Cancel buttons from edit mode
     (header icon buttons are sufficient).

P4 - Description field now shows dash placeholder in view mode when
     empty, consistent with other fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 18:46:40 +08:00
80418172db Rework Nominatim results: name as label, address as full street address
Name now uses the Nominatim place/building label (e.g. "The Quadrant")
when available, falling back to street address. Address field now
contains the full formatted address (house number, road, suburb, city,
state, postcode, country) instead of just the city/state portion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:58:20 +08:00
d4117818c7 Preserve house number from user query when Nominatim omits it
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>
2026-03-17 14:52:46 +08:00
90bfd00a82 Fix Nominatim stripping house numbers from location names
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>
2026-03-17 13:28:02 +08:00
a5118e36dc Add purple ghost favicon for browser tabs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:09:57 +08:00
e95931fa62 Fix read-only banner showing for editor members
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>
2026-03-17 08:03:19 +08:00
03d0742dc4 Fix task/project deletion broken by lazy='raise' on cascade relationships
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>
2026-03-17 07:51:26 +08:00
bb4212d17f Fix TS2345: add missing version to subtask toggle mutation call
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 05:34:05 +08:00
688ce1c132 Merge feature/project-collab-prep: collaborative project sharing, task assignments, delta polling
Adds multi-user project collaboration mirroring the shared calendar pattern:
- ProjectMember model with invite/accept/reject flow, permission levels (read_only/create_modify)
- ProjectTaskAssignment with multi-assign, auto-membership, field allowlist (SEC-P02)
- Optimistic locking via version column with 409 conflict handling
- Delta polling for projects and calendars (5s interval, background tab support)
- Disconnect cascade cleans up memberships + assignments on connection removal
- Frontend: ProjectShareSheet, AssignmentPicker, permission gating, assigned column in task list
- Notification integration: project_invite, project_invite_accepted, task_assigned with action toasts
- Kanban DragOverlay for smooth drag-and-drop
- 4 migrations (057-060), 31 files, ~2500 LOC
- QA: 3 agent reviews (performance, pentest, code), all findings actioned
2026-03-17 05:30:16 +08:00
0a449f166c Polish pass: action all remaining QA suggestions before merge
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>
2026-03-17 05:28:34 +08:00
dd637bdc84 Fix QA findings from performance, pentest, and code review
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>
2026-03-17 04:55:47 +08:00
e0a5f4855f Fix Kanban drag jitter: use DragOverlay + ghost placeholder
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>
2026-03-17 04:37:56 +08:00
7eac213c20 Wire AssignmentPicker into TaskDetailPanel for task assignment
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>
2026-03-17 04:31:39 +08:00
957939a165 Remove unused people query and Person import from TaskDetailPanel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 04:26:50 +08:00
fc2068be70 Remove unused assignedPerson variable (TS6133 build error)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 04:25:11 +08:00
d6e4938aa4 Fix task assignment visibility: show column always, wire detail panel
- 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>
2026-03-17 04:23:20 +08:00
990c660fbf Add assigned column to task list with name labels, fix user_name null
- 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>
2026-03-17 04:14:16 +08:00
f42175b3fe Improve sharing visibility: member count on cards, task assignment toast
- 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>
2026-03-17 04:09:07 +08:00
61e48c3f14 Add project notification types to CHECK constraint (migration 060)
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>
2026-03-17 03:54:54 +08:00
05f5b49e26 Fix 500 on POST /api/projects/:id/members — add project_invite types to notification CHECK constraint
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>
2026-03-17 03:53:42 +08:00
f0850ad3bf Fix MissingGreenlet in invite_members and assign_users_to_task
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>
2026-03-17 03:49:28 +08:00
a7e93aa2a3 Fix migration 057: use IF NOT EXISTS for indexes that may pre-exist
The ix_project_tasks_parent_task_id index already existed on the
production DB, causing migration 057 to fail with DuplicateTableError.
Switched all CREATE INDEX statements to raw SQL with IF NOT EXISTS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 03:30:19 +08:00
dad5c0e606 Fix QA findings: project invite toast with action buttons, rejected row cleanup
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>
2026-03-17 03:25:17 +08:00
bef856fd15 Add collaborative project sharing, task assignments, and delta polling
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>
2026-03-17 03:18:35 +08:00
7903e454dc Strip detailed security internals from README
Reduces the security section to a brief summary without exposing
specific middleware names, rate limit thresholds, lockout parameters,
or implementation details that could aid threat actors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:43:39 +08:00
2848739474 Update README to reflect event invitations, shared calendars, and current state
Updates tech stack counts (56 migrations, 20 models, 14 schemas, 16
routers), adds event invitations and shared calendars to features list,
API overview, security section, and project structure. Reflects 5
completed penetration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:39:54 +08:00
cbb62ea7aa Merge feature/event-invitations: full event invitation system
Adds event invitations with RSVP, per-occurrence overrides for recurring
events, display calendar assignment, can_modify toggle for granting
invitees edit access, active-invitee icon on owner's calendar, and
in-app notification integration. Three QA reviews and two penetration
tests passed. Includes field allowlist for invited editors, connection
validation, 20-invitation cap, and can_modify reset on decline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:33:06 +08:00
925c9caf91 Fix QA and pentest findings for event invitations
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>
2026-03-17 01:28:01 +08:00
2f45220c5d Show shared-invitee icon on owner's calendar for events with active guests
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>
2026-03-17 01:14:44 +08:00
c66fd159ea Restore 5s calendar polling for near-real-time shared event sync
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>
2026-03-17 01:09:34 +08:00