diff --git a/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts b/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts
index 4f25d6b..bca5f46 100644
--- a/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts
+++ b/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts
@@ -210,3 +210,62 @@ export async function listGhostDirectories(): Promise<{ ok: boolean; data?: Ghos
return { ok: false, message: 'Network error while loading ghost directories.' };
}
}
+
+export async function downloadExoscaleArchive(): Promise<{ ok: boolean; fileName?: string; message?: string }> {
+ const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
+ const url = `${BASE_URL}/api/admin/dev/exoscale/download-archive`;
+
+ log('š§Ŗ Dev Exoscale: GET', url);
+ try {
+ const res = await authFetch(url, { method: 'GET', headers: { Accept: 'application/zip' } });
+
+ if (res.status === 401) {
+ return { ok: false, message: 'Unauthorized. Please log in.' };
+ }
+
+ let body: any = null;
+ if (res.status === 403 || !res.ok) {
+ try {
+ body = await res.clone().json();
+ } catch {
+ try {
+ body = await res.text();
+ } catch {
+ body = null;
+ }
+ }
+ }
+
+ if (res.status === 403) {
+ return {
+ ok: false,
+ message: typeof body === 'string' ? body : body?.error || 'Forbidden. Admin access required.'
+ };
+ }
+
+ if (!res.ok) {
+ return {
+ ok: false,
+ message: typeof body === 'string' ? body : body?.error || 'Failed to download Exoscale archive.'
+ };
+ }
+
+ const blob = await res.blob();
+ const contentDisposition = res.headers.get('content-disposition') || '';
+ const fileNameMatch = contentDisposition.match(/filename="?([^";]+)"?/i);
+ const fileName = fileNameMatch?.[1] || `exoscale-storage-${Date.now()}.zip`;
+ const downloadUrl = window.URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = downloadUrl;
+ anchor.download = fileName;
+ document.body.appendChild(anchor);
+ anchor.click();
+ anchor.remove();
+ window.URL.revokeObjectURL(downloadUrl);
+
+ return { ok: true, fileName };
+ } catch (e: any) {
+ log('ā Dev Exoscale: archive download error', e?.message || e);
+ return { ok: false, message: 'Network error while downloading Exoscale archive.' };
+ }
+}
diff --git a/src/app/admin/dev-management/page.tsx b/src/app/admin/dev-management/page.tsx
index e565612..08e9100 100644
--- a/src/app/admin/dev-management/page.tsx
+++ b/src/app/admin/dev-management/page.tsx
@@ -7,11 +7,11 @@ import React from 'react'
import Header from '../../components/nav/Header'
import Footer from '../../components/Footer'
import PageTransitionEffect from '../../components/animation/pageTransitionEffect'
-import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon, WrenchScrewdriverIcon, FolderOpenIcon, ArrowPathIcon } from '@heroicons/react/24/outline'
+import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon, WrenchScrewdriverIcon, FolderOpenIcon, ArrowPathIcon, ArrowDownTrayIcon } from '@heroicons/react/24/outline'
import useAuthStore from '../../store/authStore'
import { useRouter } from 'next/navigation'
import { importSqlDump, SqlExecutionData, SqlExecutionMeta } from './hooks/executeSql'
-import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance'
+import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, downloadExoscaleArchive, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance'
export default function DevManagementPage() {
const { t } = useTranslation();
@@ -70,6 +70,8 @@ export default function DevManagementPage() {
const [structureStatus, setStructureStatus] = React.useState('')
const [looseStatus, setLooseStatus] = React.useState('')
const [ghostStatus, setGhostStatus] = React.useState('')
+ const [archiveLoading, setArchiveLoading] = React.useState(false)
+ const [archiveStatus, setArchiveStatus] = React.useState('')
const formatNow = () => new Date().toLocaleString()
@@ -153,6 +155,23 @@ export default function DevManagementPage() {
}
}
+ const runArchiveDownload = async () => {
+ setExoscaleError('')
+ setArchiveLoading(true)
+ setArchiveStatus('Preparing ZIP export...')
+ try {
+ const res = await downloadExoscaleArchive()
+ if (!res.ok) {
+ setExoscaleError(res.message || 'Failed to download Exoscale archive.')
+ setArchiveStatus('')
+ return
+ }
+ setArchiveStatus(`Download started: ${res.fileName || formatNow()}`)
+ } finally {
+ setArchiveLoading(false)
+ }
+ }
+
const runCreateStructure = async (userId?: number) => {
setExoscaleError('')
if (userId) {
@@ -283,6 +302,25 @@ export default function DevManagementPage() {
+
+
+
+
Exoscale Backup Export
+
Ladet alle Dateien aus dem Exoscale Storage und liefert sie als ZIP-Download aus.
+ {archiveStatus && (
+
{archiveStatus}
+ )}
+
+
+
+
+