Phase4
This commit is contained in:
@@ -190,7 +190,9 @@
|
||||
"console": "Konsole",
|
||||
"files": "Dateien",
|
||||
"properties": "Eigenschaften",
|
||||
"players": "Spieler"
|
||||
"players": "Spieler",
|
||||
"worlds": "Welten",
|
||||
"backups": "Backups"
|
||||
},
|
||||
"console": {
|
||||
"loading": "Wird geladen…",
|
||||
@@ -274,6 +276,47 @@
|
||||
"errors": {
|
||||
"loadFailed": "Spielerliste konnte nicht geladen werden."
|
||||
}
|
||||
},
|
||||
"worlds": {
|
||||
"loading": "Wird geladen…",
|
||||
"loadError": "Weltinformationen konnten nicht geladen werden.",
|
||||
"activeWorld": "Aktive Welt: {name}",
|
||||
"stoppedHint": "Upload und Ersetzen erfordern einen gestoppten Server.",
|
||||
"worldName": "Weltname",
|
||||
"uploadArchive": "Welt-Archiv (.tar.gz)",
|
||||
"uploadError": "Welt-Upload fehlgeschlagen.",
|
||||
"download": "Welt herunterladen",
|
||||
"downloadError": "Download konnte nicht gestartet werden.",
|
||||
"downloadPending": "Backup wird erstellt. Versuchen Sie den Download in Kürze erneut.",
|
||||
"empty": "Keine Welten gefunden.",
|
||||
"validWorld": "Gültige Welt",
|
||||
"invalidWorld": "Ungültig",
|
||||
"active": "Aktiv"
|
||||
},
|
||||
"backups": {
|
||||
"loading": "Wird geladen…",
|
||||
"loadError": "Backups konnten nicht geladen werden.",
|
||||
"create": "Backup erstellen",
|
||||
"manualLabel": "Manuelles Backup",
|
||||
"createError": "Backup konnte nicht erstellt werden.",
|
||||
"download": "Download",
|
||||
"downloadError": "Download-URL konnte nicht erstellt werden.",
|
||||
"restore": "Wiederherstellen",
|
||||
"restoreConfirm": "Welt wirklich aus diesem Backup wiederherstellen? Der Server muss gestoppt sein.",
|
||||
"restoreError": "Wiederherstellung fehlgeschlagen.",
|
||||
"delete": "Löschen",
|
||||
"deleteConfirm": "Backup wirklich löschen?",
|
||||
"deleteError": "Backup konnte nicht gelöscht werden.",
|
||||
"enableSchedule": "Geplante Backups aktivieren",
|
||||
"disableSchedule": "Geplante Backups deaktivieren",
|
||||
"scheduleError": "Zeitplan konnte nicht aktualisiert werden.",
|
||||
"scheduleInfo": "Intervall: alle {hours}h · Aufbewahrung: {retention} Backups",
|
||||
"label": "Bezeichnung",
|
||||
"status": "Status",
|
||||
"size": "Größe",
|
||||
"created": "Erstellt",
|
||||
"actions": "Aktionen",
|
||||
"empty": "Noch keine Backups vorhanden."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,9 @@
|
||||
"console": "Console",
|
||||
"files": "Files",
|
||||
"properties": "Properties",
|
||||
"players": "Players"
|
||||
"players": "Players",
|
||||
"worlds": "Worlds",
|
||||
"backups": "Backups"
|
||||
},
|
||||
"console": {
|
||||
"loading": "Loading…",
|
||||
@@ -274,6 +276,47 @@
|
||||
"errors": {
|
||||
"loadFailed": "Could not load player list."
|
||||
}
|
||||
},
|
||||
"worlds": {
|
||||
"loading": "Loading…",
|
||||
"loadError": "Could not load world information.",
|
||||
"activeWorld": "Active world: {name}",
|
||||
"stoppedHint": "Upload and replace require a stopped server.",
|
||||
"worldName": "World name",
|
||||
"uploadArchive": "World archive (.tar.gz)",
|
||||
"uploadError": "World upload failed.",
|
||||
"download": "Download world",
|
||||
"downloadError": "Could not start download.",
|
||||
"downloadPending": "Backup is being created. Try the download again shortly.",
|
||||
"empty": "No worlds found.",
|
||||
"validWorld": "Valid world",
|
||||
"invalidWorld": "Invalid",
|
||||
"active": "Active"
|
||||
},
|
||||
"backups": {
|
||||
"loading": "Loading…",
|
||||
"loadError": "Could not load backups.",
|
||||
"create": "Create backup",
|
||||
"manualLabel": "Manual backup",
|
||||
"createError": "Could not create backup.",
|
||||
"download": "Download",
|
||||
"downloadError": "Could not create download URL.",
|
||||
"restore": "Restore",
|
||||
"restoreConfirm": "Restore the world from this backup? The server must be stopped.",
|
||||
"restoreError": "Restore failed.",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Delete this backup?",
|
||||
"deleteError": "Could not delete backup.",
|
||||
"enableSchedule": "Enable scheduled backups",
|
||||
"disableSchedule": "Disable scheduled backups",
|
||||
"scheduleError": "Could not update schedule.",
|
||||
"scheduleInfo": "Interval: every {hours}h · Retention: {retention} backups",
|
||||
"label": "Label",
|
||||
"status": "Status",
|
||||
"size": "Size",
|
||||
"created": "Created",
|
||||
"actions": "Actions",
|
||||
"empty": "No backups yet."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
apps/web/src/app/[locale]/servers/[id]/backups/page.tsx
Normal file
13
apps/web/src/app/[locale]/servers/[id]/backups/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
import { ServerBackups } from "@/components/servers/server-backups";
|
||||
|
||||
interface ServerBackupsPageProps {
|
||||
params: Promise<{ locale: string; id: string }>;
|
||||
}
|
||||
|
||||
export default async function ServerBackupsPage({ params }: ServerBackupsPageProps) {
|
||||
const { locale, id } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
return <ServerBackups serverId={id} />;
|
||||
}
|
||||
13
apps/web/src/app/[locale]/servers/[id]/worlds/page.tsx
Normal file
13
apps/web/src/app/[locale]/servers/[id]/worlds/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
import { ServerWorlds } from "@/components/servers/server-worlds";
|
||||
|
||||
interface ServerWorldsPageProps {
|
||||
params: Promise<{ locale: string; id: string }>;
|
||||
}
|
||||
|
||||
export default async function ServerWorldsPage({ params }: ServerWorldsPageProps) {
|
||||
const { locale, id } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
return <ServerWorlds serverId={id} />;
|
||||
}
|
||||
237
apps/web/src/components/servers/server-backups.tsx
Normal file
237
apps/web/src/components/servers/server-backups.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@hexahost/ui";
|
||||
|
||||
import {
|
||||
createBackup,
|
||||
deleteBackup,
|
||||
getBackupDownloadUrl,
|
||||
getBackupSchedule,
|
||||
listBackups,
|
||||
restoreBackup,
|
||||
updateBackupSchedule,
|
||||
type BackupItem,
|
||||
type BackupSchedule,
|
||||
} from "@/lib/api/backups";
|
||||
|
||||
interface ServerBackupsProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
function formatBytes(value: string | null): string {
|
||||
if (!value) {
|
||||
return "—";
|
||||
}
|
||||
const bytes = Number(value);
|
||||
if (Number.isNaN(bytes)) {
|
||||
return value;
|
||||
}
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function ServerBackups({ serverId }: ServerBackupsProps) {
|
||||
const t = useTranslations("servers.backups");
|
||||
const [backups, setBackups] = useState<BackupItem[]>([]);
|
||||
const [schedule, setSchedule] = useState<BackupSchedule | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [backupList, scheduleData] = await Promise.all([
|
||||
listBackups(serverId),
|
||||
getBackupSchedule(serverId),
|
||||
]);
|
||||
setBackups(backupList.backups);
|
||||
setSchedule(scheduleData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("loadError"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [serverId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
async function handleCreateBackup() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await createBackup(serverId, t("manualLabel"));
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("createError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload(backupId: string) {
|
||||
try {
|
||||
const { downloadUrl } = await getBackupDownloadUrl(serverId, backupId);
|
||||
window.open(downloadUrl, "_blank", "noopener,noreferrer");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("downloadError"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestore(backupId: string) {
|
||||
if (!window.confirm(t("restoreConfirm"))) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await restoreBackup(serverId, backupId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("restoreError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(backupId: string) {
|
||||
if (!window.confirm(t("deleteConfirm"))) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await deleteBackup(serverId, backupId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("deleteError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSchedule(enabled: boolean) {
|
||||
if (!schedule) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const updated = await updateBackupSchedule(serverId, { enabled });
|
||||
setSchedule(updated);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("scheduleError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-sm text-zinc-500">{t("loading")}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error ? (
|
||||
<p className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={() => void handleCreateBackup()} disabled={busy}>
|
||||
{t("create")}
|
||||
</Button>
|
||||
{schedule ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => void toggleSchedule(!schedule.enabled)}
|
||||
disabled={busy}
|
||||
>
|
||||
{schedule.enabled ? t("disableSchedule") : t("enableSchedule")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{schedule ? (
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{t("scheduleInfo", {
|
||||
hours: schedule.intervalHours,
|
||||
retention: schedule.retentionCount,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<table className="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-800">
|
||||
<thead className="bg-zinc-50 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left font-medium">{t("label")}</th>
|
||||
<th className="px-4 py-2 text-left font-medium">{t("status")}</th>
|
||||
<th className="px-4 py-2 text-left font-medium">{t("size")}</th>
|
||||
<th className="px-4 py-2 text-left font-medium">{t("created")}</th>
|
||||
<th className="px-4 py-2 text-right font-medium">{t("actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
{backups.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-6 text-center text-zinc-500">
|
||||
{t("empty")}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
backups.map((backup) => (
|
||||
<tr key={backup.id}>
|
||||
<td className="px-4 py-2">{backup.label ?? backup.type}</td>
|
||||
<td className="px-4 py-2">{backup.status}</td>
|
||||
<td className="px-4 py-2">{formatBytes(backup.sizeBytes)}</td>
|
||||
<td className="px-4 py-2">
|
||||
{new Date(backup.createdAt).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right space-x-2">
|
||||
{backup.status === "AVAILABLE" ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => void handleDownload(backup.id)}
|
||||
>
|
||||
{t("download")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => void handleRestore(backup.id)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("restore")}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => void handleDelete(backup.id)}
|
||||
disabled={busy}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const tabs = [
|
||||
{ key: "files", suffix: "/files" },
|
||||
{ key: "properties", suffix: "/properties" },
|
||||
{ key: "players", suffix: "/players" },
|
||||
{ key: "worlds", suffix: "/worlds" },
|
||||
{ key: "backups", suffix: "/backups" },
|
||||
] as const;
|
||||
|
||||
export function ServerTabs({ serverId }: ServerTabsProps) {
|
||||
|
||||
172
apps/web/src/components/servers/server-worlds.tsx
Normal file
172
apps/web/src/components/servers/server-worlds.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@hexahost/ui";
|
||||
|
||||
import {
|
||||
completeWorldUpload,
|
||||
getWorldInfo,
|
||||
initiateWorldUpload,
|
||||
requestWorldDownload,
|
||||
type WorldSummary,
|
||||
} from "@/lib/api/worlds";
|
||||
import { getBackupDownloadUrl } from "@/lib/api/backups";
|
||||
|
||||
interface ServerWorldsProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
function formatBytes(value: string): string {
|
||||
const bytes = Number(value);
|
||||
if (Number.isNaN(bytes)) {
|
||||
return value;
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function ServerWorlds({ serverId }: ServerWorldsProps) {
|
||||
const t = useTranslations("servers.worlds");
|
||||
const [activeWorldName, setActiveWorldName] = useState("world");
|
||||
const [worlds, setWorlds] = useState<WorldSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [worldName, setWorldName] = useState("world");
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const info = await getWorldInfo(serverId);
|
||||
setActiveWorldName(info.activeWorldName);
|
||||
setWorlds(info.worlds);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("loadError"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [serverId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
async function handleUpload(file: File) {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const init = await initiateWorldUpload(serverId, worldName);
|
||||
const response = await fetch(init.uploadUrl, {
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: { "Content-Type": "application/gzip" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(t("uploadError"));
|
||||
}
|
||||
await completeWorldUpload(serverId, init.uploadId);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("uploadError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { backupId } = await requestWorldDownload(serverId);
|
||||
const { downloadUrl } = await getBackupDownloadUrl(serverId, backupId);
|
||||
if (downloadUrl) {
|
||||
window.open(downloadUrl, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
setError(t("downloadPending"));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("downloadError"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-sm text-zinc-500">{t("loading")}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error ? (
|
||||
<p className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{t("activeWorld", { name: activeWorldName })}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-zinc-500">{t("stoppedHint")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
<label className="block text-sm">
|
||||
<span className="mb-1 block text-zinc-600 dark:text-zinc-400">
|
||||
{t("worldName")}
|
||||
</span>
|
||||
<input
|
||||
className="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-950"
|
||||
value={worldName}
|
||||
onChange={(event) => setWorldName(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-sm">
|
||||
<span className="mb-1 block text-zinc-600 dark:text-zinc-400">
|
||||
{t("uploadArchive")}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz,.tgz,application/gzip"
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
void handleUpload(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<Button variant="secondary" onClick={() => void handleDownload()} disabled={busy}>
|
||||
{t("download")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul className="divide-y divide-zinc-200 rounded-lg border border-zinc-200 dark:divide-zinc-800 dark:border-zinc-800">
|
||||
{worlds.length === 0 ? (
|
||||
<li className="px-4 py-6 text-center text-sm text-zinc-500">{t("empty")}</li>
|
||||
) : (
|
||||
worlds.map((world) => (
|
||||
<li key={world.name} className="flex items-center justify-between px-4 py-3 text-sm">
|
||||
<div>
|
||||
<p className="font-medium">{world.name}</p>
|
||||
<p className="text-zinc-500">
|
||||
{world.hasLevelDat ? t("validWorld") : t("invalidWorld")} ·{" "}
|
||||
{formatBytes(world.sizeBytes)}
|
||||
</p>
|
||||
</div>
|
||||
{world.name === activeWorldName ? (
|
||||
<span className="text-xs text-sky-600 dark:text-sky-400">{t("active")}</span>
|
||||
) : null}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
apps/web/src/lib/api/backups.ts
Normal file
80
apps/web/src/lib/api/backups.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { apiFetch } from "./auth";
|
||||
|
||||
export interface BackupItem {
|
||||
id: string;
|
||||
serverId: string;
|
||||
type: string;
|
||||
status: string;
|
||||
sizeBytes: string | null;
|
||||
sha256: string | null;
|
||||
label: string | null;
|
||||
failureReason: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface BackupListResponse {
|
||||
backups: BackupItem[];
|
||||
}
|
||||
|
||||
export interface BackupSchedule {
|
||||
enabled: boolean;
|
||||
intervalHours: number;
|
||||
retentionCount: number;
|
||||
nextRunAt: string | null;
|
||||
lastRunAt: string | null;
|
||||
}
|
||||
|
||||
export async function listBackups(serverId: string): Promise<BackupListResponse> {
|
||||
return apiFetch<BackupListResponse>(`/servers/${serverId}/backups`);
|
||||
}
|
||||
|
||||
export async function createBackup(
|
||||
serverId: string,
|
||||
label?: string,
|
||||
): Promise<{ backup: BackupItem; jobId?: string }> {
|
||||
return apiFetch(`/servers/${serverId}/backups`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ label }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteBackup(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
): Promise<BackupItem> {
|
||||
return apiFetch(`/servers/${serverId}/backups/${backupId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBackupDownloadUrl(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
): Promise<{ downloadUrl: string; expiresAt: string }> {
|
||||
return apiFetch(`/servers/${serverId}/backups/${backupId}/download-url`);
|
||||
}
|
||||
|
||||
export async function restoreBackup(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`/servers/${serverId}/backups/${backupId}/restore`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ confirm: true }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBackupSchedule(serverId: string): Promise<BackupSchedule> {
|
||||
return apiFetch(`/servers/${serverId}/backups/schedule`);
|
||||
}
|
||||
|
||||
export async function updateBackupSchedule(
|
||||
serverId: string,
|
||||
input: Partial<BackupSchedule>,
|
||||
): Promise<BackupSchedule> {
|
||||
return apiFetch(`/servers/${serverId}/backups/schedule`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
48
apps/web/src/lib/api/worlds.ts
Normal file
48
apps/web/src/lib/api/worlds.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { apiFetch } from "./auth";
|
||||
|
||||
export interface WorldSummary {
|
||||
name: string;
|
||||
path: string;
|
||||
sizeBytes: string;
|
||||
hasLevelDat: boolean;
|
||||
}
|
||||
|
||||
export interface WorldInfoResponse {
|
||||
activeWorldName: string;
|
||||
worlds: WorldSummary[];
|
||||
}
|
||||
|
||||
export async function getWorldInfo(serverId: string): Promise<WorldInfoResponse> {
|
||||
return apiFetch(`/servers/${serverId}/worlds`);
|
||||
}
|
||||
|
||||
export async function initiateWorldUpload(
|
||||
serverId: string,
|
||||
worldName: string,
|
||||
): Promise<{
|
||||
uploadId: string;
|
||||
uploadUrl: string;
|
||||
expiresAt: string;
|
||||
}> {
|
||||
return apiFetch(`/servers/${serverId}/worlds/uploads`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ worldName }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function completeWorldUpload(
|
||||
serverId: string,
|
||||
uploadId: string,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`/servers/${serverId}/worlds/uploads/${uploadId}/complete`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestWorldDownload(
|
||||
serverId: string,
|
||||
): Promise<{ backupId: string }> {
|
||||
return apiFetch(`/servers/${serverId}/worlds/download`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user