Files
selfpost/docs/roadmap.md
T
mixandClaude Opus 5 1e4746e52f
test / test (push) Has been cancelled
docs: track every unimplemented review finding in the roadmap
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>
2026-08-20 00:09:43 +03:00

316 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.