Files
email_agent/web/templates/message.html
Sam Rolfe 00bb0e05c4 Phase 6: rules editor + unsubscribe detection (AUD-72)
- FR-11 rules editor: plain-text area at /rules, saved and versioned
  (SaveRules appends a version; restore re-saves an old version), parse
  errors surfaced, one-click re-classify. The reply model now reads the
  rules document too (FR-11).
- FR-7 unsubscribe: parse and store List-Unsubscribe / List-Unsubscribe-Post
  (migration v6), detect email and web links (internal/unsubscribe), a
  domain-level ignore list, and an explicit record of what was marked. No
  code path contacts an unsubscribe link; acting on one needs Sam's approval.
- Migration v6 is additive; unsubscribe gains url/updated_at.
- Tests: detector, parser, store migration/records/candidates, rules editor,
  unsubscribe handlers, message-detail rendering.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-11 22:27:08 +11:00

98 lines
4.5 KiB
HTML

<div class="panel-head"><h2>Message</h2></div>
<div class="detail-body">
{{if .Note}}<div class="scan-note" role="status">{{.Note}}</div>{{end}}
{{if .Error}}<div class="scan-note scan-note-error" role="alert">{{.Error}}</div>{{end}}
<div class="detail-meta">
<div class="detail-subject">{{.Message.Subject}}</div>
<div class="muted">{{.Message.FromAddr}}</div>
{{if .Message.SourceAddress}}<div class="msg-source">arrived at <span class="source-addr">{{.Message.SourceAddress}}</span></div>{{end}}
<div class="detail-pills">
<span class="pill action-pill">{{.Action}}</span>
{{if .Classified}}<span class="msg-confidence" title="Classifier confidence">{{.Confidence}}%</span>{{end}}
</div>
</div>
{{if .UnsubLinks}}
<section class="unsub-detected" aria-label="Unsubscribe">
<h3>Unsubscribe</h3>
{{if eq .UnsubStatus "ignored"}}<span class="pill">ignored</span>{{end}}
{{if eq .UnsubStatus "done"}}<span class="pill">recorded</span>{{end}}
<ul class="unsub-link-list">
{{range .UnsubLinks}}<li><span class="pill">{{.Method}}</span> <span class="unsub-url">{{.URL}}</span></li>{{end}}
</ul>
<a class="btn btn-utility btn-small" href="/unsubscribe">Manage unsubscribe</a>
</section>
{{end}}
<details class="detail-original" open>
<summary>Original message</summary>
<pre class="detail-text">{{.Message.BodyText}}</pre>
</details>
<section class="reply-box" aria-label="Reply">
<h3>Reply</h3>
<div class="reply-identity">
<div><span class="muted">From</span> <span class="reply-value">{{.From}}</span></div>
<div><span class="muted">To</span> <span class="reply-value">{{.To}}</span></div>
<div><span class="muted">Subject</span> <span class="reply-value">{{.Subject}}</span></div>
</div>
{{if .NoThreadHeader}}<p class="faint">This message has no stored Message-ID, so the reply cannot set threading headers.</p>{{end}}
<form class="reply-draft-form" hx-post="/actions/draft" hx-target="#message-detail" hx-swap="innerHTML">
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
<input type="hidden" name="id" value="{{.Message.ID}}">
<div class="field">
<label for="reply-template">Template</label>
<select class="text-input" id="reply-template" name="template">
{{range .Templates}}<option value="{{.ID}}" {{if eq .ID $.TemplateID}}selected{{end}}>{{.Name}} — {{.Description}}</option>{{end}}
</select>
</div>
<div class="field">
<label for="reply-instruction">Note for the model (optional)</label>
<input class="text-input" id="reply-instruction" name="instruction" maxlength="300"
placeholder="Say the invoice is paid" autocomplete="off">
</div>
<div class="reply-actions">
<button class="btn btn-primary" type="submit" {{if not .LLMReady}}disabled{{end}}>
{{if .LLMReady}}Draft reply{{else}}Drafting unavailable{{end}}
</button>
<span class="muted faint">model: {{.LLMModel}}</span>
</div>
{{if not .LLMReady}}<p class="faint">Reply drafting needs OMNIROUTE_API_KEY.</p>{{end}}
</form>
{{if .HasDraft}}
<form class="reply-send-form" hx-post="/actions/send" hx-target="#message-detail" hx-swap="innerHTML"
hx-confirm="Send this reply as {{.From}} to {{.To}}?">
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
<input type="hidden" name="id" value="{{.Message.ID}}">
<input type="hidden" name="draft_id" value="{{.Draft.ID}}">
<input type="hidden" name="confirm" value="yes">
<div class="field">
<label for="reply-body">Draft (editable)</label>
<textarea class="text-input reply-textarea" id="reply-body" name="body" rows="10">{{.Draft.Body}}</textarea>
</div>
<div class="reply-actions">
<button class="btn btn-utility" type="submit"
hx-post="/actions/draft/save" hx-target="#message-detail" hx-swap="innerHTML">Save draft</button>
{{if eq .Draft.Status "sent"}}
<span class="pill">sent</span>
{{else}}
<button class="btn btn-primary" type="submit">Send reply</button>
{{end}}
</div>
</form>
{{end}}
</section>
{{if .History}}
<section class="reply-history" aria-label="History">
<h3>History</h3>
<ul class="history-list">
{{range .History}}<li><span class="pill">{{.Action}}</span> <span class="muted">{{.CreatedAt}}</span> {{.Detail}}</li>{{end}}
</ul>
</section>
{{end}}
</div>