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:
commit
56d9bc2c76
71 changed files with 5953 additions and 0 deletions
75
templates/agents.html
Normal file
75
templates/agents.html
Normal 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
102
templates/base.html
Normal 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 · Diversity-Ball Wien 2026 ·
|
||||
<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
70
templates/chat.html
Normal 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
90
templates/email_log.html
Normal 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 |
|
||||
<span class="status-skipped me-3">— skipped</span> — Nicht auf Whitelist |
|
||||
<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
123
templates/emails.html
Normal 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
262
templates/files.html
Normal 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
126
templates/index.html
Normal 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
157
templates/orchestrator.html
Normal 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
119
templates/settings.html
Normal 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 & 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
118
templates/tasks.html
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue