Replace fixed 5 s hx-trigger polling with data-poll markers and panel.js
scheduling: 5 s while active, 30 s when idle, none when tab is hidden.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
/sendlog -> /deliveries, /queue -> /mail-queue, /logtail -> /system-log,
along with the HTMX polling fragments under each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sessions move from an in-memory map (absolute 12h TTL) to a `sessions`
table (migration 0002), storing only the SHA-256 of the token. Expiry is
now a sliding idle window (PANEL_SESSION_IDLE_DAYS, default 7, no
absolute cap), extended at most once an hour and never by the
monitoring screens' background polling (GET + HX-Request), so a
forgotten open tab doesn't keep a session alive indefinitely. A login
now survives a container restart or redeploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 14.B. The cookie already satisfied everything the __Host- prefix
requires — Secure, Path=/, no Domain — but as a promise the server makes, not
one the browser enforces. With the prefix the browser refuses to store a
same-named cookie carrying a Domain attribute, which is the second lever the
same-site neighbour from 14.A has: set selfpost_session for the parent domain,
and the browser sends two cookies of that name, oldest first. r.Cookie
returned that first one, so the admin logged in successfully and landed back
on the login form, for as long as the planted cookie lived.
The name has to stay conditional: __Host- is only valid on a Secure cookie, so
with PANEL_COOKIE_SECURE=false the browser would discard the Set-Cookie and
the dev instance would fail to log in with no visible reason. Hence the test
on that branch specifically, not just the production one.
requireAuth now reads r.Cookies() and refuses a request carrying more than one
cookie of the name, with a log line naming the cause. That is the only place
the overwrite becomes visible at all, and unlike the prefix it also works in
the dev shape. Sign-out clears both names, so the upgrade does not leave the
old cookie behind; it does sign the administrator out once, which costs
nothing given sessions live in memory and die on restart anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add/list/delete of sending domains with per-domain DKIM keys and the
OpenDKIM tables that drive signing (spec 6, 7.2.2-4, 7.2.10).
internal/domain:
- Pure-Go RSA-2048 keygen; PKCS#1 PEM written atomically at 0640; the
published DNS TXT record is derived from the key on disk (single source
of truth) rather than persisted. No os/exec for key generation.
- KeyTable/SigningTable fully regenerated from the registry on every
add/delete (idempotent), written atomically; SigningTable via refile:
with *@domain, KeyTable with absolute key paths. Table writer refuses
any unsafe character as a backstop (spec 7.6.4).
- Reload without root: the unprivileged panel signals OpenDKIM through
supervisord (`supervisorctl signal USR1 opendkim`, fixed args, no
shell, no user input — spec 7.6.3). An existing key is reused, never
overwritten, so re-adding a domain keeps its published DNS valid.
- Service orchestrates registry -> key -> table rebuild -> reload, with
rollback of the row if a downstream step fails; delete cascades apps
via the DB FK and removes the key + table entries.
Infra:
- Shared `selfpost` group bridges panel (writes keys) and opendkim
(reads them); /data/opendkim is setgid so panel-created files inherit
the group, keys are 0640, RequireSafeKeys is disabled by design.
- opendkim.conf moves from verify-only (Mode v) to signing (Mode s).
- entrypoint.sh normalises the DKIM tree on every start (ownership,
setgid, perms, empty tables before opendkim starts) — self-healing
after a restore.
- supervisord control socket opened to the `selfpost` group so the panel
can request the reload.
web/store:
- Strict domain-name validation (whitelist [a-z0-9.-], DNS shape, >=2
labels), lower-case normalisation (spec 7.6.2).
- Domain queries with application counts; delete relies on ON DELETE
CASCADE. Dashboard lists domains + add form; domain page shows the
DKIM record; a dedicated confirm page warns about the app cascade
before deletion (spec 7.2.4); manual reload button (spec 7.2.12,
OpenDKIM side; Postfix reload lands in Phase 5).
- Authenticated routes moved to a sub-mux using Go 1.22 method/wildcard
patterns.
Tests: validateDomain, DKIM keygen/record roundtrip, table rendering +
injection-safety, key reuse, store cascade. Verified on the dev server:
gofmt/vet/test green, image builds, container e2e (add/delete a domain,
DKIM record shown, OpenDKIM reads panel keys and reloads, keys and
tables persist across a restart).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>