Copy sweep: pull the seasonal surface into named values

The hero button said 'Join us this fall' while the nav button two
inches above it said 'Join us'. That is the whole problem in one
screenshot: strings that go stale on a schedule, scattered.

Seven values at the top of app.py now hold the program year, the
meeting day, and the two meeting times. Twelve inline meeting-time
strings and four program-year labels read from them, so a time change
is one edit rather than a grep. The block carries a note that this is
not a CMS: anything with a date in it belongs in the calendar feed,
and evergreen copy stays inline where git log can answer what the
site said during recruitment.

Two strings were not stale, they were false out of season:

  'There is nothing to do between now and Tuesday' on the thanks page
  and 'See you Tuesday' on the join CTA both confidently invited
  families to a meeting that did not exist through July. Now 'we will
  be in touch with the next meeting date' and 'See you on a Tuesday'.
  The properly correct fix reads the next meeting from the calendar
  feed and this is not that, but it stops the site making a promise
  it cannot keep in August.

  The chocolate booth does not run all winter. Three places now say
  in season.

'fall grade' becomes 'current grade' on the join label and in
SHEET_HEADERS. The header is only written to an empty sheet, so the
live sheet would have kept saying 'fall grade' forever; sheet_append
now repairs cell E1 in place on the next lead, the same way it
already backfills the Comments header.

