diff --git a/gsm-frontend/public/openttd.png b/gsm-frontend/public/openttd.png
new file mode 100644
index 0000000..0064c3d
Binary files /dev/null and b/gsm-frontend/public/openttd.png differ
diff --git a/gsm-frontend/public/terraria.png b/gsm-frontend/public/terraria.png
new file mode 100644
index 0000000..0d301a2
Binary files /dev/null and b/gsm-frontend/public/terraria.png differ
diff --git a/gsm-frontend/src/api.js b/gsm-frontend/src/api.js
index 7d48674..b89092b 100644
--- a/gsm-frontend/src/api.js
+++ b/gsm-frontend/src/api.js
@@ -9,6 +9,13 @@ async function fetchAPI(endpoint, options = {}) {
},
})
+ // Auto-logout on auth errors (invalid/expired token)
+ if (response.status === 401 || response.status === 403) {
+ localStorage.removeItem('gsm_token')
+ window.location.href = '/'
+ throw new Error('Session expired')
+ }
+
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }))
throw new Error(error.message || `HTTP ${response.status}`)
@@ -243,6 +250,36 @@ export async function savePalworldConfig(token, filename, content) {
})
}
+// Terraria Config Management
+export async function getTerrariaConfig(token) {
+ return fetchAPI('/servers/terraria/config', {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+}
+
+export async function saveTerrariaConfig(token, content) {
+ return fetchAPI('/servers/terraria/config', {
+ method: 'PUT',
+ headers: { Authorization: `Bearer ${token}` },
+ body: JSON.stringify({ content }),
+ })
+}
+
+// OpenTTD Config Management
+export async function getOpenTTDConfig(token) {
+ return fetchAPI('/servers/openttd/config', {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+}
+
+export async function saveOpenTTDConfig(token, content) {
+ return fetchAPI('/servers/openttd/config', {
+ method: 'PUT',
+ headers: { Authorization: `Bearer ${token}` },
+ body: JSON.stringify({ content }),
+ })
+}
+
// Activity Log
export async function getActivityLog(token, limit = 100) {
return fetchAPI(`/servers/activity-log?limit=${limit}`, {
diff --git a/gsm-frontend/src/components/OpenTTDConfigEditor.jsx b/gsm-frontend/src/components/OpenTTDConfigEditor.jsx
new file mode 100644
index 0000000..b30dfa1
--- /dev/null
+++ b/gsm-frontend/src/components/OpenTTDConfigEditor.jsx
@@ -0,0 +1,244 @@
+import { useState, useEffect, useRef } from 'react'
+import { FileText, Save, RefreshCw, AlertTriangle, Check, X } from 'lucide-react'
+import { getOpenTTDConfig, saveOpenTTDConfig } from '../api'
+
+export default function OpenTTDConfigEditor({ token }) {
+ const [content, setContent] = useState('')
+ const [originalContent, setOriginalContent] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+ const [hasChanges, setHasChanges] = useState(false)
+ const textareaRef = useRef(null)
+ const highlightRef = useRef(null)
+
+ useEffect(() => {
+ loadConfig()
+ }, [token])
+
+ useEffect(() => {
+ setHasChanges(content !== originalContent)
+ }, [content, originalContent])
+
+ const handleScroll = () => {
+ if (highlightRef.current && textareaRef.current) {
+ highlightRef.current.scrollTop = textareaRef.current.scrollTop
+ highlightRef.current.scrollLeft = textareaRef.current.scrollLeft
+ }
+ }
+
+ async function loadConfig() {
+ setLoading(true)
+ setError(null)
+ try {
+ const data = await getOpenTTDConfig(token)
+ setContent(data.content)
+ setOriginalContent(data.content)
+ } catch (err) {
+ setError('Fehler beim Laden der Config: ' + err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function handleSave() {
+ if (!hasChanges) return
+
+ setSaving(true)
+ setError(null)
+ setSuccess(null)
+ try {
+ await saveOpenTTDConfig(token, content)
+ setOriginalContent(content)
+ setSuccess('Config gespeichert! Server-Neustart erforderlich.')
+ setTimeout(() => setSuccess(null), 5000)
+ } catch (err) {
+ setError('Fehler beim Speichern: ' + err.message)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ function handleDiscard() {
+ setContent(originalContent)
+ setError(null)
+ setSuccess(null)
+ }
+
+ function highlightSyntax(text) {
+ if (!text) return ''
+
+ const lines = text.split('\n')
+
+ return lines.map((line) => {
+ let highlighted = line
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+
+ // Section headers [section]
+ if (line.trim().startsWith('[') && line.trim().endsWith(']')) {
+ highlighted = `${highlighted}`
+ }
+ // Comments (;)
+ else if (line.trim().startsWith(';')) {
+ highlighted = `${highlighted}`
+ }
+ // key = value
+ else if (line.includes('=')) {
+ const idx = line.indexOf('=')
+ const key = highlighted.substring(0, idx)
+ const value = highlighted.substring(idx + 1)
+
+ // Color numbers, true/false, and quoted strings
+ let coloredValue = value
+ .replace(/\b(true|false)\b/gi, '$1')
+ .replace(/\b(\d+)\b/g, '$1')
+
+ highlighted = `${key}=${coloredValue}`
+ }
+
+ return highlighted
+ }).join('\n')
+ }
+
+ if (loading) {
+ return (
+
+
+ Lade Config...
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ openttd.cfg - Server-Einstellungen
+
+
+
+
+ {/* Error/Success messages */}
+ {error && (
+
+ )}
+
+ {success && (
+
+
+ {success}
+
+ )}
+
+ {/* Editor */}
+
+
+ {/* Actions */}
+
+ {hasChanges && (
+
+ )}
+
+
+
+
+ {/* Legend */}
+
+
Legende
+
+
+
+ Sektion
+
+
+
+ Kommentare
+
+
+
+ Einstellung
+
+
+
+ Wert
+
+
+
+ Boolean
+
+
+
+ Zahlen
+
+
+
Änderungen werden erst nach Server-Neustart aktiv. Ein Backup wird automatisch erstellt.
+
+
+ )
+}
diff --git a/gsm-frontend/src/components/PalworldConfigEditor.jsx b/gsm-frontend/src/components/PalworldConfigEditor.jsx
index bb5f125..f3b7752 100644
--- a/gsm-frontend/src/components/PalworldConfigEditor.jsx
+++ b/gsm-frontend/src/components/PalworldConfigEditor.jsx
@@ -285,8 +285,8 @@ export default function PalworldConfigEditor({ token }) {
disabled={!hasChanges || saving}
className={"flex items-center gap-2 px-4 py-2 rounded-lg transition-colors " +
(hasChanges && !saving
- ? 'bg-green-600 hover:bg-green-500 text-white'
- : 'bg-gray-700 text-gray-500 cursor-not-allowed'
+ ? 'bg-green-600 hover:bg-green-500 text-white border-2 border-green-400'
+ : 'bg-gray-700 text-gray-500 cursor-not-allowed border-2 border-gray-600'
)}
>
{saving ? (
diff --git a/gsm-frontend/src/components/ServerCard.jsx b/gsm-frontend/src/components/ServerCard.jsx
index 608c23d..687bb41 100644
--- a/gsm-frontend/src/components/ServerCard.jsx
+++ b/gsm-frontend/src/components/ServerCard.jsx
@@ -35,6 +35,20 @@ const serverInfo = {
links: [
{ label: 'Steam', url: 'https://store.steampowered.com/app/1623730/Palworld/' }
]
+ },
+ terraria: {
+ address: 'terraria.zeasy.dev:7777',
+ logo: '/terraria.png',
+ links: [
+ { label: 'Steam', url: 'https://store.steampowered.com/app/105600/Terraria/' }
+ ]
+ },
+ openttd: {
+ address: 'openttd.zeasy.dev:3979',
+ logo: '/openttd.png',
+ links: [
+ { label: 'Steam', url: 'https://store.steampowered.com/app/1536610/OpenTTD/' }
+ ]
}
}
@@ -45,6 +59,8 @@ const getServerInfo = (serverName) => {
if (name.includes('vrising') || name.includes('v rising')) return serverInfo.vrising
if (name.includes('zomboid')) return serverInfo.zomboid
if (name.includes('palworld')) return serverInfo.palworld
+ if (name.includes('terraria')) return serverInfo.terraria
+ if (name.includes('openttd')) return serverInfo.openttd
return null
}
diff --git a/gsm-frontend/src/components/TerrariaConfigEditor.jsx b/gsm-frontend/src/components/TerrariaConfigEditor.jsx
new file mode 100644
index 0000000..38b9444
--- /dev/null
+++ b/gsm-frontend/src/components/TerrariaConfigEditor.jsx
@@ -0,0 +1,230 @@
+import { useState, useEffect, useRef } from 'react'
+import { FileText, Save, RefreshCw, AlertTriangle, Check, X } from 'lucide-react'
+import { getTerrariaConfig, saveTerrariaConfig } from '../api'
+
+export default function TerrariaConfigEditor({ token }) {
+ const [content, setContent] = useState('')
+ const [originalContent, setOriginalContent] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+ const [hasChanges, setHasChanges] = useState(false)
+ const textareaRef = useRef(null)
+ const highlightRef = useRef(null)
+
+ useEffect(() => {
+ loadConfig()
+ }, [token])
+
+ useEffect(() => {
+ setHasChanges(content !== originalContent)
+ }, [content, originalContent])
+
+ const handleScroll = () => {
+ if (highlightRef.current && textareaRef.current) {
+ highlightRef.current.scrollTop = textareaRef.current.scrollTop
+ highlightRef.current.scrollLeft = textareaRef.current.scrollLeft
+ }
+ }
+
+ async function loadConfig() {
+ setLoading(true)
+ setError(null)
+ try {
+ const data = await getTerrariaConfig(token)
+ setContent(data.content)
+ setOriginalContent(data.content)
+ } catch (err) {
+ setError('Fehler beim Laden der Config: ' + err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function handleSave() {
+ if (!hasChanges) return
+
+ setSaving(true)
+ setError(null)
+ setSuccess(null)
+ try {
+ await saveTerrariaConfig(token, content)
+ setOriginalContent(content)
+ setSuccess('Config gespeichert! Server-Neustart erforderlich.')
+ setTimeout(() => setSuccess(null), 5000)
+ } catch (err) {
+ setError('Fehler beim Speichern: ' + err.message)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ function handleDiscard() {
+ setContent(originalContent)
+ setError(null)
+ setSuccess(null)
+ }
+
+ function highlightSyntax(text) {
+ if (!text) return ''
+
+ const lines = text.split('\n')
+
+ return lines.map((line) => {
+ let highlighted = line
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+
+ // Comments (#)
+ if (line.trim().startsWith('#')) {
+ highlighted = `${highlighted}`
+ }
+ // key=value
+ else if (line.includes('=')) {
+ const idx = line.indexOf('=')
+ const key = highlighted.substring(0, idx)
+ const value = highlighted.substring(idx + 1)
+
+ let coloredValue = value
+ .replace(/\b(\d+)\b/g, '$1')
+
+ highlighted = `${key}=${coloredValue}`
+ }
+
+ return highlighted
+ }).join('\n')
+ }
+
+ if (loading) {
+ return (
+
+
+ Lade Config...
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ serverconfig.txt - Server-Einstellungen
+
+
+
+
+ {/* Error/Success messages */}
+ {error && (
+
+ )}
+
+ {success && (
+
+
+ {success}
+
+ )}
+
+ {/* Editor */}
+
+
+ {/* Actions */}
+
+ {hasChanges && (
+
+ )}
+
+
+
+
+ {/* Legend */}
+
+
Legende
+
+
+
+ Kommentare
+
+
+
+ Einstellung
+
+
+
+ Wert
+
+
+
+ Zahlen
+
+
+
Änderungen werden erst nach Server-Neustart aktiv. Ein Backup wird automatisch erstellt.
+
+
+ )
+}
diff --git a/gsm-frontend/src/components/UserManagement.jsx b/gsm-frontend/src/components/UserManagement.jsx
index ee45a0c..acf5b59 100644
--- a/gsm-frontend/src/components/UserManagement.jsx
+++ b/gsm-frontend/src/components/UserManagement.jsx
@@ -1,21 +1,12 @@
import { useState, useEffect } from 'react'
import { useUser } from '../context/UserContext'
-import { getUsers, createUser, updateUserRole, updateUserPassword, deleteUser } from '../api'
+import { getUsers } from '../api'
export default function UserManagement({ onClose }) {
const { token } = useUser()
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
- const [showAddUser, setShowAddUser] = useState(false)
- const [editingUser, setEditingUser] = useState(null)
-
- // Form state
- const [username, setUsername] = useState('')
- const [password, setPassword] = useState('')
- const [role, setRole] = useState('user')
- const [formLoading, setFormLoading] = useState(false)
- const [formError, setFormError] = useState('')
const fetchUsers = async () => {
try {
@@ -33,71 +24,14 @@ export default function UserManagement({ onClose }) {
fetchUsers()
}, [token])
- const resetForm = () => {
- setUsername('')
- setPassword('')
- setRole('user')
- setFormError('')
- setShowAddUser(false)
- setEditingUser(null)
- }
-
- const handleAddUser = async (e) => {
- e.preventDefault()
- setFormError('')
- setFormLoading(true)
-
- try {
- await createUser(token, { username, password, role })
- await fetchUsers()
- resetForm()
- } catch (err) {
- setFormError(err.message || 'Failed to create user')
- } finally {
- setFormLoading(false)
+ const getAvatarUrl = (user) => {
+ const discordId = user.discord_id || user.discordId
+ if (user.avatar && discordId) {
+ return `https://cdn.discordapp.com/avatars/${discordId}/${user.avatar}.png?size=64`
}
- }
-
- const handleUpdateUser = async (e) => {
- e.preventDefault()
- setFormError('')
- setFormLoading(true)
-
- try {
- // Update role if changed
- if (role !== editingUser.role) {
- await updateUserRole(token, editingUser.id, role)
- }
- // Update password if provided
- if (password) {
- await updateUserPassword(token, editingUser.id, password)
- }
- await fetchUsers()
- resetForm()
- } catch (err) {
- setFormError(err.message || 'Failed to update user')
- } finally {
- setFormLoading(false)
- }
- }
-
- const handleDeleteUser = async (userId) => {
- if (!confirm('Are you sure you want to delete this user?')) return
-
- try {
- await deleteUser(token, userId)
- await fetchUsers()
- } catch (err) {
- setError(err.message || 'Failed to delete user')
- }
- }
-
- const startEdit = (user) => {
- setEditingUser(user)
- setUsername(user.username)
- setRole(user.role)
- setPassword('')
- setShowAddUser(false)
+ // Default Discord avatar
+ const defaultIndex = discordId ? parseInt(discordId) % 5 : 0
+ return `https://cdn.discordapp.com/embed/avatars/${defaultIndex}.png`
}
const roleLabels = {
@@ -106,6 +40,12 @@ export default function UserManagement({ onClose }) {
superadmin: 'Admin'
}
+ const roleColors = {
+ user: 'text-gray-400',
+ moderator: 'text-blue-400',
+ superadmin: 'text-amber-400'
+ }
+
return (
e.stopPropagation()}>
@@ -125,109 +65,36 @@ export default function UserManagement({ onClose }) {
{loading ? (
Loading users...
) : (
-
+
{users.map((user) => (
-
-
-
{user.username}
-
{roleLabels[user.role]}
-
-
-
-
+
+
})
+
+
{user.username}
+
+
+ {roleLabels[user.role]}
+
+ {(user.discord_id || user.discordId) && (
+ ID: {user.discord_id || user.discordId}
+ )}
+
))}
)}
- {/* Add/Edit Form */}
- {(showAddUser || editingUser) ? (
-
- ) : (
-
- )}
+ {/* Info about Discord management */}
+
+
+ Benutzer und Rollen werden über Discord verwaltet.
+
+
diff --git a/gsm-frontend/src/components/ZomboidConfigEditor.jsx b/gsm-frontend/src/components/ZomboidConfigEditor.jsx
index 493bac0..014b8f2 100644
--- a/gsm-frontend/src/components/ZomboidConfigEditor.jsx
+++ b/gsm-frontend/src/components/ZomboidConfigEditor.jsx
@@ -267,8 +267,8 @@ export default function ZomboidConfigEditor({ token }) {
disabled={!hasChanges || saving}
className={"flex items-center gap-2 px-4 py-2 rounded-lg transition-colors " +
(hasChanges && !saving
- ? 'bg-green-600 hover:bg-green-500 text-white'
- : 'bg-gray-700 text-gray-500 cursor-not-allowed'
+ ? 'bg-green-600 hover:bg-green-500 text-white border-2 border-green-400'
+ : 'bg-gray-700 text-gray-500 cursor-not-allowed border-2 border-gray-600'
)}
>
{saving ? (
diff --git a/gsm-frontend/src/pages/Dashboard.jsx b/gsm-frontend/src/pages/Dashboard.jsx
index c48bc17..091e6b3 100644
--- a/gsm-frontend/src/pages/Dashboard.jsx
+++ b/gsm-frontend/src/pages/Dashboard.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
-import { getServers } from '../api'
+import { getServers, getAllDisplaySettings } from '../api'
import { useUser } from '../context/UserContext'
import ServerCard from '../components/ServerCard'
import UserManagement from '../components/UserManagement'
@@ -11,6 +11,7 @@ export default function Dashboard({ onLogin, onLogout }) {
const navigate = useNavigate()
const { user, token, loading: userLoading, isSuperadmin, role } = useUser()
const [servers, setServers] = useState([])
+ const [displaySettings, setDisplaySettings] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showUserMgmt, setShowUserMgmt] = useState(false)
@@ -22,8 +23,12 @@ export default function Dashboard({ onLogin, onLogout }) {
const fetchServers = async () => {
try {
- const data = await getServers(token)
+ const [data, settings] = await Promise.all([
+ getServers(token),
+ getAllDisplaySettings(token)
+ ])
setServers(data)
+ setDisplaySettings(settings)
setError('')
} catch (err) {
if (err.message.includes('401') || err.message.includes('403')) {
@@ -225,6 +230,7 @@ export default function Dashboard({ onLogin, onLogout }) {
server={server}
onClick={() => navigate('/server/' + server.id)}
isAuthenticated={isAuthenticated}
+ displaySettings={displaySettings[server.id]}
/>
))}
diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx
index 0d78063..d58d95c 100644
--- a/gsm-frontend/src/pages/ServerDetail.jsx
+++ b/gsm-frontend/src/pages/ServerDetail.jsx
@@ -6,6 +6,8 @@ import MetricsChart from '../components/MetricsChart'
import FactorioWorldManager from '../components/FactorioWorldManager'
import PalworldConfigEditor from '../components/PalworldConfigEditor'
import ZomboidConfigEditor from '../components/ZomboidConfigEditor'
+import TerrariaConfigEditor from '../components/TerrariaConfigEditor'
+import OpenTTDConfigEditor from '../components/OpenTTDConfigEditor'
const getServerLogo = (serverName) => {
const name = serverName.toLowerCase()
@@ -14,6 +16,8 @@ const getServerLogo = (serverName) => {
if (name.includes("vrising") || name.includes("v rising")) return "/vrising.png"
if (name.includes("zomboid")) return "/zomboid.png"
if (name.includes("palworld")) return "/palworld.png"
+ if (name.includes("terraria")) return "/terraria.png"
+ if (name.includes("openttd")) return "/openttd.png"
return null
}
export default function ServerDetail() {
@@ -278,9 +282,15 @@ const formatUptime = (seconds) => {
...(isModerator && server?.type === 'palworld' ? [
{ id: 'config', label: 'Config' },
] : []),
+ ...(isModerator && server?.type === 'terraria' ? [
+ { id: 'config', label: 'Config' },
+ ] : []),
...(isModerator && server?.type === 'zomboid' ? [
{ id: 'zomboid-config', label: 'Config' },
] : []),
+ ...(isModerator && server?.type === 'openttd' ? [
+ { id: 'openttd-config', label: 'Config' },
+ ] : []),
...(isModerator ? [
{ id: 'settings', label: 'Einstellungen' },
] : []),
@@ -600,6 +610,13 @@ const formatUptime = (seconds) => {
)}
+ {/* Config Tab - Terraria only */}
+ {activeTab === 'config' && isModerator && server.type === 'terraria' && (
+
+
+
+ )}
+
{/* Config Tab - Zomboid only */}
{activeTab === 'zomboid-config' && isModerator && server.type === 'zomboid' && (
@@ -607,6 +624,13 @@ const formatUptime = (seconds) => {
)}
+ {/* Config Tab - OpenTTD only */}
+ {activeTab === 'openttd-config' && isModerator && server.type === 'openttd' && (
+
+
+
+ )}
+
{/* Settings Tab */}
{activeTab === 'settings' && isModerator && (