added admin functions

This commit is contained in:
oberon 2026-02-11 19:35:42 +01:00
parent aa5b835e44
commit 0a63ceca7f
4 changed files with 227 additions and 17 deletions

70
app.py
View File

@ -76,13 +76,81 @@ def control_page():
return render_template('control.html', config=current_config) return render_template('control.html', config=current_config)
# ── Admin ────────────────────────────────────────────────────────────────────
@app.route('/admin') @app.route('/admin')
def admin(): def admin():
"""Admin-Übersicht: Configs anzeigen, bearbeiten, Logs etc."""
configs = load_configs() configs = load_configs()
return render_template('admin.html', configs=configs) return render_template('admin.html', configs=configs)
@app.route('/admin/edit/<filename>', methods=['GET', 'POST'])
def admin_edit_config(filename):
path = os.path.join(app.config['CONFIG_DIR'], filename)
if request.method == 'POST':
try:
new_content = request.form.get('config_content')
if not new_content:
raise ValueError("Kein Inhalt übermittelt")
# Validierung: versuche zu parsen
json.loads(new_content)
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
logger.info(f"Config {filename} erfolgreich gespeichert")
return redirect(url_for('admin'))
except json.JSONDecodeError as e:
logger.error(f"Ungültiges JSON in {filename}: {e}")
error_msg = f"Ungültiges JSON: {str(e)}"
except Exception as e:
logger.error(f"Speicherfehler {filename}: {e}")
error_msg = f"Fehler beim Speichern: {str(e)}"
# Bei Fehler: zurück zum Formular mit Fehlermeldung
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
return render_template('admin_edit.html', filename=filename, content=content, error=error_msg)
# GET: Formular laden
if not os.path.exists(path):
return "Konfiguration nicht gefunden", 404
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
return render_template('admin_edit.html', filename=filename, content=content)
@app.route('/admin/delete/<filename>', methods=['POST'])
def admin_delete_config(filename):
path = os.path.join(app.config['CONFIG_DIR'], filename)
if os.path.exists(path):
try:
os.remove(path)
logger.info(f"Config gelöscht: {filename}")
return jsonify({"success": True, "message": f"{filename} gelöscht"})
except Exception as e:
logger.error(f"Löschfehler {filename}: {e}")
return jsonify({"success": False, "message": str(e)}), 500
return jsonify({"success": False, "message": "Datei nicht gefunden"}), 404
@app.route('/admin/logs')
def admin_logs():
log_path = 'app.log'
if os.path.exists(log_path):
with open(log_path, 'r', encoding='utf-8') as f:
logs = f.read()
else:
logs = "Keine Logs vorhanden."
return render_template('admin_logs.html', logs=logs)
@app.route('/configs/<path:filename>') @app.route('/configs/<path:filename>')
def serve_config_file(filename): def serve_config_file(filename):
return send_from_directory(app.config['CONFIG_DIR'], filename) return send_from_directory(app.config['CONFIG_DIR'], filename)

View File

