feat: Persist outbound emails, fix @UPDATE_TEAM_MEMBER parser, add per-entry delete

- Add sent_emails table to DB for persistent outbox logging
- send_email() now writes every outgoing mail (incl. errors) to sent_emails
- parse_agent_commands() passes agent_key/task_id as triggered_by metadata
- Fix @UPDATE_TEAM_MEMBER parser: now matches Identifier/TelegramID/Role/etc.
  format from system prompt (was expecting Email/Field/Value — never matched)
- update_team_member() called correctly via **kwargs (was positional args bug)
- Set Piotr telegram_id=1578034974 directly in DB
- email_log.html: two-tab UI (Inbox Journal + Outbox), click-to-expand body
- emails.html: per-message delete button in inbox list
- New routes: DELETE inbox (IMAP expunge), journal entry, sent entry
This commit is contained in:
eric 2026-02-21 18:43:21 +00:00
parent 99df910497
commit f6ad727bf0
3 changed files with 340 additions and 165 deletions

View file

@ -4,87 +4,177 @@
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between">
<div>
<h1>Email-Verarbeitungs-Log</h1>
<p>Automatisch verarbeitete Emails und Antworten</p>
</div>
<div class="d-flex gap-2 align-items-center">
<span class="badge bg-secondary" style="font-size:.85rem;">{{ log_entries|length }} Einträge</span>
<button class="btn btn-secondary btn-sm" onclick="location.reload()">Aktualisieren</button>
<h1>Email-Log</h1>
<p>Posteingang-Journal und gesendete Emails</p>
</div>
<button class="btn btn-secondary btn-sm" onclick="location.reload()">Aktualisieren</button>
</div>
<div class="mb-3" style="font-size:.8rem;color:var(--text-muted);">
<span class="status-replied me-3">✓ replied</span> — Auto-Reply versendet &nbsp;|&nbsp;
<span class="status-skipped me-3">— skipped</span> — Nicht auf Whitelist &nbsp;|&nbsp;
<span class="status-error">✗ error</span> — Fehler beim Versenden
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="emailLogTabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-inbox">
Posteingang / Journal
<span class="badge bg-secondary ms-1">{{ journal_rows|length }}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-sent">
Gesendet
<span class="badge bg-primary ms-1">{{ sent_rows|length }}</span>
</a>
</li>
</ul>
{% if not log_entries %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📭</p>
<p>Noch keine Emails verarbeitet.<br><small>Der Poller prüft alle 2 Minuten den Posteingang.</small></p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Von</th>
<th>Betreff</th>
<th>Agent</th>
<th>Status</th>
<th>Antwort-Vorschau</th>
</tr>
</thead>
<tbody>
{% for entry in log_entries %}
<tr>
<td style="white-space:nowrap;"><small style="color:var(--text-muted);">{{ entry.timestamp }}</small></td>
<td><small>{{ entry.from }}</small></td>
<td>{{ entry.subject }}</td>
<td>
{% if entry.agent %}
<span class="badge bg-primary badge-agent">{{ entry.agent }}</span>
{% else %}
<span style="color:var(--text-muted);"></span>
{% endif %}
</td>
<td>
{% if entry.status == 'replied' or entry.status == 'completed' %}
<span class="status-replied">✓ replied</span>
{% elif entry.status == 'skipped' %}
<span class="status-skipped">— skipped</span>
{% elif entry.status == 'error' %}
<span class="status-error">✗ error</span>
{% elif entry.status == 'queued' %}
<span style="color:var(--warning);">⏳ queued</span>
{% else %}
<span style="color:var(--text-muted);">{{ entry.status }}</span>
{% endif %}
</td>
<td>
{% if entry.response_preview %}
<div class="response-preview">{{ entry.response_preview }}</div>
{% else %}
<small style="color:var(--text-muted);"></small>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="tab-content">
<!-- ── TAB: Posteingang / Journal ── -->
<div class="tab-pane fade show active" id="tab-inbox">
{% if not journal_rows %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📭</p>
<p>Keine Journal-Einträge vorhanden.</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Empfangen</th>
<th>Von</th>
<th>Betreff</th>
<th>Agent</th>
<th>Status</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
{% for row in journal_rows %}
<tr>
<td style="white-space:nowrap;">
<small style="color:var(--text-muted);">{{ row.received_at[:16] if row.received_at else '—' }}</small>
</td>
<td><small>{{ row.sender or '—' }}</small></td>
<td>{{ row.subject or '—' }}</td>
<td>
{% if row.agent_key %}
<span class="badge bg-primary badge-agent">{{ row.agent_key }}</span>
{% else %}
<span style="color:var(--text-muted);"></span>
{% endif %}
</td>
<td>
{% if row.status == 'completed' %}
<span class="status-replied">✓ completed</span>
{% elif row.status == 'skipped' %}
<span class="status-skipped">— skipped</span>
{% elif row.status == 'error' %}
<span class="status-error">✗ error</span>
{% elif row.status == 'queued' %}
<span style="color:var(--warning);">⏳ queued</span>
{% else %}
<span style="color:var(--text-muted);">{{ row.status }}</span>
{% endif %}
</td>
<td>
<form method="POST" action="/emails/journal/{{ row.message_id | urlencode }}/delete"
onsubmit="return confirm('Journal-Eintrag löschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen"></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="mt-3" style="font-size:.78rem;color:var(--text-muted);">
Whitelist: <strong>eric.fischer@signtime.media</strong>, <strong>p.dyderski@live.at</strong>,
<strong>georg.tschare@gmail.com</strong>, <strong>*@diversityball.at</strong> · Max. 50 Einträge
</div>
<!-- ── TAB: Gesendet ── -->
<div class="tab-pane fade" id="tab-sent">
{% if not sent_rows %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📤</p>
<p>Noch keine gesendeten Emails im Log.<br>
<small>Ab jetzt werden alle ausgehenden Emails hier gespeichert.</small>
</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Gesendet</th>
<th>An</th>
<th>Betreff</th>
<th>Ausgelöst von</th>
<th>Status</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
{% for row in sent_rows %}
<tr style="cursor:pointer;" onclick="toggleBody({{ row.id }})">
<td style="white-space:nowrap;">
<small style="color:var(--text-muted);">{{ row.sent_at[:16] if row.sent_at else '—' }}</small>
</td>
<td><small>{{ row.to_address or '—' }}</small></td>
<td>{{ row.subject or '—' }}</td>
<td>
<small style="color:var(--text-muted);">
{{ row.triggered_by or 'manual' }}
{% if row.task_id %}<span class="badge bg-secondary ms-1">Task #{{ row.task_id }}</span>{% endif %}
</small>
</td>
<td>
{% if row.status == 'sent' %}
<span class="status-replied">✓ gesendet</span>
{% elif row.status == 'error' %}
<span class="status-error">✗ Fehler</span>
{% else %}
<span style="color:var(--text-muted);">{{ row.status }}</span>
{% endif %}
</td>
<td onclick="event.stopPropagation()">
<form method="POST" action="/emails/sent/{{ row.id }}/delete"
onsubmit="return confirm('Eintrag löschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen"></button>
</form>
</td>
</tr>
<!-- Expandable body row -->
<tr id="body-{{ row.id }}" style="display:none;">
<td colspan="6" style="background:var(--bg-elevated);padding:1rem;">
<pre style="white-space:pre-wrap;font-size:.82rem;margin:0;color:var(--text-primary);">{{ row.body or '(kein Inhalt)' }}</pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div><!-- /.tab-content -->
{% endblock %}
{% block scripts %}
<script>
function toggleBody(id) {
const row = document.getElementById('body-' + id);
if (row) row.style.display = row.style.display === 'none' ? '' : 'none';
}
</script>
{% endblock %}