From 088cb54317c5f181ebb9d4d754a8c3d73d351902 Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Mon, 7 Jul 2025 14:11:36 -0500 Subject: [PATCH 01/14] Fix failure when "Test"ing a Java installation (#3935) * Fix failure when "Test"ing a Java installation * Fix lint --- apps/app-frontend/src/components/ui/JavaSelector.vue | 1 - apps/app-frontend/src/helpers/jre.js | 4 ++-- packages/app-lib/src/api/jre.rs | 12 ++++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/app-frontend/src/components/ui/JavaSelector.vue b/apps/app-frontend/src/components/ui/JavaSelector.vue index 9beea538..ebbeed89 100644 --- a/apps/app-frontend/src/components/ui/JavaSelector.vue +++ b/apps/app-frontend/src/components/ui/JavaSelector.vue @@ -108,7 +108,6 @@ async function testJava() { testingJava.value = true testingJavaSuccess.value = await test_jre( props.modelValue ? props.modelValue.path : '', - 1, props.version, ) testingJava.value = false diff --git a/apps/app-frontend/src/helpers/jre.js b/apps/app-frontend/src/helpers/jre.js index 0814e9b0..207c0258 100644 --- a/apps/app-frontend/src/helpers/jre.js +++ b/apps/app-frontend/src/helpers/jre.js @@ -36,8 +36,8 @@ export async function get_jre(path) { // Tests JRE version by running 'java -version' on it. // Returns true if the version is valid, and matches given (after extraction) -export async function test_jre(path, majorVersion, minorVersion) { - return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion }) +export async function test_jre(path, majorVersion) { + return await invoke('plugin:jre|jre_test_jre', { path, majorVersion }) } // Automatically installs specified java version diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 93a63674..412d4472 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -166,10 +166,18 @@ pub async fn test_jre( path: PathBuf, major_version: u32, ) -> crate::Result { - let Ok(jre) = jre::check_java_at_filepath(&path).await else { - return Ok(false); + let jre = match jre::check_java_at_filepath(&path).await { + Ok(jre) => jre, + Err(e) => { + tracing::warn!("Invalid Java at {}: {e}", path.display()); + return Ok(false); + } }; let version = extract_java_version(&jre.version)?; + tracing::info!( + "Expected Java version {major_version}, and found {version} at {}", + path.display() + ); Ok(version == major_version) } From 8ba6467f21ba495fdfa07816d8d89bdad80a189e Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 16:49:17 -0700 Subject: [PATCH 02/14] temp: do not retry MRS requests --- apps/frontend/src/composables/servers/servers-fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 137baea5..804960b6 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -134,7 +134,7 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; + const isRetryable = false; if (!isRetryable || attempts >= maxAttempts) { console.error("Fetch error:", error); From e4e77dc0d27f1ee1a9c2ed34b161147cc3a9adba Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 17:07:27 -0700 Subject: [PATCH 03/14] Revert "temp: do not retry MRS requests" This reverts commit 8ba6467f21ba495fdfa07816d8d89bdad80a189e. --- apps/frontend/src/composables/servers/servers-fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 804960b6..137baea5 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -134,7 +134,7 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = false; + const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; if (!isRetryable || attempts >= maxAttempts) { console.error("Fetch error:", error); From e0cde2d6ff1187b2ee2346ec6aea67a3190573cd Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 17:37:43 -0700 Subject: [PATCH 04/14] Revert "fix: error handling improvements (#3797)" This reverts commit 706976439db46125f9a9a9ea177ac9074d4147a8. --- .../composables/servers/modrinth-servers.ts | 111 +++--- .../composables/servers/modules/general.ts | 30 +- .../src/pages/servers/manage/[id].vue | 330 ++++++++---------- .../components/base/ErrorInformationCard.vue | 120 ------- packages/ui/src/components/index.ts | 1 - .../servers/errors/modrinth-server-error.ts | 2 +- .../errors/modrinth-servers-fetch-error.ts | 2 +- 7 files changed, 198 insertions(+), 398 deletions(-) delete mode 100644 packages/ui/src/components/base/ErrorInformationCard.vue diff --git a/apps/frontend/src/composables/servers/modrinth-servers.ts b/apps/frontend/src/composables/servers/modrinth-servers.ts index e91bb853..8c79cc62 100644 --- a/apps/frontend/src/composables/servers/modrinth-servers.ts +++ b/apps/frontend/src/composables/servers/modrinth-servers.ts @@ -124,63 +124,58 @@ export class ModrinthServer { return dataURL; } } catch (error) { - 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", - }); + 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); - }); - return dataURL; - } - } catch (externalError: any) { - console.debug("Could not process external icon:", externalError.message); + 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 (error) { + console.error("Failed to process external icon:", error); } - } else { - throw error; } } - } catch (error: any) { - console.debug("Icon processing failed:", error.message); + } catch (error) { + console.error("Failed to process server icon:", error); } sharedImage.value = undefined; @@ -244,18 +239,6 @@ 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 b2f10065..9e8a4c4c 100644 --- a/apps/frontend/src/composables/servers/modules/general.ts +++ b/apps/frontend/src/composables/servers/modules/general.ts @@ -194,25 +194,19 @@ export class GeneralModule extends ServerModule implements ServerGeneral { } async setMotd(motd: string): Promise { - 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`); + 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, - }); - } - } catch { - console.error( - "[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.", - ); + await useServersFetch(`/update?path=/server.properties`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: auth, + }); } } } diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue index 566cc1ed..35fd2834 100644 --- a/apps/frontend/src/pages/servers/manage/[id].vue +++ b/apps/frontend/src/pages/servers/manage/[id].vue @@ -18,25 +18,48 @@ 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. +

+
+ + + + + +
- - - +

+ Your server's node, where your Modrinth Server is physically hosted, is experiencing + issues. We are working with our datacenter to resolve the issue as quickly as possible. +

+

+ Your data is safe and will not be lost, and your server will be back online as soon as the + issue is resolved. +

+

+ For updates, please join the Modrinth Discord or contact Modrinth Support via the chat + bubble in the bottom right corner and we'll be happy to help. +

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

+ Something went wrong, and we couldn't connect to your server. This is likely due to a + temporary network issue. You'll be reconnected automatically. +

+
+ + + + +
{ const stopUptimeUpdates = () => { if (uptimeIntervalId) { clearInterval(uptimeIntervalId); - pollingIntervalId = null; + intervalId = null; } }; @@ -992,7 +1055,7 @@ const notifyError = (title: string, text: string) => { }); }; -let pollingIntervalId: ReturnType | null = null; +let intervalId: ReturnType | null = null; const countdown = ref(15); const formattedTime = computed(() => { @@ -1036,142 +1099,23 @@ const backupInProgress = computed(() => { }); const stopPolling = () => { - if (pollingIntervalId) { - clearTimeout(pollingIntervalId); - pollingIntervalId = null; + if (intervalId) { + clearInterval(intervalId); + intervalId = null; } }; const startPolling = () => { - 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); - } + countdown.value = 15; + intervalId = setInterval(() => { + if (countdown.value <= 0) { + reloadNuxtApp(); + } else { + countdown.value--; } - }; - - poll(); + }, 1000); }; -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 deleted file mode 100644 index 9edfc026..00000000 --- a/packages/ui/src/components/base/ErrorInformationCard.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 3057f43d..9b41b4f5 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,7 +16,6 @@ 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 c0a548a7..ece8825c 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 = 'ModrinthServersFetchError' + this.name = 'PyroServersFetchError' } } diff --git a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts index a2df7baa..ce01e737 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 = 'ModrinthFetchError' + this.name = 'PyroFetchError' } } From f256ef43c06891f333ce9855bf3775c2b7184b03 Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 22:16:26 -0700 Subject: [PATCH 05/14] Add x-archon-request header --- apps/frontend/src/composables/servers/servers-fetch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 137baea5..cd403fb1 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -69,6 +69,7 @@ export async function useServersFetch( const headers: Record = { "User-Agent": "Modrinth/1.0 (https://modrinth.com)", + "X-Archon-Request": "true", Vary: "Accept, Origin", }; From 7a12c4d5e2139a1bdf44747e1b5b021e057ed59f Mon Sep 17 00:00:00 2001 From: IMB11 Date: Tue, 8 Jul 2025 18:40:44 +0100 Subject: [PATCH 06/14] feat: reimplement error handling improvements w/o polling (#3942) * Reapply "fix: error handling improvements (#3797)" This reverts commit e0cde2d6ff1187b2ee2346ec6aea67a3190573cd. * fix: polling issues * fix: circuit breaker logic for spam * fix: remove polling & ping test node instead * fix: remove broken url from debugging * fix: show error information display if node access fails in fs module --- .../composables/servers/modrinth-servers.ts | 164 +++++--- .../src/composables/servers/modules/fs.ts | 33 +- .../composables/servers/modules/general.ts | 49 ++- .../src/composables/servers/servers-fetch.ts | 32 +- .../src/pages/servers/manage/[id].vue | 381 +++++++++--------- .../components/base/ErrorInformationCard.vue | 120 ++++++ packages/ui/src/components/index.ts | 1 + .../servers/errors/modrinth-server-error.ts | 2 +- .../errors/modrinth-servers-fetch-error.ts | 2 +- 9 files changed, 504 insertions(+), 280 deletions(-) create mode 100644 packages/ui/src/components/base/ErrorInformationCard.vue diff --git a/apps/frontend/src/composables/servers/modrinth-servers.ts b/apps/frontend/src/composables/servers/modrinth-servers.ts index 8c79cc62..8d07648d 100644 --- a/apps/frontend/src/composables/servers/modrinth-servers.ts +++ b/apps/frontend/src/composables/servers/modrinth-servers.ts @@ -102,7 +102,7 @@ export class ModrinthServer { try { const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, { override: auth, - retry: false, + retry: 1, // Reduce retries for optional resources }); if (fileData instanceof Blob && import.meta.client) { @@ -124,64 +124,114 @@ 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); - }); - return dataURL; - } - } catch (error) { - console.error("Failed to process external icon:", error); + if (error instanceof ModrinthServerError) { + if (error.statusCode && error.statusCode >= 500) { + console.debug("Service unavailable, skipping icon processing"); + sharedImage.value = undefined; + return undefined; } + + if (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 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); + } + } + } 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; return undefined; } + async testNodeReachability(): Promise { + if (!this.general?.datacenter) { + console.warn("No datacenter info available for ping test"); + return false; + } + + const datacenter = this.general.datacenter; + const wsUrl = `wss://${datacenter}.nodes.modrinth.com/pingtest`; + + try { + return await new Promise((resolve) => { + const socket = new WebSocket(wsUrl); + const timeout = setTimeout(() => { + socket.close(); + resolve(false); + }, 5000); + + socket.onopen = () => { + clearTimeout(timeout); + socket.send(performance.now().toString()); + }; + + socket.onmessage = () => { + clearTimeout(timeout); + socket.close(); + resolve(true); + }; + + socket.onerror = () => { + clearTimeout(timeout); + resolve(false); + }; + }); + } catch (error) { + console.error(`Failed to ping node ${wsUrl}:`, error); + return false; + } + } + async refresh( modules: ModuleName[] = [], options?: { @@ -195,6 +245,8 @@ export class ModrinthServer { : (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]); for (const module of modulesToRefresh) { + this.errors[module] = undefined; + try { switch (module) { case "general": { @@ -239,6 +291,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 && error.statusCode >= 500) { + console.debug(`Temporary ${module} unavailable:`, error.message); + continue; + } + } + this.errors[module] = { error: error instanceof ModrinthServerError diff --git a/apps/frontend/src/composables/servers/modules/fs.ts b/apps/frontend/src/composables/servers/modules/fs.ts index 1072789e..39fe75db 100644 --- a/apps/frontend/src/composables/servers/modules/fs.ts +++ b/apps/frontend/src/composables/servers/modules/fs.ts @@ -22,26 +22,49 @@ export class FSModule extends ServerModule { this.opsQueuedForModification = []; } - private async retryWithAuth(requestFn: () => Promise): Promise { + private async retryWithAuth( + requestFn: () => Promise, + ignoreFailure: boolean = false, + ): Promise { try { return await requestFn(); } catch (error) { if (error instanceof ModrinthServerError && error.statusCode === 401) { + console.debug("Auth failed, refreshing JWT and retrying"); await this.fetch(); // Refresh auth return await requestFn(); } + + const available = await this.server.testNodeReachability(); + if (!available && !ignoreFailure) { + this.server.moduleErrors.general = { + error: new ModrinthServerError( + "Unable to reach node. FS operation failed and subsequent ping test failed.", + 500, + error as Error, + "fs", + ), + timestamp: Date.now(), + }; + } + throw error; } } - listDirContents(path: string, page: number, pageSize: number): Promise { + listDirContents( + path: string, + page: number, + pageSize: number, + ignoreFailure: boolean = false, + ): Promise { return this.retryWithAuth(async () => { const encodedPath = encodeURIComponent(path); return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, { override: this.auth, retry: false, }); - }); + }, ignoreFailure); } createFileOrFolder(path: string, type: "file" | "directory"): Promise { @@ -150,7 +173,7 @@ export class FSModule extends ServerModule { }); } - downloadFile(path: string, raw?: boolean): Promise { + downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise { return this.retryWithAuth(async () => { const encodedPath = encodeURIComponent(path); const fileData = await useServersFetch(`/download?path=${encodedPath}`, { @@ -161,7 +184,7 @@ export class FSModule extends ServerModule { return raw ? fileData : await fileData.text(); } return fileData; - }); + }, ignoreFailure); } extractFile( diff --git a/apps/frontend/src/composables/servers/modules/general.ts b/apps/frontend/src/composables/servers/modules/general.ts index 9e8a4c4c..e46e62b4 100644 --- a/apps/frontend/src/composables/servers/modules/general.ts +++ b/apps/frontend/src/composables/servers/modules/general.ts @@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral { data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined; } - const motd = await this.getMotd(); - if (motd === "A Minecraft Server") { - await this.setMotd( - `§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`, - ); + try { + const motd = await this.getMotd(); + if (motd === "A Minecraft Server") { + await this.setMotd( + `§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`, + ); + } + data.motd = motd; + } catch { + console.error("[Modrinth Servers] [General] Failed to fetch MOTD."); + data.motd = undefined; } - data.motd = motd; // Copy data to this module Object.assign(this, data); @@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral { async getMotd(): Promise { try { - const props = await this.server.fs.downloadFile("/server.properties"); + const props = await this.server.fs.downloadFile("/server.properties", false, true); if (props) { const lines = props.split("\n"); for (const line of lines) { @@ -194,19 +199,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/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index cd403fb1..5b5d925b 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -42,6 +42,23 @@ export async function useServersFetch( retry = method === "GET" ? 3 : 0, } = options; + const circuitBreakerKey = `${module || "default"}_${path}`; + const failureCount = useState(`fetch_failures_${circuitBreakerKey}`, () => 0); + const lastFailureTime = useState(`last_failure_${circuitBreakerKey}`, () => 0); + + const now = Date.now(); + if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) { + const error = new ModrinthServersFetchError( + "[Modrinth Servers] Circuit breaker open - too many recent failures", + 503, + ); + throw new ModrinthServerError("Service temporarily unavailable", 503, error, module); + } + + if (now - lastFailureTime.value > 30000) { + failureCount.value = 0; + } + const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace( /\/$/, "", @@ -99,6 +116,7 @@ export async function useServersFetch( timeout: 10000, }); + failureCount.value = 0; return response; } catch (error) { lastError = error as Error; @@ -108,6 +126,11 @@ export async function useServersFetch( const statusCode = error.response?.status; const statusText = error.response?.statusText || "Unknown error"; + if (statusCode && statusCode >= 500) { + failureCount.value++; + lastFailureTime.value = now; + } + let v1Error: V1ErrorInfo | undefined; if (error.data?.error && error.data?.description) { v1Error = { @@ -135,9 +158,11 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; + const isRetryable = statusCode ? [408, 429].includes(statusCode) : false; + const is5xxRetryable = + statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1; - if (!isRetryable || attempts >= maxAttempts) { + if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) { console.error("Fetch error:", error); const fetchError = new ModrinthServersFetchError( @@ -148,7 +173,8 @@ export async function useServersFetch( throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error); } - const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000); + const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000; + const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000); console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue index 35fd2834..1c5d0138 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

-
- {{ - formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...` - }} -
+ + + +
-->
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 9b41b4f5..3057f43d 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' } } From 242fd713ab094297581d55338eb4b5a60ecd4a05 Mon Sep 17 00:00:00 2001 From: Prospector Date: Tue, 8 Jul 2025 11:06:50 -0700 Subject: [PATCH 07/14] Update changelog --- packages/utils/changelog.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 3c9db638..2cdf1093 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -10,6 +10,24 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2025-07-08T11:10:00-07:00`, + product: 'servers', + body: `### Improvements +- Reapplied error handling improvements, with more improvements.`, + }, + { + date: `2025-07-07T22:20:00-07:00`, + product: 'servers', + body: `### Improvements +- Fixed issue with Servers panel failing to load.`, + }, + { + date: `2025-07-07T17:45:00-07:00`, + product: 'servers', + body: `### Improvements +- Reverted error handling improvements.`, + }, { date: `2025-07-07T01:10:00-07:00`, product: 'app', @@ -73,7 +91,8 @@ const VERSIONS: VersionEntry[] = [ date: `2025-07-04T12:00:00-07:00`, product: 'web', body: `### Changed -- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers.`, +- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers. +- Fixed news section rendering incorrectly in light mode on landing page and Modrinth App page.`, }, { date: `2025-06-30T19:15:00-07:00`, From 2692953e31d056b52889e1bdb4e5eb18cc3d42c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:03:30 +0200 Subject: [PATCH 08/14] fix(app): make Party Alex bonus default skin have slim arms (#3945) This skin was incorrectly declared as having wide arms. Resolves #3941. --- .../src/api/minecraft_skins/assets/default/default_skins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs b/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs index fdbee628..6c26f07d 100644 --- a/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs +++ b/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs @@ -231,7 +231,7 @@ pub static DEFAULT_SKINS: LazyLock> = LazyLock::new(|| { Skin { texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"), name: Some(Arc::from("Party Alex")), - variant: MinecraftSkinVariant::Classic, + variant: MinecraftSkinVariant::Slim, cape_id: None, texture: Arc::from(Url::try_from( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJEjAE4f2wWAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAFFUlEQVR4nO1aXWhcRRS+XeMmClmbNVtTMdjUqlHrX5QSC0oMUnzxoVFBEAqSh4oRBEEEH0ooIvjggyDiSx4EkUgfLC1IEC0+iFKoQluFFNpGqLbotqtNGyVtynW/ac/s2ZN77+z9nb1NPviy586dmdxzZubMzJlxJicnnSBW3x92JX9/+34tH95xpyJPgwwi3VS/bRacFtBR6W5iZ7mk5bzDaIAwSi7W5hXzhJZ6QBAGnn1OsfTw6DIivd0R2wB5x6oBbH+Abax4A3jO8ybyOT8uc7EO4MA01/P0eO6mOz8UaO5ulVCeELasF22jpR7w6J5Oxb5X3nM6N9zlbHjtM/V8PcDTAFAUBKDo3JEfnLF3ruj3kJGGd+fOLDTlzx1o08K5NLvXXZj5QHHp6K/q2ZP1dzpf/VnWQ5sivjmS6badoCM/GoRCFy/+pxUDFz/9SKWBkCmd8nrVw5WFLHeLbWkAalHestwIXHkygJ/yZIB23i4vS7jv4yGXs7+/v4mmCu9+c8QF/Qyy6d3NgTTVzxtn/vPX9W9UA6SyEvzp8c16l8gJ3FguBtIEzEJgsacjkW/N9VL4hvKAc6U2p36jItcGSAQYdxjrNAYnzvTp8f/U93eotMqLWzUpH5WhMc+JMSlpcpTSSVJd0hETLx/8cNn/4P4hlA+4XLukDXLgy9u1XD2yztdwvIzEY4d+Ud2SODezP7ARsJgCaHmMaFJW0EOAO6DKQ39FrrBj09X1/SNf79cOywsv9A5oPn9PtzPSXdChNMAtn438DWGgDcBbNKjlvcA/trtyWhkBfOOWV50HD+zW77jSUsHyvWedoZPfaeI5CxR+3jjicB7a+LJzdHSXImSkndg+pCnzA2tqvbrCC9XGEOLDCUpT75BlgNqx4Oe0UNiy+xuHSFj87YQigeeRJHDlRrefbpJfGr5VvUfvkHm5DPCW73liNgEVg9G0moBCN5cqTRn+na8GVvDPqWtD51RX/c8lZ61Tqrd84z3vBX//OHg1/7W8jXJdOs/5w6VmuRF+SAWx1wFr+4tNBJaOlzzpld/EtOFpgGK9FxRFTwgCd2jrx45rJ7jtrVktU7rMjzQ8g+jyXM4CiawEuUPjM4jfmoLnp54BcMeXmRP0SuzpXa8YBtyZkbMD+JqCp/NewB0fl6WDTAWDg4N6qwsZHH5mTJGe5XtOuZ2V22lJ03ZYEstaLHs5Kc1rKUy/keMBkmHjAXmj9Q+wTWOGFd8DrnsDxI0H2FYgLlOJB3hh4qYZFwxVKAMkHg/4Yt2+ZUpC8Xa9UJVIPIADix0YAazu/ET9QvmFY384W//sMleQNZIeU+Q/tuzp06QYH9Jtj3nJVAwAR0rKk0xGsa1w6gYgI3jRtrKZGSBPtP4Btmn9A2yzMD097XLu++pb9+BsVRGyfC9pdQpLAtjTwxJ8jy/jAV55eHqeacwQdjMU934BnRHi/C+J838TjTHB8fHxNZwZdMpMYTTA1NSUy5nFRxGSOP83YfV+gNzrUxzAL5DJnzGGZHla9tLS16scl3lQk8Y/fuV9AHl5K1UfwHeDct9vigPww1Eu83JBdeB+QJaXLn2HAI/hAzxe0MplpijA3YDbxnamUrcffA3AWw/wa0F5dO51vB5E2+iYeGBbY2pbqjN6MCiXiD0LJHW/wBYSnQah0JO79jaxHZQMwopfB6RigLD3C2xitQekEQ+Icr/AGuLGA+LeL5Bn/Pz8X94JiHL+b+L/USxRwWwDOUgAAAAASUVORK5CYII=" From 6caf794ae17974bf005edeca620a18d37bb535f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:22:15 +0200 Subject: [PATCH 09/14] dist(docker): add `curl` package to Labrinth image, some other minor tweaks (#3915) * dist(docker): add `.dockerignore` as symlink to `.gitignore` This ensures that no files outside of version control are transferred to the Docker build context for Labrinth and Daedalus images, which significantly improves build speed (if a `target` directory is already present) and build reproducibility. * chore(dist/docker): simplify out unneeeded statements, move `SQLX_OFFLINE` env var setting to build command itself The latter approach ensures that developers building the image locally don't forget to set `SQLX_OFFLINE`, too. * dist(docker): add `curl` package to Labrinth image --- .dockerignore | 1 + .github/workflows/labrinth-docker.yml | 5 ----- apps/daedalus_client/Dockerfile | 4 ---- apps/labrinth/Dockerfile | 10 ++-------- 4 files changed, 3 insertions(+), 17 deletions(-) create mode 120000 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 00000000..3e4e48b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.github/workflows/labrinth-docker.yml b/.github/workflows/labrinth-docker.yml index 1ed8e968..114c8ee4 100644 --- a/.github/workflows/labrinth-docker.yml +++ b/.github/workflows/labrinth-docker.yml @@ -18,9 +18,6 @@ on: jobs: docker: runs-on: ubuntu-latest - defaults: - run: - working-directory: ./apps/labrinth steps: - name: Checkout uses: actions/checkout@v2 @@ -38,8 +35,6 @@ jobs: - name: Build and push id: docker_build uses: docker/build-push-action@v2 - env: - SQLX_OFFLINE: true with: file: ./apps/labrinth/Dockerfile push: ${{ github.event_name != 'pull_request' }} diff --git a/apps/daedalus_client/Dockerfile b/apps/daedalus_client/Dockerfile index d33fc113..9ea70f9c 100644 --- a/apps/daedalus_client/Dockerfile +++ b/apps/daedalus_client/Dockerfile @@ -1,5 +1,4 @@ FROM rust:1.88.0 AS build -ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/daedalus COPY . . @@ -10,11 +9,8 @@ FROM debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates openssl \ - && apt-get clean \ && rm -rf /var/lib/apt/lists/* -RUN update-ca-certificates - COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client WORKDIR /daedalus_client diff --git a/apps/labrinth/Dockerfile b/apps/labrinth/Dockerfile index 2ba54754..01b73d95 100644 --- a/apps/labrinth/Dockerfile +++ b/apps/labrinth/Dockerfile @@ -1,11 +1,8 @@ FROM rust:1.88.0 AS build -ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/labrinth COPY . . -COPY apps/labrinth/.sqlx/ .sqlx/ -RUN cargo build --release --package labrinth - +RUN SQLX_OFFLINE=true cargo build --release --package labrinth FROM debian:bookworm-slim @@ -14,12 +11,9 @@ LABEL org.opencontainers.image.description="Modrinth API" LABEL org.opencontainers.image.licenses=AGPL-3.0 RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates openssl dumb-init \ - && apt-get clean \ + && apt-get install -y --no-install-recommends ca-certificates openssl dumb-init curl \ && rm -rf /var/lib/apt/lists/* -RUN update-ca-certificates - COPY --from=build /usr/src/labrinth/target/release/labrinth /labrinth/labrinth COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/ COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets From 26df6f51efd4168428876475f8c5ced64c7cdc25 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Tue, 8 Jul 2025 21:09:36 +0100 Subject: [PATCH 10/14] fix: composable used outside ... issue + disable cache (#3947) --- apps/frontend/src/pages/servers/index.vue | 44 +++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/frontend/src/pages/servers/index.vue b/apps/frontend/src/pages/servers/index.vue index 9b3c363a..60495d15 100644 --- a/apps/frontend/src/pages/servers/index.vue +++ b/apps/frontend/src/pages/servers/index.vue @@ -719,31 +719,32 @@ async function fetchCapacityStatuses(customProduct = null) { product.metadata.ram < min.metadata.ram ? product : min, ), ]; - const capacityChecks = productsToCheck.map((product) => - useServersFetch("stock", { - method: "POST", - body: { - cpu: product.metadata.cpu, - memory_mb: product.metadata.ram, - swap_mb: product.metadata.swap, - storage_mb: product.metadata.storage, - }, - bypassAuth: true, - }), - ); - - const results = await Promise.all(capacityChecks); + const capacityChecks = []; + for (const product of productsToCheck) { + capacityChecks.push( + useServersFetch("stock", { + method: "POST", + body: { + cpu: product.metadata.cpu, + memory_mb: product.metadata.ram, + swap_mb: product.metadata.swap, + storage_mb: product.metadata.storage, + }, + bypassAuth: true, + }), + ); + } if (customProduct?.metadata) { return { - custom: results[0], + custom: await capacityChecks[0], }; } else { return { - small: results[0], - medium: results[1], - large: results[2], - custom: results[3], + small: await capacityChecks[0], + medium: await capacityChecks[1], + large: await capacityChecks[2], + custom: await capacityChecks[3], }; } } catch (error) { @@ -760,6 +761,11 @@ async function fetchCapacityStatuses(customProduct = null) { const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData( "ServerCapacityAll", fetchCapacityStatuses, + { + getCachedData() { + return null; // Dont cache stock data. + }, + }, ); const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0); From 1fdb5ba7488431720fed0584dc3e1d8ec3235f3b Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:48:27 -0700 Subject: [PATCH 11/14] Add authors to blog posts and shorten some summaries (#3940) --- .../src/pages/news/article/[slug].vue | 51 +++++++++++++++++-- .../src/public/news/feed/articles.json | 10 ++-- .../a-new-chapter-for-modrinth-servers.md | 1 + .../blog/articles/accelerating-development.md | 1 + .../blog/articles/becoming-sustainable.md | 1 + packages/blog/articles/capital-return.md | 1 + packages/blog/articles/carbon-ads.md | 3 +- .../blog/articles/creator-monetization.md | 3 +- packages/blog/articles/creator-update.md | 1 + .../articles/creator-updates-july-2025.md | 1 + packages/blog/articles/design-refresh.md | 1 + packages/blog/articles/download-adjustment.md | 1 + packages/blog/articles/knossos-v2.1.0.md | 3 +- packages/blog/articles/licensing-guide.md | 1 + packages/blog/articles/modpack-changes.md | 1 + packages/blog/articles/modpacks-alpha.md | 1 + packages/blog/articles/modrinth-app-beta.md | 1 + packages/blog/articles/modrinth-beta.md | 1 + .../blog/articles/modrinth-servers-beta.md | 1 + .../blog/articles/plugins-resource-packs.md | 1 + packages/blog/articles/pride-campaign-2025.md | 3 +- packages/blog/articles/redesign.md | 3 +- .../articles/skins-now-in-modrinth-app.md | 1 + .../articles/two-years-of-modrinth-history.md | 1 + .../blog/articles/two-years-of-modrinth.md | 1 + packages/blog/articles/whats-modrinth.md | 1 + .../windows-borderless-malware-disclosure.md | 1 + packages/blog/compile.ts | 5 +- .../a_new_chapter_for_modrinth_servers.ts | 1 + .../blog/compiled/accelerating_development.ts | 1 + .../blog/compiled/becoming_sustainable.ts | 1 + packages/blog/compiled/capital_return.ts | 1 + packages/blog/compiled/carbon_ads.ts | 4 +- .../blog/compiled/creator_monetization.ts | 3 +- packages/blog/compiled/creator_update.ts | 1 + .../compiled/creator_updates_july_2025.ts | 1 + packages/blog/compiled/design_refresh.ts | 1 + packages/blog/compiled/download_adjustment.ts | 1 + packages/blog/compiled/knossos_v2_1_0.ts | 3 +- packages/blog/compiled/licensing_guide.ts | 1 + packages/blog/compiled/modpack_changes.ts | 1 + packages/blog/compiled/modpacks_alpha.ts | 1 + packages/blog/compiled/modrinth_app_beta.ts | 1 + packages/blog/compiled/modrinth_beta.ts | 1 + .../blog/compiled/modrinth_servers_beta.ts | 1 + packages/blog/compiled/new_site_beta.ts | 1 + .../blog/compiled/plugins_resource_packs.ts | 1 + packages/blog/compiled/pride_campaign_2025.ts | 4 +- packages/blog/compiled/redesign.ts | 4 +- .../compiled/skins_now_in_modrinth_app.ts | 1 + .../blog/compiled/two_years_of_modrinth.ts | 1 + .../compiled/two_years_of_modrinth_history.ts | 1 + packages/blog/compiled/whats_modrinth.ts | 1 + .../windows_borderless_malware_disclosure.ts | 1 + 54 files changed, 117 insertions(+), 23 deletions(-) diff --git a/apps/frontend/src/pages/news/article/[slug].vue b/apps/frontend/src/pages/news/article/[slug].vue index fda42fae..c2e35cdf 100644 --- a/apps/frontend/src/pages/news/article/[slug].vue +++ b/apps/frontend/src/pages/news/article/[slug].vue @@ -1,9 +1,10 @@