Commit Graph
16 Commits
Author SHA1 Message Date
Mike Wichers 17c4df796a Add unlisted visibility: served by link, off the index, noindex
Internal papers need a durable link before member login exists. 'unlisted'
serves a document at its slug but keeps it off /documents and sends
X-Robots-Tag: noindex so it stays out of search results.

Serving and listing are now separate questions: visible() decides whether a
document can be served at all and stays the seam login attaches to, listed()
decides whether it shows on the index. 'members' remains hidden AND
unservable, so the weaker state cannot be mistaken for the gate.

This is obscurity, not access control, and both the README and the manifest
say so. An unlisted link is forwardable.
2026-08-30 09:21:26 -04:00
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
Claude 27f1a2104b calendar: consume the scout-calendar feed instead of local events.json
Events now come from EVENTS_FEED_URL (the scout-calendar container,
which serves Radicale mike/site73 as JSON). The last good copy is
cached to /data/events-cache.json and served whenever the feed is down
or empty; 'Nothing on the books' remains the cold-start floor only.
The round-trip test proved the feed reproduces all 32 rows of
app/events.json byte-identically, so nothing visible changes today.

Removes app/events.json, the first-boot seed copy, and the local load
path; README calendar section rewritten; compose gains EVENTS_FEED_URL
from the Portainer stack env.
2026-08-29 11:33:43 -04:00
thethreemagi 7d2c241126 Give the join confirmation its own page instead of a banner below the fold
On a phone the /join grid collapses to one column, so the success
banner rendered below the entire pitch column and the 303 landed the
parent at the top of an apparently unchanged page. The empty form
underneath then read as a failed send.

?sent=1 now returns a dedicated confirmation body: banner first, what
happens next, and no form to resubmit.
2026-08-27 07:47:15 -04:00
Mike Wichers 96caac5648 Make a never-attempted mirror visible, and stop replay restamping the date
Two real leads on 2026-08-26 landed in the DB with no Sheet row and no mirror
row at all: the empty-env window meant the Sheet call never ran. Because
/mirrors/failed looked for state='failed', a mirror that was never attempted
read exactly like one that was never owed, and the summary reported a clean
failed_mirrors: {} while two families were missing from the sheet.

/join now writes a `pending` mirror row for every target BEFORE attempting any
of them, so the gap between "owed" and "done" is a row rather than an absence.
set_mirror_pending never downgrades an attempted mirror. failed_mirror_records
covers failed and pending, since both need replaying, and summary reports
pending_mirrors alongside failed_mirrors.

sheet_append also stamped the Sheet with datetime.now(), so replaying a lead
filed it under the day of the replay rather than the day the family submitted.
It now takes the submission timestamp off the record and only falls back to now
when there isn't one.
2026-08-26 21:39:56 -04:00
Mike Wichers 781f991fbe Replace the generic records table with a lean join_leads table
records was a leads table wearing a generic name. It carried display_name /
email / phone, which only mean anything for a lead, plus status / assigned_to /
notes, which were a guess at an outreach process that has not been designed.
Contacting a family is one-to-many, so three columns on the lead row was always
the wrong shape for it.

join_leads is one row per /join submission with one column per form field, two
timestamps, and payload holding the submission verbatim. Outreach gets its own
table when the process is actually known.

Also splits heard_from from heard_from_detail. app.py folded source_place /
source_other into a composed "Other: ..." string and threw the raw answer away,
which is the half that tells you which daycare the lead came from. The composed
value is still built for the Sheet and the ntfy push.

admin API: /records -> /leads, and PATCH is gone since a lead now has nothing
mutable on it. mirrors and meta are unchanged.
2026-08-26 20:55:58 -04:00
Mike Wichers be2d81b3ab Lead pipeline: local SQLite store as source of truth
Form submissions now land in /data/scout73.db before anything leaves the
box. The Google Sheet and the ntfy push become mirrors whose per-record
outcome is recorded, so a failed sheet write is replayable instead of
surviving only as a push telling you to retype it.

The table is a generic record store keyed by 'kind', with workflow columns
and an audit trail, so RSVPs and other forms can land in the same place
later without a migration. Admin panel talks to /api/admin over HTTP and
never opens the DB file.

- app/store.py     schema, writes, reads, one-time leads.jsonl backfill
- app/admin_api.py token-gated API; fails closed when ADMIN_TOKEN is unset
- app/app.py       three hunks: imports, boot init, join_post
- compose          ADMIN_TOKEN passed through from the Portainer stack env
2026-08-26 19:38:33 -04:00
claude 18b0a85aca Troop meetings move to 7:30 PM
Pack runs 6:00-7:15, so the troop follows at 7:30 with a buffer. Updates
all eight troop time references across the footer, /troop and /calendar.
The two 7:00 AM references (Green Lane farmers market chocolate booth)
are deliberately unchanged.

Planned timing, not yet established practice.
2026-08-24 19:56:48 -04:00
claude 08bc249abb footer: Facebook badge icons + short labels; head: Open Graph tags and meta description 2026-08-24 19:14:45 -04:00
claude e319b24585 footer: Facebook links to new GreenLane usernames 2026-08-24 19:02:11 -04:00
claude dd04d3be72 footer: Follow column with Pack/Troop 73 Facebook links 2026-08-24 18:54:36 -04:00
claude 2eec0a12ae Copy pass: join headline, who-can-join FAQ, Eagle card, Farmers Market naming; ntfy title to 'New 73 lead' 2026-08-24 18:44:35 -04:00
claude 5e3eeb9243 self-contained lead pipeline (direct sheet+ntfy, +Comments col); fix joinbtn specificity + facts flex shrink-wrap 2026-08-24 13:53:57 -04:00
claude 27fcdd3d2b fix: f-string brace crash from inline source JS 2026-08-24 13:36:34 -04:00
claude f3c483a58e join form mirrors scouting73-form (children textarea, interested_in, source w/ schools) + forwards to shared sheet pipeline; firefox joinbtn fix 2026-08-24 13:34:13 -04:00
claude 83beebf35c scout-website: greenlanescouts73.org initial deploy (from scoutpoc redesign) 2026-08-24 13:15:13 -04:00