diff --git a/AstraOS/Frontend/HTML/dashboard.html b/AstraOS/Frontend/HTML/dashboard.html index 1cf8eb1..5522252 100644 --- a/AstraOS/Frontend/HTML/dashboard.html +++ b/AstraOS/Frontend/HTML/dashboard.html @@ -12,6 +12,20 @@ box-shadow: 0 0 20px rgba(192,132,252,0.3); transform: translateY(-4px); } +/* Schriftgröße für Systemstatus-Text im Wartungsmodus noch kleiner und mittig */ +#system-status-text.small { + font-size: 2rem; + text-align: center; + margin: 0 auto; + display: block; +} +#system-status-desc .wartung-desc { + font-size: 0.75rem; + display: block; + text-align: center; + margin: 0 auto; + font-weight: 500; +} {% endblock %} @@ -62,16 +76,30 @@ -
+

Systemstatus

{% set status = stats.system_status.status %} -

{{ status }}

-

Alle Systeme funktionsfähig

+

+ {{ status }} +

+

+ {% if 'wartung' in status.lower() or 'systeme im wartungsmodus' in status.lower() %} + Systeme im Wartungsmodus + {% elif 'online' in status.lower() %} + Alle Systeme funktionsfähig + {% else %} + Teilweise oder komplett offline + {% endif %} +

