feat(web): digest tracks 'since you last looked' via digest_mark (spec-mfm)
The policy-merged digest showed the last N auto-merges by count; the
design intends "what auto-merged since you last saw it", backed by the
digest_mark table that existed but nothing read. The inbox GET now reads
the mark to flag each digest row that merged after it as new, count them,
and draw a divider before the already-seen rows — staying a pure read.
Advancing the mark is a write, so it is an explicit POST /inbox/seen
behind the owner-only + same-origin guard approve/reject already use, not
a side-effecting GET. service.DigestMark/MarkDigestSeen wrap the store,
mapping "no mark yet" to (zero, false) so the first-ever view reads the
whole digest as new.
Closes spec-mfm
feat(web): review queue — inbox + policy-merged digest (Phase 4)
The backstop for work no link reached. /inbox lists every open proposal
on the instance ("waiting on you") and, below it, the digest of recently
policy-merged content — the firehose a human sees after the fact, which
is the whole reason approval=policy is kept distinct from human.
- service.InboxProposals / DigestProposals list instance-wide (one
reviewer, so a per-space inbox would make them hunt), mapping each
stored proposal's space_id back to a reference once from the space list.
- web/inbox.go + inbox.html render the two sections; the landing page
links the queue for a logged-in owner.
Follow-up: the digest currently shows recent policy-merges rather than
"since you last looked" — the digest_mark table exists to track that, but
advancing it is a write and GET stays pure. Filed separately.
feat(web): proposal review page — prose diff + approve/reject (Phase 4)
The browser review plane at /~owner/space/p/<id>, the stable URL every
write already returns. The owner opens the link an agent handed them,
reads a prose diff of each changed document, and approves (merges now) or
rejects.
- web/diff.go: the prose-diff HTML renderer, consuming prosediff's block
model (the package renders text only; HTML is the web layer's job). It
implements the Phase 0 verdict's hard requirement — inline word diffs
above 0.75 block similarity, a two-column old/new view below it, because
13% of real edits shred and are unreadable inline. All document content
is HTML-escaped; only the diff structure is markup.
- service/review.go: ProposalDiff reads each changed document's base and
proposed content for the page to diff (branch tip resolved to a sha, the
legitimate pinned-rev read, not the ReadDocumentAtRef bypass), and
MergeHuman fixes the approval kind so a browser approve is always human.
- web/proposal.go: the GET page and the approve/reject POSTs. Only the
owner may act (an agent is authenticated but has no more approval
authority than anyone); a cross-site guard on Origin/Referer is the CSRF
defense a form post needs when the session cookie is meta's. Post-
redirect-get back to the page. Stale/already-merged approve → 409.
- web.Reader gains the proposal reads and the two actions; the diff-view
styles go in scss/main.scss (inline marks, two-column, code line diffs).
Inbox and the policy-merged digest are the remaining Phase 4 surfaces.