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.
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.
app/__pycache__/app.cpython-313.pyc was committed and went stale the moment
app.py changed. It was inert because the image runs 3.12 and the bytecode was
3.13, but a stale .pyc that happened to match the interpreter would be a very
quiet way to run the wrong code.
Also ignores service_account.json, which is bind-mounted in at runtime and has
no business being committable.
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
The five troop entries were placeholders written to get the site up. Troop
73's real calendar lives in BAND, so those are replaced by what BAND
actually holds:
- First Court of Honor, Klondike Winter Derby, Troop Spring Campout,
Founder's Day, River Canoe Trip
+ Canoe Day (9/1), Gettysburg Trip (11/21), Lock In (12/12-13),
Cabin Camping at French Creek (1/16-17)
Excluded from BAND: six events already past (the /calendar page has no
date filter), the weekly Tuesday meeting series (runs to May 2027, and
the schema has no recurrence field - it is covered in page copy), and
the 10/17 Cub Scout Campout, which is the same event as the existing
Fall Campout entry and that one carries a start time.
Pack and both entries are untouched; BAND has no pack events.
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.