<template>
	<div
		v-if="filteredNotices.length > 0"
		class="relative mx-auto mb-4 flex w-full min-w-0 flex-col gap-3 px-6"
		:class="{
			'max-w-[1280px]': constrainWidth,
		}"
	>
		<ServerNotice
			v-for="notice in filteredNotices"
			:key="`notice-${notice.id}`"
			:level="notice.level"
			:message="notice.message"
			:dismissable="notice.dismissable"
			:title="notice.title"
			class="w-full"
			@dismiss="() => dismissNotice(notice.id)"
		/>
	</div>
	<div
		v-if="serverData && serverData.node === null && serverData.status !== 'suspended'"
		class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
	>
		<ErrorInformationCard
			title="We're getting your server ready"
			description="Your server's hardware is being prepared and will be available shortly!"
			:icon="TransferIcon"
			icon-color="blue"
			:action="generalErrorAction"
		/>
	</div>
	<div
		v-else-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
		class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
	>
		<ErrorInformationCard
			title="Server upgrading"
			description="Your server's hardware is currently being upgraded and will be back online shortly!"
			:icon="TransferIcon"
			icon-color="blue"
			:action="generalErrorAction"
		/>
	</div>
	<div
		v-else-if="serverData?.status === 'suspended'"
		class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
	>
		<ErrorInformationCard
			title="Server suspended"
			:description="suspendedDescription"
			:icon="LockIcon"
			icon-color="orange"
			:action="suspendedAction"
		/>
	</div>
	<div
		v-else-if="serverError?.statusCode === 403 || serverError?.statusCode === 404"
		class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
	>
		<ErrorInformationCard
			title="An error occured."
			description="Please contact Modrinth Support."
			:icon="TransferIcon"
			icon-color="orange"
			:error-details="generalErrorDetails"
			:action="generalErrorAction"
		/>
	</div>
	<div
		v-else-if="serverError || !nodeAccessible"
		class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
	>
		<ErrorInformationCard
			title="Server Node Unavailable"
			:icon="TriangleAlertIcon"
			icon-color="red"
			:action="nodeUnavailableAction"
			:error-details="nodeUnavailableDetails"
		>
			<template #description>
				<div class="text-md space-y-4">
					<p class="leading-[170%] text-secondary">
						Your server's node, where your Modrinth Server is physically hosted, is not accessible
						at the moment. We are working to resolve the issue as quickly as possible.
					</p>
					<p class="leading-[170%] text-secondary">
						Your data is safe and will not be lost, and your server will be back online as soon as
						the issue is resolved.
					</p>
					<p class="leading-[170%] text-secondary">
						If reloading does not work initially, please contact Modrinth Support via the chat
						bubble in the bottom right corner and we'll be happy to help.
					</p>
				</div>
			</template>
		</ErrorInformationCard>
	</div>
	<!-- SERVER START -->
	<div
		v-else-if="serverData"
		data-pyro-server-manager-root
		class="relative mx-auto box-border flex w-full min-w-0 flex-col gap-4 px-6 transition-all duration-300"
		:style="{
			'--server-bg-image': serverImage
				? `url(${serverImage})`
				: `linear-gradient(180deg, rgba(153,153,153,1) 0%, rgba(87,87,87,1) 100%)`,
		}"
		:class="[
			'server-panel-' + revealState,
			containedLayout
				? 'h-full min-h-0 overflow-hidden pb-6'
				: constrainWidth
					? 'min-h-[100svh] max-w-[1280px] pb-16'
					: 'min-h-[calc(100svh-100px)] pb-6',
		]"
	>
		<template v-if="revealState !== 'pending' || isOnboarding">
			<div
				v-if="!isOnboarding"
				class="w-full flex flex-col gap-4"
				:class="['server-stagger-item', containedLayout ? 'shrink-0' : '', { 'mt-4': isNuxt }]"
				:style="{ '--si': 0 }"
			>
				<PageHeader :title="serverData?.name || 'Server'">
					<template #leading>
						<ServerIcon
							:image="serverHeaderImage"
							:class="isNuxt ? 'size-20 !rounded-2xl' : 'size-16 !rounded-xl'"
						/>
					</template>

					<template #metadata>
						<PageHeaderMetadata>
							<PageHeaderMetadataItem
								v-if="serverData.flows?.intro"
								:icon="SettingsIcon"
								class="font-semibold"
							>
								Configuring server...
							</PageHeaderMetadataItem>

							<template v-else>
								<PageHeaderMetadataItem
									v-if="serverData.loader"
									:icon="TagIcon"
									:icon-props="{ tag: serverData.loader, enforceType: 'loader' }"
								>
									{{ formatLoaderLabel(serverData.loader) }} {{ serverData.mc_version }}
								</PageHeaderMetadataItem>
								<PageHeaderMetadataItem
									v-if="serverData.net?.domain && !serverPreferences.hideSubdomainLabel"
									:icon="LinkIcon"
									tooltip="Copy server address"
									:action="copyServerAddress"
								>
									{{ serverData.net.domain }}.modrinth.gg
								</PageHeaderMetadataItem>
								<PageHeaderMetadataItem v-if="showServerUptime" :icon="TimerIcon">
									{{ formattedUptime }}
								</PageHeaderMetadataItem>
								<PageHeaderMetadataItem
									v-if="serverProject"
									:to="serverProjectLink"
									class="!text-primary"
								>
									Linked to
									<Avatar :src="serverProject.icon_url" :alt="serverProject.title" size="24px" />
									{{ serverProject.title }}
								</PageHeaderMetadataItem>
							</template>
						</PageHeaderMetadata>
					</template>

					<template #actions>
						<PageHeaderActions>
							<PanelServerActionButton :disabled="!!installError" />
							<Tooltip
								theme="dismissable-prompt"
								:triggers="[]"
								:shown="showSettingsHint"
								:auto-hide="false"
								placement="bottom-end"
							>
								<IconButton
									v-tooltip="showSettingsHint ? undefined : 'Server settings'"
									size="xl"
									label="Server settings"
									native-type="button"
									@click="handleOpenServerSettings"
								>
									<SettingsIcon />
								</IconButton>
								<template #popper>
									<div class="grid grid-cols-[min-content] gap-1">
										<div class="flex min-w-48 items-center justify-between gap-8">
											<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
												{{ formatMessage(settingsHintMessages.title) }}
											</h3>
											<IconButton
												class="!size-6"
												size="xs"
												:label="formatMessage(settingsHintMessages.dismiss)"
												native-type="button"
												@click="dismissSettingsHint"
											>
												<XIcon aria-hidden="true" />
											</IconButton>
										</div>
										<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
											{{ formatMessage(settingsHintMessages.description) }}
										</p>
									</div>
								</template>
							</Tooltip>
							<TeleportOverflowMenu
								type="quiet"
								size="xl"
								label="More server options"
								:options="serverMenuOptions"
								:disabled="!!installError"
							>
								<MoreVerticalIcon aria-hidden="true" />
							</TeleportOverflowMenu>
						</PageHeaderActions>
					</template>
				</PageHeader>
			</div>

			<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />

			<template v-else>
				<div
					data-pyro-navigation
					class="server-stagger-item isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
					:class="containedLayout ? 'shrink-0' : ''"
					:style="{ '--si': 1 }"
				>
					<NavTabs :links="navLinks" replace />
				</div>

				<div
					data-pyro-mount
					class="server-stagger-item w-full flex-1"
					: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"
							:stripe-publishable-key="stripePublishableKey"
							:site-url="siteUrl"
							:products="products"
						/>
					</div>

					<div
						v-if="!isConnected && !isReconnecting && !isLoading"
						data-pyro-server-ws-error
						class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
					>
						<IssuesIcon class="size-5 text-red" />
						Something went wrong...
					</div>

					<div
						v-if="isReconnecting"
						data-pyro-server-ws-reconnecting
						class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
					>
						<LoaderCircleIcon class="h-5 w-5 animate-spin" />
						Hang on, we're reconnecting to your server.
					</div>

					<ServerPanelAdmonitions
						class="mb-4 shrink-0"
						:sync-progress="syncProgress"
						:content-error="contentError"
						@content-retry="handleContentRetry"
					/>
					<slot :on-reinstall="onReinstall" :on-reinstall-failed="onReinstallFailed" />
				</div>
			</template>
		</template>
	</div>
	<div
		v-if="showAdvancedDebugInfo"
		class="relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
	>
		<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
		<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
			safeStringify(serverData)
		}}</pre>
	</div>
	<Suspense>
		<ServerSettingsModal
			ref="serverSettingsModal"
			:resolve-viewer="resolveViewer"
			:browse-modpacks="handleBrowseModpacks"
		/>
	</Suspense>
	<ConfirmLeaveModal
		ref="confirmLeaveModal"
		:header="formatMessage(leaveMessages.uploadInProgress)"
		:body="formatMessage(leaveMessages.leavePageBody)"
		admonition-type="critical"
	/>
</template>

<script setup lang="ts">
import type { Archon, Labrinth } from '@modrinth/api-client'
import { getNodeWebSocketUrl, ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
import {
	BoxesIcon,
	CheckIcon,
	CopyIcon,
	DatabaseBackupIcon,
	FileIcon,
	FolderOpenIcon,
	IssuesIcon,
	LayoutTemplateIcon,
	LinkIcon,
	LoaderCircleIcon,
	LockIcon,
	MoreVerticalIcon,
	RightArrowIcon,
	ServerIcon as ServerAssetIcon,
	SettingsIcon,
	TimerIcon,
	TransferIcon,
	TriangleAlertIcon,
	UsersIcon,
	XIcon,
} from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
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 { 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'
import PageHeaderMetadata from '#ui/components/base/page-header/metadata/index.vue'
import PageHeaderMetadataItem from '#ui/components/base/page-header/metadata/page-header-metadata-item.vue'
import PageHeaderActions from '#ui/components/base/page-header/page-header-actions.vue'
import ServerNotice from '#ui/components/base/ServerNotice.vue'
import TagIcon from '#ui/components/base/TagIcon.vue'
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
import ServerIcon from '#ui/components/servers/icons/ServerIcon.vue'
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
import { PanelServerActionButton } from '#ui/components/servers/server-header'
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
import {
	hasServerPermission,
	useDebugLogger,
	useLoadingBarToken,
	useModrinthServersConsole,
	useReadyState,
	useServerImage,
	useServerProject,
} from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
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'
import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
import {
	injectModrinthClient,
	injectNotificationManager,
	provideServerSettingsModal,
} from '#ui/providers'
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'

interface Tab {
	label: string
	href: string
	icon?: object
	subpages?: string[]
}

const props = withDefaults(
	defineProps<{
		serverId: string
		reloadPage: () => void
		resolveViewer: () => Promise<{ userId: string | null; userRole: string | null }>
		showCopyIdAction?: boolean
		showAdvancedDebugInfo?: boolean
		showUptime?: boolean
		additionalTabs?: Tab[]
		stripePublishableKey?: string
		siteUrl?: string
		products?: Labrinth.Billing.Internal.Product[]
		authUser?: { id: string; username: string; email: string; created: string }
		navigateToBilling?: () => void
		navigateToServers?: () => void
		browseModpacks?: (args: {
			serverId: string
			worldId: string | null
			from: 'reset-server' | 'onboarding'
		}) => void | Promise<void>
		browseContent?: (args: {
			serverId: string
			worldId: string | null
			type: 'mod' | 'plugin' | 'datapack'
		}) => void | Promise<void>
		constrainWidth?: boolean
		layoutMode?: 'page' | 'contained'
	}>(),
	{
		showCopyIdAction: false,
		showAdvancedDebugInfo: false,
		showUptime: true,
		additionalTabs: () => [],
		stripePublishableKey: undefined,
		siteUrl: undefined,
		products: () => [],
		authUser: undefined,
		navigateToBilling: undefined,
		navigateToServers: undefined,
		browseModpacks: undefined,
		browseContent: undefined,
		constrainWidth: false,
		layoutMode: 'page',
	},
)

const { formatMessage } = useVIntl()

const leaveMessages = defineMessages({
	uploadInProgress: {
		id: 'servers.manage.confirm-leave.upload-in-progress',
		defaultMessage: 'Upload in progress',
	},
	leavePageBody: {
		id: 'servers.manage.confirm-leave.body',
		defaultMessage: 'A file upload is in progress. Leaving this page will cancel the upload.',
	},
})

const settingsHintMessages = defineMessages({
	title: {
		id: 'servers.manage.settings-hint.title',
		defaultMessage: 'Your server settings have moved',
	},
	description: {
		id: 'servers.manage.settings-hint.description',
		defaultMessage: 'They can now be found here!',
	},
	dismiss: {
		id: 'servers.manage.settings-hint.dismiss',
		defaultMessage: "Don't show again",
	},
})

// disabled, keeping the animation logic cos it's really nice and we might want to re-enable in future
const DISABLE_LOADING_ANIM = true

const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const constrainWidth = computed(() => props.constrainWidth)
const containedLayout = computed(() => props.layoutMode === 'contained')
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
const queryClient = useQueryClient()
const route = useRoute()
const router = useRouter()
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'
const settingsHintDismissed = useStorage(SETTINGS_HINT_KEY, false)
const showSettingsHint = ref(!settingsHintDismissed.value)
const serverPreferences = useStorage(`pyro-server-${props.serverId}-preferences`, {
	hideSubdomainLabel: false,
})

function dismissSettingsHint() {
	showSettingsHint.value = false
	settingsHintDismissed.value = true
}

const serverSettingsModal = ref<InstanceType<typeof ServerSettingsModal> | null>(null)
const confirmLeaveModal = ref<InstanceType<typeof ConfirmLeaveModal>>()

const {
	data: serverData,
	error: serverQueryError,
	isLoading: serverLoading,
} = useQuery({
	queryKey: ['servers', 'detail', props.serverId],
	queryFn: () => client.archon.servers_v0.get(props.serverId)!,
})

useLoadingBarToken(useReadyState({ isLoading: serverLoading, data: serverData }))

function updateServerData(patch: Partial<Archon.Servers.v0.Server>) {
	if (!serverData.value) return
	queryClient.setQueryData(['servers', 'detail', props.serverId], {
		...serverData.value,
		...patch,
	})
}

const serverError = computed(() => {
	const err = serverQueryError.value
	if (err instanceof ModrinthApiError) return err
	return err ? ModrinthApiError.fromUnknown(err) : null
})

const { data: serverFull } = useQuery({
	queryKey: ['servers', 'v1', 'detail', props.serverId],
	queryFn: () => client.archon.servers_v1.get(props.serverId),
})

const worldId = computed(() => {
	if (!serverFull.value) return null
	const activeWorld = serverFull.value.worlds.find((w) => w.is_active)
	return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
})

const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
	computed(() => props.serverId),
	worldId,
)

const { disconnect: disconnectPanelSync } = useServerPanelSync({
	serverId: computed(() => props.serverId),
	worldId,
})

const { image: serverImage } = useServerImage(
	props.serverId,
	computed(() => serverData.value?.upstream ?? null),
)
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 {
	cancelUpload,
	cleanupCoreRuntime,
	connectSocket,
	cpuData,
	fsOps,
	fsQueuedOps,
	isConnected,
	ramData,
	serverPowerState,
	stats,
	uptimeSeconds,
	uploadState,
} = useServerManageCoreRuntime({
	serverId: computed(() => props.serverId),
	worldId,
	server: serverData,
	serverFull,
	isSyncingContent,
	extraBusyReasons: backupsBusy,
	setDisconnectedOnAuthIncorrect: false,
	syncUptimeFromState: true,
	incrementUptimeLocally: true,
	eventGuard: () => isMounted.value,
	onStateEvent,
})

const serverHeaderImage = computed(() =>
	serverData.value?.is_medal ? 'https://cdn-raw.modrinth.com/medal_icon.webp' : serverImage.value,
)

const showServerUptime = computed(() => props.showUptime && serverPowerState.value === 'running')

const formattedUptime = computed(() => formatUptime(uptimeSeconds.value))

const serverProjectLink = computed(() => {
	if (!serverProject.value) return ''
	return `/project/${serverProject.value.slug ?? serverProject.value.id}`
})

const serverMenuOptions = computed(() => [
	{
		id: 'all-servers',
		label: 'All servers',
		icon: ServerAssetIcon,
		action: () => void router.push('/hosting/manage'),
	},
	{
		id: 'copy-id',
		label: 'Copy ID',
		icon: CopyIcon,
		action: copyServerId,
		shown: props.showCopyIdAction,
	},
])

function formatUptime(uptime: number) {
	const days = Math.floor(uptime / (24 * 3600))
	const hours = Math.floor((uptime % (24 * 3600)) / 3600)
	const minutes = Math.floor((uptime % 3600) / 60)
	const seconds = uptime % 60

	let formatted = ''
	if (days > 0) formatted += `${days}d `
	if (hours > 0 || days > 0) formatted += `${hours}h `
	formatted += `${minutes}m ${seconds}s`
	return formatted.trim()
}

function copyServerAddress() {
	const domain = serverData.value?.net?.domain
	if (!domain) return

	void navigator.clipboard.writeText(`${domain}.modrinth.gg`)
	addNotification({
		title: 'Server address copied',
		text: "Your server's address has been copied to your clipboard.",
		type: 'success',
	})
}

function copyServerId() {
	void navigator.clipboard.writeText(props.serverId)
}

function handleOpenServerSettings() {
	openServerSettingsModal()
	dismissSettingsHint()
}

const isUploading = computed(() => uploadState.value.isUploading)
const canSetup = computed(() =>
	hasServerPermission(serverData.value?.current_user_permissions ?? 0, 'SETUP'),
)
const permissionDeniedMessage = computed(() => formatMessage(commonMessages.noPermissionAction))

function handleBeforeUnload(e: BeforeUnloadEvent) {
	if (isUploading.value) {
		e.preventDefault()
		return ''
	}
}

if (typeof window !== 'undefined') {
	watch(isUploading, (uploading) => {
		if (uploading) {
			window.addEventListener('beforeunload', handleBeforeUnload)
		} else {
			window.removeEventListener('beforeunload', handleBeforeUnload)
		}
	})

	onBeforeUnmount(() => {
		window.removeEventListener('beforeunload', handleBeforeUnload)
	})

	onBeforeRouteLeave(async () => {
		if (isUploading.value) {
			const shouldLeave = (await confirmLeaveModal.value?.prompt()) ?? false
			if (shouldLeave) cancelUpload.value?.()
			return shouldLeave
		}
		return true
	})
}

type CachedWsState = {
	stats: ServerStats
	cpuData: number[]
	ramData: number[]
	powerState: Archon.Websocket.v0.PowerState
	uptimeSeconds: number
	consoleLines: LogLine[]
}

const modrinthServersConsole = useModrinthServersConsole()
const wsStateCacheKey = ['servers', 'ws-state', props.serverId] as const
const cachedWsState = queryClient.getQueryData<CachedWsState>(wsStateCacheKey)
if (cachedWsState) {
	stats.value = cachedWsState.stats
	cpuData.value = cachedWsState.cpuData
	ramData.value = cachedWsState.ramData
	serverPowerState.value = cachedWsState.powerState
	uptimeSeconds.value = cachedWsState.uptimeSeconds
}

const log = useDebugLogger('server-panel-reveal')

const hasReceivedWsData = ref(!!cachedWsState)
log('init', {
	hasCachedWsState: !!cachedWsState,
	hasReceivedWsData: hasReceivedWsData.value,
	isConnected: isConnected.value,
	serverData: !!serverData.value,
})

const saveWsStateToCache = () => {
	if (!hasReceivedWsData.value) return
	queryClient.setQueryData(wsStateCacheKey, {
		stats: stats.value,
		cpuData: cpuData.value,
		ramData: ramData.value,
		powerState: serverPowerState.value,
		uptimeSeconds: uptimeSeconds.value,
		consoleLines: modrinthServersConsole.output.value,
	} satisfies CachedWsState)
}

watch([stats, serverPowerState], () => {
	if (!isConnected.value) return
	hasReceivedWsData.value = true
})

const canReveal = computed(() => serverData.value && hasReceivedWsData.value)
log('canReveal initial', {
	canReveal: canReveal.value,
	serverData: !!serverData.value,
	hasReceivedWsData: hasReceivedWsData.value,
})

const revealState = ref<'pending' | 'revealing' | 'visible'>(
	DISABLE_LOADING_ANIM || canReveal.value ? 'visible' : 'pending',
)
log('revealState initial', revealState.value)

const REVEAL_TOTAL_MS = 2 * 80 + 400

watch(canReveal, (ready) => {
	log('canReveal changed', { ready, revealState: revealState.value })
	if (ready && revealState.value === 'pending') {
		if (DISABLE_LOADING_ANIM) {
			revealState.value = 'visible'
		} else {
			revealState.value = 'revealing'
			setTimeout(() => {
				revealState.value = 'visible'
				log('revealState -> visible')
			}, REVEAL_TOTAL_MS)
		}
	}
})

watch(isConnected, (connected) => {
	log('isConnected changed', connected)
})

watch(serverData, (data) => {
	log('serverData changed', !!data)
})

const navLinks = computed<Tab[]>(() => [
	{
		label: 'Overview',
		href: `/hosting/manage/${props.serverId}`,
		icon: LayoutTemplateIcon,
		subpages: [],
	},
	{
		label: 'Content',
		href: `/hosting/manage/${props.serverId}/content`,
		icon: BoxesIcon,
		subpages: ['mods', 'datapacks'],
	},
	{
		label: 'Files',
		href: `/hosting/manage/${props.serverId}/files`,
		icon: FolderOpenIcon,
		subpages: [],
	},
	{
		label: 'Backups',
		href: `/hosting/manage/${props.serverId}/backups`,
		icon: DatabaseBackupIcon,
		subpages: [],
	},
	{
		label: 'Access',
		href: `/hosting/manage/${props.serverId}/access`,
		icon: UsersIcon,
		subpages: [],
	},
	...props.additionalTabs,
])

const filteredNotices = computed(
	() => serverData.value?.notices?.filter((n) => n.level !== 'survey') ?? [],
)
const surveyNotice = computed(() => serverData.value?.notices?.find((n) => n.level === 'survey'))

async function dismissNotice(noticeId: number) {
	await client.archon.servers_v0.dismissNotice(props.serverId, noticeId).catch((err) => {
		addNotification({
			title: 'Error dismissing notice',
			text: err,
			type: 'error',
		})
	})
	await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
}

async function dismissSurvey() {
	const noticeId = surveyNotice.value?.id
	if (noticeId === undefined) return
	await dismissNotice(noticeId)
}

type TallyPopupOptions = {
	key?: string
	layout?: 'default' | 'modal'
	width?: number
	alignLeft?: boolean
	hideTitle?: boolean
	overlay?: boolean
	emoji?: {
		text: string
		animation:
			| 'none'
			| 'wave'
			| 'tada'
			| 'heart-beat'
			| 'spin'
			| 'flash'
			| 'bounce'
			| 'rubber-band'
			| 'head-shake'
	}
	autoClose?: number
	showOnce?: boolean
	doNotShowAfterSubmit?: boolean
	customFormUrl?: string
	hiddenFields?: { [key: string]: unknown }
	onOpen?: () => void
	onClose?: () => void
	onPageView?: (page: number) => void
	onSubmit?: (payload: unknown) => void
}

const popupOptions = computed(
	() =>
		({
			layout: 'default',
			width: 400,
			autoClose: 2000,
			hideTitle: true,
			hiddenFields: {
				username: props.authUser?.username,
				user_id: props.authUser?.id,
				user_email: props.authUser?.email,
				server_id: serverData.value?.server_id,
				loader: serverData.value?.loader,
				game_version: serverData.value?.mc_version,
				modpack_id: serverProject.value?.id,
				modpack_name: serverProject.value?.title,
			},
			onOpen: () => console.log(`Opened survey notice: ${surveyNotice.value?.id}`),
			onClose: async () => await dismissSurvey(),
			onSubmit: (payload: unknown) => {
				console.log('Form submitted:', payload)
			},
		}) satisfies TallyPopupOptions,
)

function getTally(): { openPopup?: (id: string, opts: TallyPopupOptions) => void } | undefined {
	return (
		window as Window & { Tally?: { openPopup?: (id: string, opts: TallyPopupOptions) => void } }
	).Tally
}

function showSurvey() {
	if (!surveyNotice.value) return

	try {
		const tally = getTally()
		if (tally?.openPopup) {
			tally.openPopup(surveyNotice.value.message, popupOptions.value)
		}
	} catch (e) {
		console.error('Error opening Tally popup:', e)
	}
}

function loadTallyScript() {
	if (document.querySelector('script[src*="tally.so"]')) return
	const script = document.createElement('script')
	script.src = 'https://tally.so/widgets/embed.js'
	script.defer = true
	document.head.appendChild(script)
}

async function handleContentRetry() {
	if (!worldId.value) return
	if (!canSetup.value) {
		addNotification({
			type: 'error',
			text: permissionDeniedMessage.value,
		})
		return
	}
	try {
		await client.archon.content_v1.repair(props.serverId, worldId.value)
	} catch (err) {
		addNotification({
			type: 'error',
			text: err instanceof Error ? err.message : 'Failed to retry installation',
		})
	}
}

const handleBackupProgress = (data: Archon.Websocket.v0.WSBackupProgressEvent) => {
	handleWsBackupProgress(data)
}

const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) => {
	const allOps = data.all

	if (JSON.stringify(fsOps.value) !== JSON.stringify(allOps)) {
		fsOps.value = allOps
	}

	fsQueuedOps.value = fsQueuedOps.value.filter(
		(queuedOp) => !allOps.some((x) => x.src === queuedOp.src),
	)

	const cancelled = allOps.filter((x) => x.state === 'cancelled')
	Promise.all(
		cancelled.map((x) =>
			client.kyros.files_v0.modifyOperation(x.id, 'dismiss').catch((error) => {
				console.error('Failed to dismiss cancelled operation:', error)
			}),
		),
	)
}

