Astra OS Ver. 1.0 hinzugefügt

This commit is contained in:
MrWhiff 2025-12-09 00:29:22 +01:00
parent 7c838ee732
commit 644437067a
14 changed files with 2340 additions and 8 deletions

View file

@ -0,0 +1,103 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Konsole: {{ bot.name }}{% endblock %}
{% block head %}
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<style>
#console-output {
font-family: 'Courier New', Courier, monospace;
background-color: #0d1117;
color: #c9d1d9;
height: 60vh;
overflow-y: auto;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.log-line { white-space: pre-wrap; word-break: break-all; }
.log-stdout { color: #c9d1d9; }
.log-stderr { color: #ff7b72; }
.log-system { color: #58a6ff; font-style: italic; }
.log-error { color: #f85149; font-weight: bold; }
/* Toggle Switch */
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4a5568; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #28a745; }
input:checked + .slider:before { transform: translateX(26px); }
</style>
{% endblock %}
{% block content %}
<!-- HEADER -->
<header class="flex justify-between items-center mb-6">
<div>
<a href="{{ url_for('discord_bots') }}" class="text-sm {% if maintenance_active %}text-yellow-400 hover:text-yellow-300{% else %}text-purple-400 hover:text-purple-300{% endif %}"> Zurück zur Übersicht</a>
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Konsole: {{ bot.name }}</h2>
</div>
<div class="flex items-center gap-4">
<span id="status-indicator" class="px-3 py-1 text-sm font-semibold rounded-full
{% if bot.status == 'running' %}bg-green-500/20 text-green-300{% else %}bg-red-500/20 text-red-300{% endif %}">
{{ bot.status }}
</span>
<label class="switch">
<input type="checkbox" id="bot-toggle" {% if bot.status == 'running' %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
</header>
<!-- CONSOLE -->
<div id="console-output">
<div class="log-line log-system">Initialisiere Konsolenverbindung...</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const socket = io();
const consoleOutput = document.getElementById('console-output');
const botToggle = document.getElementById('bot-toggle');
const statusIndicator = document.getElementById('status-indicator');
const botId = "{{ bot._id }}";
function addLog(log) {
const line = document.createElement('div');
line.className = `log-line log-${log.type}`;
line.textContent = log.data;
consoleOutput.appendChild(line);
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
socket.on('connect', () => {
socket.emit('join_console', { bot_id: botId });
});
socket.on('console_output', (log) => {
addLog(log);
});
socket.on('status_update', (data) => {
if (data.bot_id === botId) {
statusIndicator.textContent = data.status;
statusIndicator.className = `px-3 py-1 text-sm font-semibold rounded-full ${data.status === 'running' ? 'bg-green-500/20 text-green-300' : 'bg-red-500/20 text-red-300'}`;
botToggle.checked = data.status === 'running';
}
});
botToggle.addEventListener('change', () => {
const action = botToggle.checked ? 'start' : 'stop';
addLog({type: 'system', data: `Sende '${action}' Befehl...`});
socket.emit('toggle_bot', { bot_id: botId, action: action });
});
window.addEventListener('beforeunload', () => {
socket.emit('leave_console', { bot_id: botId });
socket.disconnect();
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,169 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Hybrid Dashboard{% endblock %}
{% block head %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<style>
.glow-card {
transition: 0.2s ease;
}
.glow-card:hover {
box-shadow: 0 0 20px rgba(192,132,252,0.3);
transform: translateY(-4px);
}
</style>
{% endblock %}
{% block content %}
<header class="flex justify-between items-center mb-6 opacity-0">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Dashboard</h2>
</header>
<div class="grid grid-cols-1 gap-8">
<div class="glow-card opacity-0 translate-y-8 lg:col-span-2 bg-black/40 p-6 rounded-xl backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-2xl font-semibold mb-4">🚀 Willkommen bei AstraOS!</h3>
<p class="text-gray-300">
AstraOS ist da! Wir haben alles von Grund auf neu gebaut, damit du deine Systeme noch intuitiver und effizienter verwalten kannst. Wir hoffen, dir gefallen das neue Design und die vielen praktischen Funktionen.
</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Aktive Benutzer -->
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Aktive Benutzer</h3>
<p class="text-4xl font-bold" id="active-users">{{ stats.active_users.value }}</p>
<p class="text-green-400 text-sm mt-1" id="active-users-percent">
{% set pct = stats.active_users.percent %}
{% if pct is not none %}
{{ '+' if pct>=0 else '' }}{{ pct }}% seit letzter Woche
{% else %}
-
{% endif %}
</p>
</div>
<!-- Server-Auslastung -->
<div id="server-load-card" class="glow-card opacity-0 translate-y-8 bg-black/40 border border-red-500/40 rounded-xl p-6 backdrop-blur">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Server-Auslastung</h3>
<p class="text-4xl font-bold" id="server-load">{{ stats.server_load.percent }}%</p>
<p class="text-red-400 text-sm mt-1" id="server-load-status">{{ stats.server_load.status }}</p>
</div>
<!-- Offene Tickets -->
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Offene Tickets</h3>
<p class="text-4xl font-bold" id="open-tickets">{{ stats.open_tickets.count }}</p>
<p class="text-gray-400 text-sm mt-1">Keine offenen Anfragen</p>
</div>
<!-- Systemstatus -->
<div class="glow-card opacity-0 translate-y-8 bg-black/40 border border-green-500/40 rounded-xl p-6 backdrop-blur">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Systemstatus</h3>
{% set status = stats.system_status.status %}
<p class="text-4xl font-bold {% if status.lower() == 'online' %}text-green-400{% else %}text-red-400{% endif %}">{{ status }}</p>
<p class="text-gray-400 text-sm mt-1">Alle Systeme funktionsfähig</p>
</div>
<!-- Geplante Wartung -->
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Geplante Wartung</h3>
{% set maint = stats.scheduled_maintenance %}
{% if maint and maint.enabled %}
<p class="text-2xl font-bold text-yellow-400">Aktiv</p>
<p class="text-yellow-400 text-sm mt-1">{{ maint.status if maint.status else 'Wartungsarbeiten laufen' }}</p>
{% elif maint and maint.time_str %}
<p class="text-2xl font-bold">{{ maint.time_str }}</p>
<p class="text-yellow-400 text-sm mt-1">{{ maint.status if maint and maint.status else 'Geplant' }} {% if maint and maint.duration_minutes %}— Dauer: {{ maint.duration_minutes }} Min.{% endif %}</p>
{% else %}
<p class="text-2xl font-bold">Keine geplant</p>
<p class="text-gray-400 text-sm mt-1">Alle Systeme sind betriebsbereit.</p>
{% endif %}
</div>
<!-- Letzte Benutzererstellung -->
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Letzte Benutzererstellung</h3>
{% set last = stats.last_user_created %}
<p class="text-2xl font-bold" id="last-user-time">{{ last.time_str if last and last.time_str else 'Keine Daten' }}</p>
<p class="text-gray-400 text-sm mt-1" id="last-user-by">Durch: {{ last.by if last and last.by else 'N/A' }}</p>
</div>
</div>
<script>
gsap.registerPlugin(ScrollTrigger);
// Lade-Timeline
setTimeout(() => {
// Timeline für Header + Cards
const tl = gsap.timeline({ defaults: { duration: 0.4, ease: "power3.out" } });
// Header
tl.to("header", { opacity: 1, y: 0 });
// Cards schnell einblenden
tl.to(".glow-card", { opacity: 1, y: 0, stagger: 0.08 });
// Header Parallax
gsap.to("header", {
y: 8,
ease: "none",
scrollTrigger: {
trigger: "header",
start: "top top",
scrub: true
}
});
}, 800);
// Hover Glow
document.querySelectorAll(".glow-card").forEach(card => {
card.addEventListener("mouseenter", () => {
gsap.to(card, { boxShadow: "0 0 30px rgba(192,132,252,0.4)", duration: 0.2 });
});
card.addEventListener("mouseleave", () => {
gsap.to(card, { boxShadow: "0 0 0 rgba(0,0,0,0)", duration: 0.2 });
});
});
// Server-Auslastung alle 5 Sekunden aktualisieren
function updateServerLoad() {
fetch('/api/server_load')
.then(res => res.json())
.then(data => {
const loadCard = document.getElementById('server-load-card');
const loadStatus = document.getElementById('server-load-status');
document.getElementById('server-load').textContent = data.percent + '%';
loadStatus.textContent = data.status;
// Klassen für Farben entfernen
loadCard.classList.remove('border-red-500/40', 'border-green-500/40', 'border-yellow-500/40');
loadStatus.classList.remove('text-red-400', 'text-green-400', 'text-yellow-400');
if (data.status === 'Online') {
if (data.percent > 75) {
// Gelb bei hoher Auslastung
loadCard.classList.add('border-yellow-500/40');
loadStatus.classList.add('text-yellow-400');
} else {
// Grün bei normaler Auslastung
loadCard.classList.add('border-green-500/40');
loadStatus.classList.add('text-green-400');
}
} else {
// Rot bei Fehler/Offline
loadCard.classList.add('border-red-500/40');
loadStatus.classList.add('text-red-400');
}
});
}
setInterval(updateServerLoad, 5000);
updateServerLoad();
</script>
{% endblock %}

View file

@ -0,0 +1,135 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Discord Bots{% endblock %}
{% block head %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<style>
/* Toggle Switch */
.switch { position: relative; display: inline-block; width: 50px; height: 28px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4a5568; transition: .4s; border-radius: 28px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #28a745; }
input:checked + .slider:before { transform: translateX(22px); }
</style>
{% endblock %}
{% block content %}
<div x-data="{ showUploadModal: false, showDeleteModal: false, botToDelete: null }">
<main class="flex-1 p-8 space-y-10" id="mainContent">
<!-- HEADER -->
<header class="flex justify-between items-center mb-10">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Discord Bot Management</h2>
<button @click="showUploadModal = true" class="font-bold py-2 px-4 rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
+ Neuen Bot hochladen
</button>
</header>
<!-- BOT LIST -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for bot in bots %}
<div class="bg-black/40 rounded-xl backdrop-blur-lg p-6 flex flex-col justify-between
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<div>
<div class="flex justify-between items-start">
<h3 class="text-2xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">{{ bot.name }}</h3>
<span id="status-{{ bot._id }}" class="px-2 py-1 text-xs font-semibold rounded-full
{% if bot.status == 'running' %}bg-green-500/20 text-green-300{% else %}bg-red-500/20 text-red-300{% endif %}">
{{ bot.status }}
</span>
</div>
<p class="text-sm text-gray-400 mb-4">Filename: {{ bot.filename }}</p>
</div>
<div class="flex justify-between items-center gap-2 mt-4">
<label class="switch">
<input type="checkbox" class="bot-toggle" data-bot-id="{{ bot._id }}" {% if bot.status == 'running' %}checked{% endif %}>
<span class="slider"></span>
</label>
<div class="flex items-center gap-2">
<a href="{{ url_for('bot_console', bot_id=bot._id) }}" class="px-4 py-2 text-sm font-semibold rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600/80 hover:bg-yellow-700{% else %}bg-purple-600/80 hover:bg-purple-700{% endif %}">
Konsole
</a>
<button @click="botToDelete = '{{ bot._id }}'; showDeleteModal = true" class="px-3 py-2 text-sm font-semibold rounded-lg transition text-white bg-red-600/80 hover:bg-red-700">
Löschen
</button>
</div>
</div>
</div>
{% else %}
<p class="text-gray-400 col-span-full text-center">Noch keine Bots hochgeladen.</p>
{% endfor %}
</div>
</main>
<!-- UPLOAD BOT MODAL -->
<div x-show="showUploadModal" @keydown.escape.window="showUploadModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showUploadModal = false" class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-md
{% if maintenance_active %}border border-yellow-500/50{% else %}border border-purple-500/50{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Neuen Bot hochladen</h3>
<form action="{{ url_for('upload_bot') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="bot_name" class="block text-sm font-medium text-gray-300">Bot-Name</label>
<input type="text" name="bot_name" id="bot_name" required class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div>
<label for="bot_file" class="block text-sm font-medium text-gray-300">Bot-Datei (.py)</label>
<input type="file" name="bot_file" id="bot_file" required accept=".py" class="mt-1 w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold {% if maintenance_active %}file:bg-yellow-600/20 file:text-yellow-300 hover:file:bg-yellow-600/30{% else %}file:bg-purple-600/20 file:text-purple-300 hover:file:bg-purple-600/30{% endif %}">
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" @click="showUploadModal = false" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 font-semibold rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">Hochladen</button>
</div>
</form>
</div>
</div>
<!-- DELETE CONFIRMATION MODAL -->
<div x-show="showDeleteModal" @keydown.escape.window="showDeleteModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showDeleteModal = false" class="bg-gray-900 border border-red-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md">
<h3 class="text-2xl font-bold text-red-400 mb-4">Bot löschen</h3>
<p class="text-gray-300 mb-6">Möchten Sie diesen Bot wirklich endgültig löschen? Die Bot-Datei wird vom Server entfernt und diese Aktion kann nicht rückgängig gemacht werden.</p>
<form :action="'/delete_bot/' + botToDelete" method="POST" class="flex justify-end gap-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="button" @click="showDeleteModal = false" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition">Endgültig löschen</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const socket = io();
socket.on('connect', () => {
console.log('Verbunden mit dem Server für Status-Updates.');
});
socket.on('status_update', (data) => {
const botId = data.bot_id;
const statusElement = document.getElementById(`status-${botId}`);
const toggle = document.querySelector(`.bot-toggle[data-bot-id="${botId}"]`);
if (statusElement && toggle) {
statusElement.textContent = data.status;
statusElement.className = `px-2 py-1 text-xs font-semibold rounded-full ${data.status === 'running' ? 'bg-green-500/20 text-green-300' : 'bg-red-500/20 text-red-300'}`;
toggle.checked = data.status === 'running';
}
});
document.querySelectorAll('.bot-toggle').forEach(toggle => {
toggle.addEventListener('change', () => {
const botId = toggle.dataset.botId;
const action = toggle.checked ? 'start' : 'stop';
socket.emit('toggle_bot', { bot_id: botId, action: action });
});
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,112 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — IP Analyzer{% endblock %}
{% block head %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
/* Toggle Switch Styles */
.switch { position: relative; display: inline-block; width: 50px; height: 28px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4a5568; transition: .4s; border-radius: 28px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #ef4444; } /* red-500 */
input:checked + .slider:before { transform: translateX(22px); }
</style>
{% endblock %}
{% block content %}
<div x-data="{ isCheater: false }" class="space-y-10">
<!-- HEADER -->
<header class="flex justify-between items-center">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">IP Analyzer</h2>
</header>
<!-- INPUT FORM -->
<div class="bg-black/40 rounded-xl backdrop-blur-lg p-8 {% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<form action="{{ url_for('ip_analyzer') }}" method="POST" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="ip_address" class="block text-sm font-medium text-gray-300">IP-Adresse</label>
<input type="text" name="ip_address" id="ip_address" required placeholder="z.B. 8.8.8.8"
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div class="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
<div>
<h3 class="text-lg font-semibold text-red-400">Als Cheater markieren</h3>
<p class="text-gray-400 text-sm mt-1">Markiert diese IP als zu einem Cheater gehörig.</p>
</div>
<label class="switch">
<input type="checkbox" name="is_cheater" x-model="isCheater">
<span class="slider"></span>
</label>
</div>
<div x-show="isCheater" x-transition>
<label for="steam_id" class="block text-sm font-medium text-gray-300">SteamID64</label>
<input type="text" name="steam_id" id="steam_id" placeholder="Erforderlich, wenn als Cheater markiert"
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="px-6 py-2 text-white font-semibold rounded-lg transition
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
Analysieren
</button>
</div>
</form>
</div>
<!-- RESULTS -->
{% if error %}
<div class="bg-red-500/20 border border-red-500/30 text-red-300 p-4 rounded-lg">{{ error }}</div>
{% endif %}
{% if ip_data %}
<div class="bg-black/40 rounded-xl backdrop-blur-lg p-8 {% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Analyse-Ergebnis</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-gray-300">
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>IP-Adresse:</strong> {{ ip_data.ip }}</div>
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>Land:</strong> {{ ip_data.location.country }} ({{ ip_data.location.city }})</div>
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>VPN:</strong> <span class="font-bold {{ 'text-red-400' if ip_data.security.vpn else 'text-green-400' }}">{{ 'Ja' if ip_data.security.vpn else 'Nein' }}</span></div>
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>Proxy:</strong> <span class="font-bold {{ 'text-red-400' if ip_data.security.proxy else 'text-green-400' }}">{{ 'Ja' if ip_data.security.proxy else 'Nein' }}</span></div>
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>Tor:</strong> <span class="font-bold {{ 'text-red-400' if ip_data.security.tor else 'text-green-400' }}">{{ 'Ja' if ip_data.security.tor else 'Nein' }}</span></div>
<div class="bg-gray-900/50 p-4 rounded-lg"><strong>ASN:</strong> {{ ip_data.network.autonomous_system_number }}</div>
<div class="md:col-span-2 bg-gray-900/50 p-4 rounded-lg"><strong>Netzwerk:</strong> {{ ip_data.network.network }}</div>
<div class="md:col-span-2 bg-gray-900/50 p-4 rounded-lg"><strong>Organisation:</strong> {{ ip_data.network.autonomous_system_organization }}</div>
</div>
</div>
{% endif %}
{% if similar_ips %}
<div class="bg-black/40 rounded-xl backdrop-blur-lg p-8 {% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Ähnliche Einträge gefunden</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/20{% endif %}">
<thead class="bg-black/20">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">IP</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Cheater</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">SteamID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Hinzugefügt von</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Datum</th>
</tr>
</thead>
<tbody class="divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/30{% endif %}">
{% for ip in similar_ips %}
<tr class="transition {% if maintenance_active %}hover:bg-yellow-500/5{% else %}hover:bg-purple-500/5{% endif %}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">{{ ip.ip }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm"><span class="font-bold {{ 'text-red-400' if ip.is_cheater else 'text-green-400' }}">{{ 'Ja' if ip.is_cheater else 'Nein' }}</span></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ ip.steam_id or 'N/A' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ ip.added_by }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ ip.timestamp.strftime('%d.%m.%Y %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -67,8 +67,18 @@
<h1 id="loginTitle" class="text-3xl font-bold text-center mb-6 tracking-wider text-purple-300"> <h1 id="loginTitle" class="text-3xl font-bold text-center mb-6 tracking-wider text-purple-300">
</h1> </h1>
<form id="loginForm" action="/login" method="POST" class="flex flex-col gap-5 opacity-0"> {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 rounded-md bg-{{ 'red' if category == 'danger' else 'blue' }}-500/20 p-4 text-sm text-{{ 'red' if category == 'danger' else 'blue' }}-300 border border-{{ 'red' if category == 'danger' else 'blue' }}-500/30">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form id="loginForm" action="/login" method="POST" class="flex flex-col gap-5 opacity-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div> <div>
<label class="text-gray-300 text-sm">Username</label> <label class="text-gray-300 text-sm">Username</label>
<input type="text" name="username" required placeholder="Username" <input type="text" name="username" required placeholder="Username"
@ -124,38 +134,47 @@
gsap.registerPlugin(TextPlugin); gsap.registerPlugin(TextPlugin);
{% if not get_flashed_messages() %}
// Nur Animation abspielen, wenn keine Nachrichten vorhanden sind (erster Besuch)
setTimeout(() => { setTimeout(() => {
gsap.to("#startup", { gsap.to("#startup", {
opacity: 0, opacity: 0,
duration: 0.6, duration: 0.5,
onComplete: () => { onComplete: () => {
document.getElementById("startup").style.display = "none"; document.getElementById("startup").style.display = "none";
const tl = gsap.timeline(); const tl = gsap.timeline();
tl.fromTo("#loginBox", tl.fromTo("#loginBox",
{ opacity: 0, scale: 0.1 }, { opacity: 0, scale: 0.8 },
{ {
opacity: 1, opacity: 1,
scale: 1, scale: 1,
duration: 0.8, duration: 0.5,
ease: "power2.out" ease: "power2.out"
} }
); );
tl.to("#loginTitle", { tl.to("#loginTitle", {
duration: 1, duration: 0.8,
text: "AstraOS Login", text: "AstraOS Login",
ease: "none" ease: "none"
}, "-=0.2"); }, "-=0.2");
tl.to("#loginForm", { tl.to("#loginForm", {
opacity: 1, opacity: 1,
duration: 0.5 duration: 0.4
}, "-=0.5"); }, "-=0.4");
} }
}); });
}, 1500); }, 800);
{% else %}
// Wenn Nachrichten vorhanden sind, Animation überspringen
document.getElementById("startup").style.display = "none";
document.getElementById("loginBox").style.opacity = 1;
document.getElementById("loginForm").style.opacity = 1;
document.getElementById("loginTitle").textContent = "AstraOS Login";
{% endif %}
</script> </script>
</body> </body>

View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AstraOS — Wartung</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body {
background: radial-gradient(circle at top, #1b1b26, #0a0a0f 70%);
}
</style>
</head>
<body class="text-white">
<div class="min-h-screen flex flex-col items-center justify-center text-center px-4">
<div id="star" class="w-16 h-16 relative mb-6">
<div class="absolute inset-0 rounded-full blur-2xl opacity-70" style="background: rgba(243, 156, 18, 0.7)"></div>
<svg class="relative z-10" viewBox="0 0 24 24" fill="none" stroke="#f39c12" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.73-.664 1.203-.832l3.528-1.604a2.652 2.652 0 10-3.528-3.528l-1.604 3.528c-.168.473-.448.886-.832 1.203l-3.03 2.496m0 0l-5.877-5.877A2.652 2.652 0 003 9.02l5.877 5.877m0 0l-2.496 3.03c-.317.384-.73.664-1.203.832L3 21a2.652 2.652 0 003.528 3.528l1.604-3.528c.168-.473.448-.886.832-1.203l3.03-2.496m0 0l5.877 5.877a2.652 2.652 0 003.528-3.528l-5.877-5.877m0 0L9.02 3a2.652 2.652 0 00-3.528 3.528L9.02 9.02" />
</svg>
</div>
<h1 class="text-4xl md:text-5xl font-bold text-yellow-400 tracking-wide mb-4">Wartungsarbeiten</h1>
<p class="text-lg text-gray-300 max-w-2xl mb-2">
{{ maint_info.status if maint_info and maint_info.status else 'Wir führen zurzeit planmäßige Wartungsarbeiten durch, um unsere Dienste zu verbessern.' }}
</p>
<p class="text-lg text-gray-300 max-w-2xl">
Wir sind bald wieder für Sie da.
</p>
{% if maint_info and maint_info.time %}
<div class="mt-8 bg-black/30 border border-yellow-500/30 rounded-lg p-4">
<p class="text-gray-400">Voraussichtliches Ende:</p>
{% set end_time = maint_info.time + timedelta(minutes=maint_info.duration_minutes or 0) %}
<p class="text-xl font-semibold text-yellow-300">{{ end_time.strftime('%d. %B %Y, %H:%M Uhr') }}</p>
</div>
{% endif %}
<div class="absolute bottom-10">
<a href="{{ url_for('maintenance_login') }}" class="px-5 py-2 border border-yellow-500/40 text-yellow-300 rounded-lg hover:bg-yellow-500/10 transition-colors text-sm">
Wartungsarbeiten Login
</a>
</div>
</div>
<script>
gsap.to("#star", {
rotation: 360,
duration: 8,
ease: "none",
repeat: -1
});
</script>
</body>
</html>

View file

@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AstraOS — Maintenance Login</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/TextPlugin.min.js"></script>
<style>
body {
background: radial-gradient(circle at top, #2c2315, #0a0a0f 70%);
}
</style>
</head>
<body class="text-white">
<div id="startup" class="fixed inset-0 bg-black flex flex-col items-center justify-center z-50">
<div id="star" class="w-16 h-16 relative">
<div class="absolute inset-0 rounded-full blur-2xl opacity-70" style="background: rgba(243, 156, 18, 0.7)"></div>
<svg class="relative z-10" viewBox="0 0 24 24" fill="none" stroke="#f39c12" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<p class="mt-4 text-xl tracking-wide opacity-80">AstraOS Maintenance</p>
</div>
<div id="loginContent" class="min-h-screen flex items-center justify-center px-4">
<div id="loginBox" class="w-full max-w-md bg-black/40 border border-yellow-500/20 rounded-2xl p-8 backdrop-blur-lg shadow-xl opacity-0">
<div class="flex justify-center items-center gap-4 mb-6">
<svg class="w-8 h-8 text-yellow-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<h1 id="loginTitle" class="text-3xl font-bold text-center tracking-wider text-yellow-300"></h1>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 rounded-md bg-{{ 'red' if category == 'danger' else 'yellow' }}-500/20 p-4 text-sm text-{{ 'red' if category == 'danger' else 'yellow' }}-300 border border-{{ 'red' if category == 'danger' else 'yellow' }}-500/30">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form id="loginForm" action="{{ url_for('maintenance_login') }}" method="POST" class="flex flex-col gap-5 opacity-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label class="text-gray-300 text-sm">Username</label>
<input type="text" name="username" required placeholder="Username"
class="mt-1 w-full px-4 py-2 rounded-lg bg-gray-800/60 text-white
focus:outline-none focus:ring-2 focus:ring-yellow-500">
</div>
<div x-data="{ showPassword: false }">
<label class="text-gray-300 text-sm">Passwort</label>
<div class="relative">
<input :type="showPassword ? 'text' : 'password'" name="password" required placeholder="Passwort"
class="mt-1 w-full px-4 py-2 rounded-lg bg-gray-800/60 text-white
focus:outline-none focus:ring-2 focus:ring-yellow-500 pr-10">
<button type="button" @click="showPassword = !showPassword" class="absolute inset-y-0 right-0 top-0 pr-3 flex items-center text-gray-400 hover:text-gray-200">
<svg x-show="!showPassword" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.432 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<svg x-show="showPassword" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.243 4.243l-4.243-4.243" />
</svg>
</button>
</div>
</div>
<button type="submit"
class="mt-2 bg-yellow-600 hover:bg-yellow-700 text-white py-2 rounded-lg
font-semibold transition">
Bypass Login
</button>
</form>
</div>
</div>
<script>
gsap.to("#star", {
rotation: 360,
duration: 8,
ease: "none",
repeat: -1
});
gsap.registerPlugin(TextPlugin);
{% if not get_flashed_messages() %}
// Nur Animation abspielen, wenn keine Nachrichten vorhanden sind
setTimeout(() => {
gsap.to("#startup", {
opacity: 0,
duration: 0.5,
onComplete: () => {
document.getElementById("startup").style.display = "none";
const tl = gsap.timeline();
tl.fromTo("#loginBox",
{ opacity: 0, scale: 0.8 },
{
opacity: 1,
scale: 1,
duration: 0.5,
ease: "power2.out"
}
);
tl.to("#loginTitle", {
duration: 0.8,
text: "Maintenance Login",
ease: "none"
}, "-=0.2");
tl.to("#loginForm", {
opacity: 1,
duration: 0.4
}, "-=0.4");
}
});
}, 800);
{% else %}
// Wenn Nachrichten vorhanden sind, Animation überspringen
document.getElementById("startup").style.display = "none";
document.getElementById("loginBox").style.opacity = 1;
document.getElementById("loginForm").style.opacity = 1;
document.getElementById("loginTitle").textContent = "Maintenance Login";
{% endif %}
</script>
</body>
</html>

View file

@ -0,0 +1,80 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Maintenance Management{% endblock %}
{% block head %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
/* Toggle Switch Styles */
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4a5568; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #8b5cf6; }
input:checked + .slider:before { transform: translateX(26px); }
</style>
{% endblock %}
{% block content %}
<main class="flex-1 p-8 space-y-10" id="mainContent">
<!-- HEADER -->
<header class="flex justify-between items-center mb-10">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Maintenance Management</h2>
</header>
<!-- SETTINGS FORM -->
<div class="bg-black/40 rounded-xl backdrop-blur-lg p-8
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<form action="{{ url_for('update_maintenance') }}" method="POST" class="space-y-8">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Maintenance Mode Toggle -->
<div class="flex items-center justify-between p-4 bg-gray-900/50 rounded-lg">
<div>
<h3 class="text-xl font-semibold {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Wartungsmodus</h3>
<p class="text-gray-400 text-sm mt-1">Aktiviert den Wartungsmodus und sperrt den Zugang für normale Benutzer.</p>
</div>
<label class="switch">
<input type="checkbox" name="maintenance_enabled" {% if maint_info.enabled %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
<!-- Maintenance Details -->
<div class="space-y-6">
<div>
<label for="status_message" class="block text-sm font-medium text-gray-300">Wartungsnachricht</label>
<input type="text" name="status_message" id="status_message" value="{{ maint_info.status if maint_info.status and maint_info.status != 'Keine' else 'Wir führen gerade Wartungsarbeiten durch.' }}"
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}"
placeholder="z.B. Wir führen gerade Wartungsarbeiten durch.">
<p class="text-xs text-gray-500 mt-1">Diese Nachricht wird auf der Wartungsseite angezeigt.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="maintenance_time" class="block text-sm font-medium text-gray-300">Geplante Startzeit</label>
<input type="datetime-local" name="maintenance_time" id="maintenance_time" value="{{ maint_info.time_local_str if maint_info.time_local_str else '' }}"
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}"
style="color-scheme: dark;">
<p class="text-xs text-gray-500 mt-1">Leer lassen für unbestimmte Zeit.</p>
</div>
<div>
<label for="duration_minutes" class="block text-sm font-medium text-gray-300">Dauer (in Minuten)</label>
<input type="number" name="duration_minutes" id="duration_minutes" value="{{ maint_info.duration_minutes if maint_info.duration_minutes else '' }}"
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}"
placeholder="z.B. 60">
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end pt-4">
<button type="submit" class="px-6 py-2 text-white font-semibold rounded-lg transition
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
Einstellungen speichern
</button>
</div>
</form>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,113 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Role Management{% endblock %}
{% block head %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{% endblock %}
{% block content %}
<div x-data="{ showAddRoleModal: false, showDeleteModal: false, roleToDelete: null, roleToEdit: null }">
<main class="flex-1 p-8 space-y-10" id="mainContent">
<!-- HEADER -->
<header class="flex justify-between items-center mb-10">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Rollen verwalten</h2>
<button @click="showAddRoleModal = true" class="font-bold py-2 px-4 rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
+ Neue Rolle hinzufügen
</button>
</header>
<!-- ROLE TABLE -->
<div class="bg-black/40 rounded-xl backdrop-blur-lg overflow-hidden
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<table class="min-w-full divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/20{% endif %}">
<thead class="bg-black/20">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Rollenname</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Berechtigungen</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Aktionen</span>
</th>
</tr>
</thead>
<tbody class="divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/30{% endif %}">
{% for role in roles %}
<tr class="transition {% if maintenance_active %}hover:bg-yellow-500/5{% else %}hover:bg-purple-500/5{% endif %}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">{{ role.name }}</td>
<td class="px-6 py-4 text-sm text-gray-300">
<div class="flex flex-wrap gap-2">
{% for perm in role.permissions %}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full
{% if maintenance_active %}bg-yellow-500/20 text-yellow-300{% else %}bg-purple-500/20 text-purple-300{% endif %}">{{ perm }}</span>
{% endfor %}
</div>
</td>
<td class="px-6 py-4 text-right text-sm font-medium">
<div class="flex items-center justify-end gap-4">
<button @click='roleToEdit = {{ role | tojson }}; showAddRoleModal = true' class="flex-shrink-0
{% if maintenance_active %}text-yellow-400 hover:text-yellow-300{% else %}text-indigo-400 hover:text-indigo-300{% endif %}">Bearbeiten</button>
{% if role.is_deletable %}
<button @click="roleToDelete = '{{ role._id }}'; showDeleteModal = true" class="flex-shrink-0 text-red-400 hover:text-red-300">Löschen</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</main>
<!-- ADD/EDIT ROLE MODAL -->
<div x-show="showAddRoleModal" @keydown.escape.window="showAddRoleModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showAddRoleModal = false" class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-lg
{% if maintenance_active %}border border-yellow-500/50{% else %}border border-purple-500/50{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}" x-text="roleToEdit ? 'Rolle bearbeiten' : 'Neue Rolle erstellen'"></h3>
<form :action="roleToEdit ? '/edit_role/' + roleToEdit._id : '/add_role'" method="POST" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="role_name" class="block text-sm font-medium text-gray-300">Rollenname</label>
<input type="text" name="role_name" id="role_name" :value="roleToEdit ? roleToEdit.name : ''" required
class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2
{% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div>
<label class="block text-sm font-medium text-gray-300">Berechtigungen</label>
<div class="mt-2 grid grid-cols-2 gap-2 max-h-60 overflow-y-auto pr-2">
{% for perm in available_permissions %}
<label class="custom-checkbox-label {% if maintenance_active %}maintenance{% endif %}">
<input type="checkbox" name="permissions" value="{{ perm }}"
:checked="roleToEdit && roleToEdit.permissions.includes('{{ perm }}')">
<span class="custom-checkbox-box">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</span>
<span class="text-gray-300">{{ perm }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" @click="showAddRoleModal = false; roleToEdit = null" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 font-semibold rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}"
x-text="roleToEdit ? 'Änderungen speichern' : 'Rolle erstellen'"></button>
</div>
</form>
</div>
</div>
<!-- DELETE CONFIRMATION MODAL -->
<div x-show="showDeleteModal" @keydown.escape.window="showDeleteModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showDeleteModal = false" class="bg-gray-900 border border-red-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md">
<h3 class="text-2xl font-bold text-red-400 mb-4">Rolle löschen</h3>
<p class="text-gray-300 mb-6">Möchten Sie diese Rolle wirklich endgültig löschen? Benutzer, die diese Rolle haben, verlieren ihre Berechtigungen.</p>
<form :action="'/delete_role/' + roleToDelete" method="POST" class="flex justify-end gap-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="button" @click="showDeleteModal = false" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition">Löschen</button>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AstraOS{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
{% block head %}{% endblock %}
<style>
body {
background: radial-gradient(circle at top, #1b1b26, #0a0a0f 70%);
overflow-x: hidden;
min-height: 100vh;
}
/* Custom Checkbox */
.custom-checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.custom-checkbox-label input[type="checkbox"] {
display: none;
}
.custom-checkbox-box {
width: 20px;
height: 20px;
border: 2px solid #4a5568; /* gray-600 */
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s, border-color 0.2s;
margin-right: 0.75rem;
}
.custom-checkbox-box svg {
width: 14px;
height: 14px;
color: white;
transform: scale(0);
transition: transform 0.2s ease-in-out;
}
.custom-checkbox-label input[type="checkbox"]:checked + .custom-checkbox-box {
background-color: #8b5cf6; /* purple-500 */
border-color: #8b5cf6;
}
.custom-checkbox-label.maintenance input[type="checkbox"]:checked + .custom-checkbox-box {
background-color: #ca8a04; /* yellow-500 */
border-color: #ca8a04;
}
.custom-checkbox-label input[type="checkbox"]:checked + .custom-checkbox-box svg {
transform: scale(1);
}
/* Footer */
footer {
text-align: center;
padding: 1rem;
font-size: 0.875rem;
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;
width: 100vw !important;
height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
background: black !important;
z-index: 9999 !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#star {
width: 64px;
height: 64px;
position: relative;
}
#star .blur {
position: absolute;
inset: 0;
border-radius: 50%;
filter: blur(12px);
opacity: 0.7;
background: rgba(192,132,252,0.7);
}
#startup p {
margin-top: 1rem;
font-size: 1.25rem;
letter-spacing: 0.05em;
opacity: 0.8;
color: #fff;
}
.animated-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.dot {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(192, 132, 252, 0.2);
animation: float 20s infinite linear;
}
.dot.maintenance {
background: rgba(243, 156, 18, 0.2);
}
@keyframes float {
0% { transform: translateY(100vh) scale(1); }
100% { transform: translateY(-10vh) scale(1.5); }
}
</style>
</head>
<body class="text-white flex flex-col min-h-screen">
<!-- Loading Screen überall -->
<div id="startup">
<div id="star">
<div class="blur"></div>
<svg class="relative z-10" viewBox="0 0 100 100">
<path d="M50 5 L63 40 L95 50 L63 60 L50 95 L37 60 L5 50 L37 40 Z" fill="#c084fc"/>
<path d="M50 28 L58 45 L76 50 L58 55 L50 72 L42 55 L24 50 L42 45 Z" fill="#fff" opacity="0.9"/>
</svg>
</div>
<p>AstraOS</p>
</div>
<!-- Animierter Hintergrund überall -->
<div class="animated-bg"></div>
<!-- Layout: Sidebar und Content nebeneinander, volle Höhe -->
<div class="flex flex-grow">
<!-- SIDEBAR -->
<aside class="w-64 h-screen bg-black/40 border-r p-6 backdrop-blur-lg flex flex-col flex-shrink-0
{% if maintenance_active %}border-yellow-500/30{% else %}border-purple-500/20{% endif %}">
<h1 class="text-3xl font-bold mb-8 tracking-wide
{% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">AstraOS</h1>
<nav class="flex flex-col gap-2 text-gray-300">
<a href="{{ url_for('dashboard') }}"
class="px-3 py-2 rounded-md transition
{% if active_page == 'dashboard' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
Dashboard
</a>
<a href="{{ url_for('status_page') }}"
class="px-3 py-2 rounded-md transition
{% if active_page == 'status_page' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
Status Page
</a>
<!-- ADMINISTRATION -->
<div>
<button class="w-full text-left px-3 py-2 rounded-md transition flex justify-between items-center collapsible
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}"
data-open="{% if active_page in ['user_management', 'role_management', 'maintenance_management'] %}true{% else %}false{% endif %}">
<span>Administration</span>
<svg class="w-4 h-4 transform transition-transform"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="pl-4 mt-1 space-y-1 content hidden">
<a href="{{ url_for('user_management') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'user_management' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
👤 User Management
</a>
<a href="{{ url_for('role_management') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'role_management' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
🛡️ Role Management
</a>
<a href="{{ url_for('maintenance_management') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'maintenance_management' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
🔧 Maintenance
</a>
</div>
</div>
<!-- PRIVATE TOOLS -->
<div>
<button class="w-full text-left px-3 py-2 rounded-md transition flex justify-between items-center collapsible
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}"
data-open="{% if active_page in ['discord_bots'] %}true{% else %}false{% endif %}">
<span>Private Tools</span>
<svg class="w-4 h-4 transform transition-transform"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="pl-4 mt-1 space-y-1 content hidden">
{% if 'manage_discord_bots' in session.get('permissions', []) %}
<a href="{{ url_for('discord_bots') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'discord_bots' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
🤖 Discord Bots
</a>
{% endif %}
</div>
</div>
<!-- MOONLAB TOOLS -->
<div>
<button class="w-full text-left px-3 py-2 rounded-md transition flex justify-between items-center collapsible
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}"
data-open="{% if active_page in ['ip_analyzer'] %}true{% else %}false{% endif %}">
<span>Moonlab Tools</span>
<svg class="w-4 h-4 transform transition-transform"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="pl-4 mt-1 space-y-1 content hidden">
{% if 'view_ip_analyzer' in session.get('permissions', []) %}
<a href="{{ url_for('ip_analyzer') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'ip_analyzer' %}{% if maintenance_active %}bg-yellow-500/10 text-yellow-300{% else %}bg-purple-500/10 text-purple-300{% endif %}{% else %}
{% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}{% endif %}">
🌐 IP Analyzer
</a>
{% endif %}
<a href="https://intra.moon-lab.de" target="_blank" rel="noopener noreferrer" class="block px-3 py-1 rounded-md {% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}">🌐 Moonlab-Intranet</a>
<a href="javascript:void(0);" onclick="alert('Diese Funktion wird in Kürze verfügbar sein.')" class="block px-3 py-1 rounded-md {% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}">🎂 Altersüberprüfung</a>
<a href="javascript:void(0);" onclick="alert('Diese Funktion wird in Kürze verfügbar sein.')" class="block px-3 py-1 rounded-md {% if maintenance_active %}hover:bg-yellow-500/10 hover:text-yellow-300{% else %}hover:bg-purple-500/10 hover:text-purple-300{% endif %}">🎉 Events</a>
</div>
</div>
</nav>
</aside>
<!-- MAIN CONTENT -->
<main class="flex-1 p-8 space-y-10 overflow-y-auto" id="mainContent">
{% block content %}{% endblock %}
</main>
</div>
<!-- LOGOUT BUTTON -->
<a href="{{ url_for('logout') }}" class="fixed bottom-6 left-6 text-white font-bold py-3 px-5 rounded-full shadow-lg transition-all backdrop-blur-sm flex items-center gap-2
{% if maintenance_active %}
bg-yellow-600/50 hover:bg-yellow-500/80 hover:shadow-yellow-500/40 border border-yellow-500/20
{% else %}
bg-purple-600/50 hover:bg-purple-500/80 hover:shadow-purple-500/40 border border-purple-500/20
{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg>
<span>Logout</span>
</a>
<!-- FOOTER -->
<footer>
&copy; 2025 AstraOS
</footer>
<script src="{{ url_for('static', filename='JAVASCRIPT/main.js') }}"></script>
<script>
gsap.to("#star", {
rotation: 360,
duration: 1.2,
ease: "power1.inOut",
repeat: -1
});
gsap.to("#star", { scale: 1.2, duration: 0.8, yoyo: true, repeat: -1, ease: "power1.inOut" });
setTimeout(() => {
gsap.to("#startup", {
opacity: 0,
duration: 0.3,
onComplete: () => {
document.getElementById("startup").style.display = "none";
gsap.to("#mainContent", { opacity: 1, duration: 0.1 });
}
});
}, 400);
// Animated Background Dots
const bg = document.querySelector('.animated-bg');
if (bg) {
for (let i = 0; i < 50; i++) {
const dot = document.createElement('div');
dot.classList.add('dot');
if (document.body.classList.contains('maintenance-active')) {
dot.classList.add('maintenance');
}
dot.style.left = `${Math.random() * 100}vw`;
dot.style.animationDelay = `${Math.random() * -20}s`;
dot.style.animationDuration = `${20 + Math.random() * 10}s`;
bg.appendChild(dot);
}
}
</script>
{% if maintenance_active %}
<script>document.body.classList.add('maintenance-active');</script>
{% endif %}
</body>
</html>

View file

@ -0,0 +1,79 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — Status Page{% endblock %}
{% block head %}
<style>
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-online {
background-color: #2ecc71;
box-shadow: 0 0 8px #2ecc71;
}
.status-offline {
background-color: #e74c3c;
box-shadow: 0 0 8px #e74c3c;
}
.status-wartung {
background-color: #f39c12;
box-shadow: 0 0 8px #f39c12;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
{% endblock %}
{% block content %}
<!-- HEADER -->
<header class="flex justify-between items-center mb-10">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">System Status</h2>
</header>
<!-- STATUS LIST -->
<div class="space-y-4">
<!-- Chainsaw Hood SCP:SL server -->
<div class="bg-black/40 rounded-xl p-6 backdrop-blur-lg flex justify-between items-center
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<span class="text-xl font-semibold text-gray-200">Chainsaw Hood SCP:SL Server</span>
<div class="flex items-center gap-3">
<span class="text-green-400 font-semibold">Online</span>
<div class="status-indicator status-online"></div>
</div>
</div>
<!-- Kettensägen Gamepanel -->
<div class="bg-black/40 rounded-xl p-6 backdrop-blur-lg flex justify-between items-center
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<span class="text-xl font-semibold text-gray-200">Kettensägen Gamepanel</span>
<div class="flex items-center gap-3">
<span class="text-green-400 font-semibold">Online</span>
<div class="status-indicator status-online"></div>
</div>
</div>
<!-- Discord Bots -->
<div class="bg-black/40 rounded-xl p-6 backdrop-blur-lg flex justify-between items-center
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<span class="text-xl font-semibold text-gray-200">Discord Bots</span>
<div class="flex items-center gap-3">
<span class="text-yellow-400 font-semibold">Wartungsarbeiten</span>
<div class="status-indicator status-wartung"></div>
</div>
</div>
<!-- Datenbank -->
<div class="bg-black/40 rounded-xl p-6 backdrop-blur-lg flex justify-between items-center
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<span class="text-xl font-semibold text-gray-200">Datenbank</span>
<div class="flex items-center gap-3">
<span class="text-red-400 font-semibold">Offline</span>
<div class="status-indicator status-offline"></div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,98 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS — User Management{% endblock %}
{% block head %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{% endblock %}
{% block content %}
<div x-data="{ showAddUserModal: false, showDeleteModal: false, userToDelete: null }">
<div class="space-y-10">
<!-- HEADER -->
<header class="flex justify-between items-center">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">User Management</h2>
<button @click="showAddUserModal = true" class="font-bold py-2 px-4 rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
+ Neuen Benutzer hinzufügen
</button>
</header>
<!-- USER TABLE -->
<div class="bg-black/40 rounded-xl backdrop-blur-lg overflow-hidden
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<table class="min-w-full divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/20{% endif %}">
<thead class="bg-black/20">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Benutzername</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Rolle</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Erstellt am</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Aktionen</span>
</th>
</tr>
</thead>
<tbody class="divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/30{% endif %}">
{% for user in users %}
<tr class="transition {% if maintenance_active %}hover:bg-yellow-500/5{% else %}hover:bg-purple-500/5{% endif %}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">{{ user.username }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ user.role_name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ user.creation_date.strftime('%d.%m.%Y') if user.creation_date else 'N/A' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button @click="userToDelete = '{{ user._id }}'; showDeleteModal = true" class="text-red-400 hover:text-red-300">Löschen</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- ADD USER MODAL -->
<div x-show="showAddUserModal" @keydown.escape.window="showAddUserModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showAddUserModal = false" class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-md
{% if maintenance_active %}border border-yellow-500/50{% else %}border border-purple-500/50{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Neuen Benutzer erstellen</h3>
<form action="{{ url_for('add_user') }}" method="POST" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="username" class="block text-sm font-medium text-gray-300">Benutzername</label>
<input type="text" name="username" id="username" required class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-300">Passwort</label>
<input type="password" name="password" id="password" required class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<div>
<label for="role" class="block text-sm font-medium text-gray-300">Rolle</label>
<select name="role" id="role" required class="mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
{% for role in roles %}
<option value="{{ role._id }}">{{ role.name }}</option>
{% endfor %}
</select>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" @click="showAddUserModal = false" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 font-semibold rounded-lg transition text-white
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">Benutzer erstellen</button>
</div>
</form>
</div>
</div>
<!-- DELETE CONFIRMATION MODAL -->
<div x-show="showDeleteModal" @keydown.escape.window="showDeleteModal = false" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50" style="display: none;">
<div @click.away="showDeleteModal = false" class="bg-gray-900 border border-red-500/50 rounded-xl shadow-2xl p-8 w-full max-w-md">
<h3 class="text-2xl font-bold text-red-400 mb-4">Benutzer löschen</h3>
<p class="text-gray-300 mb-6">Möchten Sie diesen Benutzer wirklich endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.</p>
<form :action="'/delete_user/' + userToDelete" method="POST" class="flex justify-end gap-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="button" @click="showDeleteModal = false" class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition">Abbrechen</button>
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition">Löschen</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block loading %}
<!-- Optional: Loading-Screen wie im Dashboard, falls gewünscht -->
{% endblock %}

View file

@ -0,0 +1,75 @@
document.addEventListener('DOMContentLoaded', function () {
// --- Initial Load: Set correct open/closed state ---
document.querySelectorAll('.collapsible').forEach(button => {
const content = button.nextElementSibling;
const chevron = button.querySelector('svg');
const shouldOpen = button.dataset.open === "true";
if (shouldOpen) {
content.classList.remove("hidden");
content.style.height = "auto";
content.style.opacity = "1";
if (chevron) chevron.style.transform = "rotate(180deg)";
} else {
content.classList.add("hidden");
content.style.height = "0";
content.style.opacity = "0";
if (chevron) chevron.style.transform = "rotate(0deg)";
}
});
// --- Accordion Click Behavior ---
document.querySelectorAll('.collapsible').forEach(button => {
button.addEventListener('click', () => {
const content = button.nextElementSibling;
const chevron = button.querySelector('svg');
// First close all others
document.querySelectorAll('.collapsible').forEach(other => {
if (other !== button) {
const otherContent = other.nextElementSibling;
const otherChevron = other.querySelector('svg');
if (!otherContent.classList.contains('hidden')) {
gsap.to(otherContent, {
height: 0,
opacity: 0,
duration: 0.25,
ease: 'power2.in',
onComplete: () => {
otherContent.classList.add('hidden');
if (otherChevron) gsap.to(otherChevron, { rotation: 0, duration: 0.25 });
}
});
}
}
});
// Toggle current
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
gsap.fromTo(content,
{ height: 0, opacity: 0 },
{ height: 'auto', opacity: 1, duration: 0.25, ease: 'power2.out' }
);
if (chevron) gsap.to(chevron, { rotation: 180, duration: 0.25 });
} else {
gsap.to(content, {
height: 0,
opacity: 0,
duration: 0.25,
ease: 'power2.in',
onComplete: () => {
content.classList.add('hidden');
if (chevron) gsap.to(chevron, { rotation: 0, duration: 0.25 });
}
});
}
});
});
});

View file

@ -0,0 +1,818 @@
import os
from flask import Flask, render_template, request, redirect, url_for, session, flash
from pymongo import MongoClient
from werkzeug.security import check_password_hash, generate_password_hash
from flask_wtf.csrf import CSRFProtect
from datetime import timedelta, datetime
from functools import wraps
from bson.objectid import ObjectId
import paramiko
from flask import jsonify
import logging
from flask_socketio import SocketIO, emit, join_room, leave_room
import subprocess
import threading
from werkzeug.utils import secure_filename
import requests
import ipaddress
# Verwenden Sie eventlet für die Produktion
async_mode = "threading"
app = Flask(__name__,
template_folder='Frontend/HTML',
static_folder='Frontend')
socketio = SocketIO(app, async_mode=async_mode)
# Verzeichnis für hochgeladene Bots
BOT_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Bots')
if not os.path.exists(BOT_UPLOAD_FOLDER):
os.makedirs(BOT_UPLOAD_FOLDER)
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
class SuppressServerLoadFilter(logging.Filter):
def filter(self, record):
return "GET /api/server_load" not in record.getMessage()
# Den Filter zum Werkzeug-Logger hinzufügen
log = logging.getLogger('werkzeug')
log.addFilter(SuppressServerLoadFilter())
app.secret_key = os.urandom(32)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=8)
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
csrf = CSRFProtect(app)
@app.context_processor
def inject_utility_processor():
maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
is_maintenance_active = maintenance_status and maintenance_status.get('enabled', False)
return dict(
timedelta=timedelta,
maintenance_active=is_maintenance_active
)
try:
client = MongoClient("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/")
db = client["astra_os"]
accounts_collection = db["accounts"]
logs_collection = db["logs"]
roles_collection = db["roles"]
dashboard_collection = db["dashboard_stats"]
discord_bots_collection = db["discord_bots_data"]
ip_analyzer_results_collection = db["ip_analyzer_results"]
print("INFO: MongoDB-Verbindung erfolgreich hergestellt.")
AVAILABLE_PERMISSIONS = [
"view_dashboard",
"view_user_management",
"manage_users",
"view_role_management",
"manage_roles",
"view_logs",
"maintenance_management",
"bypass_maintenance",
"manage_discord_bots",
"view_ip_analyzer"
]
if roles_collection.count_documents({}) == 0:
print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...")
roles_collection.insert_many([
{
"name": "Owner",
"permissions": AVAILABLE_PERMISSIONS,
"is_deletable": False
},
{
"name": "Benutzer",
"permissions": ["view_dashboard"],
"is_deletable": True
}
])
print("INFO: Standard-Rollen ('Owner', 'Benutzer') wurden erstellt.")
if accounts_collection.count_documents({}) == 0:
print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...")
owner_role = roles_collection.find_one({"name": "Owner"})
hashed_password = generate_password_hash("1234")
accounts_collection.insert_one({
'username': 'admin',
'password': hashed_password,
'role_id': owner_role['_id'],
'creation_date': datetime.utcnow()
})
print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.")
# Standard-Dashboard-Einträge anlegen, falls Collection leer
if dashboard_collection.count_documents({}) == 0:
print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.")
dashboard_collection.insert_many([
{"key": "active_users", "value": 1, "percent": 100},
{"key": "server_load", "percent": 0, "status": "Nicht Erreichbar"},
{"key": "open_tickets", "count": 0},
{"key": "system_status", "status": "Online"},
{"key": "scheduled_maintenance", "enabled": False, "time": None, "status": "Keine", "duration_minutes": None},
{"key": "last_user_created", "time": None, "by": None}
])
print("INFO: Standard Dashboard-Statistiken wurden erstellt.")
except Exception as e:
print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
db = None
accounts_collection = None
logs_collection = None
roles_collection = None
dashboard_collection = None
discord_bots_collection = None
ip_analyzer_results_collection = None
print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
db = None
accounts_collection = None
logs_collection = None
# MongoDB für Tickets (chainsaw_dc_data)
try:
tickets_client = MongoClient("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/")
tickets_db = tickets_client["chainsaw_dc_data"]
tickets_collection = tickets_db["tickets"]
except Exception as e:
tickets_collection = None
print(f"FATAL: Verbindung zu Tickets-DB fehlgeschlagen: {e}")
def get_user_permissions(user_id):
user = accounts_collection.find_one({'_id': ObjectId(user_id)})
if not user or 'role_id' not in user:
return []
role = roles_collection.find_one({'_id': user['role_id']})
if not role:
return []
return role.get('permissions', [])
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash("Bitte melden Sie sich an, um diese Seite zu sehen.", "warning")
return redirect(url_for('login'))
# Berechtigungen bei jeder Anfrage aktualisieren, um Änderungen sofort zu übernehmen
session['permissions'] = get_user_permissions(session['user_id'])
# Wartungsmodus-Check
maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"})
if maintenance_status and maintenance_status.get('enabled', False):
if 'bypass_maintenance' not in session['permissions']:
return redirect(url_for('maintenance'))
return f(*args, **kwargs)
return decorated_function
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('login'))
user_perms = get_user_permissions(session['user_id'])
if permission not in user_perms:
flash("Sie haben keine Berechtigung, diese Seite anzuzeigen.", "danger")
return redirect(url_for('dashboard'))
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/')
def home():
if 'user_id' in session:
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if 'user_id' in session:
return redirect(url_for('dashboard'))
# Wartungsmodus-Check vor dem Login-Versuch
maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
if maintenance_status and maintenance_status.get('enabled', False):
return redirect(url_for('maintenance'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
flash("Benutzername und Passwort sind erforderlich.", "danger")
return render_template('login.html')
if accounts_collection is None:
flash("Datenbankverbindung nicht verfügbar. Bitte versuchen Sie es später erneut.", "danger")
return render_template('login.html'), 500
user = accounts_collection.find_one({'username': username})
if user and check_password_hash(user['password'], password):
session.permanent = True
session['user_id'] = str(user['_id'])
session['username'] = user['username']
session['permissions'] = get_user_permissions(str(user['_id']))
flash("Erfolgreich angemeldet!", "success")
return redirect(url_for('dashboard'))
else:
flash("Ungültiger Benutzername oder Passwort.", "danger")
return render_template('login.html')
return render_template('login.html')
@app.route('/dashboard')
@login_required
def dashboard():
# Aktive Benutzer
user_count = accounts_collection.count_documents({}) if accounts_collection is not None else 1
# Letzte Benutzererstellung
last_user = accounts_collection.find_one(sort=[("creation_date", -1)]) if accounts_collection is not None else None
last_user_time = last_user["creation_date"] if last_user and "creation_date" in last_user else None
last_user_by = last_user["username"] if last_user and "username" in last_user else None
# Offene Tickets
open_tickets = 0
if tickets_collection is not None:
open_tickets = tickets_collection.count_documents({"status": "open"})
# Server-Auslastung (wird per AJAX geladen, hier nur Platzhalter)
server_load = {"percent": 0, "status": "Lädt..."}
# Systemstatus (hier statisch, ggf. anpassen)
system_status = {"status": "Online"}
# Geplante Wartung (aus dashboard_stats falls vorhanden)
scheduled_maintenance = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
def _dt_to_str(v):
if v is None:
return None
try:
return v.strftime("%d. %b %Y, %H:%M")
except Exception:
return str(v)
maint = scheduled_maintenance or {"time": None, "status": "Keine", "duration_minutes": None}
maint["time_str"] = _dt_to_str(maint.get("time"))
stats = {
"active_users": {"value": user_count, "percent": 0}, # Prozent ggf. später berechnen
"server_load": server_load,
"open_tickets": {"count": open_tickets},
"system_status": system_status,
"scheduled_maintenance": maint,
"last_user_created": {
"time": last_user_time,
"by": last_user_by,
"time_str": _dt_to_str(last_user_time)
}
}
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():
session.clear()
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')
@app.route('/user_management')
@login_required
@permission_required('view_user_management')
def user_management():
if accounts_collection is None or roles_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('dashboard'))
users_list = list(accounts_collection.find())
roles_list = list(roles_collection.find())
role_map = {str(role['_id']): role['name'] for role in roles_list}
for user in users_list:
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')
@app.route('/add_user', methods=['POST'])
@login_required
@permission_required('manage_users')
def add_user():
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 password or not role_id:
flash("Alle Felder sind erforderlich.", "danger")
return redirect(url_for('user_management'))
if accounts_collection.find_one({'username': username}):
flash("Ein Benutzer mit diesem Namen existiert bereits.", "danger")
return redirect(url_for('user_management'))
hashed_password = generate_password_hash(password)
accounts_collection.insert_one({
'username': username,
'password': hashed_password,
'role_id': ObjectId(role_id),
'creation_date': datetime.utcnow()
})
# Dashboard-Statistik aktualisieren
if dashboard_collection is not None:
dashboard_collection.update_one(
{"key": "last_user_created"},
{"$set": {
"time": datetime.utcnow(),
"by": username
}},
upsert=True
)
# Aktive Benutzer aktualisieren
user_count = accounts_collection.count_documents({})
dashboard_collection.update_one(
{"key": "active_users"},
{"$set": {"value": user_count}},
upsert=True
)
flash(f"Benutzer '{username}' wurde erfolgreich erstellt.", "success")
return redirect(url_for('user_management'))
@app.route('/delete_user/<user_id>', methods=['POST'])
@login_required
@permission_required('manage_users')
def delete_user(user_id):
if accounts_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('user_management'))
if session.get('user_id') == user_id:
flash("Sie können sich nicht selbst löschen.", "danger")
return redirect(url_for('user_management'))
accounts_collection.delete_one({'_id': ObjectId(user_id)})
flash("Benutzer wurde erfolgreich gelöscht.", "success")
return redirect(url_for('user_management'))
@app.route('/role_management')
@login_required
@permission_required('view_role_management')
def role_management():
if roles_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('dashboard'))
roles = list(roles_collection.find())
# Konvertiere ObjectId in String für die JSON-Serialisierung im Template
for role in roles:
role['_id'] = str(role['_id'])
return render_template('role_management.html', roles=roles, available_permissions=AVAILABLE_PERMISSIONS, active_page='role_management')
@app.route('/add_role', methods=['POST'])
@login_required
@permission_required('manage_roles')
def add_role():
role_name = request.form.get('role_name')
permissions = request.form.getlist('permissions')
if not role_name:
flash("Rollenname ist erforderlich.", "danger")
return redirect(url_for('role_management'))
if roles_collection.find_one({'name': role_name}):
flash("Eine Rolle mit diesem Namen existiert bereits.", "danger")
return redirect(url_for('role_management'))
roles_collection.insert_one({
"name": role_name,
"permissions": permissions,
"is_deletable": True
})
flash(f"Rolle '{role_name}' wurde erstellt.", "success")
return redirect(url_for('role_management'))
@app.route('/edit_role/<role_id>', methods=['POST'])
@login_required
@permission_required('manage_roles')
def edit_role(role_id):
role_name = request.form.get('role_name')
permissions = request.form.getlist('permissions')
if not role_name:
flash("Rollenname ist erforderlich.", "danger")
return redirect(url_for('role_management'))
roles_collection.update_one(
{'_id': ObjectId(role_id)},
{'$set': {'name': role_name, 'permissions': permissions}}
)
flash(f"Rolle '{role_name}' wurde aktualisiert.", "success")
return redirect(url_for('role_management'))
@app.route('/delete_role/<role_id>', methods=['POST'])
@login_required
@permission_required('manage_roles')
def delete_role(role_id):
role = roles_collection.find_one({'_id': ObjectId(role_id)})
if role and not role.get('is_deletable', True):
flash("Diese Rolle kann nicht gelöscht werden.", "danger")
return redirect(url_for('role_management'))
roles_collection.delete_one({'_id': ObjectId(role_id)})
flash("Rolle wurde gelöscht.", "success")
return redirect(url_for('role_management'))
# --- IP Analyzer ---
@app.route('/ip_analyzer', methods=['GET', 'POST'])
@login_required
@permission_required('view_ip_analyzer')
def ip_analyzer():
ip_data = None
error = None
similar_ips = []
if request.method == 'POST':
ip_address = request.form.get("ip_address")
is_cheater = request.form.get("is_cheater") == "on"
steam_id = request.form.get("steam_id") if is_cheater else None
if not ip_address:
error = "Bitte eine IP-Adresse eingeben."
elif is_cheater and not steam_id:
error = "SteamID ist erforderlich, wenn die IP als Cheater markiert wird."
else:
try:
api_key = "015ada9472664329affb2f26d7036d47" # API-Schlüssel aus altem Code
response = requests.get(f"https://vpnapi.io/api/{ip_address}?key={api_key}", timeout=5)
if response.status_code == 200:
ip_data = response.json()
# Sicherstellen, dass verschachtelte Objekte existieren
ip_data.setdefault('security', {})
ip_data.setdefault('location', {})
ip_data.setdefault('network', {})
# Ähnliche IPs finden
similar_ips = list(ip_analyzer_results_collection.find({
"$or": [
{"ip": ip_address},
{"data.location.city": ip_data.get('location', {}).get('city')},
{"data.network.autonomous_system_number": ip_data.get('network', {}).get('autonomous_system_number')}
]
}))
# Neuen Eintrag speichern
ip_analyzer_results_collection.insert_one({
"ip": ip_address,
"is_cheater": is_cheater,
"steam_id": steam_id,
"data": ip_data,
"added_by": session["username"],
"timestamp": datetime.utcnow()
})
flash("IP-Adresse erfolgreich analysiert und gespeichert.", "success")
else:
error = f"Fehler bei der API-Anfrage: Status {response.status_code}"
except requests.exceptions.RequestException as e:
error = f"Fehler bei der Verbindung zur API: {e}"
except Exception as e:
error = f"Ein unerwarteter Fehler ist aufgetreten: {e}"
return render_template('ip_analyzer.html',
active_page='ip_analyzer',
ip_data=ip_data,
error=error,
similar_ips=similar_ips)
# --- Discord Bot Management ---
def stream_bot_output(bot_id, process):
"""Liest stdout/stderr eines Prozesses, speichert den Verlauf und sendet es via SocketIO."""
def stream_to_socket(pipe, log_type):
try:
for line in iter(pipe.readline, ''):
log_entry = {'type': log_type, 'data': line}
# Verlauf speichern, wenn der Bot noch läuft
if bot_id in running_bots:
running_bots[bot_id]['history'].append(log_entry)
# Optional: Begrenzen Sie die Größe des Verlaufs, um Speicher zu sparen
max_history_lines = 1000
if len(running_bots[bot_id]['history']) > max_history_lines:
running_bots[bot_id]['history'].pop(0)
socketio.emit('console_output', log_entry, room=bot_id)
except Exception as e:
print(f"Stream-Fehler für Bot {bot_id}: {e}")
finally:
if pipe:
pipe.close()
stdout_thread = threading.Thread(target=stream_to_socket, args=(process.stdout, 'stdout'))
stderr_thread = threading.Thread(target=stream_to_socket, args=(process.stderr, 'stderr'))
stdout_thread.start()
stderr_thread.start()
process.wait() # Warten, bis der Prozess endet
# Prozess beendet, Status aktualisieren
if bot_id in running_bots:
del running_bots[bot_id]
socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id)
@app.route('/discord_bots')
@login_required
@permission_required('manage_discord_bots')
def discord_bots():
bots = list(discord_bots_collection.find())
for bot in bots:
bot['status'] = 'running' if str(bot['_id']) in running_bots else 'stopped'
return render_template('discord_bots.html', active_page='discord_bots', bots=bots)
@app.route('/upload_bot', methods=['POST'])
@login_required
@permission_required('manage_discord_bots')
def upload_bot():
bot_name = request.form.get('bot_name')
bot_file = request.files.get('bot_file')
if not bot_name or not bot_file or bot_file.filename == '':
flash("Bot-Name und Datei sind erforderlich.", "danger")
return redirect(url_for('discord_bots'))
if discord_bots_collection.find_one({'name': bot_name}):
flash("Ein Bot mit diesem Namen existiert bereits.", "danger")
return redirect(url_for('discord_bots'))
filename = secure_filename(bot_file.filename)
filepath = os.path.join(app.config['BOT_UPLOAD_FOLDER'], filename)
bot_file.save(filepath)
discord_bots_collection.insert_one({
'name': bot_name,
'filename': filename,
'filepath': filepath,
'created_at': datetime.utcnow()
})
flash(f"Bot '{bot_name}' wurde erfolgreich hochgeladen.", "success")
return redirect(url_for('discord_bots'))
@app.route('/delete_bot/<bot_id>', methods=['POST'])
@login_required
@permission_required('manage_discord_bots')
def delete_bot(bot_id):
bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
if not bot_info:
flash("Bot nicht gefunden.", "danger")
return redirect(url_for('discord_bots'))
# Bot stoppen, falls er läuft
if bot_id in running_bots:
process = running_bots[bot_id]['process']
try:
process.terminate()
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
del running_bots[bot_id]
socketio.emit('status_update', {'bot_id': bot_id, 'status': 'deleted'}, room=bot_id)
# Bot-Datei löschen
try:
filepath = bot_info.get('filepath')
if filepath and os.path.exists(filepath):
os.remove(filepath)
except Exception as e:
flash(f"Fehler beim Löschen der Bot-Datei: {e}", "danger")
# Fortfahren, um den DB-Eintrag trotzdem zu löschen
# Eintrag aus der Datenbank löschen
discord_bots_collection.delete_one({'_id': ObjectId(bot_id)})
flash(f"Bot '{bot_info['name']}' wurde erfolgreich gelöscht.", "success")
return redirect(url_for('discord_bots'))
@app.route('/bot_console/<bot_id>')
@login_required
@permission_required('manage_discord_bots')
def bot_console(bot_id):
bot = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
if not bot:
flash("Bot nicht gefunden.", "danger")
return redirect(url_for('discord_bots'))
bot['status'] = 'running' if bot_id in running_bots else 'stopped'
return render_template('bot_console.html', bot=bot, active_page='discord_bots')
@socketio.on('connect')
def handle_connect():
print('Client verbunden')
@socketio.on('join_console')
def handle_join_console(data):
bot_id = data['bot_id']
join_room(bot_id)
emit('console_output', {'type': 'system', 'data': f'Verbunden mit Konsole für Bot {bot_id}.'})
# Gespeicherten Verlauf an den neuen Client senden
if bot_id in running_bots and 'history' in running_bots[bot_id]:
for log in running_bots[bot_id]['history']:
emit('console_output', log)
@socketio.on('leave_console')
def handle_leave_console(data):
bot_id = data['bot_id']
leave_room(bot_id)
print(f'Client hat Konsole für Bot {bot_id} verlassen.')
@socketio.on('toggle_bot')
def handle_toggle_bot(data):
bot_id = data['bot_id']
action = data['action'] # 'start' or 'stop'
bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)})
if not bot_info:
emit('console_output', {'type': 'error', 'data': 'Bot nicht in der Datenbank gefunden.'}, room=bot_id)
return
if action == 'start' and bot_id not in running_bots:
filepath = bot_info.get('filepath')
if not os.path.exists(filepath):
emit('console_output', {'type': 'error', 'data': f'Bot-Datei nicht gefunden: {filepath}'}, room=bot_id)
return
try:
# Start process
process = subprocess.Popen(
['python', '-u', filepath],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace'
)
running_bots[bot_id] = {'process': process, 'history': []}
# Start thread to stream output
thread = threading.Thread(target=stream_bot_output, args=(bot_id, process))
thread.daemon = True
thread.start()
emit('status_update', {'bot_id': bot_id, 'status': 'running'}, room=bot_id)
emit('console_output', {'type': 'system', 'data': f'--- Bot-Prozess gestartet (PID: {process.pid}) ---'}, room=bot_id)
except Exception as e:
emit('console_output', {'type': 'error', 'data': f'Fehler beim Starten des Bots: {e}'}, room=bot_id)
elif action == 'stop' and bot_id in running_bots:
process = running_bots[bot_id]['process']
try:
process.terminate() # Graceful shutdown
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill() # Force kill
if bot_id in running_bots:
del running_bots[bot_id]
emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id)
emit('console_output', {'type': 'system', 'data': '--- Bot-Prozess wird gestoppt... ---'}, room=bot_id)
@app.route('/maintenance')
def maintenance():
maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {}
if not maint_info or not maint_info.get('enabled', False):
return redirect(url_for('login'))
return render_template('maintenance.html', maint_info=maint_info)
@app.route('/maintenance_login', methods=['GET', 'POST'])
def maintenance_login():
if 'user_id' in session:
return redirect(url_for('dashboard'))
maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None
if not maintenance_status or not maintenance_status.get('enabled', False):
return redirect(url_for('login'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
flash("Benutzername und Passwort sind erforderlich.", "danger")
return render_template('maintenance_login.html')
if accounts_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return render_template('maintenance_login.html'), 500
user = accounts_collection.find_one({'username': username})
if user and check_password_hash(user['password'], password):
# Überprüfen, ob der Benutzer die Bypass-Berechtigung hat
user_perms = get_user_permissions(str(user['_id']))
if 'bypass_maintenance' in user_perms:
session.permanent = True
session['user_id'] = str(user['_id'])
session['username'] = user['username']
session['permissions'] = user_perms
flash("Erfolgreich angemeldet!", "success")
return redirect(url_for('dashboard'))
else:
flash("Dieser Account hat keine Berechtigung für den Wartungs-Login.", "danger")
return render_template('maintenance_login.html')
else:
flash("Ungültiger Benutzername oder Passwort.", "danger")
return render_template('maintenance_login.html')
return render_template('maintenance_login.html')
@app.route('/maintenance_management')
@login_required
@permission_required('maintenance_management')
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')
return render_template('maintenance_management.html', active_page='maintenance_management', maint_info=maint_info)
@app.route('/update_maintenance', methods=['POST'])
@login_required
@permission_required('maintenance_management')
def update_maintenance():
if dashboard_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('maintenance_management'))
is_enabled = request.form.get('maintenance_enabled') == 'on'
status_message = request.form.get('status_message', 'Keine')
duration = request.form.get('duration_minutes')
time_str = request.form.get('maintenance_time')
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
}
dashboard_collection.update_one(
{"key": "scheduled_maintenance"},
{"$set": update_data},
upsert=True
)
flash("Wartungseinstellungen wurden aktualisiert.", "success")
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)