feat: initial commit – Frankenbot Multi-Agent Orchestration System

- Flask Web-App mit Dashboard, Chat, Orchestrator, Tasks, Dateien, Emails, Agenten, Settings
- Email-Poller (IMAP) mit SQLite-Journal als Failsafe (kein Emailverlust bei Absturz)
- Failsafe-Fenster und Poll-Intervall zur Laufzeit via /settings konfigurierbar
- TaskWorker: IMAP Seen-Flag erst nach erfolgreichem Task-Abschluss
- Whitelist-Filter: eric.fischer, p.dyderski, georg.tschare (gmail + signtime.media), *@diversityball.at
- 9 Agenten: researcher, tax_advisor, document_editor, location_manager, program_manager,
  catering_manager, musik_rechte_advisor, zusammenfasser, orchestration_ui
- Diversity Ball Wien 2026 – Wissensdatenbank, Sponsoringverträge, Email-Vorlagen
This commit is contained in:
Pjot 2026-02-20 17:31:16 +01:00
commit 56d9bc2c76
71 changed files with 5953 additions and 0 deletions

75
templates/agents.html Normal file
View file

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Agenten{% endblock %}
{% block content %}
<div class="page-header">
<h1>Agenten-Verwaltung</h1>
<p>System-Prompts bearbeiten und verwalten</p>
</div>
{% if edit_agent %}
<div class="card">
<div class="card-header bg-warning">
<h5 class="mb-0">Prompt bearbeiten: {{ edit_agent }}</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('agents') }}">
<input type="hidden" name="agent_name" value="{{ edit_agent }}">
<div class="mb-3">
<label for="prompt_content" class="form-label">System-Prompt</label>
<textarea class="form-control font-monospace" id="prompt_content" name="prompt_content"
rows="22" style="font-size:.82rem;line-height:1.5;">{{ edit_prompt }}</textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Speichern</button>
<a href="{{ url_for('agents') }}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
</div>
{% else %}
<div class="card">
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
<h5 class="mb-0">Alle Agenten</h5>
<span class="badge bg-secondary">{{ agents_list|length }}</span>
</div>
<div class="card-body p-0">
{% if agents_list %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Agent</th>
<th>Prompt-Vorschau</th>
<th style="width:120px;">Aktionen</th>
</tr>
</thead>
<tbody>
{% for agent in agents_list %}
<tr>
<td>
<strong>{{ agent.name }}</strong>
</td>
<td>
<small style="color:var(--text-muted);">
{{ agent.prompt[:160] }}{% if agent.prompt|length > 160 %}…{% endif %}
</small>
</td>
<td>
<a href="{{ url_for('agents', edit=agent.name) }}" class="btn btn-sm btn-outline-primary">Bearbeiten</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning m-3">
Keine Agenten gefunden. Stelle sicher, dass <code>agents/</code> Unterverzeichnisse mit <code>systemprompt.md</code> enthält.
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}

102
templates/base.html Normal file
View file

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Frankenbot{% endblock %} · Frankenbot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand" href="/">
<span class="brand-icon"></span>
Frankenbot
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto gap-1">
<li class="nav-item">
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">
<span></span> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/chat' %}active{% endif %}" href="/chat">
<span>💬</span> Chat
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/orchestrator' %}active{% endif %}" href="/orchestrator">
<span>🤖</span> Orchestrator
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/tasks' %}active{% endif %}" href="/tasks">
<span></span> Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/files' %}active{% endif %}" href="/files">
<span>📂</span> Dateien
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/agents' %}active{% endif %}" href="/agents">
<span></span> Agenten
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/emails' %}active{% endif %}" href="/emails">
<span></span> Emails
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/email-log' %}active{% endif %}" href="/email-log">
<span>📋</span> Log
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/settings' %}active{% endif %}" href="/settings">
<span></span> Settings
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container py-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show mb-3" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<footer>
<div class="container">
Frankenbot &middot; Diversity-Ball Wien 2026 &middot;
<span style="color:var(--accent);">Agent Orchestration System</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