let newModInvalidateTimer: ReturnType<typeof setTimeout> | null = null
const handleNewMod = () => {
	if (newModInvalidateTimer) clearTimeout(newModInvalidateTimer)
	newModInvalidateTimer = setTimeout(() => {
		newModInvalidateTimer = null
		void queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
	}, 500)
}

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

			applyOptimisticCompletion()
			installError.value = null
			invalidateAfterInstall()

			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')

			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 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,
) => {
	debug('[root.vue] onReinstall called with:', potentialArgs)

	if (serverData.value?.flows?.intro) {
		await client.archon.servers_v1.endIntro(props.serverId)
		queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
	}

	if (!serverData.value) return

	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')
	updateServerData({ status: 'available' })
	newLoader.value = null
	newLoaderVersion.value = null
	newMCVersion.value = null
}

function applyOptimisticCompletion() {
	const patch: Partial<Archon.Servers.v0.Server> = { status: 'available' }
	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] 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) 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([
				queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] }),
				queryClient.invalidateQueries({
					queryKey: ['servers', 'startup', 'v1', props.serverId],
				}),
				queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
			])
		} catch (err: unknown) {
			console.error('Error refreshing data after installation:', err)
		} finally {
			isAwaitingPostInstallRefresh.value = false
		}
	}, 2000)
}

