diff --git a/src/app/api/aos/instances/[id]/route.ts b/src/app/api/aos/instances/[id]/route.ts index cdb9e44..c8f51f9 100644 --- a/src/app/api/aos/instances/[id]/route.ts +++ b/src/app/api/aos/instances/[id]/route.ts @@ -8,7 +8,7 @@ export const dynamic = 'force-dynamic' export const maxDuration = 300 const patchSchema = z.object({ - action: z.enum(['suspend', 'reactivate', 'extend', 'retry']), + action: z.enum(['suspend', 'reactivate', 'extend', 'retry', 'redeploy', 'stop', 'start']), extendDays: z.number().int().min(1).max(90).optional(), }) @@ -143,6 +143,68 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st return NextResponse.json({ ok: true, status: 'requested', message: 'Instance queued for re-provisioning' }) } + if (action === 'redeploy') { + if (!['demo_active', 'active', 'failed'].includes(instance.status)) { + return NextResponse.json({ error: `cannot redeploy instance in status ${instance.status}` }, { status: 400 }) + } + if (dokploy && instance.dokployAppId) { + try { + await dokploy.redeployApplication(instance.dokployAppId) + } catch (e) { + await db.instanceEvent.create({ + data: { instanceId: id, type: 'redeploy.error', step: 'dokploy', result: 'error', details: JSON.stringify({ error: e instanceof Error ? e.message : 'unknown' }) }, + }) + } + } + await db.instanceEvent.create({ + data: { instanceId: id, type: 'redeploy', step: 'done', result: 'ok' }, + }) + return NextResponse.json({ ok: true, status: 'redeploying' }) + } + + if (action === 'stop') { + if (!['demo_active', 'active'].includes(instance.status)) { + return NextResponse.json({ error: `cannot stop instance in status ${instance.status}` }, { status: 400 }) + } + if (dokploy && instance.dokployAppId) { + try { + await dokploy.stopApplication(instance.dokployAppId) + } catch (e) { + await db.instanceEvent.create({ + data: { instanceId: id, type: 'stop.error', step: 'dokploy', result: 'error', details: JSON.stringify({ error: e instanceof Error ? e.message : 'unknown' }) }, + }) + } + } + await db.instanceEvent.create({ + data: { instanceId: id, type: 'stop', step: 'done', result: 'ok' }, + }) + return NextResponse.json({ ok: true, status: 'stopped' }) + } + + if (action === 'start') { + if (!['grace', 'failed'].includes(instance.status)) { + return NextResponse.json({ error: `cannot start instance in status ${instance.status}` }, { status: 400 }) + } + if (dokploy && instance.dokployAppId) { + try { + await dokploy.startApplication(instance.dokployAppId) + } catch (e) { + await db.instanceEvent.create({ + data: { instanceId: id, type: 'start.error', step: 'dokploy', result: 'error', details: JSON.stringify({ error: e instanceof Error ? e.message : 'unknown' }) }, + }) + } + } + const newStatus = instance.demoExpiresAt && new Date(instance.demoExpiresAt) > new Date() ? 'demo_active' : 'active' + await db.instance.update({ + where: { id }, + data: { status: newStatus, graceEndsAt: null }, + }) + await db.instanceEvent.create({ + data: { instanceId: id, type: 'start', step: 'done', result: 'ok', details: JSON.stringify({ status: newStatus }) }, + }) + return NextResponse.json({ ok: true, status: newStatus }) + } + return NextResponse.json({ error: 'unknown action' }, { status: 400 }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' diff --git a/src/components/aos/views/instances.tsx b/src/components/aos/views/instances.tsx index 4b73597..06afbf8 100644 --- a/src/components/aos/views/instances.tsx +++ b/src/components/aos/views/instances.tsx @@ -12,6 +12,8 @@ import { ExternalLink, Ban, Play, + Pause, + Square, RotateCcw, CalendarPlus, Trash2, @@ -20,6 +22,7 @@ import { XCircle, CircleDashed, Network, + RefreshCw, } from 'lucide-react' import { useFetch } from '@/hooks/use-fetch' import { Card } from '@/components/ui/card' @@ -554,7 +557,7 @@ function InstanceDetailSheet({ }, [open]) const handleAction = async ( - action: 'suspend' | 'reactivate' | 'extend' | 'retry', + action: 'suspend' | 'reactivate' | 'extend' | 'retry' | 'redeploy' | 'stop' | 'start', label: string, extendDays?: number ) => { @@ -904,9 +907,12 @@ function InstanceDetailSheet({ status={status} busyAction={busyAction} onSuspend={() => handleAction('suspend', 'Suspendieren')} - onReactivate={() => handleAction('reactivate', 'Reaktivieren')} + onReactivate={() => handleAction('reactivate', 'Starten')} onExtend={() => handleAction('extend', 'Demo verlängern', 7)} onRetry={() => handleAction('retry', 'Provisioning wiederholen')} + onRedeploy={() => handleAction('redeploy', 'Redeploy')} + onStop={() => handleAction('stop', 'Stop')} + onStart={() => handleAction('start', 'Start')} onDelete={() => setConfirmingDelete(true)} /> {['demo_active', 'active'].includes(status) && ( @@ -1042,6 +1048,9 @@ function ActionsRow({ onReactivate, onExtend, onRetry, + onRedeploy, + onStop, + onStart, onDelete, }: { status: string @@ -1050,6 +1059,9 @@ function ActionsRow({ onReactivate: () => void onExtend: () => void onRetry: () => void + onRedeploy: () => void + onStop: () => void + onStart: () => void onDelete: () => void }) { const busy = busyAction !== null @@ -1079,43 +1091,30 @@ function ActionsRow({ ) } - if (status === 'demo_active') { + if (status === 'demo_active' || status === 'active') { return ( -
- Für diesen Status sind keine Aktionen verfügbar. -
- ) + return null } // --------------------------------------------------------------------------- diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index 772980a..79d658e 100755 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -21,7 +21,7 @@ export function useFetch