70
templates/chat.html Normal file
View file

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Chat{% endblock %}
{% block content %}
<div class="page-header">
<h1>Agenten Chat</h1>
<p>Direkte Anfrage an einen spezifischen Agenten</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-primary">
<span>💬 Neue Anfrage</span>
</div>
<div class="card-body">
<form method="POST" action="/chat">
<div class="mb-3">
<label for="agent" class="form-label">Agent</label>
<select class="form-select" id="agent" name="agent" required>
<option value="">— Agent wählen —</option>
{% for key, agent in agents.items() %}
<option value="{{ key }}">{{ agent.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="prompt" class="form-label">Ihre Anfrage</label>
<textarea class="form-control" id="prompt" name="prompt" rows="5"
placeholder="Was soll der Agent erledigen?" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Absenden</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-secondary d-flex align-items-center justify-content-between">
<span>Chat-Verlauf</span>
{% if chat_history %}
<span class="badge bg-secondary">{{ chat_history|length }}</span>
{% endif %}
</div>
<div class="card-body chat-container" id="chatContainer">
{% if chat_history %}
{% for chat in chat_history %}
<div class="chat-message">
<div class="chat-timestamp">
{{ chat.timestamp }}
<span class="badge bg-primary">{{ chat.agent }}</span>
</div>
<div class="chat-prompt"><strong>Sie:</strong> {{ chat.prompt }}</div>
<div class="chat-response mt-1"><strong>Antwort:</strong> {{ chat.response }}</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);">
<div style="font-size:2.5rem;margin-bottom:.75rem;">💬</div>
<p style="margin:0;">Noch keine Nachrichten.<br>
<small>Starten Sie eine Konversation.</small>
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

90
templates/email_log.html Normal file
View file

@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}Email-Log{% endblock %}
{% 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>
</div>
</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>
{% 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>
</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>
{% endblock %}

123
templates/emails.html Normal file
View file

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% block title %}Emails{% endblock %}
{% block content %}
<div class="page-header">
<h1>Email-Verwaltung</h1>
<p>Posteingang und Email-Versand</p>
</div>
{% if not email_config_valid %}
<div class="alert alert-warning">
<strong>Konfiguration erforderlich</strong><br>
<small>Setze <code>IMAP_SERVER</code>, <code>SMTP_SERVER</code>, <code>EMAIL_ADDRESS</code>, <code>EMAIL_PASSWORD</code> in der <code>.env</code>-Datei.</small>
</div>
{% else %}
<div class="alert alert-success">
<strong>Verbunden</strong> · {{ current_email }}
</div>
{% endif %}
<div class="row g-4">
<!-- Neue Email -->
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-dark">
<h5 class="mb-0">Neue Email</h5>
</div>
<div class="card-body">
<form method="POST" action="/emails">
<input type="hidden" name="action" value="send">
<div class="mb-3">
<label for="to_address" class="form-label">An</label>
<input type="email" class="form-control" id="to_address" name="to_address" required>
</div>
<div class="mb-3">
<label for="subject" class="form-label">Betreff</label>
<input type="text" class="form-control" id="subject" name="subject" required>
</div>
<div class="mb-3">
<label for="body" class="form-label">Nachricht</label>
<textarea class="form-control" id="body" name="body" rows="8" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Versenden</button>
</form>
</div>
</div>
</div>
<!-- Posteingang -->
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
<h5 class="mb-0">Posteingang</h5>
<span class="badge bg-secondary">{{ emails|length }} Emails</span>
</div>
<div class="card-body p-0" style="max-height:600px;overflow-y:auto;">
{% if email_config_valid and emails %}
{% if emails[0].error is defined and emails[0].error %}
<div class="alert alert-danger m-3">{{ emails[0].error }}</div>
{% 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>
</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>
</li>
{% endfor %}
</ul>
{% endif %}
{% elif not email_config_valid %}
<div class="text-center py-5" style="color:var(--text-muted);">
<p>Email-Konfiguration erforderlich</p>
</div>
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);">
<p>Keine Emails vorhanden</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailSubject"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p style="color:var(--text-muted);font-size:.85rem;"><strong>Von:</strong> <span id="emailFrom"></span></p>
<hr>
<pre id="emailBody" style="background:var(--bg-elevated);color:var(--text-primary);
border-radius:var(--radius-sm);padding:1rem;white-space:pre-wrap;
max-height:400px;overflow-y:auto;font-size:.85rem;">Wird geladen…</pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function viewEmail(emailId, subject, from) {
document.getElementById('emailSubject').textContent = subject;
document.getElementById('emailFrom').textContent = from;
document.getElementById('emailBody').textContent = 'Wird geladen…';
const modal = new bootstrap.Modal(document.getElementById('emailModal'));
modal.show();
fetch('/emails/' + emailId)
.then(r => r.json())
.then(d => { document.getElementById('emailBody').textContent = d.content; })
.catch(e => { document.getElementById('emailBody').textContent = 'Fehler: ' + e.message; });
}
</script>
{% endblock %}

