285 Commits

Author SHA1 Message Date
89e253843c Merge chore/qa-suggestions-and-docs: QA suggestions + documentation refresh 2026-02-25 20:36:26 +08:00
ad102c24ed Apply QA suggestions and update all documentation
Code changes (S-01, S-02, S-05):
- DRY nginx proxy blocks via shared proxy-params.conf include
- Add ENVIRONMENT and CORS_ORIGINS to .env.example
- Remove unused X-Requested-With from CORS allow_headers

Documentation updates:
- README.md: reflect auth upgrade, security hardening, production
  deployment guide with secret generation commands, updated architecture
  diagram, current project structure and feature list
- CLAUDE.md: codify established dev workflow (branch → implement →
  test → QA → merge), update auth/infra/stack sections, add authority
  links for progress.md and ntfy.md
- progress.md: add Phase 11 (auth upgrade) and Phase 12 (pentest
  remediation), update file inventory, fix outstanding items
- ui_refresh.md: update current status line

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:36:12 +08:00
22d8d5414f Merge feature/pentest-remediation: harden infrastructure (7 pentest findings) 2026-02-25 20:15:34 +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
17643d54ea Suppress reminder toasts while lock screen is active
Swap LockProvider to outer wrapper so AlertsProvider can read isLocked.
When locked, dismiss all visible reminder toasts and skip firing new ones.
Toasts re-fire normally on unlock via the firedRef.clear() reset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:23:26 +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
aa2d011700 Fix lock overlay z-index and duplicate recurring event notifications
- Lock overlay: z-50 -> z-[100] so it renders above Sheet/Dialog (both z-50)
- Event notifications: skip recurring parent template rows (recurrence_rule
  set + parent_event_id NULL) which duplicate the child instance rows,
  causing double notifications for recurring events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:12:23 +08:00
f5265a589e Fix form validation: red outline only on submit, add required asterisks
- Remove instant invalid:ring/border from Input component (was showing
  red outline on empty required fields before any interaction)
- Add CSS rule: form[data-submitted] input:invalid shows red border
- Add global submit listener in main.tsx that sets data-submitted on forms
- Add required prop to Labels missing asterisks: PersonForm (First Name),
  LocationForm (Location Name), CalendarForm (Name), LockScreen
  (Username, Password, Confirm Password)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:53:15 +08:00
4207a62ad8 Fix build: remove unused cn import from NtfySettingsSection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:32:46 +08:00
0d2d321fbb Restructure Integrations card: inline title, ntfy sub-section with collapsible config
- Integrations card now has inline icon+title (no description), future-proofed
  for additional integrations as sub-sections
- ntfy is its own sub-section with Bell icon + "ntfy Push Notifications" header
- Connection settings (URL, topic, token) collapse into a "Configure ntfy
  connection" disclosure after saving, keeping the UI tidy
- Notification types and test/save buttons remain visible when enabled
- First-time setup shows connection fields expanded; they collapse on save

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:24:40 +08:00
ca1cd14ed1 Rebalance settings page columns and inline lock-after input
Left column: Profile, Appearance, Calendar, Dashboard, Weather (prefs & display)
Right column: Security, Authentication, Integrations (security & services)

Also inlines the "Lock after [input] minutes" onto a single row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:14:24 +08:00
7d6ac4d257 Fix auto-lock minutes input: allow backspace to clear before retyping
The onChange was using parseInt(...) || 5 which snapped empty input back
to 5 immediately. Now allows empty string as intermediate state; value
is clamped to 1-60 on blur/Enter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:01:06 +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
e5b6725081 Add red, pink, yellow HSL presets to useTheme accent color map
The swatches were added to SettingsPage but useTheme only had 5 presets,
so selecting the new colors saved to DB but never applied CSS variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:32:19 +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
9b261574ca Fix ImportError: remove stale SettingsCreate and ChangePinRequest from schemas registry
These were removed in the auth migration but schemas/__init__.py still imported them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 08:04:11 +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
f136a0820d Merge branch 'stage6-track-b-totp-mfa' into stage6-phase4-5-settings-totp-ntfy
# Conflicts:
#	frontend/src/components/settings/NtfySettingsSection.tsx
#	frontend/src/components/settings/TotpSetupSection.tsx
2026-02-25 04:29:33 +08:00
3268bfc5d5 Fix SSRF guard to allow private IPs for LAN ntfy servers (W5)
Remove RFC 1918 blocks from _BLOCKED_NETWORKS — only block loopback
and link-local. Self-hosted ntfy servers are typically on the same LAN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:22:48 +08:00
6ad6056125 Stage 6 Phase 4-5: TOTP setup UI and ntfy integrations settings
Adds Authentication card (password change + TOTP 5-state setup flow) and
Integrations card (ntfy master toggle, connection config, per-type toggles,
test button) to SettingsPage right column in correct order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:21:36 +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
5a8819c4a5 Stage 6 Phase 2-3: LockScreen rewrite + SettingsPage restructure
- LockScreen: full rewrite — username/password auth (setup/login/TOTP states),
  ambient glow blobs, UMBRA wordmark in flex flow, animate-slide-up card,
  HTTP 423 lockout banner, Loader2 spinner, client-side password validation
- SettingsPage: two-column lg grid (Profile/Appearance/Weather left,
  Calendar/Dashboard right), fixed h-16 page header, icon-anchored CardHeaders,
  labeled accent swatch grid with aria-pressed, max-w-xs removed from name
  input, upcoming days onBlur save with NaN+no-op guard, Security card removed
- useSettings: remove deprecated changePin/isChangingPin stubs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:06:53 +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
7f0ae0b6ef Stage 6 Phase 0-1: Foundation — Switch component, new types, useAuth rewrite, useSettings cleanup
- Create switch.tsx: pure Tailwind Switch toggle, shadcn/ui pattern (forwardRef, cn(), role=switch, aria-checked)
- types/index.ts: extend Settings with all ntfy fields; add LoginSuccessResponse, LoginMfaRequiredResponse, LoginResponse discriminated union, TotpSetupResponse
- useAuth.ts: rewrite with username/password login, ephemeral mfaToken state, totpVerifyMutation, mfaRequired boolean, full mutation exports
- useSettings.ts: remove changePinMutation logic; keep deprecated changePin/isChangingPin stubs for compile compatibility until SettingsPage is rewritten in Phase 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 04:02:55 +08:00
5feb67bf13 Merge Stage 5: Entity Pages Enhancement (People & Locations rebuild, shared components, CategoryFilterBar QOL) 2026-02-25 01:25:01 +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
1231c4b36d Polish entity pages: groups, favourite toggle, copy icon, initials, notes
- EntityTable: replace flat rows with grouped sections (label + rows),
  each group gets its own section header for visual clarity
- EntityDetailPanel: add favourite/frequent toggle star button in header,
  add multiline flag for notes with whitespace-pre-wrap
- CopyableField: use inline-flex to keep copy icon close to text
- PeoplePage: compute initials from first_name/last_name (not nickname),
  build row groups from active filters, add toggle favourite mutation
- LocationsPage: build row groups from active filters, add toggle
  frequent mutation, pass favourite props to detail panel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 00:21:23 +08:00
1b78dadf75 Fix bugs and action remaining QA suggestions
Bugs fixed:
- formatUpdatedAt treats naive UTC timestamps as UTC (append Z before parsing)
- PersonForm/LocationForm X button now inline with star toggle, matching panel style
- LocationForm contact placeholder changed from +44 to +61

QA suggestions actioned:
- CategoryAutocomplete: replace blur setTimeout with onPointerDown preventDefault
- CategoryFilterBar: replace hardcoded 600px maxWidth with 100vw
- Location "other" category shows dash instead of styled badge
- Delete dead legacy constants files (people/constants.ts, locations/constants.ts)
- EntityTable rows: add tabIndex, Enter/Space keyboard navigation, focus ring
- Replace Record<string, unknown> casts with typed keyof accessors
- Add email validation (field_validator) to Person and Location schemas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:46:38 +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
f4b1239904 UI refresh Stage 5: rebuild People page with table/panel layout
- Rewrote PeoplePage.tsx: EntityTable + EntityDetailPanel + CategoryFilterBar
  with favourites pinned section, stat bar with birthday list, animated side panel
- Rewrote PersonForm.tsx: expanded fields (first/last/nickname, mobile, company,
  job_title, birthday+age, category autocomplete, LocationPicker, favourite star)
- Stripped constants.ts to legacy-only getRelationshipColor, removed RELATIONSHIPS
- Deleted PersonCard.tsx (replaced by table rows + detail panel)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:07:26 +08:00
765f692304 UI refresh Stage 5: redesign People & Locations pages
- Add badge color constants for relationships and location categories
- Compact h-16 headers with segmented filters replacing dropdowns
- Stat cards (Total, Upcoming Birthdays, With Contact Info / Categories)
- People search expanded to match name, email, and relationship
- Avatar initials with deterministic color hash on PersonCard
- Two-click delete via useConfirmAction on both entity cards
- Error toasts use getErrorMessage for meaningful messages
- Query invalidation includes dashboard and upcoming keys
- PersonForm migrated from Dialog to Sheet with Select dropdown
- LocationForm: import CATEGORIES from constants, invalidate dashboard/upcoming
- Normalize badge colors to text-*-400 pattern (was text-*-500)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:53:44 +08:00
4d5667a78a Fix migration 018: use DROP INDEX IF EXISTS for safety
The index may not exist if migration 017 partially failed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:07:51 +08:00
937118a528 Remove .claude directory from repository tracking
Local-only Claude Code context — not part of the UMBRA application.
Files remain on disk, just untracked per .gitignore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:05:19 +08:00
e1e546c50c Add future roadmap and key features to CLAUDE.md
Document multi-user planning intent, reminder alerts pattern,
useConfirmAction hook, and naive datetime contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:01:59 +08:00
8aa662c096 Consolidate toast effects, extract useConfirmAction, extend index
- W3: Merge route-change and new-alert effects into single unified effect
- W6: Migration 018 extends due_lookup index with snoozed_until column
- S1: Extract useConfirmAction hook from TodoItem/ReminderItem
- S7: Update summary toast count on dismiss/snooze instead of dismissing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:01:50 +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
758e794880 Open snooze dropdown downward in AlertBanner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:39:55 +08:00
0c767317ab Fix snooze dropdown clipping in toasts, add Dismiss label to banner
- SnoozeDropdown: added direction prop (up/down), toasts use 'down'
  so dropdown opens below the button instead of clipping off-screen
- AlertBanner dismiss button now shows X icon + 'Dismiss' text to
  match toast style

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:37:13 +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
38bce21ac3 Polish toast actions and extend delete confirm timeout
- Toast: snooze/dismiss buttons side-by-side on the right, dismiss
  uses X icon, snooze shows clock icon + 'Snooze' label
- SnoozeDropdown trigger now shows text label alongside icon
- Delete confirm 'Sure?' lingers 4 seconds instead of 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:24:40 +08:00
c609e374e7 Show 'Sure?' text on delete confirmation instead of silent icon change
Trash icon swaps to a red 'Sure?' label on first click, making the
two-click delete pattern discoverable. Applied to both TodoItem and
ReminderItem. Resets after 2 seconds if not confirmed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:16:32 +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
42e0fff40c Revert ReminderForm to single datetime-local input
Matches EventForm pattern used across UMBRA. Remind At and Recurrence
now sit side-by-side in a 2-column grid. Empty optional fields send
null instead of empty string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:11:31 +08:00
89b4bd4870 Replace snooze button group with clock icon dropdown
Single clock icon opens a dropdown with 5/10/15 min options instead
of three inline buttons. Shared SnoozeDropdown component used in
both AlertBanner and toast notifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:09:17 +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