Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bda18ac
feat: world switching page implementation
IMB11 Apr 27, 2026
6cb7a8e
fix: lint + i18n
IMB11 Apr 27, 2026
138e855
feat: world card alignment
IMB11 Apr 27, 2026
1a98db6
feat: reshuffle layout for worlds
IMB11 Apr 27, 2026
d89792c
chore: clean up wrapped layout folder structure
IMB11 Apr 27, 2026
59b6930
fix: lint
IMB11 Apr 27, 2026
f6eb288
feat: fix btn sizing
IMB11 Apr 30, 2026
d8716a6
fix: worlds layout
IMB11 Apr 30, 2026
275eca5
fix: modpack linked text alignment
IMB11 Apr 30, 2026
9680294
chore: rename worlds -> instances
IMB11 May 18, 2026
db79630
qa: pass
IMB11 May 18, 2026
1a967cc
fix: lint
IMB11 May 18, 2026
1c33a66
fix: header issues
IMB11 May 18, 2026
3b40e89
feat: PageHeader migration start
IMB11 May 18, 2026
5f2ca85
feat: header migration pt 2
IMB11 May 18, 2026
e58e205
fix: header power state
IMB11 May 18, 2026
e7c8810
fix: ssr
IMB11 May 18, 2026
fcecd5b
fix: ssr for instances subpages with middleware
IMB11 May 18, 2026
8e748d3
feat: migrate all headers to use PageHeader shared component.
IMB11 May 18, 2026
bd9003e
feat: qa + app routing bugs
IMB11 May 23, 2026
880164a
feat: qa
IMB11 May 23, 2026
0cb60d7
fix: project middleware startLoading
IMB11 May 23, 2026
e02986f
feat: server settings split up + copy changes across panel warning mo…
IMB11 May 23, 2026
97068a8
fix: lint
IMB11 May 23, 2026
59b13a2
fix: loss of isNuxt
IMB11 Jun 5, 2026
d38fa98
feat: switch world route impl
IMB11 Jun 9, 2026
86cfe70
feat: implement world creation flow routes
IMB11 Jun 10, 2026
08c459d
fix: qa + lint + i18n
IMB11 Jun 10, 2026
a1a273a
feat: impl world scoped power endpoint
IMB11 Jun 11, 2026
8fe1c42
feat: impl delete world + move onboarding reset to server scope
IMB11 Jun 12, 2026
07ec60a
fix: reset to onboarding flow not waiting
IMB11 Jun 13, 2026
1a358c4
feat: improve onboarding flow wait logic
IMB11 Jun 13, 2026
dd614a8
fix: browse page not respecting instance source via wid param
IMB11 Jun 13, 2026
bf652f3
fix: better handling of datapacks in frontend & desync issues between…
IMB11 Jun 13, 2026
1f80bdf
fix: lint
IMB11 Jun 13, 2026
4af70fa
fix: missing serverpaneladmonitions + csp app
IMB11 Jun 16, 2026
8f14d64
fix: modal jumpiness
IMB11 Jun 16, 2026
953f372
fix: power
IMB11 Jun 16, 2026
a53cf52
fix: info admon breaking
IMB11 Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 76 additions & 4 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const ROUTE_SUSPENSE_TIMEOUT_MS = 60_000
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
Expand All @@ -154,6 +155,22 @@ const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const keepAliveRouteComponents = computed(() => [
...new Set(
router
.getRoutes()
.map((route) => route.meta.keepAliveComponent)
.filter((name) => typeof name === 'string'),
),
])

function getRouteViewKey(viewRoute) {
const keepAliveKey = viewRoute.meta.keepAliveKey
if (typeof keepAliveKey === 'function') return keepAliveKey(viewRoute)
if (typeof keepAliveKey === 'string') return keepAliveKey
return undefined
}

const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
Expand Down Expand Up @@ -494,6 +511,11 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({
},
})

router.beforeEach(async (to) => {
const redirect = await resolveLegacyServerInstanceTabRedirect(to)
if (redirect) return redirect
})

