Agreed with the owner, one question at a time; documents only, no code. From from-header-binding's Not done list, each a step of the audit plan: the Sender check column says "Connections turned away"; a stand test of which key OpenDKIM signs with; Health connects to a milter socket instead of stat-ing it; the cards layout orders a phone by the box's number; a refusal the panel did not write is recovered from mail.log; a log rotated while the panel was down is read to its end; the deferred counter keeps a place in the log; downtime is counted when supervisord is silent. "Today, UTC" became the roadmap item local-time (agreed): one instance zone, no labels beside a time, days counted in that zone, with the conditions for per-user zones and offsets, and the trap of mail.log's own stamps written down. The six steps on the log tailer are split off stage 1 into stage 2b, a cut of its own (2.5.1); stages 3 and 4 move to 2.5.2 and 2.5.3. Also: negative e2e cases pass only on their own refusal (stage 0); notifications are owed per recipient (stage 4); a DNS outage is unknown, not wrong (M14, M15 had no step); DMARC reports tested where they come in. review-2026-08-followups is closed, its rows placed in stages or dropped with a reason. Certificate warnings by lifetime: left as is, with the condition for return. The audit's checklist is 61 lines (was 48). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+187
-34
@@ -520,12 +520,14 @@ symbol prefixes and key-file format match the Go side byte for byte.
|
||||
|
||||
## Plan
|
||||
|
||||
Six stages, each a self-contained cut, each small enough for one sitting,
|
||||
Seven stages (six, and stage 2b split off stage 1 on 2026-10-14), each a
|
||||
self-contained cut, each small enough for one sitting,
|
||||
with model routing per development.md (security/infra → Opus, UI/docs →
|
||||
Sonnet). "Done when" is the test that proves it, not the edit.
|
||||
|
||||
**Agreed with the owner on 2026-10-12** (roadmap row `audit-2026-10-12`):
|
||||
stage 0 first, then stages 1 and 2 as one cut, the rest after in any order.
|
||||
stage 0 first, then stages 1 and 2 as one cut, the rest after in any order
|
||||
(stage 2b after `2.5.0`, before the others).
|
||||
This is the **active plan** in the sense of development.md § Plan checklists:
|
||||
the `- [ ]` lines below are the implementation checklist, a line is ticked in
|
||||
the commit that finishes it, and the roadmap's Progress column counts them.
|
||||
@@ -560,6 +562,16 @@ has already run is never edited — the next file is added; the published
|
||||
development.md that the image is rebuilt after each Go patch release
|
||||
(`golang:1.26-bookworm` floats, so a rebuild suffices). — **Haiku**
|
||||
- [ ] The four `staticcheck` items. — **Haiku**
|
||||
- [ ] **A negative e2e case passes only on the refusal it is about** (owner,
|
||||
2026-10-14; seen, not found by the audit). `negative_level1_ratelimit`
|
||||
sends up to sixty messages and waits for one to be refused; it once
|
||||
passed on "limit tripped on attempt 1/60" when the connection was reset
|
||||
because Postfix was still starting. The case counts as the limit only the
|
||||
reply code of the limit, and only after at least one message was
|
||||
accepted; a reset, a timeout or any other reply fails it and says what
|
||||
came. Then every other negative case of the suite is read for the same
|
||||
mistake - any failure taken for the expected refusal - and held to its
|
||||
own reply code. - **Opus**
|
||||
|
||||
### Stage 1 — mail that is shown wrong (H3, H2, M12, M13) → `2.5.0`
|
||||
|
||||
@@ -597,25 +609,6 @@ has already run is never edited — the next file is added; the published
|
||||
grown for a tick and the new file has a line). Both are fixes to the
|
||||
tailer's blindness and stay on the list whatever the sweep's fate; their
|
||||
priority follows `tailer_blind_seconds`.
|
||||
- [ ] **A refused message's page shows what Postfix wrote** (owner,
|
||||
2026-10-13: "the log box is required" - the box is the reason
|
||||
delivery-log-storage was built, and its absence from the picture he
|
||||
approved for 2.4.0 was an oversight, not a decision). Today every
|
||||
`rejected` row - `wrong From`, `wrong client`, `rate-limited` - has no
|
||||
queue id, and its page shows the box with one sentence,
|
||||
"This message never reached the queue, so Postfix wrote no delivery lines
|
||||
for it" ([handlers_monitor.go](../internal/web/handlers/handlers_monitor.go)).
|
||||
The sentence is untrue: Postfix wrote a line for each (`milter-reject:
|
||||
END-OF-MESSAGE ... 5.7.1 From: ...` from cleanup, `NOQUEUE: reject: ...`
|
||||
from smtpd). The tailer attaches that line to the row it belongs to, by
|
||||
client address, time and the addresses in the line, and stores it in
|
||||
`send_log_lines` like any other; the page shows it. First on the stand:
|
||||
read `mail.log` for the three kinds of refusal and write down what
|
||||
identifies each line. Then the mockup: the refused variant of `out-message`
|
||||
gets the box with the line in it (`design:` commit), then the code. The
|
||||
sentence stays only for a row whose line was truly never written or is
|
||||
older than this change, and says that. Done when the e2e shows the stored
|
||||
line on the page of each of the three refusals. - **Opus**
|
||||
- [ ] **Anchor `deliveryRe`** at the head of the line like its two siblings;
|
||||
test with the probed HELO and quoted-sender lines. — **Opus**
|
||||
- [ ] **TLS re-copy** (H2): `postfix-cert-reload.sh` compares the mounted files
|
||||
@@ -642,7 +635,8 @@ has already run is never edited — the next file is added; the published
|
||||
`tx`, count and act inside it; `CreateGlobalUser` likewise. A concurrency
|
||||
test with two goroutines. — **Opus**
|
||||
- [ ] **`submitUserUpdate` in one transaction** (store method that takes the
|
||||
whole change). — **Opus**
|
||||
whole change). The users pages get direct handler tests while they are
|
||||
rewired - a row of review-2026-08-followups, placed here 2026-10-14. — **Opus**
|
||||
- [ ] **Import validates like the form**: route `DMARCRua`, IPs, limit mode,
|
||||
window and count through the same parsers; refuse the file with the
|
||||
validator's text. Tests per field. — **Opus**
|
||||
@@ -662,17 +656,121 @@ has already run is never edited — the next file is added; the published
|
||||
are still in the global log. — **Opus**
|
||||
- [ ] Docs: security.md § Roles (the guard) and the struck accepted risk, guide
|
||||
§ Domain export, schema-migrations.md. — **Sonnet**
|
||||
- [ ] **Which key OpenDKIM signs with, seen and not reasoned** (owner,
|
||||
2026-10-13, from the Not verified list of from-header-binding). The check
|
||||
and OpenDKIM read `From:` with two parsers; that they agree was argued
|
||||
from the check's two rules, never run. An e2e case sends, as one
|
||||
application, the `From:` lines the check passes - quoted strings,
|
||||
comments, folded lines, groups, escapes, encoded words, an address
|
||||
extension - and reads the `d=` of the signature on what arrives: any
|
||||
domain but the application's fails the test. The lines live in one table
|
||||
shared with the check's unit test, so a line added to one is run by the
|
||||
other. A disagreement found is fixed in the check before the cut, and
|
||||
security.md § Mail path says the claim is tested. - **Opus**
|
||||
- [ ] **The 550 reply names no other domain** (L8): `sender.go:334` stops at
|
||||
"it names another sender of this server"; the refused message's page
|
||||
still shows the `From:` line as written (`header_from`), so the operator
|
||||
loses nothing. Test pins the reply text; e2e's sender-check negatives
|
||||
assert the reply does not contain the other tenant's name. — **Opus**
|
||||
|
||||
### Stage 3 — persistence and restore (M1–M5, L1–L4, L7) → `2.5.1`
|
||||
### Stage 2b — the log tailer, and what the panel shows of a refusal → `2.5.1`
|
||||
|
||||
Split off stage 1 on 2026-10-14 at the owner's word: the six steps below were
|
||||
agreed on 2026-10-13/14, after the plan, and had made stages 1 and 2 a cut of
|
||||
twenty-one steps. Stages 1 and 2 ship as `2.5.0` in their first size; this
|
||||
stage follows as a cut of its own, with its own reviews. Five of the six
|
||||
change one thing, the tailer and the state it saves, and are done in this
|
||||
order - each stands on the one before.
|
||||
|
||||
- [ ] **A log rotated while the panel was down is read to its end** (owner,
|
||||
2026-10-13; the Not done list of from-header-binding had it as "deferrals
|
||||
are lost", and it is wider). At start, a saved offset whose fingerprint
|
||||
is not the current file's means "start on the new file"
|
||||
([offset.go](../internal/logtail/offset.go)); what Postfix wrote between
|
||||
the panel's stop and the rotation is never read - delivery statuses,
|
||||
stored delivery lines, refusals, inbound journal rows, the deferred
|
||||
counter, and the recovery of unwritten refusals above. The tailer first
|
||||
opens the rotated file (`mail.log.1`), checks by fingerprint that it is
|
||||
the one the offset belongs to, reads it from the offset to its end, then
|
||||
goes on to the new file. The fingerprint gains the inode, which closes
|
||||
the roadmap's review-2026-08-followups row on collisions. Not found, or
|
||||
rotated twice: logged, with from when to when nothing was read. A test
|
||||
that stops the tailer, appends, rotates, appends, starts it, and finds
|
||||
both halves once. - **Opus**
|
||||
- [ ] **The deferred counter remembers a place in the log, not a second**
|
||||
(owner, 2026-10-14, from the Not done list of from-header-binding; he
|
||||
chose it over leaving the case written down). After an unclean stop the
|
||||
tailer re-reads lines it had read, and the counter passes over every
|
||||
warning no later than the last one it counted
|
||||
([logtail/sendercheck.go](../internal/logtail/sendercheck.go), `last`);
|
||||
`mail.log` stamps whole seconds, so a warning of that very second, later
|
||||
in the file, is lost. The count and the place in the log it has reached
|
||||
are saved in one transaction, and what to pass over is decided by the
|
||||
place. Done after the rotation step above, on the same saved state - the
|
||||
place must stay true across a rotated file. A test that stops the tailer
|
||||
between two warnings of one second and finds both counted, once each.
|
||||
- **Opus**
|
||||
- [ ] **A refused message's page shows what Postfix wrote** (owner,
|
||||
2026-10-13: "the log box is required" - the box is the reason
|
||||
delivery-log-storage was built, and its absence from the picture he
|
||||
approved for 2.4.0 was an oversight, not a decision). Today every
|
||||
`rejected` row - `wrong From`, `wrong client`, `rate-limited` - has no
|
||||
queue id, and its page shows the box with one sentence,
|
||||
"This message never reached the queue, so Postfix wrote no delivery lines
|
||||
for it" ([handlers_monitor.go](../internal/web/handlers/handlers_monitor.go)).
|
||||
The sentence is untrue: Postfix wrote a line for each (`milter-reject:
|
||||
END-OF-MESSAGE ... 5.7.1 From: ...` from cleanup, `NOQUEUE: reject: ...`
|
||||
from smtpd). The tailer attaches that line to the row it belongs to, by
|
||||
client address, time and the addresses in the line, and stores it in
|
||||
`send_log_lines` like any other; the page shows it. First on the stand:
|
||||
read `mail.log` for the three kinds of refusal and write down what
|
||||
identifies each line. Then the mockup: the refused variant of `out-message`
|
||||
gets the box with the line in it (`design:` commit), then the code. The
|
||||
sentence stays only for a row whose line was truly never written or is
|
||||
older than this change, and says that. Done when the e2e shows the stored
|
||||
line on the page of each of the three refusals. - **Opus**
|
||||
- [ ] **A refusal the panel did not get to write is written from
|
||||
`mail.log`** (owner, 2026-10-13, from the Not done list of
|
||||
from-header-binding; built on the step above). The check answers 550 and
|
||||
writes the row a moment later, through a buffer; a panel that stops in
|
||||
between - a rollout, a crash - leaves an application that was refused
|
||||
and a log that does not know it. When the tailer meets a refusal line
|
||||
that has no row, it makes the row from the line (client, sender,
|
||||
recipient, reason), marked as recovered. The tailer resumes from its
|
||||
saved offset, so this covers a kill as well as a clean stop - which a
|
||||
flush of the buffer at shutdown would not. Done when the e2e refuses a
|
||||
message, kills the panel before the row is written, and finds the row
|
||||
after the restart, once. - **Opus**
|
||||
- [ ] **Downtime is counted when supervisord does not answer at start**
|
||||
(owner, 2026-10-13, from the Not done list of from-header-binding). The
|
||||
panel asks supervisord once since when Postfix runs
|
||||
([sendercheck.go](../cmd/panel/sendercheck.go), `postfixStarted`); an
|
||||
error is logged and that start's downtime is not counted at all - the
|
||||
number the owner will decide by reads low. The question is repeated for
|
||||
about thirty seconds. Still no answer: the downtime is taken from the
|
||||
alive mark alone - an upper bound, Postfix may not have been running -
|
||||
and the day is marked as estimated (a column in `sender_check_days`, the
|
||||
next migration), which the card shows. The mark on the card is drawn in
|
||||
the mockup with the renamed column above (one `design:` commit). - **Opus**
|
||||
- [ ] **The Sender check card names what it counts** (owner, 2026-10-13, from
|
||||
the Not done list of from-header-binding): the column *Messages deferred*
|
||||
becomes *Connections turned away*. Postfix refuses from the connection on
|
||||
while the check does not answer, before any message is offered, so a
|
||||
client that connects three times with one message is three, and a monitor
|
||||
that only connects is one; messages cannot be counted. The mockup `health`
|
||||
and help topic `sender-check` first (`design:` commit), then the page and
|
||||
`view/help.go`, word for word. - **Sonnet**
|
||||
- [ ] Docs: architecture § Log tailer (the rotated file, the saved place, a
|
||||
refusal's line and a recovered row), guide § Outbound log and § Health,
|
||||
schema-migrations.md for the mark on `sender_check_days`. — **Sonnet**
|
||||
|
||||
### Stage 3 — persistence and restore (M1–M5, L1–L4, L7) → `2.5.2`
|
||||
|
||||
- [ ] **Consume the manifest after the resync succeeds** (`CheckRestore` reports;
|
||||
`run` deletes after `resyncAfterRestore` returns nil). Test: Resync fails →
|
||||
manifest kept → second boot resyncs. — **Opus**
|
||||
manifest kept → second boot resyncs. And a corrupted or truncated archive
|
||||
is refused on restore and on import with its message, nothing half
|
||||
written (a row of review-2026-08-followups, placed here 2026-10-14). — **Opus**
|
||||
- [ ] **Indexes**: `idx_send_log_domain_created_at (domain_id, created_at)` —
|
||||
on the id column stage 2 adds, replacing `idx_send_log_domain`;
|
||||
partial `idx_send_log_queued (created_at) WHERE status = 'queued'`;
|
||||
@@ -691,7 +789,7 @@ has already run is never edited — the next file is added; the published
|
||||
package. — **Opus**
|
||||
- [ ] Docs: security.md § Backup; guide § Backup. — **Sonnet**
|
||||
|
||||
### Stage 4 — panel behaviour (M8, M11, L10–L19) → `2.5.2`
|
||||
### Stage 4 — panel behaviour (M8, M11, L10–L19) → `2.5.3`
|
||||
|
||||
- [ ] Login: dummy bcrypt on unknown user; a test that measures both paths within
|
||||
a tolerance. Account password change gets its own limiter bucket. Login
|
||||
@@ -705,7 +803,9 @@ has already run is never edited — the next file is added; the published
|
||||
- [ ] `parsePage` clamp; `NaN` refused; `rate-limited` tag red (add the status to
|
||||
`components.go`, flip the test); the recalc button posts only what it
|
||||
needs or warns; `HandleReload` either covers DMARC maps and filter lists or
|
||||
the flash and the two comments say what it covers. — **Sonnet**
|
||||
the flash and the two comments say what it covers. The log's paging is
|
||||
tested at its edges: first page, last, past the last, an empty log (a row
|
||||
of review-2026-08-followups, placed here 2026-10-14). — **Sonnet**
|
||||
- [ ] `domain.Service.Delete`: say the domain is deleted when a later step fails,
|
||||
and run `apps.Resync()` anyway. — **Opus**
|
||||
- [ ] **A status filter and a day filter in the Outbound log** (owner, 2026-10-13, from the
|
||||
@@ -720,6 +820,61 @@ has already run is never edited — the next file is added; the published
|
||||
commit). And a filter by day, as the Inbound log has it (owner, the same
|
||||
day): the same control, drawn in the mockup first. Both filters survive
|
||||
paging and the rows' polling. - **Sonnet**
|
||||
- [ ] **Milter sockets: "Listening" is asked of the socket** (owner,
|
||||
2026-10-13, from the Not done list of from-header-binding).
|
||||
`health.CheckSocket` ([socket.go](../internal/health/socket.go)) stats the
|
||||
file and writes "Listening"; a process that died and left its socket
|
||||
reads ok while mail is deferred or goes out unsigned. It connects and
|
||||
closes instead, and a refused connection is an error that says nobody
|
||||
answers. Find out first, on the stand: whether the panel's user may
|
||||
connect to OpenDKIM's socket, and whether a connection closed without a
|
||||
byte ends the sender check (security.md § Accepted risks: the milter
|
||||
library panics on an empty packet) - if it does, that is fixed first, in
|
||||
this step. A test with a socket file nobody listens on. - **Opus**
|
||||
- [ ] **The `cards` layout takes its phone order from the box's number**
|
||||
(owner, 2026-10-13, from the Not done list of from-header-binding). The
|
||||
order on a phone is written in `panel.css` for four boxes to a stack
|
||||
(`.sp-cards > .column:first-child > .box:nth-child(1..4)`); a fifth box
|
||||
gets no rule and lands out of place with nothing failing. The box partial
|
||||
sets an order class from its own number (a class, not a style: the CSP
|
||||
allows no inline styles), the stylesheet orders by that class, and a test
|
||||
checks that the numbers of a rendered `cards` page run in order. A change
|
||||
to a component: the mockup's components page and `theme.css` first
|
||||
(`design:` commit). - **Sonnet**
|
||||
- [ ] **A notification is owed to each recipient, not to the subject**
|
||||
(owner, 2026-10-14; from the reviews of panel-notifications, not found by
|
||||
the audit). `Notifier.send` ([notify.go](../internal/notify/notify.go))
|
||||
counts a subject as said when one recipient's copy was handed to Postfix;
|
||||
a recipient whose copy failed, or was "held back" by the hourly ceiling,
|
||||
is never told. Unseen while the owner is the only recipient; the first
|
||||
domain administrator makes it "the global administrator knows the
|
||||
domain's DNS broke, its administrator does not". The state of a subject
|
||||
is kept per recipient (the next migration), and a later check sends to
|
||||
those not yet told - so the ceiling holds a copy back instead of dropping
|
||||
it. A user who gains the right to hear of a subject while it is already
|
||||
open is not mailed about it. "Cleared" goes to those who were told of the
|
||||
problem. Tests: one copy fails and goes out at the next check, once; the
|
||||
ceiling; a user added mid-subject. - **Opus**
|
||||
- [ ] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
|
||||
2026-10-14 - the two findings had no step). Each resolver gets a time
|
||||
budget of its own, so a silent first resolver no longer leaves the second
|
||||
an expired context ([dnscheck/resolver.go](../internal/dnscheck/resolver.go));
|
||||
a timeout is Unknown, not "does not resolve" or "no PTR record"
|
||||
([dnscheck/server.go](../internal/dnscheck/server.go)); a failed lookup
|
||||
keeps the server's addresses it had instead of emptying them, so SPF does
|
||||
not go Unknown with it. Notifications: a domain's DNS is reported broken
|
||||
only on a result that is wrong, never on Unknown - checked in `notify`,
|
||||
and tested. Tests: a first resolver that drops packets, a PTR timeout.
|
||||
architecture.md:339-340 and the `DefaultResolvers` comment say what is
|
||||
true. — **Opus**
|
||||
- [ ] **DMARC reports tested where they come in** (owner, 2026-10-14; rows of
|
||||
review-2026-08-followups). The panel parses reports that arrive by mail
|
||||
from anyone, and the `internal/dmarc` service (`service.go`,
|
||||
`addresses.go`) and the DMARC handlers have no tests; no other step
|
||||
touches them. Tests of the service, and of a report that is malformed,
|
||||
oversized, compressed past its limit, or names a domain the instance does
|
||||
not have: refused or skipped with a reason, nothing stored, the panel
|
||||
alive. A finding is fixed in this step. — **Opus**
|
||||
- [ ] **The last untyped page data** (owner, 2026-10-13): `healthChecks`
|
||||
([handlers_status.go](../internal/web/handlers/handlers_status.go)) returns
|
||||
a struct instead of `map[string]any`, so a mistyped key in `healthPage` /
|
||||
@@ -764,14 +919,12 @@ has already run is never edited — the next file is added; the published
|
||||
### Stage 6 — documentation and roadmap sweep (no product code, no cut)
|
||||
|
||||
- [ ] Every item under *Doc drift* above, file by file. — **Sonnet**
|
||||
- [ ] roadmap.md: `template-data-typing` is struck already (owner, 2026-10-13;
|
||||
its last map is a step of stage 4); `structured-logging` is dropped (owner,
|
||||
2026-10-13, roadmap § Dropped) - nothing to reword; in
|
||||
`review-2026-08-followups` mark `parsePage` and `secretfile` rows as
|
||||
addressed by stages 3-4, keep the `offset.go` inode row (still open:
|
||||
fingerprint of the first 512 bytes only), and the `dnscheck` fan-out row
|
||||
now points at M14. Add the open tests list above as the test row's new
|
||||
content. — **Sonnet**
|
||||
- [ ] roadmap.md: done ahead on 2026-10-13/14 with the owner -
|
||||
`template-data-typing` struck, `structured-logging` dropped,
|
||||
`review-2026-08-followups` closed with each row placed in a stage or
|
||||
dropped with its reason. What is left: check that every *Test gaps* entry
|
||||
above is named by the step that touches its code, and add it to that step
|
||||
where it is not. — **Sonnet**
|
||||
- [ ] Comments named above (`milter.go`, `sendlog.go`, `store.go`, `schema_test.go`,
|
||||
`machine.go`, `inboundspam.go`, `logtail.go`, supervisord.conf,
|
||||
entrypoint.sh, docker-compose.yml). — **Haiku**
|
||||
|
||||
@@ -174,7 +174,7 @@ silence; each line says what is missing and what it would take.
|
||||
2026-10-13 (owner):** the box is required - he had overlooked its absence
|
||||
from the picture, it was never a decision to drop it. The tailer attaches
|
||||
the line to the row, for all three kinds of refusal; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 1.
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 2b.
|
||||
- **The Outbound log has no status filter.** The mockup draws one; the built
|
||||
log filters by domain and application. With three kinds of refusal now
|
||||
told apart by tag, a filter by status and reason would be worth having.
|
||||
@@ -183,32 +183,51 @@ silence; each line says what is missing and what it would take.
|
||||
- **"Messages deferred" counts connections Postfix turned away.** Postfix
|
||||
refuses from the connection on, before a message is offered, so a monitor
|
||||
that only connects during an outage is counted. The guide says so; the
|
||||
card's column head and the help topic still say "messages".
|
||||
card's column head and the help topic still say "messages". **Decided
|
||||
2026-10-13 (owner):** the column is renamed *Connections turned away*; a
|
||||
step of [audit-2026-10-12](../audit-2026-10-12.md) stage 2b.
|
||||
|
||||
### Known and small
|
||||
|
||||
- **"Today" on the card is the UTC day**, and the card does not say so.
|
||||
**Decided 2026-10-13 (owner):** not a label - the local day; it became the
|
||||
roadmap item `local-time`, for the whole panel.
|
||||
- **A deferral in the very second of the last one counted before an unclean
|
||||
stop is passed over** at the restart: `mail.log` stamps whole seconds.
|
||||
Chosen over counting a stretch of the log twice.
|
||||
Chosen over counting a stretch of the log twice. **Decided 2026-10-14
|
||||
(owner):** counted exactly - the counter keeps a place in the log, saved
|
||||
with the count; a step of [audit-2026-10-12](../audit-2026-10-12.md)
|
||||
stage 2b.
|
||||
- **Deferrals are lost when `mail.log` is rotated while the panel is down**:
|
||||
the tailer starts on the new file.
|
||||
the tailer starts on the new file. **Decided 2026-10-13 (owner):** the
|
||||
tailer reads the rotated file to its end first - not the deferrals alone,
|
||||
everything the panel takes from the log; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 2b.
|
||||
- **A start at which `supervisorctl` fails counts no downtime**; it is
|
||||
logged.
|
||||
logged. **Decided 2026-10-13 (owner):** the question is repeated, and
|
||||
without an answer the downtime is counted from the alive mark and the day
|
||||
marked as estimated; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 2b.
|
||||
- **Logins are compared as written**, Postfix compares them without regard
|
||||
to case. The panel writes logins one way, so nothing differs today.
|
||||
- **The phone order of the `cards` layout is written for four boxes to a
|
||||
stack** and takes every child of a stack for a box (said at the partial).
|
||||
The rule the technical review suggested instead - the order from the
|
||||
box's own number - changes the component and would be drawn first.
|
||||
**Decided 2026-10-13 (owner):** that rule is built; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 4.
|
||||
- **Health's socket check looks at the file, not at who listens**: a socket
|
||||
nobody answers on would read "ok". The start now removes stale sockets,
|
||||
which was the one way to get such a file.
|
||||
which was the one way to get such a file. **Decided 2026-10-13 (owner):**
|
||||
the check connects to the socket; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 4.
|
||||
- **The kit page's table of status tags does not list the new ones**
|
||||
(`wrong From`, `wrong client`, `rate-limited`), and `rate-limited` is grey
|
||||
beside two red tags - as the mockup has it, not as a decision.
|
||||
- **Refusal rows still in the buffer when the panel stops are not written**;
|
||||
the refusals themselves were answered.
|
||||
the refusals themselves were answered. **Decided 2026-10-13 (owner):**
|
||||
the tailer writes such a row from the refusal's line in `mail.log`; a step
|
||||
of [audit-2026-10-12](../audit-2026-10-12.md) stage 2b.
|
||||
|
||||
### Accepted, in security.md
|
||||
|
||||
@@ -227,7 +246,9 @@ silence; each line says what is missing and what it would take.
|
||||
parser is not in this repository. The two rules that do not depend on it -
|
||||
no other signing name after any `@`, one `@` outside quotes - are what the
|
||||
claim rests on. A test that feeds the stand's OpenDKIM the lines the check
|
||||
passes and reads which key it signs with would settle it.
|
||||
passes and reads which key it signs with would settle it. **Decided
|
||||
2026-10-13 (owner):** that test is built; a step of
|
||||
[audit-2026-10-12](../audit-2026-10-12.md) stage 2.
|
||||
- **The pages were not looked at in a browser on the deployed panel**: I
|
||||
cannot sign in there. They were rendered from the fixtures and
|
||||
screenshotted, the polling and the phone order read from the code and
|
||||
|
||||
+90
-9
@@ -33,15 +33,16 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | shipped `2.1.0` | 12/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) |
|
||||
| origin-strict | Refuse a state-changing request that carries neither `Sec-Fetch-Site` nor `Origin` (was csrf-tokens) | agreed (2026-10-13) | — | — |
|
||||
| backup-reauth | Ask for the account password before a backup or a domain export is downloaded | agreed (2026-10-13) | — | — |
|
||||
| review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — |
|
||||
| review-2026-08-followups | Remaining findings of the 2026-08-19 code review | closed 2026-10-14 (rows placed in [audit-2026-10-12](audit-2026-10-12.md)) | — | — |
|
||||
| panel-notifications | E-mail notifications about important events | shipped `2.3.0` | 10/10 | [plans/panel-notifications.md](plans/panel-notifications.md) |
|
||||
| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | shipped `2.2.0` | 8/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) |
|
||||
| password-reset | Password reset by e-mail | agreed (2026-10-13) | — | — |
|
||||
| local-time | The panel shows time, and counts its days, in a time zone other than UTC | agreed (2026-10-13; design questions open) | — | — |
|
||||
| phone-desktop-mode | A phone browser in "desktop site" mode gets the phone navigation | agreed (2026-10-13: after stages 1 and 2 of audit-2026-10-12) | — | — |
|
||||
| from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) |
|
||||
| panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, six stages | agreed (2026-10-12: stage 0 first, then 1+2; the rest after) | 0/48 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | agreed (2026-10-12: stage 0 first, then 1+2; the rest after) | 0/61 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||
|
||||
**Recommended order** (not binding; owner, 2026-10-09: feature order follows
|
||||
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
|
||||
@@ -257,13 +258,13 @@ is in the table below; the rest of it shipped in
|
||||
| Area | Item |
|
||||
|---|---|
|
||||
| Security | ~~Re-authenticate (ask for the current password) before a backup download~~ — struck: agreed on 2026-10-13 as an item of its own, [backup-reauth](#backup-reauth) |
|
||||
| Tests | The `internal/dmarc` service layer (`service.go`, `addresses.go`) has no tests |
|
||||
| Tests | No direct tests for the apps / domains / users / dmarc handlers; `authz_test` covers them only indirectly |
|
||||
| Tests | Nothing covers error page templates, a corrupted backup archive, malformed or oversized DMARC XML, delivery-log pagination edges, concurrent store operations, or zero-traffic `store` stats |
|
||||
| Edge case | `internal/secretfile` accepts an empty password at library level — guard it, or state that validation is the caller's job |
|
||||
| Edge case | The log fingerprint in [logtail/offset.go](../internal/logtail/offset.go) can collide across a rotation — include the inode |
|
||||
| Edge case | `parsePage` in [handlers_monitor.go](../internal/web/handlers/handlers_monitor.go) does not clamp to `lastPage` |
|
||||
| Edge case | [dnscheck/resolver.go](../internal/dnscheck/resolver.go) fans out four parallel queries where a fallback chain would do — change it or write down why not |
|
||||
| Tests | ~~The `internal/dmarc` service layer (`service.go`, `addresses.go`) has no tests~~ — struck: a step of audit-2026-10-12 stage 4 (DMARC reports tested where they come in) |
|
||||
| Tests | ~~No direct tests for the apps / domains / users / dmarc handlers; `authz_test` covers them only indirectly~~ — struck: users with stage 2 (`submitUserUpdate`), DMARC with stage 4; apps and domains get theirs when a step changes them - a test without a change finds nothing the e2e does not |
|
||||
| Tests | ~~Nothing covers error page templates, a corrupted backup archive, malformed or oversized DMARC XML, delivery-log pagination edges, concurrent store operations, or zero-traffic `store` stats~~ — struck: the corrupted archive goes to stage 3, DMARC XML to stage 4, paging edges to stage 4 (`parsePage`), concurrency of the last global to stage 2; error templates, other concurrency and zero-traffic stats are dropped (owner, 2026-10-14) - code no step changes, and a test kept for its own sake finds nothing |
|
||||
| Edge case | ~~`internal/secretfile` accepts an empty password at library level~~ — struck: audit-2026-10-12 stage 3 (M5) |
|
||||
| Edge case | ~~The log fingerprint in `logtail/offset.go` can collide across a rotation — include the inode~~ — struck: agreed on 2026-10-13 as part of a step of [audit-2026-10-12](audit-2026-10-12.md) stage 2b (a log rotated while the panel was down is read to its end) |
|
||||
| Edge case | ~~`parsePage` does not clamp to `lastPage`~~ — struck: audit-2026-10-12 stage 4 |
|
||||
| Edge case | ~~`dnscheck/resolver.go` fans out four parallel queries where a fallback chain would do~~ — struck: finding M14 of [audit-2026-10-12](audit-2026-10-12.md) |
|
||||
| UI | ~~`.pair` (DMARC templates) and `.split` (every other page) are two names for one layout~~ — struck: retired by [panel-redesign](#panel-redesign); both stylesheets are gone and every page is laid out by the component kit |
|
||||
| UI | ~~Two `back_link` elements in a row in `dmarc_domain.html`~~ — struck: retired by [panel-redesign](#panel-redesign); the page is rebuilt on the kit with one breadcrumb in its head |
|
||||
| UI | ~~`initShowWhen` is not re-run after an htmx swap~~ — struck: the show/hide rules listen on the document and are applied again on `htmx:load` |
|
||||
@@ -273,6 +274,8 @@ 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.
|
||||
**Closed 2026-10-14** with the owner: every row is struck above, placed in a
|
||||
stage of [audit-2026-10-12](audit-2026-10-12.md) or dropped with its reason.
|
||||
|
||||
**Dependencies / risks:** One finding of that review was **declined** and is
|
||||
deliberately absent: `//go:build linux` for the `/proc` reader in
|
||||
@@ -412,6 +415,72 @@ current password in the same cut.
|
||||
|
||||
---
|
||||
|
||||
## local-time
|
||||
|
||||
**Agreed with the owner on 2026-10-13.** It came from one cell: *Today* on
|
||||
Health's Sender check card is the UTC day and does not say so. Offered a
|
||||
label ("Today, UTC") or the local day, he chose the local day - which is a
|
||||
decision about the whole panel, not one cell.
|
||||
|
||||
**Today:** every time the panel shows is UTC and says so (*Time, UTC* in the
|
||||
logs, "UTC" after a date), and everything counted by day - the sender
|
||||
check's days, the delivery-log measurement, the statistics behind the
|
||||
charts, retention - is counted by the UTC day.
|
||||
|
||||
**Goal:** the panel shows time in a chosen time zone, and "today" is that
|
||||
zone's day.
|
||||
|
||||
**Where the zone comes from (owner, 2026-10-13):** one setting of the
|
||||
instance, *Time zone* under Server › Settings, UTC by default; it holds for
|
||||
every user and for notification mail. Not the browser's zone: only a script
|
||||
could read it, and the panel works without JavaScript. Not a setting of each
|
||||
user yet - **that is added (the same list under Account, "as the server" by
|
||||
default) when** a user of the panel lives in another zone than the one set
|
||||
for the instance, which the first domain administrator who is not the owner
|
||||
may bring.
|
||||
|
||||
**What is written beside a time (owner, 2026-10-13):** nothing. The "UTC"
|
||||
labels go - the column is *Time*, a date is a date - and the zone is stated
|
||||
in one place, the setting. His reason: there is nobody to send a line of the
|
||||
panel to yet. **The offset is written where "UTC" is today ("Time, UTC+3",
|
||||
"2026-09-21 16:40 UTC+3") when** times of the panel start to be read by
|
||||
someone who does not know the instance's zone: a second user of the panel,
|
||||
or lines copied out of it to another person.
|
||||
|
||||
**A trap this leaves, known in advance (owner asked for it to be written
|
||||
down, 2026-10-13):** Server › System log, and the *Delivery log* box on a
|
||||
message's page, show the lines of `mail.log` as Postfix wrote them, each with
|
||||
its own stamp ("Oct 13 14:01:52"). Those stamps are the container's time -
|
||||
UTC unless the container is given a `TZ` - and they are not rewritten. Once
|
||||
the rest of the panel is in the instance's zone and no time carries a label,
|
||||
a message that the Outbound log puts at 17:01 has lines stamped 14:01 on its
|
||||
own page, with nothing on the screen to say why. The plan must close this
|
||||
before the item ships, one way or another: rewrite the stamp when a line is
|
||||
shown, say on those two screens which clock the lines use, or run the
|
||||
container in the instance's zone (which moves `mail.log` itself and what the
|
||||
tailer reads). The guide's section on time must say the same thing as the
|
||||
screens.
|
||||
|
||||
**How a day is counted (owner, 2026-10-13):** the tables that keep a row
|
||||
per day - `sender_check_days`, and `delivery_log_days` of
|
||||
[audit-2026-10-12](audit-2026-10-12.md) stage 1 - key the row by the date in
|
||||
the instance's zone at the moment of counting, from the release on. Rows
|
||||
counted before it stay UTC days. Changing the zone later leaves the rows
|
||||
already written as days of the old zone, a shift of a few hours in a
|
||||
statistic; accepted - the zone is set once, and rows per hour (which would
|
||||
survive a change) cost a migration for it. The charts and statistics group
|
||||
the log by hour already and need nothing.
|
||||
|
||||
**Open, to settle at the mockups:** the trap above; which of the screens
|
||||
that show a date show the year.
|
||||
|
||||
**Boundary:** what the panel shows and how it groups by day. `mail.log`,
|
||||
the database's stored instants and the e2e stay as they are.
|
||||
|
||||
**Version:** `2.x` MINOR; `agreed`; the plan is written when it is taken up.
|
||||
|
||||
---
|
||||
|
||||
## phone-desktop-mode
|
||||
|
||||
**Problem (owner, 2026-10-11):** the panel opened on a phone with the
|
||||
@@ -572,6 +641,18 @@ without that it is not offered again.
|
||||
behaviour, and gives a new format to everything that reads the output.
|
||||
**Returns if:** the log goes to an outside collector, or it has to be
|
||||
filtered by level.
|
||||
- **Certificate warnings measured by the certificate's lifetime** (left as
|
||||
it is, owner, 2026-10-14). The panel warns 14 and 3 days before a
|
||||
certificate expires, whatever its lifetime
|
||||
([notify.go](../internal/notify/notify.go), `certFirstDays`,
|
||||
`certLastDays`). A certificate issued for less than 14 days is inside the
|
||||
first warning from its first hour, and each renewal of it is a new
|
||||
certificate and a new message. Not reachable with the 90-day certificate
|
||||
the instance has. **Returns if:** the instance takes a certificate that
|
||||
lives less than 30 days. The rule then offered: the first warning at a
|
||||
quarter of the lifetime left, the last at a tenth, and never earlier than
|
||||
the 14 and 3 days of today - so a longer certificate, a yearly one
|
||||
included, is warned about exactly as now.
|
||||
- **contributing** (dropped 2026-10-13) - a `CONTRIBUTING.md` in the root:
|
||||
the dev loop, the checks before a PR, the commit protocol. Dropped because
|
||||
there is one developer and no PRs, and everything the file would say is in
|
||||
|
||||
Reference in New Issue
Block a user