thanks_body became an f-string. It had no bare braces, so the
conversion is safe.
This commit is contained in:
claude
2026-09-01 19:35:13 -04:00
parent f0acb50190
commit eeb2ce6244
+63 -35
View File
@@ -31,7 +31,30 @@ NTFY_BASE = os.environ.get("NTFY_BASE", "").rstrip("/")
NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "scouting") NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "scouting")
NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "").strip() NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "").strip()
SHEET_HEADERS = ["Timestamp", "Parent Name", "Email", "Phone", SHEET_HEADERS = ["Timestamp", "Parent Name", "Email", "Phone",
"Children (name + fall grade)", "Interested In", "How They Heard", "Comments"] "Children (name + current grade)", "Interested In", "How They Heard", "Comments"]
# ---------------------------------------------------------------------------
# The seasonal surface.
#
# These are the only strings on the site that go stale on a schedule rather
# than when something actually changes. They live here so a meeting time move
# is one edit instead of twelve, and so the next person can see the whole
# seasonal surface at a glance rather than grepping for it.
#
# This is NOT a CMS and must not grow into one. Anything with a date in it
# belongs in the calendar feed, not here - a recruitment banner should read
# the next join night and render nothing when there is not one. Anything
# else is evergreen copy and belongs inline where git log can answer what
# the site said during recruitment.
# ---------------------------------------------------------------------------
PROGRAM_YEAR = "2026-2027"
MEETING_DAY = "Tuesday"
MEETING_DAYS = "Tuesdays"
MEETING_DAY_ABBR = "Tue"
PACK_TIME = "6:00 PM"
TROOP_TIME = "7:30 PM"
PACK_CLOCK = "6:00"
TROOP_CLOCK = "7:30"
app = FastAPI(title="Pack & Troop 73") app = FastAPI(title="Pack & Troop 73")
app.mount("/static", StaticFiles(directory="/app/static"), name="static") app.mount("/static", StaticFiles(directory="/app/static"), name="static")
@@ -382,11 +405,11 @@ def page(title, body, active=""):
<html lang="en"><head> <html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title> <title>{title}</title>
<meta name="description" content="A Cub Scout pack and a Scout troop in Zieglerville, PA. Tuesdays at St. Luke's Lutheran Church, kindergarten through age 17. Every kid welcome. Come see a meeting."> <meta name="description" content="A Cub Scout pack and a Scout troop in Zieglerville, PA. {MEETING_DAYS} at St. Luke's Lutheran Church, kindergarten through age 17. Every kid welcome. Come see a meeting.">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="Pack &amp; Troop 73"> <meta property="og:site_name" content="Pack &amp; Troop 73">
<meta property="og:title" content="{title}"> <meta property="og:title" content="{title}">
<meta property="og:description" content="A Cub Scout pack and a Scout troop in Zieglerville, PA. Tuesdays at St. Luke's Lutheran Church, kindergarten through age 17. Every kid welcome. Come see a meeting."> <meta property="og:description" content="A Cub Scout pack and a Scout troop in Zieglerville, PA. {MEETING_DAYS} at St. Luke's Lutheran Church, kindergarten through age 17. Every kid welcome. Come see a meeting.">
<meta property="og:image" content="https://greenlanescouts73.org/static/img/autumn-hike.jpg"> <meta property="og:image" content="https://greenlanescouts73.org/static/img/autumn-hike.jpg">
<meta property="og:image:width" content="1920"> <meta property="og:image:width" content="1920">
<meta property="og:image:height" content="1278"> <meta property="og:image:height" content="1278">
@@ -410,9 +433,9 @@ def page(title, body, active=""):
<footer><div class="wrap footgrid"> <footer><div class="wrap footgrid">
<div style="max-width:340px"><div style="display:flex;align-items:center;gap:10px;margin-bottom:12px"><img src="/static/img/patch-73.png" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover"><span class="arch" style="font-weight:800;letter-spacing:.06em;color:#fff">PACK &amp; TROOP 73</span></div> <div style="max-width:340px"><div style="display:flex;align-items:center;gap:10px;margin-bottom:12px"><img src="/static/img/patch-73.png" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover"><span class="arch" style="font-weight:800;letter-spacing:.06em;color:#fff">PACK &amp; TROOP 73</span></div>
<div style="font-size:14px;line-height:1.7">Chartered by St. Luke's Lutheran Church · Zieglerville, PA<br>Cradle of Liberty Council · A family program. Every kid welcome.</div></div> <div style="font-size:14px;line-height:1.7">Chartered by St. Luke's Lutheran Church · Zieglerville, PA<br>Cradle of Liberty Council · A family program. Every kid welcome.</div></div>
<div><div class="fh">VISIT</div><div style="font-size:14px;line-height:1.9">Tuesdays · Pack 6:00 · Troop 7:30<br>St. Luke's Lutheran Church<br>Zieglerville, PA</div></div> <div><div class="fh">VISIT</div><div style="font-size:14px;line-height:1.9">{MEETING_DAYS} · Pack {PACK_CLOCK} · Troop {TROOP_CLOCK}<br>St. Luke's Lutheran Church<br>Zieglerville, PA</div></div>
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">EXPLORE</div> <div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">EXPLORE</div>
<a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">2026-2027 Calendar</a><a href="/documents">Forms &amp; documents</a><a class="gold" href="/join">Join us</a></div> <a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">{PROGRAM_YEAR} Calendar</a><a href="/documents">Forms &amp; documents</a><a class="gold" href="/join">Join us</a></div>
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">FOLLOW</div> <div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">FOLLOW</div>
<a class="fb" href="https://www.facebook.com/Pack73GreenLane" target="_blank" rel="noopener" aria-label="Pack 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Pack 73</a><a class="fb" href="https://www.facebook.com/Troop73GreenLane" target="_blank" rel="noopener" aria-label="Troop 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Troop 73</a></div> <a class="fb" href="https://www.facebook.com/Pack73GreenLane" target="_blank" rel="noopener" aria-label="Pack 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Pack 73</a><a class="fb" href="https://www.facebook.com/Troop73GreenLane" target="_blank" rel="noopener" aria-label="Troop 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Troop 73</a></div>
</div></footer> </div></footer>
@@ -432,9 +455,9 @@ def home():
body = f""" body = f"""
{hero('autumn-hike.jpg', 'center', 'Family Scouting · Zieglerville, PA', 'Where kids find their trail.', {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.', '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 style="display:flex;gap:14px;flex-wrap:wrap;margin-top:28px"><a class="cta" href="/join">Join us</a><a class="cta ghost" href="/calendar">See the year</a></div>', 640)}
<div class="wrap" style="width:100%"><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:30</div></div> <div><div class="fl">Meetings</div><div class="fv">{MEETING_DAY_ABBR} · Pack {PACK_CLOCK} · Troop {TROOP_CLOCK}</div></div>
<div><div class="fl">Where</div><div class="fv">St. Luke's Lutheran Church</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> <div><div class="fl">Ages</div><div class="fv">Kindergarten to 17</div></div>
<div><div class="fl">Everyone</div><div class="fv">Every kid welcome · first visit free</div></div> <div><div class="fl">Everyone</div><div class="fv">Every kid welcome · first visit free</div></div>
@@ -457,11 +480,11 @@ def home():
<div><div class="eyebrow dark">Coming up at 73</div><h2 class="sec" style="margin-bottom:26px">Next up at 73.</h2> <div><div class="eyebrow dark">Coming up at 73</div><h2 class="sec" style="margin-bottom:26px">Next up at 73.</h2>
<div style="display:flex;flex-direction:column;gap:14px">{ev_html}</div> <div style="display:flex;flex-direction:column;gap:14px">{ev_html}</div>
<div style="margin-top:12px;font-size:13px;color:#8B93A3">This list updates itself from the shared 73 calendar.</div> <div style="margin-top:12px;font-size:13px;color:#8B93A3">This list updates itself from the shared 73 calendar.</div>
<a href="/calendar" class="plink" style="display:inline-block;margin-top:14px;text-decoration:none">See the full 2026-2027 calendar →</a></div> <a href="/calendar" class="plink" style="display:inline-block;margin-top:14px;text-decoration:none">See the full {PROGRAM_YEAR} calendar →</a></div>
<div><div class="eyebrow dark">New here?</div><h2 class="sec" style="margin-bottom:26px">Your first visit, step by step.</h2> <div><div class="eyebrow dark">New here?</div><h2 class="sec" style="margin-bottom:26px">Your first visit, step by step.</h2>
<div class="steps"> <div class="steps">
<div class="step"><div class="n">01</div><div><div class="t">Say hello</div><div class="d">Send the interest form and a real leader (a local parent, not a call center) replies within a day or two.</div></div></div> <div class="step"><div class="n">01</div><div><div class="t">Say hello</div><div class="d">Send the interest form and a real leader (a local parent, not a call center) replies within a day or two.</div></div></div>
<div class="step"><div class="n">02</div><div><div class="t">Drop in on a Tuesday</div><div class="d">Come watch a meeting, no commitment and nothing to bring. Kids usually join the fun within ten minutes.</div></div></div> <div class="step"><div class="n">02</div><div><div class="t">Drop in on a {MEETING_DAY}</div><div class="d">Come watch a meeting, no commitment and nothing to bring. Kids usually join the fun within ten minutes.</div></div></div>
<div class="step"><div class="n">03</div><div><div class="t">Take it at your pace</div><div class="d">Register when your family is ready. We help with uniforms, costs, and every question in between.</div></div></div> <div class="step"><div class="n">03</div><div><div class="t">Take it at your pace</div><div class="d">Register when your family is ready. We help with uniforms, costs, and every question in between.</div></div></div>
</div> </div>
<a href="/join" class="cta" style="margin-top:24px;font-size:15px;padding:13px 26px;background:#1E2F52;color:#fff">Start with the form →</a></div> <a href="/join" class="cta" style="margin-top:24px;font-size:15px;padding:13px 26px;background:#1E2F52;color:#fff">Start with the form →</a></div>
@@ -494,7 +517,7 @@ def cubs():
'Cub Scouts is where it starts: derby cars, first hikes, campfire songs, and a den of friends who cheer each other on.')} 'Cub Scouts is where it starts: derby cars, first hikes, campfire songs, and a den of friends who cheer each other on.')}
<section style="padding:76px 0 30px"><div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:44px;align-items:center"> <section style="padding:76px 0 30px"><div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:44px;align-items:center">
<div><div class="eyebrow dark">How it works</div><h2 class="sec">A den for every grade, a pack for every family.</h2> <div><div class="eyebrow dark">How it works</div><h2 class="sec">A den for every grade, a pack for every family.</h2>
<p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">Scouts meet in small grade-level dens, and the whole pack comes together for the big stuff: derbies, campouts, and award nights. Meetings are Tuesdays at 6:00 PM at St. Luke's, so siblings in different dens land on the same night.</p> <p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">Scouts meet in small grade-level dens, and the whole pack comes together for the big stuff: derbies, campouts, and award nights. Meetings are {MEETING_DAYS} at {PACK_TIME} at St. Luke's, so siblings in different dens land on the same night.</p>
<p style="margin:0;color:#5A6272;font-size:1.05rem">A parent or guardian stays for every Cub Scout meeting and event; it is a do-it-together program. Lions and Tigers work hands-on with their grown-up, while older dens mostly need you in the room (and extra hands are always put to work).</p></div> <p style="margin:0;color:#5A6272;font-size:1.05rem">A parent or guardian stays for every Cub Scout meeting and event; it is a do-it-together program. Lions and Tigers work hands-on with their grown-up, while older dens mostly need you in the room (and extra hands are always put to work).</p></div>
<img src="/static/img/lion-craft.jpg" alt="A Lion Cub Scout working on a craft" style="width:100%;height:380px;object-fit:cover;border-radius:18px"> <img src="/static/img/lion-craft.jpg" alt="A Lion Cub Scout working on a craft" style="width:100%;height:380px;object-fit:cover;border-radius:18px">
</div></section> </div></section>
@@ -525,14 +548,14 @@ def cubs():
<div style="background:#fff;border:1px solid #EBE4D4;border-radius:18px;padding:34px 36px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:28px;align-items:center"> <div style="background:#fff;border:1px solid #EBE4D4;border-radius:18px;padding:34px 36px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:28px;align-items:center">
<div><div class="eyebrow dark" style="margin-bottom:8px">Keeping it affordable</div> <div><div class="eyebrow dark" style="margin-bottom:8px">Keeping it affordable</div>
<h2 class="arch" style="margin:0 0 10px;font-weight:800;font-size:1.5rem;color:#1E2F52">The famous chocolate booth.</h2> <h2 class="arch" style="margin:0 0 10px;font-weight:800;font-size:1.5rem;color:#1E2F52">The famous chocolate booth.</h2>
<p style="margin:0;color:#5A6272;font-size:15.5px">You will find us selling chocolate bars at the Green Lane Farmers Market on Saturday mornings. Fundraisers like the booth keep dues low and help every scout get to camp. Cost should never keep a kid out of Scouting; if it is a stretch, talk to us.</p></div> <p style="margin:0;color:#5A6272;font-size:15.5px">You will find us selling chocolate bars at the Green Lane Farmers Market on Saturday mornings in season. Fundraisers like the booth keep dues low and help every scout get to camp. Cost should never keep a kid out of Scouting; if it is a stretch, talk to us.</p></div>
<div style="display:flex;flex-direction:column;gap:10px"> <div style="display:flex;flex-direction:column;gap:10px">
<div class="rrow"><span class="k" style="min-width:88px">Tuesdays</span><span class="v">Den and pack meetings, 6:00 PM at St. Luke's</span></div> <div class="rrow"><span class="k" style="min-width:88px">{MEETING_DAYS}</span><span class="v">Den and pack meetings, {PACK_TIME} at St. Luke's</span></div>
<div class="rrow"><span class="k" style="min-width:88px">Monthly</span><span class="v">Weekend pack hike, usually 1:00 PM</span></div> <div class="rrow"><span class="k" style="min-width:88px">Monthly</span><span class="v">Weekend pack hike, usually 1:00 PM</span></div>
<div class="rrow"><span class="k" style="min-width:88px">Saturdays</span><span class="v">Chocolate booth at the Green Lane Farmers Market, 7:00 AM</span></div> <div class="rrow"><span class="k" style="min-width:88px">Saturdays</span><span class="v">Chocolate booth at the Green Lane Farmers Market, 7:00 AM in season</span></div>
</div></div></div></section> </div></div></div></section>
<div class="band" style="background:linear-gradient(180deg,rgba(13,22,40,.5),rgba(13,22,40,.7)),url('/static/img/autumn-hike.jpg') center/cover no-repeat"> <div class="band" style="background:linear-gradient(180deg,rgba(13,22,40,.5),rgba(13,22,40,.7)),url('/static/img/autumn-hike.jpg') center/cover no-repeat">
<h2>Come try a Tuesday.</h2> <h2>Come try a {MEETING_DAY}.</h2>
<p style="max-width:520px">Your first visit is free and easy: just show up and see if it clicks. Most kids know by the end of the night.</p> <p style="max-width:520px">Your first visit is free and easy: just show up and see if it clicks. Most kids know by the end of the night.</p>
<a class="cta" href="/join" style="padding:15px 34px">Tell us you're coming</a></div>""" <a class="cta" href="/join" style="padding:15px 34px">Tell us you're coming</a></div>"""
return page("Cub Scouts · Pack 73 · Zieglerville, PA", body, "cubs") return page("Cub Scouts · Pack 73 · Zieglerville, PA", body, "cubs")
@@ -547,7 +570,7 @@ def troop():
<section style="padding:76px 0 30px"><div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:44px;align-items:center"> <section style="padding:76px 0 30px"><div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:44px;align-items:center">
<div><div class="eyebrow dark">How it works</div><h2 class="sec">The scouts run it. Really.</h2> <div><div class="eyebrow dark">How it works</div><h2 class="sec">The scouts run it. Really.</h2>
<p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">Troop 73 is organized into patrols that elect their leaders, plan the trips, cook the meals, and fix their own mistakes. Trained, background-checked adults coach from the back row.</p> <p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">Troop 73 is organized into patrols that elect their leaders, plan the trips, cook the meals, and fix their own mistakes. Trained, background-checked adults coach from the back row.</p>
<p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">We meet Tuesdays at 7:30 PM at St. Luke's, right after the pack, so scouting families land on one night. Every kid ages 11 to 17 is welcome, with or without a Cub Scout past. (Officially we charter as a Scouts BSA troop. Around here, it's just the troop.)</p> <p style="margin:0 0 14px;color:#5A6272;font-size:1.05rem">We meet {MEETING_DAYS} at {TROOP_TIME} at St. Luke's, right after the pack, so scouting families land on one night. Every kid ages 11 to 17 is welcome, with or without a Cub Scout past. (Officially we charter as a Scouts BSA troop. Around here, it's just the troop.)</p>
<p style="margin:0;color:#5A6272;font-size:1.05rem">Founded in spring 2026 at St. Luke's, the troop is built by the scouts in it; the traditions are theirs to invent.</p></div> <p style="margin:0;color:#5A6272;font-size:1.05rem">Founded in spring 2026 at St. Luke's, the troop is built by the scouts in it; the traditions are theirs to invent.</p></div>
<img src="/static/img/troop-fire.jpg" alt="Scouts around the campfire circle at dusk" style="width:100%;height:380px;object-fit:cover;border-radius:18px"> <img src="/static/img/troop-fire.jpg" alt="Scouts around the campfire circle at dusk" style="width:100%;height:380px;object-fit:cover;border-radius:18px">
</div></section> </div></section>
@@ -579,7 +602,7 @@ def troop():
<div class="pad"><div class="eyebrow dark" style="margin-bottom:8px">The rhythm</div> <div class="pad"><div class="eyebrow dark" style="margin-bottom:8px">The rhythm</div>
<h2 class="arch" style="margin:0 0 14px;font-weight:800;font-size:1.5rem;color:#1E2F52">One night a week, one adventure a month.</h2> <h2 class="arch" style="margin:0 0 14px;font-weight:800;font-size:1.5rem;color:#1E2F52">One night a week, one adventure a month.</h2>
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:16px"> <div style="display:flex;flex-direction:column;gap:10px;margin-bottom:16px">
<div class="rrow"><span class="k">Tuesdays</span><span class="v">Troop meeting, 7:30 PM at St. Luke's, right after the pack</span></div> <div class="rrow"><span class="k">{MEETING_DAYS}</span><span class="v">Troop meeting, {TROOP_TIME} at St. Luke's, right after the pack</span></div>
<div class="rrow"><span class="k">Most months</span><span class="v">A weekend campout, trip, or service project</span></div> <div class="rrow"><span class="k">Most months</span><span class="v">A weekend campout, trip, or service project</span></div>
<div class="rrow"><span class="k">All year</span><span class="v">Merit badges and rank work at each scout's own pace</span></div> <div class="rrow"><span class="k">All year</span><span class="v">Merit badges and rank work at each scout's own pace</span></div>
</div> </div>
@@ -590,8 +613,8 @@ def troop():
<h2 style="margin-bottom:10px">No experience needed. Curiosity helps.</h2> <h2 style="margin-bottom:10px">No experience needed. Curiosity helps.</h2>
<p style="margin-bottom:0">Plenty of great scouts never spent a day in Cub Scouts. If your kid is 11 to 17 and likes the idea of a weekend outdoors, they belong here.</p></div> <p style="margin-bottom:0">Plenty of great scouts never spent a day in Cub Scouts. If your kid is 11 to 17 and likes the idea of a weekend outdoors, they belong here.</p></div>
<div style="background:#FBF8F1;padding:84px 24px;text-align:center"> <div style="background:#FBF8F1;padding:84px 24px;text-align:center">
<h2 class="arch" style="margin:0 0 12px;font-weight:800;font-size:clamp(1.8rem,3.4vw,2.6rem);color:#1E2F52;letter-spacing:-.01em">Come watch a Tuesday.</h2> <h2 class="arch" style="margin:0 0 12px;font-weight:800;font-size:clamp(1.8rem,3.4vw,2.6rem);color:#1E2F52;letter-spacing:-.01em">Come watch a {MEETING_DAY}.</h2>
<p style="margin:0 auto 28px;color:#5A6272;font-size:1.06rem;max-width:480px">The troop takes over at 7:30 PM at St. Luke's. Drop in, meet the patrols, stay if it clicks.</p> <p style="margin:0 auto 28px;color:#5A6272;font-size:1.06rem;max-width:480px">The troop takes over at {TROOP_TIME} at St. Luke's. Drop in, meet the patrols, stay if it clicks.</p>
<a class="cta" href="/join" style="padding:15px 34px">Message the troop</a></div>""" <a class="cta" href="/join" style="padding:15px 34px">Message the troop</a></div>"""
return page("The Troop · Troop 73 · Zieglerville, PA", body, "troop") return page("The Troop · Troop 73 · Zieglerville, PA", body, "troop")
@@ -621,13 +644,13 @@ def calendar(unit: str = "all"):
body = f""" body = f"""
<div class="calhead"><div class="wrap"> <div class="calhead"><div class="wrap">
<div class="eyebrow gold">Pack &amp; Troop 73 program year</div> <div class="eyebrow gold">Pack &amp; Troop 73 program year</div>
<h1>The 2026-2027 year, at a glance.</h1> <h1>The {PROGRAM_YEAR} year, at a glance.</h1>
<p class="sub">The highlights below are open to visiting families. Come to any of them, or just show up on a Tuesday.</p> <p class="sub">The highlights below are open to visiting families. Come to any of them, or just show up on a {MEETING_DAY}.</p>
<div class="rhythm"> <div class="rhythm">
<div><div class="t">Pack Tuesdays · 6:00 PM</div><div class="d">Pack or den meetings at St. Luke's Lutheran Church (off on school holidays)</div></div> <div><div class="t">Pack {MEETING_DAYS} · {PACK_TIME}</div><div class="d">Pack or den meetings at St. Luke's Lutheran Church (off on school holidays)</div></div>
<div><div class="t">Troop Tuesdays · 7:30 PM</div><div class="d">Troop 73 meets right after the pack, same building</div></div> <div><div class="t">Troop {MEETING_DAYS} · {TROOP_TIME}</div><div class="d">Troop 73 meets right after the pack, same building</div></div>
<div><div class="t">About monthly · weekends</div><div class="d">Pack hikes at rotating trailheads, usually 1:00 PM</div></div> <div><div class="t">About monthly · weekends</div><div class="d">Pack hikes at rotating trailheads, usually 1:00 PM</div></div>
<div><div class="t">Saturdays · 7:00 AM</div><div class="d">Chocolate booth at the Green Lane Farmers Market</div></div> <div><div class="t">Saturdays in season · 7:00 AM</div><div class="d">Chocolate booth at the Green Lane Farmers Market</div></div>
</div></div></div> </div></div></div>
<section style="padding:52px 0 30px"> <section style="padding:52px 0 30px">
<div class="wrap" style="padding-bottom:24px;display:flex;gap:10px;flex-wrap:wrap;align-items:center"> <div class="wrap" style="padding-bottom:24px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
@@ -645,7 +668,7 @@ def calendar(unit: str = "all"):
<section style="padding:26px 0 80px"><div class="wrap"><div class="note"> <section style="padding:26px 0 80px"><div class="wrap"><div class="note">
Dates occasionally shift. Registered families get the live TeamSnap calendar with every den and troop meeting; the newest info always wins there. Dates occasionally shift. Registered families get the live TeamSnap calendar with every den and troop meeting; the newest info always wins there.
</div></div></section>""" </div></div></section>"""
return page("2026-2027 Calendar · Pack & Troop 73", body, "cal") return page(f"{PROGRAM_YEAR} Calendar · Pack & Troop 73", body, "cal")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Documents. Files come from the manifest in /docs (see documents.py); they are # Documents. Files come from the manifest in /docs (see documents.py); they are
@@ -684,7 +707,7 @@ def documents_index():
</div></div> </div></div>
<section style="padding:52px 0 70px"><div class="wrap">{inner}</div></section> <section style="padding:52px 0 70px"><div class="wrap">{inner}</div></section>
<section style="padding:0 0 80px"><div class="wrap"><div class="note"> <section style="padding:0 0 80px"><div class="wrap"><div class="note">
Can't find something, or need a form in another format? Message us on Facebook or catch a leader on a Tuesday night. Can't find something, or need a form in another format? Message us on Facebook or catch a leader on a {MEETING_DAY} night.
</div></div></section>""" </div></div></section>"""
return page("Forms & Documents · Pack & Troop 73", body, "docs") return page("Forms & Documents · Pack & Troop 73", body, "docs")
@@ -711,7 +734,7 @@ FAQ = [
("What does it cost?", "Annual registration plus modest pack or troop dues. Fundraisers like our chocolate booth keep costs down, and quiet help is available. Cost never keeps a kid out of Pack 73; just talk to us."), ("What does it cost?", "Annual registration plus modest pack or troop dues. Fundraisers like our chocolate booth keep costs down, and quiet help is available. Cost never keeps a kid out of Pack 73; just talk to us."),
("Do parents stay at meetings?", "Yes: a parent or guardian stays at every Cub Scout meeting and event. Lions and Tigers pair up with their grown-up for the hands-on parts; for older dens, being in the room is enough. Troop scouts (11 to 17) can be dropped off."), ("Do parents stay at meetings?", "Yes: a parent or guardian stays at every Cub Scout meeting and event. Lions and Tigers pair up with their grown-up for the hands-on parts; for older dens, being in the room is enough. Troop scouts (11 to 17) can be dropped off."),
("Which program fits my kid?", "K through 5th grade join Pack 73's dens. Ages 11 to 17 join Troop 73. Fifth graders get the best of both: Arrow of Light now, crossover to the troop in spring."), ("Which program fits my kid?", "K through 5th grade join Pack 73's dens. Ages 11 to 17 join Troop 73. Fifth graders get the best of both: Arrow of Light now, crossover to the troop in spring."),
("Can we join mid-year?", "Any Tuesday works. Badges and adventures meet kids where they are, so a January start is just as good as a September one."), ("Can we join mid-year?", f"Any {MEETING_DAY} works. Badges and adventures meet kids where they are, so a January start is just as good as a September one."),
("What about uniforms and gear?", "Start with sneakers and a water bottle. Uniform pieces come gradually, and we keep a stash of hand-me-downs. Nobody needs camping gear to start."), ("What about uniforms and gear?", "Start with sneakers and a water bottle. Uniform pieces come gradually, and we keep a stash of hand-me-downs. Nobody needs camping gear to start."),
("Who can join?", "Every kid. Pack 73 and Troop 73 are open to all children and teens, whatever their background or beliefs. If your kid wants to camp, build, and belong, there's a spot for them."), ("Who can join?", "Every kid. Pack 73 and Troop 73 are open to all children and teens, whatever their background or beliefs. If your kid wants to camp, build, and belong, there's a spot for them."),
] ]
@@ -721,16 +744,16 @@ SOURCE_JS = """<script>
</script>""" </script>"""
def thanks_body(): def thanks_body():
return """ return f"""
<section style="padding:72px 0 40px"><div class="wrap" style="max-width:760px"> <section style="padding:72px 0 40px"><div class="wrap" style="max-width:760px">
<div class="success" style="font-size:1.05rem;padding:20px 24px;margin-bottom:30px">Thanks! Your note is on its way. A leader will be in touch soon.</div> <div class="success" style="font-size:1.05rem;padding:20px 24px;margin-bottom:30px">Thanks! Your note is on its way. A leader will be in touch soon.</div>
<div class="eyebrow dark">Join Pack &amp; Troop 73</div> <div class="eyebrow dark">Join Pack &amp; Troop 73</div>
<h1 class="arch" style="margin:0 0 16px;font-weight:800;font-size:clamp(2rem,4.5vw,3rem);line-height:1.05;letter-spacing:-.015em;color:#1E2F52;max-width:16ch">We got it. Here is what happens next.</h1> <h1 class="arch" style="margin:0 0 16px;font-weight:800;font-size:clamp(2rem,4.5vw,3rem);line-height:1.05;letter-spacing:-.015em;color:#1E2F52;max-width:16ch">We got it. Here is what happens next.</h1>
<p style="margin:0 0 30px;color:#5A6272;font-size:1.08rem;max-width:560px">No commitment, and your first visit is always free. There is nothing to do between now and Tuesday.</p> <p style="margin:0 0 30px;color:#5A6272;font-size:1.08rem;max-width:560px">No commitment, and your first visit is always free. There is nothing else you need to do - we will be in touch with the next meeting date.</p>
<div style="display:flex;flex-direction:column;gap:16px;margin-bottom:30px"> <div style="display:flex;flex-direction:column;gap:16px;margin-bottom:30px">
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">1</span><span class="v" style="font-size:15.5px"><strong>Your form is in.</strong> It went straight to our volunteer leaders' phones.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">1</span><span class="v" style="font-size:15.5px"><strong>Your form is in.</strong> It went straight to our volunteer leaders' phones.</span></div>
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">2</span><span class="v" style="font-size:15.5px"><strong>Hear back within a day or two</strong> from a real Pack or Troop 73 parent, with the right den or troop and directions.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">2</span><span class="v" style="font-size:15.5px"><strong>Hear back within a day or two</strong> from a real Pack or Troop 73 parent, with the right den or troop and directions.</span></div>
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">3</span><span class="v" style="font-size:15.5px"><strong>Visit a Tuesday</strong> at St. Luke's: Pack 6:00, Troop 7:30. Sneakers and a water bottle are all you need.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">3</span><span class="v" style="font-size:15.5px"><strong>Visit a {MEETING_DAY}</strong> at St. Luke's: Pack {PACK_CLOCK}, Troop {TROOP_CLOCK}. Sneakers and a water bottle are all you need.</span></div>
</div> </div>
<div class="note">If a couple of days go by with nothing, check your spam folder, then send the form again. We would rather hear twice than miss you.</div> <div class="note">If a couple of days go by with nothing, check your spam folder, then send the form again. We would rather hear twice than miss you.</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:30px"> <div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:30px">
@@ -754,7 +777,7 @@ def join_body(error=""):
<div style="display:flex;flex-direction:column;gap:16px;margin-bottom:30px"> <div style="display:flex;flex-direction:column;gap:16px;margin-bottom:30px">
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">1</span><span class="v" style="font-size:15.5px"><strong>Send the form.</strong> It goes straight to our volunteer leaders' phones.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">1</span><span class="v" style="font-size:15.5px"><strong>Send the form.</strong> It goes straight to our volunteer leaders' phones.</span></div>
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">2</span><span class="v" style="font-size:15.5px"><strong>Hear back within a day or two</strong> from a real Pack or Troop 73 parent.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">2</span><span class="v" style="font-size:15.5px"><strong>Hear back within a day or two</strong> from a real Pack or Troop 73 parent.</span></div>
<div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">3</span><span class="v" style="font-size:15.5px"><strong>Visit a Tuesday</strong> at St. Luke's: Pack 6:00, Troop 7:30. Sneakers recommended.</span></div> <div class="rrow"><span class="k arch" style="min-width:32px;font-size:20px">3</span><span class="v" style="font-size:15.5px"><strong>Visit a {MEETING_DAY}</strong> at St. Luke's: Pack {PACK_CLOCK}, Troop {TROOP_CLOCK}. Sneakers recommended.</span></div>
</div> </div>
<img src="/static/img/circle-game.jpg" alt="Scouts cheering during a pack game" style="width:100%;height:280px;object-fit:cover;border-radius:16px"></div> <img src="/static/img/circle-game.jpg" alt="Scouts cheering during a pack game" style="width:100%;height:280px;object-fit:cover;border-radius:16px"></div>
<div> <div>
@@ -769,7 +792,7 @@ def join_body(error=""):
<div><label>Phone</label><input name="phone" inputmode="tel" placeholder="(610) 555-0100"></div> <div><label>Phone</label><input name="phone" inputmode="tel" placeholder="(610) 555-0100"></div>
<div><label>Interested in <span class="req">*</span></label><select name="interested_in" required><option value="" disabled selected>Select one...</option><option value="Pack 73 (Cub Scouts, K-5)">Pack 73: Cub Scouts (K-5)</option><option value="Troop 73 (Scouts BSA, 11-17)">Troop 73: ages 11-17</option><option value="Both / kids spanning ages">Both: kids spanning ages</option></select></div> <div><label>Interested in <span class="req">*</span></label><select name="interested_in" required><option value="" disabled selected>Select one...</option><option value="Pack 73 (Cub Scouts, K-5)">Pack 73: Cub Scouts (K-5)</option><option value="Troop 73 (Scouts BSA, 11-17)">Troop 73: ages 11-17</option><option value="Both / kids spanning ages">Both: kids spanning ages</option></select></div>
</div> </div>
<div><label>Child(ren): names and grades this fall <span class="req">*</span></label><textarea name="children" rows="2" required placeholder="Emma (Kindergarten), Jack (2nd grade)"></textarea><div style="font-size:12.5px;color:#8B93A3;margin-top:4px">List all kids you're considering. Any grade K-12 welcome.</div></div> <div><label>Child(ren): names and current grades <span class="req">*</span></label><textarea name="children" rows="2" required placeholder="Emma (Kindergarten), Jack (2nd grade)"></textarea><div style="font-size:12.5px;color:#8B93A3;margin-top:4px">List all kids you're considering. Any grade K-12 welcome.</div></div>
<div><label>How did you hear about us? <span class="req">*</span></label><select name="source" id="source" required><option value="" disabled selected>Select one...</option> <div><label>How did you hear about us? <span class="req">*</span></label><select name="source" id="source" required><option value="" disabled selected>Select one...</option>
<optgroup label="Daycares &amp; Preschools"><option>New Hanover Child Care</option><option>Jerusalem Lutheran Nursery</option><option>Goddard School Schwenksville</option><option>St. Teresa Early Learning</option></optgroup> <optgroup label="Daycares &amp; Preschools"><option>New Hanover Child Care</option><option>Jerusalem Lutheran Nursery</option><option>Goddard School Schwenksville</option><option>St. Teresa Early Learning</option></optgroup>
<optgroup label="Elementary Schools"><option>Schwenksville Elementary</option><option>Marlborough Elementary (Green Lane)</option><option>New Hanover Elementary</option><option>Upper Perkiomen 4/5 Center</option></optgroup> <optgroup label="Elementary Schools"><option>Schwenksville Elementary</option><option>Marlborough Elementary (Green Lane)</option><option>New Hanover Elementary</option><option>Upper Perkiomen 4/5 Center</option></optgroup>
@@ -790,8 +813,8 @@ def join_body(error=""):
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px">{faq_html}</div> <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px">{faq_html}</div>
</div></section> </div></section>
<div class="band" style="background:linear-gradient(180deg,rgba(13,22,40,.5),rgba(13,22,40,.68)),url('/static/img/campfire-dusk.jpg') center/cover no-repeat;padding:88px 24px"> <div class="band" style="background:linear-gradient(180deg,rgba(13,22,40,.5),rgba(13,22,40,.68)),url('/static/img/campfire-dusk.jpg') center/cover no-repeat;padding:88px 24px">
<h2 style="font-size:clamp(1.8rem,3.4vw,2.6rem);margin-bottom:12px">See you Tuesday.</h2> <h2 style="font-size:clamp(1.8rem,3.4vw,2.6rem);margin-bottom:12px">See you on a {MEETING_DAY}.</h2>
<p style="margin-bottom:0;max-width:480px">St. Luke's Lutheran Church, Zieglerville · Pack 6:00, Troop 7:30. Look for the kids having the most fun in the building.</p></div>""" <p style="margin-bottom:0;max-width:480px">St. Luke's Lutheran Church, Zieglerville · Pack {PACK_CLOCK}, Troop {TROOP_CLOCK}. Look for the kids having the most fun in the building.</p></div>"""
@app.get("/join", response_class=HTMLResponse) @app.get("/join", response_class=HTMLResponse)
def join_get(sent: int = 0): def join_get(sent: int = 0):
@@ -811,8 +834,13 @@ def sheet_append(rec):
vals = sheet.get_all_values() vals = sheet.get_all_values()
if not vals: if not vals:
sheet.append_row(SHEET_HEADERS) sheet.append_row(SHEET_HEADERS)
elif len(vals[0]) == 7: else:
sheet.update_cell(1, 8, "Comments") if len(vals[0]) == 7:
sheet.update_cell(1, 8, "Comments")
# The header used to read "fall grade". Repair it in place rather than
# leaving the live sheet disagreeing with the form label.
if len(vals[0]) >= 5 and vals[0][4] != SHEET_HEADERS[4]:
sheet.update_cell(1, 5, SHEET_HEADERS[4])
# The submission time, not the time of the write. A replay months later must # The submission time, not the time of the write. A replay months later must
# not relabel a lead as arriving today. # not relabel a lead as arriving today.
stamp = (rec.get("ts") or "")[:16].replace("T", " ") \ stamp = (rec.get("ts") or "")[:16].replace("T", " ") \