Add modular server update feature with Discord notifications
All checks were successful
Deploy GSM / deploy (push) Successful in 25s
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:
@@ -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
|
||||
|
||||
130
gsm-frontend/src/components/ServerUpdateButton.jsx
Normal file
130
gsm-frontend/src/components/ServerUpdateButton.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user