feat: Add password login system and upgrade agent models

- App-level password auth via Flask session (APP_PASSWORD in .env)
- login_required decorator on all routes
- Login page, logout button in navbar, 7-day session lifetime
- Upgrade musik_rechte_advisor and negotiator from Opus 4.0 to Opus 4.6
- Fix orchestrator session cookie overflow (kb/prompts no longer stored in session)
- Change app port from 5000 to 5050 (5000 occupied by Zou/Kitsu)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
eric 2026-02-21 17:26:10 +00:00
parent 7ee66397e1
commit 83b1842392
5 changed files with 124 additions and 12 deletions

View file

@ -1,3 +1,8 @@
# ─────────────────────────────────────────────────────────────────────────────
# App Login Password
# ─────────────────────────────────────────────────────────────────────────────
APP_PASSWORD=change-me
# Email Integration Configuration # Email Integration Configuration
# Gmail Example: # Gmail Example:
# 1. Enable 2-Factor Authentication on your Google Account # 1. Enable 2-Factor Authentication on your Google Account

View file

@ -15,7 +15,7 @@
"model": "anthropic/claude-sonnet-4-6" "model": "anthropic/claude-sonnet-4-6"
}, },
"musik_rechte_advisor": { "musik_rechte_advisor": {
"model": "anthropic/claude-opus-4-0" "model": "anthropic/claude-opus-4-6"
}, },
"zusammenfasser": { "zusammenfasser": {
"model": "anthropic/claude-haiku-4-5" "model": "anthropic/claude-haiku-4-5"
@ -24,7 +24,7 @@
"model": "anthropic/claude-haiku-4-5" "model": "anthropic/claude-haiku-4-5"
}, },
"negotiator": { "negotiator": {
"model": "anthropic/claude-opus-4-0" "model": "anthropic/claude-opus-4-6"
}, },
"budget_manager": { "budget_manager": {
"model": "anthropic/claude-haiku-4-5" "model": "anthropic/claude-haiku-4-5"

86
app.py
View file

@ -15,7 +15,8 @@ from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.header import decode_header from email.header import decode_header
from flask import Flask, render_template, request, redirect, url_for, session, flash, Response, send_from_directory, jsonify from flask import Flask, render_template, request, redirect, url_for, session, flash, Response, send_from_directory, jsonify
from datetime import datetime from datetime import datetime, timedelta
from functools import wraps
from dotenv import load_dotenv from dotenv import load_dotenv
from telegram import Update from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
@ -397,9 +398,21 @@ AGENT_KEYWORDS = {
'musik_rechte_advisor': ['musik', 'akm', 'gema', 'lizenz', 'rechte', 'urheber', 'copyright', 'verwertungsgesellschaft'], 'musik_rechte_advisor': ['musik', 'akm', 'gema', 'lizenz', 'rechte', 'urheber', 'copyright', 'verwertungsgesellschaft'],
'document_editor': ['dokument', 'vertrag', 'brief', 'text', 'bearbeiten', 'erstellen', 'schreiben'], 'document_editor': ['dokument', 'vertrag', 'brief', 'text', 'bearbeiten', 'erstellen', 'schreiben'],
} }
app.secret_key = 'agent-orchestration-secret-key-2026' app.secret_key = os.getenv('SECRET_KEY', 'agent-orchestration-secret-key-2026')
app.config['UPLOAD_FOLDER'] = 'uploads' app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
app.permanent_session_lifetime = timedelta(days=7)
# ── App Password ─────────────────────────────────────────────────────────────
APP_PASSWORD = os.getenv('APP_PASSWORD', '')
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('authenticated'):
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# Email Configuration - loaded AFTER load_dotenv() # Email Configuration - loaded AFTER load_dotenv()
EMAIL_CONFIG = { EMAIL_CONFIG = {
@ -1221,10 +1234,6 @@ def load_agent_prompts():
return prompts return prompts
def init_orchestrator_session(): def init_orchestrator_session():
if 'orchestrator_kb' not in session:
session['orchestrator_kb'] = load_knowledge_base()
if 'orchestrator_prompts' not in session:
session['orchestrator_prompts'] = load_agent_prompts()
if 'orchestrator_chat' not in session: if 'orchestrator_chat' not in session:
session['orchestrator_chat'] = [] session['orchestrator_chat'] = []
@ -2207,7 +2216,26 @@ start_task_beat()
start_orchestrator_beat() start_orchestrator_beat()
@app.route('/login', methods=['GET', 'POST'])
def login():
if session.get('authenticated'):
return redirect(url_for('index'))
error = None
if request.method == 'POST':
if request.form.get('password') == APP_PASSWORD:
session['authenticated'] = True
session.permanent = True
return redirect(url_for('index'))
error = 'Falsches Passwort'
return render_template('login.html', error=error)
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('login'))
@app.route('/') @app.route('/')
@login_required
def index(): def index():
# Hole die 5 neuesten Tasks aus DB # Hole die 5 neuesten Tasks aus DB
all_tasks = get_tasks() all_tasks = get_tasks()
@ -2215,6 +2243,7 @@ def index():
return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks) return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks)
@app.route('/chat', methods=['GET', 'POST']) @app.route('/chat', methods=['GET', 'POST'])
@login_required
def chat(): def chat():
# Chat-Verlauf aus Session laden # Chat-Verlauf aus Session laden
if 'chat_history' not in session: if 'chat_history' not in session:
@ -2225,6 +2254,7 @@ def chat():
@app.route('/chat/send', methods=['POST']) @app.route('/chat/send', methods=['POST'])
@login_required
def chat_send(): def chat_send():
"""Führt einen Agent aus und gibt die Antwort per Server-Sent Events LIVE zurück.""" """Führt einen Agent aus und gibt die Antwort per Server-Sent Events LIVE zurück."""
data = request.get_json() data = request.get_json()
@ -2317,6 +2347,7 @@ Die Wissensdatenbank liegt unter: {kb_file}
@app.route('/chat/save', methods=['POST']) @app.route('/chat/save', methods=['POST'])
@login_required
def chat_save(): def chat_save():
"""Speichert eine Chat-Nachricht in der Session.""" """Speichert eine Chat-Nachricht in der Session."""
data = request.get_json() data = request.get_json()
@ -2337,6 +2368,7 @@ def chat_save():
return jsonify({'success': True}) return jsonify({'success': True})
@app.route('/tasks', methods=['GET', 'POST']) @app.route('/tasks', methods=['GET', 'POST'])
@login_required
def task_list(): def task_list():
if request.method == 'POST': if request.method == 'POST':
title = request.form.get('title', '').strip() title = request.form.get('title', '').strip()
@ -2358,6 +2390,7 @@ def task_list():
return render_template('tasks.html', agents=AGENTS, tasks=all_tasks) return render_template('tasks.html', agents=AGENTS, tasks=all_tasks)
@app.route('/tasks/update/<int:task_id>/<status>') @app.route('/tasks/update/<int:task_id>/<status>')
@login_required
def update_task(task_id, status): def update_task(task_id, status):
# Update in Datenbank # Update in Datenbank
update_task_db(task_id, status=status) update_task_db(task_id, status=status)
@ -2371,6 +2404,7 @@ def update_task(task_id, status):
return redirect(url_for('task_list')) return redirect(url_for('task_list'))
@app.route('/api/agent-stream', methods=['POST']) @app.route('/api/agent-stream', methods=['POST'])
@login_required
def agent_stream(): def agent_stream():
"""Server-Sent Events Endpoint echtes Streaming direkt aus opencode JSON-Output.""" """Server-Sent Events Endpoint echtes Streaming direkt aus opencode JSON-Output."""
data = request.get_json() data = request.get_json()
@ -2444,6 +2478,7 @@ def agent_stream():
@app.route('/orchestrator', methods=['GET', 'POST']) @app.route('/orchestrator', methods=['GET', 'POST'])
@login_required
def orchestrator(): def orchestrator():
init_orchestrator_session() init_orchestrator_session()
@ -2451,8 +2486,8 @@ def orchestrator():
prompt = request.form.get('prompt', '').strip() prompt = request.form.get('prompt', '').strip()
if prompt: if prompt:
kb = session.get('orchestrator_kb', '') kb = load_knowledge_base()
agent_prompts = session.get('orchestrator_prompts', {}) agent_prompts = load_agent_prompts()
selected_agent = delegate_to_agent(prompt) selected_agent = delegate_to_agent(prompt)
agent_info = AGENTS.get(selected_agent, {}) agent_info = AGENTS.get(selected_agent, {})
agent_name = agent_info.get('name', selected_agent) agent_name = agent_info.get('name', selected_agent)
@ -2474,7 +2509,7 @@ def orchestrator():
return render_template('orchestrator.html', return render_template('orchestrator.html',
agents=AGENTS, agents=AGENTS,
chat_history=chat_display, chat_history=chat_display,
knowledge_loaded=bool(session.get('orchestrator_kb'))) knowledge_loaded=bool(load_knowledge_base()))
@app.route('/orchestrator/clear', methods=['POST']) @app.route('/orchestrator/clear', methods=['POST'])
def orchestrator_clear(): def orchestrator_clear():
@ -2484,6 +2519,7 @@ def orchestrator_clear():
return jsonify({'success': True}) return jsonify({'success': True})
@app.route('/agents', methods=['GET', 'POST']) @app.route('/agents', methods=['GET', 'POST'])
@login_required
def agents(): def agents():
agents_dir = os.path.join(os.path.dirname(__file__), 'agents') agents_dir = os.path.join(os.path.dirname(__file__), 'agents')
agents_list = [] agents_list = []
@ -2581,6 +2617,7 @@ def agents():
@app.route('/files', methods=['GET', 'POST']) @app.route('/files', methods=['GET', 'POST'])
@login_required
def files(): def files():
if request.method == 'POST': if request.method == 'POST':
if 'file' not in request.files: if 'file' not in request.files:
@ -2613,6 +2650,7 @@ def files():
@app.route('/files/delete/<filename>') @app.route('/files/delete/<filename>')
@login_required
def delete_file(filename): def delete_file(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(filepath): if os.path.exists(filepath):
@ -2622,12 +2660,14 @@ def delete_file(filename):
@app.route('/files/download/<filename>') @app.route('/files/download/<filename>')
@login_required
def download_file(filename): def download_file(filename):
"""Liefert eine hochgeladene Datei zum Download/Anzeige.""" """Liefert eine hochgeladene Datei zum Download/Anzeige."""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False) return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False)
@app.route('/files/agent/<agent_key>/<filename>') @app.route('/files/agent/<agent_key>/<filename>')
@login_required
def download_agent_file(agent_key, filename): def download_agent_file(agent_key, filename):
"""Liefert eine Datei aus dem Work-Ordner eines Agenten.""" """Liefert eine Datei aus dem Work-Ordner eines Agenten."""
if agent_key not in AGENTS: if agent_key not in AGENTS:
@ -2650,6 +2690,7 @@ def download_agent_file(agent_key, filename):
@app.route('/files/agent/<agent_key>/view/<filename>') @app.route('/files/agent/<agent_key>/view/<filename>')
@login_required
def view_agent_file(agent_key, filename): def view_agent_file(agent_key, filename):
"""Gibt Inhalt einer Agent-Datei als JSON zurück.""" """Gibt Inhalt einer Agent-Datei als JSON zurück."""
if agent_key not in AGENTS: if agent_key not in AGENTS:
@ -2678,6 +2719,7 @@ def view_agent_file(agent_key, filename):
@app.route('/files/agent/<agent_key>/delete/<filename>') @app.route('/files/agent/<agent_key>/delete/<filename>')
@login_required
def delete_agent_file(agent_key, filename): def delete_agent_file(agent_key, filename):
"""Löscht eine Datei aus dem Work-Ordner eines Agenten.""" """Löscht eine Datei aus dem Work-Ordner eines Agenten."""
if agent_key not in AGENTS: if agent_key not in AGENTS:
@ -2707,6 +2749,7 @@ def delete_agent_file(agent_key, filename):
@app.route('/files/email/view/<filename>') @app.route('/files/email/view/<filename>')
@login_required
def view_email_file(filename): def view_email_file(filename):
"""Gibt Inhalt einer Email-Vorlage als JSON oder direkten Text zurück.""" """Gibt Inhalt einer Email-Vorlage als JSON oder direkten Text zurück."""
email_dir = os.path.join(os.path.dirname(__file__), 'emails') email_dir = os.path.join(os.path.dirname(__file__), 'emails')
@ -2727,6 +2770,7 @@ def view_email_file(filename):
@app.route('/files/email/save/<filename>', methods=['POST']) @app.route('/files/email/save/<filename>', methods=['POST'])
@login_required
def save_email_file(filename): def save_email_file(filename):
"""Speichert den Inhalt einer Email-Vorlage (JSON POST).""" """Speichert den Inhalt einer Email-Vorlage (JSON POST)."""
email_dir = os.path.join(os.path.dirname(__file__), 'emails') email_dir = os.path.join(os.path.dirname(__file__), 'emails')
@ -2744,6 +2788,7 @@ def save_email_file(filename):
@app.route('/files/email/delete/<filename>') @app.route('/files/email/delete/<filename>')
@login_required
def delete_email_file(filename): def delete_email_file(filename):
"""Löscht eine Email-Vorlage.""" """Löscht eine Email-Vorlage."""
email_dir = os.path.join(os.path.dirname(__file__), 'emails') email_dir = os.path.join(os.path.dirname(__file__), 'emails')
@ -2760,6 +2805,7 @@ def delete_email_file(filename):
@app.route('/files/project/view/<filename>') @app.route('/files/project/view/<filename>')
@login_required
def view_project_file(filename): def view_project_file(filename):
"""Gibt Inhalt einer Projektdatei als JSON zurück.""" """Gibt Inhalt einer Projektdatei als JSON zurück."""
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
@ -2785,6 +2831,7 @@ def view_project_file(filename):
@app.route('/files/project/<filename>') @app.route('/files/project/<filename>')
@login_required
def download_project_file(filename): def download_project_file(filename):
"""Liefert eine Projektdatei zum Download.""" """Liefert eine Projektdatei zum Download."""
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
@ -2807,6 +2854,7 @@ def download_project_file(filename):
@app.route('/files/project/delete/<filename>') @app.route('/files/project/delete/<filename>')
@login_required
def delete_project_file(filename): def delete_project_file(filename):
"""Löscht eine Projektdatei.""" """Löscht eine Projektdatei."""
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
@ -2836,6 +2884,7 @@ def delete_project_file(filename):
@app.route('/emails', methods=['GET', 'POST']) @app.route('/emails', methods=['GET', 'POST'])
@login_required
def emails(): def emails():
"""Email Management Interface""" """Email Management Interface"""
if request.method == 'POST': if request.method == 'POST':
@ -2865,6 +2914,7 @@ def emails():
@app.route('/emails/<email_id>') @app.route('/emails/<email_id>')
@login_required
def view_email(email_id): def view_email(email_id):
"""View single email content""" """View single email content"""
if not (EMAIL_CONFIG['email_address'] and EMAIL_CONFIG['email_password']): if not (EMAIL_CONFIG['email_address'] and EMAIL_CONFIG['email_password']):
@ -2875,6 +2925,7 @@ def view_email(email_id):
@app.route('/email-log') @app.route('/email-log')
@login_required
def email_log_view(): def email_log_view():
"""Zeigt das Email-Verarbeitungs-Log.""" """Zeigt das Email-Verarbeitungs-Log."""
with email_log_lock: with email_log_lock:
@ -2883,6 +2934,7 @@ def email_log_view():
@app.route('/settings', methods=['GET', 'POST']) @app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings(): def settings():
"""App-Einstellungen & Poller-Einstellungen zur Laufzeit ändern.""" """App-Einstellungen & Poller-Einstellungen zur Laufzeit ändern."""
if request.method == 'POST': if request.method == 'POST':
@ -2936,6 +2988,7 @@ def settings():
@app.route('/settings/journal-clear', methods=['POST']) @app.route('/settings/journal-clear', methods=['POST'])
@login_required
def journal_clear(): def journal_clear():
"""Löscht abgeschlossene Journal-Einträge (completed, skipped, error).""" """Löscht abgeschlossene Journal-Einträge (completed, skipped, error)."""
con = sqlite3.connect(EMAIL_JOURNAL_DB) con = sqlite3.connect(EMAIL_JOURNAL_DB)
@ -2949,6 +3002,7 @@ def journal_clear():
@app.route('/team') @app.route('/team')
@login_required
def team(): def team():
"""Zeigt alle Team-Members an.""" """Zeigt alle Team-Members an."""
team_members = get_team_members(active_only=False) team_members = get_team_members(active_only=False)
@ -2956,6 +3010,7 @@ def team():
@app.route('/team/add', methods=['POST']) @app.route('/team/add', methods=['POST'])
@login_required
def team_add(): def team_add():
"""Fügt ein neues Team-Mitglied hinzu.""" """Fügt ein neues Team-Mitglied hinzu."""
name = request.form.get('name', '').strip() name = request.form.get('name', '').strip()
@ -2988,6 +3043,7 @@ def team_add():
@app.route('/team/<int:member_id>/activate', methods=['POST']) @app.route('/team/<int:member_id>/activate', methods=['POST'])
@login_required
def team_activate(member_id): def team_activate(member_id):
"""Aktiviert ein Team-Mitglied.""" """Aktiviert ein Team-Mitglied."""
con = sqlite3.connect(EMAIL_JOURNAL_DB) con = sqlite3.connect(EMAIL_JOURNAL_DB)
@ -3000,6 +3056,7 @@ def team_activate(member_id):
@app.route('/team/<int:member_id>/deactivate', methods=['POST']) @app.route('/team/<int:member_id>/deactivate', methods=['POST'])
@login_required
def team_deactivate(member_id): def team_deactivate(member_id):
"""Deaktiviert ein Team-Mitglied.""" """Deaktiviert ein Team-Mitglied."""
con = sqlite3.connect(EMAIL_JOURNAL_DB) con = sqlite3.connect(EMAIL_JOURNAL_DB)
@ -3012,6 +3069,7 @@ def team_deactivate(member_id):
@app.route('/team/edit', methods=['POST']) @app.route('/team/edit', methods=['POST'])
@login_required
def team_edit(): def team_edit():
"""Bearbeitet ein Team-Mitglied.""" """Bearbeitet ein Team-Mitglied."""
member_id = request.form.get('member_id') member_id = request.form.get('member_id')
@ -3051,6 +3109,7 @@ def team_edit():
@app.route('/api/telegram-qr') @app.route('/api/telegram-qr')
@login_required
def telegram_qr(): def telegram_qr():
"""Generiert QR-Code für Telegram Bot.""" """Generiert QR-Code für Telegram Bot."""
if not TELEGRAM_CONFIG['bot_token'] or not TELEGRAM_CONFIG['bot_username']: if not TELEGRAM_CONFIG['bot_token'] or not TELEGRAM_CONFIG['bot_username']:
@ -3076,6 +3135,7 @@ def telegram_qr():
# ── Task API ──────────────────────────────────────────────────────────────── # ── Task API ────────────────────────────────────────────────────────────────
@app.route('/api/tasks', methods=['GET', 'POST']) @app.route('/api/tasks', methods=['GET', 'POST'])
@login_required
def api_tasks(): def api_tasks():
"""API zum Erstellen und Abrufen von Tasks.""" """API zum Erstellen und Abrufen von Tasks."""
if request.method == 'POST': if request.method == 'POST':
@ -3121,6 +3181,7 @@ def api_tasks():
@app.route('/api/tasks/<int:task_id>', methods=['PUT']) @app.route('/api/tasks/<int:task_id>', methods=['PUT'])
@login_required
def update_task_api(task_id): def update_task_api(task_id):
"""API zum Aktualisieren eines Tasks.""" """API zum Aktualisieren eines Tasks."""
data = request.get_json() data = request.get_json()
@ -3140,6 +3201,7 @@ def update_task_api(task_id):
@app.route('/api/models', methods=['GET']) @app.route('/api/models', methods=['GET'])
@login_required
def get_models(): def get_models():
"""Gibt die Liste der verfügbaren KI-Modelle zurück.""" """Gibt die Liste der verfügbaren KI-Modelle zurück."""
force_refresh = request.args.get('refresh', 'false').lower() == 'true' force_refresh = request.args.get('refresh', 'false').lower() == 'true'
@ -3148,6 +3210,7 @@ def get_models():
@app.route('/api/agent/<agent_name>/model', methods=['POST']) @app.route('/api/agent/<agent_name>/model', methods=['POST'])
@login_required
def set_agent_model(agent_name): def set_agent_model(agent_name):
"""Setzt das Modell für einen Agenten.""" """Setzt das Modell für einen Agenten."""
data = request.get_json() data = request.get_json()
@ -3158,6 +3221,7 @@ def set_agent_model(agent_name):
@app.route('/api/agent/<agent_name>/delete', methods=['DELETE']) @app.route('/api/agent/<agent_name>/delete', methods=['DELETE'])
@login_required
def delete_agent(agent_name): def delete_agent(agent_name):
"""Löscht einen Agenten (den gesamten Ordner).""" """Löscht einen Agenten (den gesamten Ordner)."""
import shutil import shutil
@ -3188,6 +3252,7 @@ def delete_agent(agent_name):
@app.route('/api/agent/<agent_name>/reminders', methods=['GET', 'POST']) @app.route('/api/agent/<agent_name>/reminders', methods=['GET', 'POST'])
@login_required
def agent_reminders(agent_name): def agent_reminders(agent_name):
agents_dir = os.path.join(os.path.dirname(__file__), 'agents') agents_dir = os.path.join(os.path.dirname(__file__), 'agents')
agent_path = os.path.join(agents_dir, agent_name) agent_path = os.path.join(agents_dir, agent_name)
@ -3214,6 +3279,7 @@ def agent_reminders(agent_name):
@app.route('/api/orchestrator-distribute', methods=['POST']) @app.route('/api/orchestrator-distribute', methods=['POST'])
@login_required
def distribute_tasks(): def distribute_tasks():
"""Erstellt einen Planungs-Task für den Orchestrator - dieser weist dann die richtigen Agenten zu.""" """Erstellt einen Planungs-Task für den Orchestrator - dieser weist dann die richtigen Agenten zu."""
data = request.get_json() data = request.get_json()
@ -3311,4 +3377,4 @@ if __name__ == '__main__':
start_telegram_thread() start_telegram_thread()
# Flask App starten # Flask App starten
app.run(debug=False, host='0.0.0.0', port=5000, threaded=True) app.run(debug=False, host='0.0.0.0', port=5050, threaded=True)

View file

@ -70,6 +70,11 @@
<span></span> Settings <span></span> Settings
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/logout">
<span>🚪</span> Logout
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

36
templates/login.html Normal file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="de" data-theme="{{ theme or 'dark' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login · {{ app_name or '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 href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container d-flex align-items-center justify-content-center" style="min-height: 100vh;">
<div class="card" style="max-width: 400px; width: 100%;">
<div class="card-body p-4">
<div class="text-center mb-4">
<span class="material-icons" style="font-size: 3rem; color: var(--accent);">smart_toy</span>
<h3 class="mt-2" style="color: var(--text-primary);">{{ app_name or 'Frankenbot' }}</h3>
<p class="text-muted">Agent Orchestration System</p>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<form method="POST">
<div class="mb-3">
<input type="password" class="form-control" name="password" placeholder="Passwort" autofocus required>
</div>
<button type="submit" class="btn btn-primary w-100">Anmelden</button>
</form>
</div>
</div>
</div>
</body>
</html>