refactor: GPU Infrastruktur aus Core Admin entfernt (liegt im Lehrer)
All checks were successful
CI / test-python-voice (push) Successful in 28s
CI / test-bqas (push) Successful in 28s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 30s
All checks were successful
CI / test-python-voice (push) Successful in 28s
CI / test-bqas (push) Successful in 28s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 30s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,390 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GPU Infrastructure Admin Page
|
|
||||||
*
|
|
||||||
* vast.ai GPU Management for LLM Processing
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
|
||||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
||||||
|
|
||||||
interface VastStatus {
|
|
||||||
instance_id: number | null
|
|
||||||
status: string
|
|
||||||
gpu_name: string | null
|
|
||||||
dph_total: number | null
|
|
||||||
endpoint_base_url: string | null
|
|
||||||
last_activity: string | null
|
|
||||||
auto_shutdown_in_minutes: number | null
|
|
||||||
total_runtime_hours: number | null
|
|
||||||
total_cost_usd: number | null
|
|
||||||
account_credit: number | null
|
|
||||||
account_total_spend: number | null
|
|
||||||
session_runtime_minutes: number | null
|
|
||||||
session_cost_usd: number | null
|
|
||||||
message: string | null
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GPUInfrastructurePage() {
|
|
||||||
const [status, setStatus] = useState<VastStatus | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [message, setMessage] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const API_PROXY = '/api/admin/gpu'
|
|
||||||
|
|
||||||
const fetchStatus = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_PROXY)
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || `HTTP ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(data)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Verbindungsfehler')
|
|
||||||
setStatus({
|
|
||||||
instance_id: null,
|
|
||||||
status: 'error',
|
|
||||||
gpu_name: null,
|
|
||||||
dph_total: null,
|
|
||||||
endpoint_base_url: null,
|
|
||||||
last_activity: null,
|
|
||||||
auto_shutdown_in_minutes: null,
|
|
||||||
total_runtime_hours: null,
|
|
||||||
total_cost_usd: null,
|
|
||||||
account_credit: null,
|
|
||||||
account_total_spend: null,
|
|
||||||
session_runtime_minutes: null,
|
|
||||||
session_cost_usd: null,
|
|
||||||
message: 'Verbindung fehlgeschlagen'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStatus()
|
|
||||||
}, [fetchStatus])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(fetchStatus, 30000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [fetchStatus])
|
|
||||||
|
|
||||||
const powerOn = async () => {
|
|
||||||
setActionLoading('on')
|
|
||||||
setError(null)
|
|
||||||
setMessage(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_PROXY, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ action: 'on' }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessage('Start angefordert')
|
|
||||||
setTimeout(fetchStatus, 3000)
|
|
||||||
setTimeout(fetchStatus, 10000)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Fehler beim Starten')
|
|
||||||
fetchStatus()
|
|
||||||
} finally {
|
|
||||||
setActionLoading(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const powerOff = async () => {
|
|
||||||
setActionLoading('off')
|
|
||||||
setError(null)
|
|
||||||
setMessage(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_PROXY, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ action: 'off' }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessage('Stop angefordert')
|
|
||||||
setTimeout(fetchStatus, 3000)
|
|
||||||
setTimeout(fetchStatus, 10000)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Fehler beim Stoppen')
|
|
||||||
fetchStatus()
|
|
||||||
} finally {
|
|
||||||
setActionLoading(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusBadge = (s: string) => {
|
|
||||||
const baseClasses = 'px-3 py-1 rounded-full text-sm font-semibold uppercase'
|
|
||||||
switch (s) {
|
|
||||||
case 'running':
|
|
||||||
return `${baseClasses} bg-green-100 text-green-800`
|
|
||||||
case 'stopped':
|
|
||||||
case 'exited':
|
|
||||||
return `${baseClasses} bg-red-100 text-red-800`
|
|
||||||
case 'loading':
|
|
||||||
case 'scheduling':
|
|
||||||
case 'creating':
|
|
||||||
case 'starting...':
|
|
||||||
case 'stopping...':
|
|
||||||
return `${baseClasses} bg-yellow-100 text-yellow-800`
|
|
||||||
default:
|
|
||||||
return `${baseClasses} bg-slate-100 text-slate-600`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCreditColor = (credit: number | null) => {
|
|
||||||
if (credit === null) return 'text-slate-500'
|
|
||||||
if (credit < 5) return 'text-red-600'
|
|
||||||
if (credit < 15) return 'text-yellow-600'
|
|
||||||
return 'text-green-600'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Page Purpose */}
|
|
||||||
<PagePurpose
|
|
||||||
title="GPU Infrastruktur"
|
|
||||||
purpose="Verwalten Sie die vast.ai GPU-Instanzen fuer LLM-Verarbeitung und OCR. Starten/Stoppen Sie GPUs bei Bedarf und ueberwachen Sie Kosten in Echtzeit."
|
|
||||||
audience={['DevOps', 'Entwickler', 'System-Admins']}
|
|
||||||
architecture={{
|
|
||||||
services: ['vast.ai API', 'Ollama', 'VLLM'],
|
|
||||||
databases: ['PostgreSQL (Logs)'],
|
|
||||||
}}
|
|
||||||
relatedPages={[
|
|
||||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
|
||||||
{ name: 'Builds', href: '/infrastructure/builds', description: 'CI/CD Pipeline' },
|
|
||||||
]}
|
|
||||||
collapsible={true}
|
|
||||||
defaultCollapsed={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Status Cards */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">Status</div>
|
|
||||||
{loading ? (
|
|
||||||
<span className="px-3 py-1 rounded-full text-sm font-semibold bg-slate-100 text-slate-600">
|
|
||||||
Laden...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className={getStatusBadge(
|
|
||||||
actionLoading === 'on' ? 'starting...' :
|
|
||||||
actionLoading === 'off' ? 'stopping...' :
|
|
||||||
status?.status || 'unknown'
|
|
||||||
)}>
|
|
||||||
{actionLoading === 'on' ? 'starting...' :
|
|
||||||
actionLoading === 'off' ? 'stopping...' :
|
|
||||||
status?.status || 'unbekannt'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">GPU</div>
|
|
||||||
<div className="font-semibold text-slate-900">
|
|
||||||
{status?.gpu_name || '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">Kosten/h</div>
|
|
||||||
<div className="font-semibold text-slate-900">
|
|
||||||
{status?.dph_total ? `$${status.dph_total.toFixed(3)}` : '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">Auto-Stop</div>
|
|
||||||
<div className="font-semibold text-slate-900">
|
|
||||||
{status && status.auto_shutdown_in_minutes !== null
|
|
||||||
? `${status.auto_shutdown_in_minutes} min`
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">Budget</div>
|
|
||||||
<div className={`font-bold text-lg ${getCreditColor(status?.account_credit ?? null)}`}>
|
|
||||||
{status && status.account_credit !== null
|
|
||||||
? `$${status.account_credit.toFixed(2)}`
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-slate-500 mb-2">Session</div>
|
|
||||||
<div className="font-semibold text-slate-900">
|
|
||||||
{status && status.session_runtime_minutes !== null && status.session_cost_usd !== null
|
|
||||||
? `${Math.round(status.session_runtime_minutes)} min / $${status.session_cost_usd.toFixed(3)}`
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<div className="flex items-center gap-4 mt-6 pt-6 border-t border-slate-200">
|
|
||||||
<button
|
|
||||||
onClick={powerOn}
|
|
||||||
disabled={actionLoading !== null || status?.status === 'running'}
|
|
||||||
className="px-6 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Starten
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={powerOff}
|
|
||||||
disabled={actionLoading !== null || status?.status !== 'running'}
|
|
||||||
className="px-6 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
Stoppen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={fetchStatus}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
{loading ? 'Aktualisiere...' : 'Aktualisieren'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{message && (
|
|
||||||
<span className="ml-4 text-sm text-green-600 font-medium">{message}</span>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<span className="ml-4 text-sm text-red-600 font-medium">{error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Extended Stats */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
||||||
<h3 className="font-semibold text-slate-900 mb-4">Kosten-Uebersicht</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Session Laufzeit</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status && status.session_runtime_minutes !== null
|
|
||||||
? `${Math.round(status.session_runtime_minutes)} Minuten`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Session Kosten</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status && status.session_cost_usd !== null
|
|
||||||
? `$${status.session_cost_usd.toFixed(4)}`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center pt-4 border-t border-slate-100">
|
|
||||||
<span className="text-slate-600">Gesamtlaufzeit</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status && status.total_runtime_hours !== null
|
|
||||||
? `${status.total_runtime_hours.toFixed(1)} Stunden`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Gesamtkosten</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status && status.total_cost_usd !== null
|
|
||||||
? `$${status.total_cost_usd.toFixed(2)}`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">vast.ai Ausgaben</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status && status.account_total_spend !== null
|
|
||||||
? `$${status.account_total_spend.toFixed(2)}`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
||||||
<h3 className="font-semibold text-slate-900 mb-4">Instanz-Details</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Instanz ID</span>
|
|
||||||
<span className="font-mono text-sm">
|
|
||||||
{status?.instance_id || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">GPU</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status?.gpu_name || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Stundensatz</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{status?.dph_total ? `$${status.dph_total.toFixed(4)}/h` : '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-slate-600">Letzte Aktivitaet</span>
|
|
||||||
<span className="text-sm">
|
|
||||||
{status?.last_activity
|
|
||||||
? new Date(status.last_activity).toLocaleString('de-DE')
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{status?.endpoint_base_url && status.status === 'running' && (
|
|
||||||
<div className="pt-4 border-t border-slate-100">
|
|
||||||
<div className="text-slate-600 text-sm mb-1">Endpoint</div>
|
|
||||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded block overflow-x-auto">
|
|
||||||
{status.endpoint_base_url}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Info */}
|
|
||||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-orange-900">Auto-Shutdown</h4>
|
|
||||||
<p className="text-sm text-orange-800 mt-1">
|
|
||||||
Die GPU-Instanz wird automatisch gestoppt, wenn sie laengere Zeit inaktiv ist.
|
|
||||||
Der Status wird alle 30 Sekunden automatisch aktualisiert.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -38,15 +38,6 @@ export const navigation: NavCategory[] = [
|
|||||||
colorClass: 'infrastructure',
|
colorClass: 'infrastructure',
|
||||||
description: 'GPU, Security, CI/CD & Monitoring',
|
description: 'GPU, Security, CI/CD & Monitoring',
|
||||||
modules: [
|
modules: [
|
||||||
{
|
|
||||||
id: 'gpu',
|
|
||||||
name: 'GPU Infrastruktur',
|
|
||||||
href: '/infrastructure/gpu',
|
|
||||||
description: 'vast.ai GPU Management',
|
|
||||||
purpose: 'GPU-Instanzen auf vast.ai fuer ML-Training und Inferenz verwalten.',
|
|
||||||
audience: ['DevOps', 'Entwickler'],
|
|
||||||
subgroup: 'Compute',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'middleware',
|
id: 'middleware',
|
||||||
name: 'Middleware',
|
name: 'Middleware',
|
||||||
|
|||||||
Reference in New Issue
Block a user