audit 4: cards ordered by number, a notification owed to each recipient
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
The cards layout (owner, 2026-10-13; the mockup first, b90a2f0): box_open
writes data-order for a box with InCards and a number, and panel.css orders
by it up to twelve - Health's eight and the kit's three.
TestCardsBoxesCarryTheirNumberAsTheirOrder reads the rendered pages.
Notifications (owner, 2026-10-14): migration 0011, notify_addressees. open
sends a subject's first message to every recipient and records who was
handed it; followUp, on every check while the subject is open and still
wrong, hands it to the owed and untold who are still recipients; closed
sends the end to the told (all recipients for a subject from before the
table). Tests: a failed copy goes out at the next check, once; the hourly
ceiling holds one back; a user added mid-subject hears nothing of it.
Also in the roadmap, from the owner today: dns-record-collapse,
notification-mail-recheck, settings-save-row (agreed, design first).
Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 644 s) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
-- Who a notification is owed to, per subject (audit 2026-10-12, stage 4;
|
||||
-- owner, 2026-10-14). A subject used to count as said once one recipient's
|
||||
-- copy was handed to Postfix: a recipient whose copy failed, or was held back
|
||||
-- by the hourly ceiling, was never told. The recipients a subject's first
|
||||
-- message was addressed to are kept here with whether each was handed it;
|
||||
-- a later check sends to those not told yet, and "cleared" goes to those who
|
||||
-- were told. Rows of a subject go with its notify_state row
|
||||
-- (Store.DeleteNotifyState). A subject opened before this file has none, and
|
||||
-- is told cleared to every recipient, as before.
|
||||
CREATE TABLE notify_addressees (
|
||||
key TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
told_at TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (key, address)
|
||||
);
|
||||
@@ -127,11 +127,69 @@ func (s *Store) SetNotifyState(st NotifyState) error {
|
||||
}
|
||||
|
||||
// DeleteNotifyState forgets a subject: nothing is wrong with it any more, or
|
||||
// it no longer exists.
|
||||
// it no longer exists. Who it was owed to goes with it.
|
||||
func (s *Store) DeleteNotifyState(key string) error {
|
||||
if _, err := s.db.Exec("DELETE FROM notify_state WHERE key = ?", key); err != nil {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete notify state %s: %w", key, err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
for _, stmt := range []string{"DELETE FROM notify_addressees WHERE key = ?", "DELETE FROM notify_state WHERE key = ?"} {
|
||||
if _, err := tx.Exec(stmt, key); err != nil {
|
||||
return fmt.Errorf("delete notify state %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("delete notify state %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyAddressees returns who the subject's message is owed to, each with
|
||||
// whether they were handed a copy (migration 0011). None for a subject not
|
||||
// said yet, or said before 2.5.4.
|
||||
func (s *Store) NotifyAddressees(key string) (map[string]bool, error) {
|
||||
rows, err := s.db.Query("SELECT address, told_at FROM notify_addressees WHERE key = ?", key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("notify addressees %s: %w", key, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var addr, toldAt string
|
||||
if err := rows.Scan(&addr, &toldAt); err != nil {
|
||||
return nil, fmt.Errorf("notify addressees %s: %w", key, err)
|
||||
}
|
||||
out[addr] = toldAt != ""
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// SetNotifyAddressees records who the subject's message is owed to: owed
|
||||
// maps an address to whether it was handed a copy, and replaces what was
|
||||
// recorded. The told are dated at, the time of this record: the date is
|
||||
// for a person reading the table, the code asks only whether.
|
||||
func (s *Store) SetNotifyAddressees(key string, owed map[string]bool, at time.Time) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("set notify addressees %s: %w", key, err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec("DELETE FROM notify_addressees WHERE key = ?", key); err != nil {
|
||||
return fmt.Errorf("set notify addressees %s: %w", key, err)
|
||||
}
|
||||
for addr, told := range owed {
|
||||
toldAt := ""
|
||||
if told {
|
||||
toldAt = at.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if _, err := tx.Exec("INSERT INTO notify_addressees (key, address, told_at) VALUES (?, ?, ?)", key, addr, toldAt); err != nil {
|
||||
return fmt.Errorf("set notify addressees %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("set notify addressees %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ func TestBaselineSchema(t *testing.T) {
|
||||
"inbound_spam_list": "id list_type entry_type value note created_at",
|
||||
"send_log_lines": "id queue_id logged_at line",
|
||||
"notify_state": "key phase count mark since sent_at",
|
||||
"notify_addressees": "key address told_at",
|
||||
"sender_check_days": "day down_seconds deferred messages estimated",
|
||||
"delivery_log_days": "day reconcile_closed reconcile_missed reconcile_contradicted tailer_blind_seconds queued_over_lifetime",
|
||||
}
|
||||
@@ -117,8 +118,8 @@ func TestBaselineSchema(t *testing.T) {
|
||||
}
|
||||
|
||||
var version, appID int64
|
||||
if err := st.db.QueryRow("PRAGMA user_version").Scan(&version); err != nil || version != 10 {
|
||||
t.Errorf("user_version = %d, %v; want 10", version, err)
|
||||
if err := st.db.QueryRow("PRAGMA user_version").Scan(&version); err != nil || version != 11 {
|
||||
t.Errorf("user_version = %d, %v; want 11", version, err)
|
||||
}
|
||||
if err := st.db.QueryRow("PRAGMA application_id").Scan(&appID); err != nil || appID != applicationID {
|
||||
t.Errorf("application_id = %#x, %v; want %#x", appID, err, applicationID)
|
||||
@@ -194,8 +195,8 @@ func TestOpenMigratesA20Database(t *testing.T) {
|
||||
}
|
||||
defer st.Close()
|
||||
var version int
|
||||
if err := st.db.QueryRow("PRAGMA user_version").Scan(&version); err != nil || version != 10 {
|
||||
t.Errorf("user_version = %d, %v; want 10", version, err)
|
||||
if err := st.db.QueryRow("PRAGMA user_version").Scan(&version); err != nil || version != 11 {
|
||||
t.Errorf("user_version = %d, %v; want 11", version, err)
|
||||
}
|
||||
if ok, err := st.UserExists(); err != nil || !ok {
|
||||
t.Errorf("the user of the 2.0 database: exists = %v, %v", ok, err)
|
||||
|
||||
Reference in New Issue
Block a user