test / test (push) Has been cancelled
Nothing from the 2026-08-19 code review now lives only in its plan file. Four new roadmap items, all `candidate`: - csrf-tokens — §4.4. The review recorded a decision to add per-session tokens but never turned it into a task, and security.md still argues the header checks suffice. The item says to settle which of the two stands first. - template-data-typing — §7.1, map[string]any template payloads. - structured-logging — §7.2 and §5.5, log/slog plus the per-package logf aliases. - review-2026-08-followups — the small remainder: backup re-auth, the missing DMARC-service / handler / error-page / corrupted-backup tests, and the nine minor findings (§6.1, §6.2, §6.4, §7.4, §8.6, §9.3, §10.3-10.5) that were never carried into the phase tables. §7.3 (build tags for the /proc reader) is deliberately not carried over: it was declined during the pass, and both documents now say so, so it does not get re-raised at the next review. The plan's status table links each open row to its roadmap item, and the roadmap's recommended-order note points back at the plan. Also unlinked two CHANGELOG references to plan files deleted after those releases shipped; the text stays, the dead links do not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
316 lines
14 KiB
Markdown
316 lines
14 KiB
Markdown
# Roadmap: open work (1.x+)
|
||
|
||
**Status:** a working tracker of extensions to the v1.0 boundary, each taken up
|
||
only after explicit agreement ([product.md](product.md),
|
||
[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)). Detailed
|
||
design lives in [plans/](plans/). Items marked `candidate` need an OK before
|
||
any code is written.
|
||
|
||
**Reading this from outside the project:** nothing here is a commitment or a
|
||
release promise. There are no dates, the order is a recommendation rather than
|
||
a schedule, and an item can be dropped or reshaped once its plan is written.
|
||
What the project *will not* do is a separate question, answered in
|
||
[product.md](product.md) — an item's absence from this file does not mean it is
|
||
planned but unlisted.
|
||
|
||
**Versioning:** SemVer MINOR in the **1.x+** line by default (`1.1.0`…), as long
|
||
as defaults and migrations stay compatible with `1.0.0`. A major `2.x` only for
|
||
an explicit break. One such break, when 2.x is cut for any reason, is
|
||
[schema-squash](#schema-squash) — replacing the 1.x SQLite migration chain
|
||
with a baseline. That item does not by itself justify a major.
|
||
|
||
**Process:** [development.md](development.md). The history of closed phases is
|
||
in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||
|
||
---
|
||
|
||
## Index
|
||
|
||
| ID | Topic | Status | Progress | Plan |
|
||
|---|---|---|---|---|
|
||
| preflight | Installation check page (`/preflight`) | candidate | — | [plans/preflight.md](plans/preflight.md) |
|
||
| contributing | `CONTRIBUTING.md` | candidate | — | — |
|
||
| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | agreed | 1/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) |
|
||
| inbound-quarantine | Inbound spam quarantine (hold / review / release) | candidate | — | [plans/inbound-quarantine.md](plans/inbound-quarantine.md) |
|
||
| csrf-tokens | Per-session CSRF tokens on state-changing forms | candidate | — | — |
|
||
| template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — |
|
||
| structured-logging | `log/slog` with levels and fields | candidate | — | — |
|
||
| review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | [plans/code-review-2026-08.md](plans/code-review-2026-08.md) |
|
||
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
|
||
|
||
**Recommended order** (not binding): the next feature is
|
||
**inbound-antispam-panel** (`1.10.0`). panel-docs shipped in
|
||
[CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]`
|
||
(security review of the ingest path pending); domain-stats-auto-ratelimit in
|
||
`[1.6.0]`; send-log-retention in `[1.5.0]`; inbound-relay in `[1.4.0]`;
|
||
queue-retries in `[1.3.1]`; the 2026-08-13 full-tree review follow-ups are in
|
||
`[1.3.0]`. Candidates need explicit agreement before they join the queue.
|
||
|
||
The 2026-08-19 code review ([plans/code-review-2026-08.md](plans/code-review-2026-08.md))
|
||
is closed except for four items, all listed above and none of them urgent:
|
||
[csrf-tokens](#csrf-tokens), [template-data-typing](#template-data-typing),
|
||
[structured-logging](#structured-logging) and
|
||
[review-2026-08-followups](#review-2026-08-followups). What that review
|
||
already produced is in [CHANGELOG.md](../CHANGELOG.md) `[Unreleased]`; the
|
||
per-task status table is at the end of its plan file.
|
||
|
||
After a context reset, pick an item marked `agreed` or `in progress`, then work
|
||
the **Implementation checklist** in its linked plan. The `Progress` column above
|
||
is `done/total` checklist steps in that plan ([development.md](development.md)
|
||
§ Plan checklists).
|
||
|
||
---
|
||
|
||
## contributing
|
||
|
||
**Goal:** `CONTRIBUTING.md` in the root — the dev loop, the checks to run
|
||
before a PR, the commit protocol; [development.md](development.md) links to it
|
||
rather than repeating it.
|
||
|
||
**Boundary:** process documentation; worth writing once there is an external
|
||
flow of PRs.
|
||
|
||
**Done when:** the file is in the root and development.md does not duplicate
|
||
it.
|
||
|
||
**Dependencies / risks:** with a single developer and no PRs, this is low
|
||
priority.
|
||
**Version:** no bearing on semver.
|
||
|
||
---
|
||
|
||
## panel-docs
|
||
|
||
**Status:** done — shipped in `[1.8.0]` (2026-08-18). The implementation plan
|
||
file was removed as shipped cleanup; release details remain in
|
||
[CHANGELOG.md](../CHANGELOG.md).
|
||
|
||
**Goal:** built-in operator documentation in the panel — short pages (or a
|
||
help drawer) that explain what each Status check and other controls mean,
|
||
without sending the operator out to `docs/guide.md`.
|
||
|
||
**Boundary:** in-panel help only; not a second copy of the full operator guide.
|
||
Seed content includes the Status blurbs removed from the cards in favour of a
|
||
denser layout — Machine (kernel counters / rate window), TLS certificate
|
||
(port 465, reverse-proxy mount), Hostname / reverse DNS (forward-confirmed
|
||
reverse DNS, PTR at the hosting provider), and similar notes for other panel
|
||
surfaces as they lose inline commentary.
|
||
|
||
**Done when:** an operator can open help from the panel for those topics; the
|
||
removed Status blurbs are preserved there (or equivalent); no requirement to
|
||
read the git tree for day-to-day meaning of a card.
|
||
|
||
**Dependencies / risks:** copy ownership and translation; keeping help in sync
|
||
when checks change; not bloating every page with a second column of prose.
|
||
|
||
**Version:** shipped `1.8.0`.
|
||
|
||
---
|
||
|
||
## inbound-antispam-panel
|
||
|
||
**Goal:** panel view of inbound anti-spam decisions (when, from, to, subject,
|
||
action) plus editable allow/deny lists that tune the external filter — without
|
||
shipping rspamd or another engine inside the image.
|
||
|
||
**Boundary:** metadata journal and list sync only; SelfPost does not run the
|
||
filter ([product.md](product.md)). Quarantine:
|
||
[inbound-quarantine](#inbound-quarantine). Design closed in
|
||
[plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) § Decisions.
|
||
|
||
**Done when:** with inbound relay enabled, the operator sees journal rows
|
||
(accepts via inbound-journal milter, rejects via mail.log tailer) and can
|
||
maintain instance-wide allow/deny lists synced to rspamd maps when the antispam
|
||
hook is set.
|
||
|
||
**Dependencies / risks:** inbound relay `[1.4.0]`; rspamd map/header coupling;
|
||
journal PII; global-admin-only RBAC.
|
||
|
||
**Version:** `1.10.0` MINOR; **agreed**.
|
||
|
||
---
|
||
|
||
## inbound-quarantine
|
||
|
||
**Goal:** optional hold for suspicious inbound mail — review in the panel, then
|
||
release to upstream, discard, or tune lists. Design not settled.
|
||
|
||
**Boundary:** expands inbound from pure relay toward short-lived message
|
||
storage; may conflict with "no mailboxes" in [product.md](product.md) until
|
||
product explicitly agrees. Alternatives (rspamd-only quarantine vs SelfPost
|
||
store) are listed in [plans/inbound-quarantine.md](plans/inbound-quarantine.md).
|
||
|
||
**Done when:** TBD after open questions in the plan are answered.
|
||
|
||
**Dependencies / risks:** inbound relay `[1.4.0]`; storage/retention/backup
|
||
size; malware in held MIME; overlap with
|
||
[inbound-antispam-panel](#inbound-antispam-panel). No implementation checklist
|
||
yet.
|
||
|
||
**Version:** TBD; `candidate` until explicitly agreed.
|
||
|
||
---
|
||
|
||
## preflight
|
||
|
||
**Goal:** a `/preflight` page (global-admin only) that verifies instance-level
|
||
infrastructure: rDNS/PTR, TLS certificate + hostname match, port 25/465/587
|
||
reachability, HELO banner correctness, reverse-proxy headers, DKIM milter
|
||
socket, and a test-email sender. Each check shows an actionable fix
|
||
recommendation on failure.
|
||
|
||
**Boundary:** instance infrastructure only; per-domain DNS checks remain on
|
||
domain detail pages. Does not track test-email delivery beyond local MTA
|
||
acceptance.
|
||
|
||
**Done when:** the page loads, all checks run with traffic-light results and
|
||
recommendations, and the test-email form successfully submits a message through
|
||
Postfix.
|
||
|
||
**Dependencies / risks:** port-connectivity check may not reflect external
|
||
reachability behind NAT without hairpin; test email needs at least one domain or
|
||
falls back to `postmaster@SELFPOST_HOSTNAME`.
|
||
|
||
**Version:** `1.x` MINOR; `candidate`.
|
||
|
||
---
|
||
|
||
## csrf-tokens
|
||
|
||
**Goal:** a per-session CSRF token on every state-changing form, checked
|
||
server-side in addition to the `Sec-Fetch-Site` / `Origin` checks the panel
|
||
relies on today.
|
||
|
||
**Boundary:** defence in depth, not a replacement — the header checks stay and
|
||
keep covering requests that carry no form. The token is bound to the existing
|
||
session record; no new session model, no cookie changes.
|
||
|
||
**Done when:** every POST form and htmx fragment that changes state carries a
|
||
token, a missing or mismatched token is refused with a test that proves it, and
|
||
[security.md](security.md) describes the layered model instead of arguing that
|
||
the header checks alone are sufficient.
|
||
|
||
**Dependencies / risks:** the current documented decision in
|
||
[security.md](security.md) is that header checks suffice; this item exists
|
||
because the 2026-08-19 review recorded the opposite decision without ever
|
||
turning it into a task (§4.4). Confirm which of the two stands before writing
|
||
code — the honest outcomes are "implement tokens" or "keep headers and say so
|
||
in one place only". Token plumbing through htmx swaps is where the work is.
|
||
|
||
**Version:** `1.x` MINOR; `candidate`.
|
||
|
||
---
|
||
|
||
## template-data-typing
|
||
|
||
**Goal:** replace `map[string]any` template payloads in `internal/web/handlers`
|
||
with typed structs (`DomainDetailData`, `SettingsData`, …), so a renamed or
|
||
mistyped key is a compile error instead of a silently empty page section.
|
||
|
||
**Boundary:** handler-to-template plumbing only; no template redesign, no route
|
||
or behaviour change. `renderDomainDetail` (~40 keys) is the worst case and the
|
||
one that motivates the item.
|
||
|
||
**Done when:** the page handlers pass structs, the shared footer/base fields
|
||
still arrive on every page, and the template guard tests pass unchanged.
|
||
|
||
**Dependencies / risks:** mechanical but wide — it touches every
|
||
`handlers_*.go` and every template that reads those keys; a missed key is
|
||
invisible unless a test renders that page. Worth pairing with the handler
|
||
tests in [review-2026-08-followups](#review-2026-08-followups) so the pages
|
||
are covered before they are rewired. Review estimate: 2–3 hours (§7.1).
|
||
|
||
**Version:** no bearing on semver; `candidate`.
|
||
|
||
---
|
||
|
||
## structured-logging
|
||
|
||
**Goal:** move from `log.Printf` to `log/slog` — levels, structured fields, and
|
||
one logger injected through constructors instead of a package-level `logf` in
|
||
five packages (§5.5, §7.2).
|
||
|
||
**Boundary:** internal logging only. The log destination stays stdout/stderr
|
||
under supervisord; no log shipping, no new dependency, and the operator-visible
|
||
lines the Status page and the log tailer parse must keep parsing.
|
||
|
||
**Done when:** packages take a `*slog.Logger`, the per-package `logf = log.Printf`
|
||
aliases are gone, levels are used deliberately (an error is not an info), and
|
||
the panel's own journal/log-tailer expectations still hold.
|
||
|
||
**Dependencies / risks:** the log tailer reads Postfix's log, not the panel's,
|
||
so the risk is contained — but anything that greps the panel's output (e2e,
|
||
operator habits) sees a new format. Review estimate: 3–4 hours (§7.2).
|
||
|
||
**Version:** no bearing on semver; `candidate`.
|
||
|
||
---
|
||
|
||
## review-2026-08-followups
|
||
|
||
**Goal:** the leftovers of the 2026-08-19 code review that are too small to be
|
||
roadmap items of their own. The full findings and the per-task status table are
|
||
in [plans/code-review-2026-08.md](plans/code-review-2026-08.md).
|
||
|
||
| § | Item |
|
||
|---|---|
|
||
| §4.7 | Re-authenticate (current password) before a backup download |
|
||
| §9.2 | Tests for the DMARC service layer (`service.go`, `addresses.go`) |
|
||
| §9.2 | Tests for the apps / domains / users / dmarc handlers |
|
||
| §9.4 | Tests: error page templates, corrupted backup archive, delivery-log pagination edges |
|
||
| §9.3 | Thin spots: `store/stats_test` zero-traffic case, concurrent store operations |
|
||
| §6.1 | `secretfile` accepts an empty password at library level — guard or document |
|
||
| §6.2 | Log-rotation fingerprint collision in `logtail/offset.go` — include the inode |
|
||
| §6.4 | `parsePage` does not clamp to `lastPage` |
|
||
| §7.4 | `dnscheck` resolver fans out four parallel queries instead of falling back — decide or document |
|
||
| §8.6 | `dev/workflow.md` still names an outdated base version |
|
||
| §10.3 | `.pair` vs `.split`: two class names for one layout |
|
||
| §10.4 | Two `back_link` elements in `dmarc_domain.html` |
|
||
| §10.5 | `initShowWhen` is not re-run after an htmx swap (safe today, fragile) |
|
||
|
||
**Boundary:** no feature work and no behaviour change an operator would notice,
|
||
apart from the backup re-auth prompt. Anything here that grows past an hour
|
||
becomes its own roadmap item instead.
|
||
|
||
**Done when:** each row is either implemented or struck with a written reason,
|
||
and the status table in the plan file says which.
|
||
|
||
**Dependencies / risks:** the test rows overlap
|
||
[template-data-typing](#template-data-typing) — write the handler tests first
|
||
if both are taken up. §7.3 (build tags for the `/proc` reader) was **declined**
|
||
during the review pass and is deliberately absent: that code is parameterised by
|
||
root, has no platform-specific calls, already degrades gracefully without
|
||
`/proc`, and tagging it `linux` would only delete cross-platform test coverage.
|
||
|
||
**Version:** no bearing on semver; `candidate`.
|
||
|
||
---
|
||
|
||
## schema-squash
|
||
|
||
**Goal:** when 2.x is cut, stop shipping the 1.x migration chain in the binary
|
||
and replace it with one baseline equal to the schema at the then-current head.
|
||
Fresh 2.x data directories no longer create-then-drop the historical `admin`
|
||
table. Current chain, legacy notes, and gate thresholds:
|
||
[schema-migrations.md](schema-migrations.md) (update that file when migrations
|
||
ship; adjust the squash gate to its head at cut time).
|
||
|
||
**Boundary:** 1.x keeps the full chain so a `1.0.0` data directory still boots.
|
||
Do not delete, rename, or reorder migration files while MINOR compatibility with
|
||
`1.0.0` holds. Git history keeps the old files either way; only the embedded
|
||
set in the 2.x image changes.
|
||
|
||
Restore remains a separate lock: the backup manifest version must match the
|
||
running binary ([architecture.md](architecture.md) § Persistence). It does not
|
||
replace the squash upgrade gate.
|
||
|
||
**Done when:** 2.x embeds a single baseline (plus any 2.x-only migrations after
|
||
it); the gate in [schema-migrations.md](schema-migrations.md) is tested; the
|
||
operator guide says a 2.x image will not open an unfinished 1.x database.
|
||
|
||
**Dependencies / risks:** a decided 2.x cut (another breaking change, or an
|
||
explicit major). Squashing the current short chain is not a reason to cut 2.x on
|
||
its own. A missed gate leaves a mid-chain 1.x database silently stuck.
|
||
**Version:** `2.x` major only; not a 1.x item.
|
||
|