+ {% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">

Geplante Wartung

{% set maint = stats.scheduled_maintenance %} {% if maint and maint.enabled %} @@ -165,5 +193,26 @@ function updateServerLoad() { } setInterval(updateServerLoad, 5000); updateServerLoad(); + +// Systemstatus dynamisch Randfarbe anpassen +function updateSystemStatusCard() { + const card = document.getElementById('system-status-card'); + const statusText = document.getElementById('system-status-text'); + if (!card || !statusText) return; + card.classList.remove('border-green-500/40', 'border-yellow-500/40', 'border-red-500/40'); + statusText.classList.remove('small'); + const status = statusText.textContent.toLowerCase(); + if (status.includes('wartung') || status.includes('systeme im wartungsmodus')) { + card.classList.add('border-yellow-500/40'); + statusText.classList.add('small'); + } else if (status.includes('online')) { + card.classList.add('border-green-500/40'); + } else { + card.classList.add('border-red-500/40'); + } +} +setTimeout(updateSystemStatusCard, 100); // initial nach Render +// Optional: falls Systemstatus dynamisch per AJAX kommt, hier regelmäßig prüfen +setInterval(updateSystemStatusCard, 2000); {% endblock %} diff --git a/AstraOS/Frontend/HTML/login.html b/AstraOS/Frontend/HTML/login.html index 65fdbcf..7ef1865 100644 --- a/AstraOS/Frontend/HTML/login.html +++ b/AstraOS/Frontend/HTML/login.html @@ -4,6 +4,7 @@ AstraOS — Login + diff --git a/AstraOS/Frontend/HTML/maintenance_management.html b/AstraOS/Frontend/HTML/maintenance_management.html index 0a0d0c1..106cfe2 100644 --- a/AstraOS/Frontend/HTML/maintenance_management.html +++ b/AstraOS/Frontend/HTML/maintenance_management.html @@ -40,6 +40,18 @@
+ +
+
+

Discord Bots Wartungsmodus

+

Setzt alle Discord Bots in den Wartungsmodus (Status wird auf der Status-Seite angezeigt).

+
+ +
+
diff --git a/AstraOS/Frontend/HTML/sidebar.html b/AstraOS/Frontend/HTML/sidebar.html index 32793b8..933964f 100644 --- a/AstraOS/Frontend/HTML/sidebar.html +++ b/AstraOS/Frontend/HTML/sidebar.html @@ -4,6 +4,7 @@ {% block title %}AstraOS{% endblock %} + @@ -15,7 +16,6 @@ overflow-x: hidden; min-height: 100vh; } - /* Custom Checkbox */ .custom-checkbox-label { display: flex; align-items: center; @@ -28,7 +28,7 @@ .custom-checkbox-box { width: 20px; height: 20px; - border: 2px solid #4a5568; /* gray-600 */ + border: 2px solid #4a5568; border-radius: 4px; display: flex; align-items: center; @@ -44,18 +44,16 @@ transition: transform 0.2s ease-in-out; } .custom-checkbox-label input[type="checkbox"]:checked + .custom-checkbox-box { - background-color: #8b5cf6; /* purple-500 */ + background-color: #8b5cf6; border-color: #8b5cf6; } .custom-checkbox-label.maintenance input[type="checkbox"]:checked + .custom-checkbox-box { - background-color: #ca8a04; /* yellow-500 */ + background-color: #ca8a04; border-color: #ca8a04; } .custom-checkbox-label input[type="checkbox"]:checked + .custom-checkbox-box svg { transform: scale(1); } - - /* Footer */ footer { text-align: center; padding: 1rem; @@ -63,7 +61,6 @@ color: #888; background: rgba(0,0,0,0.3); } - /* Loading Screen fix: wirklich alles abdecken */ #startup { position: fixed; top: 0; left: 0; right: 0; bottom: 0; @@ -127,7 +124,6 @@ -
@@ -139,12 +135,9 @@

AstraOS

-
-
-
-
-
- System Status -
- +
Chainsaw Hood SCP:SL Server
- Online -
+ Lädt... +
- -
Kettensägen Gamepanel
- Online -
+ Lädt... +
- -
Discord Bots
- Wartungsarbeiten -
+ Lädt... +
- -
Datenbank
- Offline -
+ Lädt... +
+ {% endblock %} diff --git a/AstraOS/Frontend/HTML/user_management.html b/AstraOS/Frontend/HTML/user_management.html index 1b9e4e9..124dde1 100644 --- a/AstraOS/Frontend/HTML/user_management.html +++ b/AstraOS/Frontend/HTML/user_management.html @@ -7,12 +7,25 @@ {% endblock %} {% block content %} -
+

User Management

- @@ -32,13 +45,14 @@ - {% for user in users %} + {% for user_table_item in users_for_table %} - {{ user.username }} - {{ user.role_name }} - {{ user.creation_date.strftime('%d.%m.%Y') if user.creation_date else 'N/A' }} + {{ user_table_item.username }} + {{ user_table_item.role_name }} + {{ user_table_item.creation_date.strftime('%d.%m.%Y') if user_table_item.creation_date else 'N/A' }} - + + {% endfor %} @@ -47,33 +61,58 @@
- + + + +
{% endblock %} diff --git a/AstraOS/Frontend/media/AstraOS_Logo.ico b/AstraOS/Frontend/media/AstraOS_Logo.ico new file mode 100644 index 0000000..4e640e2 Binary files /dev/null and b/AstraOS/Frontend/media/AstraOS_Logo.ico differ diff --git a/AstraOS/astra_os.py b/AstraOS/astra_os.py index b8e69c5..c8af718 100644 --- a/AstraOS/astra_os.py +++ b/AstraOS/astra_os.py @@ -15,6 +15,8 @@ import threading from werkzeug.utils import secure_filename import requests import ipaddress +import socket +import ssl # Verwenden Sie eventlet für die Produktion async_mode = "threading" @@ -33,10 +35,14 @@ app.config['BOT_UPLOAD_FOLDER'] = BOT_UPLOAD_FOLDER # Dictionary zum Speichern laufender Bot-Prozesse running_bots = {} -# Filter, um die GET /api/server_load Anfragen aus den Logs auszublenden +# Filter, um die GET /api/server_load und GET /api/system_status Anfragen aus den Logs auszublenden class SuppressServerLoadFilter(logging.Filter): def filter(self, record): - return "GET /api/server_load" not in record.getMessage() + msg = record.getMessage() + return ( + "GET /api/server_load" not in msg + and "GET /api/system_status" not in msg + ) # Den Filter zum Werkzeug-Logger hinzufügen log = logging.getLogger('werkzeug') @@ -233,6 +239,95 @@ def login(): return render_template('login.html') +@app.route('/api/server_load') +@login_required +def api_server_load(): + # SSH zu 89.144.42.175, user root, pw iwkms@1812:( + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # Timeout zur Verbindung hinzugefügt, um Hängenbleiben zu verhindern + ssh.connect("89.144.42.175", username="root", password="iwkms@1812:(", timeout=10) + stdin, stdout, stderr = ssh.exec_command("top -bn1 | grep 'Cpu(s)'") + output = stdout.read().decode() + ssh.close() + # Parse CPU usage + import re + match = re.search(r"(\d+\.\d+)\s*id", output) + if match: + idle = float(match.group(1)) + usage = 100 - idle + return jsonify({"percent": round(usage, 1), "status": "Online"}) + else: + return jsonify({"percent": 0, "status": "Fehler"}) + except paramiko.AuthenticationException: + print("SSH-Fehler: Authentifizierung fehlgeschlagen.") + return jsonify({"percent": 0, "status": "Auth Fehler"}) + except paramiko.SSHException as e: + print(f"SSH-Fehler: {e}") + return jsonify({"percent": 0, "status": "SSH Fehler"}) + except Exception as e: + print(f"Allgemeiner Fehler bei Server-Auslastung: {e}") + return jsonify({"percent": 0, "status": "Nicht erreichbar"}) + +@app.route('/api/system_status') +@login_required +def api_system_status(): + # Wartungsstatus prüfen + maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None + maintenance_active = maintenance_status and maintenance_status.get('enabled', False) + discord_bots_maintenance = maintenance_status.get('discord_bots_maintenance', False) if maintenance_status else False + + # SCP:SL Server Status (UDP-Ping statt TCP) + scpsl_status = "offline" + try: + udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp_sock.settimeout(2) + udp_sock.sendto(b"\x00", ("89.144.42.175", 7101)) + # Wenn keine Exception, dann ist der Port erreichbar (Server antwortet nicht, aber Port offen) + scpsl_status = "online" + udp_sock.close() + except Exception as e: + print(f"[DEBUG] SCP:SL UDP-Ping failed: {e}") + scpsl_status = "offline" + if maintenance_active: + scpsl_status = "wartung" + + # Gamepanel Status + gamepanel_status = "offline" + try: + resp = requests.get("https://gamepanel.mrsniff.de", timeout=3) + if resp.status_code == 200: + gamepanel_status = "online" + except Exception: + gamepanel_status = "offline" + if maintenance_active: + gamepanel_status = "wartung" + + # Discord Bots Status (online/wartung) + discord_bots_status = "wartung" if discord_bots_maintenance or maintenance_active else "online" + + # Datenbank Status + db_status = "online" if db is not None else "offline" + if maintenance_active: + db_status = "wartung" + + return jsonify({ + "scpsl": scpsl_status, + "gamepanel": gamepanel_status, + "discord_bots": discord_bots_status, + "database": db_status, + "maintenance": maintenance_active + }) + +@app.route('/status') +@login_required +def status_page(): + # Wartungsstatus prüfen + maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None + maintenance_active = maintenance_status and maintenance_status.get('enabled', False) + return render_template('status_page.html', active_page='status_page', maintenance_active=maintenance_active) + @app.route('/dashboard') @login_required def dashboard(): @@ -252,8 +347,25 @@ def dashboard(): # Server-Auslastung (wird per AJAX geladen, hier nur Platzhalter) server_load = {"percent": 0, "status": "Lädt..."} - # Systemstatus (hier statisch, ggf. anpassen) + # Systemstatus per API holen (wie Status-Seite) system_status = {"status": "Online"} + try: + resp = requests.get(request.url_root.rstrip('/') + '/api/system_status', cookies=request.cookies, timeout=2) + if resp.status_code == 200: + data = resp.json() + # Priorität: Wartung > Wartungsmodus > Offline > Online + if data.get("maintenance"): + system_status = {"status": "Online (Wartungsarbeiten)"} + elif any(data.get(k) == "wartung" for k in ["scpsl", "gamepanel", "discord_bots", "database"]): + system_status = {"status": "Systeme im Wartungsmodus"} + elif all(data.get(k) == "online" for k in ["scpsl", "gamepanel", "discord_bots", "database"]): + system_status = {"status": "Online"} + else: + system_status = {"status": "Offline"} + else: + system_status = {"status": "Offline"} + except Exception: + system_status = {"status": "Offline"} # Geplante Wartung (aus dashboard_stats falls vorhanden) scheduled_maintenance = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None @@ -268,7 +380,7 @@ def dashboard(): maint["time_str"] = _dt_to_str(maint.get("time")) stats = { - "active_users": {"value": user_count, "percent": 0}, # Prozent ggf. später berechnen + "active_users": {"value": user_count, "percent": 0}, "server_load": server_load, "open_tickets": {"count": open_tickets}, "system_status": system_status, @@ -282,29 +394,6 @@ def dashboard(): return render_template('dashboard.html', stats=stats, active_page='dashboard') -@app.route('/api/server_load') -@login_required -def api_server_load(): - # SSH zu 89.144.42.175, user root, pw iwkms@1812:( - try: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect("89.144.42.175", username="root", password="iwkms@1812:(") - stdin, stdout, stderr = ssh.exec_command("top -bn1 | grep 'Cpu(s)'") - output = stdout.read().decode() - ssh.close() - # Parse CPU usage - import re - match = re.search(r"(\d+\.\d+)\s*id", output) - if match: - idle = float(match.group(1)) - usage = 100 - idle - return jsonify({"percent": round(usage, 1), "status": "Online"}) - else: - return jsonify({"percent": 0, "status": "Fehler"}) - except Exception as e: - return jsonify({"percent": 0, "status": "Nicht erreichbar"}) - @app.route('/logout') @login_required def logout(): @@ -312,10 +401,7 @@ def logout(): flash("Sie wurden erfolgreich abgemeldet.", "info") return redirect(url_for('login')) -@app.route('/status') -@login_required -def status_page(): - return render_template('status_page.html', active_page='status_page') +# --- User Management --- @app.route('/user_management') @login_required @@ -328,12 +414,32 @@ def user_management(): users_list = list(accounts_collection.find()) roles_list = list(roles_collection.find()) - role_map = {str(role['_id']): role['name'] for role in roles_list} + # Konvertiere ObjectId in String für die JSON-Serialisierung in roles_list + for role in roles_list: + role['_id'] = str(role['_id']) + role_map = {role['_id']: role['name'] for role in roles_list} + + # Daten für das Alpine.js Modal vorbereiten (ohne Datum) + users_for_modal = {} for user in users_list: + user_id_str = str(user['_id']) + users_for_modal[user_id_str] = { + '_id': user_id_str, + 'username': user['username'], + 'role_id': str(user.get('role_id')) if user.get('role_id') else None + } + + # Erstelle eine separate Liste für die Anzeige in der Tabelle, die das Datum enthält + users_for_table = list(accounts_collection.find()) + for user in users_for_table: user['role_name'] = role_map.get(str(user.get('role_id')), 'Unbekannt') - return render_template('user_management.html', users=users_list, roles=roles_list, active_page='user_management') + return render_template('user_management.html', + users_for_modal=users_for_modal, + users_for_table=users_for_table, + roles=roles_list, + active_page='user_management') @app.route('/add_user', methods=['POST']) @login_required @@ -382,6 +488,45 @@ def add_user(): flash(f"Benutzer '{username}' wurde erfolgreich erstellt.", "success") return redirect(url_for('user_management')) +@app.route('/edit_user/', methods=['POST']) +@login_required +@permission_required('manage_users') +def edit_user(user_id): + if accounts_collection is None: + flash("Datenbankverbindung nicht verfügbar.", "danger") + return redirect(url_for('user_management')) + + username = request.form.get('username') + password = request.form.get('password') + role_id = request.form.get('role') + + if not username or not role_id: + flash("Benutzername und Rolle sind erforderlich.", "danger") + return redirect(url_for('user_management')) + + # Überprüfen, ob der neue Benutzername bereits von einem anderen Benutzer verwendet wird + existing_user = accounts_collection.find_one({'username': username}) + if existing_user and str(existing_user['_id']) != user_id: + flash("Ein anderer Benutzer mit diesem Namen existiert bereits.", "danger") + return redirect(url_for('user_management')) + + update_data = { + 'username': username, + 'role_id': ObjectId(role_id) + } + + # Passwort nur aktualisieren, wenn ein neues eingegeben wurde + if password: + update_data['password'] = generate_password_hash(password) + + accounts_collection.update_one( + {'_id': ObjectId(user_id)}, + {'$set': update_data} + ) + + flash(f"Benutzer '{username}' wurde erfolgreich aktualisiert.", "success") + return redirect(url_for('user_management')) + @app.route('/delete_user/', methods=['POST']) @login_required @permission_required('manage_users') @@ -398,6 +543,8 @@ def delete_user(user_id): flash("Benutzer wurde erfolgreich gelöscht.", "success") return redirect(url_for('user_management')) +# --- Role Management --- + @app.route('/role_management') @login_required @permission_required('view_role_management') @@ -781,8 +928,10 @@ def maintenance_login(): def maintenance_management(): maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {} if maint_info and maint_info.get('time'): - # Konvertiere UTC-Zeit aus DB in ein für datetime-local passendes Format maint_info['time_local_str'] = maint_info['time'].strftime('%Y-%m-%dT%H:%M') + # Discord Bots Wartungsstatus für das Template bereitstellen + if 'discord_bots_maintenance' not in maint_info: + maint_info['discord_bots_maintenance'] = False return render_template('maintenance_management.html', active_page='maintenance_management', maint_info=maint_info) @app.route('/update_maintenance', methods=['POST']) @@ -797,12 +946,14 @@ def update_maintenance(): status_message = request.form.get('status_message', 'Keine') duration = request.form.get('duration_minutes') time_str = request.form.get('maintenance_time') + discord_bots_maintenance = request.form.get('discord_bots_maintenance') == 'on' update_data = { "enabled": is_enabled, "status": status_message, "duration_minutes": int(duration) if duration and duration.isdigit() else None, - "time": datetime.fromisoformat(time_str) if time_str else None + "time": datetime.fromisoformat(time_str) if time_str else None, + "discord_bots_maintenance": discord_bots_maintenance } dashboard_collection.update_one( @@ -815,4 +966,26 @@ def update_maintenance(): return redirect(url_for('maintenance_management')) if __name__ == '__main__': - socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True) + # Optionale SSL-Unterstützung: + # Legen Sie entweder die Umgebungsvariablen SSL_CERT_FILE und SSL_KEY_FILE fest + # oder legen Sie die Dateien certs/fullchain.pem und certs/privkey.pem im Projektordner ab. + cert_file = os.environ.get('SSL_CERT_FILE', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs', 'fullchain.pem')) + key_file = os.environ.get('SSL_KEY_FILE', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs', 'privkey.pem')) + + use_ssl = os.path.exists(cert_file) and os.path.exists(key_file) + + # Sitzungscookies nur als Secure markieren, wenn tatsächlich HTTPS verwendet wird + app.config['SESSION_COOKIE_SECURE'] = bool(use_ssl) + + if use_ssl: + print(f"INFO: Starte mit SSL, cert={cert_file}, key={key_file}") + # Für bessere Kontrolle können Sie auch ein SSLContext erstellen: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + context.load_cert_chain(certfile=cert_file, keyfile=key_file) + # In Produktion debug=False setzen und Port 443 verwenden (oder Reverse Proxy) + socketio.run(app, host='0.0.0.0', port=443, debug=False, ssl_context=context) + else: + print("WARNUNG: SSL-Zertifikat nicht gefunden. Starte ohne HTTPS (nur für Entwicklung).") + # allow_unsafe_werkzeug bleibt hier, da dies Entwicklungsmodus ist + socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)