Phase 7: daily summary and read-only Hub API (AUD-73)
- internal/hub: project side of the frozen ask-contract. GET /capabilities, read-only POST /ask answerer (no LLM, <3s), and the outbound POST /notify client. - internal/apprise: daily-summary delivery through the house Apprise API. - internal/summary: deterministic plain-text daily summary builder. - internal/store: CountByCategory, MessagesByCategory, and summary CRUD. - internal/server: /capabilities and /ask (bearer-token protected), plus the Reports screen (/reports, build, send, delete). - main.go: -summary, -summary-send, and -notify CLI entry points for n8n. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -531,3 +531,25 @@ a:hover { text-decoration: underline; }
|
||||
.unsub-row { flex-direction: column; align-items: stretch; }
|
||||
.rules-version { flex-direction: column; align-items: stretch; }
|
||||
}
|
||||
|
||||
/* Phase 7 (FR-10): Reports screen and daily summary. */
|
||||
.report {
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 4px;
|
||||
padding: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
display: grid;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
.report:last-child { margin-bottom: 0; }
|
||||
.report-head { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
|
||||
.report-text {
|
||||
margin: 0;
|
||||
padding: var(--space-sm);
|
||||
background: var(--surface-2, var(--canvas));
|
||||
border-radius: 4px;
|
||||
font: var(--type-caption);
|
||||
color: var(--ink-secondary);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<div class="nav-actions">
|
||||
<a class="btn btn-utility" href="/rules">Rules</a>
|
||||
<a class="btn btn-utility" href="/unsubscribe">Unsubscribe</a>
|
||||
<a class="btn btn-utility" href="/reports">Reports</a>
|
||||
<button class="btn btn-utility theme-toggle" type="button" onclick="eaToggleTheme()" aria-label="Toggle dark mode">
|
||||
<span class="icon-light" aria-hidden="true">☾</span>
|
||||
<span class="icon-dark" aria-hidden="true">☀</span>
|
||||
|
||||
108
web/templates/reports.html
Normal file
108
web/templates/reports.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Reports · Email Agent</title>
|
||||
<link rel="stylesheet" href="/static/tokens.css">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("ea-theme");
|
||||
if (!t) t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
document.documentElement.setAttribute("data-theme", t);
|
||||
} catch (e) {}
|
||||
})();
|
||||
function eaToggleTheme() {
|
||||
var el = document.documentElement;
|
||||
var next = el.getAttribute("data-theme") === "dark" ? "light" : "dark";
|
||||
el.setAttribute("data-theme", next);
|
||||
try { localStorage.setItem("ea-theme", next); } catch (e) {}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav-bar">
|
||||
<span class="wordmark">Email Agent</span>
|
||||
<div class="nav-actions">
|
||||
<a class="btn btn-utility" href="/">Back to inbox</a>
|
||||
<a class="btn btn-utility" href="/rules">Rules</a>
|
||||
<a class="btn btn-utility" href="/unsubscribe">Unsubscribe</a>
|
||||
<button class="btn btn-utility theme-toggle" type="button" onclick="eaToggleTheme()" aria-label="Toggle dark mode">
|
||||
<span class="icon-light" aria-hidden="true">☾</span>
|
||||
<span class="icon-dark" aria-hidden="true">☀</span>
|
||||
</button>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
|
||||
<button class="btn btn-utility" type="submit">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="page">
|
||||
<h1 class="greeting">Reports</h1>
|
||||
|
||||
{{if .Note}}<div class="scan-note" role="status">{{.Note}}</div>{{end}}
|
||||
{{if .Error}}<div class="alert alert-error" role="alert">{{.Error}}</div>{{end}}
|
||||
|
||||
<section class="panel" aria-label="Daily summary">
|
||||
<div class="panel-head">
|
||||
<h2>Daily summary</h2>
|
||||
<span class="muted">delivered by email via Apprise</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="muted">
|
||||
The summary counts today's inbox by category and lists what needs
|
||||
attention. Building a summary never sends mail. Only <strong>Send
|
||||
now</strong> emails it, and only when Apprise is configured.
|
||||
</p>
|
||||
<div class="rules-actions">
|
||||
<form method="post" action="/reports/generate">
|
||||
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
|
||||
<button class="btn btn-utility" type="submit">Build today's summary</button>
|
||||
</form>
|
||||
{{if .AppriseReady}}
|
||||
<form method="post" action="/reports/send"
|
||||
onsubmit="return confirm('Email today\'s summary now?');">
|
||||
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
|
||||
<button class="btn btn-primary" type="submit">Build & send now</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span class="faint">Sending is unavailable until APPRISE_URL and a key (or APPRISE_URLS) are set.</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .HubReady}}
|
||||
<p class="faint" style="margin-top: var(--space-xs);">The Hub API is configured. The outbound /notify path is available to the summary job.</p>
|
||||
{{else}}
|
||||
<p class="faint" style="margin-top: var(--space-xs);">The Hub API is not configured (set HUB_TOKEN to serve /capabilities and /ask).</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-label="Stored summaries">
|
||||
<div class="panel-head"><h2>Stored summaries</h2></div>
|
||||
<div class="panel-body">
|
||||
{{range .Summaries}}
|
||||
<article class="report">
|
||||
<div class="report-head">
|
||||
<span class="pill">{{.Date}}</span>
|
||||
{{if .Delivered}}<span class="muted">sent to {{.Delivered}}</span>{{else}}<span class="faint">not sent</span>{{end}}
|
||||
<span class="faint">{{.CreatedAt}}</span>
|
||||
<form method="post" action="/reports/delete"
|
||||
onsubmit="return confirm('Delete the report for {{.Date}}?');">
|
||||
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button class="btn btn-utility btn-small" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
<pre class="report-text">{{.Text}}</pre>
|
||||
</article>
|
||||
{{else}}
|
||||
<p class="muted">No summaries stored yet. Build today's summary to create one.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -28,6 +28,7 @@
|
||||
<div class="nav-actions">
|
||||
<a class="btn btn-utility" href="/">Back to inbox</a>
|
||||
<a class="btn btn-utility" href="/unsubscribe">Unsubscribe</a>
|
||||
<a class="btn btn-utility" href="/reports">Reports</a>
|
||||
<button class="btn btn-utility theme-toggle" type="button" onclick="eaToggleTheme()" aria-label="Toggle dark mode">
|
||||
<span class="icon-light" aria-hidden="true">☾</span>
|
||||
<span class="icon-dark" aria-hidden="true">☀</span>
|
||||
|
||||
Reference in New Issue
Block a user