The one string on this site where a deploy is the wrong latency is
"tonight's meeting is cancelled, the lot is flooded" at 4pm on a
Tuesday. That is a record with a lifecycle, not site copy, so it gets
a table and a write path rather than a commit.
store.py
announcements table, created by the existing IF NOT EXISTS path so
there is no migration. ends_at is REQUIRED: an announcement that
never expires is site copy, and site copy belongs in the repo where
it has a diff. Nothing is hard-deleted; taking one down early sets
revoked_at, so what the site said and when survives.
Ranking is urgent first, then most recent. Recency alone would let a
routine Wednesday notice bury a Tuesday cancellation still live.
Two caps, enforced here rather than in the route so the future panel
inherits them: 200 characters, and 3 live at once. Both reject rather
than truncate. Clipping a cancellation mid-sentence is worse than
making someone shorten it, and a 4th live notice is a signal nobody
is expiring things rather than something to render.
app.py
announcement_bar() above the sticky nav. Native <details>, no
JavaScript, which matters on a read-only rootfs with no build step.
Collapsed clamps to one line with a count; expanded lists all of
them and caps at 40vh. One notice renders with no chevron and no
count: the common case must not look like a widget.
FAILS OPEN. The admin API fails closed because it serves family
phone numbers. This is the opposite case, and a broken announcement
must never take down the public homepage.
Colours are the existing note and rust token pairs from the brand
standard. No new colour enters the palette.
admin_api.py
GET/POST/DELETE on /api/admin/announcements. Leads stay read-only;
announcements are the deliberate exception, because being mutable
and expiring is the entire feature rather than a guess at a process.
list returns a computed state per row (live, scheduled, expired,
revoked, over_cap) so "why is my notice not showing" is answerable
from the API and not from the homepage.
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.
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.