Files
scout-website/README.md
T
Mike Wichers 1ecdb32139 Add a document shelf at /documents, served through the app
Files live outside the repo at /srv/scout-website-assets/docs (bind-mounted
read-only at /docs), the same arrangement as the photos, published by a
manifest.json beside them. Adding a document is a file drop plus a manifest
entry - the app re-reads the manifest on mtime change, so no rebuild and no
redeploy.

The manifest maps a stable slug to a filename, so next year's permission slip
can replace this year's without breaking a link already printed on a flyer.

Documents are served through /documents/{slug} rather than from a static
mount, and NOT placed under /app/static, which is public forever. That is the
point: when member login exists it plugs into documents.visible() and no
public URL moves. The visibility field already carries 'members', which today
is hidden from the index and 404s rather than 403s, since a 403 would
advertise a document we cannot yet gate.
2026-08-30 09:02:29 -04:00

69 lines
4.5 KiB
Markdown

# scout-website
Public recruiting site for **Cub Scout Pack 73 and Scouts BSA Troop 73** (Zieglerville / Green Lane, PA).
Live at **https://greenlanescouts73.org** (NPM proxy → `scout-website` container, port 8132 on arrstack).
Replaces the `scoutpoc` proof of concept (claude-workspace/stacks/scoutpoc).
## Updating the calendar (the part you'll actually do)
All dates on the site (home "Next up," both program pages, the Calendar page) come from the **scout-calendar feed** (claude-workspace/stacks/scout-calendar), which serves the Radicale collection `mike/site73` as JSON:
```
EVENTS_FEED_URL ← Portainer stack env; http://scout-calendar:8000/events.json
/data/events-cache.json ← last good copy, served automatically whenever the feed is down
```
Edit the calendar in any CalDAV client pointed at Radicale (or ask Claude), and the site follows within ~5 minutes. **No file edits, no rebuild, no redeploy.** The public ICS families can subscribe to is https://calendar.greenlanescouts73.org/calendar.ics.
Event fields the feed emits (one object per event):
```json
{"date": "2026-10-17", "end": "2026-10-18", "time": "from 4:00 PM",
"title": "Fall Campout", "location": "Green Lane Park",
"unit": "both", "badge": "OVERNIGHT"}
```
- `date` required (YYYY-MM-DD). `end` only for multi-day events.
- `unit`: `"pack"`, `"troop"`, or `"both"` (default pack). Drives the Pack/Troop calendar filter and each page's "Next up."
- `badge`: optional shouty label (`FALL OPEN HOUSE`, `OVERNIGHT`, `THE BIG ONE`).
- Past events are hidden from "Next up" automatically; the Calendar page shows the whole program year.
- TeamSnap (team 8198615) stays the source of truth for registered families; Radicale `mike/site73` is the source for this public site.
## Documents
Files at `/srv/scout-website-assets/docs` (bind-mounted read-only at `/docs`), published by
`manifest.json` in that same folder. **Adding a document is a file drop plus a manifest entry: no
rebuild, no redeploy.** The app re-reads the manifest whenever its mtime changes.
```json
{"slug": "permission-slip", "file": "2026-permission-slip.pdf",
"title": "Activity permission slip", "description": "One per scout, per outing.",
"category": "forms", "unit": "both", "visibility": "public", "updated": "2026-08-30"}
```
- `slug` is the permanent URL: `greenlanescouts73.org/documents/<slug>`. **Never change one that has
been printed or emailed.** To publish a new version, point the same slug at the new filename.
- `category` matches an id in the manifest's `categories`; anything else lands under "Everything else".
- `visibility`: `public`, or `members` for later. `members` documents are hidden from the index and
return 404 - **this is not a working gate yet**, it is the seam login will attach to.
- `updated` optional; without it the file's own mtime is shown.
Documents are served **through the app** (`/documents/{slug}`), never from a static mount. Anything
under `/app/static` is public forever, so nothing that will ever need gating goes there. When member
login exists it plugs into `documents.visible()` and no public URL moves.
## Layout
- `app/app.py` — FastAPI app, all pages server-rendered (Home, /cubs, /troop, /calendar?unit=, /join, /documents). `/contact` 301s to `/join`.
- `app/documents.py` — the document shelf: manifest loading, the slug-to-file map, and the one visibility gate.
- `docker-compose.yml` — Portainer Repository stack definition (port 8131).
- Images: **not in the repo.** Bind-mounted read-only from `/srv/scout-website-assets/img`. Source photos live on the NAS (`Scouting-Recruitment-Images`, `scouting-comms/pack-73/assets/social`); web-sized copies in `Scouting-Recruitment-Images/_web`.
## Leads
`/srv/scout-website/data/leads.jsonl` — one JSON line per interest-form submission, plus an ntfy push per lead.
## Lead pipeline (self-contained)
`/join` submissions write **directly** to the shared Google Sheet (`SHEET_ID` stack env, creds bind-mounted from `/srv/scout-website/secrets/service_account.json`) and push to the shared ntfy `scouting` topic (`NTFY_BASE`/`NTFY_TOPIC`/`NTFY_TOKEN`). The extra comments box lands in a new 8th sheet column, **Comments**. Every submission also appends to `/data/leads.jsonl`; if both sheet and push fail, a warning push goes to the fallback `NTFY_URL` topic. No dependency on the scoutpoc or scouting73-form containers: both can be decommissioned without breaking this site. (The flyer QR domain scouting73.thewichersfamily.com should be repointed here before its container is retired.)