claude eeb2ce6244 Copy sweep: pull the seasonal surface into named values
The hero button said 'Join us this fall' while the nav button two
inches above it said 'Join us'. That is the whole problem in one
screenshot: strings that go stale on a schedule, scattered.

Seven values at the top of app.py now hold the program year, the
meeting day, and the two meeting times. Twelve inline meeting-time
strings and four program-year labels read from them, so a time change
is one edit rather than a grep. The block carries a note that this is
not a CMS: anything with a date in it belongs in the calendar feed,
and evergreen copy stays inline where git log can answer what the
site said during recruitment.

Two strings were not stale, they were false out of season:

  'There is nothing to do between now and Tuesday' on the thanks page
  and 'See you Tuesday' on the join CTA both confidently invited
  families to a meeting that did not exist through July. Now 'we will
  be in touch with the next meeting date' and 'See you on a Tuesday'.
  The properly correct fix reads the next meeting from the calendar
  feed and this is not that, but it stops the site making a promise
  it cannot keep in August.

  The chocolate booth does not run all winter. Three places now say
  in season.

'fall grade' becomes 'current grade' on the join label and in
SHEET_HEADERS. The header is only written to an empty sheet, so the
live sheet would have kept saying 'fall grade' forever; sheet_append
now repairs cell E1 in place on the next lead, the same way it
already backfills the Comments header.

thanks_body became an f-string. It had no bare braces, so the
conversion is safe.
2026-09-01 19:35:13 -04:00

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):

{"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.

{"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 - listed on /documents, open to anyone.
    • unlisted - served at its slug to anyone holding the link, kept off the index, sent with X-Robots-Tag: noindex. For internal papers that need a durable link before login exists. This is obscurity, not access control. An unlisted link is forwardable; assume it will be.
    • members - hidden and unservable, returns 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.)

S
Description
greenlanescouts73.org - Pack & Troop 73 recruiting site
Readme
1.5 MiB
Languages
Python 99.9%
Dockerfile 0.1%