262
templates/files.html Normal file
View file

@ -0,0 +1,262 @@
{% extends "base.html" %}
{% block title %}Dateien{% endblock %}
{% block content %}
<div class="page-header">
<h1>Dateiverwaltung</h1>
<p>Uploads, Email-Vorlagen und Projektdokumente</p>
</div>
<div class="row g-4">
<!-- Upload sidebar -->
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-info">
<span>📤 Datei hochladen</span>
</div>
<div class="card-body">
<form method="POST" action="/files" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">Datei auswählen</label>
<input type="file" class="form-control" id="file" name="file" required>
</div>
<p style="color:var(--text-muted);font-size:.76rem;margin-bottom:.75rem;">Max. 16 MB</p>
<button type="submit" class="btn btn-info w-100">Hochladen</button>
</form>
</div>
</div>
<!-- Quick stats -->
<div class="card mt-3">
<div class="card-body p-0">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center" style="padding:.7rem 1rem;">
<span style="font-size:.85rem;color:var(--text-secondary);">📁 Uploads</span>
<span class="badge bg-secondary">{{ files|length }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" style="padding:.7rem 1rem;">
<span style="font-size:.85rem;color:var(--text-secondary);">✉ Email-Vorlagen</span>
<span class="badge bg-secondary">{{ email_files|length }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center" style="padding:.7rem 1rem;">
<span style="font-size:.85rem;color:var(--text-secondary);">📄 Projektdokumente</span>
<span class="badge bg-secondary">{{ project_files|length }}</span>
</li>
</ul>
</div>
</div>
</div>
<!-- File lists -->
<div class="col-lg-8">
<!-- Uploads -->
<div class="card mb-3">
<div class="card-header bg-primary d-flex justify-content-between align-items-center">
<span>📁 Hochgeladene Dateien</span>
<span class="badge bg-secondary">{{ files|length }}</span>
</div>
<div class="card-body">
{% if files %}
{% for file in files %}
<div class="file-item">
<span class="file-icon">{{ '📄' if file.name.endswith('.txt') else '📝' if file.name.endswith('.md') else '📋' if file.name.endswith('.docx') else '📁' }}</span>
<div style="flex:1;min-width:0;">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified }}</div>
</div>
<div class="file-actions">
<a href="/files/download/{{ file.name }}" class="btn btn-sm btn-secondary" title="Herunterladen"></a>
<a href="/files/delete/{{ file.name }}" class="btn btn-sm btn-danger"
onclick="return confirm('Datei löschen?')" title="Löschen">✕</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-4" style="color:var(--text-muted);">
<div style="font-size:2rem;margin-bottom:.5rem;">📂</div>
<p style="margin:0;font-size:.875rem;">Noch keine Dateien hochgeladen.</p>
</div>
{% endif %}
</div>
</div>
<!-- Email-Vorlagen -->
<div class="card mb-3">
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
<span>✉ Email-Vorlagen <small style="font-weight:400;font-size:.72rem;color:var(--text-muted);margin-left:.4rem;">emails/</small></span>
<span class="badge bg-secondary">{{ email_files|length }}</span>
</div>
<div class="card-body">
{% if email_files %}
{% for file in email_files %}
<div class="file-item" id="file-row-{{ loop.index }}">
<span class="file-icon"></span>
<div style="flex:1;min-width:0;">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified }}</div>
</div>
<div class="file-actions">
<button class="btn btn-sm btn-secondary"
onclick="viewEmailFile(event, '{{ file.name }}')" title="Anzeigen">👁</button>
<button class="btn btn-sm btn-primary"
onclick="editEmailFile('{{ file.name }}')" title="Bearbeiten">✎</button>
<a href="/files/email/delete/{{ file.name }}" class="btn btn-sm btn-danger"
onclick="return confirm('Email-Vorlage löschen?')" title="Löschen">✕</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-4" style="color:var(--text-muted);">
<div style="font-size:2rem;margin-bottom:.5rem;"></div>
<p style="margin:0;font-size:.875rem;">Keine Email-Vorlagen gefunden.</p>
</div>
{% endif %}
</div>
</div>
<!-- Projektdokumente -->
<div class="card">
<div class="card-header bg-secondary d-flex justify-content-between align-items-center">
<span>📄 Projektdokumente <small style="font-weight:400;font-size:.72rem;color:var(--text-muted);margin-left:.4rem;">Arbeitsverzeichnis</small></span>
<span class="badge bg-secondary">{{ project_files|length }}</span>
</div>
<div class="card-body">
{% if project_files %}
{% for file in project_files %}
<div class="file-item">
<span class="file-icon">{{ '📋' if file.name.endswith('.docx') else '📝' if file.name.endswith('.md') else '📄' }}</span>
<div style="flex:1;min-width:0;">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified }}</div>
</div>
<div class="file-actions">
<button class="btn btn-sm btn-secondary"
onclick="viewProjectFile(event, '{{ file.name }}')" title="Anzeigen">👁</button>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-4" style="color:var(--text-muted);">
<p style="margin:0;font-size:.875rem;color:var(--text-muted);">Keine Projektdokumente gefunden.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- File Viewer Modal -->
<div class="modal fade" id="fileModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="fileModalTitle">Datei</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<pre id="fileModalContent" style="background:var(--bg-base);color:#a5f3fc;
border-radius:var(--radius-sm);padding:1.1rem;white-space:pre-wrap;
word-break:break-word;max-height:70vh;overflow-y:auto;font-size:.83rem;
border:1px solid var(--border);font-family:'JetBrains Mono',monospace;">Wird geladen…</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
<!-- Email Editor Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">✎ Bearbeiten: <span id="editModalFilename"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea id="editModalContent" class="inline-editor" spellcheck="false"></textarea>
<div id="editSaveStatus" style="font-size:.8rem;color:var(--text-muted);margin-top:.5rem;height:1.2em;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="saveEmailFile()">💾 Speichern</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentEditFile = null;
function openFileModal(title, url) {
document.getElementById('fileModalTitle').textContent = title;
const content = document.getElementById('fileModalContent');
content.textContent = 'Wird geladen…';
const modal = new bootstrap.Modal(document.getElementById('fileModal'));
modal.show();
fetch(url)
.then(r => r.json())
.then(d => { content.textContent = d.content || d.error || '(leer)'; })
.catch(e => { content.textContent = 'Fehler: ' + e.message; });
}
function viewEmailFile(event, name) {
event.preventDefault();
openFileModal(name, '/files/email/view/' + encodeURIComponent(name) + '?json=1');
}
function viewProjectFile(event, name) {
event.preventDefault();
openFileModal(name, '/files/project/view/' + encodeURIComponent(name) + '?json=1');
}
function editEmailFile(name) {
currentEditFile = name;
document.getElementById('editModalFilename').textContent = name;
const textarea = document.getElementById('editModalContent');
const status = document.getElementById('editSaveStatus');
textarea.value = 'Wird geladen…';
status.textContent = '';
const modal = new bootstrap.Modal(document.getElementById('editModal'));
modal.show();
fetch('/files/email/view/' + encodeURIComponent(name) + '?json=1')
.then(r => r.json())
.then(d => { textarea.value = d.content || d.error || ''; })
.catch(e => { textarea.value = 'Fehler: ' + e.message; });
}
function saveEmailFile() {
if (!currentEditFile) return;
const content = document.getElementById('editModalContent').value;
const status = document.getElementById('editSaveStatus');
status.style.color = 'var(--text-muted)';
status.textContent = 'Speichern…';
fetch('/files/email/save/' + encodeURIComponent(currentEditFile), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
})
.then(r => r.json())
.then(d => {
if (d.ok) {
status.style.color = 'var(--success)';
status.textContent = '✓ Gespeichert';
setTimeout(() => { status.textContent = ''; }, 3000);
} else {
status.style.color = 'var(--danger)';
status.textContent = '✗ Fehler: ' + (d.error || 'Unbekannt');
}
})
.catch(e => {
status.style.color = 'var(--danger)';
status.textContent = '✗ ' + e.message;
});
}
</script>
{% endblock %}

