diff --git a/apps/frontend/src/composables/servers/modrinth-servers.ts b/apps/frontend/src/composables/servers/modrinth-servers.ts index 8c79cc62..e91bb853 100644 --- a/apps/frontend/src/composables/servers/modrinth-servers.ts +++ b/apps/frontend/src/composables/servers/modrinth-servers.ts @@ -124,58 +124,63 @@ export class ModrinthServer { return dataURL; } } catch (error) { - if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { - // Handle external icon processing - 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 useServersFetch(`/create?path=/server-icon.png&type=file`, { - method: "POST", - contentType: "application/octet-stream", - body: scaledFile, - override: auth, - }); - await useServersFetch(`/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); + if (error instanceof ModrinthServerError && error.statusCode === 404) { + if (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", }); - return dataURL; + + 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 useServersFetch(`/create?path=/server-icon.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: scaledFile, + override: auth, + }); + await useServersFetch(`/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 (externalError: any) { + console.debug("Could not process external icon:", externalError.message); } - } catch (error) { - console.error("Failed to process external icon:", error); } + } else { + throw error; } } - } catch (error) { - console.error("Failed to process server icon:", error); + } catch (error: any) { + console.debug("Icon processing failed:", error.message); } sharedImage.value = undefined; @@ -239,6 +244,18 @@ export class ModrinthServer { break; } } catch (error) { + if (error instanceof ModrinthServerError) { + if (error.statusCode === 404 && ["fs", "content"].includes(module)) { + console.debug(`Optional ${module} resource not found:`, error.message); + continue; + } + + if (error.statusCode === 503) { + console.debug(`Temporary ${module} unavailable:`, error.message); + continue; + } + } + this.errors[module] = { error: error instanceof ModrinthServerError diff --git a/apps/frontend/src/composables/servers/modules/general.ts b/apps/frontend/src/composables/servers/modules/general.ts index e3c4cc38..21546cb7 100644 --- a/apps/frontend/src/composables/servers/modules/general.ts +++ b/apps/frontend/src/composables/servers/modules/general.ts @@ -155,19 +155,25 @@ export class GeneralModule extends ServerModule implements ServerGeneral { } async setMotd(motd: string): Promise { - const props = (await this.server.fetchConfigFile("ServerProperties")) as any; - if (props) { - props.motd = motd; - const newProps = this.server.constructServerProperties(props); - const octetStream = new Blob([newProps], { type: "application/octet-stream" }); - const auth = await useServersFetch(`servers/${this.serverId}/fs`); + try { + const props = (await this.server.fetchConfigFile("ServerProperties")) as any; + if (props) { + props.motd = motd; + const newProps = this.server.constructServerProperties(props); + const octetStream = new Blob([newProps], { type: "application/octet-stream" }); + const auth = await useServersFetch(`servers/${this.serverId}/fs`); - await useServersFetch(`/update?path=/server.properties`, { - method: "PUT", - contentType: "application/octet-stream", - body: octetStream, - override: auth, - }); + await useServersFetch(`/update?path=/server.properties`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: auth, + }); + } + } catch { + console.error( + "[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.", + ); } } } diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue index 35fd2834..566cc1ed 100644 --- a/apps/frontend/src/pages/servers/manage/[id].vue +++ b/apps/frontend/src/pages/servers/manage/[id].vue @@ -18,48 +18,25 @@ v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" > -
-
-
-
- -
-

Server upgrading

-
-

- Your server's hardware is currently being upgraded and will be back online shortly! -

-
-
+
-
-
-
-
- -
-

Server 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." - }} -
- Contact Modrinth Support if you believe this is an error. -

-
- - - -
+
-
-
-
-
- -
-

Server not found

-
-

- You don't have permission to view this server or it no longer exists. If you believe this - is an error, please contact Modrinth Support. -

-
- - - - - -
+
-
-
-
-
- -
-

Server Node Unavailable

+ + +
-
-
-
-
- -
-

Connection lost

+ + +
{ const stopUptimeUpdates = () => { if (uptimeIntervalId) { clearInterval(uptimeIntervalId); - intervalId = null; + pollingIntervalId = null; } }; @@ -1055,7 +992,7 @@ const notifyError = (title: string, text: string) => { }); }; -let intervalId: ReturnType | null = null; +let pollingIntervalId: ReturnType | null = null; const countdown = ref(15); const formattedTime = computed(() => { @@ -1099,23 +1036,142 @@ const backupInProgress = computed(() => { }); const stopPolling = () => { - if (intervalId) { - clearInterval(intervalId); - intervalId = null; + if (pollingIntervalId) { + clearTimeout(pollingIntervalId); + pollingIntervalId = null; } }; const startPolling = () => { - countdown.value = 15; - intervalId = setInterval(() => { - if (countdown.value <= 0) { - reloadNuxtApp(); - } else { - countdown.value--; + stopPolling(); + + let retryCount = 0; + const maxRetries = 10; + + const poll = async () => { + try { + await server.refresh(["general", "ws"]); + + if (!server.moduleErrors?.general?.error) { + stopPolling(); + connectWebSocket(); + return; + } + + retryCount++; + if (retryCount >= maxRetries) { + console.error("Max retries reached, stopping polling"); + stopPolling(); + return; + } + + // Exponential backoff: 3s, 6s, 12s, 24s, etc. + const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); + + pollingIntervalId = setTimeout(poll, delay); + } catch (error) { + console.error("Polling failed:", error); + retryCount++; + + if (retryCount < maxRetries) { + const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); + pollingIntervalId = setTimeout(poll, delay); + } } - }, 1000); + }; + + poll(); }; +const nodeUnavailableDetails = computed(() => [ + { + label: "Server ID", + value: server.serverId, + type: "inline" as const, + }, + { + label: "Node", + value: server.general?.datacenter ?? "Unknown! Please contact support!", + type: "inline" as const, + }, +]); + +const suspendedDescription = computed(() => { + if (serverData.value?.suspension_reason === "cancelled") { + return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error."; + } + if (serverData.value?.suspension_reason) { + return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`; + } + return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error."; +}); + +const generalErrorDetails = computed(() => [ + { + label: "Server ID", + value: server.serverId, + type: "inline" as const, + }, + { + label: "Timestamp", + value: String(server.moduleErrors?.general?.timestamp), + type: "inline" as const, + }, + { + label: "Error Name", + value: server.moduleErrors?.general?.error.name, + type: "inline" as const, + }, + { + label: "Error Message", + value: server.moduleErrors?.general?.error.message, + type: "block" as const, + }, + ...(server.moduleErrors?.general?.error.originalError + ? [ + { + label: "Original Error", + value: String(server.moduleErrors.general.error.originalError), + type: "hidden" as const, + }, + ] + : []), + ...(server.moduleErrors?.general?.error.stack + ? [ + { + label: "Stack Trace", + value: server.moduleErrors.general.error.stack, + type: "hidden" as const, + }, + ] + : []), +]); + +const suspendedAction = computed(() => ({ + label: "Go to billing settings", + onClick: () => router.push("/settings/billing"), + color: "brand" as const, +})); + +const generalErrorAction = computed(() => ({ + label: "Go back to all servers", + onClick: () => router.push("/servers/manage"), + color: "brand" as const, +})); + +const nodeUnavailableAction = computed(() => ({ + label: "Join Modrinth Discord", + onClick: () => navigateTo("https://discord.modrinth.com", { external: true }), + color: "standard" as const, +})); + +const connectionLostAction = computed(() => ({ + label: "Reload", + onClick: () => reloadNuxtApp(), + color: "brand" as const, + disabled: formattedTime.value !== "00", +})); + const copyServerDebugInfo = () => { const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`; navigator.clipboard.writeText(debugInfo); diff --git a/packages/ui/src/components/base/ErrorInformationCard.vue b/packages/ui/src/components/base/ErrorInformationCard.vue new file mode 100644 index 00000000..9edfc026 --- /dev/null +++ b/packages/ui/src/components/base/ErrorInformationCard.vue @@ -0,0 +1,120 @@ + + + diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index a1c39233..3ca10fb9 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,6 +16,7 @@ export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DropArea } from './base/DropArea.vue' export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' +export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue' export { default as FileInput } from './base/FileInput.vue' export { default as FilterBar } from './base/FilterBar.vue' export type { FilterBarOption } from './base/FilterBar.vue' diff --git a/packages/utils/servers/errors/modrinth-server-error.ts b/packages/utils/servers/errors/modrinth-server-error.ts index ece8825c..c0a548a7 100644 --- a/packages/utils/servers/errors/modrinth-server-error.ts +++ b/packages/utils/servers/errors/modrinth-server-error.ts @@ -54,6 +54,6 @@ export class ModrinthServerError extends Error { } super(errorMessage) - this.name = 'PyroServersFetchError' + this.name = 'ModrinthServersFetchError' } } diff --git a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts index ce01e737..a2df7baa 100644 --- a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts +++ b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts @@ -5,6 +5,6 @@ export class ModrinthServersFetchError extends Error { public originalError?: Error, ) { super(message) - this.name = 'PyroFetchError' + this.name = 'ModrinthFetchError' } }