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:
+48
-15
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user