126
templates/index.html Normal file
View file

@ -0,0 +1,126 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="hero-section">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h1 style="font-size:1.75rem;margin-bottom:.4rem;-webkit-text-fill-color:unset;background:none;color:var(--text-primary);">
Diversity-Ball Wien 2026
</h1>
<p style="color:var(--text-secondary);margin:0;font-size:.9rem;">
Agenten-Orchestrierung · Samstag 5. September · Wiener Rathaus
</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a href="/orchestrator" class="btn btn-primary">🤖 Orchestrator</a>
<a href="/chat" class="btn btn-secondary">💬 Chat</a>
<a href="/files" class="btn btn-secondary">📂 Dateien</a>
</div>
</div>
</div>
<!-- Stats -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-number">{{ agents|length }}</div>
<div class="stat-label">Agenten</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-number">{{ recent_tasks|length }}</div>
<div class="stat-label">Letzte Tasks</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-number">5. Sep</div>
<div class="stat-label">Event-Datum</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="stat-card">
<div class="stat-number">3.500</div>
<div class="stat-label">Gäste</div>
</div>
</div>
</div>
<!-- Agents -->
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 style="margin:0;">Verfügbare Agenten</h3>
<a href="/agents" class="btn btn-outline-secondary btn-sm">Alle verwalten</a>
</div>
<div class="row g-3 mb-4">
{% for key, agent in agents.items() %}
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card agent-card h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-2 mb-2">
<span style="width:8px;height:8px;border-radius:50%;background:var(--success);flex-shrink:0;box-shadow:0 0 6px var(--success);"></span>
<h5 style="margin:0;font-size:.875rem;font-weight:600;">{{ agent.name }}</h5>
</div>
<p style="font-size:.775rem;color:var(--text-muted);line-height:1.45;margin-bottom:.75rem;">
{{ agent.description[:90] }}{% if agent.description|length > 90 %}…{% endif %}
</p>
<span class="badge bg-success">aktiv</span>
</div>
<div class="card-footer">
<a href="/agents?edit={{ key }}" class="btn btn-outline-primary btn-sm w-100">⚙ Bearbeiten</a>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Recent Tasks -->
{% if recent_tasks %}
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 style="margin:0;">Letzte Tasks</h3>
<a href="/tasks" class="btn btn-outline-secondary btn-sm">Alle Tasks</a>
</div>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Titel</th>
<th>Agent</th>
<th>Status</th>
<th>Erstellt</th>
</tr>
</thead>
<tbody>
{% for task in recent_tasks %}
<tr>
<td><span style="color:var(--text-muted);font-size:.8rem;">#{{ task.id }}</span></td>
<td style="font-weight:500;">{{ task.title }}</td>
<td style="font-size:.8rem;color:var(--text-muted);">{{ task.assigned_agent }}</td>
<td>
{% if task.status == 'pending' %}
<span class="badge bg-warning">Pending</span>
{% elif task.status == 'in_progress' %}
<span class="badge bg-primary">Läuft</span>
{% elif task.status == 'completed' %}
<span class="badge bg-success">Fertig</span>
{% elif task.status == 'error' %}
<span class="badge bg-danger">Fehler</span>
{% else %}
<span class="badge bg-secondary">{{ task.status }}</span>
{% endif %}
</td>
<td style="color:var(--text-muted);font-size:.78rem;">{{ task.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% endblock %}

157
templates/orchestrator.html Normal file
View file

@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}Orchestrator{% endblock %}
{% block content %}
<div class="page-header">
<h1>Master-Orchestrator</h1>
<p>Delegiert automatisch an den passenden Agenten</p>
</div>
{% if knowledge_loaded %}
<div class="alert alert-success mb-4">
<strong>Wissensdatenbank geladen</strong> · Agenten-Prompts initialisiert
</div>
{% endif %}
<div class="row g-4">
<!-- Sidebar -->
<div class="col-lg-4">
<div class="card mb-3">
<div class="card-header bg-dark">
<h5 class="mb-0">Prompt eingeben</h5>
</div>
<div class="card-body">
<form id="promptForm" method="POST" action="/orchestrator">
<div class="mb-3">
<label for="prompt" class="form-label">Ihre Anfrage</label>
<textarea class="form-control" id="prompt" name="prompt" rows="5"
placeholder="Was soll erledigt werden?" required></textarea>
</div>
<button type="button" class="btn btn-primary w-100 mb-2" id="streamBtn"
onclick="sendPromptWithStream()">Live-Antwort anfordern</button>
<button type="submit" class="btn btn-secondary w-100">Klassisch senden</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header bg-secondary">
<h6 class="mb-0">Aktive Agenten</h6>
</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
{% for key, agent in agents.items() %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span style="font-size:.85rem;">{{ agent.name }}</span>
<span class="badge bg-success" style="font-size:.65rem;">aktiv</span>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<!-- Chat -->
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
<h5 class="mb-0">Orchestrator-Chat</h5>
<small style="color:var(--text-muted);">Automatische Agenten-Delegation</small>
</div>
<div class="card-body chat-container" id="chatContainer">
{% if chat_history %}
{% for chat in chat_history %}
<div class="chat-message">
<div class="chat-timestamp">
{{ chat.timestamp }} ·
<span class="badge bg-primary" style="font-size:.65rem;">{{ chat.agent }}</span>
</div>
<div class="chat-prompt mt-1"><strong>Sie:</strong> {{ chat.user_prompt }}</div>
<div class="chat-response mt-1"><strong>Orchestrator:</strong> {{ chat.response }}</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);" id="emptyState">
<p style="font-size:2rem;margin-bottom:.5rem;">🤖</p>
<p>Noch keine Anfragen.<br>Der Orchestrator delegiert automatisch an den passenden Agenten.</p>
<hr>
<p style="font-size:.8rem;">
✓ Immer zuerst delegieren · ✓ Neuen Agenten erstellen falls nötig · ✓ Niemals selbst ausführen
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function sendPromptWithStream() {
const prompt = document.getElementById('prompt').value.trim();
const streamBtn = document.getElementById('streamBtn');
if (!prompt) { alert('Bitte Anfrage eingeben!'); return; }
streamBtn.disabled = true;
streamBtn.textContent = '⏳ Agent arbeitet…';
const chatContainer = document.getElementById('chatContainer');
const emptyState = document.getElementById('emptyState');
if (emptyState) emptyState.remove();
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message';
msgDiv.innerHTML = `
<div class="chat-timestamp">${new Date().toLocaleTimeString()} · <span class="badge bg-primary" style="font-size:.65rem;" id="agentBadge">wird ausgewählt…</span></div>
<div class="chat-prompt mt-1"><strong>Sie:</strong> ${prompt}</div>
<div class="chat-response mt-1" id="responseDiv"><strong>Orchestrator:</strong> <span id="responseText">⏳ Agent arbeitet…</span></div>
`;
chatContainer.insertBefore(msgDiv, chatContainer.firstChild);
fetch('/api/agent-stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function read() {
reader.read().then(({ done, value }) => {
if (done) {
streamBtn.disabled = false;
streamBtn.textContent = 'Live-Antwort anfordern';
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const event = JSON.parse(line.slice(6));
if (event.type === 'agent_selected') {
document.getElementById('agentBadge').textContent = event.agent;
} else if (event.type === 'response_chunk') {
const t = document.getElementById('responseText');
if (t.textContent.startsWith('⏳')) t.textContent = '';
t.textContent += event.text;
} else if (event.type === 'error') {
document.getElementById('responseText').textContent = '❌ ' + event.message;
}
} catch(e) {}
}
read();
});
}
read();
}).catch(err => {
document.getElementById('responseText').textContent = '❌ ' + err.message;
streamBtn.disabled = false;
streamBtn.textContent = 'Live-Antwort anfordern';
});
}
</script>
{% endblock %}

