mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
Revert "feat: sync individual content installation states on panel" (#7154)
Revert "feat: sync individual content installation states on panel (#6909)"
This reverts commit 9628b4c269.
This commit is contained in:
@@ -173,7 +173,7 @@
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<PanelServerActionButton />
|
||||
<PanelServerActionButton :disabled="!!installError" />
|
||||
<Tooltip
|
||||
theme="dismissable-prompt"
|
||||
:triggers="[]"
|
||||
@@ -217,6 +217,7 @@
|
||||
size="xl"
|
||||
label="More server options"
|
||||
:options="serverMenuOptions"
|
||||
:disabled="!!installError"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
</TeleportOverflowMenu>
|
||||
@@ -243,6 +244,92 @@
|
||||
:class="containedLayout ? 'flex min-h-0 flex-col overflow-hidden' : 'h-full'"
|
||||
:style="{ '--si': 2 }"
|
||||
>
|
||||
<div
|
||||
v-if="installError"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<div class="flex flex-col gap-2 leading-[150%]">
|
||||
<div class="flex items-center gap-3">
|
||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||
class="font-normal"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||
"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Hosting supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||
not be compatible with the loader.
|
||||
</li>
|
||||
<li>
|
||||
Your server may need to be reinstalled with a valid mod loader and version.
|
||||
You can change the loader by clicking the "Change Loader" button.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<Button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||
An internal error occurred while installing your server. Don't fret — try
|
||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||
support with your server's debug information.
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||
>
|
||||
An error occurred while installing your server because Modrinth Hosting does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||
your server with a different version or loader, and if the problem persists,
|
||||
please contact Modrinth Support with your server's debug information.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<Button v-if="errorLog" @click="openInstallLog"
|
||||
><FileIcon />Open Installation Log</Button
|
||||
>
|
||||
<Button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="red"
|
||||
class="whitespace-pre"
|
||||
@click="openServerSettingsModal('installation')"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="serverData.is_medal" class="mb-4">
|
||||
<MedalServerCountdown
|
||||
:server-id="serverId"
|
||||
@@ -272,7 +359,9 @@
|
||||
|
||||
<ServerPanelAdmonitions
|
||||
class="mb-4 shrink-0"
|
||||
@installation-retry="handleInstallationRetry"
|
||||
:sync-progress="syncProgress"
|
||||
:content-error="contentError"
|
||||
@content-retry="handleContentRetry"
|
||||
/>
|
||||
<slot :on-reinstall="onReinstall" :on-reinstall-failed="onReinstallFailed" />
|
||||
</div>
|
||||
@@ -305,11 +394,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
|
||||
import { getNodeWebSocketUrl, ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
|
||||
import {
|
||||
BoxesIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DatabaseBackupIcon,
|
||||
FileIcon,
|
||||
FolderOpenIcon,
|
||||
IssuesIcon,
|
||||
LayoutTemplateIcon,
|
||||
@@ -317,6 +408,7 @@ import {
|
||||
LoaderCircleIcon,
|
||||
LockIcon,
|
||||
MoreVerticalIcon,
|
||||
RightArrowIcon,
|
||||
ServerIcon as ServerAssetIcon,
|
||||
SettingsIcon,
|
||||
TimerIcon,
|
||||
@@ -326,14 +418,14 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import { IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
|
||||
import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||
import PageHeader from '#ui/components/base/page-header/index.vue'
|
||||
@@ -359,10 +451,6 @@ import {
|
||||
} from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
|
||||
import type {
|
||||
ServerInstallationKey,
|
||||
ServerInstallationState,
|
||||
} from '#ui/composables/server-installation-tracker'
|
||||
import { useServerManageCoreRuntime } from '#ui/composables/server-manage-core-runtime'
|
||||
import { useServerPanelSync } from '#ui/composables/server-panel-sync'
|
||||
import type { LogLine } from '#ui/layouts/shared/console'
|
||||
@@ -375,6 +463,11 @@ import {
|
||||
import type { ServerStats } from '#ui/providers/server-context'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
import {
|
||||
pendingServerContentInstallsEvent,
|
||||
readPendingServerContentInstalls,
|
||||
writePendingServerContentInstalls,
|
||||
} from '#ui/utils/server-content-installing'
|
||||
|
||||
import ServerOnboardingPanelPage from './[id]/onboarding.vue'
|
||||
|
||||
@@ -475,6 +568,12 @@ const debug = useDebugLogger('ServerManage')
|
||||
const isReconnecting = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const isMounted = ref(true)
|
||||
const copied = ref(false)
|
||||
const installError = ref<Error | null>(null)
|
||||
const errorTitle = ref('Error')
|
||||
const errorMessage = ref('An unexpected error occurred.')
|
||||
const errorLog = ref('')
|
||||
const errorLogFile = ref('')
|
||||
const isOnboarding = computed(() => serverData.value?.flows?.intro)
|
||||
|
||||
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
|
||||
@@ -528,13 +627,6 @@ const worldId = computed(() => {
|
||||
return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
|
||||
})
|
||||
|
||||
const { data: serverContent } = useQuery({
|
||||
queryKey: ['content', 'list', 'v1', props.serverId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(props.serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
})
|
||||
|
||||
const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
|
||||
computed(() => props.serverId),
|
||||
worldId,
|
||||
@@ -551,25 +643,101 @@ const { image: serverImage } = useServerImage(
|
||||
)
|
||||
const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null))
|
||||
|
||||
const syncProgress = ref<Archon.Websocket.v0.SyncContentProgress | null>(null)
|
||||
const contentError = ref<Archon.Websocket.v0.SyncContentError | null>(null)
|
||||
const syncProgressActive = ref(false)
|
||||
const hasPendingServerContentInstalls = ref(false)
|
||||
const hasSeenPendingServerContentSync = ref(false)
|
||||
const isAwaitingPostInstallRefresh = ref(false)
|
||||
const { start: startSyncHide, stop: cancelSyncHide } = useTimeoutFn(
|
||||
() => (syncProgressActive.value = false),
|
||||
1000,
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
watch(syncProgress, (progress) => {
|
||||
if (progress != null) {
|
||||
cancelSyncHide()
|
||||
syncProgressActive.value = true
|
||||
if (progress.phase !== 'Analyzing' && hasPendingServerContentInstalls.value) {
|
||||
hasSeenPendingServerContentSync.value = true
|
||||
}
|
||||
} else if (syncProgressActive.value) {
|
||||
startSyncHide()
|
||||
if (hasSeenPendingServerContentSync.value) {
|
||||
writePendingServerContentInstalls(props.serverId, worldId.value, [])
|
||||
hasSeenPendingServerContentSync.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(contentError, (error) => {
|
||||
if (!error || !hasPendingServerContentInstalls.value) return
|
||||
writePendingServerContentInstalls(props.serverId, worldId.value, [])
|
||||
hasSeenPendingServerContentSync.value = false
|
||||
})
|
||||
|
||||
const isSyncingContent = computed(
|
||||
() =>
|
||||
syncProgressActive.value ||
|
||||
isAwaitingPostInstallRefresh.value ||
|
||||
hasPendingServerContentInstalls.value,
|
||||
)
|
||||
|
||||
function syncPendingServerContentInstalls() {
|
||||
hasPendingServerContentInstalls.value =
|
||||
readPendingServerContentInstalls(props.serverId, worldId.value).length > 0
|
||||
}
|
||||
|
||||
function handlePendingServerContentInstallsChanged(event: Event) {
|
||||
const detail = (event as CustomEvent<{ serverId?: string | null; worldId?: string | null }>)
|
||||
.detail
|
||||
if (detail?.serverId !== props.serverId || detail?.worldId !== worldId.value) return
|
||||
syncPendingServerContentInstalls()
|
||||
}
|
||||
|
||||
watch(worldId, syncPendingServerContentInstalls, { immediate: true })
|
||||
|
||||
let hasSeenInstallProgress = false
|
||||
|
||||
const onStateEvent = (data: Archon.Websocket.v0.WSStateEvent) => {
|
||||
debug('[root.vue] handleState received:', {
|
||||
power_variant: data.power_variant,
|
||||
progress: data.progress,
|
||||
serverStatus: serverData.value?.status,
|
||||
})
|
||||
hasReceivedWsData.value = true
|
||||
syncProgress.value = data.progress
|
||||
contentError.value = data.content_error
|
||||
|
||||
if (serverData.value) {
|
||||
if (data.progress != null && serverData.value.status !== 'installing') {
|
||||
debug('[root.vue] handleState: progress != null, setting status to installing')
|
||||
hasSeenInstallProgress = true
|
||||
updateServerData({ status: 'installing' })
|
||||
} else if (data.progress != null) {
|
||||
hasSeenInstallProgress = true
|
||||
} else if (
|
||||
data.progress == null &&
|
||||
data.content_error == null &&
|
||||
serverData.value.status === 'installing' &&
|
||||
hasSeenInstallProgress
|
||||
) {
|
||||
debug('[root.vue] handleState: progress null + was installing, applying optimistic update')
|
||||
hasSeenInstallProgress = false
|
||||
applyOptimisticCompletion()
|
||||
invalidateAfterInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
beginInstallation,
|
||||
cancelUpload,
|
||||
cancelOptimisticInstallation,
|
||||
cleanupCoreRuntime,
|
||||
connectSocket,
|
||||
cpuData,
|
||||
dismissInstallation,
|
||||
fsOps,
|
||||
fsQueuedOps,
|
||||
installation,
|
||||
isConnected,
|
||||
ramData,
|
||||
serverPowerState,
|
||||
@@ -581,7 +749,7 @@ const {
|
||||
worldId,
|
||||
server: serverData,
|
||||
serverFull,
|
||||
content: serverContent,
|
||||
isSyncingContent,
|
||||
extraBusyReasons: backupsBusy,
|
||||
setDisconnectedOnAuthIncorrect: false,
|
||||
syncUptimeFromState: true,
|
||||
@@ -912,7 +1080,7 @@ function loadTallyScript() {
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
async function handleInstallationRetry() {
|
||||
async function handleContentRetry() {
|
||||
if (!worldId.value) return
|
||||
if (!canSetup.value) {
|
||||
addNotification({
|
||||
@@ -921,16 +1089,9 @@ async function handleInstallationRetry() {
|
||||
})
|
||||
return
|
||||
}
|
||||
const failedInstallationId =
|
||||
installation.value?.status === 'failed' ? installation.value.id : null
|
||||
if (failedInstallationId) dismissInstallation(failedInstallationId)
|
||||
beginInstallation({ type: 'unknown' })
|
||||
updateServerData({ status: 'installing' })
|
||||
try {
|
||||
await client.archon.content_v1.repair(props.serverId, worldId.value)
|
||||
} catch (err) {
|
||||
cancelOptimisticInstallation()
|
||||
updateServerData({ status: 'available' })
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to retry installation',
|
||||
@@ -972,56 +1133,54 @@ const handleNewMod = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
type InstallationServerSnapshot = Pick<
|
||||
Archon.Servers.v0.Server,
|
||||
'loader' | 'loader_version' | 'mc_version'
|
||||
>
|
||||
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
||||
debug('[root.vue] handleInstallationResult received:', data)
|
||||
switch (data.result) {
|
||||
case 'ok': {
|
||||
debug('[root.vue] handleInstallationResult: ok received')
|
||||
if (!serverData.value) break
|
||||
|
||||
let installationServerSnapshot: InstallationServerSnapshot | null = null
|
||||
applyOptimisticCompletion()
|
||||
installError.value = null
|
||||
invalidateAfterInstall()
|
||||
|
||||
function applyInstallationTarget(current: ServerInstallationState) {
|
||||
if (!serverData.value) return
|
||||
break
|
||||
}
|
||||
case 'err': {
|
||||
console.log('failed to install')
|
||||
console.log(data)
|
||||
errorTitle.value = 'Installation error'
|
||||
errorMessage.value = data.reason ?? 'Unknown error'
|
||||
installError.value = new Error(data.reason ?? 'Unknown error')
|
||||
|
||||
if (!installationServerSnapshot) {
|
||||
installationServerSnapshot = {
|
||||
loader: serverData.value.loader,
|
||||
loader_version: serverData.value.loader_version,
|
||||
mc_version: serverData.value.mc_version,
|
||||
try {
|
||||
let files = await client.kyros.files_v0.listDirectory('/', 1, 100)
|
||||
if (files && files.total > 1) {
|
||||
for (let i = 2; i <= files.total; i++) {
|
||||
const nextFiles = await client.kyros.files_v0.listDirectory('/', i, 100)
|
||||
if (nextFiles?.items?.length === 0) break
|
||||
if (nextFiles) files = nextFiles
|
||||
}
|
||||
}
|
||||
const fileName = files?.items?.find((file) =>
|
||||
file.name.startsWith('modrinth-installation'),
|
||||
)?.name
|
||||
errorLogFile.value = fileName ?? ''
|
||||
if (fileName) {
|
||||
const content = await client.kyros.files_v0.downloadFile(fileName)
|
||||
errorLog.value = await content.text()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch installation log:', err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const patch: Partial<Archon.Servers.v0.Server> = { status: 'installing' }
|
||||
if (current.key.type === 'platform') {
|
||||
patch.loader = formatLoaderLabel(current.key.platform) as Archon.Servers.v0.Loader
|
||||
patch.loader_version = current.key.platform === 'vanilla' ? null : current.key.platform_version
|
||||
patch.mc_version = current.key.game_version
|
||||
}
|
||||
|
||||
if (
|
||||
serverData.value.status === patch.status &&
|
||||
(current.key.type !== 'platform' ||
|
||||
(serverData.value.loader === patch.loader &&
|
||||
serverData.value.loader_version === patch.loader_version &&
|
||||
serverData.value.mc_version === patch.mc_version))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
void queryClient.cancelQueries({
|
||||
queryKey: ['servers', 'detail', props.serverId],
|
||||
exact: true,
|
||||
})
|
||||
updateServerData(patch)
|
||||
}
|
||||
|
||||
function restoreInstallationServerSnapshot() {
|
||||
const snapshot = installationServerSnapshot
|
||||
updateServerData({
|
||||
...(snapshot ?? {}),
|
||||
status: 'available',
|
||||
})
|
||||
installationServerSnapshot = null
|
||||
}
|
||||
const newLoader = ref<string | null>(null)
|
||||
const newLoaderVersion = ref<string | null>(null)
|
||||
const newMCVersion = ref<string | null>(null)
|
||||
|
||||
const onReinstall = async (
|
||||
potentialArgs: { loader?: string; lVersion?: string; mVersion?: string } | undefined,
|
||||
@@ -1035,63 +1194,70 @@ const onReinstall = async (
|
||||
|
||||
if (!serverData.value) return
|
||||
|
||||
if (
|
||||
!installation.value ||
|
||||
installation.value.status === 'complete' ||
|
||||
installation.value.status === 'failed'
|
||||
) {
|
||||
if (potentialArgs?.loader && potentialArgs.mVersion) {
|
||||
beginInstallation({
|
||||
type: 'platform',
|
||||
platform: potentialArgs.loader as Extract<
|
||||
Archon.Websocket.v0.InstallProgressKey,
|
||||
{ type: 'platform' }
|
||||
>['platform'],
|
||||
platform_version: potentialArgs.lVersion ?? '',
|
||||
game_version: potentialArgs.mVersion,
|
||||
})
|
||||
} else {
|
||||
beginInstallation({ type: 'unknown' })
|
||||
}
|
||||
debug('[root.vue] onReinstall: setting serverData.status to installing')
|
||||
hasSeenInstallProgress = false
|
||||
updateServerData({ status: 'installing' })
|
||||
|
||||
if (potentialArgs?.loader) {
|
||||
newLoader.value = potentialArgs.loader
|
||||
}
|
||||
if (potentialArgs?.lVersion) {
|
||||
newLoaderVersion.value = potentialArgs.lVersion
|
||||
}
|
||||
if (potentialArgs?.mVersion) {
|
||||
newMCVersion.value = potentialArgs.mVersion
|
||||
}
|
||||
|
||||
installError.value = null
|
||||
errorTitle.value = 'Error'
|
||||
errorMessage.value = 'An unexpected error occurred.'
|
||||
|
||||
modrinthServersConsole.clear()
|
||||
|
||||
debug('[root.vue] onReinstall: triggering immediate invalidation')
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}
|
||||
|
||||
const onReinstallFailed = () => {
|
||||
debug('[root.vue] onReinstallFailed: reverting status to available')
|
||||
cancelOptimisticInstallation()
|
||||
restoreInstallationServerSnapshot()
|
||||
updateServerData({ status: 'available' })
|
||||
newLoader.value = null
|
||||
newLoaderVersion.value = null
|
||||
newMCVersion.value = null
|
||||
}
|
||||
|
||||
function applyInstallationCompletion(key: ServerInstallationKey) {
|
||||
const platformKey = key?.type === 'platform' ? key : null
|
||||
function applyOptimisticCompletion() {
|
||||
const patch: Partial<Archon.Servers.v0.Server> = { status: 'available' }
|
||||
if (platformKey) {
|
||||
patch.loader = formatLoaderLabel(platformKey.platform) as Archon.Servers.v0.Loader
|
||||
patch.loader_version = platformKey.platform === 'vanilla' ? null : platformKey.platform_version
|
||||
patch.mc_version = platformKey.game_version
|
||||
}
|
||||
if (newLoader.value) patch.loader = formatLoaderLabel(newLoader.value) as Archon.Servers.v0.Loader
|
||||
if (newLoaderVersion.value) patch.loader_version = newLoaderVersion.value
|
||||
if (newMCVersion.value) patch.mc_version = newMCVersion.value
|
||||
|
||||
debug('[root.vue] applyInstallationCompletion: patch:', patch)
|
||||
debug('[root.vue] applyOptimisticCompletion: patch:', patch)
|
||||
updateServerData(patch)
|
||||
|
||||
const addonsQueries = queryClient.getQueriesData<Archon.Content.v1.Addons>({
|
||||
queryKey: ['content', 'list', 'v1', props.serverId],
|
||||
})
|
||||
for (const [key, data] of addonsQueries) {
|
||||
if (!data || !platformKey) continue
|
||||
queryClient.setQueryData(key, {
|
||||
...data,
|
||||
modloader: platformKey.platform === 'neoforge' ? 'neo_forge' : platformKey.platform,
|
||||
modloader_version: platformKey.platform === 'vanilla' ? null : platformKey.platform_version,
|
||||
game_version: platformKey.game_version,
|
||||
})
|
||||
if (!data) continue
|
||||
const addonsPatch: Record<string, string> = {}
|
||||
if (newLoader.value) addonsPatch.modloader = newLoader.value
|
||||
if (newLoaderVersion.value) addonsPatch.modloader_version = newLoaderVersion.value
|
||||
if (newMCVersion.value) addonsPatch.game_version = newMCVersion.value
|
||||
if (Object.keys(addonsPatch).length > 0) {
|
||||
queryClient.setQueryData(key, { ...data, ...addonsPatch })
|
||||
}
|
||||
}
|
||||
|
||||
newLoader.value = null
|
||||
newLoaderVersion.value = null
|
||||
newMCVersion.value = null
|
||||
}
|
||||
|
||||
async function invalidateAfterInstall() {
|
||||
debug('[root.vue] invalidateAfterInstall: scheduling 2s delayed invalidation')
|
||||
isAwaitingPostInstallRefresh.value = true
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -1103,48 +1269,12 @@ async function invalidateAfterInstall() {
|
||||
])
|
||||
} catch (err: unknown) {
|
||||
console.error('Error refreshing data after installation:', err)
|
||||
} finally {
|
||||
isAwaitingPostInstallRefresh.value = false
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
let handledFailedInstallationId: string | null = null
|
||||
watch(
|
||||
installation,
|
||||
(current, previous) => {
|
||||
if (!current) {
|
||||
if (
|
||||
isMounted.value &&
|
||||
previous?.source === 'optimistic' &&
|
||||
previous.status === 'pending' &&
|
||||
serverData.value?.status === 'installing'
|
||||
) {
|
||||
restoreInstallationServerSnapshot()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (current.status === 'pending' || current.status === 'installing') {
|
||||
handledFailedInstallationId = null
|
||||
applyInstallationTarget(current)
|
||||
return
|
||||
}
|
||||
|
||||
if (current.status === 'failed') {
|
||||
if (handledFailedInstallationId === current.id) return
|
||||
handledFailedInstallationId = current.id
|
||||
if (current.source === 'server') return
|
||||
onReinstallFailed()
|
||||
void invalidateAfterInstall()
|
||||
return
|
||||
}
|
||||
|
||||
applyInstallationCompletion(current.key)
|
||||
installationServerSnapshot = null
|
||||
dismissInstallation(current.id)
|
||||
void invalidateAfterInstall()
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
|
||||
const nodeAccessible = ref(true)
|
||||
|
||||
const nodeUnavailableDetails = computed(() => [
|
||||
@@ -1165,7 +1295,7 @@ const nodeUnavailableDetails = computed(() => [
|
||||
label: 'Error message',
|
||||
value: nodeAccessible.value
|
||||
? (serverError.value?.message ?? 'Unknown')
|
||||
: 'Unable to establish the node WebSocket connection.',
|
||||
: 'Unable to reach node. Ping test failed.',
|
||||
type: 'block' as const,
|
||||
},
|
||||
])
|
||||
@@ -1240,6 +1370,21 @@ const nodeUnavailableAction = computed(() => ({
|
||||
disabled: false,
|
||||
}))
|
||||
|
||||
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)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const openInstallLog = () => {
|
||||
const url = `/hosting/manage/${props.serverId}/files?editing=${encodeURIComponent(errorLogFile.value)}`
|
||||
window.history.pushState({}, '', url)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
function openServerSettingsModal(tabId?: ServerSettingsTabId) {
|
||||
if (!props.serverId) return
|
||||
serverSettingsModal.value?.show({ serverId: props.serverId, tabId })
|
||||
@@ -1283,6 +1428,48 @@ function safeStringify(obj: unknown, indent = ' '): string {
|
||||
)
|
||||
}
|
||||
|
||||
async function testNodeReachability(): Promise<boolean> {
|
||||
const nodeInstance = serverData.value?.node?.instance
|
||||
if (!nodeInstance) return false
|
||||
|
||||
try {
|
||||
const auth = await client.archon.servers_v0.getWebSocketAuth(props.serverId)
|
||||
const authUrl = getNodeWebSocketUrl(auth.url)
|
||||
const protocol = authUrl.toLowerCase().startsWith('ws://') ? 'ws' : 'wss'
|
||||
const wsUrl = getNodeWebSocketUrl(`${nodeInstance}/pingtest`).replace(
|
||||
/^wss?:\/\//i,
|
||||
`${protocol}://`,
|
||||
)
|
||||
|
||||
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 ${nodeInstance}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function initializeServer() {
|
||||
if (serverData.value?.status === 'suspended') {
|
||||
isLoading.value = false
|
||||
@@ -1294,18 +1481,31 @@ function initializeServer() {
|
||||
return
|
||||
}
|
||||
|
||||
testNodeReachability()
|
||||
.then((result) => {
|
||||
nodeAccessible.value = result
|
||||
if (!nodeAccessible.value) {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error testing node reachability:', err)
|
||||
nodeAccessible.value = false
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
if (serverError.value) {
|
||||
isLoading.value = false
|
||||
} else {
|
||||
void connectSocket(props.serverId, {
|
||||
extraSubscriptions: (targetServerId) => [
|
||||
client.archon.sockets.on(targetServerId, 'installation-result', handleInstallationResult),
|
||||
client.archon.sockets.on(targetServerId, 'backup-progress', handleBackupProgress),
|
||||
client.archon.sockets.on(targetServerId, 'filesystem-ops', handleFilesystemOps),
|
||||
client.archon.sockets.on(targetServerId, 'new-mod', handleNewMod),
|
||||
],
|
||||
})
|
||||
.then((connected) => {
|
||||
nodeAccessible.value = connected
|
||||
if (connected && cachedWsState?.consoleLines?.length) {
|
||||
modrinthServersConsole.clear()
|
||||
modrinthServersConsole.addLines(cachedWsState.consoleLines)
|
||||
@@ -1343,6 +1543,11 @@ const cleanup = () => {
|
||||
|
||||
onMounted(() => {
|
||||
isMounted.value = true
|
||||
syncPendingServerContentInstalls()
|
||||
window.addEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
|
||||
if (serverData.value) {
|
||||
initializeServer()
|
||||
@@ -1384,6 +1589,10 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user