")
+@admin_required
+def view_group(group_id):
+ """Site-Admin wechselt temporär in eine Gruppe, um deren MC-Daten zu sehen."""
+ group = db.get_group_by_id(group_id)
+ if not group:
+ flash("Gruppe nicht gefunden.", "danger")
+ return redirect(url_for("site_admin.dashboard"))
+ if not db.has_db_configured(group_id):
+ flash("Für diese Gruppe ist noch keine Datenbank konfiguriert.", "warning")
+ return redirect(url_for("site_admin.dashboard"))
+ # Alle Berechtigungen als Admin
+ all_perms = {k: True for k in ["view_dashboard","view_players","view_sessions",
+ "view_chat","view_commands","view_deaths","view_blocks",
+ "view_proxy","view_server_events","view_perms"]}
+ session["group_id"] = group_id
+ session["group_name"] = group["name"]
+ session["role"] = "admin"
+ session["permissions"] = all_perms
+ session["admin_viewing"] = True
+ return redirect(url_for("panel.dashboard"))
+
+
+@site_admin.route("/stop-view")
+@admin_required
+def stop_view():
+ """Kehrt zum Site-Admin-Dashboard zurück."""
+ session.pop("group_id", None)
+ session.pop("group_name", None)
+ session.pop("role", None)
+ session.pop("permissions", None)
+ session.pop("admin_viewing", None)
+ return redirect(url_for("site_admin.dashboard"))
diff --git a/web/config.py b/web/config.py
new file mode 100644
index 0000000..ea35334
--- /dev/null
+++ b/web/config.py
@@ -0,0 +1,46 @@
+"""
+MCLogger – Konfiguration
+Alle Einstellungen über ENV-Variablen (Coolify-kompatibel).
+"""
+import os
+
+
+class Config:
+ # ── Flask ──────────────────────────────────────────────────
+ SECRET_KEY = os.getenv("SECRET_KEY", "change-me-use-a-long-random-string-min-32-chars")
+ HOST = os.getenv("HOST", "0.0.0.0")
+ PORT = int(os.getenv("PORT", "5000"))
+ DEBUG = os.getenv("DEBUG", "false").lower() == "true"
+
+ # ── Panel-Datenbank (Nutzer, Gruppen, Mitgliedschaften) ────
+ PANEL_DB_HOST = os.getenv("PANEL_DB_HOST", "localhost")
+ PANEL_DB_PORT = int(os.getenv("PANEL_DB_PORT", "3306"))
+ PANEL_DB_USER = os.getenv("PANEL_DB_USER", "root")
+ PANEL_DB_PASSWORD = os.getenv("PANEL_DB_PASSWORD", "")
+ PANEL_DB_NAME = os.getenv("PANEL_DB_NAME", "mclogger_panel")
+
+ # ── Credentials-Datenbank (verschlüsselte MC-DB-Zugangsdaten) ──
+ CREDS_DB_HOST = os.getenv("CREDS_DB_HOST", os.getenv("PANEL_DB_HOST", "localhost"))
+ CREDS_DB_PORT = int(os.getenv("CREDS_DB_PORT", os.getenv("PANEL_DB_PORT", "3306")))
+ CREDS_DB_USER = os.getenv("CREDS_DB_USER", os.getenv("PANEL_DB_USER", "root"))
+ CREDS_DB_PASSWORD = os.getenv("CREDS_DB_PASSWORD", os.getenv("PANEL_DB_PASSWORD", ""))
+ CREDS_DB_NAME = os.getenv("CREDS_DB_NAME", "mclogger_creds")
+
+ # ── Sicherheit ────────────────────────────────────────────
+ PASSWORD_PEPPER = os.getenv("PASSWORD_PEPPER", "change-me-global-pepper-secret-never-change")
+ # Generieren: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
+ FERNET_KEY = os.getenv("FERNET_KEY", "")
+
+ # ── Standard-Berechtigungen neuer Gruppenmitglieder ───────
+ DEFAULT_PERMISSIONS = {
+ "view_dashboard": True,
+ "view_players": True,
+ "view_sessions": True,
+ "view_chat": True,
+ "view_commands": True,
+ "view_deaths": True,
+ "view_blocks": True,
+ "view_proxy": False,
+ "view_server_events": False,
+ "view_perms": False,
+ }
diff --git a/web/crypto.py b/web/crypto.py
new file mode 100644
index 0000000..c807d35
--- /dev/null
+++ b/web/crypto.py
@@ -0,0 +1,63 @@
+"""
+MCLogger – Kryptographie-Utilities
+- Passwort-Hashing: PBKDF2-HMAC-SHA256 mit Salt (pro Nutzer) + Pepper (global, via ENV)
+- DB-Credential-Verschlüsselung: Fernet (symmetrisch, Schlüssel via ENV)
+"""
+import hashlib
+import os
+from cryptography.fernet import Fernet
+from config import Config
+
+
+# ─────────────────────────────────────────────────────────────
+# Passwort-Hashing
+# ─────────────────────────────────────────────────────────────
+
+def generate_salt() -> str:
+ """Generiert einen zufälligen 32-Byte Hex-Salt."""
+ return os.urandom(32).hex()
+
+
+def hash_password(password: str, salt: str) -> str:
+ """
+ Hasht ein Passwort mit PBKDF2-HMAC-SHA256.
+ Verwendet: salt (pro Nutzer) + pepper (global aus ENV)
+ """
+ dk = hashlib.pbkdf2_hmac(
+ "sha256",
+ password.encode("utf-8"),
+ (salt + Config.PASSWORD_PEPPER).encode("utf-8"),
+ iterations=260_000,
+ )
+ return dk.hex()
+
+
+def verify_password(password: str, salt: str, stored_hash: str) -> bool:
+ """Prüft ob ein Passwort korrekt ist."""
+ return hash_password(password, salt) == stored_hash
+
+
+# ─────────────────────────────────────────────────────────────
+# Fernet-Verschlüsselung (für DB-Zugangsdaten)
+# ─────────────────────────────────────────────────────────────
+
+def _get_fernet() -> Fernet:
+ key = Config.FERNET_KEY
+ if not key:
+ raise RuntimeError(
+ "FERNET_KEY ist nicht gesetzt! "
+ "Generieren: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
+ )
+ if isinstance(key, str):
+ key = key.encode()
+ return Fernet(key)
+
+
+def encrypt_str(plaintext: str) -> str:
+ """Verschlüsselt einen String mit Fernet."""
+ return _get_fernet().encrypt(plaintext.encode("utf-8")).decode("utf-8")
+
+
+def decrypt_str(ciphertext: str) -> str:
+ """Entschlüsselt einen Fernet-verschlüsselten String."""
+ return _get_fernet().decrypt(ciphertext.encode("utf-8")).decode("utf-8")
diff --git a/web/docker-compose.yml b/web/docker-compose.yml
new file mode 100644
index 0000000..209a26d
--- /dev/null
+++ b/web/docker-compose.yml
@@ -0,0 +1,46 @@
+version: "3.9"
+
+services:
+ mclogger-panel:
+ build: .
+ image: mclogger-panel:latest
+ container_name: mclogger-panel
+ restart: unless-stopped
+ ports:
+ - "${PORT:-5000}:5000"
+ environment:
+ # ── Flask ──────────────────────────────────────────────
+ SECRET_KEY: "${SECRET_KEY}"
+
+ # ── Panel-Datenbank (Benutzer / Gruppen) ───────────────
+ PANEL_DB_HOST: "${PANEL_DB_HOST:-localhost}"
+ PANEL_DB_PORT: "${PANEL_DB_PORT:-3306}"
+ PANEL_DB_USER: "${PANEL_DB_USER:-mclogger_panel}"
+ PANEL_DB_PASSWORD: "${PANEL_DB_PASSWORD}"
+ PANEL_DB_NAME: "${PANEL_DB_NAME:-mclogger_panel}"
+
+ # ── Credentials-Datenbank (verschlüsselte MC-DB-Daten) ─
+ CREDS_DB_HOST: "${CREDS_DB_HOST:-localhost}"
+ CREDS_DB_PORT: "${CREDS_DB_PORT:-3306}"
+ CREDS_DB_USER: "${CREDS_DB_USER:-mclogger_creds}"
+ CREDS_DB_PASSWORD: "${CREDS_DB_PASSWORD}"
+ CREDS_DB_NAME: "${CREDS_DB_NAME:-mclogger_creds}"
+
+ # ── Sicherheit ──────────────────────────────────────────
+ # Fernet-Schlüssel (32 URL-safe base64 bytes):
+ # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
+ FERNET_KEY: "${FERNET_KEY}"
+ # Pepper für Passwort-Hashing (beliebige lange Zeichenkette)
+ PASSWORD_PEPPER: "${PASSWORD_PEPPER}"
+
+ # ── Server ──────────────────────────────────────────────
+ HOST: "0.0.0.0"
+ PORT: "5000"
+ DEBUG: "${DEBUG:-false}"
+
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:5000/login"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 15s
diff --git a/web/panel_db.py b/web/panel_db.py
new file mode 100644
index 0000000..9d11e0f
--- /dev/null
+++ b/web/panel_db.py
@@ -0,0 +1,349 @@
+"""
+MCLogger – Panel-Datenbank-Operationen
+Verwaltet Nutzer, Gruppen, Mitgliedschaften (PANEL_DB)
+und verschlüsselte MC-DB-Zugangsdaten (CREDS_DB).
+"""
+import json
+import pymysql
+import pymysql.cursors
+from config import Config
+from crypto import generate_salt, hash_password, verify_password, encrypt_str, decrypt_str
+
+
+# ─────────────────────────────────────────────────────────────
+# Datenbankverbindungen
+# ─────────────────────────────────────────────────────────────
+
+def get_panel_db():
+ return pymysql.connect(
+ host=Config.PANEL_DB_HOST,
+ port=Config.PANEL_DB_PORT,
+ user=Config.PANEL_DB_USER,
+ password=Config.PANEL_DB_PASSWORD,
+ database=Config.PANEL_DB_NAME,
+ charset="utf8mb4",
+ cursorclass=pymysql.cursors.DictCursor,
+ autocommit=True,
+ )
+
+
+def get_creds_db():
+ return pymysql.connect(
+ host=Config.CREDS_DB_HOST,
+ port=Config.CREDS_DB_PORT,
+ user=Config.CREDS_DB_USER,
+ password=Config.CREDS_DB_PASSWORD,
+ database=Config.CREDS_DB_NAME,
+ charset="utf8mb4",
+ cursorclass=pymysql.cursors.DictCursor,
+ autocommit=True,
+ )
+
+
+def _panel_query(sql, args=None, fetchone=False, write=False):
+ conn = get_panel_db()
+ try:
+ with conn.cursor() as cur:
+ cur.execute(sql, args or ())
+ if write:
+ return cur.lastrowid
+ return cur.fetchone() if fetchone else cur.fetchall()
+ finally:
+ conn.close()
+
+
+def _creds_query(sql, args=None, fetchone=False, write=False):
+ conn = get_creds_db()
+ try:
+ with conn.cursor() as cur:
+ cur.execute(sql, args or ())
+ if write:
+ return cur.lastrowid
+ return cur.fetchone() if fetchone else cur.fetchall()
+ finally:
+ conn.close()
+
+
+# ─────────────────────────────────────────────────────────────
+# Initialisierung – Tabellen anlegen
+# ─────────────────────────────────────────────────────────────
+
+PANEL_SCHEMA = [
+ """CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(50) UNIQUE NOT NULL,
+ email VARCHAR(255) UNIQUE NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ salt VARCHAR(64) NOT NULL,
+ is_site_admin TINYINT(1) DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ last_login TIMESTAMP NULL
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
+
+ """CREATE TABLE IF NOT EXISTS user_groups (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(100) UNIQUE NOT NULL,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
+
+ """CREATE TABLE IF NOT EXISTS group_members (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ group_id INT NOT NULL,
+ role ENUM('admin','member') DEFAULT 'member',
+ permissions JSON,
+ joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uq_user_group (user_id, group_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
+]
+
+CREDS_SCHEMA = [
+ """CREATE TABLE IF NOT EXISTS group_databases (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ group_id INT UNIQUE NOT NULL,
+ enc_host TEXT NOT NULL,
+ enc_port TEXT NOT NULL,
+ enc_user TEXT NOT NULL,
+ enc_password TEXT NOT NULL,
+ enc_database TEXT NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
+]
+
+
+def init_databases():
+ """Erstellt alle benötigten Tabellen falls nicht vorhanden."""
+ panel = get_panel_db()
+ try:
+ with panel.cursor() as cur:
+ for stmt in PANEL_SCHEMA:
+ cur.execute(stmt)
+ finally:
+ panel.close()
+
+ creds = get_creds_db()
+ try:
+ with creds.cursor() as cur:
+ for stmt in CREDS_SCHEMA:
+ cur.execute(stmt)
+ finally:
+ creds.close()
+
+
+# ─────────────────────────────────────────────────────────────
+# Nutzer
+# ─────────────────────────────────────────────────────────────
+
+def create_user(username: str, email: str, password: str, is_site_admin: bool = False) -> int:
+ salt = generate_salt()
+ pw_hash = hash_password(password, salt)
+ return _panel_query(
+ "INSERT INTO users (username, email, password_hash, salt, is_site_admin) VALUES (%s,%s,%s,%s,%s)",
+ (username, email, pw_hash, salt, int(is_site_admin)), write=True
+ )
+
+
+def get_user_by_username(username: str):
+ return _panel_query("SELECT * FROM users WHERE username=%s", (username,), fetchone=True)
+
+
+def get_user_by_id(user_id: int):
+ return _panel_query("SELECT * FROM users WHERE id=%s", (user_id,), fetchone=True)
+
+
+def update_user(user_id: int, username: str, email: str, is_site_admin: bool):
+ _panel_query(
+ "UPDATE users SET username=%s, email=%s, is_site_admin=%s WHERE id=%s",
+ (username, email, int(is_site_admin), user_id), write=True
+ )
+
+
+def change_password(user_id: int, new_password: str):
+ salt = generate_salt()
+ pw_hash = hash_password(new_password, salt)
+ _panel_query(
+ "UPDATE users SET password_hash=%s, salt=%s WHERE id=%s",
+ (pw_hash, salt, user_id), write=True
+ )
+
+
+def delete_user(user_id: int):
+ _panel_query("DELETE FROM users WHERE id=%s", (user_id,), write=True)
+
+
+def check_login(username: str, password: str):
+ """Prüft Anmeldedaten. Gibt den Nutzer zurück oder None."""
+ user = get_user_by_username(username)
+ if not user:
+ return None
+ if not verify_password(password, user["salt"], user["password_hash"]):
+ return None
+ _panel_query("UPDATE users SET last_login=NOW() WHERE id=%s", (user["id"],), write=True)
+ return user
+
+
+def list_all_users():
+ return _panel_query(
+ "SELECT u.*, COUNT(gm.group_id) AS group_count "
+ "FROM users u LEFT JOIN group_members gm ON gm.user_id=u.id "
+ "GROUP BY u.id ORDER BY u.username"
+ )
+
+
+# ─────────────────────────────────────────────────────────────
+# Gruppen
+# ─────────────────────────────────────────────────────────────
+
+def create_group(name: str, description: str = "") -> int:
+ return _panel_query(
+ "INSERT INTO user_groups (name, description) VALUES (%s,%s)",
+ (name, description), write=True
+ )
+
+
+def get_group_by_id(group_id: int):
+ return _panel_query("SELECT * FROM user_groups WHERE id=%s", (group_id,), fetchone=True)
+
+
+def get_group_by_name(name: str):
+ return _panel_query("SELECT * FROM user_groups WHERE name=%s", (name,), fetchone=True)
+
+
+def update_group(group_id: int, name: str, description: str):
+ _panel_query(
+ "UPDATE user_groups SET name=%s, description=%s WHERE id=%s",
+ (name, description, group_id), write=True
+ )
+
+
+def delete_group(group_id: int):
+ _panel_query("DELETE FROM user_groups WHERE id=%s", (group_id,), write=True)
+
+
+def list_all_groups():
+ return _panel_query(
+ "SELECT g.*, COUNT(gm.user_id) AS member_count "
+ "FROM user_groups g LEFT JOIN group_members gm ON gm.group_id=g.id "
+ "GROUP BY g.id ORDER BY g.name"
+ )
+
+
+# ─────────────────────────────────────────────────────────────
+# Gruppenmitgliedschaften
+# ─────────────────────────────────────────────────────────────
+
+def get_user_groups(user_id: int):
+ return _panel_query(
+ "SELECT g.*, gm.role, gm.permissions "
+ "FROM user_groups g "
+ "JOIN group_members gm ON gm.group_id=g.id "
+ "WHERE gm.user_id=%s ORDER BY g.name",
+ (user_id,)
+ )
+
+
+def get_group_member(user_id: int, group_id: int):
+ return _panel_query(
+ "SELECT * FROM group_members WHERE user_id=%s AND group_id=%s",
+ (user_id, group_id), fetchone=True
+ )
+
+
+def get_group_members(group_id: int):
+ return _panel_query(
+ "SELECT u.id, u.username, u.email, u.last_login, gm.role, gm.permissions, gm.joined_at "
+ "FROM group_members gm "
+ "JOIN users u ON u.id=gm.user_id "
+ "WHERE gm.group_id=%s ORDER BY gm.role DESC, u.username",
+ (group_id,)
+ )
+
+
+def add_group_member(user_id: int, group_id: int, role: str = "member", permissions: dict = None):
+ if permissions is None:
+ permissions = Config.DEFAULT_PERMISSIONS
+ _panel_query(
+ "INSERT INTO group_members (user_id, group_id, role, permissions) VALUES (%s,%s,%s,%s) "
+ "ON DUPLICATE KEY UPDATE role=VALUES(role), permissions=VALUES(permissions)",
+ (user_id, group_id, role, json.dumps(permissions)), write=True
+ )
+
+
+def update_member(user_id: int, group_id: int, role: str, permissions: dict):
+ _panel_query(
+ "UPDATE group_members SET role=%s, permissions=%s WHERE user_id=%s AND group_id=%s",
+ (role, json.dumps(permissions), user_id, group_id), write=True
+ )
+
+
+def remove_group_member(user_id: int, group_id: int):
+ _panel_query(
+ "DELETE FROM group_members WHERE user_id=%s AND group_id=%s",
+ (user_id, group_id), write=True
+ )
+
+
+def get_permissions(user_id: int, group_id: int) -> dict:
+ """Gibt die Berechtigungen des Nutzers in der Gruppe zurück."""
+ member = get_group_member(user_id, group_id)
+ if not member:
+ return {}
+ raw = member.get("permissions")
+ if isinstance(raw, str):
+ return json.loads(raw)
+ if isinstance(raw, dict):
+ return raw
+ return {}
+
+
+# ─────────────────────────────────────────────────────────────
+# MC-Datenbank-Zugangsdaten (verschlüsselt)
+# ─────────────────────────────────────────────────────────────
+
+def set_group_db_creds(group_id: int, host: str, port: int, user: str, password: str, database: str):
+ """Verschlüsselt und speichert die MC-DB-Zugangsdaten einer Gruppe."""
+ _creds_query(
+ "INSERT INTO group_databases (group_id, enc_host, enc_port, enc_user, enc_password, enc_database) "
+ "VALUES (%s,%s,%s,%s,%s,%s) "
+ "ON DUPLICATE KEY UPDATE enc_host=VALUES(enc_host), enc_port=VALUES(enc_port), "
+ "enc_user=VALUES(enc_user), enc_password=VALUES(enc_password), enc_database=VALUES(enc_database)",
+ (group_id,
+ encrypt_str(host),
+ encrypt_str(str(port)),
+ encrypt_str(user),
+ encrypt_str(password),
+ encrypt_str(database)),
+ write=True
+ )
+
+
+def get_group_db_creds(group_id: int) -> dict | None:
+ """Gibt die entschlüsselten MC-DB-Zugangsdaten zurück oder None."""
+ row = _creds_query(
+ "SELECT * FROM group_databases WHERE group_id=%s",
+ (group_id,), fetchone=True
+ )
+ if not row:
+ return None
+ return {
+ "host": decrypt_str(row["enc_host"]),
+ "port": int(decrypt_str(row["enc_port"])),
+ "user": decrypt_str(row["enc_user"]),
+ "password": decrypt_str(row["enc_password"]),
+ "database": decrypt_str(row["enc_database"]),
+ }
+
+
+def delete_group_db_creds(group_id: int):
+ _creds_query("DELETE FROM group_databases WHERE group_id=%s", (group_id,), write=True)
+
+
+def has_db_configured(group_id: int) -> bool:
+ row = _creds_query(
+ "SELECT id FROM group_databases WHERE group_id=%s",
+ (group_id,), fetchone=True
+ )
+ return row is not None
diff --git a/web/requirements.txt b/web/requirements.txt
new file mode 100644
index 0000000..14cc2a3
--- /dev/null
+++ b/web/requirements.txt
@@ -0,0 +1,4 @@
+Flask==3.1.0
+PyMySQL==1.1.1
+cryptography==42.0.8
+gunicorn==22.0.0
diff --git a/web/static/css/style.css b/web/static/css/style.css
new file mode 100644
index 0000000..1753b98
--- /dev/null
+++ b/web/static/css/style.css
@@ -0,0 +1,230 @@
+/* ============================================================
+ MCLogger – Admin Interface CSS
+ Author: SimolZimol
+ ============================================================ */
+
+:root {
+ --sidebar-width: 230px;
+ --sidebar-bg: #0f1117;
+ --sidebar-border: #1e2230;
+ --topbar-bg: #13161f;
+ --content-bg: #181c27;
+ --card-bg: #1e2230;
+ --card-border: #2a2f42;
+ --text-muted-custom: #6b7280;
+ --accent-green: #1db954;
+}
+
+/* ── Layout ─────────────────────────────────────────────── */
+html, body {
+ height: 100%;
+ overflow: hidden;
+ background: var(--content-bg);
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ font-size: 14px;
+}
+
+#wrapper {
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* ── Sidebar ─────────────────────────────────────────────── */
+#sidebar {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ background: var(--sidebar-bg);
+ border-right: 1px solid var(--sidebar-border);
+ height: 100vh;
+ overflow-y: auto;
+ overflow-x: hidden;
+ transition: width .25s ease, min-width .25s ease;
+}
+
+#sidebar.collapsed {
+ width: 64px;
+ min-width: 64px;
+}
+
+#sidebar.collapsed .sidebar-brand div,
+#sidebar.collapsed .sidebar-brand small,
+#sidebar.collapsed .nav-link span {
+ display: none;
+}
+
+.sidebar-brand {
+ padding: .25rem 0;
+}
+
+#sidebar .nav-link {
+ color: #9ca3af;
+ border-radius: 6px;
+ padding: .45rem .75rem;
+ font-size: .875rem;
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ transition: background .15s, color .15s;
+ white-space: nowrap;
+}
+
+#sidebar .nav-link i {
+ font-size: 1rem;
+ flex-shrink: 0;
+}
+
+#sidebar .nav-link:hover { background: rgba(255,255,255,.05); color: #e5e7eb; }
+#sidebar .nav-link.active { background: rgba(29,185,84,.15); color: var(--accent-green); }
+
+/* ── Topbar ──────────────────────────────────────────────── */
+.topbar {
+ background: var(--topbar-bg);
+ border-bottom: 1px solid var(--sidebar-border);
+ height: 52px;
+ flex-shrink: 0;
+}
+
+/* ── Content ─────────────────────────────────────────────── */
+#page-content {
+ display: flex;
+ flex-direction: column;
+ background: var(--content-bg);
+ height: 100vh;
+}
+
+main {
+ flex: 1;
+ overflow-y: auto;
+}
+
+/* ── Cards ───────────────────────────────────────────────── */
+.card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 10px;
+}
+
+.card-header {
+ background: rgba(0,0,0,.2);
+ border-bottom: 1px solid var(--card-border);
+ font-size: .85rem;
+ font-weight: 600;
+ color: #d1d5db;
+}
+
+/* ── Statistik-Karten ────────────────────────────────────── */
+.stat-card { transition: transform .15s; cursor: default; }
+.stat-card:hover { transform: translateY(-2px); }
+
+.stat-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ font-size: 1.3rem;
+}
+
+.stat-value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1;
+ color: #f3f4f6;
+}
+
+.stat-label {
+ font-size: .72rem;
+ color: var(--text-muted-custom);
+ margin-top: 2px;
+}
+
+/* ── Tabellen ────────────────────────────────────────────── */
+.table {
+ color: #d1d5db;
+ font-size: .8rem;
+}
+
+.table > thead {
+ font-size: .75rem;
+ letter-spacing: .03em;
+ color: #9ca3af;
+}
+
+.table-hover > tbody > tr:hover > td {
+ background: rgba(255,255,255,.04);
+}
+
+.table-dark {
+ --bs-table-bg: rgba(0,0,0,.3);
+}
+
+/* ── Badges ──────────────────────────────────────────────── */
+.badge { font-size: .7rem; font-weight: 500; }
+
+/* ── Inputs ──────────────────────────────────────────────── */
+.form-control, .form-select {
+ background-color: #111827;
+ border-color: var(--card-border);
+ color: #e5e7eb;
+ font-size: .8rem;
+}
+
+.form-control:focus, .form-select:focus {
+ border-color: var(--accent-green);
+ box-shadow: 0 0 0 2px rgba(29,185,84,.25);
+ background-color: #111827;
+ color: #f9fafb;
+}
+
+.form-control::placeholder { color: #6b7280; }
+
+/* ── Buttons ─────────────────────────────────────────────── */
+.btn-success { background-color: var(--accent-green); border-color: var(--accent-green); }
+.btn-success:hover { background-color: #17a34a; border-color: #17a34a; }
+
+/* ── Pagination ──────────────────────────────────────────── */
+.page-link {
+ background-color: var(--card-bg);
+ border-color: var(--card-border);
+ color: #9ca3af;
+ font-size: .8rem;
+}
+.page-link:hover { background-color: rgba(255,255,255,.07); color: #f3f4f6; }
+.page-item.active .page-link { background-color: var(--accent-green); border-color: var(--accent-green); color: #000; }
+.page-item.disabled .page-link { background-color: transparent; }
+
+/* ── Login-Seite ─────────────────────────────────────────── */
+body .card.shadow-lg {
+ background: #1e2230;
+ border: 1px solid #2a2f42;
+}
+
+/* ── Scrollbars ──────────────────────────────────────────── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: #4b5563; }
+
+/* ── Diverse ─────────────────────────────────────────────── */
+.blink {
+ animation: blink-anim 1.5s infinite;
+}
+@keyframes blink-anim {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: .2; }
+}
+
+.font-monospace { font-family: 'Consolas', 'Cascadia Code', monospace !important; }
+
+.text-truncate { max-width: 250px; }
+
+/* Chart.js Canvas */
+canvas { max-height: 250px; }
+
+/* Alert */
+.alert { font-size: .85rem; }
+
+/* sticky-top in dark scrollable containers */
+.sticky-top { z-index: 1; }
diff --git a/web/static/js/main.js b/web/static/js/main.js
new file mode 100644
index 0000000..62d7483
--- /dev/null
+++ b/web/static/js/main.js
@@ -0,0 +1,97 @@
+/* ============================================================
+ MCLogger – main.js
+ Author: SimolZimol
+ ============================================================ */
+
+// ── Sidebar Toggle ────────────────────────────────────────
+document.addEventListener('DOMContentLoaded', () => {
+ const btn = document.getElementById('sidebarToggle');
+ const sidebar = document.getElementById('sidebar');
+
+ if (btn && sidebar) {
+ btn.addEventListener('click', () => {
+ sidebar.classList.toggle('collapsed');
+ localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
+ });
+
+ // Zustand beim Laden wiederherstellen
+ if (localStorage.getItem('sidebar-collapsed') === 'true') {
+ sidebar.classList.add('collapsed');
+ }
+ }
+
+ // Online-Count aktualisieren
+ updateOnlineCount();
+ setInterval(updateOnlineCount, 30_000);
+
+ // Tooltips initialisieren
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
+ new bootstrap.Tooltip(el);
+ });
+
+ // Automatisch Tabellen sortieren ermöglichen
+ initTableSort();
+});
+
+// ── Online-Count API ──────────────────────────────────────
+function updateOnlineCount() {
+ fetch('/api/online')
+ .then(r => r.json())
+ .then(data => {
+ const el = document.getElementById('online-count');
+ if (el) el.textContent = data.length;
+ })
+ .catch(() => {/* Ignorieren wenn nicht eingeloggt */});
+}
+
+// ── Einfache Tabellen-Sortierung ──────────────────────────
+function initTableSort() {
+ document.querySelectorAll('th[data-sort]').forEach(th => {
+ th.style.cursor = 'pointer';
+ th.addEventListener('click', () => {
+ const table = th.closest('table');
+ const idx = Array.from(th.parentNode.children).indexOf(th);
+ const asc = th.dataset.order !== 'asc';
+ th.dataset.order = asc ? 'asc' : 'desc';
+
+ const rows = Array.from(table.querySelectorAll('tbody tr'));
+ rows.sort((a, b) => {
+ const av = a.cells[idx]?.textContent.trim() ?? '';
+ const bv = b.cells[idx]?.textContent.trim() ?? '';
+ return asc ? av.localeCompare(bv, 'de', {numeric: true}) : bv.localeCompare(av, 'de', {numeric: true});
+ });
+ const tbody = table.querySelector('tbody');
+ rows.forEach(r => tbody.appendChild(r));
+ });
+ });
+}
+
+// ── Formatierung ──────────────────────────────────────────
+function fmtDuration(sec) {
+ sec = parseInt(sec) || 0;
+ const h = Math.floor(sec / 3600);
+ const m = Math.floor((sec % 3600) / 60);
+ const s = sec % 60;
+ if (h) return `${h}h ${m}m`;
+ if (m) return `${m}m ${s}s`;
+ return `${s}s`;
+}
+
+// ── Live-Chat-Reload (optional) ───────────────────────────
+if (window.location.pathname === '/chat') {
+ // Kein automatisches Reload im Chat (würde Filter zurücksetzen)
+}
+
+// ── Kopieren in Zwischenablage ────────────────────────────
+document.querySelectorAll('.copy-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const target = document.querySelector(btn.dataset.target);
+ if (target) {
+ navigator.clipboard.writeText(target.textContent.trim())
+ .then(() => {
+ btn.innerHTML = '';
+ setTimeout(() => btn.innerHTML = '', 1500);
+ });
+ }
+ });
+});
diff --git a/web/templates/_pagination.html b/web/templates/_pagination.html
new file mode 100644
index 0000000..eb6c1aa
--- /dev/null
+++ b/web/templates/_pagination.html
@@ -0,0 +1,22 @@
+{% if pages > 1 %}
+
+{% endif %}
diff --git a/web/templates/admin/base.html b/web/templates/admin/base.html
new file mode 100644
index 0000000..513b168
--- /dev/null
+++ b/web/templates/admin/base.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+ {% block title %}Site Admin{% endblock %} — MCLogger
+
+
+
+
+
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for cat, msg in messages %}
+
+ {{ msg }}
+
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+ {% block content %}{% endblock %}
+
+
+{% block scripts %}{% endblock %}
+
+
diff --git a/web/templates/admin/dashboard.html b/web/templates/admin/dashboard.html
new file mode 100644
index 0000000..b1db800
--- /dev/null
+++ b/web/templates/admin/dashboard.html
@@ -0,0 +1,121 @@
+{% extends "admin/base.html" %}
+{% block title %}Dashboard{% endblock %}
+{% block content %}
+Site Admin Dashboard
+
+
+
+
+
+
{{ stats.group_count }}
+
Gruppen
+
+
+
+
+
+
+
{{ stats.user_count }}
+
Benutzer
+
+
+
+
+
+
+
{{ stats.db_configured }}
+
DBs konfiguriert
+
+
+
+
+
+
+
{{ stats.admin_count }}
+
Site Admins
+
+
+
+
+
+
+
+
+
+
+
+ | Name | Mitglieder | DB | |
+
+ {% for g in groups %}
+
+ | {{ g.name }} |
+ {{ g.member_count }} |
+
+ {% if g.has_db %}
+ Konfiguriert
+ {% else %}
+ Keine
+ {% endif %}
+ |
+
+
+
+
+
+
+
+ |
+
+ {% else %}
+ | Keine Gruppen vorhanden |
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+ | Benutzer | Gruppen | Admin | |
+
+ {% for u in users %}
+
+ | {{ u.username }} |
+ {{ u.group_count }} |
+ {% if u.is_site_admin %}{% endif %} |
+
+
+
+
+ |
+
+ {% else %}
+ | Keine Benutzer vorhanden |
+ {% endfor %}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/admin/group_edit.html b/web/templates/admin/group_edit.html
new file mode 100644
index 0000000..416062e
--- /dev/null
+++ b/web/templates/admin/group_edit.html
@@ -0,0 +1,36 @@
+{% extends "admin/base.html" %}
+{% block title %}{{ 'Gruppe bearbeiten' if group else 'Neue Gruppe' }}{% endblock %}
+{% block content %}
+
+
+
+
+
{{ 'Gruppe bearbeiten' if group else 'Neue Gruppe erstellen' }}
+
+
+
+{% endblock %}
diff --git a/web/templates/admin/group_members.html b/web/templates/admin/group_members.html
new file mode 100644
index 0000000..2963720
--- /dev/null
+++ b/web/templates/admin/group_members.html
@@ -0,0 +1,86 @@
+{% extends "admin/base.html" %}
+{% block title %}Mitglieder – {{ group.name }}{% endblock %}
+{% block content %}
+
+
+
+
+
Mitglieder: {{ group.name }}
+
+
+
+
+
+
+
+
+
+ | Benutzer | Rolle | Aktionen |
+
+ {% for m in members %}
+
+ | {{ m.username }} |
+
+ {% if m.role == 'admin' %}
+ Admin
+ {% else %}
+ Member
+ {% endif %}
+ |
+
+
+
+ |
+
+ {% else %}
+ | Keine Mitglieder |
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+ {% if non_members %}
+
+ {% else %}
+
Alle Benutzer sind bereits Mitglied.
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/web/templates/admin/groups.html b/web/templates/admin/groups.html
new file mode 100644
index 0000000..e3f7414
--- /dev/null
+++ b/web/templates/admin/groups.html
@@ -0,0 +1,59 @@
+{% extends "admin/base.html" %}
+{% block title %}Gruppen{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | ID | Name | Beschreibung | Mitglieder | DB | Erstellt | Aktionen |
+
+
+
+ {% for g in groups %}
+
+ | {{ g.id }} |
+ {{ g.name }} |
+ {{ g.description or '—' }} |
+ {{ g.member_count }} |
+
+ {% if g.has_db %}
+ Konfiguriert
+ {% else %}
+ Keine DB
+ {% endif %}
+ |
+ {{ g.created_at | fmt_dt }} |
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ {% else %}
+ | Noch keine Gruppen vorhanden. |
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/web/templates/admin/user_edit.html b/web/templates/admin/user_edit.html
new file mode 100644
index 0000000..2deafc2
--- /dev/null
+++ b/web/templates/admin/user_edit.html
@@ -0,0 +1,50 @@
+{% extends "admin/base.html" %}
+{% block title %}{{ 'Benutzer bearbeiten' if user else 'Neuer Benutzer' }}{% endblock %}
+{% block content %}
+
+
+
+
+
{{ 'Benutzer bearbeiten: ' ~ user.username if user else 'Neuer Benutzer' }}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/admin/users.html b/web/templates/admin/users.html
new file mode 100644
index 0000000..0895469
--- /dev/null
+++ b/web/templates/admin/users.html
@@ -0,0 +1,53 @@
+{% extends "admin/base.html" %}
+{% block title %}Benutzer{% endblock %}
+{% block content %}
+
+
+
+
+
+
+ | Benutzer | Gruppen | Site Admin | Erstellt | Aktionen |
+
+
+ {% for u in users %}
+
+ | {{ u.username }} |
+
+ {% for g in u.groups %}
+ {{ g.name }}
+ {% if g.role == 'admin' %}{% endif %}
+
+ {% else %}Keine{% endfor %}
+ |
+
+ {% if u.is_site_admin %}
+ Site Admin
+ {% else %}—{% endif %}
+ |
+ {{ u.created_at | fmt_dt }} |
+
+
+
+
+
+ |
+
+ {% else %}
+ | Keine Benutzer vorhanden. |
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/web/templates/auth/admin_login.html b/web/templates/auth/admin_login.html
new file mode 100644
index 0000000..5879da4
--- /dev/null
+++ b/web/templates/auth/admin_login.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ MCLogger – Site Admin Login
+
+
+
+
+
+
+
+
+
Site Admin
+
Administrativer Zugang
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for cat, msg in messages %}
+
{{ msg }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/templates/auth/login.html b/web/templates/auth/login.html
new file mode 100644
index 0000000..7231159
--- /dev/null
+++ b/web/templates/auth/login.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ MCLogger – Login
+
+
+
+
+
+
+
+
+
MCLogger
+
Panel Login
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for cat, msg in messages %}
+
{{ msg }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/templates/base.html b/web/templates/base.html
new file mode 100644
index 0000000..10a9061
--- /dev/null
+++ b/web/templates/base.html
@@ -0,0 +1,173 @@
+
+
+
+
+
+ {% block title %}MCLogger{% endblock %} — Panel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% block page_title %}{% endblock %}
+ {{ now.strftime('%d.%m.%Y %H:%M') }}
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+
+ {% for cat, msg in messages %}
+
+ {{ msg }}
+
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
{% block content %}{% endblock %}
+
+
+
+
+
+
+{% block scripts %}{% endblock %}
+
+
diff --git a/web/templates/blocks.html b/web/templates/blocks.html
new file mode 100644
index 0000000..343cb53
--- /dev/null
+++ b/web/templates/blocks.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+{% block title %}Block-Events{% endblock %}
+{% block page_title %}Block-Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Player | Block | World | Position | Tool | Silk |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+
+ {% set colors = {'break':'danger','place':'success','ignite':'warning','burn':'orange','explode':'dark'} %}
+ {{ r.event_type }}
+ |
+ {{ r.player_name or '—' }} |
+ {{ r.block_type }} |
+ {{ r.world }} |
+ {{ r.x }}, {{ r.y }}, {{ r.z }} |
+ {{ r.tool or '—' }} |
+ {% if r.is_silk %}{% else %}—{% endif %} |
+
+ {% else %}
+ | No block events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/chat.html b/web/templates/chat.html
new file mode 100644
index 0000000..51a5760
--- /dev/null
+++ b/web/templates/chat.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% block title %}Chat Log{% endblock %}
+{% block page_title %}Chat Log{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ | Time | Player | Server | Channel | Message |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.channel or 'global' }} |
+ {{ r.message }} |
+
+ {% else %}
+ | No messages found |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/commands.html b/web/templates/commands.html
new file mode 100644
index 0000000..b7a88c7
--- /dev/null
+++ b/web/templates/commands.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+{% block title %}Commands{% endblock %}
+{% block page_title %}Commands{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Player | Server | Command | Position |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.command }} |
+
+ {% if r.world %}{{ r.world }} ({{ r.x|round(0)|int }}, {{ r.y|round(0)|int }}, {{ r.z|round(0)|int }}){% else %}—{% endif %}
+ |
+
+ {% else %}
+ | No commands |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html
new file mode 100644
index 0000000..7938519
--- /dev/null
+++ b/web/templates/dashboard.html
@@ -0,0 +1,221 @@
+{% extends "base.html" %}
+{% block title %}Dashboard{% endblock %}
+{% block page_title %}Dashboard{% endblock %}
+
+{% block content %}
+
+
+
+ {% set cards = [
+ ('Total Players', stats.players_total, 'bi-people-fill', 'success'),
+ ('Sessions Today', stats.sessions_today, 'bi-clock-history', 'info'),
+ ('Chats Today', stats.chat_today, 'bi-chat-dots-fill', 'primary'),
+ ('Commands Today', stats.commands_today, 'bi-terminal-fill', 'warning'),
+ ('Blocks Today', stats.blocks_today, 'bi-bricks', 'secondary'),
+ ('Deaths Today', stats.deaths_today, 'bi-heartbreak-fill', 'danger'),
+ ('Entity Events', stats.entity_events_today, 'bi-bug-fill', 'light'),
+ ('Proxy Events', stats.proxy_events_today, 'bi-diagram-3-fill', 'dark'),
+ ] %}
+ {% for label, value, icon, color in cards %}
+
+
+
+
+
+
+
+
{{ value | int }}
+
{{ label }}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ {% if online %}
+
+
+ | Player | Server | Country | Since |
+
+
+ {% for s in online %}
+
+ | {{ s.username }} |
+ {{ s.server_name }} |
+ {{ s.country or '—' }} |
+ {{ s.login_time | fmt_dt }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
+
+ No players online
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ | Time | Type | Player | Server | Detail |
+
+
+ {% for r in recent %}
+
+ | {{ r.timestamp | fmt_dt }} |
+
+ {% set badge = {'chat':'primary','command':'warning','block':'secondary','death':'danger'} %}
+ {{ r.source }}
+ |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.detail }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for p in top_players %}
+
+ | {{ loop.index }}. {{ p.username }} |
+ {{ p.total_playtime_sec | fmt_duration }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Time | Type | Server | Message |
+
+ {% for e in server_events %}
+
+ | {{ e.timestamp | fmt_dt }} |
+ {{ e.event_type }} |
+ {{ e.server_name }} |
+ {{ e.message }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+{% endblock %}
diff --git a/web/templates/deaths.html b/web/templates/deaths.html
new file mode 100644
index 0000000..64710f7
--- /dev/null
+++ b/web/templates/deaths.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+{% block title %}Deaths{% endblock %}
+{% block page_title %}Deaths{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Player | Cause | Killer | Killer Type | Level | World | Death Message |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name }} |
+ {{ r.cause or '—' }} |
+ {{ r.killer_name or '—' }} |
+ {{ r.killer_type or '—' }} |
+ {{ r.exp_level }} |
+ {{ r.world }} |
+ {{ r.death_message or '—' }} |
+
+ {% else %}
+ | No deaths |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/group_admin/base.html b/web/templates/group_admin/base.html
new file mode 100644
index 0000000..7d4c712
--- /dev/null
+++ b/web/templates/group_admin/base.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ {% block title %}Gruppen Admin{% endblock %} — {{ session.get('group_name','') }}
+
+
+
+
+
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for cat, msg in messages %}
+
+ {{ msg }}
+
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+ {% block content %}{% endblock %}
+
+
+{% block scripts %}{% endblock %}
+
+
diff --git a/web/templates/group_admin/dashboard.html b/web/templates/group_admin/dashboard.html
new file mode 100644
index 0000000..075d260
--- /dev/null
+++ b/web/templates/group_admin/dashboard.html
@@ -0,0 +1,77 @@
+{% extends "group_admin/base.html" %}
+{% block title %}Dashboard{% endblock %}
+{% block content %}
+Gruppenadmin: {{ session.get('group_name') }}
+
+
+
+
+
+
{{ stats.member_count }}
+
Mitglieder
+
+
+
+
+
+
+
+ {{ 'Ja' if stats.db_configured else 'Nein' }}
+
+
DB konfiguriert
+
+
+
+
+
+
+
{{ stats.admin_count }}
+
Admins
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Name
+ - {{ session.get('group_name') }}
+ - Deine Rolle
+ - Admin
+ - Datenbank
+ -
+ {% if stats.db_configured %}
+ Verbunden
+ {% else %}
+ Nicht konfiguriert
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/group_admin/database.html b/web/templates/group_admin/database.html
new file mode 100644
index 0000000..437ac2f
--- /dev/null
+++ b/web/templates/group_admin/database.html
@@ -0,0 +1,98 @@
+{% extends "group_admin/base.html" %}
+{% block title %}Datenbank{% endblock %}
+{% block content %}
+MC Datenbank konfigurieren
+
+
+
+
+
+
+ {% if test_result is defined %}
+
+ {% if test_result %}
+ Verbindung erfolgreich! Daten wurden gespeichert.
+ {% else %}
+ Verbindung fehlgeschlagen: {{ test_error }}
+ {% endif %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+ Gib hier die Verbindungsdaten zu deiner MCLogger MySQL-Datenbank ein.
+ Das Panel liest nur Daten (SELECT) — schreibender Zugriff ist nicht nötig.
+
+
+ Die Zugangsdaten werden verschlüsselt gespeichert und sind nur für deine Gruppe sichtbar.
+
+
+
Benötigte Tabellen:
+
+ - player_sessions
+ - chat_messages
+ - player_commands
+ - block_events
+ - player_deaths
+ - proxy_events
+ - server_events
+ - permission_changes
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/group_admin/member_edit.html b/web/templates/group_admin/member_edit.html
new file mode 100644
index 0000000..a50b30c
--- /dev/null
+++ b/web/templates/group_admin/member_edit.html
@@ -0,0 +1,54 @@
+{% extends "group_admin/base.html" %}
+{% block title %}Berechtigungen – {{ member.username }}{% endblock %}
+{% block content %}
+
+
+
+
+
Berechtigungen: {{ member.username }}
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/group_admin/members.html b/web/templates/group_admin/members.html
new file mode 100644
index 0000000..3e40e67
--- /dev/null
+++ b/web/templates/group_admin/members.html
@@ -0,0 +1,65 @@
+{% extends "group_admin/base.html" %}
+{% block title %}Mitglieder{% endblock %}
+{% block content %}
+Mitglieder
+
+
+
+
+
+
+
+
+ | Benutzer | Rolle | Aktionen |
+
+ {% for m in members %}
+
+ | {{ m.username }} |
+
+ {% if m.role == 'admin' %}
+ Admin
+ {% else %}
+ Member
+ {% endif %}
+ |
+
+ {% if m.id != session.get('user_id') %}
+
+
+
+
+ {% else %}
+ Du
+ {% endif %}
+ |
+
+ {% else %}
+ | Keine Mitglieder |
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Neue Mitglieder müssen vom Site Admin zur Gruppe hinzugefügt werden.
+
+
+ Als Gruppenadmin kannst du Berechtigungen bestehender Mitglieder verwalten und Mitglieder entfernen.
+
+
+
+
+
+{% endblock %}
diff --git a/web/templates/login.html b/web/templates/login.html
new file mode 100644
index 0000000..782e13a
--- /dev/null
+++ b/web/templates/login.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ MCLogger – Login
+
+
+
+
+
+
+
+
+
+
MCLogger
+
Admin-Interface · SimolZimol
+
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+
+
+
+
+
+
diff --git a/web/templates/panel/blocks.html b/web/templates/panel/blocks.html
new file mode 100644
index 0000000..a3688f2
--- /dev/null
+++ b/web/templates/panel/blocks.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+{% block title %}Block-Events{% endblock %}
+{% block page_title %}Block-Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Player | Block | World | Position | Tool | Silk |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+
+ {% set colors = {'break':'danger','place':'success','ignite':'warning','burn':'orange','explode':'dark'} %}
+ {{ r.event_type }}
+ |
+ {{ r.player_name or '—' }} |
+ {{ r.block_type }} |
+ {{ r.world }} |
+ {{ r.x }}, {{ r.y }}, {{ r.z }} |
+ {{ r.tool or '—' }} |
+ {% if r.is_silk %}{% else %}—{% endif %} |
+
+ {% else %}
+ | No block events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/chat.html b/web/templates/panel/chat.html
new file mode 100644
index 0000000..1b9e7f4
--- /dev/null
+++ b/web/templates/panel/chat.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% block title %}Chat Log{% endblock %}
+{% block page_title %}Chat Log{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ | Time | Player | Server | Channel | Message |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.channel or 'global' }} |
+ {{ r.message }} |
+
+ {% else %}
+ | No messages found |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/commands.html b/web/templates/panel/commands.html
new file mode 100644
index 0000000..aeb1854
--- /dev/null
+++ b/web/templates/panel/commands.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+{% block title %}Commands{% endblock %}
+{% block page_title %}Commands{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Player | Server | Command | Position |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.command }} |
+
+ {% if r.world %}{{ r.world }} ({{ r.x|round(0)|int }}, {{ r.y|round(0)|int }}, {{ r.z|round(0)|int }}){% else %}—{% endif %}
+ |
+
+ {% else %}
+ | No commands |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/dashboard.html b/web/templates/panel/dashboard.html
new file mode 100644
index 0000000..08e976d
--- /dev/null
+++ b/web/templates/panel/dashboard.html
@@ -0,0 +1,194 @@
+{% extends "base.html" %}
+{% block title %}Dashboard{% endblock %}
+{% block page_title %}Dashboard{% endblock %}
+
+{% block content %}
+
+
+ {% set cards = [
+ ('Total Players', stats.players_total, 'bi-people-fill', 'success'),
+ ('Sessions Today', stats.sessions_today, 'bi-clock-history', 'info'),
+ ('Chats Today', stats.chat_today, 'bi-chat-dots-fill', 'primary'),
+ ('Commands Today', stats.commands_today, 'bi-terminal-fill', 'warning'),
+ ('Blocks Today', stats.blocks_today, 'bi-bricks', 'secondary'),
+ ('Deaths Today', stats.deaths_today, 'bi-heartbreak-fill', 'danger'),
+ ('Entity Events', stats.entity_events_today, 'bi-bug-fill', 'light'),
+ ('Proxy Events', stats.proxy_events_today, 'bi-diagram-3-fill', 'dark'),
+ ] %}
+ {% for label, value, icon, color in cards %}
+
+
+
+
+
+
+
+
{{ value | int }}
+
{{ label }}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {% if online %}
+
+
+ | Player | Server | Since |
+
+
+ {% for s in online %}
+
+ | {{ s.player_name }} |
+ {{ s.server_name }} |
+ {{ s.login_time | fmt_dt }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
+
No players online
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+ | Time | Type | Player | Server | Detail |
+
+
+ {% for r in recent %}
+
+ | {{ r.timestamp | fmt_dt }} |
+
+ {% set badge = {'chat':'primary','command':'warning','block':'secondary','death':'danger'} %}
+ {{ r.source }}
+ |
+ {{ r.player_name or '—' }} |
+ {{ r.server_name or '—' }} |
+ {{ r.detail }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for p in top_players %}
+
+ | {{ loop.index }}. {{ p.username }} |
+ {{ p.total_playtime_sec | fmt_duration }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Time | Type | Server | Message |
+
+ {% for e in server_events %}
+
+ | {{ e.timestamp | fmt_dt }} |
+ {{ e.event_type }} |
+ {{ e.server_name }} |
+ {{ e.message }} |
+
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+{% endblock %}
diff --git a/web/templates/panel/deaths.html b/web/templates/panel/deaths.html
new file mode 100644
index 0000000..4cfec03
--- /dev/null
+++ b/web/templates/panel/deaths.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+{% block title %}Deaths{% endblock %}
+{% block page_title %}Deaths{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Player | Cause | Killer | Killer Type | Level | World | Death Message |
+
+
+ {% for r in rows %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.player_name }} |
+ {{ r.cause or '—' }} |
+ {{ r.killer_name or '—' }} |
+ {{ r.killer_type or '—' }} |
+ {{ r.exp_level }} |
+ {{ r.world }} |
+ {{ r.death_message or '—' }} |
+
+ {% else %}
+ | No deaths |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/no_db.html b/web/templates/panel/no_db.html
new file mode 100644
index 0000000..594b00a
--- /dev/null
+++ b/web/templates/panel/no_db.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+{% block title %}No Database{% endblock %}
+{% block page_title %}Keine Datenbank{% endblock %}
+{% block content %}
+
+
+
+
Keine Datenbank konfiguriert
+
+ Für diese Gruppe ist noch keine MC-Datenbank eingerichtet.
+ {% if session.get('role') == 'admin' %}
+ Du kannst die Verbindung als Gruppen-Admin konfigurieren.
+ {% else %}
+ Bitte wende dich an deinen Gruppenadmin.
+ {% endif %}
+
+ {% if session.get('role') == 'admin' %}
+
+ Datenbank konfigurieren
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/web/templates/panel/perms.html b/web/templates/panel/perms.html
new file mode 100644
index 0000000..1c7fa18
--- /dev/null
+++ b/web/templates/panel/perms.html
@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+{% block title %}Permissions{% endblock %}
+{% block page_title %}Permissions{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+
+ | Time | Plugin | Event Type | Target Player | Actor | Target Type | Target ID | Action | Server |
+
+
+ {% for r in rows %}
+ {% set badge_colors = {
+ 'luckperms_permission_set': 'success',
+ 'luckperms_permission_unset': 'danger',
+ 'luckperms_parent_add': 'primary',
+ 'luckperms_parent_remove': 'warning',
+ 'luckperms_meta_set': 'info',
+ 'luckperms_meta_unset': 'secondary',
+ 'luckperms_group_create': 'light',
+ 'luckperms_group_delete': 'dark',
+ } %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.plugin_name or '—' }} |
+ {{ r.event_type }} |
+ {{ r.player_name or '—' }} |
+ {{ r.actor_name or '—' }} |
+ {{ r.target_type or '—' }} |
+ {{ r.target_id or '—' }} |
+ {{ r.action or '—' }} |
+ {{ r.server_name or '—' }} |
+
+ {% else %}
+ | No permission events found |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/player_detail.html b/web/templates/panel/player_detail.html
new file mode 100644
index 0000000..9aed45a
--- /dev/null
+++ b/web/templates/panel/player_detail.html
@@ -0,0 +1,142 @@
+{% extends "base.html" %}
+{% block title %}{{ player.username }}{% endblock %}
+{% block page_title %}{{ player.username }}{% endblock %}
+
+{% block content %}
+
+
+
+
+

+
{{ player.username }}
+ {% if player.is_op %}
+
OP
+ {% endif %}
+
+ | UUID | {{ player.uuid }} |
+ | IP | {{ player.ip_address or '—' }} |
+ | Locale | {{ player.locale or '—' }} |
+ | Playtime | {{ player.total_playtime_sec | fmt_duration }} |
+ | Since | {{ player.first_seen | fmt_dt }} |
+ | Last Seen | {{ player.last_seen | fmt_dt }} |
+
+
+
+
+
+
+
+
+
+
+
+
+ | Login | Logout | Duration | Server | IP |
+
+ {% for s in sessions %}
+ | {{ s.login_time | fmt_dt }} |
+ {{ s.logout_time | fmt_dt }} |
+ {{ s.duration_sec | fmt_duration }} |
+ {{ s.server_name or '—' }} |
+ {{ s.ip_address or '—' }} |
+
{% else %}| No sessions |
{% endfor %}
+
+
+
+
+
+
+
+
+ | Time | Server | Message |
+
+ {% for c in chat %}
+ | {{ c.timestamp | fmt_dt }} |
+ {{ c.server_name or '—' }} |
+ {{ c.message }} |
+
{% else %}| No chat messages |
{% endfor %}
+
+
+
+
+
+
+
+
+ | Time | Server | Command | Position |
+
+ {% for c in commands %}
+ | {{ c.timestamp | fmt_dt }} |
+ {{ c.server_name or '—' }} |
+ {{ c.command }} |
+ {{ c.world or '' }} {% if c.x %}({{ c.x|round(1) }}, {{ c.y|round(1) }}, {{ c.z|round(1) }}){% endif %} |
+
{% else %}| No commands |
{% endfor %}
+
+
+
+
+
+
+
+
+ | Time | Cause | Killer | Level | World |
+
+ {% for d in deaths %}
+ | {{ d.timestamp | fmt_dt }} |
+ {{ d.cause or '—' }} |
+ {{ d.killer_name or '—' }} |
+ {{ d.exp_level }} |
+ {{ d.world }} |
+
{% else %}| No deaths |
{% endfor %}
+
+
+
+
+
+
+
+
+ | Time | From | To | Cause |
+
+ {% for t in teleports %}
+ | {{ t.timestamp | fmt_dt }} |
+ {{ t.from_world }} ({{ t.from_x|round(0)|int }}, {{ t.from_y|round(0)|int }}, {{ t.from_z|round(0)|int }}) |
+ {{ t.to_world }} ({{ t.to_x|round(0)|int }}, {{ t.to_y|round(0)|int }}, {{ t.to_z|round(0)|int }}) |
+ {{ t.cause or '—' }} |
+
{% else %}| No teleports |
{% endfor %}
+
+
+
+
+
+
+
+
+ | Time | Type | From | To |
+
+ {% for e in proxy_events %}
+ | {{ e.timestamp | fmt_dt }} |
+ {{ e.event_type }} |
+ {{ e.from_server or '—' }} |
+ {{ e.to_server or '—' }} |
+
{% else %}| No proxy events |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ Back to Overview
+
+{% endblock %}
diff --git a/web/templates/panel/players.html b/web/templates/panel/players.html
new file mode 100644
index 0000000..abad74c
--- /dev/null
+++ b/web/templates/panel/players.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% block title %}Players{% endblock %}
+{% block page_title %}Players{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ | Player | IP | First Seen | Last Seen | Playtime | OP | |
+
+
+ {% for p in players %}
+
+ |
+ {{ p.username }}
+ |
+ {{ p.ip_address or '—' }} |
+ {{ p.first_seen | fmt_dt }} |
+ {{ p.last_seen | fmt_dt }} |
+ {{ p.total_playtime_sec | fmt_duration }} |
+
+ {% if p.is_op %}
+ OP
+ {% else %}—{% endif %}
+ |
+
+
+
+
+ |
+
+ {% else %}
+ | No players found |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/proxy.html b/web/templates/panel/proxy.html
new file mode 100644
index 0000000..67ef5a7
--- /dev/null
+++ b/web/templates/panel/proxy.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+{% block title %}Proxy Events{% endblock %}
+{% block page_title %}Proxy Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Player | Proxy | From | To | IP |
+
+
+ {% for r in rows %}
+ {% set badge = {'login':'success','disconnect':'danger','server_switch':'primary','command':'warning','proxy_start':'info','proxy_stop':'dark'} %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.event_type }} |
+ {{ r.player_name or '—' }} |
+ {{ r.proxy_name or '—' }} |
+ {{ r.from_server or '—' }} |
+ {{ r.to_server or '—' }} |
+ {{ r.ip_address or '—' }} |
+
+ {% else %}
+ | No proxy events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/server_events.html b/web/templates/panel/server_events.html
new file mode 100644
index 0000000..ae451f9
--- /dev/null
+++ b/web/templates/panel/server_events.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+{% block title %}Server Events{% endblock %}
+{% block page_title %}Server Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Server | Message |
+
+
+ {% for r in rows %}
+ {% set badge = {'server_start':'success','server_stop':'danger','player_join':'info','player_quit':'secondary','player_kick':'warning'} %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.event_type }} |
+ {{ r.server_name or '—' }} |
+ {{ r.message or '—' }} |
+
+ {% else %}
+ | No events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/panel/sessions.html b/web/templates/panel/sessions.html
new file mode 100644
index 0000000..a12c246
--- /dev/null
+++ b/web/templates/panel/sessions.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% block title %}Sessions{% endblock %}
+{% block page_title %}Sessions{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Player | Server | Login | Logout | Duration | IP | Country | Client |
+
+
+ {% for r in rows %}
+
+ |
+
+ {{ r.player_name }}
+
+ |
+ {{ r.server_name or '—' }} |
+ {{ r.login_time | fmt_dt }} |
+ {{ r.logout_time | fmt_dt }} |
+
+ {% if r.logout_time %}{{ r.duration_sec | fmt_duration }}
+ {% else %}Online{% endif %}
+ |
+ {{ r.ip_address or '—' }} |
+ {{ r.country or '—' }} |
+ {{ r.client_version or '—' }} |
+
+ {% else %}
+ | No sessions |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/perms.html b/web/templates/perms.html
new file mode 100644
index 0000000..ea65d44
--- /dev/null
+++ b/web/templates/perms.html
@@ -0,0 +1,74 @@
+{% extends "base.html" %}
+{% block title %}Permissions{% endblock %}
+{% block page_title %}Permissions{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+
+
+ | Time |
+ Plugin |
+ Event Type |
+ Target Player |
+ Actor |
+ Target Type |
+ Target ID |
+ Action |
+ Server |
+
+
+
+ {% for r in rows %}
+ {% set badge_colors = {
+ 'luckperms_permission_set': 'success',
+ 'luckperms_permission_unset': 'danger',
+ 'luckperms_parent_add': 'primary',
+ 'luckperms_parent_remove': 'warning',
+ 'luckperms_meta_set': 'info',
+ 'luckperms_meta_unset': 'secondary',
+ 'luckperms_group_create': 'light',
+ 'luckperms_group_delete': 'dark',
+ } %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.plugin_name or '—' }} |
+ {{ r.event_type }} |
+ {{ r.player_name or '—' }} |
+ {{ r.actor_name or '—' }} |
+ {{ r.target_type or '—' }} |
+ {{ r.target_id or '—' }} |
+ {{ r.action or '—' }} |
+ {{ r.server_name or '—' }} |
+
+ {% else %}
+ | No permission events found |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/player_detail.html b/web/templates/player_detail.html
new file mode 100644
index 0000000..872e735
--- /dev/null
+++ b/web/templates/player_detail.html
@@ -0,0 +1,182 @@
+{% extends "base.html" %}
+{% block title %}{{ player.username }}{% endblock %}
+{% block page_title %}{{ player.username }}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+

+
{{ player.username }}
+ {% if player.is_op %}
+
OP
+ {% endif %}
+
+ | UUID | {{ player.uuid }} |
+ | IP | {{ player.ip_address or '—' }} |
+ | Locale | {{ player.locale or '—' }} |
+ | Playtime | {{ player.total_playtime_sec | fmt_duration }} |
+ | Since | {{ player.first_seen | fmt_dt }} |
+ | Last Seen | {{ player.last_seen | fmt_dt }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Login | Logout | Duration | Server | IP |
+
+ {% for s in sessions %}
+
+ | {{ s.login_time | fmt_dt }} |
+ {{ s.logout_time | fmt_dt }} |
+ {{ s.duration_sec | fmt_duration }} |
+ {{ s.server_name or '—' }} |
+ {{ s.ip_address or '—' }} |
+
+ {% else %}| No sessions |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | Server | Message |
+
+ {% for c in chat %}
+
+ | {{ c.timestamp | fmt_dt }} |
+ {{ c.server_name or '—' }} |
+ {{ c.message }} |
+
+ {% else %}| No chat messages |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | Server | Command | Position |
+
+ {% for c in commands %}
+
+ | {{ c.timestamp | fmt_dt }} |
+ {{ c.server_name or '—' }} |
+ {{ c.command }} |
+ {{ c.world or '' }} {% if c.x %}({{ c.x|round(1) }}, {{ c.y|round(1) }}, {{ c.z|round(1) }}){% endif %} |
+
+ {% else %}| No commands |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | Cause | Killer | Level | World |
+
+ {% for d in deaths %}
+
+ | {{ d.timestamp | fmt_dt }} |
+ {{ d.cause or '—' }} |
+ {{ d.killer_name or '—' }} |
+ {{ d.exp_level }} |
+ {{ d.world }} |
+
+ {% else %}| No deaths |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | From | To | Cause |
+
+ {% for t in teleports %}
+
+ | {{ t.timestamp | fmt_dt }} |
+ {{ t.from_world }} ({{ t.from_x|round(0)|int }}, {{ t.from_y|round(0)|int }}, {{ t.from_z|round(0)|int }}) |
+ {{ t.to_world }} ({{ t.to_x|round(0)|int }}, {{ t.to_y|round(0)|int }}, {{ t.to_z|round(0)|int }}) |
+ {{ t.cause or '—' }} |
+
+ {% else %}| No teleports |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | Type | Old | New |
+
+ {% for s in stats %}
+
+ | {{ s.timestamp | fmt_dt }} |
+ {{ s.event_type }} |
+ {{ s.old_value or '—' }} |
+ {{ s.new_value or '—' }} |
+
+ {% else %}| No stats |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ | Time | Type | From | To |
+
+ {% for e in proxy_events %}
+
+ | {{ e.timestamp | fmt_dt }} |
+ {{ e.event_type }} |
+ {{ e.from_server or '—' }} |
+ {{ e.to_server or '—' }} |
+
+ {% else %}| No proxy events |
{% endfor %}
+
+
+
+
+
+
+
+
+
+ Back to Overview
+
+{% endblock %}
diff --git a/web/templates/players.html b/web/templates/players.html
new file mode 100644
index 0000000..71c51b4
--- /dev/null
+++ b/web/templates/players.html
@@ -0,0 +1,72 @@
+{% extends "base.html" %}
+{% block title %}Players{% endblock %}
+{% block page_title %}Players{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+ | Player | IP | First Seen |
+ Last Seen | Playtime | OP | |
+
+
+
+ {% for p in players %}
+
+ |
+ {{ p.username }}
+ |
+ {{ p.ip_address or '—' }} |
+ {{ p.first_seen | fmt_dt }} |
+ {{ p.last_seen | fmt_dt }} |
+ {{ p.total_playtime_sec | fmt_duration }} |
+
+ {% if p.is_op %}
+ OP
+ {% else %}—{% endif %}
+ |
+
+
+
+
+ |
+
+ {% else %}
+ | No players found |
+ {% endfor %}
+
+
+
+
+
+
+
+{% if pages > 1 %}
+
+{% endif %}
+{% endblock %}
diff --git a/web/templates/proxy.html b/web/templates/proxy.html
new file mode 100644
index 0000000..7b96266
--- /dev/null
+++ b/web/templates/proxy.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+{% block title %}Proxy Events{% endblock %}
+{% block page_title %}Proxy Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Player | Proxy | From | To | IP |
+
+
+ {% for r in rows %}
+ {% set badge = {'login':'success','disconnect':'danger','server_switch':'primary','command':'warning','proxy_start':'info','proxy_stop':'dark'} %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.event_type }} |
+ {{ r.player_name or '—' }} |
+ {{ r.proxy_name or '—' }} |
+ {{ r.from_server or '—' }} |
+ {{ r.to_server or '—' }} |
+ {{ r.ip_address or '—' }} |
+
+ {% else %}
+ | No proxy events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/server_events.html b/web/templates/server_events.html
new file mode 100644
index 0000000..ecba039
--- /dev/null
+++ b/web/templates/server_events.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+{% block title %}Server Events{% endblock %}
+{% block page_title %}Server Events{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Time | Type | Server | Message |
+
+
+ {% for r in rows %}
+ {% set badge = {'server_start':'success','server_stop':'danger','player_join':'info','player_quit':'secondary','player_kick':'warning'} %}
+
+ | {{ r.timestamp | fmt_dt }} |
+ {{ r.event_type }} |
+ {{ r.server_name or '—' }} |
+ {{ r.message or '—' }} |
+
+ {% else %}
+ | No events |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}
diff --git a/web/templates/sessions.html b/web/templates/sessions.html
new file mode 100644
index 0000000..f34fb85
--- /dev/null
+++ b/web/templates/sessions.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% block title %}Sessions{% endblock %}
+{% block page_title %}Sessions{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ | Player | Server | Login | Logout | Duration | IP | Country | Client |
+
+
+ {% for r in rows %}
+
+ |
+
+ {{ r.player_name }}
+
+ |
+ {{ r.server_name or '—' }} |
+ {{ r.login_time | fmt_dt }} |
+ {{ r.logout_time | fmt_dt }} |
+
+ {% if r.logout_time %}{{ r.duration_sec | fmt_duration }}
+ {% else %}Online{% endif %}
+ |
+ {{ r.ip_address or '—' }} |
+ {{ r.country or '—' }} |
+ {{ r.client_version or '—' }} |
+
+ {% else %}
+ | No sessions |
+ {% endfor %}
+
+
+
+
+
+{% include "_pagination.html" %}
+{% endblock %}