Bug-fix report — 2026-06-15
Defects found and fixed on Ethica.no during the newsletter follow-up and the
CMS (Hugo → live) Phase 1 work. Patch-note summary: Patch/patch-notes-2026-06-15.md.
Newsletter
BUG-1 · A second, bare “Unsubscribe” link rendered below the branded footer
Symptom: a campaign using a branded footer showed two unsubscribe links — the
styled one inside the footer box, plus a stray plain one appended at the very bottom.
Cause: the layout used a {{unsubscribe_url}} slot, but the engine only recognised
{{unsubscribe}}. Not finding it, render_campaign appended its default footer link —
and left href="{{unsubscribe_url}}" literally unfilled (a broken link).
Fix: support {{unsubscribe_url}} (raw href) alongside {{unsubscribe}}; a layout
that provides either slot no longer gets the fallback appended. The fallback still
applies when a layout uses neither, so an email can never go out without a way to
unsubscribe. Covered by a unit test (render_unsubscribe_url_slot).
BUG-2 · Unsubscribed subscribers showed as “pending”
Symptom: after clicking unsubscribe, the subscriber went back to a “pending” state
in the admin list rather than reading as unsubscribed.
Cause: unsubscribe set confirmed=0, and the list renders any confirmed=0 row as
“pending” — one flag overloaded for three states (pending / confirmed / unsubscribed).
Fix: added an unsubscribed_at column (idempotent migration); unsubscribe stamps
it (and clears confirmed so it stays out of sends). The list now shows a distinct
“unsubscribed” badge + stat, and re-confirming clears the flag. Smoke asserts the
stamp + badge.
BUG-3 · Opt-in confirmation sent from the wrong mailer
Symptom: the homepage signup confirmation used a different sender identity than the
newsletter, and could fail silently if only the newsletter SMTP was configured.
Cause: handle_subscribe sent via send_mail (the contact-form mailer on the env
SMTP_* config), not the newsletter SMTP the campaigns use.
Fix: the confirmation now sends over the newsletter SMTP (nl_send_text, with
Date/Message-ID), falling back to the general mailer only when the newsletter SMTP
isn’t configured. Shared _base_headers/_deliver helpers extracted so both paths get
the same headers.
CMS (Hugo → live, Phase 1)
BUG-4 · init_db crashed at container start — semicolons in SQL comments
Symptom: with the new content table added to the schema, the web container failed
to start: init_db raised psycopg.errors.SyntaxError, so the whole app was down
(every request returned nothing). Caught in the Docker smoke during development —
never reached production.
Cause: init_db.main() applies the schema by naively splitting the SQL on ;. Two
of the new table’s comments contained a semicolon — an inline column comment
(-- unix; gates …) and, ironically, a meta note that read “no semicolons in these
comments — splits on ‘;’.” Each ; shattered the CREATE TABLE mid-statement.
Fix: removed every semicolon from the table’s comments. Added a stronger local
check (the parsed content statement must contain its last column and close with )),
replacing the earlier check that only looked at columns before the break. This is the
same class of bug that previously bit the newsletter schema — now guarded explicitly in
the comment and the validation.
BUG-5 · Image-upload smoke assertion too strict (test-only)
Symptom: the new image-upload smoke check failed against a correct response.
Cause: the assertion matched "url":"/uploads/, but json.dumps emits
"url": "/uploads/…" (a space after the colon). The endpoint worked — the GET of the
returned URL passed; only the substring match was wrong.
Fix: corrected the assertion to include the space. No app change.
Files changed
newsletter.py—{{unsubscribe_url}}slot,nl_send_text,_base_headers/_deliver.server.py— opt-in over newsletter SMTP; unsubscribeunsubscribed_at; the live CMS render + image upload/serve.scripts/init_db.py—content+content_imagestables (comments de-semicoloned),subscribers.unsubscribed_at.newsletter_tests.py,.claude/skills/run-ethica-no/smoke.sh— coverage for the above.
Notes
All fixes verified by py_compile, the newsletter unit tests (7/7), and the Docker
smoke (91/0). BUG-4 was found and fixed in development before any deploy; the others
were user-reported on the live newsletter and are now on main.