Astra OS Ver. 1.0 hinzugefügt
This commit is contained in:
parent
7c838ee732
commit
644437067a
103
AstraOS/Frontend/HTML/bot_console.html
Normal file
103
AstraOS/Frontend/HTML/bot_console.html
Normal 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 %}
|
||||||
|
|
||||||
169
AstraOS/Frontend/HTML/dashboard.html
Normal file
169
AstraOS/Frontend/HTML/dashboard.html
Normal 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 %}
|
||||||
135
AstraOS/Frontend/HTML/discord_bots.html
Normal file
135
AstraOS/Frontend/HTML/discord_bots.html
Normal 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 %}
|
||||||
112
AstraOS/Frontend/HTML/ip_analyzer.html
Normal file
112
AstraOS/Frontend/HTML/ip_analyzer.html
Normal 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 %}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
58
AstraOS/Frontend/HTML/maintenance.html
Normal file
58
AstraOS/Frontend/HTML/maintenance.html
Normal 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>
|
||||||
148
AstraOS/Frontend/HTML/maintenance_login.html
Normal file
148
AstraOS/Frontend/HTML/maintenance_login.html
Normal 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>
|
||||||
80
AstraOS/Frontend/HTML/maintenance_management.html
Normal file
80
AstraOS/Frontend/HTML/maintenance_management.html
Normal 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 %}
|
||||||
113
AstraOS/Frontend/HTML/role_management.html
Normal file
113
AstraOS/Frontend/HTML/role_management.html
Normal 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 %}
|
||||||
325
AstraOS/Frontend/HTML/sidebar.html
Normal file
325
AstraOS/Frontend/HTML/sidebar.html
Normal 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>
|
||||||
|
© 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>
|
||||||
79
AstraOS/Frontend/HTML/status_page.html
Normal file
79
AstraOS/Frontend/HTML/status_page.html
Normal 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 %}
|
||||||
98
AstraOS/Frontend/HTML/user_management.html
Normal file
98
AstraOS/Frontend/HTML/user_management.html
Normal 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 %}
|
||||||
75
AstraOS/Frontend/JAVASCRIPT/main.js
Normal file
75
AstraOS/Frontend/JAVASCRIPT/main.js
Normal 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in a new issue