73 Commits

Author SHA1 Message Date
02efe04fc4 Fix QA critical/warning findings on profile feature
C-01: Replace setattr loop with explicit field assignment in update_profile
C-02: Fix useEffect dependency to profileQuery.dataUpdatedAt for re-sync
W-01: Add audit log entry for profile updates
W-02: Use less misleading generic error for email uniqueness on registration
W-03: Early return on empty PUT body to avoid unnecessary commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:09:15 +08:00
45f3788fb0 Add preferred name + email to registration, profile card to settings
Registration form now collects optional preferred_name and email fields.
Settings page Profile card expanded with first name, last name, and email
(editable via new GET/PUT /api/auth/profile endpoints). Email uniqueness
enforced on both registration and profile update. No migrations needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:02:42 +08:00
ab7e4a7c7e Backend pentest remediation (PT-03/05/06/07)
PT-03: Make UMBRA_URL configurable via env var (default http://localhost).
Replaces hardcoded http://10.0.69.35 in notification dispatch job and
ntfy test endpoint. Add UMBRA_URL to .env.example.

PT-05: Add explicit path="/" to session cookie for clarity.

PT-06: Add concurrent session limit (MAX_SESSIONS_PER_USER, default 10).
When exceeded, oldest sessions are revoked. New login always succeeds.

PT-07: Escape LIKE metacharacters (%, _) in admin audit log action
filter to prevent wildcard abuse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:43:27 +08:00
21aa670a39 Extract real client IP from proxy headers instead of Docker bridge IP
Nginx already forwards X-Forwarded-For and X-Real-IP, but the backend
read request.client.host directly — always returning 172.18.0.x. Added
get_client_ip() helper to audit service; updated all 13 call sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:20:07 +08:00
d269742aa2 Fix pentest findings: setup 500 error + password reuse prevention
- L-01: Setup endpoint used scalar_one_or_none() on unbounded User
  query, throwing 500 MultipleResultsFound when >1 user exists.
  Replaced with select(func.count()) for a reliable check.
- L-02: Change-password allowed reusing the same password, defeating
  must_change_password enforcement. Added equality check before
  accepting the new password.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 02:42:00 +08:00
1aeb725410 Fix issues from QA review: hash upgrade ordering, interceptor scope, guard
- W-01: Move is_active check before hash upgrade so disabled accounts
  don't get their password hash silently mutated on rejected login
- W-02: Narrow interceptor exclusion to specific auth endpoints instead
  of blanket /auth/* prefix (future-proofs against new auth routes)
- W-03: Add null guard on optimistic setQueryData to handle undefined
  cache gracefully instead of spreading undefined
- S-01: Clear loginError when switching from register back to login mode
- S-03: Add detail dict to auth.login_blocked_inactive audit event

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 02:13:48 +08:00
b2d81f7015 Block inactive user login + fix login flicker + inline error alerts
- Backend: reject is_active=False users with HTTP 403 after password
  verification but before session creation (prevents last_login_at
  update, lockout reset, and MFA token issuance for disabled accounts)
- Frontend: optimistic setQueryData on successful login eliminates the
  form flash between mutation success and auth query refetch
- LockScreen: replace lockoutMessage + toast.error with unified
  loginError inline alert for 401/403/423 responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:21:06 +08:00
8582b41b03 Add user profile fields + IAM search, email column, detail panel
Backend:
- Migration 037: add email, first_name, last_name to users table
- User model: add 3 profile columns
- Admin schemas: extend UserListItem/UserDetailResponse/CreateUserRequest
  with profile fields, email validator, name field sanitization
- _create_user_defaults: accept optional preferred_name kwarg
- POST /users: set profile fields, email uniqueness check, IntegrityError guard
- GET /users/{id}: join Settings for preferred_name, include must_change_password/locked_until

Frontend:
- AdminUser/AdminUserDetail types: add profile + detail fields
- useAdmin: add CreateUserPayload profile fields + useAdminUserDetail query
- CreateUserDialog: optional profile section (first/last name, email, preferred name)
- IAMPage: search bar filtering on username/email/name, email column in table,
  row click to select user with accent highlight
- UserDetailSection: two-column detail panel (User Info + Security & Permissions)
  with inline role editing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:40:20 +08:00
c3654dc069 Fix audit log target for deleted users + create user 500 error
1. Audit log: COALESCE target_username with detail JSON fallback so
   deleted users still show their username in the target column
   (target_user_id is SET NULL by FK cascade, but detail JSON
   preserves the username).

2. Create/get user: add exclude={"active_sessions"} to model_dump()
   calls — UserListItem defaults active_sessions=0, so model_dump()
   includes it, then the explicit active_sessions=N keyword argument
   causes a duplicate keyword TypeError. DB commit already happened,
   so the user exists but the response was a 500.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:44:10 +08:00
48e15fa677 UX polish for delete-user: username toast, hide self-delete
S-03: Delete toast now shows the deleted username from the API response
S-04: Delete button hidden for the current admin's own row (backend
still guards with 403, but no reason to show a dead button)

Adds username to auth status response so the frontend can identify
the current user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:30:43 +08:00
e7cb6de7d5 Add admin delete-user with full cascade cleanup
Migration 036 adds ondelete rules to 5 transitive FKs that would
otherwise block user deletion (calendar_events via calendars,
project_tasks via projects, todos via projects, etc.).

DELETE /api/admin/users/{user_id} with self-action guard, last-admin
guard, session revocation, and audit logging. Frontend gets a red
two-click confirm button in the IAM actions menu.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 19:20:47 +08:00
c56830ddb0 Fix SyntaxError in admin.py: add default to Request params
Three functions (update_user_role, toggle_mfa_enforce, toggle_user_active)
had `request: Request` without a default after Path params with defaults,
causing Python SyntaxError on import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:31:25 +08:00
1ebc41b9d7 L-03: Session 7-day sliding window with 30-day hard ceiling
Reduce session expiry from 30 days to 7 days of inactivity while
preserving a 30-day absolute token lifetime for itsdangerous:

- SESSION_MAX_AGE_DAYS=7: sliding window for DB expires_at + cookie
- SESSION_TOKEN_HARD_CEILING_DAYS=30: itsdangerous max_age (prevents
  rejecting renewed tokens whose creation timestamp exceeds 7 days)
- get_current_user: silently extends expires_at and re-issues cookie
  when >1 day has elapsed since last renewal
- Active users never notice; 7 days of inactivity forces re-login;
  30-day absolute ceiling forces re-login regardless of activity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:45:15 +08:00
8e27f2920b M-02: Timing-safe login prevents username enumeration
When a login targets a non-existent username, run Argon2id against a
pre-computed dummy hash so response time (~60ms) matches wrong-password
attempts. Also reorder the login flow to run verify_password_with_upgrade
BEFORE the lockout check, preventing timing side-channels that could
distinguish locked accounts from wrong passwords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:44:27 +08:00
2f58282c31 M-01+M-03: Add input validation and extra=forbid to all request schemas
- Add max_length constraints to all string fields in request schemas,
  matching DB column limits (title:255, description:5000, etc.)
- Add min_length=1 to required name/title fields
- Add ConfigDict(extra="forbid") to all request schemas to reject
  unknown fields (prevents silent field injection)
- Add Path(ge=1, le=2147483647) to all integer path parameters across
  all routers to prevent integer overflow → 500 errors
- Add max_length to TOTP inline schemas (code:6, mfa_token:256, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:43:55 +08:00
581efa183a H-01: Add global CSRF middleware for all mutating endpoints
Replace the admin-only verify_xhr dependency with a pure ASGI
CSRFHeaderMiddleware that validates X-Requested-With: XMLHttpRequest
on all POST/PUT/PATCH/DELETE requests globally. Pre-auth endpoints
(login, setup, register, totp-verify, enforce-setup/confirm) are
exempt. This closes the CSRF gap where non-admin routes accepted
requests without origin validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:39:42 +08:00
9f7bbbfcbb Add per-user active session counts to IAM user list
Move active_sessions field from UserDetailResponse into UserListItem
so GET /admin/users returns session counts. Uses a correlated subquery
to count non-revoked, non-expired sessions per user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:26:32 +08:00
619e220622 Fix QA review #2: W-03/W-04, S-01 through S-04
W-03: Unify split transactions — _create_db_session() now uses flush()
      instead of commit(), callers own the final commit.
W-04: Time-bound dedup key fetch to 7-day purge window.
S-01: Type admin dashboard response with RecentLoginItem/RecentAuditItem.
S-02: Convert starred events index to partial index WHERE is_starred = true.
S-03: EventTemplate.created_at default changed to func.now() for consistency.
S-04: Add single-worker scaling note to weather cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 05:41:16 +08:00
cbf4663e8d Fix TS build errors and apply remaining QA fixes
Remove unused imports (UserCheck, Loader2, ShieldOff) and replace
non-existent SmartphoneOff icon with Smartphone in admin components.
Includes backend query fixes, performance indexes migration, and
admin page shared utilities extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 04:42:23 +08:00
e57a5b00c9 Fix QA review findings: C-01 through C-04, W-01 through W-07, S-01/S-04/S-05/S-06
Critical fixes:
- C-01: Pass user_id to _mark_sent/_already_sent (ntfy crash)
- C-02: Align frontend HTTP methods with backend routes (PATCH->PUT,
  DELETE->POST, fix reset-password/enforce-mfa/disable-mfa paths)
- C-03: Add X-Requested-With to CORS allow_headers
- C-04: Replace scalar_one_or_none with func.count for auth/status

Warning fixes:
- W-01: Batch audit log into same transaction in create_user, setup, register
- W-02: Extract users array from UserListResponse wrapper in useAdminUsers
- W-03: Update password hint from "8 chars" to "12 chars" in CreateUserDialog
- W-04: Remove password input from reset flow, show returned temp password
- W-06: Remove unused actor_alias variable in admin_dashboard
- W-07: Resolve usernames in dashboard audit entries via JOIN, remove
  ip_address column from recent_logins (not tracked on User model)

Suggestions applied:
- S-01/S-06: Add extra="forbid" to all admin mutation schemas
- S-04: Add ondelete="SET NULL" to audit_log.actor_user_id FK
- S-05: Improve registration error message for better UX

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:19:04 +08:00
d8bdae8ec3 Implement multi-user RBAC: database, auth, routing, admin API (Phases 1-6)
Phase 1: Add role, mfa_enforce_pending, must_change_password to users table.
Create system_config (singleton) and audit_log tables. Migration 026.

Phase 2: Add user_id FK to all 8 data tables (todos, reminders, projects,
calendars, people, locations, event_templates, ntfy_sent) with 4-step
nullable→backfill→FK→NOT NULL pattern. Migrations 027-034.

Phase 3: Harden auth schemas (extra="forbid" on RegisterRequest), add
MFA enforcement token serializer with distinct salt, rewrite auth router
with require_role() factory and registration endpoint.

Phase 4: Scope all 12 routers by user_id, fix dependency type bugs,
bound weather cache (SEC-15), multi-user ntfy dispatch.

Phase 5: Create admin router (14 endpoints), admin schemas, audit
service, rate limiting in nginx. SEC-08 CSRF via X-Requested-With.

Phase 6: Update frontend types, useAuth hook (role/isAdmin/register),
App.tsx (AdminRoute guard), Sidebar (admin link), api.ts (XHR header).

Security findings addressed: SEC-01, SEC-02, SEC-03, SEC-04, SEC-05,
SEC-06, SEC-07, SEC-08, SEC-12, SEC-13, SEC-15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:06:25 +08:00
464b8b911f Phase 8: Registration flow & MFA enforcement UI
- backend: add POST /auth/totp/enforce-setup and /auth/totp/enforce-confirm
  endpoints that operate on mfa_enforce_token (not session cookie), generate
  TOTP secret/QR/backup codes, verify confirmation code, enable TOTP, clear
  mfa_enforce_pending flag, and issue a full session cookie
- frontend: expand LockScreen to five modes — login, first-run setup,
  open registration, TOTP challenge, MFA enforcement setup (QR -> verify ->
  backup codes), and forced password change; all modes share AmbientBackground
  and the existing card layout; registration visible only when
  authStatus.registration_open is true

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:39:18 +08:00
a0b50a2b13 Remediate pentest findings F-01, F-02, F-06
- Remove ineffective in-memory IP rate limiter from auth.py (F-01):
  nginx limit_req_zone handles real-IP throttling, DB lockout is the per-user guard
- Block RFC 1918 + IPv6 ULA ranges in ntfy SSRF guard (F-02):
  prevents requests to Docker-internal services via user-controlled ntfy URL
- Rate-limit /api/auth/setup at nginx with burst=3 (F-06)
- Document production deployment checklist in .env.example (F-03/F-04/F-05)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:25:37 +08:00
72075b7b71 Address QA review warnings for pentest remediation
- Clear IP failure counter on successful /change-password (W-01)
- Add nginx rate limiting for /api/auth/totp-verify (W-02)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:15:21 +08:00
f372e7bd99 Harden infrastructure: address 7 pentest findings
- Remove external backend port 8000 exposure (VULN-01)
- Conditionally disable Swagger/ReDoc/OpenAPI in non-dev (VULN-01)
- Suppress nginx and uvicorn server version headers (VULN-07)
- Fix CSP to allow Google Fonts (fonts.googleapis.com/gstatic) (VULN-08)
- Add nginx rate limiting on auth endpoints (10r/m burst=5) (VULN-09)
- Block dotfile access (/.env, /.git) while preserving .well-known (VULN-10)
- Make CORS origins configurable, tighten methods/headers (VULN-11)
- Run both containers as non-root users (VULN-12)
- Add IP rate limit + account lockout to /change-password

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:57:29 +08:00
5ad0a610bd Address all QA review warnings and suggestions for lock screen feature
- [C-1] Add rate limiting and account lockout to /verify-password endpoint
- [W-3] Add max length validator (128 chars) to VerifyPasswordRequest
- [W-1] Move activeMutations to ref in useLock to prevent timer thrashing
- [W-5] Add user_id field to frontend Settings interface
- [S-1] Export auth schemas from schemas registry
- [S-3] Add aria-label to LockOverlay password input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:20:42 +08:00
b0af07c270 Add lock screen, auto-lock timeout, and login visual upgrade
- Backend: POST /verify-password endpoint for lock screen re-auth,
  auto_lock_enabled/auto_lock_minutes columns on Settings with migration 025
- Frontend: LockProvider context with idle detection (throttled activity
  listeners, pauses during mutations), Lock button in sidebar, full-screen
  LockOverlay with password re-entry and "Switch account" option
- Settings: Security card with auto-lock toggle and configurable timeout (1-60 min)
- Visual: Upgraded login screen with large title, animated floating gradient
  orbs (3 drift keyframes), subtle grid overlay, shared AmbientBackground component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:03:12 +08:00
6094561d74 Fix 500 on settings update: include user_id in explicit SettingsResponse constructor
The W9 fix added user_id to SettingsResponse but missed the manual
_to_settings_response() builder, causing Pydantic validation failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 08:32:52 +08:00
4a98b67b0b Address all QA review warnings and suggestions for entity pages
- W1: Add ntfy_has_token property to Settings model for safe from_attributes usage
- W2: Eager-load event location and pass location_name to ntfy template builder
- W3: Add missing accent color swatches (red, pink, yellow) to match backend Literal
- W7: Cap IP rate-limit dict at 10k entries with stale-entry purge to prevent OOM
- W9: Include user_id in SettingsResponse for multi-user readiness

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:48:45 +08:00
15c99152d3 Address QA review: model registry, NOT NULL constraint, variable naming, toggle defaults, lockout UX
- C3: Register User, UserSession, NtfySent, TOTPUsage, BackupCode in models/__init__.py
- C4: Enforce settings.user_id NOT NULL after backfill in migration 023, update model
- W4: Rename misleading current_user → current_settings in dashboard.py
- W5: Match NtfySettingsSection initial state defaults to backend (true/1/2)
- W8: Clear lockout banner on username/password input change in LockScreen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:34:21 +08:00
b134ad9e8b Implement Stage 6 Track B: TOTP MFA (pyotp, Fernet-encrypted secrets, backup codes)
- models/totp_usage.py: replay-prevention table, unique on (user_id, code, window)
- models/backup_code.py: Argon2id-hashed recovery codes with used_at tracking
- services/totp.py: Fernet encrypt/decrypt, verify_totp_code returns actual window, QR base64, backup code generation
- routers/totp.py: setup (idempotent), confirm, totp-verify (mfa_token + TOTP or backup code), disable, regenerate, status
- alembic/024: creates totp_usage and backup_codes tables
- main.py: register totp router, import new models for Alembic discovery
- requirements.txt: add pyotp>=2.9.0, qrcode[pil]>=7.4.0, cryptography>=42.0.0
- jobs/notifications.py: periodic cleanup for totp_usage (5 min) and expired user_sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:18:05 +08:00
fbc452a004 Implement Stage 6 Track A: PIN → Username/Password auth migration
- New User model (username, argon2id password_hash, totp fields, lockout)
- New UserSession model (DB-backed revocation, replaces in-memory set)
- New services/auth.py: Argon2id hashing, bcrypt→Argon2id upgrade path, URLSafeTimedSerializer session/MFA tokens
- New schemas/auth.py: SetupRequest, LoginRequest, ChangePasswordRequest with OWASP password strength validation
- Full rewrite of routers/auth.py: setup/login/logout/status/change-password with account lockout (10 failures → 30-min, HTTP 423), IP rate limiting retained as outer layer, get_current_user + get_current_settings dependencies replacing get_current_session
- Settings model: drop pin_hash, add user_id FK (nullable for migration)
- Schemas/settings.py: remove SettingsCreate, ChangePinRequest, _validate_pin_length
- Settings router: rewrite to use get_current_user + get_current_settings, preserve ntfy test endpoint
- All 11 consumer routers updated: auth-gate-only routers use get_current_user, routers reading Settings fields use get_current_settings
- config.py: add SESSION_MAX_AGE_DAYS, MFA_TOKEN_MAX_AGE_SECONDS, TOTP_ISSUER
- main.py: import User and UserSession models for Alembic discovery
- requirements.txt: add argon2-cffi>=23.1.0
- Migration 023: create users + user_sessions tables, migrate pin_hash → User row (admin), backfill settings.user_id, drop pin_hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:12:37 +08:00
67456c78dd Implement Track C: NTFY push notification integration
- Add ntfy columns to Settings model (server_url, topic, auth_token, enabled, per-type toggles, lead times)
- Create NtfySent dedup model to prevent duplicate notifications
- Create ntfy service with SSRF validation and async httpx send
- Create ntfy_templates service with per-type payload builders
- Create APScheduler background dispatch job (60s interval, events/reminders/todos/projects)
- Register scheduler in main.py lifespan with max_instances=1
- Update SettingsUpdate with ntfy validators (URL scheme, topic regex, lead time ranges)
- Update SettingsResponse with ntfy fields; ntfy_has_token computed, token never exposed
- Add POST /api/settings/ntfy/test endpoint
- Update GET/PUT settings to use explicit _to_settings_response() helper
- Add Alembic migration 022 for ntfy settings columns + ntfy_sent table
- Add httpx==0.27.2 and apscheduler==3.10.4 to requirements.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:04:23 +08:00
1806e15487 Address all QA review warnings and suggestions for entity pages
Warnings fixed:
- 3.1: _compute_display_name stale-data bug on all-names-clear
- 3.3: Location getValue unsafe type cast replaced with typed helper
- 3.5: Explicit updated_at timestamp refresh in locations router
- 3.6: Drop deprecated relationship column (migration 021, model, schema, TS type)

Suggestions fixed:
- 4.1: CategoryAutocomplete keyboard navigation (ArrowUp/Down, Enter, Escape)
- 4.2: Mobile detail panel backdrop click-to-close on both pages
- 4.3: PersonCreate whitespace bypass in require_some_name validator
- 4.5/4.6: Extract SortIcon, DataRow, SectionHeader from EntityTable render body
- 4.8: PersonForm sends null instead of empty string for birthday
- 4.10: Remove unnecessary executeDelete wrapper in EntityDetailPanel

Also includes previously completed fixes from prior session:
- 2.1: Remove Z suffix from naive timestamp in formatUpdatedAt
- 3.2: Drag-then-click conflict prevention in SortableCategoryChip
- 3.4: localStorage JSON shape validation in useCategoryOrder
- 4.4: Category chip styling consistency (both pages use inline hsl styles)
- 4.9: restrictToHorizontalAxis modifier on CategoryFilterBar drag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:04:20 +08:00
8cbc95939a Fix issues from QA review: schema safety, search scope, clipboard handling, UX polish
- Remove `name` from PersonUpdate schema (always computed, prevents bypass)
- Auto-split legacy `name` into first/last on create when only name provided
- Expand backend search to cover first_name, last_name, nickname, email, company
- Add server_default=text('false') to is_favourite and is_frequent model columns
- Add .catch() to clipboard API call in CopyableField
- Extract duplicate renderHeader into shared function in PeoplePage
- Add Escape key handler to close mobile detail panel overlays
- Extract calculate() out of useTableVisibility effects to single function
- Guard getInitials against empty string input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:19:22 +08:00
cb9f74a387 Entity pages enhancement: backend model extensions, shared components, Locations rebuild, panel animations
- Add migrations 019/020: extend Person (first/last name, nickname, is_favourite, company, job_title, mobile, category) and Location (is_frequent, contact_number, email)
- Update Person/Location models, schemas, and routers with new fields + name denormalisation
- Create shared component library: EntityTable, EntityDetailPanel, CategoryFilterBar, CopyableField, CategoryAutocomplete, useTableVisibility hook
- Rebuild LocationsPage: table layout with sortable columns, detail side panel, category filter bar, frequent pinned section
- Extend LocationForm with contact number, email, frequent toggle, category autocomplete
- Add animated panel transitions to ProjectDetail (55/45 split with cubic-bezier easing)
- Update TypeScript interfaces for Person and Location

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:10:26 +08:00
17f331477f Fix QA review issues: error handlers, validation, accessibility, cleanup
- C1: Add onError handlers to dismiss/snooze mutations in useAlerts
- C2: Clear snoozed_until when dismissing via update endpoint
- W1: Handle future dates in getRelativeTime
- W2+S3: Add Escape key, aria-expanded, role=menu to SnoozeDropdown
- W4: Remove redundant field_validator (Literal suffices)
- W7: Validate recurrence_rule with Literal['daily','weekly','monthly']
- S2: Clean up delete confirmation setTimeout on unmount
- S6: Cap AlertBanner height with scroll for many alerts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:52:28 +08:00
6cd5b71d1c Add Dismiss label to toast button, reactivate on remind_at change
- Toast dismiss button now shows X icon + 'Dismiss' text to match
  the snooze button style
- Updating remind_at on a dismissed reminder clears is_dismissed
  and snoozed_until, making the reminder active again

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:31:29 +08:00
daf2a4d5f1 Fix snooze/due using container UTC instead of client local time
Docker container datetime.now() returns UTC, but all stored datetimes
are naive local time from the browser. Both /due and /snooze now
accept client_now from the frontend, ensuring snooze computes from
the user's actual current time, not the container's clock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:12:31 +08:00
b7251b72c7 Address remaining QA items: index, validation, accessibility, guard
- S1: Add composite index (is_active, is_dismissed, remind_at) for
  /due query performance with multi-user scaling
- W3: Snooze endpoint rejects dismissed/inactive reminders (409)
- W4: Custom field_validator on ReminderSnooze for clear error message
- S2: aria-label on all snooze/dismiss buttons in banner and toasts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:02:19 +08:00
5b1b9cc5b7 Fix QA issues: single AlertsProvider, null safety, snooze cleanup
- C1: Replaced duplicate useAlerts() calls with AlertsProvider context
  wrapping AppLayout — single hook instance, no double polling/toasts
- C2: Added null guard on remind_at in Active Reminders card format()
- W2: Clear snoozed_until when dismissing a reminder
- W5: Extracted getRelativeTime to shared lib/date-utils.ts
- S3: Replaced inline SVG with Lucide Bell component in toasts
- S4: Clear snoozed_until when remind_at is updated via PUT

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:56:56 +08:00
b2e336ab4a Fix reminder alerts not firing and add AM/PM time picker
- Backend: /due endpoint now matches both NULL and empty string for
  recurrence_rule (form was sending '' not null, excluding all reminders)
- Form: sends null instead of '' for empty recurrence_rule
- ReminderForm: replaced datetime-local with date + hour/minute/AM-PM
  selects for 12-hour time format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:50:08 +08:00
5080e23256 Add real-time reminder alerts with snooze/dismiss
- Backend: GET /api/reminders/due endpoint, PATCH snooze endpoint,
  snoozed_until column + migration
- Frontend: useAlerts hook polls every 30s, fires Sonner toasts on
  non-dashboard pages (max 3 + summary), renders AlertBanner on
  dashboard below stats row
- Dashboard Active Reminders card filters out items shown in banner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:15:56 +08:00
250cbd0239 Address remaining QA items: indexes, validation, UX improvements
Backend:
- [W1] Add server_default=func.now() on created_at/updated_at
- [W2] Add index on reset_at column (migration 016)
- [W7] Document weekly reset edge case in code comment

Frontend:
- [W4] Extract shared isTodoOverdue() utility in lib/utils.ts,
  used consistently across TodosPage, TodoItem, TodoList
- [W5] Delete requires double-click confirmation (button turns red
  for 2s, second click confirms) with optimistic removal
- [W6] Stat cards now reflect filtered counts, not global
- [S3] Optimistic delete with rollback on error
- [S4] Add "None" to priority segmented filter
- [S7] Sort todos within groups by due date ascending

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:24:59 +08:00
79b3097410 Fix QA review issues: transaction safety, validation, accessibility
Critical fixes:
- [C1] _reactivate_recurring_todos now uses flush + with_for_update
  instead of mid-request commit; get_todos commits the full transaction
- [C2] recurrence_rule validated via Literal["daily","weekly","monthly"]
  in Pydantic schemas (rejects invalid values with 422)

Warnings fixed:
- [W3] Clear due_time when due_date is set to null in update endpoint

Suggestions applied:
- [S2] Wrap filteredTodos in useMemo for consistent memoization
- [S6] Add aria-labels to edit/delete icon buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:38:01 +08:00
66ad8902d7 Calculate recurrence schedule on todo update, not just toggle
When editing an already-completed todo to add/change recurrence or
due_date, recalculate reset_at and next_due_date so the reset info
displays immediately without needing to uncomplete and re-complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:09:42 +08:00
46d4c5e28b Implement todo recurrence logic with auto-reset scheduling
Backend:
- Add reset_at (datetime) and next_due_date (date) columns to todos
- Toggle endpoint calculates reset schedule when completing recurring todos:
  daily resets next day, weekly resets start of next week (respects
  first_day_of_week setting), monthly resets 1st of next month
- GET /todos auto-reactivates recurring todos whose reset_at has passed,
  updating due_date to next_due_date and clearing completion state
- Alembic migration 014

Frontend:
- Add reset_at and next_due_date to Todo type
- TodoItem shows recurrence badge (Daily/Weekly/Monthly) in purple
- Completed recurring todos display reset info:
  "Resets Mon 02/03/26 · Next due 06/03/26"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:04:12 +08:00
bb87bd3743 Fix issues from QA review: cache invalidation, model server_default, route comment
- Add ['tracked-tasks'] cache invalidation to toggle mutations in
  ProjectDetail and ProjectCard so dashboard widget stays fresh
- Add server_default=sa.false() to model for consistency with migration
- Add route ordering comment above /tracked-tasks endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 03:07:06 +08:00
819a4689b8 Add track project feature with sidebar nav and dashboard widget
Adds is_tracked boolean to projects, expandable tracked projects
in sidebar navigation, pin toggle on project cards/detail, and a
dashboard widget showing upcoming tasks from tracked projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 01:20:36 +08:00
80f3f3ed10 Calendar enhancements: scroll navigation, birthday color editing, event templates
- Add wheel scroll navigation in month view (debounced, prevents rapid scrolling)
- Allow editing color on system calendars (Birthdays) - name field disabled
- Event templates: full CRUD backend (model, schema, router, migration 011)
- Event templates: sidebar section with create/edit/delete, click to pre-fill EventForm
- Register event_templates router at /api/event-templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:34:16 +08:00