self-contained lead pipeline (direct sheet+ntfy, +Comments col); fix joinbtn specificity + facts flex shrink-wrap
This commit is contained in:
+77
-31
@@ -16,9 +16,15 @@ LEADS = DATA / "leads.jsonl"
|
||||
EVENTS_LIVE = DATA / "events.json"
|
||||
EVENTS_SEED = Path(__file__).parent / "events.json"
|
||||
NTFY_URL = os.environ.get("NTFY_URL", "")
|
||||
# Same lead pipeline as the flyer form (scouting73.thewichersfamily.com):
|
||||
# forward /join posts to that container's /submit (Google Sheet + ntfy push).
|
||||
FORM_UPSTREAM = os.environ.get("FORM_UPSTREAM", "http://scouting73-form:8000/submit")
|
||||
# Lead pipeline lives IN THIS REPO (no dependency on the scouting73-form
|
||||
# container): direct Google Sheet append + ntfy push, same sheet and topic.
|
||||
SHEET_ID = os.environ.get("SHEET_ID", "").strip()
|
||||
SA_FILE = os.environ.get("SA_FILE", "/app/service_account.json")
|
||||
NTFY_BASE = os.environ.get("NTFY_BASE", "").rstrip("/")
|
||||
NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "scouting")
|
||||
NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "").strip()
|
||||
SHEET_HEADERS = ["Timestamp", "Parent Name", "Email", "Phone",
|
||||
"Children (name + fall grade)", "Interested In", "How They Heard", "Comments"]
|
||||
|
||||
app = FastAPI(title="Pack & Troop 73")
|
||||
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
|
||||
@@ -93,8 +99,8 @@ nav.links{display:flex;align-items:center;gap:26px;flex-wrap:wrap}
|
||||
nav.links a{text-decoration:none;font-weight:600;font-size:15px;color:#333D52;padding:6px 0}
|
||||
nav.links a:hover{color:#1E2F52}
|
||||
nav.links a.on{font-weight:700;color:#1E2F52;border-bottom:2px solid #F0AD2D}
|
||||
a.joinbtn{display:inline-block;line-height:1.2;font-family:'Archivo';font-weight:700;font-size:15px;background:#F0AD2D;color:#1E2F52 !important;padding:11px 22px;border-radius:999px;border-bottom:none !important}
|
||||
a.joinbtn:hover{background:#DB9714}
|
||||
nav.links a.joinbtn{display:inline-block;line-height:1.2;font-family:'Archivo';font-weight:700;font-size:15px;background:#F0AD2D;color:#1E2F52 !important;padding:11px 22px;border-radius:999px;border-bottom:none !important}
|
||||
nav.links a.joinbtn:hover{background:#DB9714}
|
||||
.hero{position:relative;display:flex;align-items:flex-end;min-height:520px}
|
||||
.hero .wrap{padding-top:110px;padding-bottom:58px;width:100%}
|
||||
.eyebrow{font-family:'Archivo';font-weight:700;font-size:13px;letter-spacing:.2em;text-transform:uppercase;margin-bottom:12px}
|
||||
@@ -257,7 +263,7 @@ def home():
|
||||
{hero('autumn-hike.jpg', 'center', 'Family Scouting · Zieglerville, PA', 'Where kids find their trail.',
|
||||
'A Cub Scout pack and a Scout troop for kindergarten through age 17. Camp, build, lead, and grow alongside friends, right here in the Perkiomen Valley.',
|
||||
'<div style="display:flex;gap:14px;flex-wrap:wrap;margin-top:28px"><a class="cta" href="/join">Join us this fall</a><a class="cta ghost" href="/calendar">See the year</a></div>', 640)}
|
||||
<div class="wrap"><div class="facts">
|
||||
<div class="wrap" style="width:100%"><div class="facts">
|
||||
<div><div class="fl">Meetings</div><div class="fv">Tue · Pack 6:00 · Troop 7:00</div></div>
|
||||
<div><div class="fl">Where</div><div class="fv">St. Luke's Lutheran Church</div></div>
|
||||
<div><div class="fl">Ages</div><div class="fv">Kindergarten to 17</div></div>
|
||||
@@ -541,6 +547,57 @@ def join_get(sent: int = 0):
|
||||
def contact_redirect():
|
||||
return RedirectResponse(url="/join", status_code=301)
|
||||
|
||||
def sheet_append(rec):
|
||||
import gspread
|
||||
from google.oauth2.service_account import Credentials
|
||||
creds = Credentials.from_service_account_file(SA_FILE, scopes=["https://www.googleapis.com/auth/spreadsheets"])
|
||||
sheet = gspread.authorize(creds).open_by_key(SHEET_ID).sheet1
|
||||
vals = sheet.get_all_values()
|
||||
if not vals:
|
||||
sheet.append_row(SHEET_HEADERS)
|
||||
elif len(vals[0]) == 7:
|
||||
sheet.update_cell(1, 8, "Comments")
|
||||
sheet.append_row([
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
rec["parent_name"], rec["email"], rec["phone"], rec["children"],
|
||||
rec["interested_in"], rec["source"], rec["message"],
|
||||
])
|
||||
|
||||
def pipeline_notify(rec, sheet_error=None):
|
||||
"""Push to the shared 'scouting' ntfy topic (JSON endpoint, UTF-8 safe)."""
|
||||
if not NTFY_BASE:
|
||||
return False
|
||||
lines = [
|
||||
"Email: %s" % rec["email"],
|
||||
"Phone: %s" % (rec["phone"] or "(none given)"),
|
||||
"Kids: %s" % rec["children"],
|
||||
"Wants: %s" % rec["interested_in"],
|
||||
]
|
||||
if rec["source"]:
|
||||
lines.append("Heard via: %s" % rec["source"])
|
||||
if rec["message"]:
|
||||
lines.append("Note: %s" % rec["message"])
|
||||
if sheet_error:
|
||||
lines.append("")
|
||||
lines.append("!! NOT saved to the sheet: %s" % sheet_error)
|
||||
lines.append("!! This push is the only copy - record it manually.")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if NTFY_TOKEN:
|
||||
headers["Authorization"] = "Bearer " + NTFY_TOKEN
|
||||
try:
|
||||
body = json.dumps({
|
||||
"topic": NTFY_TOPIC,
|
||||
"title": "New Pack 73 lead - %s" % rec["parent_name"],
|
||||
"message": "\n".join(lines),
|
||||
"tags": ["tent", "inbox_tray"],
|
||||
"priority": 5 if sheet_error else 4,
|
||||
}).encode()
|
||||
rq = urllib.request.Request(NTFY_BASE, data=body, headers=headers)
|
||||
with urllib.request.urlopen(rq, timeout=5) as resp:
|
||||
return resp.status < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@app.post("/join")
|
||||
def join_post(parent_name: str = Form(...), email: str = Form(...), phone: str = Form(""),
|
||||
children: str = Form(""), interested_in: str = Form(""), source: str = Form(""),
|
||||
@@ -551,38 +608,27 @@ def join_post(parent_name: str = Form(...), email: str = Form(...), phone: str =
|
||||
source = "Other: " + source_other.strip()
|
||||
rec = {"ts": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"parent_name": parent_name.strip(), "email": email.strip(), "phone": phone.strip(),
|
||||
"children": children.strip(), "interested_in": interested_in.strip(),
|
||||
"source": source.strip(), "message": message.strip()}
|
||||
"children": children.strip() or "(not given)",
|
||||
"interested_in": interested_in.strip() or "(not given)",
|
||||
"source": source.strip() or "greenlanescouts73.org", "message": message.strip()}
|
||||
try:
|
||||
with open(LEADS, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
# Forward into the shared lead pipeline (Google Sheet + ntfy on the 'scouting'
|
||||
# topic), same as scouting73.thewichersfamily.com. The comment rides along in
|
||||
# the source column so it reaches the sheet and the phone push.
|
||||
src = rec["source"] or "greenlanescouts73.org"
|
||||
if rec["message"]:
|
||||
src = src + " | Note: " + rec["message"][:300]
|
||||
forwarded = False
|
||||
sheet_error = None
|
||||
try:
|
||||
data = urllib.parse.urlencode({
|
||||
"parent_name": rec["parent_name"], "email": rec["email"], "phone": rec["phone"],
|
||||
"children": rec["children"] or "(not given)",
|
||||
"interested_in": rec["interested_in"] or "(not given)", "source": src,
|
||||
}).encode()
|
||||
rq = urllib.request.Request(FORM_UPSTREAM, data=data)
|
||||
with urllib.request.urlopen(rq, timeout=6) as resp:
|
||||
forwarded = resp.status < 400
|
||||
except Exception:
|
||||
forwarded = False
|
||||
if not forwarded and NTFY_URL:
|
||||
# fallback push so a lead is never silently lost
|
||||
sheet_append(rec)
|
||||
except Exception as e:
|
||||
sheet_error = str(e)
|
||||
print("SHEET WRITE FAILED for %s <%s>: %s" % (rec["parent_name"], rec["email"], e), flush=True)
|
||||
delivered = pipeline_notify(rec, sheet_error)
|
||||
if sheet_error and not delivered and NTFY_URL:
|
||||
# last-ditch push on the fallback topic so a lead is never silently lost
|
||||
try:
|
||||
b = ("New Scout interest: %s (%s) - %s | Kids: %s | %s" % (
|
||||
rec["parent_name"], rec["email"], rec["interested_in"] or "program TBD",
|
||||
rec["children"] or "?", src)).encode()
|
||||
rq = urllib.request.Request(NTFY_URL, data=b, headers={"Title": "greenlanescouts73.org lead (sheet forward FAILED)", "Tags": "tent,warning"})
|
||||
b = ("LEAD (sheet+push failed): %s (%s) | Kids: %s | %s | %s" % (
|
||||
rec["parent_name"], rec["email"], rec["children"], rec["interested_in"], rec["source"])).encode()
|
||||
rq = urllib.request.Request(NTFY_URL, data=b, headers={"Title": "greenlanescouts73.org lead", "Tags": "tent,warning"})
|
||||
urllib.request.urlopen(rq, timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user