const nodeAccessible = ref(true)

const nodeUnavailableDetails = computed(() => [
	{
		label: 'Server ID',
		value: props.serverId,
		type: 'inline' as const,
	},
	{
		label: 'Node',
		value:
			(serverError.value?.responseData as { hostname?: string } | undefined)?.hostname ??
			serverData.value?.datacenter ??
			'Unknown',
		type: 'inline' as const,
	},
	{
		label: 'Error message',
		value: nodeAccessible.value
			? (serverError.value?.message ?? 'Unknown')
			: 'Unable to reach node. Ping test failed.',
		type: 'block' 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: props.serverId,
		type: 'inline' as const,
	},
	{
		label: 'Timestamp',
		value: String(new Date().toISOString()),
		type: 'inline' as const,
	},
	{
		label: 'Error Name',
		value: serverError.value?.name,
		type: 'inline' as const,
	},
	{
		label: 'Error Message',
		value: serverError.value?.message,
		type: 'block' as const,
	},
	...(serverError.value?.originalError
		? [
				{
					label: 'Original Error',
					value: String(serverError.value.originalError),
					type: 'hidden' as const,
				},
			]
		: []),
	...(serverError.value?.stack
		? [
				{
					label: 'Stack Trace',
					value: serverError.value.stack,
					type: 'hidden' as const,
				},
			]
		: []),
])

