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.
This commit is contained in:
Claude
2026-08-29 11:33:43 -04:00
parent 7d2c241126
commit 27f1a2104b
4 changed files with 58 additions and 253 deletions
+48 -15
View File
@@ -1,4 +1,4 @@
import json, os, shutil, datetime, urllib.request, urllib.parse
import json, os, time, datetime, urllib.request, urllib.parse
from pathlib import Path
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -11,15 +11,16 @@ import admin_api
# ---------------------------------------------------------------------------
# Pack & Troop 73 - greenlanescouts73.org
# Calendar lives in ONE file: /data/events.json (seeded from app/events.json on
# first boot). Edit that file on the host (or ask Claude) and every page that
# shows dates updates on the next refresh. No rebuild needed.
# Calendar comes from the scout-calendar JSON feed (EVENTS_FEED_URL) -
# Radicale is the source of truth, edited in any calendar app. The last good
# copy is cached to /data/events-cache.json and served whenever the feed is
# down, so a feed outage never blanks the site.
# ---------------------------------------------------------------------------
DATA = Path("/data"); DATA.mkdir(parents=True, exist_ok=True)
LEADS = DATA / "leads.jsonl"
EVENTS_LIVE = DATA / "events.json"
EVENTS_SEED = Path(__file__).parent / "events.json"
EVENTS_FEED_URL = os.environ.get("EVENTS_FEED_URL", "").strip()
EVENTS_CACHE = DATA / "events-cache.json"
NTFY_URL = os.environ.get("NTFY_URL", "")
# Lead pipeline lives IN THIS REPO (no dependency on the scouting73-form
# container): direct Google Sheet append + ntfy push, same sheet and topic.
@@ -38,23 +39,55 @@ app.mount("/static", StaticFiles(directory="/app/static"), name="static")
store.init(backfill_jsonl=LEADS)
app.include_router(admin_api.router)
if not EVENTS_LIVE.exists() and EVENTS_SEED.exists():
try: shutil.copyfile(EVENTS_SEED, EVENTS_LIVE)
except Exception: pass
MB = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']
MF = ['January','February','March','April','May','June','July','August','September','October','November','December']
DW = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
DWU = ['MON','TUE','WED','THU','FRI','SAT','SUN']
_FEED_TTL = 300 # seconds between feed fetches; a failure also waits this long
_feed_state = {"events": None, "fetched": 0.0}
def _fetch_feed():
rq = urllib.request.Request(EVENTS_FEED_URL, headers={"User-Agent": "scout-website"})
with urllib.request.urlopen(rq, timeout=5) as resp:
events = json.load(resp).get("events", [])
if not isinstance(events, list) or not events:
raise ValueError("feed returned no events")
return events
def load_events():
for p in (EVENTS_LIVE, EVENTS_SEED):
"""Events come from the scout-calendar feed. The last good copy is cached
to /data and served whenever the feed is unreachable or empty - the pages
never go blank because the feed had a bad moment. With no feed and no
cache yet, pages fall back to their 'Nothing on the books' empty state."""
now = time.time()
st = _feed_state
if st["events"] is not None and now - st["fetched"] < _FEED_TTL:
return st["events"]
if EVENTS_FEED_URL:
try:
with open(p, encoding="utf-8") as f:
return json.load(f).get("events", [])
events = _fetch_feed()
st.update(events=events, fetched=now)
try:
tmp = EVENTS_CACHE.with_suffix(".tmp")
tmp.write_text(json.dumps({"events": events}), encoding="utf-8")
tmp.replace(EVENTS_CACHE)
except Exception:
pass
return events
except Exception:
continue
return []
pass
if st["events"] is not None: # feed down: keep serving stale, retry after TTL
st["fetched"] = now
return st["events"]
try:
with open(EVENTS_CACHE, encoding="utf-8") as f:
events = json.load(f).get("events", [])
except Exception:
events = []
if events:
st.update(events=events, fetched=now)
return events
def _dates(ev):
s = datetime.date.fromisoformat(ev["date"])
-226
View File
@@ -1,226 +0,0 @@
{
"updated": "2026-08-24",
"note": "One event per object. Home shows the next 3 automatically; Calendar groups by month with Pack/Troop filters. Fields: date (YYYY-MM-DD), end (optional), time (optional), title, location (optional), unit ('pack' | 'troop' | 'both'; default pack), badge (optional short label).",
"events": [
{
"date": "2026-08-29",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Plank Road City Park",
"unit": "pack"
},
{
"date": "2026-09-01",
"time": "3:00 PM",
"title": "Canoe Day",
"unit": "troop"
},
{
"date": "2026-09-01",
"time": "6:30 PM",
"title": "Kickoff Party",
"location": "Green Lane Park, Morrow Pavilion",
"unit": "both"
},
{
"date": "2026-09-12",
"time": "10:00 AM",
"title": "Lower Frederick Community Day",
"location": "PV Middle School West",
"unit": "both"
},
{
"date": "2026-09-19",
"time": "7:00 AM",
"title": "Farmers Market chocolate booth",
"location": "Green Lane Park",
"unit": "pack"
},
{
"date": "2026-09-20",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Perkiomen Trail, Foy Park",
"unit": "pack"
},
{
"date": "2026-09-22",
"time": "6:00 PM",
"title": "Boxcar Derby",
"location": "St. Luke's",
"unit": "pack",
"badge": "FALL OPEN HOUSE"
},
{
"date": "2026-09-26",
"time": "10:30 AM",
"title": "Upper Frederick Community Day",
"location": "Municipal Building",
"unit": "both"
},
{
"date": "2026-10-02",
"time": "6:00 PM",
"title": "Parent & Scout Movie Night",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2026-10-10",
"title": "Haunted Hayride (scouts performing)",
"location": "Green Lane Park",
"unit": "pack"
},
{
"date": "2026-10-17",
"time": "7:00 AM",
"title": "Farmers Market chocolate booth",
"location": "Green Lane Park",
"unit": "pack"
},
{
"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": "2026-10-27",
"time": "6:00 PM",
"title": "Trunk or Treat pack meeting",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2026-11-07",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Schwenksville",
"unit": "pack"
},
{
"date": "2026-11-21",
"time": "3:00 PM",
"title": "Gettysburg Trip",
"unit": "troop"
},
{
"date": "2026-12-05",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Perkiomen Trail, Crusher Road",
"unit": "pack"
},
{
"date": "2026-12-11",
"time": "6:00 PM",
"title": "Parent & Scout Movie Night",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2026-12-12",
"end": "2026-12-13",
"title": "Lock In",
"unit": "troop"
},
{
"date": "2027-01-16",
"title": "Pack Mario Party",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2027-01-16",
"end": "2027-01-17",
"title": "Cabin Camping",
"location": "French Creek State Park",
"unit": "troop"
},
{
"date": "2027-01-23",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Red Trail, Pennsburg",
"unit": "pack"
},
{
"date": "2027-02-06",
"title": "Blue & Gold banquet",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2027-02-20",
"title": "Pinewood Derby",
"location": "St. Luke's",
"unit": "pack",
"badge": "THE BIG ONE"
},
{
"date": "2027-02-27",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "East Greenville",
"unit": "pack"
},
{
"date": "2027-03-13",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Black Rock Rd, Royersford",
"unit": "pack"
},
{
"date": "2027-03-19",
"time": "6:00 PM",
"title": "Parent & Scout Movie Night",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2027-04-10",
"title": "Pack Spring Campout",
"location": "Camp Garrison",
"unit": "pack",
"badge": "OVERNIGHT"
},
{
"date": "2027-04-24",
"title": "Arrow of Light Crossover & Awards",
"location": "St. Luke's",
"unit": "both"
},
{
"date": "2027-05-08",
"time": "1:00 PM",
"title": "Pack Bike Ride",
"location": "Swamp Creek Park",
"unit": "pack"
},
{
"date": "2027-05-21",
"time": "6:00 PM",
"title": "Parent & Scout Movie Night",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2027-05-25",
"time": "6:00 PM",
"title": "Kickball Party",
"location": "St. Luke's",
"unit": "pack"
},
{
"date": "2027-05-29",
"time": "1:00 PM",
"title": "Pack Hike",
"location": "Perkiomenville",
"unit": "pack"
}
]
}