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>
<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 %}">
data-open="{% if active_page in ['discord_bots', 'ets2_dlcs'] %}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">
@ -257,6 +257,14 @@
🤖 Discord Bots
</a>
{% 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>

View file

@ -7,7 +7,7 @@ from datetime import timedelta, datetime
from functools import wraps
from bson.objectid import ObjectId
import paramiko
from flask import jsonify
from flask import jsonify, send_file, after_this_request
import logging
from flask_socketio import SocketIO, emit, join_room, leave_room
import subprocess
@ -17,6 +17,8 @@ import requests
import ipaddress
import socket
import ssl
import zipfile
import tempfile
from dotenv import load_dotenv
load_dotenv()
async_mode = "eventlet"
@ -68,6 +70,7 @@ try:
roles_collection = db["roles"]
dashboard_collection = db["dashboard_stats"]
discord_bots_collection = db["discord_bots_data"]
ets2_dlcs_collection = db["ets2_dlcs_packages"]
ip_analyzer_results_collection = db["ip_analyzer_results"]
age_verification_collection = db["age_verification"]
print("INFO: MongoDB-Verbindung erfolgreich hergestellt.")
@ -81,6 +84,8 @@ try:
"maintenance_management",
"bypass_maintenance",
"manage_discord_bots",
"view_ets2_dlcs",
"manage_ets2_dlcs",
"view_ip_analyzer",
"view_age_verification",
"manage_age_verification"
@ -100,6 +105,12 @@ try:
}
])
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:
print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...")
owner_role = roles_collection.find_one({"name": "Owner"})
@ -133,11 +144,13 @@ except Exception as e:
roles_collection = None
dashboard_collection = None
discord_bots_collection = None
ets2_dlcs_collection = None
ip_analyzer_results_collection = None
print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}")
db = None
accounts_collection = None
logs_collection = None
ets2_dlcs_collection = None
def _venv_paths():
venv_bin = "/home/astraos/Astra-OS-Panel/venv/bin"
@ -973,6 +986,152 @@ def logs():
total_pages=total_pages,
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_to_socket(pipe, log_type):
try: