AstraOS Ver. 1.21:

Update:
ETS2 DLCs management hinzugefügt
This commit is contained in:
MrWhiff 2026-04-19 12:14:12 +02:00
parent d89f4731e9
commit 1174c1b7d0
3 changed files with 259 additions and 2 deletions

View file

@ -0,0 +1,90 @@
{% extends "sidebar.html" %}
{% block title %}AstraOS - ETS2 DLCs{% endblock %}
{% block content %}
<div x-data="{ showDeleteModal: false, packageToDelete: null }" class="space-y-10">
<header class="flex justify-between items-center gap-4">
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">ETS2 DLCs</h2>
</header>
<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 class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">DLC Datum</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Ordnerpfad</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Erstellt von</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-300 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/30{% endif %}">
{% for package in dlc_packages %}
<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">{{ package.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ package.dlc_date }}</td>
<td class="px-6 py-4 text-sm text-gray-300 break-all">{{ package.folder_path }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ package.created_by or 'N/A' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ url_for('download_ets2_dlc_package', package_id=package._id) }}" class="px-3 py-2 text-sm 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 %}">
Download
</a>
{% if can_manage_ets2_dlcs %}
<button @click="packageToDelete = '{{ package._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">
Loschen
</button>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="px-6 py-8 text-center text-gray-400">Noch keine DLC-Pakete vorhanden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if can_manage_ets2_dlcs %}
<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 %}">Neues DLC-Paket erstellen</h3>
<form action="{{ url_for('create_ets2_dlc_package') }}" method="POST" class="space-y-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="package_name" class="block text-sm font-medium text-gray-300">Paketname</label>
<input type="text" name="package_name" id="package_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="folder_path" class="block text-sm font-medium text-gray-300">Ordnerpfad</label>
<input type="text" name="folder_path" id="folder_path" required placeholder="z.B. C:\\ETS2\\DLCs\\Package1" 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="dlc_date" class="block text-sm font-medium text-gray-300">DLC Datum</label>
<input type="date" name="dlc_date" id="dlc_date" 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 class="flex justify-end">
<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 %}">
Paket erstellen
</button>
</div>
</form>
</div>
{% endif %}
<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">DLC-Paket loschen</h3>
<p class="text-gray-300 mb-6">Mochtest du dieses DLC-Paket wirklich loschen? Der Eintrag wird entfernt.</p>
<form :action="'/ets2_dlcs/delete/' + packageToDelete" 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">Loschen</button>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -239,7 +239,7 @@
<div> <div>
<button class="w-full text-left px-3 py-2 rounded-md transition flex justify-between items-center collapsible <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 %}" {% 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 %}"> data-open="{% if active_page in ['discord_bots', 'ets2_dlcs'] %}true{% else %}false{% endif %}">
<span>Private Tools</span> <span>Private Tools</span>
<svg class="w-4 h-4 transform transition-transform" <svg class="w-4 h-4 transform transition-transform"
fill="none" stroke="currentColor" viewBox="0 0 24 24"> fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -257,6 +257,14 @@
🤖 Discord Bots 🤖 Discord Bots
</a> </a>
{% endif %} {% endif %}
{% if 'view_ets2_dlcs' in session.get('permissions', []) %}
<a href="{{ url_for('ets2_dlcs') }}"
class="block px-3 py-1 rounded-md transition
{% if active_page == 'ets2_dlcs' %}{% 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 %}">
🚚 ETS2 DLCs
</a>
{% endif %}
</div> </div>
</div> </div>

View file

