Replace env_file: .env with explicit environment: variables using
${VAR} substitution. Works with both .env files (local dev) and
Portainer's environment UI (no .env file needed on the host).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>