The site becomes a second writer to scouts/site73, as the scoped scoutsite principal (rw on that one collection, denied everywhere else by the rights file). Two rules enforced in calendar_write.py, not left to callers: the site owns only UIDs ending @site73.greenlanescouts73.org and refuses any other before a network call, the same shape as band-cal-sync and @band.us; and with no RADICALE_* configuration every write is a 503, never a silent no-op. The VEVENT layout matches seed.py exactly so the feed reverses it into the row shape the public pages already render: all-day DTEND exclusive, TZID + VTIMEZONE on timed events, 90-minute default for a timed one-day event, noon on the end date for a timed multi-day one, CATEGORIES for the unit, X-SCOUT73-BADGE, 75-octet folding. Reads come from the scout-calendar feed (now carrying uid and recurring); rows the site created and that are not part of a series are marked mine. Writes are logged to auth_events and bust the page cache so a leader sees their event within the feed's minute. DELETE really deletes - the calendar's history is Radicale's git log. Endpoints under calendar:write. tests/smoke_admin.py 78 -> 102. Proven against the real store on a 2036 probe (outside the feed window): create, read back byte-for-byte, replace, delete, second delete 404, foreign UID 403 with no store call, unconfigured 503.
66 lines
2.4 KiB
YAML
66 lines
2.4 KiB
YAML
# scout-website — greenlanescouts73.org (Pack & Troop 73)
|
|
#
|
|
# Deployed as a Portainer Repository stack pointed at this repo
|
|
# (ops/portainer-stacks/00-the-rule.md in claude-workspace applies):
|
|
# - no relative bind mounts: `.` resolves to Portainer's clone, not /srv
|
|
# - no env_file: NTFY_URL comes from the Portainer stack env
|
|
# Images live OUTSIDE the repo at /srv/scout-website-assets/img (bind-mounted
|
|
# read-only) so photos never bloat clones. Documents work the same way, from
|
|
# /srv/scout-website-assets/docs mounted at /docs - NOT under /app/static,
|
|
# because they are served through the app so login can gate them later.
|
|
# The calendar comes from the scout-calendar feed (EVENTS_FEED_URL, set in
|
|
# the Portainer stack env); /data/events-cache.json is the stale fallback.
|
|
|
|
name: scout-website
|
|
services:
|
|
scout-website:
|
|
build:
|
|
context: ./app
|
|
container_name: scout-website
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp
|
|
cap_drop:
|
|
- ALL
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
restart: unless-stopped
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
ports:
|
|
# localhost only: NPM reaches this as scout-website:8000 over the docker
|
|
# network, so publishing on 0.0.0.0 only bought a LAN path around the proxy.
|
|
- "127.0.0.1:8132:8000"
|
|
environment:
|
|
- NTFY_URL=${NTFY_URL}
|
|
- SHEET_ID=${SHEET_ID}
|
|
- NTFY_BASE=${NTFY_BASE}
|
|
- NTFY_TOPIC=${NTFY_TOPIC}
|
|
- NTFY_TOKEN=${NTFY_TOKEN}
|
|
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
|
- EVENTS_FEED_URL=${EVENTS_FEED_URL}
|
|
# P0 identity. Portainer stack env only reaches the container if it is
|
|
# forwarded here; adding it to the stack alone is not enough.
|
|
- ADMIN_BOOTSTRAP_EMAIL=${ADMIN_BOOTSTRAP_EMAIL}
|
|
- SITE_BASE_URL=${SITE_BASE_URL}
|
|
# P4 calendar write-back. The scoped `scoutsite` Radicale principal, rw
|
|
# on scouts/site73 only. Unset = every calendar write is a 503.
|
|
- RADICALE_URL=${RADICALE_URL}
|
|
- RADICALE_USER=${RADICALE_USER}
|
|
- RADICALE_PASS=${RADICALE_PASS}
|
|
volumes:
|
|
- /srv/scout-website/data:/data
|
|
- /srv/scout-website/secrets/service_account.json:/app/service_account.json:ro
|
|
- /srv/scout-website-assets/img:/app/static/img:ro
|
|
- /srv/scout-website-assets/docs:/docs:ro
|
|
networks:
|
|
- default
|
|
- npm
|
|
networks:
|
|
npm:
|
|
external: true
|
|
name: arrstack_arr_net
|