diff --git a/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue b/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue
index 2a6c6267a..a92c8b618 100644
--- a/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue
+++ b/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue
@@ -106,6 +106,7 @@ const fetchSettings = async () => {
initialSettings.value = settings as { interval: number; enabled: boolean };
autoBackupEnabled.value = settings?.enabled ?? false;
autoBackupInterval.value = settings?.interval || 6;
+ return true;
} catch (error) {
console.error("Error fetching backup settings:", error);
addNotification({
@@ -114,6 +115,7 @@ const fetchSettings = async () => {
text: "Failed to load backup settings",
type: "error",
});
+ return false;
} finally {
isLoadingSettings.value = false;
}
@@ -155,8 +157,10 @@ const saveSettings = async () => {
defineExpose({
show: async () => {
- await fetchSettings();
- modal.value?.show();
+ const success = await fetchSettings();
+ if (success) {
+ modal.value?.show();
+ }
},
});
diff --git a/apps/frontend/src/components/ui/servers/OverviewLoading.vue b/apps/frontend/src/components/ui/servers/OverviewLoading.vue
new file mode 100644
index 000000000..a3d5135f1
--- /dev/null
+++ b/apps/frontend/src/components/ui/servers/OverviewLoading.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
/ 100%
+
+
CPU usage
+
+
+
+
+
+
+
+
/ 100%
+
+
Memory usage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
index ebb5cf0b7..073909fd3 100644
--- a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
+++ b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
@@ -1,23 +1,25 @@
-
+
-
Are you sure you want to {{ currentPendingAction }} the server?
-
+
+ Are you sure you want to {{ confirmActionText }} the
+ server?
+
-
+
- {{ currentPendingActionFriendly }} server
+ {{ confirmActionText }} server
-
+
Cancel
@@ -29,7 +31,7 @@
Installing...
-
+
+
-
+
- {{ stopButtonText }}
+ {{ isStoppingState ? "Stopping..." : "Stop" }}
+
-
-
+
+
-
-
-
-
- {{ actionButtonText }}
-
+
+ {{ primaryActionText }}
-
-
-
-
-
-
-
- Kill server
-
-
-
- All servers
-
-
-
- Details
-
-
-
+
+
+
+
+
+ Kill server
+
+
+
+ All servers
+
+
+
+ Details
+
+
+
+
diff --git a/apps/frontend/src/components/ui/servers/PanelServerStatus.vue b/apps/frontend/src/components/ui/servers/PanelServerStatus.vue
index 81e6ebc6f..9f6674b0b 100644
--- a/apps/frontend/src/components/ui/servers/PanelServerStatus.vue
+++ b/apps/frontend/src/components/ui/servers/PanelServerStatus.vue
@@ -1,66 +1,72 @@
- {{ getStatusText }}
+ {{ getStatusText(state) }}
diff --git a/apps/frontend/src/components/ui/servers/PanelTerminal.vue b/apps/frontend/src/components/ui/servers/PanelTerminal.vue
index 138baa507..02d7edb90 100644
--- a/apps/frontend/src/components/ui/servers/PanelTerminal.vue
+++ b/apps/frontend/src/components/ui/servers/PanelTerminal.vue
@@ -260,7 +260,25 @@
@@ -272,6 +290,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { NewModal } from "@modrinth/ui";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
+import DOMPurify from "dompurify";
import { usePyroConsole } from "~/store/console.ts";
const { $cosmetics } = useNuxtApp();
@@ -984,6 +1003,38 @@ const jumpToLine = (line: string, event?: MouseEvent) => {
});
};
+const sanitizeUrl = (url: string): string => {
+ try {
+ const parsed = new URL(url);
+ if (!["http:", "https:"].includes(parsed.protocol)) {
+ return "#";
+ }
+ return parsed.toString();
+ } catch {
+ return "#";
+ }
+};
+
+const detectedLinks = computed(() => {
+ const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g;
+ const matches = [...selectedLog.value.matchAll(urlRegex)].map((match) => match[0]);
+ return matches.filter((url) => sanitizeUrl(url) !== "#");
+});
+
+const processedLogWithLinks = computed(() => {
+ const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g;
+ const sanitizedLog = DOMPurify.sanitize(selectedLog.value, {
+ ALLOWED_TAGS: [],
+ ALLOWED_ATTR: [],
+ });
+
+ return sanitizedLog.replace(urlRegex, (url) => {
+ const safeUrl = sanitizeUrl(url);
+ if (safeUrl === "#") return url;
+ return `${url} `;
+ });
+});
+
watch(
() => pyroConsole.filteredOutput.value,
() => {
diff --git a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue b/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue
index 6bcaef3e1..a7dc1083a 100644
--- a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue
+++ b/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue
@@ -337,7 +337,7 @@ watch(
selectedLoaderVersions,
(newVersions) => {
if (newVersions.length > 0 && !selectedLoaderVersion.value) {
- selectedLoaderVersion.value = String(newVersions[0]); // Ensure string type
+ selectedLoaderVersion.value = String(newVersions[0]);
}
},
{ immediate: true },
@@ -516,8 +516,6 @@ const handleReinstall = async () => {
const onShow = () => {
selectedMCVersion.value = props.server.general?.mc_version || "";
- selectedLoaderVersion.value = "";
- hardReset.value = false;
};
const onHide = () => {
@@ -528,13 +526,15 @@ const onHide = () => {
loadingServerCheck.value = false;
isLoading.value = false;
selectedMCVersion.value = "";
- selectedLoaderVersion.value = "";
serverCheckError.value = "";
paperVersions.value = {};
purpurVersions.value = {};
};
const show = (loader: Loaders) => {
+ if (selectedLoader.value !== loader) {
+ selectedLoaderVersion.value = "";
+ }
selectedLoader.value = loader;
selectedMCVersion.value = props.server.general?.mc_version || "";
versionSelectModal.value?.show();
diff --git a/apps/frontend/src/components/ui/servers/ServerListing.vue b/apps/frontend/src/components/ui/servers/ServerListing.vue
index 104891aa6..a8bfeece4 100644
--- a/apps/frontend/src/components/ui/servers/ServerListing.vue
+++ b/apps/frontend/src/components/ui/servers/ServerListing.vue
@@ -69,11 +69,13 @@
-
- Your server has been suspended due to a billing issue. Please visit your billing settings or
- contact Modrinth Support for more information.
+
+ Your server has been suspended. Please
+ update your billing information or contact Modrinth Support for more information.
+
+
diff --git a/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue b/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue
index 1ffc5c92f..f90be6640 100644
--- a/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue
+++ b/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue
@@ -2,7 +2,7 @@
diff --git a/apps/frontend/src/composables/pyroServers.ts b/apps/frontend/src/composables/pyroServers.ts
index 7130e6d2c..206063a25 100644
--- a/apps/frontend/src/composables/pyroServers.ts
+++ b/apps/frontend/src/composables/pyroServers.ts
@@ -10,19 +10,111 @@ interface PyroFetchOptions {
url?: string;
token?: string;
};
- retry?: boolean;
+ retry?: number | boolean;
}
-async function PyroFetch
(path: string, options: PyroFetchOptions = {}): Promise {
+class PyroServerError extends Error {
+ public readonly errors: Map = new Map();
+ public readonly timestamp: number = Date.now();
+
+ constructor(message?: string) {
+ super(message || "Multiple errors occurred");
+ this.name = "PyroServerError";
+ }
+
+ addError(module: string, error: Error) {
+ this.errors.set(module, error);
+ this.message = this.buildErrorMessage();
+ }
+
+ hasErrors() {
+ return this.errors.size > 0;
+ }
+
+ private buildErrorMessage(): string {
+ return Array.from(this.errors.entries())
+ .map(([_module, error]) => error.message)
+ .join("\n");
+ }
+}
+
+export class PyroServersFetchError extends Error {
+ constructor(
+ message: string,
+ public readonly statusCode?: number,
+ public readonly originalError?: Error,
+ public readonly module?: string,
+ ) {
+ let errorMessage = message;
+ let method = "GET";
+ let path = "";
+
+ if (originalError instanceof FetchError) {
+ const matches = message.match(/\[([A-Z]+)\]\s+"([^"]+)":/);
+ if (matches) {
+ method = matches[1];
+ path = matches[2].replace(/https?:\/\/[^/]+\/[^/]+\/v\d+\//, "");
+ }
+
+ const statusMessage = (() => {
+ if (!statusCode) return "Unknown Error";
+ switch (statusCode) {
+ case 400:
+ return "Bad Request";
+ case 401:
+ return "Unauthorized";
+ case 403:
+ return "Forbidden";
+ case 404:
+ return "Not Found";
+ case 408:
+ return "Request Timeout";
+ case 429:
+ return "Too Many Requests";
+ case 500:
+ return "Internal Server Error";
+ case 502:
+ return "Bad Gateway";
+ case 503:
+ return "Service Unavailable";
+ case 504:
+ return "Gateway Timeout";
+ default:
+ return `HTTP ${statusCode}`;
+ }
+ })();
+
+ errorMessage = `[${method}] ${statusMessage} (${statusCode}) while fetching ${path}${module ? ` in ${module}` : ""}`;
+ } else {
+ errorMessage = `${message}${statusCode ? ` (${statusCode})` : ""}${module ? ` in ${module}` : ""}`;
+ }
+
+ super(errorMessage);
+ this.name = "PyroServersFetchError";
+ }
+}
+
+async function PyroFetch(
+ path: string,
+ options: PyroFetchOptions = {},
+ module?: string,
+): Promise {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
if (!authToken) {
- throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
+ throw new PyroServersFetchError("Missing auth token", 401, undefined, module);
}
- const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
+ const {
+ method = "GET",
+ contentType = "application/json",
+ body,
+ version = 0,
+ override,
+ retry = method === "GET" ? 3 : 0,
+ } = options;
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
@@ -30,9 +122,11 @@ async function PyroFetch(path: string, options: PyroFetchOptions = {}): Promi
);
if (!base) {
- throw new PyroFetchError(
- "Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
- 10001,
+ throw new PyroServersFetchError(
+ "Configuration error: Missing PYRO_BASE_URL",
+ 500,
+ undefined,
+ module,
);
}
@@ -40,9 +134,7 @@ async function PyroFetch(path: string, options: PyroFetchOptions = {}): Promi
? `https://${override.url}/${path.replace(/^\//, "")}`
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
- type HeadersRecord = Record;
-
- const headers: HeadersRecord = {
+ const headers: Record = {
Authorization: `Bearer ${override?.token ?? authToken}`,
"Access-Control-Allow-Headers": "Authorization",
"User-Agent": "Pyro/1.0 (https://pyro.host)",
@@ -57,43 +149,47 @@ async function PyroFetch(path: string, options: PyroFetchOptions = {}): Promi
headers.Origin = window.location.origin;
}
- try {
- const response = await $fetch(fullUrl, {
- method,
- headers,
- body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
- timeout: 10000,
- retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
- });
- return response;
- } catch (error) {
- console.error("[PyroServers/PyroFetch]:", error);
- if (error instanceof FetchError) {
- const statusCode = error.response?.status;
- const statusText = error.response?.statusText || "[no status text available]";
- const errorMessages: { [key: number]: string } = {
- 400: "Bad Request",
- 401: "Unauthorized",
- 403: "Forbidden",
- 404: "Not Found",
- 405: "Method Not Allowed",
- 429: "Too Many Requests",
- 500: "Internal Server Error",
- 502: "Bad Gateway",
- 503: "Service Unavailable",
- };
- const message =
- statusCode && statusCode in errorMessages
- ? errorMessages[statusCode]
- : `HTTP Error: ${statusCode || "[unhandled status code]"} ${statusText}`;
- throw new PyroFetchError(`[PyroServers/PyroFetch] ${message}`, statusCode, error);
+ let attempts = 0;
+ const maxAttempts = (typeof retry === "boolean" ? (retry ? 1 : 0) : retry) + 1;
+ let lastError: Error | null = null;
+
+ while (attempts < maxAttempts) {
+ try {
+ const response = await $fetch(fullUrl, {
+ method,
+ headers,
+ body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
+ timeout: 10000,
+ });
+
+ return response;
+ } catch (error) {
+ lastError = error as Error;
+ attempts++;
+
+ if (error instanceof FetchError) {
+ const statusCode = error.response?.status;
+ const isRetryable = statusCode ? [408, 429, 500, 502, 503, 504].includes(statusCode) : true;
+
+ if (!isRetryable || attempts >= maxAttempts) {
+ throw new PyroServersFetchError(error.message, statusCode, error, module);
+ }
+
+ const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ continue;
+ }
+
+ throw new PyroServersFetchError(
+ "Unexpected error during fetch operation",
+ undefined,
+ error as Error,
+ module,
+ );
}
- throw new PyroFetchError(
- "[PyroServers/PyroFetch] An unexpected error occurred during the fetch operation.",
- undefined,
- error as Error,
- );
}
+
+ throw lastError || new Error("Maximum retry attempts reached");
}
const internalServerRefrence = ref(null);
@@ -271,100 +367,96 @@ const constructServerProperties = (properties: any): string => {
};
const processImage = async (iconUrl: string | undefined) => {
- const image = ref(null);
const sharedImage = useState(
`server-icon-${internalServerRefrence.value.serverId}`,
- () => undefined,
);
- const auth = await PyroFetch(`servers/${internalServerRefrence.value.serverId}/fs`);
- try {
- const fileData = await PyroFetch(`/download?path=/server-icon-original.png`, {
- override: auth,
- retry: false,
- });
- if (fileData instanceof Blob) {
- if (import.meta.client) {
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- const img = new Image();
- img.src = URL.createObjectURL(fileData);
- await new Promise((resolve) => {
- img.onload = () => {
- canvas.width = 512;
- canvas.height = 512;
- ctx?.drawImage(img, 0, 0, 512, 512);
- const dataURL = canvas.toDataURL("image/png");
- internalServerRefrence.value.general.image = dataURL;
- image.value = dataURL;
- sharedImage.value = dataURL; // Store in useState
- resolve();
- };
- });
- }
- }
- } catch (error) {
- if (error instanceof PyroFetchError && error.statusCode === 404) {
- sharedImage.value = undefined;
- } else {
- console.error(error);
- }
+ if (sharedImage.value) {
+ return sharedImage.value;
}
- if (image.value === null && iconUrl) {
- console.log("iconUrl", iconUrl);
+ try {
+ const auth = await PyroFetch(`servers/${internalServerRefrence.value.serverId}/fs`);
try {
- const response = await fetch(iconUrl);
- const file = await response.blob();
- const originalfile = new File([file], "server-icon-original.png", {
- type: "image/png",
+ const fileData = await PyroFetch(`/download?path=/server-icon-original.png`, {
+ override: auth,
+ retry: false,
});
- if (import.meta.client) {
- const scaledFile = await new Promise((resolve, reject) => {
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- const img = new Image();
- img.src = URL.createObjectURL(file);
- img.onload = () => {
- canvas.width = 64;
- canvas.height = 64;
- ctx?.drawImage(img, 0, 0, 64, 64);
- canvas.toBlob((blob) => {
- if (blob) {
- const data = new File([blob], "server-icon.png", { type: "image/png" });
- resolve(data);
- } else {
- reject(new Error("Canvas toBlob failed"));
- }
- }, "image/png");
- };
- img.onerror = reject;
- });
- if (scaledFile) {
- await PyroFetch(`/create?path=/server-icon.png&type=file`, {
- method: "POST",
- contentType: "application/octet-stream",
- body: scaledFile,
- override: auth,
- });
- await PyroFetch(`/create?path=/server-icon-original.png&type=file`, {
- method: "POST",
- contentType: "application/octet-stream",
- body: originalfile,
- override: auth,
+ if (fileData instanceof Blob) {
+ if (import.meta.client) {
+ const dataURL = await new Promise((resolve) => {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ const img = new Image();
+ img.onload = () => {
+ canvas.width = 512;
+ canvas.height = 512;
+ ctx?.drawImage(img, 0, 0, 512, 512);
+ const dataURL = canvas.toDataURL("image/png");
+ sharedImage.value = dataURL;
+ resolve(dataURL);
+ URL.revokeObjectURL(img.src);
+ };
+ img.src = URL.createObjectURL(fileData);
});
+ return dataURL;
}
}
} catch (error) {
- if (error instanceof PyroFetchError && error.statusCode === 404) {
- console.log("[PYROSERVERS] No server icon found");
- } else {
- console.error(error);
+ if (error instanceof PyroServersFetchError && error.statusCode === 404 && iconUrl) {
+ try {
+ const response = await fetch(iconUrl);
+ if (!response.ok) throw new Error("Failed to fetch icon");
+ const file = await response.blob();
+ const originalFile = new File([file], "server-icon-original.png", { type: "image/png" });
+
+ if (import.meta.client) {
+ const dataURL = await new Promise((resolve) => {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ const img = new Image();
+ img.onload = () => {
+ canvas.width = 64;
+ canvas.height = 64;
+ ctx?.drawImage(img, 0, 0, 64, 64);
+ canvas.toBlob(async (blob) => {
+ if (blob) {
+ const scaledFile = new File([blob], "server-icon.png", { type: "image/png" });
+ await PyroFetch(`/create?path=/server-icon.png&type=file`, {
+ method: "POST",
+ contentType: "application/octet-stream",
+ body: scaledFile,
+ override: auth,
+ });
+ await PyroFetch(`/create?path=/server-icon-original.png&type=file`, {
+ method: "POST",
+ contentType: "application/octet-stream",
+ body: originalFile,
+ override: auth,
+ });
+ }
+ }, "image/png");
+ const dataURL = canvas.toDataURL("image/png");
+ sharedImage.value = dataURL;
+ resolve(dataURL);
+ URL.revokeObjectURL(img.src);
+ };
+ img.src = URL.createObjectURL(file);
+ });
+ return dataURL;
+ }
+ } catch (error) {
+ console.error("Failed to process external icon:", error);
+ }
}
}
+ } catch (error) {
+ console.error("Failed to process server icon:", error);
}
- return image.value;
+
+ sharedImage.value = undefined;
+ return undefined;
};
// ------------------ GENERAL ------------------ //
@@ -564,10 +656,14 @@ const reinstallContent = async (replace: string, projectId: string, versionId: s
const createBackup = async (backupName: string) => {
try {
- const response = (await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups`, {
- method: "POST",
- body: { name: backupName },
- })) as { id: string };
+ const response = await PyroFetch<{ id: string }>(
+ `servers/${internalServerRefrence.value.serverId}/backups`,
+ {
+ method: "POST",
+ body: { name: backupName },
+ },
+ );
+ await internalServerRefrence.value.refresh(["backups"]);
return response.id;
} catch (error) {
console.error("Error creating backup:", error);
@@ -581,6 +677,7 @@ const renameBackup = async (backupId: string, newName: string) => {
method: "POST",
body: { name: newName },
});
+ await internalServerRefrence.value.refresh(["backups"]);
} catch (error) {
console.error("Error renaming backup:", error);
throw error;
@@ -592,6 +689,7 @@ const deleteBackup = async (backupId: string) => {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}`, {
method: "DELETE",
});
+ await internalServerRefrence.value.refresh(["backups"]);
} catch (error) {
console.error("Error deleting backup:", error);
throw error;
@@ -606,6 +704,7 @@ const restoreBackup = async (backupId: string) => {
method: "POST",
},
);
+ await internalServerRefrence.value.refresh(["backups"]);
} catch (error) {
console.error("Error restoring backup:", error);
throw error;
@@ -644,12 +743,10 @@ const getAutoBackup = async () => {
const lockBackup = async (backupId: string) => {
try {
- return await PyroFetch(
- `servers/${internalServerRefrence.value.serverId}/backups/${backupId}/lock`,
- {
- method: "POST",
- },
- );
+ await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/lock`, {
+ method: "POST",
+ });
+ await internalServerRefrence.value.refresh(["backups"]);
} catch (error) {
console.error("Error locking backup:", error);
throw error;
@@ -658,14 +755,12 @@ const lockBackup = async (backupId: string) => {
const unlockBackup = async (backupId: string) => {
try {
- return await PyroFetch(
- `servers/${internalServerRefrence.value.serverId}/backups/${backupId}/unlock`,
- {
- method: "POST",
- },
- );
+ await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/unlock`, {
+ method: "POST",
+ });
+ await internalServerRefrence.value.refresh(["backups"]);
} catch (error) {
- console.error("Error locking backup:", error);
+ console.error("Error unlocking backup:", error);
throw error;
}
};
@@ -760,7 +855,7 @@ const retryWithAuth = async (requestFn: () => Promise) => {
try {
return await requestFn();
} catch (error) {
- if (error instanceof PyroFetchError && error.statusCode === 401) {
+ if (error instanceof PyroServersFetchError && error.statusCode === 401) {
await internalServerRefrence.value.refresh(["fs"]);
return await requestFn();
}
@@ -947,17 +1042,18 @@ const modules: any = {
general: {
get: async (serverId: string) => {
try {
- const data = await PyroFetch(`servers/${serverId}`);
- // TODO: temp hack to fix hydration error
+ const data = await PyroFetch(`servers/${serverId}`, {}, "general");
if (data.upstream?.project_id) {
const res = await $fetch(
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
);
data.project = res as Project;
}
+
if (import.meta.client) {
data.image = (await processImage(data.project?.icon_url)) ?? undefined;
}
+
const motd = await getMotd();
if (motd === "A Minecraft Server") {
await setMotd(
@@ -967,8 +1063,19 @@ const modules: any = {
data.motd = motd;
return data;
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ status: "error",
+ server_id: serverId,
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
updateName,
@@ -982,16 +1089,23 @@ const modules: any = {
content: {
get: async (serverId: string) => {
try {
- const mods = await PyroFetch(`servers/${serverId}/mods`);
+ const mods = await PyroFetch(`servers/${serverId}/mods`, {}, "content");
return {
- data:
- internalServerRefrence.value.error === undefined
- ? mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""))
- : [],
+ data: mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? "")),
};
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ data: [],
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
install: installContent,
@@ -1001,10 +1115,22 @@ const modules: any = {
backups: {
get: async (serverId: string) => {
try {
- return { data: await PyroFetch(`servers/${serverId}/backups`) };
+ return {
+ data: await PyroFetch(`servers/${serverId}/backups`, {}, "backups"),
+ };
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ data: [],
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
create: createBackup,
@@ -1020,10 +1146,26 @@ const modules: any = {
network: {
get: async (serverId: string) => {
try {
- return { allocations: await PyroFetch(`servers/${serverId}/allocations`) };
+ return {
+ allocations: await PyroFetch(
+ `servers/${serverId}/allocations`,
+ {},
+ "network",
+ ),
+ };
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ allocations: [],
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
reserveAllocation,
@@ -1035,10 +1177,19 @@ const modules: any = {
startup: {
get: async (serverId: string) => {
try {
- return await PyroFetch(`servers/${serverId}/startup`);
+ return await PyroFetch(`servers/${serverId}/startup`, {}, "startup");
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
update: updateStartupSettings,
@@ -1046,20 +1197,39 @@ const modules: any = {
ws: {
get: async (serverId: string) => {
try {
- return await PyroFetch(`servers/${serverId}/ws`);
+ return await PyroFetch(`servers/${serverId}/ws`, {}, "ws");
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
},
fs: {
get: async (serverId: string) => {
try {
- return { auth: await PyroFetch(`servers/${serverId}/fs`) };
+ return { auth: await PyroFetch(`servers/${serverId}/fs`, {}, "fs") };
} catch (error) {
- internalServerRefrence.value.setError(error);
- return undefined;
+ const fetchError =
+ error instanceof PyroServersFetchError
+ ? error
+ : new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
+
+ return {
+ auth: undefined,
+ error: {
+ error: fetchError,
+ timestamp: Date.now(),
+ },
+ };
}
},
listDirContents,
@@ -1367,12 +1537,44 @@ type FSFunctions = {
downloadFile: (path: string, raw?: boolean) => Promise;
};
-type GeneralModule = General & GeneralFunctions;
-type ContentModule = { data: Mod[] } & ContentFunctions;
-type BackupsModule = { data: Backup[] } & BackupFunctions;
-type NetworkModule = { allocations: Allocation[] } & NetworkFunctions;
-type StartupModule = Startup & StartupFunctions;
-export type FSModule = { auth: JWTAuth } & FSFunctions;
+type ModuleError = {
+ error: PyroServersFetchError;
+ timestamp: number;
+};
+
+type GeneralModule = General &
+ GeneralFunctions & {
+ error?: ModuleError;
+ };
+
+type ContentModule = {
+ data: Mod[];
+ error?: ModuleError;
+} & ContentFunctions;
+
+type BackupsModule = {
+ data: Backup[];
+ error?: ModuleError;
+} & BackupFunctions;
+
+type NetworkModule = {
+ allocations: Allocation[];
+ error?: ModuleError;
+} & NetworkFunctions;
+
+type StartupModule = Startup &
+ StartupFunctions & {
+ error?: ModuleError;
+ };
+
+type WSModule = JWTAuth & {
+ error?: ModuleError;
+};
+
+type FSModule = {
+ auth: JWTAuth;
+ error?: ModuleError;
+} & FSFunctions;
type ModulesMap = {
general: GeneralModule;
@@ -1380,7 +1582,7 @@ type ModulesMap = {
backups: BackupsModule;
network: NetworkModule;
startup: StartupModule;
- ws: JWTAuth;
+ ws: WSModule;
fs: FSModule;
};
@@ -1401,6 +1603,7 @@ export type Server = {
preserveInstallState?: boolean;
},
) => Promise;
+ loadModules: (modulesToLoad: avaliableModules) => Promise;
setError: (error: Error) => void;
error?: Error;
serverId: string;
@@ -1419,58 +1622,92 @@ export const usePyroServer = async (serverId: string, includedModules: avaliable
return;
}
- const modulesToRefresh = refreshModules || includedModules;
- const promises: Promise[] = [];
+ const modulesToRefresh = [...new Set(refreshModules || includedModules)];
+ const serverError = new PyroServerError();
- const uniqueModules = [...new Set(modulesToRefresh)];
+ const modulePromises = modulesToRefresh.map(async (module) => {
+ try {
+ const mods = modules[module];
+ if (!mods?.get) return;
- for (const module of uniqueModules) {
- const mods = modules[module];
- if (mods.get) {
- promises.push(
- (async () => {
- const data = await mods.get(serverId);
- if (data) {
- if (module === "general" && options?.preserveConnection) {
- const updatedData = {
- ...server[module],
- ...data,
- };
- if (server[module]?.image) {
- updatedData.image = server[module].image;
- }
- if (server[module]?.motd) {
- updatedData.motd = server[module].motd;
- }
- if (options.preserveInstallState && server[module]?.status === "installing") {
- updatedData.status = "installing";
- }
- server[module] = updatedData;
- } else {
- server[module] = { ...server[module], ...data };
- }
- }
- })(),
- );
+ const data = await mods.get(serverId);
+ if (!data) return;
+
+ if (module === "general" && options?.preserveConnection) {
+ server[module] = {
+ ...server[module],
+ ...data,
+ image: server[module]?.image || data.image,
+ motd: server[module]?.motd || data.motd,
+ status:
+ options.preserveInstallState && server[module]?.status === "installing"
+ ? "installing"
+ : data.status,
+ };
+ } else {
+ server[module] = { ...server[module], ...data };
+ }
+ } catch (error) {
+ console.error(`Failed to refresh module ${module}:`, error);
+ if (error instanceof Error) {
+ serverError.addError(module, error);
+ }
+ }
+ });
+
+ await Promise.allSettled(modulePromises);
+
+ if (serverError.hasErrors()) {
+ if (server.error && server.error instanceof PyroServerError) {
+ serverError.errors.forEach((error, module) => {
+ (server.error as PyroServerError).addError(module, error);
+ });
+ } else {
+ server.setError(serverError);
}
}
+ },
+ loadModules: async (modulesToLoad: avaliableModules) => {
+ const newModules = modulesToLoad.filter((module) => !server[module]);
+ if (newModules.length === 0) return;
- await Promise.all(promises);
+ newModules.forEach((module) => {
+ server[module] = modules[module];
+ });
+
+ await server.refresh(newModules);
},
setError: (error: Error) => {
- server.error = error;
+ if (!server.error) {
+ server.error = error;
+ } else if (error instanceof PyroServerError) {
+ if (!(server.error instanceof PyroServerError)) {
+ const newError = new PyroServerError();
+ newError.addError("previous", server.error);
+ server.error = newError;
+ }
+ error.errors.forEach((err, module) => {
+ (server.error as PyroServerError).addError(module, err);
+ });
+ }
},
+
serverId,
});
- for (const module of includedModules) {
- const mods = modules[module];
- server[module] = mods;
- }
+ const initialModules = includedModules.filter((module) => ["general", "ws"].includes(module));
+ const deferredModules = includedModules.filter((module) => !["general", "ws"].includes(module));
+
+ initialModules.forEach((module) => {
+ server[module] = modules[module];
+ });
internalServerRefrence.value = server;
+ await server.refresh(initialModules);
- await server.refresh();
+ if (deferredModules.length > 0) {
+ await server.loadModules(deferredModules);
+ }
return server as Server;
};
diff --git a/apps/frontend/src/pages/search/[searchProjectType].vue b/apps/frontend/src/pages/search/[searchProjectType].vue
index 28463d969..5cb3b3282 100644
--- a/apps/frontend/src/pages/search/[searchProjectType].vue
+++ b/apps/frontend/src/pages/search/[searchProjectType].vue
@@ -258,7 +258,8 @@
- Server Upgrading
+ Server upgrading
- Your server's hardware is currently being upgraded and will be back online shortly.
+ Your server's hardware is currently being upgraded and will be back online shortly!
@@ -47,17 +47,18 @@
- Server Suspended
+ Server suspended
{{
- serverData.suspension_reason
- ? `Your server has been suspended: ${serverData.suspension_reason}`
- : "Your server has been suspended."
+ serverData.suspension_reason === "cancelled"
+ ? "Your subscription has been cancelled."
+ : serverData.suspension_reason
+ ? `Your server has been suspended: ${serverData.suspension_reason}`
+ : "Your server has been suspended."
}}
- This is most likely due to a billing issue. Please check your billing information and
- contact Modrinth support if you believe this is an error.
+ Contact Modrinth support if you believe this is an error.
router.push('/settings/billing')">
@@ -66,7 +67,10 @@
@@ -82,14 +86,15 @@
this is an error, please contact Modrinth support.
-
+
+
router.push('/servers/manage')">
Go back to all servers
@@ -164,7 +169,7 @@
temporary network issue. You'll be reconnected automatically.
-
+
@@ -363,7 +368,6 @@
-
{
+ if (server.general?.status === "suspended") {
+ return;
+ }
+ return server.loadModules(["content", "backups", "network", "startup", "fs"]);
+});
+
+provide("modulesLoaded", loadModulesPromise);
watch(
- () => server.error,
- (newError) => {
+ () => [server.general?.error, server.ws?.error],
+ ([generalError, wsError]) => {
if (server.general?.status === "suspended") return;
- if (newError && !newError.message.includes("Forbidden")) {
+
+ const error = generalError?.error || wsError?.error;
+ if (error && error.statusCode !== 403) {
startPolling();
}
},
@@ -450,11 +458,9 @@ const errorMessage = ref("An unexpected error occurred.");
const errorLog = ref("");
const errorLogFile = ref("");
const serverData = computed(() => server.general);
-const error = ref(null);
const isConnected = ref(false);
const isWSAuthIncorrect = ref(false);
const pyroConsole = usePyroConsole();
-console.log("||||||||||||||||||||||| console", pyroConsole.output);
const cpuData = ref([]);
const ramData = ref([]);
const isActioning = ref(false);
@@ -465,6 +471,7 @@ const powerStateDetails = ref<{ oom_killed?: boolean; exit_code?: number }>();
const uptimeSeconds = ref(0);
const firstConnect = ref(true);
const copied = ref(false);
+const error = ref(null);
const initialConsoleMessage = [
" __________________________________________________",
@@ -665,6 +672,26 @@ const newLoader = ref(null);
const newLoaderVersion = ref(null);
const newMCVersion = ref(null);
+const onReinstall = (potentialArgs: any) => {
+ if (!serverData.value) return;
+
+ serverData.value.status = "installing";
+
+ if (potentialArgs?.loader) {
+ newLoader.value = potentialArgs.loader;
+ }
+ if (potentialArgs?.lVersion) {
+ newLoaderVersion.value = potentialArgs.lVersion;
+ }
+ if (potentialArgs?.mVersion) {
+ newMCVersion.value = potentialArgs.mVersion;
+ }
+
+ error.value = null;
+ errorTitle.value = "Error";
+ errorMessage.value = "An unexpected error occurred.";
+};
+
const handleInstallationResult = async (data: WSInstallationResultEvent) => {
switch (data.result) {
case "ok": {
@@ -738,26 +765,6 @@ const handleInstallationResult = async (data: WSInstallationResultEvent) => {
}
};
-const onReinstall = (potentialArgs: any) => {
- if (!serverData.value) return;
-
- serverData.value.status = "installing";
-
- if (potentialArgs?.loader) {
- newLoader.value = potentialArgs.loader;
- }
- if (potentialArgs?.lVersion) {
- newLoaderVersion.value = potentialArgs.lVersion;
- }
- if (potentialArgs?.mVersion) {
- newMCVersion.value = potentialArgs.mVersion;
- }
-
- error.value = null;
- errorTitle.value = "Error";
- errorMessage.value = "An unexpected error occurred.";
-};
-
const updateStats = (currentStats: Stats["current"]) => {
isConnected.value = true;
stats.value = {
@@ -924,6 +931,10 @@ const cleanup = () => {
onMounted(() => {
isMounted.value = true;
+ if (server.general?.status === "suspended") {
+ isLoading.value = false;
+ return;
+ }
if (server.error) {
if (!server.error.message.includes("Forbidden")) {
startPolling();
@@ -991,7 +1002,7 @@ definePageMeta({
});
-