- 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>
127 lines
5.3 KiB
HTML
127 lines
5.3 KiB
HTML
<!doctype html>
|
|
<html lang="en" data-theme="light">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Rules · 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="/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>
|
|
</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">Rules</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 rules-panel" aria-label="Rules editor">
|
|
<div class="panel-head">
|
|
<h2>Rules editor</h2>
|
|
<span class="muted">version {{.Version}} · {{.RuleCount}} active rule(s) · {{.TotalMessages}} message(s)</span>
|
|
</div>
|
|
|
|
<div class="panel-body">
|
|
<p class="muted rules-help">
|
|
One rule per line: <code><condition> => <effects></code>.
|
|
Fields are <code>from</code>, <code>to</code>, <code>subject</code>,
|
|
<code>source</code>, and <code>body</code>; operators are
|
|
<code>contains</code>, <code>equals</code>, <code>starts_with</code>,
|
|
and <code>ends_with</code>. The first matching rule wins and overrides
|
|
the JEV fields it names. Lines starting with <code>#</code> are comments.
|
|
The classifier and the reply model both read this text.
|
|
</p>
|
|
|
|
{{if .Errors}}
|
|
<div class="alert alert-error" role="alert">
|
|
<strong>{{len .Errors}} line(s) could not be parsed and are being skipped:</strong>
|
|
<ul class="rules-errors">
|
|
{{range .Errors}}<li>{{.Error}}</li>{{end}}
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
|
|
<form class="rules-form" method="post" action="/rules/save">
|
|
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
|
|
<label class="field" for="rules-text">
|
|
<span>Rules text (plain text)</span>
|
|
</label>
|
|
<textarea class="text-input rules-textarea" id="rules-text" name="text" rows="18" spellcheck="false">{{.Text}}</textarea>
|
|
<div class="rules-actions">
|
|
<button class="btn btn-primary" type="submit">Save new version</button>
|
|
<span class="muted faint">Saving appends a version; nothing is re-classified until you re-classify.</span>
|
|
</div>
|
|
</form>
|
|
|
|
{{if .ClassifierReady}}
|
|
<form class="rules-reclassify" method="post" action="/rules/reclassify"
|
|
onsubmit="return confirm('Re-classify every stored message with the current rules?');">
|
|
<input type="hidden" name="csrf" value="{{.CSRFToken}}">
|
|
<button class="btn btn-utility" type="submit">Re-classify all now</button>
|
|
</form>
|
|
{{else}}
|
|
<p class="faint">Re-classify is unavailable until JEV_API_KEY is set.</p>
|
|
{{end}}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel rules-panel" aria-label="Rules versions">
|
|
<div class="panel-head"><h2>Versions</h2></div>
|
|
<div class="panel-body">
|
|
<div class="rules-versions">
|
|
{{range .Revisions}}
|
|
<div class="rules-version">
|
|
<div class="rules-version-main">
|
|
<span class="pill">v{{.Version}}</span>
|
|
<span class="muted">{{.CreatedAt}}</span>
|
|
<span class="muted">{{.Lines}} rule line(s)</span>
|
|
<span class="rules-version-preview">{{.Preview}}</span>
|
|
</div>
|
|
<form method="post" action="/rules/restore"
|
|
onsubmit="return confirm('Restore version {{.Version}} as a new version?');">
|
|
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
|
|
<input type="hidden" name="version" value="{{.Version}}">
|
|
<button class="btn btn-utility btn-small" type="submit">Restore</button>
|
|
</form>
|
|
</div>
|
|
{{else}}
|
|
<p class="muted">No versions stored yet.</p>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|