From 01b0b9d4d04f24ebfa62a6296bc571e3724943bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Thu, 22 Oct 2026 12:53:59 -0400 Subject: [PATCH 1/2] feat: repair frontend --- .../archon/backup-repositories/internal.ts | 62 ++ .../api-client/src/modules/archon/index.ts | 1 + .../api-client/src/modules/archon/types.ts | 116 +++ packages/api-client/src/modules/index.ts | 2 + .../backup-repository-reparation.vue | 773 ++++++++++++++++++ .../shared/server-settings/pages/support.vue | 3 + packages/ui/src/locales/en-US/index.json | 171 ++++ 7 files changed, 1128 insertions(+) create mode 100644 packages/api-client/src/modules/archon/backup-repositories/internal.ts create mode 100644 packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue diff --git a/packages/api-client/src/modules/archon/backup-repositories/internal.ts b/packages/api-client/src/modules/archon/backup-repositories/internal.ts new file mode 100644 index 00000000000..e92a1efa7ac --- /dev/null +++ b/packages/api-client/src/modules/archon/backup-repositories/internal.ts @@ -0,0 +1,62 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { Archon } from '../types' + +export class ArchonBackupRepositoriesInternalModule extends AbstractModule { + public getModuleID(): string { + return 'archon_backup_repositories_internal' + } + + /** + * List the latest reparation runs of a backup repository with their operations, newest first. + * GET /_internal/backups/repositories/:id/reparation + */ + public async getReparations( + repositoryId: number, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation`, + { + api: 'archon', + version: 'internal', + method: 'GET', + }, + ) + } + + /** + * Start a reparation run of a backup repository by checking it. + * POST /_internal/backups/repositories/:id/reparation + */ + public async startReparation( + repositoryId: number, + options?: Archon.BackupRepositories.Internal.StartReparationQuery, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation`, + { + api: 'archon', + version: 'internal', + method: 'POST', + params: options?.read ? { read: true } : undefined, + }, + ) + } + + /** + * Start an operation in the backup repository's latest reparation run. + * POST /_internal/backups/repositories/:id/reparation/:operation + */ + public async startOperation( + repositoryId: number, + operationType: Archon.BackupRepositories.Internal.RunnableOperationType, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation/${operationType}`, + { + api: 'archon', + version: 'internal', + method: 'POST', + }, + ) + } +} diff --git a/packages/api-client/src/modules/archon/index.ts b/packages/api-client/src/modules/archon/index.ts index 335bd234345..44a71626377 100644 --- a/packages/api-client/src/modules/archon/index.ts +++ b/packages/api-client/src/modules/archon/index.ts @@ -1,4 +1,5 @@ export * from './actions/v1' +export * from './backup-repositories/internal' export * from './backups/v1' export * from './backups-queue/v1' export * from './content/v1' diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index edc4205ec75..7492705f50a 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -758,6 +758,8 @@ export namespace Archon { tags: string[] location: ServerLocation worlds: WorldFull[] + /** IDs of the server's backup repositories. Only returned to support agents. */ + repository_ids?: number[] } export type SftpCredentials = { @@ -998,6 +1000,120 @@ export namespace Archon { } } + export namespace BackupRepositories { + export namespace Internal { + export type OperationType = + | 'check' + | 'check-read-data' + | 'repair-index' + | 'repair-packs' + | 'repair-snapshots' + | 'prune' + | 'restore-hidden-versions' + + /** Operations started in an existing run. Checks start a new run instead. */ + export type RunnableOperationType = Exclude + + /** + * `completed` when the operation ran, regardless of what it found, or `failed` when it + * couldn't run. + */ + export type OperationStatus = 'pending' | 'ongoing' | 'completed' | 'failed' + + export type StartReparationQuery = { + /** Whether the run's check reads all pack data, which is much slower. */ + read?: boolean + } + + export type RepairCheckOutput = { + type: 'check' + exit_code: number | null + num_errors: number + broken_packs: string[] + suggest_repair_index: boolean + suggest_prune: boolean + /** The first reported errors, capped to keep the result small. */ + errors: string[] + /** Whether more errors were reported than included in `errors`. */ + errors_truncated: boolean + } + + export type RepairSnapshot = { + id: string + time: string + original?: string + tags: string[] + } + + export type RepairOutput = + | RepairCheckOutput + | { + type: 'snapshots' + snapshots: RepairSnapshot[] + } + | { + type: 'command' + exit_code: number | null + output_tail: string + } + + /** Results of a completed `repair-snapshots` operation. */ + export type RepairSnapshotsResults = { + repair: RepairOutput + remapped_backups: { + backup_id: string + original_snapshot_id: string + snapshot_id: string + }[] + missing_snapshot_backups: string[] + } + + /** Results of a `restore-hidden-versions` operation. Only the destination is set while it runs. */ + export type RestoreHiddenVersionsResults = { + destination_repository_id: number + packs?: number + hidden_packs?: number + snapshots?: number + hidden_snapshots?: number + bytes?: number + copied_objects?: number + skipped_objects?: number + missing_objects?: string[] + repair_index?: RepairOutput + imported_backups?: number + matched_backups?: number + } + + export type ReparationOperation = { + id: number + operation_type: OperationType + status: OperationStatus + params: Record + /** + * A {@link RepairOutput} for checks, `repair-index`, `repair-packs` and `prune`, a + * {@link RepairSnapshotsResults} for `repair-snapshots`, and a + * {@link RestoreHiddenVersionsResults} for `restore-hidden-versions`. + */ + results: RepairOutput | RepairSnapshotsResults | RestoreHiddenVersionsResults | null + error: string | null + node_id: string | null + created_by: string | null + created_at: string + started_at: string | null + completed_at: string | null + } + + export type ReparationRun = { + id: number + repository_id: number + server_id: string | null + created_by: string | null + created_at: string + operations: ReparationOperation[] + } + } + } + export namespace Sync { export namespace v1 { export type SyncCategory = 'backup' | 'users' | 'server' | 'protocol' | 'world' diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 2463d7d1d7b..b0699c3280f 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -1,6 +1,7 @@ import type { AbstractModrinthClient } from '../core/abstract-client' import type { AbstractModule } from '../core/abstract-module' import { ArchonActionsV1Module } from './archon/actions/v1' +import { ArchonBackupRepositoriesInternalModule } from './archon/backup-repositories/internal' import { ArchonBackupsV1Module } from './archon/backups/v1' import { ArchonBackupsQueueV1Module } from './archon/backups-queue/v1' import { ArchonContentV1Module } from './archon/content/v1' @@ -81,6 +82,7 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule */ export const MODULE_REGISTRY = { archon_actions_v1: ArchonActionsV1Module, + archon_backup_repositories_internal: ArchonBackupRepositoriesInternalModule, archon_backups_queue_v1: ArchonBackupsQueueV1Module, archon_backups_v1: ArchonBackupsV1Module, archon_content_v1: ArchonContentV1Module, diff --git a/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue new file mode 100644 index 00000000000..038ca372554 --- /dev/null +++ b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue @@ -0,0 +1,773 @@ + + + diff --git a/packages/ui/src/layouts/shared/server-settings/pages/support.vue b/packages/ui/src/layouts/shared/server-settings/pages/support.vue index 68e686c2bd4..5f3dc4d08bd 100644 --- a/packages/ui/src/layouts/shared/server-settings/pages/support.vue +++ b/packages/ui/src/layouts/shared/server-settings/pages/support.vue @@ -70,6 +70,8 @@ + + @@ -84,6 +86,7 @@ import LockServerModal from '#ui/components/servers/LockServerModal.vue' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { useModrinthServersConsole } from '#ui/composables/server-console' import { useServerPermissions } from '#ui/composables/server-permissions' +import BackupRepositoryReparation from '#ui/layouts/shared/server-settings/components/backup-repository-reparation.vue' import { injectServerSettings } from '#ui/layouts/shared/server-settings/providers/server-settings' import { injectModrinthClient, diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index 0320cdd2f49..74fd3a80cc2 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -6104,6 +6104,177 @@ "servers.settings.support.clear-action-log-title": { "defaultMessage": "Clear action log?" }, + "servers.settings.support.reparation.actions-title": { + "defaultMessage": "Repairs" + }, + "servers.settings.support.reparation.active-operation": { + "defaultMessage": "{operation} is {status}" + }, + "servers.settings.support.reparation.active-operation-description": { + "defaultMessage": "Other operations can start once it finishes. This page refreshes automatically." + }, + "servers.settings.support.reparation.already-ran": { + "defaultMessage": "Already started in this run. Run a new check to repeat it" + }, + "servers.settings.support.reparation.broken-pack-count": { + "defaultMessage": "Broken packs" + }, + "servers.settings.support.reparation.check-required": { + "defaultMessage": "Requires a completed check" + }, + "servers.settings.support.reparation.check-results": { + "defaultMessage": "Check results" + }, + "servers.settings.support.reparation.check-title": { + "defaultMessage": "Check" + }, + "servers.settings.support.reparation.confirm-description": { + "defaultMessage": "Run {operation} on repository {id}? Backups of the repository are paused until it finishes." + }, + "servers.settings.support.reparation.confirm-prune-description": { + "defaultMessage": "Prune repository {id}? Data that isn't referenced by a snapshot is permanently deleted." + }, + "servers.settings.support.reparation.confirm-restore-description": { + "defaultMessage": "Copy every retained version of repository {id}'s objects into a new repository and publish its snapshots as restored backups? The new repository becomes the world's backup target. This can take hours." + }, + "servers.settings.support.reparation.description": { + "defaultMessage": "Check a backup repository for damage and repair it. Backups of the repository are paused while an operation runs." + }, + "servers.settings.support.reparation.error-count": { + "defaultMessage": "Errors" + }, + "servers.settings.support.reparation.loading": { + "defaultMessage": "Loading backup repositories…" + }, + "servers.settings.support.reparation.missing-pack-count": { + "defaultMessage": "Missing pack errors" + }, + "servers.settings.support.reparation.no-broken-packs": { + "defaultMessage": "The check found no broken packs" + }, + "servers.settings.support.reparation.no-repositories": { + "defaultMessage": "This server has no backup repositories." + }, + "servers.settings.support.reparation.operation-running": { + "defaultMessage": "Another operation is running on this repository" + }, + "servers.settings.support.reparation.operation.check": { + "defaultMessage": "Check" + }, + "servers.settings.support.reparation.operation.check-read-data": { + "defaultMessage": "Check with data" + }, + "servers.settings.support.reparation.operation.prune": { + "defaultMessage": "Prune" + }, + "servers.settings.support.reparation.operation.repair-index": { + "defaultMessage": "Repair index" + }, + "servers.settings.support.reparation.operation.repair-packs": { + "defaultMessage": "Repair packs" + }, + "servers.settings.support.reparation.operation.repair-snapshots": { + "defaultMessage": "Repair snapshots" + }, + "servers.settings.support.reparation.operation.restore-hidden-versions": { + "defaultMessage": "Restore hidden versions" + }, + "servers.settings.support.reparation.prune-requirement": { + "defaultMessage": "Requires the check to suggest pruning, or a completed snapshot repair in this run" + }, + "servers.settings.support.reparation.raw-results": { + "defaultMessage": "Raw results" + }, + "servers.settings.support.reparation.read-data-description": { + "defaultMessage": "Not recommended. Downloads and verifies every pack, which can take hours on large repositories. Only use it when a regular check passes but restores still fail." + }, + "servers.settings.support.reparation.read-data-label": { + "defaultMessage": "Read all pack data" + }, + "servers.settings.support.reparation.recommend-healthy": { + "defaultMessage": "None, the repository is healthy." + }, + "servers.settings.support.reparation.recommend-prune": { + "defaultMessage": "Prune to remove unused data." + }, + "servers.settings.support.reparation.recommend-repair-index": { + "defaultMessage": "Repair the index, then repair snapshots." + }, + "servers.settings.support.reparation.recommend-repair-packs": { + "defaultMessage": "Repair packs, then repair snapshots." + }, + "servers.settings.support.reparation.recommend-restore-hidden-versions": { + "defaultMessage": "Packs are missing. If the bucket keeps old object versions, restore hidden versions. Otherwise, repair the index, then repair snapshots." + }, + "servers.settings.support.reparation.recommend-review-errors": { + "defaultMessage": "The errors don't match a known repair. Review them below." + }, + "servers.settings.support.reparation.recommended-action": { + "defaultMessage": "Recommended action" + }, + "servers.settings.support.reparation.repair-snapshots-requirement": { + "defaultMessage": "Requires a completed index or pack repair in this run" + }, + "servers.settings.support.reparation.repository-label": { + "defaultMessage": "Repository {id}" + }, + "servers.settings.support.reparation.run-title": { + "defaultMessage": "Run {id}, started {time}" + }, + "servers.settings.support.reparation.show-errors": { + "defaultMessage": "Show {count, plural, one {# error} other {# errors}}" + }, + "servers.settings.support.reparation.show-errors-truncated": { + "defaultMessage": "Show the first {count, plural, one {# error} other {# errors}}" + }, + "servers.settings.support.reparation.start-check": { + "defaultMessage": "Run check" + }, + "servers.settings.support.reparation.start-error": { + "defaultMessage": "Failed to start the operation" + }, + "servers.settings.support.reparation.start-new-check": { + "defaultMessage": "Run new check" + }, + "servers.settings.support.reparation.status.completed": { + "defaultMessage": "completed" + }, + "servers.settings.support.reparation.status.failed": { + "defaultMessage": "failed" + }, + "servers.settings.support.reparation.status.ongoing": { + "defaultMessage": "running" + }, + "servers.settings.support.reparation.status.pending": { + "defaultMessage": "pending" + }, + "servers.settings.support.reparation.summary-check": { + "defaultMessage": "{errors, plural, one {# error} other {# errors}}, {packs, plural, one {# broken pack} other {# broken packs}}" + }, + "servers.settings.support.reparation.summary-command": { + "defaultMessage": "Restic exited with code {code}" + }, + "servers.settings.support.reparation.summary-repair-snapshots": { + "defaultMessage": "{remapped, plural, one {# backup} other {# backups}} moved to repaired snapshots, {missing, plural, one {# backup} other {# backups}} missing their snapshot" + }, + "servers.settings.support.reparation.summary-restore-done": { + "defaultMessage": "{imported, plural, one {# backup} other {# backups}} restored into repository {id} ({matched} matched to existing backups). {copied, plural, one {# object} other {# objects}} copied, {missing, plural, one {# object} other {# objects}} missing." + }, + "servers.settings.support.reparation.summary-restore-running": { + "defaultMessage": "Restoring into repository {id}" + }, + "servers.settings.support.reparation.timing-completed": { + "defaultMessage": "finished {time}" + }, + "servers.settings.support.reparation.timing-created": { + "defaultMessage": "queued {time}" + }, + "servers.settings.support.reparation.timing-started": { + "defaultMessage": "started {time}" + }, + "servers.settings.support.reparation.title": { + "defaultMessage": "Backup repository reparation" + }, "servers.setup.onboarding.installation-failed.text": { "defaultMessage": "An unexpected error occurred while installing. Please try again later." }, From 3a2b21f24c07275456ba16a48975fa07ae3a8820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Mon, 26 Oct 2026 16:16:12 -0400 Subject: [PATCH 2/2] chore: remove intl, specify (very slow) --- .../backup-repository-reparation.vue | 418 ++++-------------- packages/ui/src/locales/en-US/index.json | 171 ------- 2 files changed, 76 insertions(+), 513 deletions(-) diff --git a/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue index 038ca372554..41e0e701641 100644 --- a/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue +++ b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue @@ -15,15 +15,18 @@
- {{ formatMessage(messages.title) }} - {{ formatMessage(messages.description) }} + Backup repository reparation + + Check a backup repository for damage and repair it. Backups of the repository are paused + while an operation runs. +
- {{ formatMessage(messages.loading) }} + Loading backup repositories… - {{ formatMessage(messages.noRepositories) }} + This server has no backup repositories.