feat: impl world scoped power endpoint

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent ad7a4d7d76
commit 2c1b8a2326
10 changed files with 61 additions and 37 deletions
@@ -97,19 +97,6 @@ export class ArchonServersV0Module extends AbstractModule {
}) })
} }
/**
* Send a power action to a server (Start, Stop, Restart, Kill)
* POST /modrinth/v0/servers/:id/power
*/
public async power(serverId: string, action: Archon.Servers.v0.PowerAction): Promise<void> {
await this.client.request(`/servers/${serverId}/power`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
body: { action },
})
}
/** /**
* Reinstall a server with a new loader or modpack * Reinstall a server with a new loader or modpack
* POST /modrinth/v0/servers/:id/reinstall * POST /modrinth/v0/servers/:id/reinstall
@@ -91,6 +91,23 @@ export class ArchonServersV1Module extends AbstractModule {
}) })
} }
/**
* Run a power action for a specific world
* POST /v1/servers/:id/worlds/:wid/power
*/
public async powerWorld(
serverId: string,
worldId: string,
request: Archon.Servers.v1.WorldPowerActionRequest,
): Promise<void> {
await this.client.request(`/servers/${serverId}/worlds/${worldId}/power`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** /**
* Reset a world to onboarding * Reset a world to onboarding
* POST /v1/servers/:id/worlds/:wid/onboard * POST /v1/servers/:id/worlds/:wid/onboard
@@ -681,8 +681,6 @@ export namespace Archon {
token: string // JWT token for filesystem access token: string // JWT token for filesystem access
} }
export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
export type ReinstallLoaderRequest = { export type ReinstallLoaderRequest = {
loader: string loader: string
loader_version?: string loader_version?: string
@@ -715,6 +713,13 @@ export namespace Archon {
} }
export namespace v1 { export namespace v1 {
export type WorldPowerAction = 'start' | 'stop' | 'restart' | 'kill'
export type WorldPowerActionRequest = {
action: WorldPowerAction
shutdown_strategy?: string | null
}
export type ServerFull = { export type ServerFull = {
id: string id: string
name: string name: string
@@ -14,7 +14,7 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled v-if="props.restart" color="brand"> <ButtonStyled v-if="props.restart" color="brand">
<button :disabled="props.isUpdating || isTransitioning" @click="saveAndPower"> <button :disabled="props.isUpdating || isTransitioning || !worldId" @click="saveAndPower">
<SpinnerIcon v-if="props.isUpdating || isTransitioning" class="animate-spin" /> <SpinnerIcon v-if="props.isUpdating || isTransitioning" class="animate-spin" />
{{ powerButtonLabel }} {{ powerButtonLabel }}
</button> </button>
@@ -43,7 +43,7 @@ const props = defineProps<{
const client = injectModrinthClient() const client = injectModrinthClient()
const { powerState } = injectModrinthServerContext() const { powerState, worldId } = injectModrinthServerContext()
const isStopped = computed(() => powerState.value === 'stopped' || powerState.value === 'crashed') const isStopped = computed(() => powerState.value === 'stopped' || powerState.value === 'crashed')
@@ -63,6 +63,9 @@ const saveAndPower = async () => {
} catch { } catch {
return return
} }
await client.archon.servers_v0.power(props.serverId, isStopped.value ? 'Start' : 'Restart') if (!worldId.value) return
await client.archon.servers_v1.powerWorld(props.serverId, worldId.value, {
action: isStopped.value ? 'start' : 'restart',
})
} }
</script> </script>
@@ -76,7 +76,7 @@ const props = withDefaults(
loader: null, loader: null,
loaderVersion: null, loaderVersion: null,
lastActive: null, lastActive: null,
fallbackName: 'World', fallbackName: 'Instance',
headerClass: '', headerClass: '',
actions: () => [], actions: () => [],
}, },
@@ -1,3 +1,3 @@
export { default as PanelServerActionButton } from './PanelServerActionButton.vue' export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
export { default as ServerManageHeader } from './ServerManageHeader.vue' export { default as ServerManageHeader } from './ServerManageHeader.vue'
export { default as WorldManageHeader } from './WorldManageHeader.vue' export { default as ServerInstanceManageHeader } from './ServerInstanceManageHeader.vue'
@@ -9,12 +9,19 @@ import {
injectNotificationManager, injectNotificationManager,
} from '#ui/providers' } from '#ui/providers'
export type PowerAction = Archon.Servers.v0.PowerAction export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
const powerActionMap = {
Start: 'start',
Stop: 'stop',
Restart: 'restart',
Kill: 'kill',
} as const satisfies Record<PowerAction, Archon.Servers.v1.WorldPowerAction>
export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) { export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const client = injectModrinthClient() const client = injectModrinthClient()
const { serverId, server, powerState, isSyncingContent, busyReasons } = const { serverId, worldId, server, powerState, isSyncingContent, busyReasons } =
injectModrinthServerContext() injectModrinthServerContext()
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { canUsePowerActions, permissionDeniedMessage } = useServerPermissions() const { canUsePowerActions, permissionDeniedMessage } = useServerPermissions()
@@ -46,16 +53,18 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
const busyTooltip = computed(() => { const busyTooltip = computed(() => {
if (!canUsePowerActions.value) return permissionDeniedMessage.value if (!canUsePowerActions.value) return permissionDeniedMessage.value
if (!worldId.value) return 'Your server instance is loading'
if (isStarting.value) return 'Your server is starting' if (isStarting.value) return 'Your server is starting'
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
}) })
const canTakeAction = computed( const canTakeAction = computed(
() => !isTransitioning.value && !isBlockedByPropsBusyOrPermission.value, () => !!worldId.value && !isTransitioning.value && !isBlockedByPropsBusyOrPermission.value,
) )
const canKill = computed( const canKill = computed(
() => () =>
!!worldId.value &&
!isBlockedByPropsBusyOrPermission.value && !isBlockedByPropsBusyOrPermission.value &&
(isStopping.value || isRunning.value || isStarting.value), (isStopping.value || isRunning.value || isStarting.value),
) )
@@ -72,9 +81,13 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
} }
}) })
async function sendPowerAction(action: PowerAction) { async function sendPowerAction(action: PowerAction, targetWorldId = worldId.value) {
if (!targetWorldId) return
try { try {
await client.archon.servers_v0.power(serverId, action) await client.archon.servers_v1.powerWorld(serverId, targetWorldId, {
action: powerActionMap[action],
})
} catch (error) { } catch (error) {
console.error(`Error performing ${action} on server:`, error) console.error(`Error performing ${action} on server:`, error)
addNotification({ addNotification({
@@ -85,13 +98,13 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
} }
} }
function initiateAction(action: PowerAction) { function initiateAction(action: PowerAction, targetWorldId = worldId.value) {
if (action === 'Kill') { if (action === 'Kill') {
if (!canKill.value) return if (!canKill.value) return
} else { } else {
if (!canTakeAction.value) return if (!canTakeAction.value) return
} }
void sendPowerAction(action) void sendPowerAction(action, targetWorldId)
} }
function handlePrimaryAction() { function handlePrimaryAction() {
@@ -389,8 +389,8 @@ onMounted(async () => {
// Restart // Restart
async function restartServer() { async function restartServer() {
if (!canUsePowerActions.value) return if (!canUsePowerActions.value || !worldId.value) return
await client.archon.servers_v0.power(serverId, 'Restart') await client.archon.servers_v1.powerWorld(serverId, worldId.value, { action: 'restart' })
} }
function getSessionUploadFilename(fileName: string) { function getSessionUploadFilename(fileName: string) {
@@ -1,6 +1,6 @@
<template> <template>
<div class="flex min-h-[36rem] flex-col gap-4 text-primary"> <div class="flex min-h-[36rem] flex-col gap-4 text-primary">
<WorldManageHeader <ServerInstanceManageHeader
:name="worldName" :name="worldName"
:game-version="gameVersion" :game-version="gameVersion"
:loader="loader" :loader="loader"
@@ -38,7 +38,7 @@ import { useRouter } from 'vue-router'
import type { JoinedButtonAction } from '#ui/components/base/JoinedButtons.vue' import type { JoinedButtonAction } from '#ui/components/base/JoinedButtons.vue'
import NavTabs from '#ui/components/base/NavTabs.vue' import NavTabs from '#ui/components/base/NavTabs.vue'
import { WorldManageHeader } from '#ui/components/servers/server-header' import { ServerInstanceManageHeader } from '#ui/components/servers/server-header'
import { useServerPowerAction } from '#ui/components/servers/server-header/use-server-power-action' import { useServerPowerAction } from '#ui/components/servers/server-header/use-server-power-action'
import { useRelativeTime } from '#ui/composables' import { useRelativeTime } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n' import { defineMessages, useVIntl } from '#ui/composables/i18n'
@@ -179,12 +179,11 @@ const restartSplitActions = computed<JoinedButtonAction[]>(() => [
icon: UpdatedIcon, icon: UpdatedIcon,
action: () => initiateAction('Restart'), action: () => initiateAction('Restart'),
}, },
// TODO: Implement world scoping when Archon/Kyros support target worlds in power requests.
...restartableWorlds.value.map((world) => ({ ...restartableWorlds.value.map((world) => ({
id: `restart-${world.id}`, id: `restart-${world.id}`,
label: `Restart with ${world.name}`, label: `Restart with ${world.name}`,
icon: GlobeIcon, icon: GlobeIcon,
action: () => initiateAction('Restart'), action: () => initiateAction('Restart', world.id),
})), })),
]) ])
const powerActions = computed(() => { const powerActions = computed(() => {
@@ -1,11 +1,11 @@
import { PlayIcon, SettingsIcon } from '@modrinth/assets' import { PlayIcon, SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite' import type { Meta, StoryObj } from '@storybook/vue3-vite'
import WorldManageHeader from '../../components/servers/server-header/WorldManageHeader.vue' import ServerInstanceManageHeader from '../../components/servers/server-header/ServerInstanceManageHeader.vue'
const meta = { const meta = {
title: 'Servers/WorldManageHeader', title: 'Servers/ServerInstanceManageHeader',
component: WorldManageHeader, component: ServerInstanceManageHeader,
parameters: { parameters: {
layout: 'padded', layout: 'padded',
}, },
@@ -15,7 +15,7 @@ const meta = {
template: '<div style="max-width: 920px;"><story /></div>', template: '<div style="max-width: 920px;"><story /></div>',
}), }),
], ],
} satisfies Meta<typeof WorldManageHeader> } satisfies Meta<typeof ServerInstanceManageHeader>
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>