feat: backups page cleanup before worlds (#5844)

* feat: card alignment + fix modals

* feat: change admon title in restore alert modal

* fix: lint

* feat: backups queue api into api-client

* feat: impl backup queue api endpoints into frontend

* feat: ack fix

* feat: bulk actions

* feat: bulk delete impl

* fix: lint

* fix: align error states

* fix: transition group

* feat: ready for qa

* fix: lint

* feat: qa

* feat: stacked admonitions component

* fix: issues with stacking

* feat: hook up admonition stacking + fix app csp for staging kyros nodes

* fix: logs.vue

* qa: close stack on admonitions click

* fix: all problems with stacked admonitions

* qa: admonition cleanup and copy overhaul draft

* fix: qa issues padding

* fix: padding bug

* feat: qa

* fix: intercom in app csp bug

* fix: positioning intercom

* feat: loading overlay on top of console + admon consistency changes

* feat: scroll indicator fade in backup delete modal + admon timestamp fix

* feat: move action bar behind modal

* fix: lint + i18n

* fix: server ping spam on filter (cache but clear on unmount)

* fix: 1 admon fade in flicker issue

* chore: temp staging undo

* qa: changes

* fix: lint

* chore: revert staging to use staging

* fix: scoping
This commit is contained in:
Calum H.
2026-04-27 19:03:48 +00:00
committed by GitHub
parent 85ae1f2074
commit 620894aecb
79 changed files with 4640 additions and 1656 deletions
@@ -1,12 +1,13 @@
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
import {
injectAppBackup,
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
} from '#ui/providers/'
} from '#ui/providers'
export function useInlineBackup(backupName: string | (() => string)) {
const serverCtx = injectModrinthServerContext(null)
@@ -60,110 +61,65 @@ export function useInlineBackup(backupName: string | (() => string)) {
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const { serverId, worldId, backupsState, markBackupCancelled } = serverCtx
const { serverId, worldId } = serverCtx
const isBackingUp = ref(false)
const { activeOperationByBackupId, backups, hasActiveCreate, invalidate } = useServerBackupsQueue(
computed(() => serverId),
worldId,
)
const createdBackupId = ref<string | null>(null)
const pendingCreate = ref(false)
const backupFailed = ref(false)
const backupComplete = ref(false)
const backupCancelled = ref(false)
const isCancelling = ref(false)
const createdBackupId = ref<string | null>(null)
const externalBackupInProgress = computed(() => {
for (const [id, entry] of backupsState.entries()) {
if (id !== createdBackupId.value && entry.create?.state === 'ongoing') return true
}
return false
})
// Watch backupsState for websocket progress events from Kyros
watch(
() => {
if (!createdBackupId.value) return null
return backupsState.get(createdBackupId.value)
},
(entry) => {
if (!entry?.create) return
if (entry.create.state === 'done') {
stopPolling()
isBackingUp.value = false
backupComplete.value = true
} else if (entry.create.state === 'cancelled') {
stopPolling()
isBackingUp.value = false
isCancelling.value = false
backupCancelled.value = true
} else if (entry.create.state === 'failed') {
stopPolling()
isBackingUp.value = false
backupFailed.value = true
}
},
{ deep: true },
const myBackup = computed(() =>
createdBackupId.value ? backups.value.find((b) => b.id === createdBackupId.value) : undefined,
)
const myActiveOp = computed(() =>
createdBackupId.value ? activeOperationByBackupId.value.get(createdBackupId.value) : undefined,
)
// Fallback: poll the REST API in case websocket events don't arrive
let pollTimer: ReturnType<typeof setInterval> | null = null
const isBackingUp = computed(
() =>
!backupComplete.value &&
!backupFailed.value &&
!backupCancelled.value &&
(!!createdBackupId.value || pendingCreate.value),
)
function stopPolling() {
if (pollTimer !== null) {
clearInterval(pollTimer)
pollTimer = null
}
}
const externalBackupInProgress = computed(() => hasActiveCreate.value && !myActiveOp.value)
async function pollBackupStatus(backupId: string) {
if (!isBackingUp.value) {
stopPolling()
return
}
try {
const backup = await client.archon.backups_v1.get(serverId, worldId.value!, backupId)
const isTerminal =
backup.status === 'done' || backup.status === 'error' || backup.status === 'timed_out'
if (isTerminal) {
stopPolling()
if (!isBackingUp.value) return
if (backup.status === 'error' || backup.status === 'timed_out') {
isBackingUp.value = false
backupFailed.value = true
} else {
isBackingUp.value = false
backupComplete.value = true
}
}
} catch {
stopPolling()
isBackingUp.value = false
backupFailed.value = true
}
}
watch(
myBackup,
(b) => {
if (!createdBackupId.value || !b) return
if (b.status === 'done') backupComplete.value = true
else if (b.status === 'error' || b.status === 'timed_out') backupFailed.value = true
},
{ immediate: true },
)
async function startBackup() {
if (!worldId.value) return
const name = typeof backupName === 'function' ? backupName() : backupName
isBackingUp.value = true
backupFailed.value = false
backupComplete.value = false
backupCancelled.value = false
isCancelling.value = false
createdBackupId.value = null
pendingCreate.value = true
try {
const { id } = await client.archon.backups_v1.create(serverId, worldId.value, { name })
const { id } = await client.archon.backups_queue_v1.create(serverId, worldId.value, { name })
createdBackupId.value = id
stopPolling()
pollTimer = setInterval(() => pollBackupStatus(id), 3000)
await invalidate()
} catch (error) {
isBackingUp.value = false
backupFailed.value = true
const message = error instanceof Error ? error.message : String(error)
const isRateLimit = message.includes('429')
addNotification({
@@ -171,6 +127,8 @@ export function useInlineBackup(backupName: string | (() => string)) {
title: 'Error creating backup',
text: isRateLimit ? "You're creating backups too fast." : message,
})
} finally {
pendingCreate.value = false
}
}
@@ -178,23 +136,19 @@ export function useInlineBackup(backupName: string | (() => string)) {
if (!worldId.value || !createdBackupId.value || !isBackingUp.value) return
isCancelling.value = true
stopPolling()
markBackupCancelled(createdBackupId.value)
try {
await client.archon.backups_v1.delete(serverId, worldId.value, createdBackupId.value)
isBackingUp.value = false
isCancelling.value = false
backupCancelled.value = true
isCancelling.value = false
await invalidate()
addNotification({
type: 'info',
title: 'Backup cancelled',
text: 'The backup has been cancelled. You can create a new one or proceed without a backup.',
})
} catch {
isBackingUp.value = false
isCancelling.value = false
backupFailed.value = true
isCancelling.value = false
}
}
@@ -216,7 +170,6 @@ export function useInlineBackup(backupName: string | (() => string)) {
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
stopPolling()
})
onBeforeRouteLeave(() => {