From 5e3eeb9243e5045f3f84a7e0c2776d2f406a551a Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 24 Aug 2026 13:53:57 -0400 Subject: [PATCH] self-contained lead pipeline (direct sheet+ntfy, +Comments col); fix joinbtn specificity + facts flex shrink-wrap --- app/Dockerfile | 2 +- app/__pycache__/app.cpython-313.pyc | Bin 57758 -> 60987 bytes app/app.py | 108 ++++++++++++++++++++-------- docker-compose.yml | 5 ++ 4 files changed, 83 insertions(+), 32 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 8e1f8ee..ee89588 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.12-slim -RUN pip install --no-cache-dir fastapi "uvicorn[standard]" python-multipart +RUN pip install --no-cache-dir fastapi "uvicorn[standard]" python-multipart gspread WORKDIR /app COPY . /app RUN mkdir -p /app/static/img diff --git a/app/__pycache__/app.cpython-313.pyc b/app/__pycache__/app.cpython-313.pyc index f7ecee611dd15641f2b17d7dc8d305d264384921..0ca86d8fc57cb44c0343743334362efd999217d2 100644 GIT binary patch delta 7746 zcmahu3v?URl{5N|{{AWc+haS%RuW5g9LKQ}C$SwTc0P{di5n6dgd&Y)1zA$w$PN}C zxv*VgpyV?J3WzF+U|!v6dK9`QXH@t0)#H-lx(+K1KDjCdd}{>qmPq7 zPbb!W@7?>}o%`P0@5|G#D1UKDY5u}w(o^uP`_4m0=r>Q93lu8N3$3;skljQlmy&XMMh=ⓈNYhyld9T?2AZ+ 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 delta 4867 zcmaJ^4Nz3q6@K@<{a<$3gK48hRi}x)WiRofY z6HUzsxlYuyQKH6)ZKlE5CNVbsnK5aNW+o7lfUnkI>cpB(W^lAkVmfKheY>k*P0{z= zbI&>V-gC}7=f1PQy~G_p%NcJP3_1q>GI#Fn{QUg^VXd^ohIPM@$n^yxA4{y&dxY3jpbx7n@sXJjmDkBFwTTKj99dk zyfSVHIuo3h=+1T~pW$aTAe;eX0*sk3Cc>BnV-k!r`Q&5<&*E)>*?bkBlFW1>JeyC2 zc@Ccj=-`+0=`wc?Z^v`_6?}#~o5!!@Gi53lEG`av0!W&XE-7mP3N!eb$;^U?4Cgsh zNIkM9%yJs|>=KrrJw=uuk>x~Wjw!N&h-^+oHg}4wFf1#X7m-!-xz3{bET0FaDLT=e zvx~vud~kR^Uw{{YONFxO3&X3kiXy9P_+sZGSUvxq)r-S=wgq?mS~$gq5(q#KePyXd zQMpqOuWr%w8SyE*nie~Y_!4+Z;aLJt89e3iEQO~6o@HQt9AA0Q`AgbS2II7=8JB`# z>`XdSgX)^wm~t+iX+w?&NP%j9k)e8Bp=4EFbKdCX)@Zw>vTOnPoTx?+AMgn7uaPa)s%@ELd#4x zG~*#)oMa_7uRubNi?llJu#9Xm&p{RB7v{#f)MN)M8!20OM+f+lTG-msB?Me}2h5g| zq_`^B*BSRGq~d{r*OMH}uaS|ASr!%22+GHYgRlvv61%oaQg-=$fewkIF`~QR?Enrn zE+Z9ItDeRt9GEuJVogYr17U~+qNUY^-6Gx%I+bLf^@@tV0#{OK>x`L7a@_Xmv0W*S zWa^-UmC$37%GDb1Y!$)|^nsHvuS`lC2eB9r@NFM6xOK>U?1}!a;n>pCEC6l@eX{ah z4t0hGRuM&>5ws-5X1{wo-U}pto-T#tgotg5gj&(Sx*ZUVkLnr_dWdAy zFEPosD>?-1$A>}VpTt|=i5kdQ{j#OhNGt<&A^h4@9fHdn=)gyT^Bp{*0icf=pOH4G z7`DzDHqMe$PD1&lvElcL>*>Nyfa@HyqJKdNcV=Ulr6+`tqlKWNQ~!2 zJ+-}8ncv#8StPDqhe&m2tV%ZP1ZnOZ00X}6Je!c+PglK0;UvKH9c3AX#pJW6f)Jbs zcZF2cDSU#&KK%l0arEiENf|$~T2`dsCF>49WtPp+=C``Mg3s;3m=Aq%c)ud~PN47` zbmMTMQmTpcAIrCGprrgN9tLVk9pPpo9ZSy%L1je>|8&Ob$}=E_!gJWhT%{sfd=@2jP#kG%Qn5%glH>BJc& zB(mxCGB!8>N)FT18wnVaV!HrA+C@p7Co2$ngz?&#TCHiqda7)~Z4Ndq7;NYhz z&{CCaT2i?^Tf6*jsOV}rd0=}QSV`56{XN}c`1+t16BM%>kY00mBh^rbi})Ft4qsA1 zN-OBSBBAXP>XJASt}Z$MOdx{%oAwtj7fm#F_&v}$5z2q#dEEmq0$C?V+`(Z?w?$hd z4$D%=4=MrSj)%J zyHE3FSaSRaVG6^f+?lowJ!#|fq!?K&8C>B?`|lo=IrRcQLYOT zn@k>S ?G-yAN9g6%QHX@{%77N0GYBvR+FBN#k#w0n!;|T+%>1k#Lwhs z@!5IMf|hX8-L)C{9O|x}X?8@-=NGx3V)$+iNaqg zT&8e^!Z`{h6iO-Vq(G&;)^;oiKCjCs$bF!=s5sA$+vP>QTKf)roj)Ly+CB3S!&KjU zP>`O~tIzTI1NL_46?_i)ScmFf#pxC~kfeY<(REz~%VDApc)A4IgZ9HAy_Py*o1LdU zsomoX2v`sUrS@Ldp3|#v*mLc@`fluR$F45>`l^~0vWc}Gw^$0_l8Rmq4DKzwuSa&T z;Bwn@#126S*r9XX2F;MYx^mT;l`9KOK0%#}k?*GpQSb%I z3qAQ@ApVqEZIzT_0DHRS&Q;=k{%w->ZcAK2H&U)yS-FCjl-sZe+*l^F+&x`zy)UP< zst0?$p3Rc7+l56z3Tk04_*(sLK~hCVH5Rt?fd7)ZF-&0E)JSU3^mhwBJP0$ar=SLK znE!v+@#j=?g2EL5iGvFfuaqefx7JD8NK^!OI1nt}A}JvJ-4fa?nn7>cXM|3|pOWz9 z-tC8YAc&703ST@!W`21*`*iMD>DrOfwWFo$#;vvk&H*RcIHZ2taLN#hy`kpxm8fs| zq=x+G%YvYh(HnPn?8zO|6%E^p`j(HYb-PQR{mHn|++RClOdre|^t_TcWEsjD&RjTR zT)3MXH(C2uO|m*=@}*eoz8!mZjK$6xiJdiWOFQ`Fh%NUtce>`Cm~-MV|HubB#uIG^ zS09QUOI-X~`FN^*FnuJoa4fZI=+SenH`I#Rx|>W;VbXu2VPfL;Ed1HxNiCx_+|n^d z%f5|!HjWvxMhscU*Pd1k8?uHC<-6HShWNXJ!Kxtyf)PW}kb68a<%s@}ez0{U(Xo5g zHymRu{ugX&J(BoR;-o^YHBXr0``3PA${bw&n)>*fp{ybQNKy4z(S~!4xgQs87zgR< zf!Hxq_F(bHrtDvGlS(!&_gdcklWo81m{c+b<1G~vmv|s;AZ^S%d&E5Zb+B{*gP8;agUHcT$>ZDM2xZEGeiFOSM`>g3{n~-P0S<*-R1mdcX9zhz-eNuaJZUyLyUOKZL@($D_c1+l9unD-Qp)w$ezxf-C~Pph)l zW-xy)Hr1NAbDRd~bE#FT+G6H>u5NW%jr#l|4%E(<$ylMQHEbSPbvEQGW=6*2c>pXc!7f4&K#!HQ40Gg^i!ZsH*{$MEH_{H zJmp=WK$|0pgKJN2Z=hKzg1O>OY2EvqM^gq(WRF)^if2IK!GA{liY0G-)stY3KcE}X z9WV|U4+e(|){mN+`V3c8hR{3zcm=VOD$@RSeQ+MCLJoin us this fallSee the year', 640)} -
+
Meetings
Tue · Pack 6:00 · Troop 7:00
Where
St. Luke's Lutheran Church
Ages
Kindergarten to 17
@@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index d65c5a7..4cb6cab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,13 @@ services: - "8132:8000" environment: - NTFY_URL=${NTFY_URL} + - SHEET_ID=${SHEET_ID} + - NTFY_BASE=${NTFY_BASE} + - NTFY_TOPIC=${NTFY_TOPIC} + - NTFY_TOKEN=${NTFY_TOKEN} volumes: - /srv/scout-website/data:/data + - /srv/scout-website/secrets/service_account.json:/app/service_account.json:ro - /srv/scout-website-assets/img:/app/static/img:ro networks: - default