Write design 63 and playbook 10, so the mail page's next steps are built from a picked design

Design 63 turns the mail page into the operator's list with Owly beside it (ADR 0515
to 0519), in six milestones with their issues; its reference mockup is kept beside it,
its data invented. Playbook 10 says how a page is designed before it is built: mockups
with fixed data, the operator picks, the pick is the reference. The as-is of the mail
modules now describes mail reading as delivered, the glossary gains design 63's words,
design 59's milestone 4 is marked replaced, and AGENTS.md allows a design's mockup.

Issues 1855 and 1856.
This commit is contained in:
jochen
2026-10-22 18:42:42 +02:00
parent 58555ec330
commit 5d745d04db
10 changed files with 635 additions and 34 deletions
+22 -2
View File
@@ -778,8 +778,8 @@ it was told, and the servarr apps' own libraries. **Upstream of** nothing. **Dec
operator granted, note each mail for the operator and tell them of the mail that matters, and send a mail only on
the operator's Approve. **Recorded by** `mesh-mail` (its index database and attachment bucket, ADR 0359 §3; its
account buckets, ADR 0439) and `mesh-mail-reader` (its database of mail entries, mail groups and reading lessons,
ADR 0459). **Downstream of** Identity and access and Operator and conversation; **upstream of** nothing.
**Decided by** ADR 0359, 0361, 0378, 0439, 0440, 0458, 0459, 0461, 0462, 0472.
ADR 0459; and its handling, action log and suggested actions, ADR 0517, ADR 0519). **Downstream of** Identity and access and Operator and conversation; **upstream of** nothing.
**Decided by** ADR 0359, 0361, 0378, 0439, 0440, 0458, 0459, 0461, 0462, 0472, 0515 to 0519.
**Uses:** operator, person, module, seat; grant, identity (Identity and access); ask, warrant, operator channel,
content rule, untrusted input (Operator and conversation). A *judgement* is one call of the model with no tools
and no session (ADR 0278), in its plain sense.
@@ -819,6 +819,26 @@ and no session (ADR 0278), in its plain sense.
tools `mail_search` and `mail_read` only, the operator's senders and folders, and the reading lessons (ADR 0462).
- **mail question** — what the operator writes in the mail conversation to find or understand mail. Not *ask*
(Operator and conversation's request for the operator's input).
- **the operator's list** — what the mail page opens on: the tasks by their due date, the promises, *good to know*,
*read later* and *handled today*, for one mode (ADR 0515 §1). Always *the operator's list*: the bare *list* is
the classical list of mail, shown as *All mail*.
- **task** — what a mail asks the operator to do, with a due date or none, written by mail reading in its own words
(ADR 0516 §1).
- **promise** — what a mail says someone will do: *you promised*, *waiting on others* or *on its way*, with who and
when (ADR 0516 §1).
- **read later** — the mark of a mail worth reading that asks nothing (ADR 0516 §1).
- **handled** — what the operator marked done on the operator's list; it changes nothing in the mailbox (ADR 0517
§1).
- **action log** — mail reading's record of every act of the operator on the mail page, never mail text, kept a
year (ADR 0517 §3).
- **Owly** — the mail page's assistant: it answers mail questions through mail reading, gives the morning briefing,
and suggests actions; it sends and changes nothing by itself (ADR 0515 §2, ADR 0518).
- **morning briefing** — Owly's account of the day, asked the first time the mail page opens each day (ADR 0518).
- **suggested action** — an *archive*, *report as phishing* or *unsubscribe* that mail reading puts forward for the
operator's Approve on the mail page (ADR 0519 §1). Not *proposal* (Operator and conversation's, which takes effect
on a warrant).
- **approval inbox** — the place on the mail page where the suggested actions wait, each with *Approve* and *Not
now* (ADR 0519 §4).
## The record — how this repository works
+1
View File
@@ -62,6 +62,7 @@ A drawn overview of them, the cycle, a feature's steps, the review loop and an i
| [07](07-feature-branches.md) | Features | Work that changes code across issues or repositories: prepared context, local development, one review, one check, one delivery |
| [08](08-small-fixes.md) | Small fixes | One issue in one repository, about an hour of work: one try, one review, one delivery |
| [09](09-cleanup.md) | Cleanup | Merged branches, stale pull requests, finished worktrees and folders: read first, delete only what the operator approves, report the rest |
| [10](10-visual-design.md) | Visual design | A page the operator will use is about to be designed or rebuilt: mockups with fixed data, the operator picks, the pick is the reference |
## The cycle is checked
+64
View File
@@ -0,0 +1,64 @@
# Playbook 10 — Visual design
**Trigger.** A page or a screen the operator will use is about to be designed or rebuilt, or the operator judges
one unusable. It runs before the decisions about its look are written and before any of its code: a page built
first and judged after it ships is rebuilt (the operator, 2026-10-22, on design 59's mail page, judged unusable
the day it was delivered).
**Who runs it.** The lead of the design, on the larger model. Mockups may be drawn by an agent on the smaller
model from the lead's brief. The operator picks; nobody else does.
## What it produces
- **The reference**: one mockup the operator picked, kept with the design as `NN-mockup/` beside its to-be
document. It is a direction, not a pixel specification: the build matches what it shows the operator doing,
in the order and the weight it shows, not its exact pixels.
- **The decisions it needs**: what the pick changes about what the mesh keeps, shows or does, written as decision
records (playbook [02](02-graduation.md)) before the design's milestones (playbook [04](04-build-handoff.md)).
- **An acceptance test in the operator's words**: the page is done when the operator uses it, on the devices they
named, for the work the mockup shows.
## Steps
1. **Gather what the page must carry.** Read the as-is of what it replaces and the real shape of its data from the
mesh (how many items, how long a line, what is missing in practice), the operator's complaint in their own
words, and a short survey of how others solve it (a handful of products, one line each on what changes for
the person). Write it into the work folder; nothing goes on the mesh.
2. **Draw three to five mockups with fixed data.** Each is one static HTML file that opens from disk: invented
data shaped like the real (names, amounts, lengths, a long subject, an empty sender), no backend, a mock script
only where an interaction is the point. Each mockup is a different direction, not a variation of one; each
shows a phone's width and a desktop's. One index page links them all.
3. **Show them and ask one question.** Put the index on the machine the operator is at, and ask which direction to
keep, one question at a time with the lead's recommendation. A round's answer may be a pick, a combination of
two, or a change; the next round draws only that. Three rounds without a pick mean something the operator wants
is missing: ask what, before drawing again.
4. **Make the pick the reference.** Copy it into the design's `NN-mockup/`, with every name, address, employer,
mail address and amount of the operator's own replaced by invented ones: hq is public. Say in the design that
the reference is a direction, and link it.
5. **Write what is new as decisions,** each with the operator's Approve where it changes what they see, then the
design and its milestones. The design's acceptance tests end with the operator using the page live.
6. **Hold the build to it.** Playbook [07](07-feature-branches.md)'s review reads the built page against the
reference (a screenshot at each width beside the mockup), and the milestone is done only when the operator has
used the page.
## Bounds
| Waiting on | Bound | Past the bound |
|---|---|---|
| a round of mockups | 1 hour | say to the operator what is taking long |
| the operator's pick | the next exchange | ask once more, one question, with the recommendation |
| rounds without a pick | 3 | ask what is missing before drawing again |
## Rules
- No code reaches the mesh during a visual design: mockups live in the work folder until one is picked.
- Mockups use invented data, never a copy of the operator's mail, files or accounts.
- The operator's pick stands over the lead's taste; the lead's recommendation is said once, with its reason.
## How it is checked
- A to-be design of a page the operator uses links its reference mockup in `NN-mockup/`, and that mockup holds
none of the operator's own names, addresses or amounts (read by the reviewer of the design's pull request).
- The milestone's review report shows the built page beside the reference at a phone's and a desktop's width.
- The design's last acceptance test is the operator using the page, and it is proven before the milestone's
issues are resolved.
+9 -1
View File
@@ -485,7 +485,15 @@ const P = [
prod:"A stock report, the approved deletions, issues for missing tools.",
next:"03",bounds:"A worktree is safe only if untouched for 2 h · a PR is stale after 3 days",
prev:["Deleting unfinished work","Deletion by name or age alone","Missing tools left as session notes"],
check:"The report names the evidence for each deletion."}
check:"The report names the evidence for each deletion."},
{n:"10",name:"Visual design",kind:"idea",c:"--idea",
trig:"A page the operator will use is about to be designed or rebuilt, or the operator judges one unusable.",
who:"The design's lead; the operator picks.",
steps:["Gather what the page must carry: the as-is, the real data's shape, the operator's words, how others do it.","Draw three to five mockups with fixed, invented data, phone and desktop. (new)","Show them; ask one question per round. (new)","Make the pick the reference, with the operator's own details replaced. (new)","Write what is new as decisions, then the design and its milestones.","Hold the build to the reference; done when the operator uses it."],
prod:"The reference mockup beside the design, its decisions, an acceptance test of live use.",
next:"02, then 04",bounds:"A round 1 h · 3 rounds without a pick: ask what is missing",
prev:["A page judged unusable the day it ships","Rebuilding after delivery"],
check:"The design links its mockup; the review shows the build beside it; the operator's use is the last test."}
];
const kinds={all:"All",idea:"From an idea",symptom:"From a symptom",build:"Code changes",side:"Alongside"};
let current="all";
+137 -28
View File
@@ -1,8 +1,8 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier]
updated: 2026-10-20
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier, mesh-catalog modules/mesh-mail-reader]
updated: 2026-10-22
decisions:
- "ADR 0359"
- "ADR 0361"
@@ -11,30 +11,42 @@ decisions:
- "ADR 0436"
- "ADR 0439"
- "ADR 0440"
- "ADR 0459"
- "ADR 0461"
- "ADR 0472"
---
# The mail modules, as they run
**One module, `mesh-mail`, holds every mail account and is the mesh's one `mail-store`. A second module,
`mesh-mail-web`, is the mesh's one mail web client. A third, `mesh-mail-notifier`, shows a
desktop notice of new mail on the machines it is assigned to. A caller may read an account's mail, or ask to
send from it, only when `mesh-mail`'s trusted setting `grants` names it for that account — one list per
account, giving both at once, to the same callers. A send is never performed by a tool: it is queued for a
process of its own, which sends only once the operator approves it on Telegram.** Read from `mesh-catalog` on
`origin/main` at `a67f0196` (2026-10-20) and from the live mesh through the mesh MCP server on 2026-10-20.
desktop notice of new mail on the machines it is assigned to. A fourth, `mesh-mail-reader`, holds the seat
`mail-reading`: it hears every new mail, reads it in small batches, and keeps a mail entry — its mail group, a
one-line summary, its mark and why, never a body — for the mail page to show; the operator's own corrections
on the mail page become reading lessons that outrank a mail's own words in a later batch. A caller may read an
account's mail, or ask to send from it, only when `mesh-mail`'s trusted setting `grants` names it for that
account — one list per account, giving both at once, to the same callers; `mesh-mail-reader` is on that list
too, and asks no send, by its own design rather than by a narrower grant. A send is never performed by a tool:
it is queued for a process of its own, which sends only once the operator approves it on Telegram.** Read
from `mesh-catalog` on `origin/main` at `835bf22b` (2026-10-22) and from the live mesh through the mesh MCP
server on 2026-10-22.
```
mesh-mail (mail-store, once for the mesh — the control-node)
accounts search read send send-status ── MAIL stream (headers only, 7 days) ──▶ a granted caller
own database (index) · own object-store bucket (attachments) · three key-value buckets (sends, account state, newest mails)
process keep (IDLE + index) · process watch (mail_health) · process send (mesh-mail-send account; holds the passwords)
▲ grants: one list per account, read AND send alike
▲ grants: one list per account, read AND send alike (mesh-mail-reader on it too, asking no send)
│
┌────┴─────────────────────────────┬───────────────────────────────┐
mesh-mail-notifier (mail-notifier, mesh-mail-web (mail-webclient,
once per machine — laptop, work- once for the mesh — the control-node)
station) sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve (Telegram)
node-notifier.send on new inbox mail
┌────┴────────────────┬─────────────────────────┬──────────────────────────────────┐
mesh-mail-notifier mesh-mail-reader mesh-mail-web (mail-webclient,
(mail-notifier, once (mail-reading, once for once for the mesh — the control-node)
per machine — laptop, the mesh — the control- sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve
workstation) node) ▲ (Telegram)
node-notifier.send on hears MAIL's added events; │ entries groups correct
new inbox mail batches of 10 judged ─▶ its own │ lessons undo status
database (entries, groups, lessons) ─▶ ─┘
mail signal ─▶ operator's channel (Telegram)
```
## 1. The modules and their seats
@@ -55,9 +67,13 @@ mesh-mail (mail-store, once for the mesh — the control-node)
module claiming it needs `node-notifier` held on the same machine (ADR 0378 §7). It is assigned today to two
machines, the laptop and the workstation (live read of each machine's modules, 2026-10-20); the control-node
and the home server hold no instance of it.
- None of the three seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
`mail-webclient.<verb>`, `mail-notifier.<verb>`); `mesh-mail` also answers its own tool names (`mail_accounts`
and the rest) through the same handlers.
- **`mesh-mail-reader`** is a module of its own (`modules/mesh-mail-reader`), also built from `mesh-mail`'s
source. It claims `mail-reading`, a fourth seat `mesh-mail`'s own manifest declares beside its three mail
seats, held once for the mesh (ADR 0459 §1, §2). It is assigned to the same machine as `mesh-mail`, under
its own account, `mesh-mail-reader` (`runs-as`; confirmed live, 2026-10-22). It is covered whole in §8.
- None of the four seats is named `mesh-*`. A caller reaches them by seat (`mail-store.<verb>`,
`mail-webclient.<verb>`, `mail-notifier.<verb>`, `mail-reading.<verb>`); `mesh-mail` also answers its own
tool names (`mail_accounts` and the rest) through the same handlers.
## 2. Accounts and the MAIL stream
@@ -92,11 +108,15 @@ mesh-mail (mail-store, once for the mesh — the control-node)
second list and no narrower grant for sending alone: a caller named for an account may do both, and a caller
left out of it may do neither.
- **Who is granted, today, for every account alike** (`mesh-controller.settings` for `mesh-mail`, read live on
2026-10-20): `mesh-mail-notifier` (named alone, so wherever it is assigned it may read); each of the four
2026-10-22): `mesh-mail-notifier` (named alone, so wherever it is assigned it may read); each of the four
machines' own `node-tools` — the control-node's, the laptop's, the workstation's and the home server's, which
is how an agent session on any of them reaches a mail account at all; and the control-node's `mesh-mail-web`.
So the notifier, every machine's agent sessions, and the web client can each both read an account's mail and
ask to send from it, from the one list.
is how an agent session on any of them reaches a mail account at all; the control-node's `mesh-mail-web`; and
the control-node's `mesh-mail-reader`. So the notifier, every machine's agent sessions, the web client and
the mail reader can each both read an account's mail and ask to send from it, from the one list — ADR 0472
§1 and §3 decided the mail reader joins this same list, rather than a narrower one of its own, and that it
simply never calls a send verb: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts`
alone, no send verb of `mail-store` (`modules/mesh-mail-reader/module.json`, confirmed on `origin/main` at
`835bf22b`).
## 4. The send path
@@ -123,12 +143,28 @@ mesh-mail (mail-store, once for the mesh — the control-node)
- **Its pages**, all behind sign-in: the accounts page (`/`), a folder (`/folder`), search (`/search`), one
mail (`/mail`), an attachment download (`/attachment`), writing a mail (`/write`), its send (`/send`) and its
state (`/send-status`) — plus the sign-in callback (`/signed-in`). Every changing form (today, the send)
carries a token bound to the session (`server.go`, `csrf.go`).
state (`/send-status`), a mail correction (`/mail-correct`) and a reading-lesson undo (`/mail-undo`) — plus
the sign-in callback (`/signed-in`). Every changing form (the send, a correction, an undo) carries a token
bound to the session (`server.go`, `csrf.go`).
- **The accounts page now opens on mail reading, above the classical list** (ADR 0459 §13, ADR 0515 §5, issue
1430, 1523; `accounts.go`). It asks `mail-reading.status` first; not answering at all makes the whole
section say mail reading is unavailable and why, and nothing else here is asked — the classical list below
still renders regardless. Otherwise it shows, in order: the mail that matters (`mail-reading.entries`, mark
`matters`), each with a correction form (mark, mail group) that posts to `/mail-correct`; the mail groups
(`mail-reading.groups`) when mail reading is healthy, or the unhealthy cause and since when in their place
when it is not; the scam warnings (the same `entries` answer, mark `scam warning`), each with the same kind
of correction form, shown either way; the reading lessons (`mail-reading.lessons`, newest first), each with
its own undo form posting to `/mail-undo`; and the weekly reading-lessons-per-100-read-mails measure, from
`mail-reading.status`'s own answer. A mail body never reaches this section — only a mail entry's headers,
mail group, summary, mark, why and due date.
- **A correction or an undo is asked of `mail-reading`, not acted on here.** `/mail-correct` and `/mail-undo`
are refused with no sign-in or a form token from another session, before `mail-reading.correct` or
`.undo` is ever asked; whatever `mail-reading` answers or refuses, the page is simply shown again, so a
refused correction is never surfaced as its own error (`mailwrite.go`, issue 1523).
- **Sign-in is Keycloak's.** The module requires `oidc-client` and uses the authorisation-code flow with PKCE;
the identity (the ID token's own username claim) must be in the trusted setting `operators` — nobody by
default, and today one identity, the operator's own (ADR 0440 §3; `mesh-controller.settings` for
`mesh-mail-web`, read live on 2026-10-20). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict`,
`mesh-mail-web`, read live on 2026-10-22). The session cookie is `Secure`, `HttpOnly` and `SameSite=Strict`,
ending 12 hours after sign-in (`auth.go`).
- **Reachable only from the mesh's own machines.** Its route's reach is set to `internal`, so the route proxy
composes only its internal name, never a public one, and the module itself answers any other host 404 (ADR
@@ -137,10 +173,12 @@ mesh-mail (mail-store, once for the mesh — the control-node)
never parsed as markup; an attachment is answered only as a download, `Content-Disposition: attachment` and
`X-Content-Type-Options: nosniff`; every page carries a content security policy allowing nothing from
elsewhere (ADR 0440 §5; `mail.go`).
- **It writes nothing of its own.** No database, nothing on disk beyond its own settings; it reaches mail only
through `mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
- **It keeps no database of its own.** Nothing on disk beyond its own settings; it reaches mail only through
`mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
`mesh-mail`'s account bucket, for folders and counts, of an account its own grant already let it ask about
(ADR 0440 §4, §6).
(ADR 0440 §4, §6). Its manifest now also invokes `mail-reading`'s six verbs (ADR 0459 §13, ADR 0515 §5): it
reads `entries`, `groups`, `lessons` and `status`, and writes a correction or an undo with `correct` and
`undo` — writes that land in `mail-reading`'s own database, never in anything `mesh-mail-web` keeps itself.
## 6. `mesh-mail-notifier`
@@ -156,8 +194,12 @@ mesh-mail (mail-store, once for the mesh — the control-node)
sender; a message already shown — tracked by its message id, or by its folder, UIDVALIDITY and UID where it
has none — is never shown twice, even redelivered. A notice that fails to send is retried at most once a
minute (`notifier.go`, its constants `FoldOver`, `Window`, `RetryBackoff`).
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-20), where it needs
`node-notifier` to be held beneath it.
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-22; `mesh_machine`
for each shows its seat `mail-notifier` and tool `mail_notifier_health` there, and neither on the
home server or the control-node), where it needs `node-notifier` to be held beneath it. ADR 0461 §7 names
these same two machines as the ones it is to be unassigned from, on the operator's Approve, once mail
signals (§8) are proven live on the operator's real accounts (issue 1418) — as built, that unassignment has
not happened.
## 7. Health
@@ -175,3 +217,70 @@ mesh-mail (mail-store, once for the mesh — the control-node)
the module by it. A watching process of its own is tracked by issue 841, not yet decided (its `README.md`).
The comment in its `health.go` that still names a process `notify` predates that commit. It keeps one state
bucket, `shown`, for 30 days, to show no mail twice (`module.json`).
## 8. `mesh-mail-reader`
- **It claims `mail-reading`**, the fourth mail seat, held once for the mesh, serving six verbs:
`entries`, `groups`, `correct`, `lessons`, `undo` and `status` (ADR 0459 §1, §2; `module.json`,
`tools.go`). Each answers only a caller the trusted setting `callers` names — a bare module name, granted
on any machine, `<machine>.<module>`, or `person.<name>`, the same shapes `mesh-mail`'s own `grants`
matches, copied in miniature so this module never imports `mesh-mail`'s code (`callers.go`). The setting
defaults to `["mesh-mail-web"]` and is unchanged on the live mesh (`mesh-controller.settings` for
`mesh-mail-reader`, read live on 2026-10-22), so only `mesh-mail-web` may call a `mail-reading` verb today;
any other caller is refused, naming why.
- **Its own account and database.** It runs as its own account, `mesh-mail-reader` (`runs-as`), never the
tool runner's, because it hears `mesh-mail`'s restricted stream `MAIL` under it. It is granted reading on
every account `mesh-mail` holds — named as the control-node's `mesh-mail-reader` in `mesh-mail`'s `grants`
for each of the three accounts alike (`mesh-controller.settings` for `mesh-mail`, read live on 2026-10-22) —
and never asks to send: its manifest invokes `seat:mail-store.read` and `seat:mail-store.accounts` only, no
send verb, and its code calls neither `mail_send` nor anything that moves, flags or deletes a mail (ADR
0459 §11, ADR 0472 §3). It keeps its own Postgres database (`postgres-database`, contributed as
`mail-reader`), separate from `mesh-mail`'s index.
- **Its database holds four tables, no mail body in any of them** (`schema.sql`): `mail_entry` — one row per
judged mail, its account, message id, folder, date, sender name and address, subject, its `mail_group`, a
`summary` of at most 160 characters, its `mark` (`matters`, `scam warning` or `neither`, or null), `why`,
a `due_date`, whether it was `told` and when it was `judged`; `mail_group` — a label and when it was first
made; `reading_lesson` — the corrected mail's own headers (account, folder, sender name and address,
subject), the mail group, the `change` made, when (`made`), and whether it was `undone`; and `waiting_mail`
— a mail heard but not yet read, by its account, folder, message id, UIDVALIDITY and UID (unique together,
so a mail heard twice is kept once), its date, sender, subject, flags and size, in the order it was heard. A
fifth table, `reader_health`, keeps its own health (below) across a restart.
- **It hears mail under its own account.** It consumes `mesh-mail`'s stream `MAIL` for
`mesh-mail.mail.*.message.added` events alone (`AddedPattern`, `hear.go`), recording each as a row in
`waiting_mail` before the handler returns — so the controller's durable consumer redelivers an event a
store failure lost, rather than it being silently dropped — and keeping a mail heard twice to one row, by
its own unique key above (ADR 0459 §3, §12; `hear.go`).
- **Its batches**: at most 10 mails at a time (`MaxMailsPerBatch`), taken from `waiting_mail` in the order
they were heard; a batch starts no sooner than 160 seconds after the previous batch started (`BatchGap`,
pinned to the constant `MinBatchGap` in every production build; only a build tagged `integration` may
shorten it, and never in a way a unit test lets reach production) — bounding mail reading to at most 225
mails an hour (ADR 0459 §5, §15; `batch.go`, `question.go`). Each batch is one judgement, asked of
`claude-code.claude_code_judge` on this module's own machine (`MESH_NODE`) and collected with
`claude_code_judged` when it answers pending; it is asked to answer `within` at most 40 seconds
(`JudgeWithin`), and the whole question — the framing that a mail is data and never an instruction, the
existing mail groups (largest first, at most 4 KiB), the reading lessons that apply (at most 40, matching
sender or mail group first then newest, at most 12 KiB) and up to 10 mails (each at most 3.5 KiB: headers
at most 512 bytes, text cut to its first 3 KiB) — is bounded to 55 KiB. A mail the provider filed as junk
(its folder named Junk or Spam, or carrying the `\Junk` flag) is said as that fact beside its text. A mail
that has simply gone (moved or deleted since it was heard) is taken out of the waiting queue without ever
being judged; any other read failure leaves the whole batch waiting for the next slot; on success, one
mail entry is written per mail and any new mail group is made; on a refused, timed-out or out-of-shape
judgement, nothing is written, every mail given stays waiting, and the reader's own health (below) is set
unhealthy (`batch.go`'s `RunOneBatch`, `RunBatches`).
- **Its mail signal.** After each batch, win or lose, it tries to tell the operator: every entry marked
`matters` and not yet `told` — carrying one over from an earlier batch that went untold — is gathered into
one `operator-channel.tell` with subject `mail`, naming the mail with the nearest due date first, then the
next one, ending "and N-2 more on the mail page" past two; nothing is sent, and nothing asked at all, when
there is nothing to tell or while the reader is unhealthy (read fresh from the store, not assumed). The
words are checked first, line by line, with `messenger.messenger_check`; any one refused falls back to the
safe form "N mails matter: open the mail page". Only once `tell` answers that some channel actually sent it
are those entries marked `told` (ADR 0461 §1-§6; `tell.go`, `signal.go`).
- **Its health**, `mail_reader_health`, is asked by the node-engine itself (`runtime-health` of kind `tool`,
every 60 seconds) and carries no caller check. It is unhealthy once a batch judgement is refused, times
out, or answers out of shape, naming which of the three, the judgement's own words, and since the first
such failure in a row (a later one in the same run never pushes that time forward); healthy again once a
judgement succeeds. The state survives a restart, kept in its own database rather than only in memory (ADR
0459 §12; `health.go`).
- **What it never does.** It changes no mail: it moves, flags, deletes and sends nothing, asking no verb of
`mail-store` that would (ADR 0459 §11). It keeps no mail body and no text taken from one — only headers, a
summary at most 160 characters, a mark, why, and a due date (ADR 0459 §4).
+6 -3
View File
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-20
updated: 2026-10-22
decisions:
- "ADR 0440"
- "ADR 0378"
@@ -274,7 +274,7 @@ before `mesh-mail-notifier` is unassigned; test 12 with milestone 4.
| 1 | In a browser on the laptop and the workstation, the operator signs in and browses, searches and reads every account's mail with its attachments, and writes a mail that goes out on their Approve | tests 1 to 9 | mesh-catalog: `mesh-mail`'s account bucket (issue 1255, ADR 0439 §2); the module `mesh-mail-web` (manifest, sign-in, the pages, the send, the seat's verbs, the integration test) | — |
| 2 | Each new mail is read into a mail group on the mail page; the mail that matters reaches the operator's phone as one mail signal, a scam warning shows on the page only, the operator's corrections become reading lessons, and the pop-up for every new mail is gone | tests 10, 11, 13 to 17 | mesh-catalog: the seat `mail-reading` declared in `mesh-mail`'s manifest; the module `mesh-mail-reader` (batches, mail entries, reading lessons, mail signals, health, the integration test); `mesh-mail-web`'s mail groups, mail signals, scam warnings and reading lessons; `mesh-mail-notifier` unassigned (issue 1418) | 1 |
| 3 | Betterbird is gone from the laptop and the workstation, its profile and mail kept in the home | ADR 0379 §2 | mesh-catalog: Betterbird unassigned on the operator's Approve, then the `betterbird` module removed from the catalogue (issue 806) | 2 |
| 4 | On the mail page the operator asks questions about their mail and gets the mails that answer them, in a mail conversation kept as a thread | test 12 | mesh-catalog: `mesh-mail-web`'s mail conversation as a client of the thread seat; the thread's mail tools under a caller of their own (issue 1419); `mesh-mail`'s folder case and `-any` switches | 2 (the reading lessons); design 58's milestone 3 (issue 880) |
| 4 | **Replaced by design 63** (2026-10-22): on the mail page the operator asks questions about their mail and gets the mails that answer them, in a mail conversation kept as a thread | test 12 | mesh-catalog: `mesh-mail-web`'s mail conversation as a client of the thread seat; the thread's mail tools under a caller of their own (issue 1419); `mesh-mail`'s folder case and `-any` switches | 2 (the reading lessons); design 58's milestone 3 (issue 880) |
**Milestone 1, confirmed by the operator on 2026-10-19, is delivered;** its live proof on the laptop and the
workstation (a search, an attachment, one send on the Approve) is still the operator's. Its issues, one repository
@@ -305,4 +305,7 @@ Milestone 2 is one release of `mesh-catalog`: the new seat, the new module and t
`mesh-mail-reader` is named in `grants` after that delivery, on the operator's Approve; `mesh-mail-notifier` is unassigned only after test 13 passes live. Milestone 3 is issue 806.
ADR 0379 §2's bar for removing Betterbird is milestone 1's; keeping Betterbird until mail reading (milestone 2) works,
and not until the mail conversation, is the lead's proposal, for the operator to confirm with the milestones
(playbook 04, step 5). Milestone 4 waits for threads and for the decision that names its thread's caller (issue 1419, which needs a decision first).
(playbook 04, step 5). **Milestone 4 is replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md)**
(2026-10-22): Owly answers mail questions through mail reading before threads exist (ADR 0518), and moves onto a
thread once they do. Issue 1432 will not be done; issue 1433 moves to design 63's milestone 3; issue 1419 stays
open for the move onto a thread.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,199 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog]
updated: 2026-10-22
decisions:
- "ADR 0515"
- "ADR 0516"
- "ADR 0517"
- "ADR 0518"
- "ADR 0519"
- "ADR 0459"
- "ADR 0440"
- "ADR 0461"
- "ADR 0462"
- "ADR 0472"
- "ADR 0378"
- "ADR 0278"
---
# 63 — The mail web UI with mail reading and Owly
**The mail page stops being a list of mail and becomes the operator's list: what needs them today, this week and
later, what is promised, what is good to know and what can be read later, each line written by mail reading from
the mail, never copied from it. Beside it sits Owly, the mail page's assistant, which answers questions about the
mail, writes drafts the operator sends on their Approve, and suggests tidying the mailbox into an approval inbox,
where only the operator's Approve changes anything. Every action of the operator on the page is recorded, so mail
reading and Owly judge better over time. The classical list stays one click away, as All mail.**
(ADR 0515 to ADR 0519, building on design 59 and ADR 0459.)
The page has no name of its own: it is `mesh-mail-web`'s page, the mail page. Its reference is the mockup the
operator picked on 2026-10-22, kept beside this design as [`63-mockup/mail-page.html`](63-mockup/mail-page.html), a
direction and not a pixel specification; its mail is invented, and the names, addresses and amounts in it are
not the operator's.
## 1. What it builds on
Design [59](59-the-mail-web-client.md) delivered the mail web client (its milestone 1) and mail reading (its
milestone 2): `mesh-mail-web` renders the mail page on the server, and `mesh-mail-reader` reads each new mail into
a mail entry, sends a mail signal for the mail that matters and keeps the operator's corrections as reading
lessons. What runs today is the as-is [18](../00-as-is/18-the-mail-modules.md). The operator judged that page
unusable on 2026-10-22: it shows mail groups over a classical list, so the operator still reads each mail to know
what it asks.
This design replaces design 59's milestone 4 (the mail conversation on a thread): Owly answers before threads
exist (ADR 0518) and moves onto a thread once they do. Design 59 keeps its own last steps (test 13 live, issue
1418, and Betterbird's removal, issue 806).
## 2. The operator's list (ADR 0515, ADR 0516)
- **Mail reading notes what each mail asks.** A mail entry also holds at most 3 tasks (what to do, and by when),
at most 3 promises (*you promised*, *waiting on others*, *on its way*, with who and when) and whether the mail is
*read later*. Each line is the model's own sentence, at most 120 characters, and never quotes the mail. A batch
holds at most 6 mails, so its larger answer stays within 40 s (ADR 0516).
- **The page opens on the list.** A greeting says what mail signals told the operator's phone today; then
*today*, *this week* and *later* (tasks by their due date), *you promised*, *waiting on others*, *on its way*,
*good to know*, *read later*, *handled today* with undo, and the scam warnings in their own place (ADR 0515 §1).
- **Modes choose accounts.** *Work*, *Personal* and *Everything* each show the accounts `mesh-mail-web`'s setting
`modes` names (ADR 0515 §4).
- **Phone first.** One column on a phone; a wide screen adds the reading pane, and Owly beside the list (ADR 0515
§2, §3).
- **All mail is the classical list.** Accounts, folders, search and one mail stay a view the operator can always
open; a correction of mail reading is made from the reading pane (ADR 0515 §5).
- **One script and one stylesheet from its own origin.** The page stays rendered on the server; the script makes
handling, undo and Owly work without a reload and talks only to the page's own origin. A mail's body stays
escaped text, and an attachment stays a download until milestone 2's decision (ADR 0515 §6).
- **The list starts full.** After the delivery, mail reading reads the last 14 days of inbox mail once more
(ADR 0516 §5).
## 3. Handling and the action log (ADR 0517)
- **Handled is the operator's.** Ticking a task, a promise or a mail handled hides it under *handled today*, with
an undo until the day ends; *remind me tomorrow* hides it until the next day. Nothing in the mailbox changes.
- **Every action is a line in the action log:** handled, undone, reminded, opened, corrected, asked, approved or
*not now*, the mode chosen, and the page opened (*last looked*, which gives *new since you last looked*). A line
holds the act, the mail by its id, the mode and when, and a question's own words; never mail text. Kept a year.
- **The log teaches.** Per sender over 90 days, the counts of mails, opened, handled unopened, reminded and
corrected reach each batch and each question in a part of their own, at most 4 KiB (milestone 5).
- **The verbs.** `mail-reading` gains `list`, `handle`, `unhandle`, `remind`, `record` and `actions`; the page
calls them for the signed-in operator and keeps no state of its own.
## 4. Owly answers (ADR 0518)
- **`ask` on `mail-reading`.** A question (at most 2 KiB) is answered in two judgements: the first chooses up to 5
mails and 3 search terms from the mail entries; mail reading calls `mail_search` and `mail_read`; the second
answers from those mails and names only mails it was given. A question the mails do not answer is answered so.
- **On another node.** Owly's judgements run on the node `ask-node` names (the workstation by default), never on
the node of the batches and the issue tracker, so ADR 0459 §15's spacing holds unchanged. An answer takes up to
about 90 s; the page says what Owly is doing meanwhile.
- **The morning briefing** is one judgement a day from the last 7 days' mail entries, read aloud by the browser's
own speech on the operator's click.
- **Nothing of the conversation is kept** but the question, in the action log. A follow-up is answered fresh until
Owly moves onto a thread (issue 880, then issue 1419 for the thread's caller).
- **On the page:** Owly is a panel beside the list on a wide screen and a sheet on a phone, with suggestion chips
for the questions the list makes likely.
## 5. Owly writes (milestone 4)
Owly offers three short reply options for a mail that asks a question, a draft in the mail's own language, a new
mail from a sentence ("mail Marie that I'll pick the photos tomorrow"), a nudge for a promise the operator waits
on, and an answer to an invitation. Every one is the operator's to change, and goes out only through `mail_send`
on the operator's Approve on Telegram (ADR 0440 §6, ADR 0361 §2). Drafting is outside every decision so far (ADR
0462 §7 leaves it to issue 1259), and a reply needs `In-Reply-To` and cc (issue 1257): the decision is written in
milestone 4's prepare step, tracked by issue 1864.
## 6. Owly learns and tidies (ADR 0519, ADR 0517 §6)
- **Suggested actions.** Mail reading may suggest *archive*, *report as phishing* or *unsubscribe*, with why; it
carries none out. Batches suggest from scam warnings and from senders the operator never opens; Owly suggests
when asked in words.
- **The approval inbox** on the page shows each, with *Approve* and *Not now*. The signed-in operator's Approve is
the decision; `mesh-mail-web` then calls `mail-store`'s new verb. *Not now* keeps a sender quiet for 30 days.
- **mail-store's writes:** `archive`, `report`, `restore` (the same day's undo) and `unsubscribe`, by the mail's own
one-click way (RFC 8058, to a public address only) or as a mail send on the operator's Approve. `mesh-mail`
answers them only for the mail web client, which calls them only from the page's Approve form, so nothing else
on the mesh can change the mailbox. Flag, delete and draft stay with issue 730.
- **Teaching in the chat.** "Stop telling me about Strava" becomes a reading lesson, shown under *Owly learned*
with an undo; *remind me tomorrow* and the read-later feed come with it, and the action log's counts reach the
batches. Whether a lesson taught in words needs more than ADR 0459 §8 decides is settled in milestone 5's
prepare step, tracked by issue 1866.
## 7. Fast on the desktop (milestone 6)
Keyboard shortcuts (`j` and `k` move, `x` ticks, `r` opens the reply options, `/` jumps to Owly, `?` lists them)
and bulk actions (tick several, handle or archive them at once, each archive still on the approval inbox's
Approve). Tracked by issue 1867.
## 8. What stays outside
- **A calendar.** "Put the viewing in my calendar" needs a calendar on the mesh; none exists, so the page shows the
date and offers nothing more. No issue is opened until a calendar is decided.
- **Attachments shown in place** change ADR 0440 §5 (an attachment is only a download); milestone 2 writes that
decision before it builds (issue 1861).
- **The mail conversation on a thread** waits for threads (issue 880) and its caller (issue 1419); ADR 0462 then
decides again for Owly.
- **The work account** stays a later decision of its own (ADR 0359 §7).
## 9. The build's acceptance tests
1. Each new mail's entry holds its tasks, promises and read-later mark; every line is at most 120 characters and
shares no run of 8 words with the mail's text; a batch holds at most 6 mails and its question stays under 64
KiB (ADR 0516 §1 to §3).
2. A correction removing a task or a promise, or changing the read-later mark, is kept as a reading lesson and
followed by the next batch (ADR 0516 §4).
3. After the delivery the last 14 days of inbox mail are read once, and a restart does not read them again
(ADR 0516 §5).
4. The mail page opens on the list: the greeting with today's mail signals, the sections in their order, scam
warnings in their own place, the modes from `modes`, and *All mail* opening the classical list (ADR 0515).
5. Handling a task moves it to *handled today*, undo returns it, remind hides it until the next day, and each act
leaves one line in `actions` with no mail text; a line older than 365 days is removed (ADR 0517).
6. Every page's content security policy allows script and style from its own origin only, no inline script, and
`connect-src 'self'`; a mail body with `<script>` and a remote image is shown as text (ADR 0515 §6).
7. Tests 1, 2, 4 and 5 run in one integration test that starts `mesh-mail-reader` and `mesh-mail-web` whole
against the real bus and store, with a fixed judgement (issue 1860).
8. Live: the operator uses the list on the phone and on the desktop for a day.
9. (Milestone 2.) A photo, a PDF's pages, an invitation, a sheet and a zip's list open in place, as its decision
says (issue 1861).
10. (Milestone 3.) `ask` answers naming only mails it was given, says so when nothing answers, never judges on the
batches' node, and records the question with no answer text; the morning briefing is one judgement a day from
the entries alone (ADR 0518).
11. (Milestone 3.) Live: the operator asks three questions on the phone and the desktop, and each answer names
real mails.
12. (Milestone 4.) A reply option, a draft and a nudge each reach the operator's Approve on Telegram, and nothing
is sent without it (issue 1864).
13. (Milestone 5.) A suggestion appears in the approval inbox; Approve archives the mail, undo restores it,
*Not now* is recorded and not suggested again for 30 days; a one-click unsubscribe refuses a private address;
`mesh-mail-reader` invokes no write verb (ADR 0519).
14. (Milestone 5.) The per-sender counts reach a batch's question within 4 KiB, after the whole of the reading
lessons (ADR 0517 §6); a lesson taught in the chat changes the next batch and is undone from *Owly learned*.
15. (Milestone 6.) Each shortcut does what section 7 says, and a bulk handle of five tasks is five lines in the
action log.
## 10. Milestones
| # | Outcome, in one sentence | Proves | Repositories and their parts | Depends on |
|---|---|---|---|---|
| 1 | The operator opens the mail page on the phone or the desktop and sees what needs them, ticks it off, undoes it, and switches between Work and Personal | tests 1 to 8 | mesh-catalog: `mesh-mail-reader`'s tasks, promises and read later (1857), handling and the action log (1858); `mesh-mail-web`'s list (1859, with 1850); the integration test (1860); one entry per message (1849), the health's first state (1580) | — |
| 2 | Photos, PDFs, invitations, sheets and zips open in place on the mail page | test 9 | mesh-catalog: `mesh-mail`'s previews and `mesh-mail-web`'s viewer (1861), after its decision | 1 |
| 3 | The operator asks Owly about their mail and hears a morning briefing | tests 10, 11 | mesh-catalog: `mail_search` made fit (1433); `mail-reading`'s `ask` (1862); Owly on the page (1863) | 1 |
| 4 | Owly writes reply options, drafts, new mail and nudges, each sent on the operator's Approve | test 12 | mesh-catalog: drafting and replies (1864), after its decision | 3 |
| 5 | Owly suggests tidying into an approval inbox, and learns from what the operator teaches and does | tests 13, 14 | mesh-catalog: `mail-store`'s writes and the approval inbox (1865, answering 730 for its four verbs); teaching, reminders, read later and the counts (1866) | 3 |
| 6 | On the desktop the operator works the list from the keyboard and handles several items at once | test 15 | mesh-catalog: shortcuts and bulk actions (1867) | 1 |
**Milestones 1 to 6 and their issues, confirmed by the operator on 2026-10-22.** Each milestone is one release of
`mesh-catalog` (playbook [07](../../00-META/process/07-feature-branches.md)), built one at a time, the smallest useful
one first. Their issues, in their order (each depends on the one before unless said):
- Milestone 1: 1857 (tasks, promises, read later), 1858 (handling and the action log; after 1857), 1859 (the list
on the page, with 1850; after 1858), 1860 (the integration test; after 1859); 1849 and 1580 depend on none.
- Milestone 2: 1861 (a decision first, ADR 0440 §5).
- Milestone 3: 1433 (depends on none), 1862 (`ask`; after 1857), 1863 (Owly on the page; after 1862 and 1859).
- Milestone 4: 1864 (a decision first; after 1863).
- Milestone 5: 1865 (after 1863; answers 730 for archive, report, restore and unsubscribe), 1866 (after 1858 and
1863).
- Milestone 6: 1867 (after 1859).
Issue 1432 (design 59's mail conversation) will not be done: Owly replaces it (ADR 0518). Issue 1419 stays open
for the move onto a thread, which ADR 0518 leaves to threads; Owly's first version reads as `mesh-mail-reader`
and does not need it.
+1
View File
@@ -62,6 +62,7 @@ document is written and this one's status becomes `implemented`.
| [`60-the-brother-module.md`](60-the-brother-module.md) | **In progress.** One module, `brother`, holds the `scanner` and `printer` seats for the Brother multifunction device: scans from the glass (cropped) or the feeder (one PDF), keeps each scan in its bucket and announces it, prints in one call, reads the admin pages and changes settings on those where a mistake cannot lock the operator out, each change recorded and told; only its device process holds the credentials | ADR 0467, ADR 0464, ADR 0468, ADR 0474 |
| [`61-the-print-and-scan-page.md`](61-the-print-and-scan-page.md) | **Designed.** A web page, behind a Keycloak sign-in and on a public name, where the people the operator names print a PDF or a photo and scan the glass, and only managers manage the printer: a module of its own, `print-and-scan`, that calls only the printer, scanner and object-store seats, so any holder of those seats works with it; documents go from the browser to the page and by signed links to the store, never over the bus | ADR 0479, ADR 0468, ADR 0440 |
| [`62-the-bookkeeping-portal.md`](62-the-bookkeeping-portal.md) | **Proposed.** A web page on the private network, behind the operator's Keycloak sign-in, showing the business's income, expenses and to-do as the bookkeeping service holds them now; `okioki` moves whole to the home server and the page `okioki-web` runs beside it, calling only the `bookkeeping` seat; nothing the accountant reviewed or submitted is ever changed | ADR 0481, ADR 0440, ADR 0479 |
| [`63-the-mail-web-ui-with-mail-reading-and-owly.md`](63-the-mail-web-ui-with-mail-reading-and-owly.md) | **In progress.** The mail page becomes the operator's list: what needs them today, this week and later, what is promised, what is good to know and what can be read later, written by mail reading; Owly beside it answers, writes drafts sent on the operator's Approve, and suggests tidying into an approval inbox; every action on the page is recorded so reading judges better; in six milestones | ADR 0515, ADR 0516, ADR 0517, ADR 0518, ADR 0519 |
## Not yet written
+3
View File
@@ -59,6 +59,9 @@ in running prose; a quotation keeps its words.
`00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
2026-10-19); a change to a playbook's steps updates it in the same pull request. Nothing checks that
yet: issue 1222 asks for the check.
A to-be design may also keep its reference mockup, the static HTML page the operator picked, in a
folder beside it (`NN-mockup/`, playbook [10](00-META/process/10-visual-design.md); the operator,
2026-10-22), its data invented: no real names, addresses, domains, providers or amounts.
No new top-level folders without explicit confirmation.
- **Status lives in YAML frontmatter** — on design docs (`layer`, `status`, `code`, `updated`,
and `decisions` as "ADR NNNN"). A decision record's and a research effort's status live in the knowledge