diff --git a/.env.example b/.env.example index 103ce52..0c233cc 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,8 @@ +# ───────────────────────────────────────────────────────────────────────────── +# App Login Password +# ───────────────────────────────────────────────────────────────────────────── +APP_PASSWORD=change-me + # Email Integration Configuration # Gmail Example: # 1. Enable 2-Factor Authentication on your Google Account diff --git a/agent_config.json b/agent_config.json index 5a2e7e4..bd8cf55 100644 --- a/agent_config.json +++ b/agent_config.json @@ -15,7 +15,7 @@ "model": "anthropic/claude-sonnet-4-6" }, "musik_rechte_advisor": { - "model": "anthropic/claude-opus-4-0" + "model": "anthropic/claude-opus-4-6" }, "zusammenfasser": { "model": "anthropic/claude-haiku-4-5" @@ -24,7 +24,7 @@ "model": "anthropic/claude-haiku-4-5" }, "negotiator": { - "model": "anthropic/claude-opus-4-0" + "model": "anthropic/claude-opus-4-6" }, "budget_manager": { "model": "anthropic/claude-haiku-4-5" diff --git a/app.py b/app.py index 34fcd21..2f9030d 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,8 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.header import decode_header 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 telegram import Update 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'], '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['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_CONFIG = { @@ -1221,10 +1234,6 @@ def load_agent_prompts(): return prompts 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: session['orchestrator_chat'] = [] @@ -2207,7 +2216,26 @@ start_task_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('/') +@login_required def index(): # Hole die 5 neuesten Tasks aus DB all_tasks = get_tasks() @@ -2215,6 +2243,7 @@ def index(): return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks) @app.route('/chat', methods=['GET', 'POST']) +@login_required def chat(): # Chat-Verlauf aus Session laden if 'chat_history' not in session: @@ -2225,6 +2254,7 @@ def chat(): @app.route('/chat/send', methods=['POST']) +@login_required def chat_send(): """Führt einen Agent aus und gibt die Antwort per Server-Sent Events LIVE zurück.""" data = request.get_json() @@ -2317,6 +2347,7 @@ Die Wissensdatenbank liegt unter: {kb_file} @app.route('/chat/save', methods=['POST']) +@login_required def chat_save(): """Speichert eine Chat-Nachricht in der Session.""" data = request.get_json() @@ -2337,6 +2368,7 @@ def chat_save(): return jsonify({'success': True}) @app.route('/tasks', methods=['GET', 'POST']) +@login_required def task_list(): if request.method == 'POST': title = request.form.get('title', '').strip() @@ -2358,6 +2390,7 @@ def task_list(): return render_template('tasks.html', agents=AGENTS, tasks=all_tasks) @app.route('/tasks/update//') +@login_required def update_task(task_id, status): # Update in Datenbank update_task_db(task_id, status=status) @@ -2371,6 +2404,7 @@ def update_task(task_id, status): return redirect(url_for('task_list')) @app.route('/api/agent-stream', methods=['POST']) +@login_required def agent_stream(): """Server-Sent Events Endpoint – echtes Streaming direkt aus opencode JSON-Output.""" data = request.get_json() @@ -2444,6 +2478,7 @@ def agent_stream(): @app.route('/orchestrator', methods=['GET', 'POST']) +@login_required def orchestrator(): init_orchestrator_session() @@ -2451,8 +2486,8 @@ def orchestrator(): prompt = request.form.get('prompt', '').strip() if prompt: - kb = session.get('orchestrator_kb', '') - agent_prompts = session.get('orchestrator_prompts', {}) + kb = load_knowledge_base() + agent_prompts = load_agent_prompts() selected_agent = delegate_to_agent(prompt) agent_info = AGENTS.get(selected_agent, {}) agent_name = agent_info.get('name', selected_agent) @@ -2474,7 +2509,7 @@ def orchestrator(): return render_template('orchestrator.html', agents=AGENTS, chat_history=chat_display, - knowledge_loaded=bool(session.get('orchestrator_kb'))) + knowledge_loaded=bool(load_knowledge_base())) @app.route('/orchestrator/clear', methods=['POST']) def orchestrator_clear(): @@ -2484,6 +2519,7 @@ def orchestrator_clear(): return jsonify({'success': True}) @app.route('/agents', methods=['GET', 'POST']) +@login_required def agents(): agents_dir = os.path.join(os.path.dirname(__file__), 'agents') agents_list = [] @@ -2581,6 +2617,7 @@ def agents(): @app.route('/files', methods=['GET', 'POST']) +@login_required def files(): if request.method == 'POST': if 'file' not in request.files: @@ -2613,6 +2650,7 @@ def files(): @app.route('/files/delete/') +@login_required def delete_file(filename): filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) if os.path.exists(filepath): @@ -2622,12 +2660,14 @@ def delete_file(filename): @app.route('/files/download/') +@login_required def download_file(filename): """Liefert eine hochgeladene Datei zum Download/Anzeige.""" return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False) @app.route('/files/agent//') +@login_required def download_agent_file(agent_key, filename): """Liefert eine Datei aus dem Work-Ordner eines Agenten.""" if agent_key not in AGENTS: @@ -2650,6 +2690,7 @@ def download_agent_file(agent_key, filename): @app.route('/files/agent//view/') +@login_required def view_agent_file(agent_key, filename): """Gibt Inhalt einer Agent-Datei als JSON zurück.""" if agent_key not in AGENTS: @@ -2678,6 +2719,7 @@ def view_agent_file(agent_key, filename): @app.route('/files/agent//delete/') +@login_required def delete_agent_file(agent_key, filename): """Löscht eine Datei aus dem Work-Ordner eines Agenten.""" if agent_key not in AGENTS: @@ -2707,6 +2749,7 @@ def delete_agent_file(agent_key, filename): @app.route('/files/email/view/') +@login_required def view_email_file(filename): """Gibt Inhalt einer Email-Vorlage als JSON oder direkten Text zurück.""" email_dir = os.path.join(os.path.dirname(__file__), 'emails') @@ -2727,6 +2770,7 @@ def view_email_file(filename): @app.route('/files/email/save/', methods=['POST']) +@login_required def save_email_file(filename): """Speichert den Inhalt einer Email-Vorlage (JSON POST).""" email_dir = os.path.join(os.path.dirname(__file__), 'emails') @@ -2744,6 +2788,7 @@ def save_email_file(filename): @app.route('/files/email/delete/') +@login_required def delete_email_file(filename): """Löscht eine Email-Vorlage.""" email_dir = os.path.join(os.path.dirname(__file__), 'emails') @@ -2760,6 +2805,7 @@ def delete_email_file(filename): @app.route('/files/project/view/') +@login_required def view_project_file(filename): """Gibt Inhalt einer Projektdatei als JSON zurück.""" base_dir = os.path.dirname(__file__) @@ -2785,6 +2831,7 @@ def view_project_file(filename): @app.route('/files/project/') +@login_required def download_project_file(filename): """Liefert eine Projektdatei zum Download.""" base_dir = os.path.dirname(__file__) @@ -2807,6 +2854,7 @@ def download_project_file(filename): @app.route('/files/project/delete/') +@login_required def delete_project_file(filename): """Löscht eine Projektdatei.""" base_dir = os.path.dirname(__file__) @@ -2836,6 +2884,7 @@ def delete_project_file(filename): @app.route('/emails', methods=['GET', 'POST']) +@login_required def emails(): """Email Management Interface""" if request.method == 'POST': @@ -2865,6 +2914,7 @@ def emails(): @app.route('/emails/') +@login_required def view_email(email_id): """View single email content""" if not (EMAIL_CONFIG['email_address'] and EMAIL_CONFIG['email_password']): @@ -2875,6 +2925,7 @@ def view_email(email_id): @app.route('/email-log') +@login_required def email_log_view(): """Zeigt das Email-Verarbeitungs-Log.""" with email_log_lock: @@ -2883,6 +2934,7 @@ def email_log_view(): @app.route('/settings', methods=['GET', 'POST']) +@login_required def settings(): """App-Einstellungen & Poller-Einstellungen zur Laufzeit ändern.""" if request.method == 'POST': @@ -2936,6 +2988,7 @@ def settings(): @app.route('/settings/journal-clear', methods=['POST']) +@login_required def journal_clear(): """Löscht abgeschlossene Journal-Einträge (completed, skipped, error).""" con = sqlite3.connect(EMAIL_JOURNAL_DB) @@ -2949,6 +3002,7 @@ def journal_clear(): @app.route('/team') +@login_required def team(): """Zeigt alle Team-Members an.""" team_members = get_team_members(active_only=False) @@ -2956,6 +3010,7 @@ def team(): @app.route('/team/add', methods=['POST']) +@login_required def team_add(): """Fügt ein neues Team-Mitglied hinzu.""" name = request.form.get('name', '').strip() @@ -2988,6 +3043,7 @@ def team_add(): @app.route('/team//activate', methods=['POST']) +@login_required def team_activate(member_id): """Aktiviert ein Team-Mitglied.""" con = sqlite3.connect(EMAIL_JOURNAL_DB) @@ -3000,6 +3056,7 @@ def team_activate(member_id): @app.route('/team//deactivate', methods=['POST']) +@login_required def team_deactivate(member_id): """Deaktiviert ein Team-Mitglied.""" con = sqlite3.connect(EMAIL_JOURNAL_DB) @@ -3012,6 +3069,7 @@ def team_deactivate(member_id): @app.route('/team/edit', methods=['POST']) +@login_required def team_edit(): """Bearbeitet ein Team-Mitglied.""" member_id = request.form.get('member_id') @@ -3051,6 +3109,7 @@ def team_edit(): @app.route('/api/telegram-qr') +@login_required def telegram_qr(): """Generiert QR-Code für Telegram Bot.""" if not TELEGRAM_CONFIG['bot_token'] or not TELEGRAM_CONFIG['bot_username']: @@ -3076,6 +3135,7 @@ def telegram_qr(): # ── Task API ──────────────────────────────────────────────────────────────── @app.route('/api/tasks', methods=['GET', 'POST']) +@login_required def api_tasks(): """API zum Erstellen und Abrufen von Tasks.""" if request.method == 'POST': @@ -3121,6 +3181,7 @@ def api_tasks(): @app.route('/api/tasks/', methods=['PUT']) +@login_required def update_task_api(task_id): """API zum Aktualisieren eines Tasks.""" data = request.get_json() @@ -3140,6 +3201,7 @@ def update_task_api(task_id): @app.route('/api/models', methods=['GET']) +@login_required def get_models(): """Gibt die Liste der verfügbaren KI-Modelle zurück.""" force_refresh = request.args.get('refresh', 'false').lower() == 'true' @@ -3148,6 +3210,7 @@ def get_models(): @app.route('/api/agent//model', methods=['POST']) +@login_required def set_agent_model(agent_name): """Setzt das Modell für einen Agenten.""" data = request.get_json() @@ -3158,6 +3221,7 @@ def set_agent_model(agent_name): @app.route('/api/agent//delete', methods=['DELETE']) +@login_required def delete_agent(agent_name): """Löscht einen Agenten (den gesamten Ordner).""" import shutil @@ -3188,6 +3252,7 @@ def delete_agent(agent_name): @app.route('/api/agent//reminders', methods=['GET', 'POST']) +@login_required def agent_reminders(agent_name): agents_dir = os.path.join(os.path.dirname(__file__), 'agents') 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']) +@login_required def distribute_tasks(): """Erstellt einen Planungs-Task für den Orchestrator - dieser weist dann die richtigen Agenten zu.""" data = request.get_json() @@ -3311,4 +3377,4 @@ if __name__ == '__main__': start_telegram_thread() # 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) diff --git a/templates/base.html b/templates/base.html index 3abf8e7..8c7a553 100644 --- a/templates/base.html +++ b/templates/base.html @@ -70,6 +70,11 @@ Settings + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..9b6564d --- /dev/null +++ b/templates/login.html @@ -0,0 +1,36 @@ + + + + + + Login · {{ app_name or 'Frankenbot' }} + + + + + + + + +
+
+
+
+ smart_toy +

{{ app_name or 'Frankenbot' }}

+

Agent Orchestration System

+
+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+ +
+ +
+
+
+
+ +