From 6333cca056abbba7d17ad88061ab49ec0ad70e65 Mon Sep 17 00:00:00 2001 From: tdgao Date: Wed, 1 Apr 2026 23:01:24 -0600 Subject: [PATCH 01/17] feat: implement shared server header for app and website --- .../src/pages/hosting/manage/Index.vue | 204 +++++++------ .../ui/servers/PanelServerActionButton.vue | 276 ------------------ .../src/pages/hosting/manage/[id].vue | 116 ++------ packages/ui/src/components/servers/index.ts | 1 + .../server-header/PanelActionConfirmModal.vue | 70 +++++ .../server-header/PanelServerActionButton.vue | 83 ++++++ .../server-header/PanelServerOverflowMenu.vue | 138 +++++++++ .../server-header/ServerManageHeader.vue | 157 ++++++++++ .../components/servers/server-header/index.ts | 3 + .../server-header/use-server-power-action.ts | 141 +++++++++ 10 files changed, 718 insertions(+), 471 deletions(-) delete mode 100644 apps/frontend/src/components/ui/servers/PanelServerActionButton.vue create mode 100644 packages/ui/src/components/servers/server-header/PanelActionConfirmModal.vue create mode 100644 packages/ui/src/components/servers/server-header/PanelServerActionButton.vue create mode 100644 packages/ui/src/components/servers/server-header/PanelServerOverflowMenu.vue create mode 100644 packages/ui/src/components/servers/server-header/ServerManageHeader.vue create mode 100644 packages/ui/src/components/servers/server-header/index.ts create mode 100644 packages/ui/src/components/servers/server-header/use-server-power-action.ts diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue index fc1015e7a0..aebc4146a6 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Index.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue @@ -5,78 +5,27 @@
Failed to load server.
diff --git a/packages/ui/src/layouts/shared/server-settings/tabs.ts b/packages/ui/src/layouts/shared/server-settings/tabs.ts index 82427d4288..7dcb580245 100644 --- a/packages/ui/src/layouts/shared/server-settings/tabs.ts +++ b/packages/ui/src/layouts/shared/server-settings/tabs.ts @@ -31,7 +31,7 @@ export interface ServerSettingsTabDefinition { id: ServerSettingsTabId label: string icon: Component - href: (ctx: ServerSettingsTabContext) => string + href?: (ctx: ServerSettingsTabContext) => string external?: boolean shown?: (ctx: ServerSettingsTabContext) => boolean } @@ -41,32 +41,27 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [ id: 'general', label: 'General', icon: SettingsIcon, - href: ({ serverId }) => `/hosting/manage/${serverId}/options`, }, { id: 'installation', label: 'Installation', icon: WrenchIcon, - href: ({ serverId }) => `/hosting/manage/${serverId}/options/loader`, }, { id: 'network', label: 'Network', icon: VersionIcon, - href: ({ serverId }) => `/hosting/manage/${serverId}/options/network`, }, { id: 'properties', label: 'Properties', icon: ListIcon, - href: ({ serverId }) => `/hosting/manage/${serverId}/options/properties`, shown: ({ serverStatus }) => serverStatus !== 'installing', }, { id: 'advanced', label: 'Advanced', icon: TextQuoteIcon, - href: ({ serverId }) => `/hosting/manage/${serverId}/options/advanced`, }, { id: 'billing', @@ -85,14 +80,3 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [ shown: ({ isAdmin }) => isAdmin, }, ] - -export function getServerSettingsNavLinks(ctx: ServerSettingsTabContext) { - return serverSettingsTabDefinitions.map((tab) => ({ - id: tab.id, - icon: tab.icon, - label: tab.label, - href: tab.href(ctx), - external: tab.external, - shown: tab.shown ? tab.shown(ctx) : true, - })) -} diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue index 30776a944a..0688755220 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue @@ -11,6 +11,7 @@ import { injectModrinthClient, injectModrinthServerContext, injectNotificationManager, + injectServerSettingsModal, } from '#ui/providers' import { commonMessages } from '#ui/utils/common-messages' @@ -95,6 +96,7 @@ const leaveMessages = defineMessages({ const client = injectModrinthClient() const { server, worldId, busyReasons, isSyncingContent } = injectModrinthServerContext() const { addNotification } = injectNotificationManager() +const { openServerSettings } = injectServerSettingsModal() const route = useRoute() const router = useRouter() const queryClient = useQueryClient() @@ -894,7 +896,7 @@ provideContentManager({ updateModpack: handleModpackUpdate, viewModpackContent: handleViewModpackContent, unlinkModpack: handleModpackUnlink, - openSettings: () => router.push(`/hosting/manage/${serverId}/options/loader`), + openSettings: () => openServerSettings({ tabId: 'installation' }), switchVersion: handleSwitchVersion, getOverflowOptions, mapToTableItem: (item) => { diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue index 940d2a6cad..46d88a5924 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue @@ -214,13 +214,10 @@ - + @@ -321,8 +318,8 @@ import { import { useQuery, useQueryClient } from '@tanstack/vue-query' import { useTimeoutFn } from '@vueuse/core' import DOMPurify from 'dompurify' -import { computed, onMounted, onUnmounted, ref, watch } from 'vue' -import { RouterLink } from 'vue-router' +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue' @@ -340,7 +337,12 @@ import { import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue' import { useDebugLogger, useServerImage, useServerProject } from '#ui/composables' import { useServerManageCoreRuntime } from '#ui/composables/server-manage-core-runtime' -import { injectModrinthClient, injectNotificationManager } from '#ui/providers' +import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings' +import { + injectModrinthClient, + injectNotificationManager, + provideServerSettingsModal, +} from '#ui/providers' import { formatLoaderLabel } from '#ui/utils/loaders' import ServerOnboardingPanelPage from './[id]/onboarding.vue' @@ -385,6 +387,8 @@ const props = withDefaults( const { addNotification } = injectNotificationManager() const client = injectModrinthClient() const queryClient = useQueryClient() +const route = useRoute() +const router = useRouter() const debug = useDebugLogger('ServerManage') const isReconnecting = ref(false) @@ -1051,11 +1055,15 @@ const openInstallLog = () => { window.dispatchEvent(new PopStateEvent('popstate')) } -function openServerSettingsModal() { +function openServerSettingsModal(tabId?: ServerSettingsTabId) { if (!props.serverId) return - serverSettingsModal.value?.show({ serverId: props.serverId }) + serverSettingsModal.value?.show({ serverId: props.serverId, tabId }) } +provideServerSettingsModal({ + openServerSettings: (options) => openServerSettingsModal(options?.tabId), +}) + function safeStringify(obj: unknown, indent = ' '): string { const seen = new WeakSet() return JSON.stringify( @@ -1219,6 +1227,12 @@ onMounted(() => { if (surveyNotice.value) { showSurvey() } + + if (route.query.openSettings) { + const tabId = route.query.openSettings as ServerSettingsTabId + router.replace({ query: { ...route.query, openSettings: undefined } }) + nextTick(() => openServerSettingsModal(tabId)) + } }) onUnmounted(() => { diff --git a/packages/ui/src/providers/index.ts b/packages/ui/src/providers/index.ts index e6d7c9726e..9c5fcf8740 100644 --- a/packages/ui/src/providers/index.ts +++ b/packages/ui/src/providers/index.ts @@ -13,5 +13,6 @@ export * from './popup-notifications' export * from './project-page' export * from './project-page-new' export * from './server-context' +export * from './server-settings-modal' export * from './tags' export * from './web-notifications' diff --git a/packages/ui/src/providers/server-settings-modal.ts b/packages/ui/src/providers/server-settings-modal.ts new file mode 100644 index 0000000000..cf099afcd4 --- /dev/null +++ b/packages/ui/src/providers/server-settings-modal.ts @@ -0,0 +1,10 @@ +import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings' + +import { createContext } from './create-context' + +export interface ServerSettingsModalContext { + openServerSettings: (options?: { tabId?: ServerSettingsTabId }) => void +} + +export const [injectServerSettingsModal, provideServerSettingsModal] = + createContext('root.vue', 'serverSettingsModalContext') From 5cfef66bef074d2ad11289ac749577864a8d9cce Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 2 Apr 2026 16:14:41 +0200 Subject: [PATCH 08/17] fix: discovery --- .../src/pages/hosting/manage/[id].vue | 4 +++ .../servers/ServerSettingsModal.vue | 5 +-- .../layouts/wrapped/hosting/manage/root.vue | 32 ++++++++----------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/pages/hosting/manage/[id].vue b/apps/frontend/src/pages/hosting/manage/[id].vue index 8160fb2549..2ae037d4e4 100644 --- a/apps/frontend/src/pages/hosting/manage/[id].vue +++ b/apps/frontend/src/pages/hosting/manage/[id].vue @@ -12,6 +12,10 @@ :auth-user="authUser" :navigate-to-billing="() => router.push('/settings/billing')" :navigate-to-servers="() => router.push('/hosting/manage')" + :browse-modpacks="({ serverId: sid, worldId: wid, from }) => navigateTo({ + path: '/discover/modpacks', + query: { sid, from, wid: wid ?? undefined }, + })" > @@ -369,6 +373,7 @@ const props = withDefaults( authUser?: { id: string; username: string; email: string; created: string } navigateToBilling?: () => void navigateToServers?: () => void + browseModpacks?: (args: { serverId: string; worldId: string | null; from: 'reset-server' }) => void | Promise }>(), { showCopyIdAction: false, @@ -381,6 +386,7 @@ const props = withDefaults( authUser: undefined, navigateToBilling: undefined, navigateToServers: undefined, + browseModpacks: undefined, }, ) @@ -405,7 +411,6 @@ const serverSettingsModal = ref | null> const INTERCOM_APP_ID = 'ykeritl9' -// --- Server data queries --- const { data: serverData, error: serverQueryError } = useQuery({ queryKey: ['servers', 'detail', props.serverId], @@ -443,7 +448,6 @@ const serverImage = useServerImage( ) const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null)) -// --- Installation progress tracking --- const cancelledBackups = new Set() const markBackupCancelled = (backupId: string) => { @@ -505,7 +509,6 @@ const onStateEvent = (data: Archon.Websocket.v0.WSStateEvent) => { } } -// --- Core runtime --- const { backupsState, @@ -529,7 +532,6 @@ const { onStateEvent, }) -// --- Navigation tabs --- const navLinks = computed(() => [ { @@ -559,7 +561,6 @@ const navLinks = computed(() => [ ...props.additionalTabs, ]) -// --- Notices --- const filteredNotices = computed( () => serverData.value?.notices?.filter((n) => n.level !== 'survey') ?? [], @@ -583,7 +584,6 @@ async function dismissSurvey() { await dismissNotice(noticeId) } -// --- Tally survey --- type TallyPopupOptions = { key?: string @@ -668,7 +668,6 @@ function loadTallyScript() { document.head.appendChild(script) } -// --- Content retry --- async function handleContentRetry() { if (!worldId.value) return @@ -682,7 +681,6 @@ async function handleContentRetry() { } } -// --- WebSocket event handlers --- const handleBackupProgress = (data: Archon.Websocket.v0.WSBackupProgressEvent) => { if (data.task === 'file') return @@ -796,7 +794,6 @@ const handleNewMod = () => { queryClient.invalidateQueries({ queryKey: ['content', 'list'] }) } -// --- Installation result --- const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => { debug('[root.vue] handleInstallationResult received:', data) @@ -843,7 +840,6 @@ const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallation } } -// --- Reinstall handlers --- const newLoader = ref(null) const newLoaderVersion = ref(null) @@ -940,7 +936,6 @@ async function invalidateAfterInstall() { }, 2000) } -// --- Error state computeds --- const nodeAccessible = ref(true) @@ -1037,7 +1032,6 @@ const nodeUnavailableAction = computed(() => ({ disabled: false, })) -// --- Debug / copy --- 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}` @@ -1049,7 +1043,6 @@ const copyServerDebugInfo = () => { } const openInstallLog = () => { - // Navigate to files page with editing query param — works via router on both platforms const url = `/hosting/manage/${props.serverId}/files?editing=${encodeURIComponent(errorLogFile.value)}` window.history.pushState({}, '', url) window.dispatchEvent(new PopStateEvent('popstate')) @@ -1060,6 +1053,10 @@ function openServerSettingsModal(tabId?: ServerSettingsTabId) { serverSettingsModal.value?.show({ serverId: props.serverId, tabId }) } +function handleBrowseModpacks(args: { serverId: string; worldId: string | null; from: 'reset-server' }) { + props.browseModpacks?.(args) +} + provideServerSettingsModal({ openServerSettings: (options) => openServerSettingsModal(options?.tabId), }) @@ -1081,7 +1078,6 @@ function safeStringify(obj: unknown, indent = ' '): string { ) } -// --- Node reachability --- async function testNodeReachability(): Promise { const nodeInstance = serverData.value?.node?.instance @@ -1119,7 +1115,6 @@ async function testNodeReachability(): Promise { } } -// --- Initialization --- function initializeServer() { if (serverData.value?.status === 'suspended') { @@ -1167,7 +1162,6 @@ function initializeServer() { } } -// --- Cleanup --- const cleanup = () => { isMounted.value = false @@ -1184,7 +1178,6 @@ const cleanup = () => { DOMPurify.removeHook('afterSanitizeAttributes') } -// --- Lifecycle --- onMounted(() => { isMounted.value = true @@ -1231,6 +1224,9 @@ onMounted(() => { 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)) } }) From b3abc38e28084f10d5f2e776531824941b86aada Mon Sep 17 00:00:00 2001 From: tdgao Date: Thu, 2 Apr 2026 09:56:42 -0600 Subject: [PATCH 09/17] fix: misc style/layout issues --- apps/app-frontend/src/App.vue | 2 +- .../manage/components/ServerManageStats.vue | 4 +- .../layouts/wrapped/hosting/manage/root.vue | 34 ++-- pnpm-lock.yaml | 170 ++++++++++++------ 4 files changed, 132 insertions(+), 78 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index a0825c7107..54d9320c26 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -1200,7 +1200,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@@ -87,7 +87,7 @@ const props = withDefaults( const apexChartComponent = shallowRef(null) const chartsReady = ref(new Set()) const storageKey = computed(() => `pyro-server-${serverId || 'unknown'}-preferences`) -const userPreferences = useStorage(storageKey, { +const userPreferences = useStorage(storageKey.value, { ramAsNumber: false, }) diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue index 29fcfc74d3..fd5bd36f0e 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue @@ -1,7 +1,7 @@