Compare commits

...

26 Commits

Author SHA1 Message Date
373030b81a Fix health check: use DEPLOY_PORT variable for host port
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 43s
The frontend port varies per deployment (80 on dev, 8088 on
dedicated host). Use a Gitea variable so it works across environments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:33:56 +08:00
c96004f91d Fix CI/CD deploy: use stack.env for Portainer-managed stacks
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 49s
Portainer stores environment variables in stack.env, not .env.
Add --env-file stack.env to compose commands in the deploy step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:24:41 +08:00
3496cf0f26 Action performance audit findings
Some checks failed
Build and Deploy UMBRA / build-and-deploy (push) Failing after 11m24s
- Add /health proxy block with rate limiting for external uptime monitoring
- Fix Permissions-Policy on API responses: add passkey directives
- Strengthen CSP: add frame-ancestors 'none' + upgrade-insecure-requests
- Relax backend healthcheck interval from 10s to 30s (reduce overhead)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:03:07 +08:00
e5869e0b19 Fix frontend healthcheck: use 127.0.0.1 instead of localhost
All checks were successful
Build and Deploy UMBRA / build-and-deploy (push) Successful in 52s
Alpine resolves localhost to IPv6 [::1] but nginx only listens on
IPv4, causing the healthcheck to fail with connection refused.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:55:06 +08:00
3075495d1c Remove build: directives — images pulled from Gitea registry only
All checks were successful
Build and Deploy UMBRA / build-and-deploy (push) Successful in 57s
All builds now go through the CI/CD pipeline. The compose file
only needs image: to pull pre-built images from the registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:42:02 +08:00
d945de3837 Switch compose from env_file to environment blocks
All checks were successful
Build and Deploy UMBRA / build-and-deploy (push) Successful in 52s
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>
2026-03-18 17:37:56 +08:00
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
10 changed files with 185 additions and 59 deletions

View File

@ -0,0 +1,86 @@
name: Build and Deploy UMBRA
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
with:
token: ${{ secrets.REGISTRY_TOKEN }}
- name: Login to Gitea Container Registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY_HOST }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
- name: Build and push backend
run: |
docker build --pull \
-t ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-backend:main-latest \
-t ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-backend:${{ github.sha }} \
./backend
docker push ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-backend:main-latest
docker push ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-backend:${{ github.sha }}
- name: Build and push frontend
run: |
docker build --pull \
-t ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-frontend:main-latest \
-t ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-frontend:${{ github.sha }} \
./frontend
docker push ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-frontend:main-latest
docker push ${{ vars.REGISTRY_HOST }}/rohskiddo/umbra-frontend:${{ github.sha }}
- name: Deploy
run: |
# Spawn a short-lived container that mounts the host deploy path
# and runs compose commands against the host Docker daemon.
# DEPLOY_PATH is a Gitea variable — update it when moving hosts.
docker run --rm \
--network host \
--security-opt label:disable \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ${{ vars.DEPLOY_PATH }}:/deploy \
-w /deploy \
docker:27-cli sh -c "
docker compose -p umbra --env-file stack.env pull backend frontend &&
docker compose -p umbra --env-file stack.env up -d db backend frontend
"
- name: Health check
run: |
echo "Waiting for services to start..."
sleep 30
curl -f http://localhost:${{ vars.DEPLOY_PORT }}/health || exit 1
- name: Prune old images
if: success()
run: docker image prune -f
- name: Notify success
if: success()
run: |
curl -s \
-H "Title: UMBRA Deploy Success" \
-H "Tags: white_check_mark" \
--data-binary @- https://ntfy.ghost6.xyz/claude <<'NTFY_EOF'
Build ${{ github.sha }} deployed successfully to umbra.ghost6.xyz.
Triggered by push to main.
NTFY_EOF
- name: Notify failure
if: failure()
run: |
curl -s \
-H "Title: UMBRA Deploy FAILED" \
-H "Tags: fire" \
-H "Priority: high" \
--data-binary @- https://ntfy.ghost6.xyz/claude <<'NTFY_EOF'
Deploy failed for commit ${{ github.sha }}.
Check Gitea Actions logs at git.sentinelforest.xyz.
NTFY_EOF

