- LLM Compare Seiten, Configs und alle Referenzen geloescht - Kommunikation-Kategorie in Sidebar mit Video & Chat, Voice Service, Alerts - Compliance SDK Kategorie aus Sidebar entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
391 lines
14 KiB
TypeScript
391 lines
14 KiB
TypeScript
'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>
|
|
)
|
|
}
|