Replace em-dashes in user-facing copy across the server-rendered pages, static frontend views, and the strings the app injects at runtime, using colons, commas, parentheses or full stops as the context wants. Data placeholder glyphs (a lone "—" for a missing reading) are left alone, since a hyphen there reads as a minus sign in temperature columns. Also tighten the high-visibility surfaces (home hero and meta, about, privacy, city and records ledes, glossary blurbs) toward a plainer, more direct voice while keeping every factual claim intact. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""Monthly-digest signup.
|
|
|
|
The form ships ahead of email delivery on purpose (see models.PendingDigest):
|
|
collecting the list is the slow part, so signups are stored now and confirmed
|
|
once SMTP is live. Nothing is mailed while mailer.enabled() is False.
|
|
|
|
Privacy: the address is used for the digest and nothing else, and the only other
|
|
thing recorded is the bare referrer domain for attribution.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import time
|
|
|
|
from fastapi import Request, Response
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy import select
|
|
|
|
import mailer
|
|
import metrics
|
|
from db import async_session_maker
|
|
from models import PendingDigest
|
|
|
|
# Deliberately permissive: real-world addresses defeat clever patterns, and the
|
|
# confirmation email is the actual validator. This only rejects obvious junk.
|
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s.]+(\.[^@\s.]+)+$")
|
|
|
|
# The same answer for every outcome — new row, duplicate, or throttled. Anything
|
|
# else would turn the endpoint into an oracle for "is this address subscribed?".
|
|
GENERIC_OK = "Check your inbox to confirm your address."
|
|
|
|
# Don't re-send a confirmation to an unconfirmed address more than this often.
|
|
RESEND_INTERVAL_S = 600
|
|
|
|
|
|
def _valid(email: str) -> bool:
|
|
return bool(email) and len(email) <= 320 and bool(_EMAIL_RE.match(email))
|
|
|
|
|
|
async def _store(email: str, place: str | None, source: str | None) -> None:
|
|
"""Upsert the signup. A repeat submit updates the existing row rather than
|
|
creating a second one — the unique constraint on email is the idempotency."""
|
|
async with async_session_maker() as session:
|
|
existing = (
|
|
await session.execute(
|
|
select(PendingDigest).where(PendingDigest.email == email)
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
now = time.time()
|
|
if existing is None:
|
|
session.add(PendingDigest(
|
|
email=email, place_label=place, source=source, created_at=now,
|
|
))
|
|
else:
|
|
if place and not existing.place_label:
|
|
existing.place_label = place
|
|
# Re-subscribing after an unsubscribe re-opens the row.
|
|
existing.unsubscribed_at = None
|
|
if existing.confirmed_at is None:
|
|
if existing.last_sent_at and now - existing.last_sent_at < RESEND_INTERVAL_S:
|
|
await session.commit()
|
|
return
|
|
existing.last_sent_at = now
|
|
await session.commit()
|
|
|
|
# Nothing is sent until a real mail backend is configured; the console
|
|
# backend logs it so the flow can be verified end-to-end without a server.
|
|
if mailer.enabled():
|
|
mailer.send(
|
|
email,
|
|
"Confirm your Thermograph digest",
|
|
"Thanks for signing up for the Thermograph monthly digest.\n\n"
|
|
"Confirmation links are not active yet, but you're on the list, and "
|
|
"you'll get a confirmation as soon as they are.\n",
|
|
)
|
|
|
|
|
|
async def signup(request: Request) -> Response:
|
|
"""POST {BASE}/digest — accepts a JSON body (the fetch path) or a plain form
|
|
post (the no-JS path). Always answers 200 with the same message."""
|
|
email = ""
|
|
place = None
|
|
try:
|
|
ctype = request.headers.get("content-type", "")
|
|
if ctype.startswith("application/json"):
|
|
body = await request.json()
|
|
if isinstance(body, dict):
|
|
email = str(body.get("email") or "").strip().lower()
|
|
place = (str(body.get("place")).strip() or None) if body.get("place") else None
|
|
else:
|
|
form = await request.form()
|
|
email = str(form.get("email") or "").strip().lower()
|
|
place = (str(form.get("place")).strip() or None) if form.get("place") else None
|
|
except Exception: # noqa: BLE001 - a malformed body is just an invalid signup
|
|
pass
|
|
|
|
if _valid(email):
|
|
source = metrics.normalize_referrer(
|
|
request.headers.get("referer"), request.headers.get("host")
|
|
)
|
|
try:
|
|
await _store(email, place, source)
|
|
except Exception: # noqa: BLE001 - never surface storage detail to the form
|
|
pass
|
|
metrics.record_event(
|
|
"home.digest_signup",
|
|
referer=request.headers.get("referer"),
|
|
host=request.headers.get("host"),
|
|
ip=request.client.host if request.client else None,
|
|
)
|
|
return JSONResponse({"ok": True, "message": GENERIC_OK})
|
|
|
|
return JSONResponse(
|
|
{"ok": False, "message": "That doesn't look like an email address."},
|
|
status_code=400,
|
|
)
|