119
templates/settings.html Normal file
View file

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}Einstellungen{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1 class="page-title">Einstellungen</h1>
<p class="page-subtitle text-muted">Poller-Konfiguration &amp; System-Status</p>
</div>
<div class="row g-4">
<!-- Poller-Einstellungen -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<span class="me-2"></span> Email-Poller
</div>
<div class="card-body">
<form method="POST">
<div class="mb-4">
<label class="form-label fw-semibold">Poll-Intervall <span class="text-muted fw-normal">(Sekunden)</span></label>
<input type="number" class="form-control" name="poll_interval"
value="{{ poller_settings.poll_interval }}" min="10" max="3600" required>
<div class="form-text">Wie oft der Poller den IMAP-Eingang prüft. Aktuell: <strong>{{ poller_settings.poll_interval }}s</strong> ({{ (poller_settings.poll_interval / 60) | round(1) }} min)</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Failsafe-Fenster <span class="text-muted fw-normal">(Sekunden)</span></label>
<input type="number" class="form-control" name="failsafe_window"
value="{{ poller_settings.failsafe_window }}" min="30" max="86400" required>
<div class="form-text">
Wie lange ein Task laufen darf bevor er als hängend gilt und erneut verarbeitet wird.
Aktuell: <strong>{{ poller_settings.failsafe_window }}s</strong> ({{ (poller_settings.failsafe_window / 60) | round(1) }} min).
<span class="text-warning">Muss größer als das Poll-Intervall sein.</span>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Speichern</button>
<a href="/settings" class="btn btn-outline-secondary">Zurücksetzen</a>
</div>
</form>
</div>
</div>
</div>
<!-- Journal-Status -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<span class="me-2">📋</span> Email-Journal Status
</div>
<div class="card-body">
{% if journal_stats %}
<table class="table table-sm">
<thead>
<tr>
<th>Status</th>
<th class="text-end">Anzahl</th>
</tr>
</thead>
<tbody>
{% for status, count in journal_stats.items() %}
<tr>
<td>
{% if status == 'completed' %}
<span class="badge" style="background:var(--success)">✓ {{ status }}</span>
{% elif status == 'queued' %}
<span class="badge" style="background:var(--warning);color:#000">⏳ {{ status }}</span>
{% elif status == 'skipped' %}
<span class="badge" style="background:var(--text-muted)">— {{ status }}</span>
{% elif status == 'error' %}
<span class="badge" style="background:var(--danger)">✗ {{ status }}</span>
{% else %}
<span class="badge bg-secondary">{{ status }}</span>
{% endif %}
</td>
<td class="text-end fw-semibold">{{ count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">Noch keine Einträge im Journal.</p>
{% endif %}
<hr class="my-3" style="border-color:var(--border)">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Journal-Datenbank: <code>email_journal.db</code></small>
<form method="POST" action="/settings/journal-clear"
onsubmit="return confirm('Alle abgeschlossenen Journal-Einträge löschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger">Abgeschlossene löschen</button>
</form>
</div>
</div>
</div>
</div>
<!-- Whitelist -->
<div class="col-12">
<div class="card">
<div class="card-header">
<span class="me-2">🔒</span> Email-Whitelist
</div>
<div class="card-body">
<p class="text-muted mb-2">Nur Emails von diesen Absendern werden verarbeitet (aktuell hardcoded in <code>app.py</code>):</p>
<div class="d-flex flex-wrap gap-2">
{% set whitelist = ['eric.fischer@signtime.media', 'p.dyderski@live.at', 'georg.tschare@gmail.com', 'georg.tschare@signtime.media'] %}
{% for addr in whitelist %}
<code class="px-2 py-1 rounded" style="background:var(--bg-elevated);color:var(--accent-light)">{{ addr }}</code>
{% endfor %}
<code class="px-2 py-1 rounded" style="background:var(--bg-elevated);color:var(--accent2)">*@diversityball.at</code>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

118
templates/tasks.html Normal file
View file

@ -0,0 +1,118 @@
{% extends "base.html" %}
{% block title %}Tasks{% endblock %}
{% block content %}
<div class="page-header">
<h1>Task-Verwaltung</h1>
<p>Manuelle und automatische Email-Tasks</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-success">
<h5 class="mb-0">Neuen Task erstellen</h5>
</div>
<div class="card-body">
<form method="POST" action="/tasks">
<div class="mb-3">
<label for="title" class="form-label">Titel</label>
<input type="text" class="form-control" id="title" name="title" placeholder="Task-Titel" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Beschreibung</label>
<textarea class="form-control" id="description" name="description" rows="3"
placeholder="Optionale Beschreibung"></textarea>
</div>
<div class="mb-3">
<label for="assigned_agent" class="form-label">Agent (optional)</label>
<select class="form-select" id="assigned_agent" name="assigned_agent">
<option value="">— Agent wählen —</option>
{% for key, agent in agents.items() %}
<option value="{{ key }}">{{ agent.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-success w-100">Task erstellen</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-primary d-flex justify-content-between align-items-center">
<h5 class="mb-0">Alle Tasks</h5>
<span class="badge bg-secondary">{{ tasks|length }}</span>
</div>
<div class="card-body p-0">
{% if tasks %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Titel</th>
<th>Agent</th>
<th>Status</th>
<th>Erstellt</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td style="color:var(--text-muted);">{{ task.id }}</td>
<td>
<strong>{{ task.title }}</strong>
{% if task.type == 'email' %}
<span class="badge bg-info ms-1" title="Von: {{ task.reply_to }}">Email</span>
{% endif %}
{% if task.description %}
<div style="font-size:.75rem;color:var(--text-muted);">
{{ task.description[:60] }}{% if task.description|length > 60 %}…{% endif %}
</div>
{% endif %}
</td>
<td style="font-size:.8rem;">{{ task.assigned_agent }}</td>
<td>
{% if task.status == 'pending' %}
<span class="badge bg-warning">Pending</span>
{% elif task.status == 'in_progress' %}
<span class="badge bg-primary">In Progress</span>
{% elif task.status == 'completed' %}
<span class="badge bg-success">Done</span>
{% elif task.status == 'error' %}
<span class="badge bg-danger">Fehler</span>
{% else %}
<span class="badge bg-secondary">{{ task.status }}</span>
{% endif %}
</td>
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
<td>
{% if task.type == 'email' %}
<span style="color:var(--text-muted);font-size:.75rem;">Auto</span>
{% elif task.status == 'pending' %}
<a href="/tasks/update/{{ task.id }}/in_progress" class="btn btn-sm btn-primary">Start</a>
{% elif task.status == 'in_progress' %}
<a href="/tasks/update/{{ task.id }}/completed" class="btn btn-sm btn-success">Fertig</a>
{% else %}
<span style="color:var(--text-muted);"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📋</p>
<p>Noch keine Tasks. Erstellen Sie den ersten Task!</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}