20
act_runner_config.yaml Normal file
View File

@ -0,0 +1,20 @@
log:
level: info
runner:
capacity: 1
timeout: 3h
insecure: false
cache:
enabled: false
container:
network: host
privileged: false
options: "--security-opt label:disable"
valid_volumes:
- "**"
host:
workdir_parent: /tmp/act_runner

View File

@ -2,7 +2,10 @@ services:
db: db:
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
env_file: .env environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
networks: networks:
@ -19,9 +22,17 @@ services:
cpus: "1.0" cpus: "1.0"
backend: backend:
build: ./backend image: git.sentinelforest.xyz/rohskiddo/umbra-backend:main-latest
restart: unless-stopped restart: unless-stopped
env_file: .env environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- SECRET_KEY=${SECRET_KEY}
- ENVIRONMENT=${ENVIRONMENT:-production}
- UMBRA_URL=${UMBRA_URL:-https://umbra.ghost6.xyz}
- OPENWEATHERMAP_API_KEY=${OPENWEATHERMAP_API_KEY:-}
- WEBAUTHN_RP_ID=${WEBAUTHN_RP_ID:-umbra.ghost6.xyz}
- WEBAUTHN_RP_NAME=${WEBAUTHN_RP_NAME:-UMBRA}
- WEBAUTHN_ORIGIN=${WEBAUTHN_ORIGIN:-https://umbra.ghost6.xyz}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -30,7 +41,7 @@ services:
- frontend_net - frontend_net
healthcheck: healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""] test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
interval: 10s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 30s start_period: 30s
@ -41,7 +52,7 @@ services:
cpus: "1.0" cpus: "1.0"
frontend: frontend:
build: ./frontend image: git.sentinelforest.xyz/rohskiddo/umbra-frontend:main-latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:8080" - "80:8080"
@ -51,7 +62,7 @@ services:
networks: networks:
- frontend_net - frontend_net
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/"] test: ["CMD", "wget", "--spider", "--quiet", "http://127.0.0.1:8080/"]
interval: 15s interval: 15s
timeout: 5s timeout: 5s
retries: 3 retries: 3

View File

@ -12,6 +12,8 @@ limit_req_zone $binary_remote_addr zone=conn_search_limit:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=conn_request_limit:10m rate=3r/m; limit_req_zone $binary_remote_addr zone=conn_request_limit:10m rate=3r/m;
# Event creation recurrence amplification means 1 POST = up to 90-365 child rows # Event creation recurrence amplification means 1 POST = up to 90-365 child rows
limit_req_zone $binary_remote_addr zone=event_create_limit:10m rate=30r/m; limit_req_zone $binary_remote_addr zone=event_create_limit:10m rate=30r/m;
# Health endpoint lightweight but rate-limited for resilience
limit_req_zone $binary_remote_addr zone=health_limit:1m rate=30r/m;
# Use X-Forwarded-Proto from upstream proxy when present, fall back to $scheme for direct access # Use X-Forwarded-Proto from upstream proxy when present, fall back to $scheme for direct access
map $http_x_forwarded_proto $forwarded_proto { map $http_x_forwarded_proto $forwarded_proto {
@ -164,6 +166,13 @@ server {
include /etc/nginx/proxy-params.conf; include /etc/nginx/proxy-params.conf;
} }
# Health endpoint proxied to backend for external uptime monitoring
location = /health {
limit_req zone=health_limit burst=5 nodelay;
limit_req_status 429;
include /etc/nginx/proxy-params.conf;
}
# API proxy (catch-all for non-rate-limited endpoints) # API proxy (catch-all for non-rate-limited endpoints)
location /api { location /api {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@ -184,7 +193,7 @@ server {
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
} }
@ -192,7 +201,7 @@ server {
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# PT-I03: Restrict unnecessary browser APIs # PT-I03: Restrict unnecessary browser APIs
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), publickey-credentials-get=(self), publickey-credentials-create=(self)" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), publickey-credentials-get=(self), publickey-credentials-create=(self)" always;

View File

@ -11,6 +11,6 @@ add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), publickey-credentials-get=(self), publickey-credentials-create=(self)" always;

View File

@ -591,32 +591,32 @@ export default function EventDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={() => setScopeStep(null)} onClick={() => setScopeStep(null)}
title="Cancel" title="Cancel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
) : (isEditing || isCreating) ? ( ) : (isEditing || isCreating) ? (
<> <>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-green-400 hover:text-green-300" className="h-8 w-8 text-green-400 hover:text-green-300"
onClick={handleEditSave} onClick={handleEditSave}
disabled={saveMutation.isPending} disabled={saveMutation.isPending}
title="Save" title="Save"
> >
<Save className="h-3.5 w-3.5" /> <Save className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditCancel} onClick={handleEditCancel}
title="Cancel" title="Cancel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
) : ( ) : (
@ -628,12 +628,12 @@ export default function EventDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditStart} onClick={handleEditStart}
disabled={isAcquiringLock || !!activeLockInfo} disabled={isAcquiringLock || !!activeLockInfo}
title={activeLockInfo ? `Locked by ${activeLockInfo.locked_by_name || 'another user'}` : 'Edit event'} title={activeLockInfo ? `Locked by ${activeLockInfo.locked_by_name || 'another user'}` : 'Edit event'}
> >
{isAcquiringLock ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Pencil className="h-3.5 w-3.5" />} {isAcquiringLock ? <Loader2 className="h-4 w-4 animate-spin" /> : <Pencil className="h-4 w-4" />}
</Button> </Button>
)} )}
{/* Leave button for invited events */} {/* Leave button for invited events */}
@ -641,11 +641,11 @@ export default function EventDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive" className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => setShowLeaveDialog(true)} onClick={() => setShowLeaveDialog(true)}
title="Leave event" title="Leave event"
> >
<LogOut className="h-3.5 w-3.5" /> <LogOut className="h-4 w-4" />
</Button> </Button>
)} )}
{/* Delete button for own events */} {/* Delete button for own events */}
@ -664,12 +664,12 @@ export default function EventDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive" className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={handleDeleteStart} onClick={handleDeleteStart}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
title="Delete event" title="Delete event"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
) )
)} )}
@ -678,11 +678,11 @@ export default function EventDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={onClose} onClick={onClose}
title="Close panel" title="Close panel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
)} )}

View File

@ -285,21 +285,21 @@ export default function TaskDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-green-400 hover:text-green-300" className="h-8 w-8 text-green-400 hover:text-green-300"
onClick={handleEditSave} onClick={handleEditSave}
disabled={updateTaskMutation.isPending} disabled={updateTaskMutation.isPending}
title="Save changes" title="Save changes"
> >
<Save className="h-3.5 w-3.5" /> <Save className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditCancel} onClick={handleEditCancel}
title="Cancel editing" title="Cancel editing"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
) : ( ) : (
@ -307,30 +307,30 @@ export default function TaskDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditStart} onClick={handleEditStart}
title="Edit task" title="Edit task"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive" className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => onDelete(task.id)} onClick={() => onDelete(task.id)}
title="Delete task" title="Delete task"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
{onClose && ( {onClose && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={onClose} onClick={onClose}
title="Close panel" title="Close panel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
)} )}
</> </>
@ -562,7 +562,7 @@ export default function TaskDetailPanel({
{/* Comments */} {/* Comments */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<MessageSquare className="h-3.5 w-3.5 text-muted-foreground" /> <MessageSquare className="h-4 w-4 text-muted-foreground" />
<h4 className="text-[11px] text-muted-foreground uppercase tracking-wider"> <h4 className="text-[11px] text-muted-foreground uppercase tracking-wider">
Comments Comments
{comments.length > 0 && ( {comments.length > 0 && (

View File

@ -236,21 +236,21 @@ export default function ReminderDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-green-400 hover:text-green-300" className="h-8 w-8 text-green-400 hover:text-green-300"
onClick={handleEditSave} onClick={handleEditSave}
disabled={saveMutation.isPending} disabled={saveMutation.isPending}
title="Save" title="Save"
> >
<Save className="h-3.5 w-3.5" /> <Save className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditCancel} onClick={handleEditCancel}
title="Cancel" title="Cancel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
) : ( ) : (
@ -259,22 +259,22 @@ export default function ReminderDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 hover:bg-orange-500/10 hover:text-orange-400" className="h-8 w-8 hover:bg-orange-500/10 hover:text-orange-400"
onClick={() => dismissMutation.mutate()} onClick={() => dismissMutation.mutate()}
disabled={dismissMutation.isPending} disabled={dismissMutation.isPending}
title="Dismiss reminder" title="Dismiss reminder"
> >
<BellOff className="h-3.5 w-3.5" /> <BellOff className="h-4 w-4" />
</Button> </Button>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditStart} onClick={handleEditStart}
title="Edit reminder" title="Edit reminder"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-4 w-4" />
</Button> </Button>
{confirmingDelete ? ( {confirmingDelete ? (
<Button <Button
@ -290,22 +290,22 @@ export default function ReminderDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive" className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={handleDeleteClick} onClick={handleDeleteClick}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
title="Delete reminder" title="Delete reminder"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={onClose} onClick={onClose}
title="Close panel" title="Close panel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
)} )}

View File

@ -61,7 +61,7 @@ export function EntityDetailPanel<T>({
size="icon" size="icon"
onClick={onToggleFavourite} onClick={onToggleFavourite}
aria-label={isFavourite ? `Remove from ${favouriteLabel}s` : `Add to ${favouriteLabel}s`} aria-label={isFavourite ? `Remove from ${favouriteLabel}s` : `Add to ${favouriteLabel}s`}
className={`h-7 w-7 ${isFavourite ? 'text-yellow-400' : 'text-muted-foreground'}`} className={`h-8 w-8 ${isFavourite ? 'text-yellow-400' : 'text-muted-foreground'}`}
> >
{isFavourite ? ( {isFavourite ? (
<Star className="h-4 w-4 fill-yellow-400" /> <Star className="h-4 w-4 fill-yellow-400" />
@ -75,7 +75,7 @@ export function EntityDetailPanel<T>({
size="icon" size="icon"
onClick={onClose} onClick={onClose}
aria-label="Close panel" aria-label="Close panel"
className="h-7 w-7" className="h-8 w-8"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>

View File

@ -272,21 +272,21 @@ export default function TodoDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-green-400 hover:text-green-300" className="h-8 w-8 text-green-400 hover:text-green-300"
onClick={handleEditSave} onClick={handleEditSave}
disabled={saveMutation.isPending} disabled={saveMutation.isPending}
title="Save" title="Save"
> >
<Save className="h-3.5 w-3.5" /> <Save className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditCancel} onClick={handleEditCancel}
title="Cancel" title="Cancel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
) : ( ) : (
@ -294,11 +294,11 @@ export default function TodoDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={handleEditStart} onClick={handleEditStart}
title="Edit todo" title="Edit todo"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-4 w-4" />
</Button> </Button>
{confirmingDelete ? ( {confirmingDelete ? (
<Button <Button
@ -314,22 +314,22 @@ export default function TodoDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive" className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={handleDeleteClick} onClick={handleDeleteClick}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
title="Delete todo" title="Delete todo"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
)} )}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-8 w-8"
onClick={onClose} onClick={onClose}
title="Close panel" title="Close panel"
> >
<X className="h-3.5 w-3.5" /> <X className="h-4 w-4" />
</Button> </Button>
</> </>
)} )}