const suspendedAction = computed(() => ({
	label: 'Go to billing settings',
	onClick: () => props.navigateToBilling?.(),
	color: 'brand' as const,
}))

const generalErrorAction = computed(() => ({
	label: 'Go back to all servers',
	onClick: () => props.navigateToServers?.(),
	color: 'brand' as const,
}))

const nodeUnavailableAction = computed(() => ({
	label: 'Reload',
	onClick: () => props.reloadPage(),
	color: 'brand' as const,
	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 })
}

function handleBrowseModpacks(args: {
	serverId: string
	worldId: string | null
	from: 'reset-server' | 'onboarding'
}) {
	props.browseModpacks?.(args)
}

function handleBrowseContent(args: {
	serverId: string
	worldId: string | null
	type: 'mod' | 'plugin' | 'datapack'
}) {
	props.browseContent?.(args)
}

provideServerSettingsModal({
	openServerSettings: (options) => openServerSettingsModal(options?.tabId),
	browseServerContent: (args) => handleBrowseContent(args),
})

function safeStringify(obj: unknown, indent = ' '): string {
	const seen = new WeakSet()
	return JSON.stringify(
		obj,
		(_key, value) => {
			if (typeof value === 'object' && value !== null) {
				if (seen.has(value)) {
					return '[Circular]'
				}
				seen.add(value)
			}
			return value
		},
		indent,
	)
}

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
		return
	}

	if (serverData.value?.node === null) {
		isLoading.value = false
		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) => {
				if (connected && cachedWsState?.consoleLines?.length) {
					modrinthServersConsole.clear()
					modrinthServersConsole.addLines(cachedWsState.consoleLines)
				}
			})
			.finally(() => {
				isLoading.value = false
			})
	}

	if (serverData.value?.flows?.intro && serverProject.value) {
		client.archon.servers_v1.endIntro(props.serverId).then(() => {
			queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
		})
	}
}

