Merge pull request 'Design 63 (the mail web UI with mail reading and Owly) and playbook 10 (visual design)' (#469) from design-63-mail-web-ui into main
This commit was merged in pull request #469.
This commit is contained in:
+23
-2
@@ -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
|
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
|
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,
|
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.
|
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.
|
**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,
|
**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
|
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.
|
and no session (ADR 0278), in its plain sense.
|
||||||
@@ -819,6 +819,27 @@ 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).
|
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*
|
- **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).
|
(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 flag of a mail worth reading that asks nothing, which a mail of any mark may carry (ADR
|
||||||
|
0516 §1). Not a *mark*.
|
||||||
|
- **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
|
## The record — how this repository works
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| [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 |
|
| [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 |
|
| [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
|
## The cycle is checked
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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.** A page whose direction needs research first starts at playbook
|
||||||
|
[01](01-research.md); this playbook then draws it. 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, domain, provider 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.** The lead gives playbook [07](07-feature-branches.md)'s reviewer the reference with
|
||||||
|
the built page, at a phone's and a desktop's width, as part of what the review reads against the issue. The
|
||||||
|
operator using the page is the milestone's live proof, made before the next milestone starts (playbook
|
||||||
|
[04](04-build-handoff.md)).
|
||||||
|
|
||||||
|
## 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, domains, providers or amounts (read by the reviewer of the design's pull request).
|
||||||
|
- The milestone's review read the built page beside the reference at a phone's and a desktop's width (its
|
||||||
|
report says so).
|
||||||
|
- The design's last acceptance test is the operator using the page, proven before the next milestone starts.
|
||||||
@@ -485,7 +485,15 @@ const P = [
|
|||||||
prod:"A stock report, the approved deletions, issues for missing tools.",
|
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",
|
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"],
|
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"};
|
const kinds={all:"All",idea:"From an idea",symptom:"From a symptom",build:"Code changes",side:"Alongside"};
|
||||||
let current="all";
|
let current="all";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
layer: as-is
|
layer: as-is
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog modules/mesh-mail, mesh-catalog modules/mesh-mail-web, mesh-catalog modules/mesh-mail-notifier]
|
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-20
|
updated: 2026-10-22
|
||||||
decisions:
|
decisions:
|
||||||
- "ADR 0359"
|
- "ADR 0359"
|
||||||
- "ADR 0361"
|
- "ADR 0361"
|
||||||
@@ -11,30 +11,42 @@ decisions:
|
|||||||
- "ADR 0436"
|
- "ADR 0436"
|
||||||
- "ADR 0439"
|
- "ADR 0439"
|
||||||
- "ADR 0440"
|
- "ADR 0440"
|
||||||
|
- "ADR 0459"
|
||||||
|
- "ADR 0461"
|
||||||
|
- "ADR 0472"
|
||||||
---
|
---
|
||||||
|
|
||||||
# The mail modules, as they run
|
# 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,
|
**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
|
`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
|
desktop notice of new mail on the machines it is assigned to. A fourth, `mesh-mail-reader`, holds the seat
|
||||||
send from it, only when `mesh-mail`'s trusted setting `grants` names it for that account — one list per
|
`mail-reading`: it hears every new mail, reads it in small batches, and keeps a mail entry — its mail group, a
|
||||||
account, giving both at once, to the same callers. A send is never performed by a tool: it is queued for a
|
one-line summary, its mark and why, never a body — for the mail page to show; the operator's own corrections
|
||||||
process of its own, which sends only once the operator approves it on Telegram.** Read from `mesh-catalog` on
|
on the mail page become reading lessons that outrank a mail's own words in a later batch. A caller may read an
|
||||||
`origin/main` at `a67f0196` (2026-10-20) and from the live mesh through the mesh MCP server on 2026-10-20.
|
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)
|
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
|
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)
|
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)
|
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,
|
mesh-mail-notifier mesh-mail-reader mesh-mail-web (mail-webclient,
|
||||||
once per machine — laptop, work- once for the mesh — the control-node)
|
(mail-notifier, once (mail-reading, once for once for the mesh — the control-node)
|
||||||
station) sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve (Telegram)
|
per machine — laptop, the mesh — the control- sign-in (Keycloak) ─ pages ─ send ask ─▶ operator's Approve
|
||||||
node-notifier.send on new inbox mail
|
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
|
## 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
|
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
|
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.
|
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>`,
|
- **`mesh-mail-reader`** is a module of its own (`modules/mesh-mail-reader`), with its own Go module and
|
||||||
`mail-webclient.<verb>`, `mail-notifier.<verb>`); `mesh-mail` also answers its own tool names (`mail_accounts`
|
source (`go.mod`, `cmd/mesh-mail-reader`), importing nothing of `mesh-mail` (`callers.go`). It claims `mail-reading`, a fourth seat `mesh-mail`'s own manifest declares beside its three mail
|
||||||
and the rest) through the same handlers.
|
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
|
## 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
|
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.
|
left out of it may do neither.
|
||||||
- **Who is granted, today, for every account alike** (`mesh-controller.settings` for `mesh-mail`, read live on
|
- **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
|
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`.
|
is how an agent session on any of them reaches a mail account at all; the control-node's `mesh-mail-web`; and
|
||||||
So the notifier, every machine's agent sessions, and the web client can each both read an account's mail and
|
the control-node's `mesh-mail-reader`. So the notifier, every machine's agent sessions, the web client and
|
||||||
ask to send from it, from the one list.
|
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
|
## 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
|
- **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
|
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)
|
state (`/send-status`), a mail correction (`/mail-correct`) and a reading-lesson undo (`/mail-undo`) — plus
|
||||||
carries a token bound to the session (`server.go`, `csrf.go`).
|
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, 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;
|
- **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
|
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
|
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`).
|
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
|
- **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
|
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
|
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
|
`X-Content-Type-Options: nosniff`; every page carries a content security policy allowing nothing from
|
||||||
elsewhere (ADR 0440 §5; `mail.go`).
|
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
|
- **It keeps no database of its own.** Nothing on disk beyond its own settings; it reaches mail only through
|
||||||
through `mail-store`'s verbs, calling as `<machine>.mesh-mail-web`, and reads one thing directly besides —
|
`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
|
`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): 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`
|
## 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
|
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
|
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`).
|
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
|
- **Assignment.** It is assigned today to the laptop and the workstation (live read, 2026-10-22; `mesh_machine`
|
||||||
`node-notifier` to be held beneath it.
|
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
|
## 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 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
|
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`).
|
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).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-catalog]
|
code: [mesh-catalog]
|
||||||
updated: 2026-10-20
|
updated: 2026-10-22
|
||||||
decisions:
|
decisions:
|
||||||
- "ADR 0440"
|
- "ADR 0440"
|
||||||
- "ADR 0378"
|
- "ADR 0378"
|
||||||
@@ -20,6 +20,8 @@ decisions:
|
|||||||
- "ADR 0461"
|
- "ADR 0461"
|
||||||
- "ADR 0462"
|
- "ADR 0462"
|
||||||
- "ADR 0278"
|
- "ADR 0278"
|
||||||
|
- "ADR 0515"
|
||||||
|
- "ADR 0518"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 59 — The mail web client
|
# 59 — The mail web client
|
||||||
@@ -165,7 +167,7 @@ mail-store (mesh-mail) ── MAIL: mail.<account>.message.added ──▶ mesh-
|
|||||||
with no further Approve: it is the signed-in operator's own act. The page lists the reading lessons, undoes one
|
with no further Approve: it is the signed-in operator's own act. The page lists the reading lessons, undoes one
|
||||||
(later mail reading only), and shows reading lessons per 100 read mails per week, which answers whether mail
|
(later mail reading only), and shows reading lessons per 100 read mails per week, which answers whether mail
|
||||||
reading learns the operator's way.
|
reading learns the operator's way.
|
||||||
- **The page.** Above the classical list it shows the mail that matters at the top, the mail groups, and the scam
|
- **The page** (replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md)'s operator's list, ADR 0515 §5). Above the classical list it shows the mail that matters at the top, the mail groups, and the scam
|
||||||
warnings in a place of their own, each with why; the classical list (accounts, folders, search, one mail) stays
|
warnings in a place of their own, each with why; the classical list (accounts, folders, search, one mail) stays
|
||||||
and always opens. `mesh-mail-web` reads these through `mail-reading`'s verbs and still keeps no mail state
|
and always opens. `mesh-mail-web` reads these through `mail-reading`'s verbs and still keeps no mail state
|
||||||
(ADR 0378 §5 and ADR 0440 §4 as ADR 0459 §13 changes them); it writes the operator's corrections with
|
(ADR 0378 §5 and ADR 0440 §4 as ADR 0459 §13 changes them); it writes the operator's corrections with
|
||||||
@@ -195,6 +197,10 @@ mail-store (mesh-mail) ── MAIL: mail.<account>.message.added ──▶ mesh-
|
|||||||
|
|
||||||
## 8. The mail conversation (ADR 0462)
|
## 8. The mail conversation (ADR 0462)
|
||||||
|
|
||||||
|
**Replaced by design [63](63-the-mail-web-ui-with-mail-reading-and-owly.md):** Owly answers mail questions through
|
||||||
|
mail reading before threads exist (ADR 0518, changing ADR 0462 §6); the conversation below is what Owly moves onto
|
||||||
|
once threads exist.
|
||||||
|
|
||||||
A mail conversation is a thread (ADR 0383), run by the thread handler (ADR 0384), with a mail context: the framing
|
A mail conversation is a thread (ADR 0383), run by the thread handler (ADR 0384), with a mail context: the framing
|
||||||
that a mail is data, the tools `mail_search` and `mail_read` only, under a caller of the thread's own named in `grants`
|
that a mail is data, the tools `mail_search` and `mail_read` only, under a caller of the thread's own named in `grants`
|
||||||
(ADR 0472 §4; issue 1419), the operator's real senders and folder names, and the reading lessons. The operator writes a mail
|
(ADR 0472 §4; issue 1419), the operator's real senders and folder names, and the reading lessons. The operator writes a mail
|
||||||
@@ -274,7 +280,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) | — |
|
| 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 |
|
| 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 |
|
| 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
|
**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
|
workstation (a search, an attachment, one send on the Approve) is still the operator's. Its issues, one repository
|
||||||
@@ -305,4 +311,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.
|
`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,
|
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
|
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,213 @@
|
|||||||
|
---
|
||||||
|
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"
|
||||||
|
- "ADR 0359"
|
||||||
|
- "ADR 0361"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 no name, address, employer, domain, provider
|
||||||
|
or amount in it is 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) and its page of mail groups:
|
||||||
|
Owly answers before threads exist (ADR 0518) and moves onto a thread once they do, and the page opens on the
|
||||||
|
operator's list (ADR 0515 §5). It is a design of its own, not an amendment of design 59, by the operator's choice
|
||||||
|
on 2026-10-22: design 59 is the mail web client and mail reading as first built and closes with its own last steps
|
||||||
|
(test 13 live, issue 1418, and Betterbird's removal, issue 806); this one is the page rebuilt from a visual design
|
||||||
|
(playbook [10](../../00-META/process/10-visual-design.md)), in six milestones of its own. Design 59 says where it is
|
||||||
|
replaced.
|
||||||
|
|
||||||
|
## 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 operator's list.** A greeting says what mail signals told the operator 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 operator's 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 operator's 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, 130 s behind the morning briefing; the page shows how long Owly has been waiting.
|
||||||
|
- **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 operator's list on a wide screen and a sheet on a phone, with suggestion
|
||||||
|
chips for the questions the operator's list makes likely.
|
||||||
|
|
||||||
|
## 5. Owly writes (milestone 4)
|
||||||
|
|
||||||
|
**Proposed scope, not yet decided.** This section is what the mockup shows; its decision is written in
|
||||||
|
milestone 4's prepare step, before any of it is built, and the section is then rewritten to it.
|
||||||
|
|
||||||
|
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 Lena 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 and undo forms, so nothing
|
||||||
|
else on the mesh can change the mailbox; an undo writes an *undone* line through `decide`. Flag, delete and draft stay with issue 730.
|
||||||
|
- **The action log's counts reach the batches** and Owly's questions, in a part of their own (ADR 0517 §6 to §8).
|
||||||
|
- **Teaching in the chat (proposed scope, not yet decided).** "Stop telling me about Strava" becomes a reading
|
||||||
|
lesson, shown under *Owly learned* with an undo. 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, before it is built. *Remind me tomorrow*
|
||||||
|
and *read later* are milestone 1's (ADR 0516, ADR 0517).
|
||||||
|
|
||||||
|
## 7. Fast on the desktop (milestone 6)
|
||||||
|
|
||||||
|
**Proposed scope, not yet decided:** its decision is written in milestone 6's prepare step, tracked by issue
|
||||||
|
1867, before it is built.
|
||||||
|
|
||||||
|
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 operator's 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 operator's list on the phone and on the desktop for a day.
|
||||||
|
9. (Milestone 2, proposed until its decision.) A photo, a PDF's pages, an invitation, a sheet and a zip's contents 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, proposed until its decision.) 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 in a part of their own of at most 4 KiB, the
|
||||||
|
batch's senders first, and the reading lessons keep their 12 KiB (ADR 0517 §6); a lesson taught in the chat
|
||||||
|
changes the next batch and is undone from *Owly learned* (proposed until its decision).
|
||||||
|
15. (Milestone 6, proposed until its decision.) 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 operator'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 and the counts (1866) | 3 |
|
||||||
|
| 6 | On the desktop the operator works the operator's 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 operator's 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 1857, 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.
|
||||||
@@ -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 |
|
| [`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 |
|
| [`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 |
|
| [`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
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
`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
|
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.
|
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.
|
No new top-level folders without explicit confirmation.
|
||||||
- **Status lives in YAML frontmatter** — on design docs (`layer`, `status`, `code`, `updated`,
|
- **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
|
and `decisions` as "ADR NNNN"). A decision record's and a research effort's status live in the knowledge
|
||||||
|
|||||||
Reference in New Issue
Block a user