Compare commits

...
11 Commits
Author SHA1 Message Date
Michael H. bcf469ef91 fix: generatedstate memory leak & bump ts query 2026-08-29 12:26:46 +02:00
Michael H. 1bee2f83d8 fix: tanstack query memory leak 2026-08-29 02:31:35 +02:00
Michael H. 7823f83d04 fix: tailscale ssh user 2026-08-29 01:30:30 +02:00
Michael H. a6a9139019 chore: set tailscale hostname to magic container hostname 2026-08-29 01:05:40 +02:00
Michael H. b9c3b363e8 chore: frontend debug 2026-08-29 00:48:43 +02:00
Michael H. 0ab9100c46 Revert "fix: i18n nuxt context error"
This reverts commit a1b73d089e.
2026-08-28 16:30:32 +02:00
Michael H. a1b73d089e fix: i18n nuxt context error 2026-08-28 15:30:55 +02:00
Michael H. c2cbf16434 fix: use node cluster 2026-08-28 15:19:37 +02:00
Michael H. ff4c9be910 fix: 404 handling but actually 2026-08-28 15:14:47 +02:00
Truman Gao 98f8bbcad9 fix: 404 always logs error (#7341)
* fix: error page

* fix: nuxt always logs console error on 404

* Revert "fix: error page"

This reverts commit ec15bf3b40.
2026-08-28 13:27:54 +02:00
Prospector a9b774005b fix error pages (#7338) 2026-08-28 01:48:14 +00:00
13 changed files with 115 additions and 39 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
- name: Build frontend
run: pnpm web:build
env:
NITRO_PRESET: node-server
NITRO_PRESET: node_cluster
BUILD_ENV: ${{ steps.meta.outputs.env }}
CF_PAGES_BRANCH: ${{ github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
+1 -1
View File
@@ -21,7 +21,7 @@
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@tanstack/vue-query": "5.90.7",
"@tanstack/vue-query": "5.101.4",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.4.5",
+67 -6
View File
@@ -1,6 +1,8 @@
# syntax=docker/dockerfile:1
FROM node:24-slim
FROM docker.io/tailscale/tailscale:latest AS tailscale
FROM node:24-trixie-slim
LABEL org.opencontainers.image.source=https://github.com/modrinth/code
LABEL org.opencontainers.image.title=frontend
@@ -11,6 +13,67 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends dumb-init \
&& rm -rf /var/lib/apt/lists/*
# Debug
COPY --from=tailscale /usr/local/bin/tailscale /usr/local/bin/tailscaled /usr/local/bin/
COPY --chmod=0755 <<'EOF' /usr/local/bin/entrypoint.sh
#!/bin/sh
set -eu
TAILSCALE_DIR=/tmp/tailscale
TAILSCALE_SOCKET="$TAILSCALE_DIR/tailscaled.sock"
start_tailscale() {
echo "entrypoint: starting tailscale as euid=$(id -u) groups=$(id -G)" >&2
mkdir -p "$TAILSCALE_DIR"
# Userspace networking needs no TUN device or extra capabilities.
# --state=mem: registers an ephemeral node; --statedir gives the SSH host keys
# somewhere writable to live, which mem: on its own does not.
tailscaled \
--tun=userspace-networking \
--state=mem: \
--statedir="$TAILSCALE_DIR" \
--socket="$TAILSCALE_SOCKET" &
waited=0
while [ ! -S "$TAILSCALE_SOCKET" ]; do
if [ "$waited" -ge 50 ]; then
echo "entrypoint: tailscaled socket never appeared" >&2
return 1
fi
waited=$((waited + 1))
sleep 0.1
done
if [ -n "${TAILSCALE_HOSTNAME:-}" ]; then
node_hostname="$TAILSCALE_HOSTNAME"
elif [ -n "${BUNNYNET_MC_PODID:-}" ] && [ -n "${BUNNYNET_MC_REGION:-}" ]; then
node_hostname="frontend-$BUNNYNET_MC_PODID-$BUNNYNET_MC_REGION"
else
node_hostname="frontend-$(hostname)"
fi
tailscale --socket="$TAILSCALE_SOCKET" up \
--authkey="$TAILSCALE_AUTH_KEY" \
--hostname="$node_hostname" \
--accept-dns=false \
--timeout=30s \
--ssh
}
if [ -n "${TAILSCALE_AUTH_KEY:-}" ]; then
start_tailscale || echo "entrypoint: tailscale setup failed, serving without SSH" >&2
fi
# Only drop if we actually started as root; some runtimes pin their own uid.
if [ "$(id -u)" = "0" ]; then
exec setpriv --reuid=node --regid=node --init-groups -- "$@"
fi
exec "$@"
EOF
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
@@ -20,11 +83,9 @@ WORKDIR /app
# Nitro bundles every runtime dependency into .output, so no node_modules is needed.
COPY --chown=node:node .output ./.output
USER node
# Stays root so tailscaled can setgroups/setuid for SSH sessions; the entrypoint
# drops the server itself to the node user.
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/robots.txt').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"
ENTRYPOINT ["dumb-init", "--"]
ENTRYPOINT ["dumb-init", "--", "/usr/local/bin/entrypoint.sh"]
CMD ["node", "/app/.output/server/index.mjs"]
+1 -1
View File
@@ -48,7 +48,7 @@
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/nuxt": "^10.33.0",
"@tanstack/vue-query": "5.90.7",
"@tanstack/vue-query": "5.101.4",
"@types/three": "^0.172.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vue-email/components": "^0.0.21",
+1 -1
View File
@@ -33,7 +33,7 @@ import {
import { useAuth } from './composables/auth'
const auth = await useAuth()
const userPreferences = setupProviders(auth)
const { userPreferences } = setupProviders(auth)
const cosmetics = useCosmetics()
const theme = useTheme()
const { locale, setLocale } = injectI18n()
+14 -1
View File
@@ -1,5 +1,6 @@
import type { ISO3166, Labrinth } from '@modrinth/api-client'
import type { DisplayProjectType } from '@modrinth/utils'
import type { ShallowRef } from 'vue'
import {
apiUrl,
@@ -155,11 +156,23 @@ const generatedState = shallowRef<GeneratedState>(
}) as GeneratedState,
)
/**
* Non-reactive server-side view of the state above.
*
* `generatedState` lives for the lifetime of the isolate, so every per-request `computed()` that
* reads it registers a subscriber link on its dep. SSR never unmounts, so those links are never
* released, and each one pins the whole request graph that created it. The data is frozen and
* `setGameVersions` is client-only, so the server has nothing to react to.
*/
const serverGeneratedState = {
value: generatedState.value,
} as unknown as ShallowRef<GeneratedState>
/**
* Composable for accessing the globally used generated state.
* This includes both fetched data and runtime-defined constants.
*/
export const useGeneratedState = () => generatedState
export const useGeneratedState = () => (import.meta.server ? serverGeneratedState : generatedState)
/**
* Replaces the build-time game versions with a freshly fetched list. Client-only: mutating this
+2 -3
View File
@@ -110,7 +110,6 @@ import {
commonMessages,
defineMessage,
defineMessages,
injectNotificationManager,
IntlFormatted,
LoadingBar,
normalizeChildren,
@@ -131,10 +130,10 @@ import { getSignInRouteObj } from '~/composables/auth.js'
import { setupProviders } from '~/providers/setup.ts'
const auth = await useAuth()
setupProviders(auth)
const { notificationManager } = setupProviders(auth)
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const { addNotification } = notificationManager
const isSwitchingAccount = useIsSwitchingAccount()
const props = defineProps({
-3
View File
@@ -970,13 +970,10 @@ const showTinMismatchBanner = computed(() => {
const PRIDE_COLLECTION_ID = 'M4c3ITvd'
const PRIDE_ARTICLE_SLUGS = ['pride-campaign-2025', 'pride-campaign-2026', 'proud-of-you-2026']
const PRIDE_CACHE_TIME = 1000 * 60 * 60 * 24
const { data: prideCollection } = useQuery({
queryKey: computed(() => ['collection', PRIDE_COLLECTION_ID]),
queryFn: () => client.labrinth.collections.get(PRIDE_COLLECTION_ID),
staleTime: PRIDE_CACHE_TIME,
gcTime: PRIDE_CACHE_TIME,
})
const prideProjectIds = computed(() => new Set(prideCollection.value?.projects ?? []))
+3
View File
@@ -29,6 +29,9 @@ export default defineNuxtPlugin((nuxt) => {
if (import.meta.server) {
nuxt.hooks.hook('app:rendered', () => {
vueQueryState.value = dehydrate(queryClient)
// Hack to prevent memory leak when gcTime is being set on the server side.
queryClient.clear()
})
}
+1 -1
View File
@@ -28,5 +28,5 @@ export function setupProviders(auth: Awaited<ReturnType<typeof useAuth>>) {
setupLoadingStateProvider()
setupUserCountryProvider()
return userPreferences
return { userPreferences, notificationManager }
}
@@ -1,5 +1,8 @@
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', async (error, { event }) => {
const statusCode = (error as { statusCode?: number }).statusCode ?? 500
if (statusCode < 500) return
console.error(`[Context Error] at ${event?.path}:`, error)
})
})
+1 -1
View File
@@ -60,7 +60,7 @@
"@modrinth/assets": "workspace:*",
"@modrinth/blog": "workspace:*",
"@modrinth/utils": "workspace:*",
"@tanstack/vue-query": "5.90.7",
"@tanstack/vue-query": "5.101.4",
"@tresjs/cientos": "^4.3.0",
"@tresjs/core": "^4.3.4",
"@tresjs/post-processing": "^2.4.0",
+20 -20
View File
@@ -102,8 +102,8 @@ importers:
specifier: ^1.1.6
version: 1.1.6
'@tanstack/vue-query':
specifier: 5.90.7
version: 5.90.7(vue@3.5.27(typescript@5.9.3))
specifier: 5.101.4
version: 5.101.4(vue@3.5.27(typescript@5.9.3))
'@tauri-apps/api':
specifier: ^2.5.0
version: 2.10.1
@@ -300,8 +300,8 @@ importers:
specifier: ^10.33.0
version: 10.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)(magicast@0.5.1)(nuxt@3.20.2(@parcel/watcher@2.5.6)(@types/node@24.12.2)(@vue/compiler-sfc@3.5.27)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(lightningcss@1.32.0)(magicast@0.5.1)(optionator@0.9.4)(rolldown@1.0.0-rc.12)(rollup@4.57.1)(sass@1.97.3)(terser@5.46.0)(typescript@5.9.3)(vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.2))(rollup@4.57.1)(vue@3.5.27(typescript@5.9.3))
'@tanstack/vue-query':
specifier: 5.90.7
version: 5.90.7(vue@3.5.27(typescript@5.9.3))
specifier: 5.101.4
version: 5.101.4(vue@3.5.27(typescript@5.9.3))
'@types/three':
specifier: ^0.172.0
version: 0.172.0
@@ -671,8 +671,8 @@ importers:
specifier: workspace:*
version: link:../utils
'@tanstack/vue-query':
specifier: 5.90.7
version: 5.90.7(vue@3.5.27(typescript@5.9.3))
specifier: 5.101.4
version: 5.101.4(vue@3.5.27(typescript@5.9.3))
'@tresjs/cientos':
specifier: ^4.3.0
version: 4.3.1(@tresjs/core@4.3.6(three@0.172.0)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3)))(@types/three@0.172.0)(react@19.2.8)(three@0.172.0)(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
@@ -721,12 +721,12 @@ importers:
es-toolkit:
specifier: ^1.44.0
version: 1.44.0
flatpickr:
specifier: ^4.6.13
version: 4.6.13
fabric:
specifier: ^7.4.0
version: 7.4.0
flatpickr:
specifier: ^4.6.13
version: 4.6.13
floating-vue:
specifier: ^5.2.2
version: 5.2.2(@nuxt/kit@3.21.0(magicast@0.5.1))(vue@3.5.27(typescript@5.9.3))
@@ -4453,11 +4453,11 @@ packages:
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'}
'@tanstack/query-core@5.90.7':
resolution: {integrity: sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==}
'@tanstack/query-core@5.101.4':
resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==}
'@tanstack/vue-query@5.90.7':
resolution: {integrity: sha512-2h0esebc2qVRVDge3gFArhss5+qn/Wb4abXuaEliSy+/xPWQMrEX7Ny0UKkCy1HcMZZjvfTtNRUfpYrx+vpipw==}
'@tanstack/vue-query@5.101.4':
resolution: {integrity: sha512-UYjkUZhnWQIFGNb7SdgjCitAftNyYQfOIVUh6vBUQAG4SQKNMSBKeQERFDubpxLAMpIBJTaOrE8fM4c1kZcIGQ==}
peerDependencies:
'@vue/composition-api': ^1.1.2
vue: ^2.6.0 || ^3.3.0
@@ -10381,8 +10381,8 @@ packages:
vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
vue-component-type-helpers@3.3.10:
resolution: {integrity: sha512-t7IQivQ3oD4D01b7s7a9AWzHAcr3DGBIa/1jZREsFQJcFgSL92gqUkqNiHTYgzTt7QDuJa0I9wCeDwEtZICoBQ==}
vue-component-type-helpers@3.3.11:
resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==}
vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -14128,7 +14128,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3)
vue-component-type-helpers: 3.3.10
vue-component-type-helpers: 3.3.11
'@stripe/stripe-js@7.9.0': {}
@@ -14255,12 +14255,12 @@ snapshots:
dependencies:
remove-accents: 0.5.0
'@tanstack/query-core@5.90.7': {}
'@tanstack/query-core@5.101.4': {}
'@tanstack/vue-query@5.90.7(vue@3.5.27(typescript@5.9.3))':
'@tanstack/vue-query@5.101.4(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@tanstack/match-sorter-utils': 8.19.4
'@tanstack/query-core': 5.90.7
'@tanstack/query-core': 5.101.4
'@vue/devtools-api': 6.6.4
vue: 3.5.27(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.27(typescript@5.9.3))
@@ -21551,7 +21551,7 @@ snapshots:
vue-component-type-helpers@3.2.4: {}
vue-component-type-helpers@3.3.10: {}
vue-component-type-helpers@3.3.11: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies: