Add modular server update feature with Discord notifications
All checks were successful
Deploy GSM / deploy (push) Successful in 25s

- Add serverUpdates.js service with handler registry for extensibility
- Implement Factorio Docker image update (pull + container recreate)
- Add GET/POST /servers/:id/update routes for check/perform
- Add ServerUpdateButton component with auto-check and confirm dialog
- Integrate update card in ServerDetail overview tab
- Auto-send Discord notification on successful update

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Zielonka
2026-01-23 11:42:22 +01:00
parent ed60bc33c7
commit d840faeda9
5 changed files with 540 additions and 0 deletions

View File

@@ -332,5 +332,20 @@ export async function sendDiscordUpdate(token, title, description, serverType, c
})
}
// Server Updates
export async function checkServerUpdate(token, serverId) {
return fetchAPI(`/servers/${serverId}/update`, {
headers: { Authorization: `Bearer ${token}` },
})
}
export async function performServerUpdate(token, serverId, sendDiscord = true) {
return fetchAPI(`/servers/${serverId}/update`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ sendDiscord }),
})
}
// Alias for backwards compatibility
export const setDisplaySettings = saveDisplaySettings

View File

@@ -0,0 +1,130 @@
import { useState, useEffect } from 'react'
import { checkServerUpdate, performServerUpdate } from '../api'
import ConfirmModal from './ConfirmModal'
export default function ServerUpdateButton({ server, token, onUpdateComplete }) {
const [checking, setChecking] = useState(false)
const [updating, setUpdating] = useState(false)
const [updateInfo, setUpdateInfo] = useState(null)
const [error, setError] = useState(null)
const [showConfirm, setShowConfirm] = useState(false)
const isServerRunning = server.status === 'online' || server.status === 'starting' || server.status === 'stopping' || server.running
// Check for updates when component mounts or server status changes
useEffect(() => {
if (!isServerRunning && token) {
handleCheckUpdate()
}
}, [server.status, server.running])
const handleCheckUpdate = async () => {
setChecking(true)
setError(null)
try {
const result = await checkServerUpdate(token, server.id)
setUpdateInfo(result)
} catch (err) {
setError(err.message)
setUpdateInfo(null)
} finally {
setChecking(false)
}
}
const handleUpdate = async () => {
setShowConfirm(false)
setUpdating(true)
setError(null)
try {
const result = await performServerUpdate(token, server.id, true)
setUpdateInfo({ hasUpdate: false, message: result.message })
if (onUpdateComplete) {
onUpdateComplete(result)
}
} catch (err) {
setError(err.message)
} finally {
setUpdating(false)
}
}
// Don't show if server type doesn't support updates
if (updateInfo && !updateInfo.supported) {
return null
}
return (
<div className="flex items-center gap-3 flex-wrap">
{/* Check for Updates Button */}
<button
onClick={handleCheckUpdate}
disabled={checking || updating || isServerRunning}
className="btn btn-secondary"
title={isServerRunning ? 'Server muss gestoppt sein' : 'Auf Updates prüfen'}
>
{checking ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Prüfe...
</>
) : (
'Auf Updates prüfen'
)}
</button>
{/* Update Button - only show if update available */}
{updateInfo?.hasUpdate && (
<button
onClick={() => setShowConfirm(true)}
disabled={updating || isServerRunning}
className="btn btn-primary"
>
{updating ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Aktualisiere...
</>
) : (
'Jetzt updaten'
)}
</button>
)}
{/* Status Badge */}
{updateInfo && !checking && (
<span className={`badge ${updateInfo.hasUpdate ? 'badge-warning' : 'badge-success'}`}>
{updateInfo.message}
</span>
)}
{/* Error Message */}
{error && (
<span className="text-red-400 text-sm">{error}</span>
)}
{/* Disabled Hint */}
{isServerRunning && (
<span className="text-neutral-500 text-sm">Server muss offline sein</span>
)}
{/* Confirm Modal */}
{showConfirm && (
<ConfirmModal
title="Server aktualisieren?"
message={`Möchtest du ${server.name} aktualisieren? Dies wird auch eine Discord-Benachrichtigung senden.`}
confirmText="Aktualisieren"
variant="primary"
onConfirm={handleUpdate}
onCancel={() => setShowConfirm(false)}
/>
)}
</div>
)
}

View File

@@ -10,6 +10,7 @@ import ZomboidConfigEditor from '../components/ZomboidConfigEditor'
import TerrariaConfigEditor from '../components/TerrariaConfigEditor'
import OpenTTDConfigEditor from '../components/OpenTTDConfigEditor'
import HytaleConfigEditor from '../components/HytaleConfigEditor'
import ServerUpdateButton from '../components/ServerUpdateButton'
const getServerLogo = (serverName) => {
const name = serverName.toLowerCase()
@@ -493,6 +494,21 @@ const formatUptime = (seconds) => {
</div>
</div>
)}
{/* Server Update */}
{isModerator && server.type === 'factorio' && (
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-3">Server Update</h3>
<p className="text-neutral-500 text-sm mb-4">
Prüfe auf neue Versionen und aktualisiere den Server
</p>
<ServerUpdateButton
server={server}
token={token}
onUpdateComplete={() => fetchServer()}
/>
</div>
)}
</div>
)}