router.beforeEach(() => {
suspensePending = false
if (routerToken) loading.end(routerToken)
Expand Down Expand Up @@ -525,6 +547,50 @@ function onSuspensePending() {
suspenseToken = loading.begin()
}

async function resolveLegacyServerInstanceTabRedirect(to) {
if (!['ServerManageContent', 'ServerManageFiles', 'ServerManageBackups'].includes(to.name)) {
return null
}

const serverId = getRouteParam(to.params.id)
if (!serverId) return null

const tabPath =
to.name === 'ServerManageFiles' ? '/files' : to.name === 'ServerManageBackups' ? '/backups' : ''
const instancesPath = `/hosting/manage/${encodeURIComponent(serverId)}/instances`

try {
const serverFull = await tauriApiClient.archon.servers_v1.get(serverId)
const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0]
if (world) {
return {
path: `${instancesPath}/${encodeURIComponent(world.id)}${tabPath}`,
query: to.query,
hash: to.hash,
replace: true,
}
}
} catch {
return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}

return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}

function getRouteParam(param) {
return Array.isArray(param) ? param[0] : param
}

function onSuspenseResolve() {
if (suspenseToken) {
loading.end(suspenseToken)
Expand Down Expand Up @@ -1607,11 +1673,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
<RouterView v-slot="{ Component }">
<RouterView v-slot="{ Component, route: viewRoute }">
<template v-if="Component">
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
<component :is="Component"></component>
</Suspense>
<KeepAlive :include="keepAliveRouteComponents" :max="3">
<Suspense
:timeout="ROUTE_SUSPENSE_TIMEOUT_MS"
@pending="onSuspensePending"
@resolve="onSuspenseResolve"
>
<component :is="Component" :key="getRouteViewKey(viewRoute)"></component>
</Suspense>
</KeepAlive>
</template>
</RouterView>
</div>
Expand Down
51 changes: 21 additions & 30 deletions apps/app-frontend/src/composables/browse/use-app-server-browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
import type { CardAction } from '@modrinth/ui'
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { ComputedRef, Ref } from 'vue'
import { onUnmounted, ref, shallowRef } from 'vue'
import type { Router } from 'vue-router'

import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, list as listInstances } from '@/helpers/profile.js'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
import { add_server_to_profile } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'

interface BrowseServerInstance {
Expand Down Expand Up @@ -65,14 +70,13 @@ const messages = defineMessages({

export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
const runningServerProjects = ref<Record<string, string>>({})
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingCacheActive = true
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null

async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
Expand Down Expand Up @@ -145,37 +149,26 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
const cachedStatus = getFreshCachedServerStatus(queryClient, address)
if (cachedStatus) {
nextPings[hit.project_id] = cachedStatus.ping
}
}
serverPings.value = nextPings

await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
if (getFreshCachedServerStatus(queryClient, address)) return

let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((error) => {
console.error(`Failed to ping server ${address}:`, error)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
try {
const status = await fetchCachedServerStatus(queryClient, address)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
} catch (error) {
console.error(`Failed to ping server ${address}:`, error)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: undefined }
}

const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
Expand Down Expand Up @@ -307,10 +300,8 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
.catch(options.handleError)

onUnmounted(() => {
serverPingCacheActive = false
serverPingsActive = false
unlistenProcesses?.()
serverPingCache.clear()
pendingServerPings.clear()
})

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { QueryClient } from '@tanstack/vue-query'

import {
get_server_status,
normalizeServerAddress,
type ProtocolVersion,
type ServerStatus,
} from '@/helpers/worlds'

export const SERVER_STATUS_CACHE_MS = 10 * 60 * 1000

function getProtocolVersionKey(protocolVersion: ProtocolVersion | null) {
if (!protocolVersion) return 'default'
return `${protocolVersion.version}:${protocolVersion.legacy ? 'legacy' : 'modern'}`
}

export function getServerStatusQueryKey(
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return [
'minecraft-server-status',
normalizeServerAddress(address) || address.trim().toLowerCase(),
getProtocolVersionKey(protocolVersion),
] as const
}

export function getFreshCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
const queryKey = getServerStatusQueryKey(address, protocolVersion)
const updatedAt = queryClient.getQueryState(queryKey)?.dataUpdatedAt ?? 0
if (!updatedAt || Date.now() - updatedAt >= SERVER_STATUS_CACHE_MS) return undefined
return queryClient.getQueryData<ServerStatus>(queryKey)
}

export async function fetchCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return await queryClient.fetchQuery({
queryKey: getServerStatusQueryKey(address, protocolVersion),
queryFn: () => get_server_status(address, protocolVersion),
staleTime: SERVER_STATUS_CACHE_MS,
gcTime: SERVER_STATUS_CACHE_MS,
})
}
6 changes: 6 additions & 0 deletions apps/app-frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.browse.server.world-fallback-name": {
"message": "Instance"
},
"app.content-install.no-compatible-versions": {
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
},
Expand Down Expand Up @@ -326,6 +329,9 @@
"app.project.install-context.install-content-to-instance": {
"message": "Install content to instance"
},
"app.project.install-context.world-fallback-name": {
"message": "Instance"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
Expand Down
Loading
Loading