const cleanup = () => {
	isMounted.value = false
	if (newModInvalidateTimer) {
		clearTimeout(newModInvalidateTimer)
		newModInvalidateTimer = null
	}

	saveWsStateToCache()

	cleanupCoreRuntime(props.serverId)
	disconnectPanelSync()

	isReconnecting.value = false
	isLoading.value = true

	DOMPurify.removeHook('afterSanitizeAttributes')
}

onMounted(() => {
	isMounted.value = true
	syncPendingServerContentInstalls()
	window.addEventListener(
		pendingServerContentInstallsEvent,
		handlePendingServerContentInstallsChanged,
	)

	if (serverData.value) {
		initializeServer()
	} else {
		const stopWatch = watch(serverData, (data) => {
			if (data) {
				stopWatch()
				initializeServer()
			}
		})
	}

	DOMPurify.addHook(
		'afterSanitizeAttributes',
		(node: {
			tagName: string
			getAttribute: (arg0: string) => string | null
			setAttribute: (arg0: string, arg1: string) => void
		}) => {
			if (node.tagName === 'A' && node.getAttribute('target')) {
				node.setAttribute('rel', 'noopener noreferrer')
			}
		},
	)

	loadTallyScript()
	if (surveyNotice.value) {
		showSurvey()
	}

	if (route.query.openSettings) {
		const tabId = route.query.openSettings as ServerSettingsTabId
		router.replace({ query: { ...route.query, openSettings: undefined } })
		queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
		queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', props.serverId] })
		queryClient.invalidateQueries({ queryKey: ['servers', 'startup', 'v1', props.serverId] })
		nextTick(() => openServerSettingsModal(tabId))
	}
})

onUnmounted(() => {
	window.removeEventListener(
		pendingServerContentInstallsEvent,
		handlePendingServerContentInstallsChanged,
	)
	cleanup()
})
</script>

<style>
@keyframes server-action-buttons-anim {
	0% {
		opacity: 0;
		transform: translateX(1rem);
	}

	100% {
		opacity: 1;
		transform: none;
	}
}

.server-action-buttons-anim {
	animation: server-action-buttons-anim 0.2s ease-out;
}

.server-panel-pending .server-stagger-item {
	opacity: 0;
}

.server-panel-revealing .server-stagger-item {
	animation: serverReveal 0.4s ease-out both;
	animation-delay: calc(var(--si) * 80ms);
}

@keyframes serverReveal {
	from {
		opacity: 0;
		transform: translateY(12px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}
</style>