@ -7,7 +7,7 @@ from datetime import timedelta, datetime
from functools import wraps from functools import wraps
from bson.objectid import ObjectId from bson.objectid import ObjectId
import paramiko import paramiko
from flask import jsonify from flask import jsonify, send_file, after_this_request
import logging import logging
from flask_socketio import SocketIO, emit, join_room, leave_room from flask_socketio import SocketIO, emit, join_room, leave_room
import subprocess import subprocess
@ -17,6 +17,8 @@ import requests
import ipaddress import ipaddress
import socket import socket
import ssl import ssl
import zipfile
import tempfile
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
async_mode = "eventlet" async_mode = "eventlet"
@ -68,6 +70,7 @@ try:
roles_collection = db["roles"] roles_collection = db["roles"]
dashboard_collection = db["dashboard_stats"] dashboard_collection = db["dashboard_stats"]
discord_bots_collection = db["discord_bots_data"] discord_bots_collection = db["discord_bots_data"]
ets2_dlcs_collection = db["ets2_dlcs_packages"]
ip_analyzer_results_collection = db["ip_analyzer_results"] ip_analyzer_results_collection = db["ip_analyzer_results"]
age_verification_collection = db["age_verification"] age_verification_collection = db["age_verification"]
print("INFO: MongoDB-Verbindung erfolgreich hergestellt.") print("INFO: MongoDB-Verbindung erfolgreich hergestellt.")
@ -81,6 +84,8 @@ try:
"maintenance_management", "maintenance_management",
"bypass_maintenance", "bypass_maintenance",
"manage_discord_bots", "manage_discord_bots",
"view_ets2_dlcs",
"manage_ets2_dlcs",
"view_ip_analyzer", "view_ip_analyzer",
"view_age_verification", "view_age_verification",
"manage_age_verification" "manage_age_verification"
@ -100,6 +105,12 @@ try:
} }
]) ])
print("INFO: Standard-Rollen ('Owner', 'Benutzer') wurden erstellt.") print("INFO: Standard-Rollen ('Owner', 'Benutzer') wurden erstellt.")
else:
# Bestehende Owner-Rollen auf neue Permissions anheben.
roles_collection.update_many(
{"name": "Owner"},
{"$addToSet": {"permissions": {"$each": AVAILABLE_PERMISSIONS}}}
)
if accounts_collection.count_documents({}) == 0: if accounts_collection.count_documents({}) == 0:
print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...") print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...")
owner_role = roles_collection.find_one({"name": "Owner"}) owner_role = roles_collection.find_one({"name": "Owner"})
@ -133,11 +144,13 @@ except Exception as e:
roles_collection = None roles_collection = None
dashboard_collection = None dashboard_collection = None
discord_bots_collection = None discord_bots_collection = None
ets2_dlcs_collection = None
ip_analyzer_results_collection = None ip_analyzer_results_collection = None
print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}") print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
db = None db = None
accounts_collection = None accounts_collection = None
logs_collection = None logs_collection = None
ets2_dlcs_collection = None
def _venv_paths(): def _venv_paths():
venv_bin = "/home/astraos/Astra-OS-Panel/venv/bin" venv_bin = "/home/astraos/Astra-OS-Panel/venv/bin"
@ -973,6 +986,152 @@ def logs():
total_pages=total_pages, total_pages=total_pages,
active_page='logs') active_page='logs')
@app.route('/ets2_dlcs')
@login_required
@permission_required('view_ets2_dlcs')
def ets2_dlcs():
if ets2_dlcs_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('dashboard'))
packages = list(ets2_dlcs_collection.find().sort('created_at', -1))
return render_template(
'ets2_dlcs.html',
active_page='ets2_dlcs',
dlc_packages=packages,
can_manage_ets2_dlcs='manage_ets2_dlcs' in session.get('permissions', [])
)
@app.route('/ets2_dlcs/create', methods=['POST'])
@login_required
@permission_required('manage_ets2_dlcs')
def create_ets2_dlc_package():
if ets2_dlcs_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('ets2_dlcs'))
package_name = (request.form.get('package_name') or '').strip()
folder_path_input = (request.form.get('folder_path') or '').strip()
dlc_date = (request.form.get('dlc_date') or '').strip()
if not package_name or not folder_path_input or not dlc_date:
flash("Name, Ordnerpfad und Datum sind erforderlich.", "danger")
return redirect(url_for('ets2_dlcs'))
try:
datetime.strptime(dlc_date, '%Y-%m-%d')
except ValueError:
flash("Ungültiges Datumsformat. Bitte YYYY-MM-DD verwenden.", "danger")
return redirect(url_for('ets2_dlcs'))
folder_path = os.path.abspath(folder_path_input)
if not os.path.isdir(folder_path):
flash("Der angegebene Ordnerpfad existiert nicht oder ist kein Ordner.", "danger")
return redirect(url_for('ets2_dlcs'))
if ets2_dlcs_collection.find_one({'name': package_name}):
flash("Ein DLC-Paket mit diesem Namen existiert bereits.", "danger")
return redirect(url_for('ets2_dlcs'))
ets2_dlcs_collection.insert_one({
'name': package_name,
'folder_path': folder_path,
'dlc_date': dlc_date,
'created_by': session.get('username'),
'created_at': datetime.utcnow()
})
add_log("ETS2 DLC-Paket erstellt", f"Name: {package_name}, Pfad: {folder_path}")
flash("DLC-Paket wurde erfolgreich erstellt.", "success")
return redirect(url_for('ets2_dlcs'))
@app.route('/ets2_dlcs/delete/<package_id>', methods=['POST'])
@login_required
@permission_required('manage_ets2_dlcs')
def delete_ets2_dlc_package(package_id):
if ets2_dlcs_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('ets2_dlcs'))
try:
package_object_id = ObjectId(package_id)
except Exception:
flash("Ungültige Paket-ID.", "danger")
return redirect(url_for('ets2_dlcs'))
package = ets2_dlcs_collection.find_one({'_id': package_object_id})
if not package:
flash("DLC-Paket nicht gefunden.", "danger")
return redirect(url_for('ets2_dlcs'))
ets2_dlcs_collection.delete_one({'_id': package_object_id})
add_log("ETS2 DLC-Paket gelöscht", f"Name: {package.get('name', 'N/A')}")
flash("DLC-Paket wurde gelöscht.", "success")
return redirect(url_for('ets2_dlcs'))
@app.route('/ets2_dlcs/download/<package_id>')
@login_required
@permission_required('view_ets2_dlcs')
def download_ets2_dlc_package(package_id):
if ets2_dlcs_collection is None:
flash("Datenbankverbindung nicht verfügbar.", "danger")
return redirect(url_for('ets2_dlcs'))
try:
package_object_id = ObjectId(package_id)
except Exception:
flash("Ungültige Paket-ID.", "danger")
return redirect(url_for('ets2_dlcs'))
package = ets2_dlcs_collection.find_one({'_id': package_object_id})
if not package:
flash("DLC-Paket nicht gefunden.", "danger")
return redirect(url_for('ets2_dlcs'))
folder_path = package.get('folder_path')
if not folder_path or not os.path.isdir(folder_path):
flash("Der gespeicherte Ordnerpfad ist ungültig oder nicht mehr vorhanden.", "danger")
return redirect(url_for('ets2_dlcs'))
temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
temp_zip_path = temp_zip.name
temp_zip.close()
try:
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
has_content = False
for root, _, files in os.walk(folder_path):
for filename in files:
has_content = True
file_path = os.path.join(root, filename)
arcname = os.path.relpath(file_path, folder_path)
zipf.write(file_path, arcname)
if not has_content:
zipf.writestr('EMPTY_PACKAGE.txt', 'Dieses DLC-Paket enthaelt aktuell keine Dateien.')
except Exception as e:
try:
os.remove(temp_zip_path)
except Exception:
pass
flash(f"Fehler beim Erstellen des Downloads: {e}", "danger")
return redirect(url_for('ets2_dlcs'))
@after_this_request
def cleanup_temp_file(response):
try:
os.remove(temp_zip_path)
except Exception:
pass
return response
safe_name = secure_filename(package.get('name', 'ets2_dlc_package')) or 'ets2_dlc_package'
add_log("ETS2 DLC-Paket heruntergeladen", f"Name: {package.get('name', 'N/A')}")
return send_file(temp_zip_path, as_attachment=True, download_name=f"{safe_name}.zip")
def stream_bot_output(bot_id, process): def stream_bot_output(bot_id, process):
def stream_to_socket(pipe, log_type): def stream_to_socket(pipe, log_type):
try: try: