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:
parent
99df910497
commit
f6ad727bf0
3 changed files with 340 additions and 165 deletions
|
|
@ -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 |
|
||||
<span class="status-skipped me-3">— skipped</span> — Nicht auf Whitelist |
|
||||
<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 %}
|
||||
|
|
|
|||
|
|
@ -60,14 +60,19 @@
|
|||
{% else %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for mail in emails %}
|
||||
<li class="list-group-item list-group-item-action"
|
||||
style="cursor:pointer;" onclick="viewEmail('{{ mail.id }}', '{{ mail.subject|e }}', '{{ mail.from|e }}')">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<strong style="font-size:.875rem;">{{ mail.subject }}</strong>
|
||||
<small style="color:var(--text-muted);white-space:nowrap;margin-left:.5rem;">{{ mail.date[:10] }}</small>
|
||||
<li class="list-group-item list-group-item-action d-flex align-items-start gap-2" style="cursor:pointer;">
|
||||
<div class="flex-grow-1" onclick="viewEmail('{{ mail.id }}', '{{ mail.subject|e }}', '{{ mail.from|e }}')">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<strong style="font-size:.875rem;">{{ mail.subject }}</strong>
|
||||
<small style="color:var(--text-muted);white-space:nowrap;margin-left:.5rem;">{{ mail.date[:10] }}</small>
|
||||
</div>
|
||||
<div style="font-size:.8rem;color:var(--text-muted);">{{ mail.from }}</div>
|
||||
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.2rem;">{{ mail.preview }}</div>
|
||||
</div>
|
||||
<div style="font-size:.8rem;color:var(--text-muted);">{{ mail.from }}</div>
|
||||
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.2rem;">{{ mail.preview }}</div>
|
||||
<form method="POST" action="/emails/inbox/{{ mail.id }}/delete"
|
||||
onsubmit="return confirm('Email aus Posteingang löschen?')" style="flex-shrink:0;">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen">✕</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue