commit 2b1fbb9f02f9ef10a4da62bdfc20918a37f1925a Author: Alexander Zielonka Date: Mon Jan 5 06:16:05 2026 +0100 Initial commit: Homelab documentation - infrastructure.md: Network topology, server overview, credentials - gsm.md: Gameserver Monitor detailed documentation - todo.md: Project roadmap and completed tasks - CLAUDE.md: AI assistant context - temp/: Frontend component backups 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2c41e8e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(ssh:*)", + "Bash(scp:*)", + "Bash(veth.*)", + "Bash(docker.*)", + "Bash(curl:*)", + "Bash(findstr:*)", + "Bash(cat:*)", + "Bash(powershell -Command @'\n$content = @\"\"\nimport { useState, useEffect } from 'react'\nimport { useNavigate } from 'react-router-dom'\nimport { getServers } from '../api'\nimport { useUser } from '../context/UserContext'\nimport ServerCard from '../components/ServerCard'\nimport SettingsModal from '../components/SettingsModal'\nimport UserManagement from '../components/UserManagement'\n\nexport default function Dashboard\\({ onLogout }\\) {\n const navigate = useNavigate\\(\\)\n const { user, token, loading: userLoading, isSuperadmin, role } = useUser\\(\\)\n const [servers, setServers] = useState\\([]\\)\n const [loading, setLoading] = useState\\(true\\)\n const [error, setError] = useState\\(''\\)\n const [showSettings, setShowSettings] = useState\\(false\\)\n const [showUserMgmt, setShowUserMgmt] = useState\\(false\\)\n\n const fetchServers = async \\(\\) => {\n try {\n const data = await getServers\\(token\\)\n setServers\\(data\\)\n setError\\(''\\)\n } catch \\(err\\) {\n setError\\('Failed to connect to server'\\)\n if \\(err.message.includes\\('401'\\) || err.message.includes\\('403'\\)\\) {\n onLogout\\(\\)\n }\n } finally {\n setLoading\\(false\\)\n }\n }\n\n useEffect\\(\\(\\) => {\n if \\(!userLoading\\) {\n fetchServers\\(\\)\n const interval = setInterval\\(fetchServers, 10000\\)\n return \\(\\) => clearInterval\\(interval\\)\n }\n }, [token, userLoading]\\)\n\n const roleLabels = {\n user: 'Viewer',\n moderator: 'Operator',\n superadmin: 'Admin'\n }\n\n if \\(userLoading\\) {\n return \\(\n
\n
Loading...
\n
\n \\)\n }\n\n const onlineCount = servers.filter\\(s => s.running\\).length\n const totalPlayers = servers.reduce\\(\\(sum, s\\) => sum + \\(s.players?.online || 0\\), 0\\)\n\n return \\(\n
\n {/* Header */}\n
\n
\n
\n
\n

\n Gameserver Monitor\n

\n
\n \n {onlineCount}/{servers.length} online\n \n |\n \n {totalPlayers} players\n \n
\n
\n\n
\n
\n
{user?.username}
\n
{roleLabels[role]}
\n
\n\n {isSuperadmin && \\(\n setShowUserMgmt\\(true\\)}\n className=\"\"btn btn-ghost\"\"\n >\n Users\n \n \\)}\n\n setShowSettings\\(true\\)}\n className=\"\"btn btn-ghost\"\"\n >\n Settings\n \n\n \n Sign out\n \n
\n
\n
\n
\n\n {/* Main Content */}\n
\n {error && \\(\n
\n {error}\n
\n \\)}\n\n {loading ? \\(\n
\n
Loading servers...
\n
\n \\) : \\(\n
\n {servers.map\\(\\(server, index\\) => \\(\n \n navigate\\('/server/' + server.id\\)}\n />\n
\n \\)\\)}\n
\n \\)}\n \n\n {/* Modals */}\n {showSettings && \\(\n setShowSettings\\(false\\)} />\n \\)}\n\n {showUserMgmt && \\(\n setShowUserMgmt\\(false\\)} />\n \\)}\n \n \\)\n}\n\"\"@\n$content | Out-File -FilePath \"\"Dashboard.jsx\"\" -Encoding UTF8\n'@)", + "Bash(git add:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b11349 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gsm-frontend/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0b24dd5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,33 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Purpose + +This is a documentation repository for a homelab infrastructure. It contains technical documentation for servers, services, and network configuration - no application code. + +## Infrastructure Overview + +The homelab consists of: +- **Raspberry Pi (192.168.2.10)**: Runs Nginx Proxy Manager, Cloudflare DDNS, and Nextcloud via Docker +- **Proxmox Server (192.168.2.20)**: Virtualization host running LXC containers and VMs +- **Gameserver Monitor (192.168.2.30)**: React/Node.js webapp for monitoring game servers (LXC) +- **Factorio Server (192.168.2.50)**: Docker-based game server (LXC) +- **Minecraft Server (192.168.2.51)**: ATM10 modded server running via screen (VM) + +## Key Technical Details + +**Gameserver Monitor Stack**: React + Vite + TailwindCSS frontend, Node.js + Express backend, SQLite for auth, nginx as reverse proxy. Located at `/opt/gameserver-monitor/` on the monitor LXC. + +**Gameserver Monitor Rollensystem**: +- `user`: Kann nur Server-Metriken sehen (CPU, RAM, Players, Uptime) +- `moderator`: Zusätzlich Konsole, RCON, Server Start/Stop/Restart +- `superadmin`: Zusätzlich Nutzerverwaltung (User anlegen/löschen, Rollen ändern) + +**Domain**: dimension47.de with subdomains managed via Cloudflare DDNS + +**SSH Access**: The monitor server (.30) has SSH key access to Proxmox and both game servers for remote management. + +## Language Note + +Documentation is written in German. diff --git a/gsm.md b/gsm.md new file mode 100644 index 0000000..6f616c0 --- /dev/null +++ b/gsm.md @@ -0,0 +1,306 @@ +# Gameserver Monitor (GSM) - Dokumentation + +## Uebersicht + +Der Gameserver Monitor ist eine Web-Applikation zur Ueberwachung und Verwaltung von Gameservern. + +**URL:** https://monitor.dimension47.de +**Server:** 192.168.2.30 (LXC Container) + +--- + +## Architektur + +``` +Browser + | + v ++------------------+ +| nginx (Port 80) | Reverse Proxy ++--------+---------+ + | + +-----+-----+ + | | + v v +Frontend Backend +(dist/) (Port 3000) + | | + | +-----+-----+ + | | | | + | v v v + | SQLite SSH RCON + | | | | + | | +-----+-----+ + | | | | + | v v v + | users.db .50 .51 .52 + | whitelist Facto MC VRis + | + +---> Prometheus (Port 9090) + | + v + Grafana (Port 3001) +``` + +--- + +## Tech Stack + +| Komponente | Technologie | +|------------|-------------| +| Frontend | React 18 + Vite + TailwindCSS 4 + recharts | +| Backend | Node.js 20 + Express | +| Datenbank | SQLite (better-sqlite3) | +| Auth | JWT + bcrypt | +| Metrics | Prometheus + Node Exporter | +| Dashboards | Grafana | +| Reverse Proxy | nginx | + +--- + +## Verzeichnisstruktur + +``` +/opt/gameserver-monitor/ +| ++-- backend/ +| +-- server.js # Express Server Entry +| +-- config.json # Server-Konfiguration +| +-- .env # JWT_SECRET +| +-- package.json +| | +| +-- routes/ +| | +-- servers.js # /api/servers Endpoints +| | +-- auth.js # /api/auth Endpoints +| | +| +-- services/ +| | +-- ssh.js # SSH-Verbindungen, Status, Uptime +| | +-- rcon.js # RCON-Kommunikation +| | +-- prometheus.js # Prometheus Queries +| | +| +-- middleware/ +| | +-- auth.js # JWT Middleware +| | +| +-- db/ +| +-- init.js # DB-Schema, Whitelist-Cache +| +-- users.sqlite # User-Datenbank +| ++-- frontend/ + +-- src/ + | +-- main.jsx + | +-- App.jsx + | +-- api.js # API Client + | | + | +-- pages/ + | | +-- Dashboard.jsx + | | +-- ServerDetail.jsx + | | + | +-- components/ + | | +-- ServerCard.jsx + | | +-- MetricsChart.jsx + | | +-- LoginModal.jsx + | | +-- SettingsModal.jsx + | | +-- UserManagement.jsx + | | + | +-- context/ + | +-- UserContext.jsx + | + +-- public/ + | +-- minecraft.png + | +-- factorio.png + | +-- vrising.png + | +-- navbarlogograuer.png + | +-- navbarlogoweiss.png + | + +-- dist/ # Build Output (wird von nginx served) + +-- index.html + +-- package.json + +-- vite.config.js + +-- tailwind.config.js +``` + +--- + +## API Endpoints + +### Authentifizierung + +| Method | Endpoint | Auth | Beschreibung | +|--------|----------|------|--------------| +| POST | /api/auth/login | - | Login, gibt JWT zurueck | +| GET | /api/auth/me | JWT | Aktueller User + Rolle | +| POST | /api/auth/change-password | JWT | Eigenes Passwort aendern | +| GET | /api/auth/users | superadmin | Alle User auflisten | +| POST | /api/auth/users | superadmin | Neuen User erstellen | +| PATCH | /api/auth/users/:id/role | superadmin | Rolle aendern | +| PATCH | /api/auth/users/:id/password | superadmin | Passwort setzen | +| DELETE | /api/auth/users/:id | superadmin | User loeschen | + +### Server + +| Method | Endpoint | Auth | Beschreibung | +|--------|----------|------|--------------| +| GET | /api/servers | optional | Alle Server mit Status/Metrics | +| GET | /api/servers/:id | optional | Einzelner Server | +| POST | /api/servers/:id/start | moderator | Server starten | +| POST | /api/servers/:id/stop | moderator | Server stoppen | +| POST | /api/servers/:id/restart | moderator | Server neustarten | +| GET | /api/servers/:id/logs | moderator | Console Logs | +| POST | /api/servers/:id/rcon | moderator | RCON Befehl senden | +| GET | /api/servers/:id/whitelist | optional | Whitelist (gecached) | +| GET | /api/servers/:id/metrics/history | optional | Prometheus History | + +--- + +## Rollensystem + +| Rolle | Rechte | +|-------|--------| +| (kein Login) | Dashboard ansehen, Metriken sehen | +| user | Wie ohne Login | +| moderator | + Server starten/stoppen, Logs, RCON, Whitelist | +| superadmin | + Nutzerverwaltung | + +--- + +## Server-Konfiguration + +`/opt/gameserver-monitor/backend/config.json`: + +```json +{ + "servers": [ + { + "id": "minecraft", + "name": "All the Mods 10 | Minecraft", + "host": "192.168.2.51", + "type": "minecraft", + "runtime": "screen", + "screenName": "minecraft", + "workDir": "/opt/minecraft", + "startCmd": "./run.sh", + "rconPort": 25575, + "rconPassword": "gsm-mc-2026" + }, + { + "id": "factorio", + "name": "Factorio", + "host": "192.168.2.50", + "type": "factorio", + "runtime": "docker", + "containerName": "factorio", + "rconPort": 27015, + "rconPassword": "jieTig6IkixaKuu" + }, + { + "id": "vrising", + "name": "V Rising", + "host": "192.168.2.52", + "type": "vrising", + "runtime": "systemd", + "serviceName": "vrising", + "workDir": "/home/steam/vrising" + } + ] +} +``` + +### Runtime-Typen + +| Runtime | Status-Check | Start | Stop | Uptime | +|---------|--------------|-------|------|--------| +| docker | docker inspect | docker start | docker stop | Container StartedAt | +| screen | screen -ls | screen -dmS | screen -X quit | ps -o etimes | +| systemd | systemctl is-active | systemctl start | systemctl stop | ActiveEnterTimestamp | + +--- + +## Features + +### Oeffentliches Dashboard +- Dashboard ist ohne Login sichtbar +- Login-Modal fuer Admins ueber "Sign in" Button +- Alle Server-Karten sichtbar, aber ohne Admin-Hints + +### Gameserver-Uptime +- Zeigt Prozess-Uptime statt Host-Uptime +- Docker: Container-Laufzeit +- Screen: Session-Laufzeit +- Systemd: Service-Aktivzeit + +### Whitelist-Caching (Minecraft) +- Whitelist wird serverseitig in SQLite gecached +- Anzeige auch wenn Server offline +- Bearbeitung nur wenn Server online +- Cache wird bei jeder Aenderung aktualisiert + +### Navbar-Logo +- Grau (navbarlogograuer.png) im Normalzustand +- Weiss (navbarlogoweiss.png) bei Hover +- Weiche CSS-Transition (300ms) +- Link zu https://zeasy.software + +### Game-Logos +- Automatische Erkennung anhand Server-Name +- minecraft.png, factorio.png, vrising.png +- Angezeigt in ServerCard und ServerDetail + +--- + +## Wartung + +### Backend neustarten +```bash +ssh root@192.168.2.30 +pkill -f 'node server.js' +cd /opt/gameserver-monitor/backend +node server.js & +``` + +### Frontend neu bauen +```bash +ssh root@192.168.2.30 +cd /opt/gameserver-monitor/frontend +npm run build +nginx -s reload +``` + +### Logs pruefen +```bash +# Backend (laeuft im Hintergrund) +# Fehler werden auf stderr ausgegeben + +# nginx +tail -f /var/log/nginx/error.log +tail -f /var/log/nginx/access.log +``` + +### Neuen Server hinzufuegen +1. config.json bearbeiten (siehe oben) +2. SSH-Key auf neuem Server hinterlegen +3. Node Exporter installieren (fuer Prometheus) +4. Backend neustarten + +### User verwalten +- Ueber UI: Settings > Users (nur superadmin) +- Direkt in DB: `/opt/gameserver-monitor/backend/db/users.sqlite` + +--- + +## Troubleshooting + +### 502 Bad Gateway +- Backend laeuft nicht +- Loesung: Backend manuell starten + +### Server zeigt "offline" obwohl online +- SSH-Verbindung fehlgeschlagen +- Loesung: SSH-Key pruefen, Firewall pruefen + +### Whitelist leer +- RCON-Verbindung fehlgeschlagen +- Loesung: RCON-Port und Passwort pruefen + +### Metrics zeigen 0 +- Prometheus Target nicht erreichbar +- Loesung: Node Exporter auf Gameserver pruefen diff --git a/infrastructure.md b/infrastructure.md new file mode 100644 index 0000000..376c367 --- /dev/null +++ b/infrastructure.md @@ -0,0 +1,181 @@ +# Homelab Infrastructure + +## Netzwerktopologie + +``` +Internet + | + v ++------------------+ +| Router/Modem | 62.155.227.77 (dynamisch, Telekom) +| 192.168.2.1 | ++--------+---------+ + | + v ++-------------------------------------------------------------+ +| LAN 192.168.2.0/24 | ++-------------------------------------------------------------+ +| | +| +--------------+ +--------------+ +--------------+ | +| | Raspberry | | Proxmox | | Windows | | +| | Pi (Himbeer)| | Server | | PC | | +| | .10 | | .20 | | | | +| +------+-------+ +------+-------+ +--------------+ | +| | | | +| | +-------+-------+-------+ | +| | | | | | | +| | v v v v | +| | +-----+ +-----+ +-----+ +-----+ | +| | | .30 | | .50 | | .51 | | .52 | | +| | | LXC | | LXC | | VM | | VM | | +| | |Monit| |Facto| | MC | |VRis | | +| | +-----+ +-----+ +-----+ +-----+ | +| | | ++---------+---------------------------------------------------+ + | + v + +-----------+ + | Docker | + | Services | + +-----------+ +``` + +--- + +## Server-Uebersicht + +### Raspberry Pi (alex@Himbeer) - 192.168.2.10 + +**Rolle:** Reverse Proxy & DNS Management + +| Service | Container | Funktion | +|---------|-----------|----------| +| Nginx Proxy Manager | nginx-proxy-manager | Reverse Proxy + SSL (Let's Encrypt) | +| Cloudflare DDNS | cloudflare-ddns | Dynamische DNS-Updates | +| Nextcloud | nextcloud | Cloud Storage | +| MariaDB | nextcloud-db | Nextcloud Datenbank | + +**NPM Admin-UI:** http://192.168.2.10:81 + +**Cloudflare DDNS Domains:** +- home.dimension47.de +- factorio.dimension47.de +- minecraft.dimension47.de +- monitor.dimension47.de +- grafana.dimension47.de + +--- + +### Proxmox Server - 192.168.2.20 + +**Rolle:** Virtualisierungshost + +| VMID | Name | Typ | IP | Cores | RAM | Funktion | +|------|------|-----|-----|-------|-----|----------| +| 100 | atm10 | VM (QEMU) | .51 | 4 | 12 GB | Minecraft ATM 10 | +| 101 | factorio | LXC | .50 | 2 | 4 GB | Factorio Server | +| 102 | gameserver-monitor | LXC | .30 | 2 | 4 GB | Monitoring Webapp | +| 103 | vrising | VM (QEMU) | .52 | 4 | 12 GB | V Rising Server | + +--- + +### Gameserver Monitor (root@192.168.2.30) - LXC 102 + +**Rolle:** Gameserver Ueberwachung & Administration + +**URL:** https://monitor.dimension47.de + +**Default Login:** admin / admin (Passwort aendern nach erstem Login!) + +**Tech Stack:** +- OS: Debian 13 (Trixie) +- Frontend: React + Vite + TailwindCSS 4 + recharts +- Backend: Node.js + Express +- Auth: JWT + bcrypt + SQLite +- Monitoring: Prometheus + Grafana +- Reverse Proxy: nginx + +**Features:** +- Oeffentliches Dashboard (ohne Login sichtbar) +- Live CPU/RAM Metriken (via SSH) +- Gameserver-Uptime (Prozess-Uptime statt Host-Uptime) +- Metrics History via Prometheus (15m/1h/6h/24h) +- Player Count + Spielerliste via RCON +- Game-Logos neben Servernamen +- Interaktives Navbar-Logo (Hover-Effekt, Link zu zeasy.software) +- Start/Stop/Restart Server (Moderator+) +- Console Logs (live, Moderator+) +- RCON Console (Moderator+) +- Minecraft Whitelist-Verwaltung mit serverseitigem Caching +- Rollensystem: user, moderator, superadmin + +**Prometheus Targets:** +- localhost:9100 (monitor) +- 192.168.2.50:9100 (factorio) +- 192.168.2.51:9100 (minecraft) +- 192.168.2.52:9100 (vrising) + +**Grafana:** https://grafana.dimension47.de + +--- + +### Factorio Server (root@192.168.2.50) - LXC 101 + +| Eigenschaft | Wert | +|-------------|------| +| Runtime | Docker | +| Container | factorio | +| Game Port | 34197/udp | +| RCON Port | 27015 | +| RCON Password | jieTig6IkixaKuu | + +--- + +### Minecraft Server (root@192.168.2.51) - VM 100 + +| Eigenschaft | Wert | +|-------------|------| +| Modpack | All The Mods 10 (ATM10) | +| Runtime | screen | +| Screen Name | minecraft | +| Game Port | 25565 | +| RCON Port | 25575 | +| RCON Password | gsm-mc-2026 | +| Pfad | /opt/minecraft | + +--- + +### V Rising Server (root@192.168.2.52) - VM 103 + +| Eigenschaft | Wert | +|-------------|------| +| Runtime | systemd | +| Service Name | vrising | +| Game Port | 9876/udp, 9877/udp | +| Pfad | /home/steam/vrising | + +--- + +## SSH-Zugang + +Der Gameserver-Monitor (.30) hat SSH-Key-Zugang zu: +- 192.168.2.20 (Proxmox) +- 192.168.2.50 (Factorio) +- 192.168.2.51 (Minecraft) +- 192.168.2.52 (V Rising) + +Key: /root/.ssh/id_ed25519 + +--- + +## Wartung + +### Backend neu starten +``` +ssh root@192.168.2.30 "pkill -f 'node server.js'; cd /opt/gameserver-monitor/backend && node server.js &" +``` + +### Frontend neu bauen +``` +ssh root@192.168.2.30 "cd /opt/gameserver-monitor/frontend && npm run build && nginx -s reload" +``` diff --git a/temp/App.jsx b/temp/App.jsx new file mode 100644 index 0000000..059a9d0 --- /dev/null +++ b/temp/App.jsx @@ -0,0 +1,38 @@ +import { useState } from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { UserProvider } from './context/UserContext' +import Login from './pages/Login' +import Dashboard from './pages/Dashboard' +import ServerDetail from './pages/ServerDetail' + +function App() { + const [token, setToken] = useState(localStorage.getItem('token')) + + const handleLogin = (newToken) => { + localStorage.setItem('token', newToken) + setToken(newToken) + } + + const handleLogout = () => { + localStorage.removeItem('token') + setToken(null) + } + + if (!token) { + return + } + + return ( + + + + } /> + } /> + } /> + + + + ) +} + +export default App diff --git a/temp/Dashboard.jsx b/temp/Dashboard.jsx new file mode 100644 index 0000000..1710409 --- /dev/null +++ b/temp/Dashboard.jsx @@ -0,0 +1,147 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { getServers } from '../api' +import { useUser } from '../context/UserContext' +import ServerCard from '../components/ServerCard' +import SettingsModal from '../components/SettingsModal' +import UserManagement from '../components/UserManagement' + +export default function Dashboard({ onLogout }) { + const navigate = useNavigate() + const { user, token, loading: userLoading, isSuperadmin, role } = useUser() + const [servers, setServers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [showSettings, setShowSettings] = useState(false) + const [showUserMgmt, setShowUserMgmt] = useState(false) + + const fetchServers = async () => { + try { + const data = await getServers(token) + setServers(data) + setError('') + } catch (err) { + setError('Failed to connect to server') + if (err.message.includes('401') || err.message.includes('403')) { + onLogout() + } + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (!userLoading) { + fetchServers() + const interval = setInterval(fetchServers, 10000) + return () => clearInterval(interval) + } + }, [token, userLoading]) + + const roleLabels = { + user: 'Viewer', + moderator: 'Operator', + superadmin: 'Admin' + } + + if (userLoading) { + return ( +
+
Loading...
+
+ ) + } + + const onlineCount = servers.filter(s => s.running).length + const totalPlayers = servers.reduce((sum, s) => sum + (s.players?.online || 0), 0) + + return ( +
+ {/* Header */} +
+
+
+
+

+ Gameserver Monitor +

+
+ + {onlineCount}/{servers.length} online + + | + + {totalPlayers} players + +
+
+ +
+
+
{user?.username}
+
{roleLabels[role]}
+
+ + {isSuperadmin && ( + + )} + + + + +
+
+
+
+ + {/* Main Content */} +
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
Loading servers...
+
+ ) : ( +
+ {servers.map((server) => ( + navigate('/server/' + server.id)} + /> + ))} +
+ )} +
+ + {/* Modals */} + {showSettings && ( + setShowSettings(false)} /> + )} + + {showUserMgmt && ( + setShowUserMgmt(false)} /> + )} +
+ ) +} diff --git a/temp/Dashboard_current.jsx b/temp/Dashboard_current.jsx new file mode 100644 index 0000000..92a3d6a --- /dev/null +++ b/temp/Dashboard_current.jsx @@ -0,0 +1,196 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { getServers } from '../api' +import { useUser } from '../context/UserContext' +import ServerCard from '../components/ServerCard' +import SettingsModal from '../components/SettingsModal' +import UserManagement from '../components/UserManagement' + +export default function Dashboard({ onLogout }) { + const navigate = useNavigate() + const { user, token, loading: userLoading, isSuperadmin, role } = useUser() + const [servers, setServers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [showSettings, setShowSettings] = useState(false) + const [showUserMgmt, setShowUserMgmt] = useState(false) + const [currentTime, setCurrentTime] = useState(new Date()) + + const fetchServers = async () => { + try { + const data = await getServers(token) + setServers(data) + setError('') + } catch (err) { + setError('CONNECTION_FAILED: Unable to reach server cluster') + if (err.message.includes('401') || err.message.includes('403')) { + onLogout() + } + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (!userLoading) { + fetchServers() + const interval = setInterval(fetchServers, 10000) + return () => clearInterval(interval) + } + }, [token, userLoading]) + + useEffect(() => { + const timer = setInterval(() => setCurrentTime(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + const roleLabels = { + user: 'VIEWER', + moderator: 'OPERATOR', + superadmin: 'SYSADMIN' + } + + if (userLoading) { + return ( +
+
INITIALIZING SYSTEM...
+
+ ) + } + + const onlineCount = servers.filter(s => s.running).length + const totalPlayers = servers.reduce((sum, s) => sum + (s.players?.online || 0), 0) + + return ( +
+ {/* Matrix rain overlay */} +
+ + {/* Header */} +
+
+
+
+

+ GAMESERVER_MONITOR +

+
+ + + {onlineCount}/{servers.length} NODES + + | + {totalPlayers} USERS_CONNECTED +
+
+ +
+
+ {currentTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} +
+ +
+
+
{user?.username?.toUpperCase()}
+
[{roleLabels[role]}]
+
+ + {isSuperadmin && ( + + )} + + + + +
+
+
+
+
+ +
+ {/* System Status Bar */} +
+
+ > + SYSTEM_STATUS: {error ? 'ERROR' : 'OPERATIONAL'} +
+ {error && ( +
+ ! + {error} +
+ )} +
+ + {loading ? ( +
+
+ LOADING_NODES... +
+
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+
+ ) : ( +
+ {servers.map((server, index) => ( +
+ navigate(`/server/${server.id}`)} + /> +
+ ))} +
+ )} + + {/* Footer Stats */} +
+
+ REFRESH_RATE: 10s + PROTOCOL: RCON/SSH + METRICS: PROMETHEUS + BUILD: v2.0.0-matrix +
+
+
+ + {/* Modals */} + {showSettings && ( + setShowSettings(false)} /> + )} + + {showUserMgmt && ( + setShowUserMgmt(false)} /> + )} +
+ ) +} diff --git a/temp/Login.jsx b/temp/Login.jsx new file mode 100644 index 0000000..6b4f490 --- /dev/null +++ b/temp/Login.jsx @@ -0,0 +1,86 @@ +import { useState } from 'react' +import { login } from '../api' + +export default function Login({ onLogin }) { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + setError('') + + try { + const { token } = await login(username, password) + onLogin(token) + } catch (err) { + setError('Invalid username or password') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Gameserver Monitor

+

Sign in to your account

+
+ +
+
+ {error && ( +
+ {error} +
+ )} + +
+ + setUsername(e.target.value)} + className="input" + placeholder="Enter your username" + required + autoFocus + /> +
+ +
+ + setPassword(e.target.value)} + className="input" + placeholder="Enter your password" + required + /> +
+ + +
+
+ +

+ Gameserver Monitor v2.0 +

+
+
+ ) +} diff --git a/temp/MetricsChart.jsx b/temp/MetricsChart.jsx new file mode 100644 index 0000000..87cbca4 --- /dev/null +++ b/temp/MetricsChart.jsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from 'react' +import { AreaChart, Area, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import { getMetricsHistory } from '../api' +import { useUser } from '../context/UserContext' + +const GRAFANA_URL = 'https://grafana.dimension47.de' + +export default function MetricsChart({ serverId, serverName, expanded = false }) { + const { token } = useUser() + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [range, setRange] = useState('1h') + + useEffect(() => { + const fetchData = async () => { + try { + const metrics = await getMetricsHistory(token, serverId, range) + setData(metrics) + } catch (err) { + console.error('Failed to fetch metrics:', err) + } finally { + setLoading(false) + } + } + + fetchData() + const interval = setInterval(fetchData, 60000) + return () => clearInterval(interval) + }, [token, serverId, range]) + + const formatTime = (timestamp) => { + const date = new Date(timestamp) + return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + } + + const formatBytes = (bytes) => { + if (bytes < 1024) return bytes.toFixed(0) + ' B/s' + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB/s' + return (bytes / 1024 / 1024).toFixed(1) + ' MB/s' + } + + if (loading) { + return ( +
+
+ LOADING_METRICS... +
+
+ ) + } + + if (!data || data.cpu.length === 0) { + return ( +
+
+ NO_DATA_AVAILABLE +
+
+ ) + } + + const combinedData = data.cpu.map((point, i) => ({ + timestamp: point.timestamp, + cpu: point.value, + memory: data.memory[i]?.value || 0, + networkRx: data.networkRx[i]?.value || 0, + networkTx: data.networkTx[i]?.value || 0 + })) + + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+
{formatTime(label)}
+ {payload.map((entry, i) => ( +
+ {entry.name}: + + {entry.name.includes('Network') ? formatBytes(entry.value) : entry.value.toFixed(1) + '%'} + +
+ ))} +
+ ) + } + return null + } + + const ChartCard = ({ title, dataKey, color, domain, formatter }) => ( +
+
+ {title} + + {formatter + ? formatter(combinedData[combinedData.length - 1]?.[dataKey] || 0) + : (combinedData[combinedData.length - 1]?.[dataKey] || 0).toFixed(1) + '%' + } + +
+ + + + + + + + + {expanded && ( + <> + + formatBytes(v).split(' ')[0] : (v) => v.toFixed(0)} + /> + } /> + + )} + + + +
+ ) + + return ( +
+ {/* Header */} +
+ + // METRICS_HISTORY + +
+ + + 📊 + GRAFANA + +
+
+ + {/* Charts */} + {expanded ? ( +
+ + +
+ + +
+
+ ) : ( +
+ + +
+ )} +
+ ) +} diff --git a/temp/ServerCard.jsx b/temp/ServerCard.jsx new file mode 100644 index 0000000..dfb29c4 --- /dev/null +++ b/temp/ServerCard.jsx @@ -0,0 +1,96 @@ +export default function ServerCard({ server, onClick }) { + const formatUptime = (seconds) => { + const hours = Math.floor(seconds / 3600) + if (hours > 24) { + const days = Math.floor(hours / 24) + return days + 'd ' + (hours % 24) + 'h' + } + const minutes = Math.floor((seconds % 3600) / 60) + return hours + 'h ' + minutes + 'm' + } + + const cpuPercent = Math.min(server.metrics.cpu, 100) + const memPercent = Math.min(server.metrics.memory, 100) + + const getProgressColor = (percent) => { + if (percent > 80) return 'progress-bar-danger' + if (percent > 60) return 'progress-bar-warning' + return 'progress-bar-success' + } + + return ( +
+ {/* Header */} +
+

+ {server.name} +

+ + {server.running ? 'Online' : 'Offline'} + +
+ + {/* Metrics */} +
+ {/* CPU */} +
+
+ CPU + {server.metrics.cpu.toFixed(1)}% +
+
+
+
+
+ + {/* RAM */} +
+
+ Memory + + {server.metrics.memoryUsed?.toFixed(1) || 0} / {server.metrics.memoryTotal?.toFixed(1) || 0} {server.metrics.memoryUnit} + +
+
+
+
+
+
+ + {/* Footer Stats */} +
+
+ {server.players.online} + {server.players.max ? ' / ' + server.players.max : ''} players +
+ {server.running && ( +
+ Uptime: {formatUptime(server.metrics.uptime)} +
+ )} +
+ + {/* Players List */} + {server.players?.list?.length > 0 && ( +
+
+ {server.players.list.map((player, i) => ( + + {player} + + ))} +
+
+ )} +
+ ) +} diff --git a/temp/ServerCard_new.jsx b/temp/ServerCard_new.jsx new file mode 100644 index 0000000..6196258 --- /dev/null +++ b/temp/ServerCard_new.jsx @@ -0,0 +1,109 @@ +import { useState } from 'react' + +export default function ServerCard({ server, onClick }) { + const [isHovered, setIsHovered] = useState(false) + + const formatUptime = (seconds) => { + const hours = Math.floor(seconds / 3600) + if (hours > 24) { + const days = Math.floor(hours / 24) + return days + 'd ' + (hours % 24) + 'h' + } + const minutes = Math.floor((seconds % 3600) / 60) + return hours + 'h ' + minutes + 'm' + } + + const cpuPercent = Math.min(server.metrics.cpu, 100) + const memPercent = Math.min(server.metrics.memory, 100) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Header */} +
+

+ {server.name.toUpperCase()} +

+
+ + + {server.running ? 'ONLINE' : 'OFFLINE'} + +
+
+ + {/* CPU & RAM Bars */} +
+ {/* CPU */} +
+
+ CPU + {server.metrics.cpu.toFixed(1)}% +
+
+
+
+
+ + {/* RAM */} +
+
+ RAM + + {server.metrics.memoryUsed?.toFixed(1) || 0} / {server.metrics.memoryTotal?.toFixed(1) || 0} {server.metrics.memoryUnit} + +
+
+
+
+
+
+ + {/* Stats Row */} +
+
+
{server.players.online}
+
PLAYERS
+
+ {server.running && ( +
+
{formatUptime(server.metrics.uptime)}
+
UPTIME
+
+ )} +
+
{server.metrics.cpuCores}
+
CORES
+
+
+ + {/* Players List */} + {server.players?.list?.length > 0 && ( +
+
+ {server.players.list.map((player, i) => ( + + {player} + + ))} +
+
+ )} + + {/* Click Indicator */} +
+ CLICK FOR DETAILS +
+
+ ) +} diff --git a/temp/ServerDetail.jsx b/temp/ServerDetail.jsx new file mode 100644 index 0000000..895658b --- /dev/null +++ b/temp/ServerDetail.jsx @@ -0,0 +1,323 @@ +import { useState, useEffect, useRef } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { getServers, serverAction, sendRcon, getServerLogs } from '../api' +import { useUser } from '../context/UserContext' +import MetricsChart from '../components/MetricsChart' + +export default function ServerDetail() { + const { serverId } = useParams() + const navigate = useNavigate() + const { token, isModerator } = useUser() + const [server, setServer] = useState(null) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(false) + const [activeTab, setActiveTab] = useState('overview') + const [rconCommand, setRconCommand] = useState('') + const [rconHistory, setRconHistory] = useState([]) + const [logs, setLogs] = useState('') + const logsRef = useRef(null) + const rconRef = useRef(null) + + const fetchServer = async () => { + try { + const servers = await getServers(token) + const found = servers.find(s => s.id === serverId) + if (found) { + setServer(found) + } else { + navigate('/') + } + } catch (err) { + console.error(err) + navigate('/') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchServer() + const interval = setInterval(fetchServer, 10000) + return () => clearInterval(interval) + }, [token, serverId]) + + const handleAction = async (action) => { + setActionLoading(true) + try { + await serverAction(token, server.id, action) + setTimeout(() => { + fetchServer() + setActionLoading(false) + }, 2000) + } catch (err) { + console.error(err) + setActionLoading(false) + } + } + + const handleRcon = async (e) => { + e.preventDefault() + if (!rconCommand.trim()) return + const cmd = rconCommand + setRconCommand('') + try { + const { response } = await sendRcon(token, server.id, cmd) + setRconHistory([...rconHistory, { cmd, res: response, time: new Date() }]) + } catch (err) { + setRconHistory([...rconHistory, { cmd, res: 'Error: ' + err.message, time: new Date(), error: true }]) + } + } + + const fetchLogs = async () => { + try { + const data = await getServerLogs(token, server.id, 20) + setLogs(data.logs || '') + if (logsRef.current) { + logsRef.current.scrollTop = logsRef.current.scrollHeight + } + } catch (err) { + console.error(err) + } + } + + useEffect(() => { + if (activeTab === 'logs' && isModerator && server) { + fetchLogs() + const interval = setInterval(fetchLogs, 5000) + return () => clearInterval(interval) + } + }, [activeTab, isModerator, server]) + + useEffect(() => { + if (rconRef.current) { + rconRef.current.scrollTop = rconRef.current.scrollHeight + } + }, [rconHistory]) + + const formatUptime = (seconds) => { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm' + return hours + 'h ' + minutes + 'm' + } + + const tabs = [ + { id: 'overview', label: 'Overview' }, + { id: 'metrics', label: 'Metrics' }, + ...(isModerator ? [ + { id: 'console', label: 'Console' }, + { id: 'logs', label: 'Logs' }, + ] : []), + ] + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + if (!server) { + return ( +
+
Server not found
+
+ ) + } + + const cpuPercent = Math.min(server.metrics.cpu, 100) + const memPercent = Math.min(server.metrics.memory, 100) + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

{server.name}

+ + {server.running ? 'Online' : 'Offline'} + +
+ {server.running && ( +

+ Uptime: {formatUptime(server.metrics.uptime)} +

+ )} +
+
+ + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+
+
+ + {/* Content */} +
+ {/* Overview Tab */} + {activeTab === 'overview' && ( +
+ {/* Stats Grid */} +
+
+
CPU Usage
+
{server.metrics.cpu.toFixed(1)}%
+
+
+
+
+
+
Memory
+
+ {server.metrics.memoryUsed?.toFixed(1)} {server.metrics.memoryUnit} +
+
+ of {server.metrics.memoryTotal?.toFixed(1)} {server.metrics.memoryUnit} +
+
+
+
Players
+
{server.players.online}
+
+ {server.players.max ? 'of ' + server.players.max + ' max' : 'No limit'} +
+
+
+
CPU Cores
+
{server.metrics.cpuCores}
+
+
+ + {/* Players List */} + {server.players?.list?.length > 0 && ( +
+

Online Players

+
+ {server.players.list.map((player, i) => ( + {player} + ))} +
+
+ )} + + {/* Power Controls */} + {isModerator && ( +
+

Server Controls

+
+ {server.running ? ( + <> + + + + ) : ( + + )} +
+
+ )} +
+ )} + + {/* Metrics Tab */} + {activeTab === 'metrics' && ( + + )} + + {/* Console Tab */} + {activeTab === 'console' && isModerator && ( +
+
+
RCON Console - {server.name}
+ {rconHistory.length === 0 && ( +
Waiting for commands...
+ )} + {rconHistory.map((entry, i) => ( +
+
+ [{entry.time.toLocaleTimeString()}] > {entry.cmd} +
+
+ {entry.res} +
+
+ ))} +
+ +
+ setRconCommand(e.target.value)} + placeholder="Enter command..." + className="input flex-1" + /> + +
+
+ )} + + {/* Logs Tab */} + {activeTab === 'logs' && isModerator && ( +
+
+ Last 20 lines + +
+
+ {logs || 'Loading...'} +
+
+ )} +
+
+ ) +} diff --git a/temp/ServerDetailModal.jsx b/temp/ServerDetailModal.jsx new file mode 100644 index 0000000..bf93c83 --- /dev/null +++ b/temp/ServerDetailModal.jsx @@ -0,0 +1,326 @@ +import { useState, useEffect, useRef } from 'react' +import { serverAction, sendRcon, getServerLogs } from '../api' +import { useUser } from '../context/UserContext' +import MetricsChart from './MetricsChart' + +export default function ServerDetailModal({ server, onClose, onUpdate }) { + const { token, isModerator } = useUser() + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState('overview') + const [rconCommand, setRconCommand] = useState('') + const [rconHistory, setRconHistory] = useState([]) + const [logs, setLogs] = useState('') + const logsRef = useRef(null) + const rconRef = useRef(null) + + const typeIcons = { + minecraft: '⛏️', + factorio: '⚙️' + } + + const handleAction = async (action) => { + setLoading(true) + try { + await serverAction(token, server.id, action) + setTimeout(() => { + onUpdate() + setLoading(false) + }, 2000) + } catch (err) { + console.error(err) + setLoading(false) + } + } + + const handleRcon = async (e) => { + e.preventDefault() + if (!rconCommand.trim()) return + + const cmd = rconCommand + setRconCommand('') + + try { + const { response } = await sendRcon(token, server.id, cmd) + setRconHistory([...rconHistory, { cmd, res: response, time: new Date() }]) + } catch (err) { + setRconHistory([...rconHistory, { cmd, res: 'ERROR: ' + err.message, time: new Date(), error: true }]) + } + } + + const fetchLogs = async () => { + try { + const data = await getServerLogs(token, server.id, 100) + setLogs(data.logs || '') + if (logsRef.current) { + logsRef.current.scrollTop = logsRef.current.scrollHeight + } + } catch (err) { + console.error(err) + } + } + + useEffect(() => { + if (activeTab === 'logs' && isModerator) { + fetchLogs() + const interval = setInterval(fetchLogs, 5000) + return () => clearInterval(interval) + } + }, [activeTab, isModerator]) + + useEffect(() => { + if (rconRef.current) { + rconRef.current.scrollTop = rconRef.current.scrollHeight + } + }, [rconHistory]) + + useEffect(() => { + const handleEsc = (e) => { + if (e.key === 'Escape') onClose() + } + window.addEventListener('keydown', handleEsc) + return () => window.removeEventListener('keydown', handleEsc) + }, [onClose]) + + const formatUptime = (seconds) => { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (days > 0) return `${days}d ${hours}h ${minutes}m` + return `${hours}h ${minutes}m` + } + + const tabs = [ + { id: 'overview', label: 'OVERVIEW', icon: '📊' }, + { id: 'metrics', label: 'METRICS', icon: '📈' }, + ...(isModerator ? [ + { id: 'console', label: 'CONSOLE', icon: '💻' }, + { id: 'logs', label: 'LOGS', icon: '📜' }, + ] : []), + ] + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+
+ {typeIcons[server.type] || '🎮'} +
+

+ {server.name.toUpperCase()} +

+
+ + + {server.running ? 'ONLINE' : 'OFFLINE'} + + | + + UPTIME: {formatUptime(server.metrics.uptime)} + +
+
+
+ + +
+ + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Content */} +
+ {/* Overview Tab */} + {activeTab === 'overview' && ( +
+ {/* Stats Grid */} +
+ + + + +
+ + {/* Players List */} + {server.players?.list?.length > 0 && ( +
+

CONNECTED_USERS:

+
+ {server.players.list.map((player, i) => ( + + {player} + + ))} +
+
+ )} + + {/* Power Controls */} + {isModerator && ( +
+

POWER_CONTROLS:

+
+ {server.running ? ( + <> + + + + ) : ( + + )} +
+
+ )} +
+ )} + + {/* Metrics Tab */} + {activeTab === 'metrics' && ( +
+ +
+ )} + + {/* Console Tab */} + {activeTab === 'console' && isModerator && server.hasRcon && ( +
+
+
+ // RCON Terminal - {server.name} +
+ {rconHistory.length === 0 && ( +
+ Waiting for commands... +
+ )} + {rconHistory.map((entry, i) => ( +
+
+ [{entry.time.toLocaleTimeString()}] > {entry.cmd} +
+
+ {entry.res} +
+
+ ))} +
+ +
+ > + setRconCommand(e.target.value)} + placeholder="Enter RCON command..." + className="input-matrix flex-1 px-4 py-2 rounded font-mono text-sm" + /> + +
+
+ )} + + {/* Logs Tab */} + {activeTab === 'logs' && isModerator && ( +
+
+ + // Server Logs - Last 100 lines + + +
+
+ {logs || 'Loading...'} +
+
+ )} +
+ + {/* Footer */} +
+
+ SERVER_ID: {server.id} + PRESS [ESC] TO CLOSE +
+
+
+
+ ) +} + +function StatBox({ label, value, sub }) { + return ( +
+
{label}
+
{value}
+
{sub}
+
+ ) +} diff --git a/temp/index.css b/temp/index.css new file mode 100644 index 0000000..f7c2774 --- /dev/null +++ b/temp/index.css @@ -0,0 +1,291 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + scrollbar-width: thin; + scrollbar-color: #404040 #171717; +} + +*::-webkit-scrollbar { + width: 8px; +} + +*::-webkit-scrollbar-track { + background: #171717; +} + +*::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 4px; +} + +*::-webkit-scrollbar-thumb:hover { + background: #525252; +} + +body { + background-color: #0a0a0a; + color: #fafafa; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +/* Buttons */ +button { + background: transparent; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + padding: 0.5rem 1rem; + transition: all 0.15s ease; + cursor: pointer; + border: none; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: #fafafa; + color: #0a0a0a; +} + +.btn-primary:hover:not(:disabled) { + background-color: #e5e5e5; +} + +.btn-secondary { + background-color: #262626; + color: #fafafa; +} + +.btn-secondary:hover:not(:disabled) { + background-color: #363636; +} + +.btn-destructive { + background-color: #dc2626; + color: #fafafa; +} + +.btn-destructive:hover:not(:disabled) { + background-color: #b91c1c; +} + +.btn-outline { + background: transparent; + border: 1px solid #404040; + color: #fafafa; +} + +.btn-outline:hover:not(:disabled) { + background-color: #262626; + border-color: #525252; +} + +.btn-ghost { + background: transparent; + color: #a3a3a3; +} + +.btn-ghost:hover:not(:disabled) { + background-color: #262626; + color: #fafafa; +} + +/* Cards */ +.card { + background-color: #171717; + border: 1px solid #262626; + border-radius: 0.5rem; +} + +.card:hover { + border-color: #404040; +} + +.card-clickable { + cursor: pointer; + transition: all 0.15s ease; +} + +.card-clickable:hover { + background-color: #1c1c1c; + border-color: #404040; +} + +/* Inputs */ +.input { + display: flex; + width: 100%; + border-radius: 0.375rem; + border: 1px solid #404040; + background-color: #171717; + color: #fafafa; + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.input:focus { + outline: none; + border-color: #737373; + box-shadow: 0 0 0 2px rgba(115, 115, 115, 0.2); +} + +.input::placeholder { + color: #737373; +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + border-radius: 9999px; + padding: 0.25rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; +} + +.badge-success { + background-color: #14532d; + color: #86efac; +} + +.badge-destructive { + background-color: #450a0a; + color: #fca5a5; +} + +.badge-secondary { + background-color: #262626; + color: #a3a3a3; +} + +/* Progress */ +.progress { + height: 0.5rem; + background-color: #262626; + border-radius: 9999px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background-color: #fafafa; + border-radius: 9999px; + transition: width 0.3s ease; +} + +.progress-bar-success { + background-color: #22c55e; +} + +.progress-bar-warning { + background-color: #eab308; +} + +.progress-bar-danger { + background-color: #ef4444; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0.25rem; + border-bottom: 1px solid #262626; +} + +.tab { + padding: 0.75rem 1rem; + font-size: 0.875rem; + font-weight: 500; + color: #737373; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + cursor: pointer; + transition: all 0.15s ease; + background: transparent; +} + +.tab:hover { + color: #a3a3a3; +} + +.tab-active { + color: #fafafa; + border-bottom-color: #fafafa; +} + +/* Status */ +.status-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 9999px; +} + +.status-online { + background-color: #22c55e; +} + +.status-offline { + background-color: #ef4444; +} + +/* Terminal/Logs */ +.terminal { + background-color: #0a0a0a; + border: 1px solid #262626; + border-radius: 0.375rem; + font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; +} + +.logs-container { + height: 320px; + max-height: 320px; + min-height: 320px; + overflow-y: scroll; +} + +/* Layout */ +.container-main { + max-width: 900px; + margin-left: auto; + margin-right: auto; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +/* Utilities */ +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.fade-in { + animation: fadeIn 0.2s ease-out; +} + +/* Modal */ +.modal-backdrop { + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..b1a7d8b --- /dev/null +++ b/todo.md @@ -0,0 +1,60 @@ +# Homelab TODOs + +## Prioritaet Hoch + +- [ ] **Pentest fuer Server durchfuehren** + - [ ] Portscan aller Server (nmap) + - [ ] SSH-Konfiguration pruefen (fail2ban, Key-Only) + - [ ] RCON-Passwoerter auf Staerke pruefen + - [ ] Firewall-Regeln auditieren + - [ ] SSL/TLS-Konfiguration testen + - [ ] Nginx-Sicherheitsheader pruefen + - [ ] JWT-Secret Rotation implementieren + +- [ ] **GSM Modularisierung & Wiederverwendbarkeit** + - [ ] Server-Typen als Plugins auslagern (minecraft, factorio, vrising, ...) + - [ ] Generisches Interface fuer neue Gameserver-Typen + - [ ] Konfiguration per UI statt config.json + - [ ] Docker-Compose fuer einfaches Deployment + - [ ] Environment-basierte Konfiguration + - [ ] Multi-Instanz-Faehigkeit (mehrere Homelabs) + +## Prioritaet Mittel + +- [ ] Backup-Loesung fuer Gameserver-Welten + - [ ] Automatische Snapshots (taeglich) + - [ ] Offsite-Backup (Nextcloud/S3) + - [ ] Restore-Prozedur dokumentieren + +- [ ] Monitoring-Alerts + - [ ] Discord Webhook bei Server-Crash + - [ ] Email-Benachrichtigung optional + - [ ] Alerting-Regeln in Prometheus/Grafana + +- [ ] Automatische Restarts bei Crash + - [ ] Watchdog-Service implementieren + - [ ] Health-Checks definieren + - [ ] Restart-Limits (kein Endlos-Loop) + +## Prioritaet Niedrig + +- [ ] Dark/Light Mode Toggle im Frontend +- [ ] Server-Logs durchsuchbar machen +- [ ] Scheduled Restarts (z.B. taeglich 4 Uhr) +- [ ] Player-Statistiken (Spielzeit, Join-History) +- [ ] Changelog/Audit-Log fuer Admin-Aktionen + +## Erledigt + +- [x] ~~Admin-Passwort im GSM aenderbar~~ (UI) +- [x] ~~JWT_SECRET sicher setzen~~ +- [x] ~~Prometheus + Grafana installieren~~ +- [x] ~~Grafana extern erreichbar~~ +- [x] ~~Benutzer-Verwaltung (Rollensystem)~~ +- [x] ~~Ressourcen-Graphen (CPU/RAM Historie)~~ +- [x] ~~Oeffentliches Dashboard~~ +- [x] ~~Whitelist-Caching serverseitig~~ +- [x] ~~Gameserver-Uptime statt Host-Uptime~~ +- [x] ~~Game-Logos in UI~~ +- [x] ~~Navbar-Logo mit Hover-Effekt~~ +- [x] ~~V Rising Server hinzugefuegt~~