@ -4,27 +4,108 @@
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-5">
<h1>Admin-Bereich</h1> <h1 class="mb-4">Admin-Bereich</h1>
<p class="lead mb-4">Konfigurationen verwalten, Logs einsehen, etc.</p>
<div class="row mb-4">
<div class="col">
<a href="{{ url_for('admin_logs') }}" class="btn btn-outline-info">
<i class="bi bi-journal-text me-2"></i>Logs ansehen
</a>
</div>
<div class="col text-end">
<a href="#" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#newConfigModal">
<i class="bi bi-plus-circle me-2"></i>Neue Konfiguration
</a>
</div>
</div>
<h3 class="mt-5 mb-3">Vorhandene Konfigurationen</h3>
<h3>Vorhandene Konfigurationen</h3>
{% if configs %} {% if configs %}
<ul class="list-group"> <div class="table-responsive">
{% for cfg in configs %} <table class="table table-hover align-middle">
<li class="list-group-item d-flex justify-content-between align-items-center"> <thead class="table-dark">
{{ cfg.name }} ({{ cfg.filename }}) <tr>
<span> <th>Name</th>
<a href="#" class="btn btn-sm btn-outline-primary">Bearbeiten</a> <th>Hub-ID</th>
</span> <th>Datei</th>
</li> <th>Aktionen</th>
{% endfor %} </tr>
</ul> </thead>
<tbody>
{% for cfg in configs %}
<tr>
<td>{{ cfg.name }}</td>
<td>{{ cfg.hub_id | default('') }}</td>
<td><code>{{ cfg.filename }}</code></td>
<td>
<a href="{{ url_for('admin_edit_config', filename=cfg.filename) }}"
class="btn btn-sm btn-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-sm btn-danger delete-config-btn"
data-filename="{{ cfg.filename }}"
data-name="{{ cfg.name }}">
<i class="bi bi-trash"></i> Löschen
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %} {% else %}
<div class="alert alert-info">Noch keine Konfigurationen vorhanden.</div> <div class="alert alert-info">
Noch keine Konfigurationen vorhanden. Erstelle eine neue.
</div>
{% endif %} {% endif %}
</div>
<div class="mt-5"> <!-- Modal für neue Config (Platzhalter später ausbauen) -->
<a href="{{ url_for('index') }}" class="btn btn-secondary">Zurück zur Übersicht</a> <div class="modal fade" id="newConfigModal" tabindex="-1" aria-labelledby="newConfigModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newConfigModalLabel">Neue Konfiguration anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Hier könnte später ein Formular kommen (Name, Hub-ID, Kanäle...).<br>
Für den Moment: Erstelle einfach eine .json-Datei im <code>configs/</code>-Ordner.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.delete-config-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const filename = btn.dataset.filename;
const name = btn.dataset.name;
if (!confirm(`Sicher löschen: ${name} (${filename}) ?`)) return;
try {
const res = await fetch(`/admin/delete/${filename}`, { method: 'POST' });
const data = await res.json();
if (data.success) {
alert('Gelöscht');
location.reload();
} else {
alert('Fehler: ' + data.message);
}
} catch (err) {
alert('Netzwerkfehler beim Löschen');
}
});
});
});
</script>
{% endblock %} {% endblock %}

44
templates/admin_edit.html Normal file
View File

@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Bearbeite {{ filename }}{% endblock %}
{% block content %}
<div class="container my-5">
<h1>Bearbeite Konfiguration</h1>
<h4 class="text-muted mb-4">{{ filename }}</h4>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<form method="POST">
<div class="mb-3">
<label for="config_content" class="form-label">JSON-Inhalt (achte auf gültiges Format):</label>
<textarea class="form-control font-monospace" id="config_content" name="config_content" rows="20">{{ content }}</textarea>
</div>
<div class="d-flex gap-3">
<button type="submit" class="btn btn-primary btn-lg px-5">Speichern</button>
<a href="{{ url_for('admin') }}" class="btn btn-outline-secondary btn-lg px-5">Abbrechen</a>
</div>
</form>
<div class="mt-5">
<h5>Hilfe Beispielstruktur</h5>
<pre class="bg-light p-3 rounded">
{
"name": "BR 01 Dampflok",
"image": "br01.jpg",
"hub_id": 0,
"hub_type": "6channel",
"channels": [
{"port": "A", "type": "motor", "name": "Fahrtrichtung", "invert": false},
{"port": "C", "type": "light", "name": "Spitzenlicht", "on_value": 1.0}
]
}
</pre>
</div>
</div>
{% endblock %}

17
templates/admin_logs.html Normal file
View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}Logs MK Control{% endblock %}
{% block content %}
<div class="container my-5">
<h1>Log-Datei (app.log)</h1>
<pre class="bg-dark text-light p-4 rounded" style="max-height: 70vh; overflow-y: auto; font-size: 0.9rem;">
{{ logs | safe }}
</pre>
<div class="mt-4">
<a href="{{ url_for('admin') }}" class="btn btn-secondary">Zurück zum Admin</a>
</div>
</div>
{% endblock %}