473 Commits

Author SHA1 Message Date
329c057632 Remove act_runner from main docker-compose.yaml
All checks were successful
Build and Deploy UMBRA / build-and-deploy (push) Successful in 51s
The runner is CI/CD infrastructure, not part of the application.
Self-hosters cloning UMBRA don't need a runner. The runner now
lives as a standalone stack (documented in .claude/docs/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:34:55 +08:00
ae3dc4e9db Fix CI/CD deploy: don't recreate the runner during deploy
All checks were successful
Build and Deploy UMBRA / build-and-deploy (push) Successful in 57s
docker compose up -d was recreating act_runner, killing the job
mid-execution. Explicitly target db, backend, frontend only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:46:21 +08:00
70cf033fdc Fix CI/CD deploy: use -p umbra to match existing project name
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 10m31s
The docker:cli container's working dir /deploy caused compose to
create a new 'deploy' project instead of updating the existing
'umbra' stack. Adding -p umbra ensures it manages the right containers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:53:56 +08:00
618afeb336 Apply docker specialist review: pin image, fresh bases, longer wait
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 12s
- Pin deploy container to docker:27-cli (avoid compose version drift)
- Add --pull to both docker build commands (keep base images fresh)
- Increase health check sleep to 30s (backend start_period is 30s)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:44:41 +08:00
76b19cd33a Fix CI/CD deploy: mount host DEPLOY_PATH for compose access
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 10s
The job container can't access the host filesystem directly.
Spawn a docker:cli container that mounts the host's DEPLOY_PATH
(where docker-compose.yaml and .env live) and runs compose commands.

Requires DEPLOY_PATH variable in Gitea (e.g. /home/user/.../UMBRA).
When moving to a new host, only the Gitea variable needs updating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:38:28 +08:00
c98e47a050 Fix CI/CD deploy: use workspace compose file instead of /opt/umbra
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 13s
The job container doesn't have /opt/umbra. Use the checked-out
repo's docker-compose.yaml (already in the working directory).
Combined pull + deploy into one step. Increased health check wait.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:35:05 +08:00
fac953fcea Fix CI/CD: use catthehacker/ubuntu:act-22.04 for job containers
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 2s
Host mode failed because the act_runner container lacks node/curl/git.
catthehacker/ubuntu:act-22.04 is the standard act_runner job image —
includes node, git, docker CLI, curl, and common CI tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:50:11 +08:00
7a9122c235 Fix CI/CD: use host execution mode for runner jobs
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 4s
The node:20-bookworm container doesn't have Docker CLI installed,
causing 'docker: command not found'. Switch runner label from
docker://node:20-bookworm to host mode so jobs run directly on
the runner host where Docker is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:12:00 +08:00
7f38df22db Fix CI/CD: full runner config, shell-only workflow, config mount fix
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 6s
- Replace all GitHub action clones (login-action, build-push-action)
  with plain docker CLI commands — eliminates GitHub dependency
- Expand act_runner_config.yaml to full format (partial config was
  silently falling back to defaults)
- Mount config at /etc/act_runner/ with CONFIG_FILE env var to avoid
  named volume shadowing at /data/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:48:30 +08:00
86c113c412 Fix checkout token: use REGISTRY_TOKEN (GITEA_ prefix reserved)
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 13m32s
Gitea reserves the GITEA_ prefix for secrets. Reuse the existing
REGISTRY_TOKEN PAT which already has repo read access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:19:28 +08:00
1f34da9199 Fix CI/CD checkout failure + enlarge panel action buttons
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 11m16s
CI/CD fixes (from debugger + docker specialist review):
- Add explicit GITEA_TOKEN for checkout auth
- Add act_runner_config.yaml with container.network: host so job
  containers can reach git.sentinelforest.xyz (root cause of 0s
  silent checkout failure)
- Mount config into act_runner container

UI: Enlarge save/close/edit/delete icons in all detail panels
(EventDetailPanel, TodoDetailPanel, ReminderDetailPanel,
TaskDetailPanel, EntityDetailPanel) from h-7/h-3.5 to h-8/h-4
for better visibility and click targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 08:28:15 +08:00
507c841a92 Fix act_runner: SELinux label:disable, host network, pin image
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 15m48s
Docker specialist review findings:
- Replace :z with security_opt: label:disable (correct SELinux fix)
- Remove user: 0:0 (unnecessary with SELinux handled)
- Remove redundant DOCKER_HOST env var
- Add network_mode: host (workflow steps need host access)
- Pin image to 0.2.11 (avoid non-deterministic latest tag)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:39:59 +08:00
3ad216ab0c Fix act_runner: add :z SELinux label to Docker socket mount
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
SELinux in enforcing mode blocks container access to the Docker
socket. The :z flag relabels the socket for shared container access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:38:16 +08:00
3ca1a9af08 Fix act_runner: run as root for Docker socket access
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
group_add didn't resolve the permission issue. Running the runner
as root (user 0:0) is the standard approach for CI runners that
need Docker socket access on internal/single-user deployments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:36:07 +08:00
d981b9346f Fix act_runner: add docker group (971) for socket access
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
The runner process runs as non-root but needs access to the Docker
socket owned by root:docker (GID 971). group_add grants it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:34:41 +08:00
571268c9b4 Fix act_runner: add explicit DOCKER_HOST env var
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
The act_runner container couldn't find the Docker socket despite the
volume mount. Adding DOCKER_HOST=unix:///var/run/docker.sock explicitly
tells the runner where to find it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:31:42 +08:00
55891eb7b5 Add build: fallback alongside image: for initial bootstrap
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
When both image: and build: are present, docker compose up --build
builds locally and tags with the image name. This allows the stack
to start before registry images exist, solving the bootstrap problem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 04:20:33 +08:00
4f8b83ba87 Merge feature/gitea-cicd: Gitea Actions CI/CD pipeline
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Has been cancelled
2026-03-18 04:12:55 +08:00
5d64034bb6 Add Gitea Actions CI/CD pipeline for automated builds and deploys
Adds a workflow that triggers on push to main: builds backend/frontend
Docker images, pushes to Gitea container registry, pulls and restarts
on the host, health checks, prunes old images, and sends ntfy notifications.
docker-compose.yaml updated to pull pre-built images from registry and
includes act_runner as a 4th service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 03:36:39 +08:00
0f58edf607 Merge feature/passkey-authentication: WebAuthn passkeys + passwordless login
Major feature: Passkey authentication (WebAuthn/FIDO2) with passwordless
login support, passkey-based lock screen unlock, and full admin controls.

Includes:
- Session consolidation (shared services/session.py)
- Passkey registration, login, management (6 endpoints + py_webauthn)
- Passwordless login toggle (per-account, admin-gated, 2-key minimum)
- Passkey lock screen unlock
- Admin: per-user force-disable, system config toggle
- Pentest: 30+ attack vectors tested, 3 low findings remediated
- QA: 2 critical + 5 warnings + 6 suggestions actioned
- Performance: EXISTS over COUNT, bulk session cap, dynamic imports

19 commits, 35 files changed, 2 migrations (061-062).
2026-03-18 02:34:57 +08:00
ed98924716 Action remaining QA suggestions + performance optimizations
S-02: Extract extract_credential_raw_id() helper in services/passkey.py
  — replaces 2 inline rawId parsing blocks in passkeys.py
S-03: Add PasskeyLoginResponse type, use in useAuth passkeyLoginMutation
S-04: Add Cancel button to disable-passwordless dialog
W-03: Invalidate auth queries on disable ceremony error/cancel

Perf-2: Session cap uses ID-only query + bulk UPDATE instead of loading
  full ORM objects and flipping booleans individually
Perf-3: Remove passkey_count from /auth/status hot path (polled every
  15s). Use EXISTS for has_passkeys boolean. Count derived from passkeys
  list query in PasskeySection (passkeys.length).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 02:34:00 +08:00
0a8e163e47 Fix QA review findings: 2 critical, 3 warnings, 1 suggestion
C-01: Initialize config=None before conditional in auth/status to
prevent NameError on fresh instance (setup_required=True path)

C-02: Use generic "Authentication failed" on passkey lockout trigger
instead of leaking lockout state (consistent with F-02 remediation)

W-01: Add nginx rate limit for /api/auth/passkeys/passwordless
endpoints (enable accepts password — brute force protection)

W-02: Call record_successful_login in passkey unlock path to reset
failed_login_count (prevents unexpected lockout accumulation)

W-05: Auto-clear must_change_password on passkey login — user can't
provide old password in forced-change form after passkey auth

S-01: Pin webauthn to >=2.1.0,<3 (prevent major version breakage)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 02:27:16 +08:00
94891d8a70 Fix IAM actions dropdown rendering behind System Settings card
Add relative z-10 to the Users Card so its stacking context sits above
the sibling System Settings Card. Without this, the absolutely-positioned
dropdown menu was painted behind the later sibling in DOM order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 02:04:41 +08:00
0f6e40a5ba Fix dropdown clipping: remove overflow constraints on parent containers
Revert fixed-positioning approach (caused z-index and placement issues).
Instead fix the root cause: parent containers with overflow that clipped
absolutely-positioned dropdowns.

- IAMPage: Remove overflow-x-auto on table wrapper (columns already
  hide via responsive classes, no horizontal scroll needed)
- AlertBanner: Remove max-h-48 overflow-y-auto on alerts list
  (alerts are naturally bounded, constraint clipped SnoozeDropdown)
- Revert UserActionsMenu and SnoozeDropdown to simple absolute positioning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:20:57 +08:00
a327890b57 Fix dropdown clipping: use fixed positioning to escape overflow
UserActionsMenu and SnoozeDropdown were clipped by parent containers
with overflow-x-auto/overflow-y-auto. Switch from absolute to fixed
positioning — compute viewport-relative coordinates on open via
getBoundingClientRect. Dropdowns now render above all overflow
boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:10:36 +08:00
44e6c8e3e5 Fix 3 pentest findings: lockout status disclosure, timing side-channel, XFF trust scope
F-01 (passkeys.py): Add constant-time DB no-op on login/begin when username not
found. Without it the absent credential-fetch query makes the "no user" path
measurably faster, leaking username existence via timing.

F-02 (session.py, auth.py, passkeys.py, totp.py): Change check_account_lockout
from HTTP 423 to 401 — status-code analysis can no longer distinguish a locked
account from an invalid credential. record_failed_login now returns remaining
attempt count; callers use it for progressive UX warnings (<=3 attempts left,
and on the locking attempt) without changing the 401 status code visible to
attackers. Session-lock 423 path in get_current_user is unaffected.

F-03 (nginx.conf): Replace set_real_ip_from 0.0.0.0/0 with RFC 1918 ranges
(172.16.0.0/12, 10.0.0.0/8) to prevent external clients from spoofing
X-Forwarded-For to bypass rate limiting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:01:19 +08:00
863e9e2c45 feat: improve account lockout UX with severity-aware error styling
Login errors now distinguish between wrong-password (red), progressive
lockout warnings (amber, Lock icon), and temporary lockout (amber, Lock
icon) based on the backend detail string. Removes the dead 423 branch
from handleCredentialSubmit — account lockout is now returned as 401
with a descriptive detail message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:00:42 +08:00
1b868ba503 Fix: hide passwordless toggle when disabled, remove lock auto-trigger
1. Passwordless toggle in Settings is now hidden when admin hasn't
   enabled allow_passwordless in system config (or when user already
   has it enabled — so they can still disable it). Backend exposes
   allow_passwordless in /auth/status response.

2. Remove auto-trigger passkey ceremony on lock screen — previously
   fired immediately when session locked for passwordless users.
   Now waits for user to click "Unlock with passkey" button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:32:03 +08:00
42d73526f5 feat(passkeys): implement passwordless login frontend (Phase 2)
- types/index.ts: add passkey_count, passwordless_enabled to AuthStatus; add allow_passwordless to SystemConfig; add passwordless_enabled to AdminUser
- useAuth: expose passwordlessEnabled and passkeyCount from auth query
- useLock: add unlockWithPasskey() — clears lock state without password verification
- LockOverlay: passkey unlock support with three modes: passwordless-primary (passkey only, auto-triggers), hybrid (password + "or use a passkey"), password-only (existing behaviour)
- PasskeySection: passwordless toggle below passkey list — enable via password dialog, disable via WebAuthn ceremony dialog; requires 2+ passkeys
- useAdmin: add useDisablePasswordless mutation (PUT /admin/users/{id}/passwordless)
- IAMPage: add allow_passwordless system config toggle
- UserActionsMenu: add "Disable Passwordless" two-click confirm item (shown when user.passwordless_enabled)
- UserDetailSection: add Passwordless badge in Security & Permissions card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 00:16:48 +08:00
bcfebbc9ae feat(backend): Phase 1 passwordless login — migration, models, toggle endpoints, unlock, delete guard, admin controls
- Migration 062: adds users.passwordless_enabled and system_config.allow_passwordless (both default false)
- User model: passwordless_enabled field after must_change_password
- SystemConfig model: allow_passwordless field after enforce_mfa_new_users
- auth.py login(): block passwordless-enabled accounts from password login path (403) with audit log
- auth.py auth_status(): change has_passkeys query to full COUNT, add passkey_count + passwordless_enabled to response
- auth.py get_current_user(): add /api/auth/passkeys/login/begin and /login/complete to lock_exempt set
- passkeys.py: add PasswordlessEnableRequest + PasswordlessDisableRequest schemas
- passkeys.py: PUT /passwordless/enable — verify password, check system config, require >= 2 passkeys, set flag
- passkeys.py: POST /passwordless/disable/begin — generate user-bound challenge for passkey auth ceremony
- passkeys.py: PUT /passwordless/disable — verify passkey auth response, clear flag, update sign count
- passkeys.py: PasskeyLoginCompleteRequest.unlock field — passkey re-auth into locked session without new session
- passkeys.py: delete guard — 409 if passwordless user attempts to drop below 2 passkeys
- schemas/admin.py: add passwordless_enabled to UserListItem + UserDetailResponse; add allow_passwordless to SystemConfigResponse + SystemConfigUpdate; add TogglePasswordlessRequest
- admin.py: PUT /users/{user_id}/passwordless — admin-only disable (enabled=False only), revokes all sessions, audit log
- admin.py: update_system_config handles allow_passwordless field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 00:15:39 +08:00
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