howl/howl/web/templates/runner.html
lorentz ecc6681432 Add web UI, sender profiles, purge rules, and infosec action
Extends the pipeline with infosec classification, sender profile tracking,
and configurable purge rules. Adds a web dashboard for managing rules and
monitoring email processing. Includes new migrations and seed script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:12:56 -04:00

156 lines
6.3 KiB
HTML

{% extends "base.html" %}
{% block title %}Run Purge{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto space-y-6">
<div>
<h1 class="text-2xl font-bold">Run Purge</h1>
<p class="text-sm text-base-content/60 mt-0.5">
Scan the mailbox and delete matching messages. Always preview first.
</p>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body space-y-5">
<!-- Source selector -->
<div>
<label class="label"><span class="label-text font-medium">Source</span></label>
<div class="join w-full">
<button type="button" id="btn-table"
class="join-item btn btn-sm btn-primary flex-1"
onclick="setSource('table')">
Active Purge Rules
{% if active_rules %}
<span class="badge badge-sm ml-1">{{ active_rules | length }}</span>
{% endif %}
</button>
<button type="button" id="btn-custom"
class="join-item btn btn-sm btn-ghost border border-base-300 flex-1"
onclick="setSource('custom')">
Custom Address
</button>
</div>
</div>
<!-- Active rules summary (shown when source=table) -->
<div id="table-summary">
{% if active_rules %}
<div class="bg-base-200 rounded-box p-3 space-y-1 max-h-48 overflow-y-auto">
{% for r in active_rules %}
<div class="flex items-center justify-between text-sm">
<span class="font-mono">{{ r.email_address }}</span>
<span class="text-base-content/50 text-xs">delete &gt;{{ r.older_than_days }}d old</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning py-2 text-sm">
No active purge rules.
<a href="/purge" class="link">Add rules →</a>
</div>
{% endif %}
</div>
<!-- Custom address fields (hidden by default) -->
<div id="custom-fields" class="hidden space-y-3">
<div class="form-control">
<label class="label"><span class="label-text">Email Address</span></label>
<input id="custom-address-input" type="email" placeholder="sender@example.com"
class="input input-bordered input-sm w-full" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Delete emails older than</span>
</label>
<div class="flex items-center gap-2">
<input id="custom-days-input" type="number" value="30" min="1" max="3650"
class="input input-bordered input-sm w-24" />
<span class="text-sm text-base-content/60">days</span>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="flex gap-3 pt-2">
<button class="btn btn-outline btn-sm flex-1"
onclick="submitRun('preview')"
id="btn-preview">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Preview (dry run)
</button>
<button class="btn btn-error btn-sm flex-1"
onclick="submitRun('execute')"
id="btn-execute">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Execute Purge
</button>
</div>
</div>
</div>
<!-- Results area -->
<div id="runner-results"></div>
</div>
<!-- Hidden form for htmx submission -->
<form id="runner-form"
hx-post="/runner/go"
hx-target="#runner-results"
hx-swap="innerHTML"
hx-indicator="#run-spinner"
style="display:none">
<input type="hidden" name="mode" id="f-mode" value="preview" />
<input type="hidden" name="source" id="f-source" value="table" />
<input type="hidden" name="custom_address" id="f-address" value="" />
<input type="hidden" name="custom_days" id="f-days" value="30" />
</form>
<script>
let currentSource = 'table';
function setSource(src) {
currentSource = src;
document.getElementById('table-summary').classList.toggle('hidden', src !== 'table');
document.getElementById('custom-fields').classList.toggle('hidden', src !== 'custom');
document.getElementById('btn-table').className =
'join-item btn btn-sm flex-1 ' + (src === 'table' ? 'btn-primary' : 'btn-ghost border border-base-300');
document.getElementById('btn-custom').className =
'join-item btn btn-sm flex-1 ' + (src === 'custom' ? 'btn-primary' : 'btn-ghost border border-base-300');
}
function submitRun(mode) {
if (mode === 'execute') {
const count = document.querySelectorAll('#runner-results .result-count').length;
const confirmed = confirm(
'This will permanently delete emails from the mailbox.\n\nRun a Preview first to see what will be affected.\n\nContinue with execution?'
);
if (!confirmed) return;
}
document.getElementById('f-mode').value = mode;
document.getElementById('f-source').value = currentSource;
document.getElementById('f-address').value =
document.getElementById('custom-address-input').value;
document.getElementById('f-days').value =
document.getElementById('custom-days-input').value;
// Show spinner in results area immediately
document.getElementById('runner-results').innerHTML =
'<div class="card bg-base-100 shadow-sm"><div class="card-body text-center py-10">' +
'<span class="loading loading-spinner loading-md text-primary"></span>' +
'<p class="text-sm text-base-content/50 mt-2">Scanning mailbox' +
(mode === 'execute' ? ' and deleting messages' : '') + '…</p></div></div>';
htmx.trigger(document.getElementById('runner-form'), 'submit');
}
</script>
{% endblock %}