55 </div >
66 <div v-else-if =" hasError" class =" py-8 text-secondary" >Failed to load server.</div >
77 <template v-else >
8- <!-- TODO-SERVERS: make a shared server header component for website and app to share -->
9- <RouterLink
10- to =" /hosting/manage"
11- class =" breadcrumb goto-link mt-6 mb-4 flex w-fit items-center"
8+ <ServerManageHeader
9+ :server =" server"
10+ :server-image =" serverImage"
11+ :server-project =" serverProject"
12+ :server-project-link =" serverProjectLink"
13+ breadcrumb-class =" breadcrumb goto-link mt-6 mb-4 flex w-fit items-center"
14+ header-class =" mb-4"
15+ :show-uptime =" false"
1216 >
13- <LeftArrowIcon />
14- All servers
15- </RouterLink >
16- <ContentPageHeader class =" mb-4" >
17- <template #icon >
18- <ServerIcon
19- :image ="
20- server.is_medal
21- ? 'https://cdn-raw.modrinth.com/medal_icon.webp'
22- : (serverImage ?? undefined)
23- "
24- />
25- </template >
26- <template #title >
27- {{ server.name || 'Server' }}
28- </template >
29- <template #stats >
30- <div
31- v-if =" server.flows?.intro"
32- class =" flex items-center gap-2 font-semibold text-secondary"
33- >
34- <SettingsIcon />
35- Configuring server...
36- </div >
37- <div v-else class =" flex flex-wrap items-center gap-2" >
38- <div v-if =" server.loader" class =" flex items-center gap-2 font-medium capitalize" >
39- <LoaderIcon :loader =" server.loader" class =" flex shrink-0 [&& ]:size-5" />
40- {{ server.loader }} {{ server.mc_version }}
41- </div >
42-
43- <div
44- v-if =" server.loader && server.net?.domain"
45- class =" h-1.5 w-1.5 rounded-full bg-surface-5"
46- />
47-
48- <div
49- v-if =" server.net?.domain"
50- v-tooltip =" 'Copy server address'"
51- class =" flex cursor-pointer items-center gap-2 font-medium hover:underline"
52- @click =" copyServerAddress"
53- >
54- <LinkIcon class =" flex size-5 shrink-0" />
55- {{ server.net.domain }}.modrinth.gg
56- </div >
57-
58- <div
59- v-if =" serverProject && (server.loader || server.net?.domain)"
60- class =" h-1.5 w-1.5 rounded-full bg-surface-5"
61- />
62-
63- <div v-if =" serverProject" class =" flex items-center gap-1.5 font-medium text-primary" >
64- Linked to
65- <Avatar :src =" serverProject.icon_url" :alt =" serverProject.title" size =" 24px" />
66- <RouterLink :to =" serverProjectLink" class =" truncate text-primary hover:underline" >
67- {{ serverProject.title }}
68- </RouterLink >
69- </div >
70- </div >
71- </template >
7217 <template #actions >
73- <ButtonStyled circular size =" large" >
74- <button v-tooltip =" 'Server settings'" @click =" openServerSettingsModal" >
75- <SettingsIcon />
76- </button >
77- </ButtonStyled >
18+ <div class =" flex gap-2" v-if =" isConnected && !server.flows?.intro" >
19+ <PanelServerActionButton />
20+ <ButtonStyled circular size =" large" >
21+ <button v-tooltip =" 'Server settings'" @click =" openServerSettingsModal" >
22+ <SettingsIcon />
23+ </button >
24+ </ButtonStyled >
25+ <PanelServerOverflowMenu :show-copy-id-action =" themeStore.devMode" />
26+ </div >
7827 </template >
79- </ContentPageHeader >
28+ </ServerManageHeader >
8029
8130 <div class =" mb-4" >
8231 <NavTabs :links =" tabs" />
10352
10453<script setup lang="ts">
10554import { type Archon , clearNodeAuthState , setNodeAuthState } from ' @modrinth/api-client'
55+ import { BoxesIcon , DatabaseBackupIcon , FolderOpenIcon , SettingsIcon } from ' @modrinth/assets'
10656import {
107- BoxesIcon ,
108- DatabaseBackupIcon ,
109- FolderOpenIcon ,
110- LeftArrowIcon ,
111- LinkIcon ,
112- SettingsIcon ,
113- } from ' @modrinth/assets'
114- import {
115- Avatar ,
11657 type BusyReason ,
11758 ButtonStyled ,
118- ContentPageHeader ,
11959 defineMessage ,
12060 injectModrinthClient ,
121- injectNotificationManager ,
122- LoaderIcon ,
12361 LoadingIndicator ,
12462 NavTabs ,
63+ PanelServerActionButton ,
64+ PanelServerOverflowMenu ,
12565 provideModrinthServerContext ,
126- ServerIcon ,
66+ ServerManageHeader ,
12767} from ' @modrinth/ui'
12868import { useQuery , useQueryClient } from ' @tanstack/vue-query'
12969import { computed , onUnmounted , reactive , ref , watch } from ' vue'
130- import { RouterLink , useRoute } from ' vue-router'
70+ import { useRoute } from ' vue-router'
13171
13272import ServerSettingsModal from ' @/components/ui/modal/ServerSettingsModal.vue'
73+ import { useTheming } from ' @/store/theme'
13374
13475const route = useRoute ()
76+ const themeStore = useTheming ()
13577const client = injectModrinthClient ()
13678const queryClient = useQueryClient ()
137- const { addNotification } = injectNotificationManager ()
13879
13980const serverId = computed (() => {
14081 const rawId = route .params .id
@@ -196,11 +137,13 @@ const serverProjectLink = computed(() => {
196137 return ` /project/${serverProject .value .slug ?? serverProject .value .id } `
197138})
198139
199- const isConnected = ref (true )
140+ const isConnected = ref (false )
200141const powerState = ref <Archon .Websocket .v0 .PowerState >(' stopped' )
201142const isServerRunning = computed (() => powerState .value === ' running' )
202143const backupsState = reactive (new Map ())
203144const isSyncingContent = ref (false )
145+ const socketUnsubscribers = ref <(() => void )[]>([])
146+ const connectedSocketServerId = ref <string | null >(null )
204147
205148const busyReasons = computed <BusyReason []>(() => {
206149 const reasons: BusyReason [] = []
@@ -277,30 +220,98 @@ function markBackupCancelled(backupId: string) {
277220 backupsState .delete (backupId )
278221}
279222
280- function copyServerAddress() {
281- if (! server .value ?.net ?.domain ) return
282- navigator .clipboard .writeText (server .value .net .domain + ' .modrinth.gg' )
283- addNotification ({
284- title: ' Server address copied' ,
285- text: " Your server's address has been copied to your clipboard." ,
286- type: ' success' ,
287- })
288- }
289-
290223function openServerSettingsModal() {
291224 if (! serverId .value ) return
292225 serverSettingsModal .value ?.show ({ serverId: serverId .value })
293226}
294227
228+ function setPowerState(state : Archon .Websocket .v0 .PowerState ) {
229+ powerState .value = state
230+ }
231+
232+ function handlePowerState(data : Archon .Websocket .v0 .WSPowerStateEvent ) {
233+ setPowerState (data .state )
234+ }
235+
236+ function handleState(data : Archon .Websocket .v0 .WSStateEvent ) {
237+ const powerMap: Record <Archon .Websocket .v0 .FlattenedPowerState , Archon .Websocket .v0 .PowerState > =
238+ {
239+ not_ready: ' stopped' ,
240+ starting: ' starting' ,
241+ running: ' running' ,
242+ stopping: ' stopping' ,
243+ idle:
244+ data .was_oom || (data .exit_code != null && data .exit_code !== 0 ) ? ' crashed' : ' stopped' ,
245+ }
246+ setPowerState (powerMap [data .power_variant ])
247+ }
248+
249+ function disconnectSocket(targetServerId ? : string ) {
250+ for (const unsub of socketUnsubscribers .value ) unsub ()
251+ socketUnsubscribers .value = []
252+
253+ if (targetServerId ) {
254+ client .archon .sockets .disconnect (targetServerId )
255+ }
256+ connectedSocketServerId .value = null
257+ isConnected .value = false
258+ setPowerState (' stopped' )
259+ }
260+
261+ async function connectSocket(targetServerId : string ) {
262+ if (connectedSocketServerId .value === targetServerId && isConnected .value ) {
263+ return
264+ }
265+ disconnectSocket (targetServerId )
266+
267+ try {
268+ await client .archon .sockets .safeConnect (targetServerId , { force: true })
269+ connectedSocketServerId .value = targetServerId
270+ isConnected .value = true
271+ socketUnsubscribers .value = [
272+ client .archon .sockets .on (targetServerId , ' state' , handleState ),
273+ client .archon .sockets .on (targetServerId , ' power-state' , handlePowerState ),
274+ client .archon .sockets .on (targetServerId , ' auth-incorrect' , () => {
275+ isConnected .value = false
276+ }),
277+ client .archon .sockets .on (targetServerId , ' auth-ok' , () => {
278+ isConnected .value = true
279+ }),
280+ ]
281+ } catch (error ) {
282+ console .error (' [hosting/manage] Failed to connect server socket:' , error )
283+ isConnected .value = false
284+ }
285+ }
286+
295287watch (
296288 () => serverId .value ,
297- () => {
289+ (newServerId , oldServerId ) => {
290+ if (oldServerId && oldServerId !== newServerId ) {
291+ disconnectSocket (oldServerId )
292+ }
298293 fsAuth .value = null
299294 void refreshFsAuth ().catch (() => {})
300295 },
301296 { immediate: true },
302297)
303298
299+ watch (
300+ () => [serverId .value , serverQuery .data .value ] as const ,
301+ ([currentServerId , currentServer ]) => {
302+ if (! currentServerId || ! currentServer ) return
303+ if (currentServer .status === ' suspended' || currentServer .node === null ) {
304+ disconnectSocket (currentServerId )
305+ return
306+ }
307+ if (connectedSocketServerId .value === currentServerId && isConnected .value ) {
308+ return
309+ }
310+ void connectSocket (currentServerId )
311+ },
312+ { immediate: true },
313+ )
314+
304315provideModrinthServerContext ({
305316 get serverId() {
306317 return serverId .value
@@ -323,6 +334,7 @@ provideModrinthServerContext({
323334setNodeAuthState (() => fsAuth .value , refreshFsAuth )
324335
325336onUnmounted (() => {
337+ disconnectSocket (serverId .value || undefined )
326338 clearNodeAuthState ()
327339})
328340
0 commit comments