feat: content tab rewrite for worlds (#5136)

* feat: base content card component

* fix: tooltips + colors

* feat: fix orgs

* feat: base content tab internals rewrite

* feat: fix invalidmodal

* feat: add ContentModpackCard

* fix: extract types

* draft: layout

* feat: unlink modal

* feat: impl content tab

* fix: lint

* fix: toggling

* temp: disable updating stuff

* feat: selection v-model

* feat: bulk selection

* feat: mods tab rough draft

* feat: use fuse.js

* feat: add project combobox

* clean up project combobox

* feat: start install to play modal

* fix: events

* feat: use v-on

* feat: bulk actions + fix floating action bar width

* feat: figma alignments

* feat: migrate toggle to tailwind

* fix: row borders

* feat: disabled state

* feat: virtual list impl for card table based on window scroll

* fix: lint

* feat: virtualization + smaller contentcard items

* feat: use ContentCardTable + ContentCardItems

* feat: fix gap + border issues on last elm

* feat: cleanup + use proper searching

* fix: use TeleportOverflowMenu

* fix: fallback to svg if src is invalid on avatar component

* fix: storybook

* feat: start on updater modal

* feat: finish content updater modal

* feat: i18n pass

* feat: impl modal

* feat(app): backend changes for content tab refactor (#5237)

* feat: include_changelog=false for updater modal

* fix: hash overrides

* feat: update checking for modpack

* feat: qa

* feat: modpack content modal

* fix: padding in table to match modals + tightness

* fix: lint

* feat: delete modal

* feat: fix toggle bugs

* fix: prepr

* fix: duplicate messages

* qa: full width search

* qa: use bg-surface-1.5

* qa: animation for filter pills

* qa: standardize hover colors

* fix: border-[1px] is border

* qa: mass de-select actually mass selecting

* qa: match figma designs for floating action bar

* qa: modal fixes

* q: modal fixes x2

* fix: table border

* qa: confirm modals

* qa: modal alignment

* qa: re-add stuck heading + dedupe logic

* qa: dedupe virtual scrolling + remove dead components

* qa: responsiveness for content table + link fixes

* qa: version column link, tooltips + lint fixes

* qa: instance busy protections

* fix: installation freeze bug

* chore: remove old mods page

* refactor: deduplicate layout

* chore: delete old content page(s)

* qa

* qa

* qa

* feat: sort btn - to iterate

* fix: ml

* feat: date added

* fix: lint

* fix: formatting.ts removal

* feat: get_dependencies_as_content_items

* qa: final QA changes

* refactor: deduplicate + polish content.rs

* feat: hook up content.vue with v1

* feat: hide v1 content api behind frontend feature flag

* fix: query keys + copy on empty state

* chore: i18n pass

* feat: reimpl unlink + upload endpoint

* feat: use bulk endpoints v1

* fix: lint

* fix: flags

* fix: responsiveness via container queries

* fix: lint

* qa: 1

* qa: fixes

* qa: fix ssr issues with browse content

* qa: header page divider

* qa: modals

* fix: prepr

* fix: issues

* fix: lint

* fix: toggle v1 ff

* qa: 5

* qa: delete modal copy

* feat: creation flow modals (#5383)

* refactor: delete content v0 usages + impl

* feat: qa + fixes

* feat: installing banner using state event

* feat: fix modpack card bugs + filtering issues

* refactor: delete backups v0 api module

* feat: v1 servers GET endpoint

* fix: backups

* feat: swap to kyros upload v1 addon

* fix: use tanstack for loader.vue

* feat: finish install from discovery modal

* qa: bug fixes

* feat: set up installation settings

* fix: lint

* fix: typos

* fix: bugs

* fix: disable inline content

* feat: content tab improvements — upload UX, installation settings, and client-only indicators

   Upload cancellation and navigation guard:
   - Add ConfirmLeaveModal that prompts when navigating away during upload
   - Cancel in-flight XHR uploads when user confirms leaving the page
   - Add beforeunload handler to warn on browser/tab close during upload
   - Track uploadedBytes/totalBytes in UploadState for progress display
   - Replace Collapsible with Transition for upload progress admonition
   - Show byte progress and percentage in upload banner
   - Clamp upload progress to prevent exceeding 100%

   Installation settings (server.properties):
   - Add KnownPropertiesFields and PropertiesFields types to Archon types
   - Add buildProperties() to creation flow context to collect gamemode,
     difficulty, seed, world type, structures, and generator settings
   - Pass properties through installContent on onboarding, discovery, and
     ServerSetupModal flows

   Server setup and discovery flow improvements:
   - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent
   - Replace loaderApiNames lookup with toApiLoader() helper
   - Remove eraseDataOnInstall toggle — always use soft_override: false
   - Simplify modpack install on discovery page to use first available version
     and route through creation flow modal for both onboarding and non-onboarding
   - Differentiate post-install navigation: content page for onboarding,
     loader options for existing servers

   Modpack update flow:
   - Replace updateModpack() call with installContent() using soft_override: true
     to support version selection in the content updater modal

   Client-only mod indicators:
   - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment)
   - Add environment to ContentItem and isClientOnly to ContentCardTableItem
   - Show orange TriangleAlertIcon with tooltip on client-only mods in content table
   - Add "Client-only" filter pill to content filtering (controlled via
     showClientOnlyFilter on ContentManagerContext)
   - Apply client-only indicators in both ContentPageLayout and ModpackContentModal

   Misc:
   - Add CLAUDE.md note about using prepr commands for lint checks
   - Export ConfirmLeaveModal from instances barrel

* fix: piping

* fix: switch content disable for linked server instances

* feat: client only filter

* fix: prepr

* feat: hasUpdate shape update

* feat: bulk update endpoint impl for content in panel

* feat: websocket state impl again with new phases

* fix: ws

* fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug

* fix: qa bugs

* fix: lint, a11y and i18n

* refactor: set up layouts folder properly

* fix: linked data cache stuff + lint

* feat: move installationsettings to shared layout

* fix: lint

* fix: issues

* feat: temp fuck staging up

* fix: lockfile

* fix: data sync issues on loader.vue

* fix: lint

* Hide shader configuration files from content list (#5499)

* feat: workaround search problem + split out reset

* fix: qa

* fix: changelog not showing on first open

* fix: qa + optimistic updating improvements

* fix: prepr+lint

* fix: qa

* feat: qa

* fix: lint

* fix: lint

* fix: build

* fix: build

* fix: type errors

* fix: fade and JAVA_HOME passthrough

* feat: qa

* feat: impl diff shit

* fix: qa

* fix: app qa

* feat: update diff modal

* fix: endpoint

* fix: qa

* fix: qa

* fix: use bulk in modpack modal

* feat: abort signal impl + fix issues

* fix: diff modal trunc

* feat: qa

* fix: qa

* feat: tooltip content tab

* fix: prepr

* fix: dismiss on settings btn

* feat: qa

* feat: dont clear handlers on disconnect

* fix: lint

* fix: wrangler + introduce staging-archon env file

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-03-12 20:24:32 +00:00
committed by GitHub
parent f0224dfff7
commit 7d92e4ec7f
302 changed files with 20016 additions and 12142 deletions

View File

@@ -1,77 +0,0 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonBackupsV0Module extends AbstractModule {
public getModuleID(): string {
return 'archon_backups_v0'
}
/** GET /modrinth/v0/servers/:server_id/backups */
public async list(serverId: string): Promise<Archon.Backups.v1.Backup[]> {
return this.client.request<Archon.Backups.v1.Backup[]>(`/servers/${serverId}/backups`, {
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
})
}
/** GET /modrinth/v0/servers/:server_id/backups/:backup_id */
public async get(serverId: string, backupId: string): Promise<Archon.Backups.v1.Backup> {
return this.client.request<Archon.Backups.v1.Backup>(
`/servers/${serverId}/backups/${backupId}`,
{ api: 'archon', version: 'modrinth/v0', method: 'GET' },
)
}
/** POST /modrinth/v0/servers/:server_id/backups */
public async create(
serverId: string,
request: Archon.Backups.v1.BackupRequest,
): Promise<Archon.Backups.v1.PostBackupResponse> {
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
`/servers/${serverId}/backups`,
{ api: 'archon', version: 'modrinth/v0', method: 'POST', body: request },
)
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/restore */
public async restore(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/restore`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** DELETE /modrinth/v0/servers/:server_id/backups/:backup_id */
public async delete(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
api: 'archon',
version: 'modrinth/v0',
method: 'DELETE',
})
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/retry */
public async retry(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/retry`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** PATCH /modrinth/v0/servers/:server_id/backups/:backup_id */
public async rename(
serverId: string,
backupId: string,
request: Archon.Backups.v1.PatchBackup,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
api: 'archon',
version: 'modrinth/v0',
method: 'PATCH',
body: request,
})
}
}

View File

@@ -1,102 +1,84 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
/**
* Default world ID - Uuid::nil() which the backend treats as "first/active world"
* See: apps/archon/src/routes/v1/servers/worlds/mod.rs - world_id_nullish()
* TODO:
* - Make sure world ID is being passed before we ship worlds.
* - The schema will change when Backups v4 (routes stay as v1) so remember to do that.
*/
const DEFAULT_WORLD_ID: string = '00000000-0000-0000-0000-000000000000' as const
export class ArchonBackupsV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_backups_v1'
}
/** GET /v1/:server_id/worlds/:world_id/backups */
public async list(
serverId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.Backup[]> {
/** GET /v1/servers/:server_id/worlds/:world_id/backups */
public async list(serverId: string, worldId: string): Promise<Archon.Backups.v1.Backup[]> {
return this.client.request<Archon.Backups.v1.Backup[]>(
`/${serverId}/worlds/${worldId}/backups`,
`/servers/${serverId}/worlds/${worldId}/backups`,
{ api: 'archon', version: 1, method: 'GET' },
)
}
/** GET /v1/:server_id/worlds/:world_id/backups/:backup_id */
/** GET /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
public async get(
serverId: string,
worldId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.Backup> {
return this.client.request<Archon.Backups.v1.Backup>(
`/${serverId}/worlds/${worldId}/backups/${backupId}`,
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`,
{ api: 'archon', version: 1, method: 'GET' },
)
}
/** POST /v1/:server_id/worlds/:world_id/backups */
/** POST /v1/servers/:server_id/worlds/:world_id/backups */
public async create(
serverId: string,
worldId: string,
request: Archon.Backups.v1.BackupRequest,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.PostBackupResponse> {
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
`/${serverId}/worlds/${worldId}/backups`,
`/servers/${serverId}/worlds/${worldId}/backups`,
{ api: 'archon', version: 1, method: 'POST', body: request },
)
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/restore */
public async restore(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/restore`, {
api: 'archon',
version: 1,
method: 'POST',
})
/** POST /v1/servers/:server_id/worlds/:world_id/backups/:backup_id/restore */
public async restore(serverId: string, worldId: string, backupId: string): Promise<void> {
await this.client.request<void>(
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}/restore`,
{
api: 'archon',
version: 1,
method: 'POST',
},
)
}
/** DELETE /v1/:server_id/worlds/:world_id/backups/:backup_id */
public async delete(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
/** DELETE /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
public async delete(serverId: string, worldId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`, {
api: 'archon',
version: 1,
method: 'DELETE',
})
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/retry */
public async retry(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/retry`, {
api: 'archon',
version: 1,
method: 'POST',
})
/** POST /v1/servers/:server_id/worlds/:world_id/backups/:backup_id/retry */
public async retry(serverId: string, worldId: string, backupId: string): Promise<void> {
await this.client.request<void>(
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}/retry`,
{
api: 'archon',
version: 1,
method: 'POST',
},
)
}
/** PATCH /v1/:server_id/worlds/:world_id/backups/:backup_id */
/** PATCH /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
public async rename(
serverId: string,
worldId: string,
backupId: string,
request: Archon.Backups.v1.PatchBackup,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`, {
api: 'archon',
version: 1,
method: 'PATCH',

View File

@@ -1,56 +0,0 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonContentV0Module extends AbstractModule {
public getModuleID(): string {
return 'archon_content_v0'
}
/** GET /modrinth/v0/servers/:server_id/mods */
public async list(serverId: string): Promise<Archon.Content.v0.Mod[]> {
return this.client.request<Archon.Content.v0.Mod[]>(`/servers/${serverId}/mods`, {
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
})
}
/** POST /modrinth/v0/servers/:server_id/mods */
public async install(
serverId: string,
request: Archon.Content.v0.InstallModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/mods`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
/** POST /modrinth/v0/servers/:server_id/deleteMod */
public async delete(
serverId: string,
request: Archon.Content.v0.DeleteModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/deleteMod`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
/** POST /modrinth/v0/servers/:server_id/mods/update */
public async update(
serverId: string,
request: Archon.Content.v0.UpdateModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/mods/update`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
}

View File

@@ -0,0 +1,276 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonContentV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_content_v1'
}
/** GET /v1/:server_id/worlds/:world_id/addons */
public async getAddons(
serverId: string,
worldId: string,
options?: {
from_modpack?: boolean
disabled?: boolean
addons?: boolean
updates?: boolean
},
): Promise<Archon.Content.v1.Addons> {
const params = new URLSearchParams()
if (options?.from_modpack !== undefined)
params.set('from_modpack', String(options.from_modpack))
if (options?.disabled !== undefined) params.set('disabled', String(options.disabled))
if (options?.addons !== undefined) params.set('addons', String(options.addons))
if (options?.updates !== undefined) params.set('updates', String(options.updates))
const query = params.toString()
return this.client.request<Archon.Content.v1.Addons>(
`/servers/${serverId}/worlds/${worldId}/addons${query ? `?${query}` : ''}`,
{
api: 'archon',
version: 1,
method: 'GET',
},
)
}
/** POST /v1/:server_id/worlds/:world_id/addons */
public async addAddon(
serverId: string,
worldId: string,
request: Archon.Content.v1.AddAddonRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/delete */
public async deleteAddon(
serverId: string,
worldId: string,
request: Archon.Content.v1.RemoveAddonRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/delete`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/disable */
public async disableAddon(
serverId: string,
worldId: string,
request: Archon.Content.v1.RemoveAddonRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/disable`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/enable */
public async enableAddon(
serverId: string,
worldId: string,
request: Archon.Content.v1.RemoveAddonRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/enable`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/delete-many */
public async deleteAddons(
serverId: string,
worldId: string,
items: Archon.Content.v1.RemoveAddonRequest[],
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/delete-many`, {
api: 'archon',
version: 1,
method: 'POST',
body: { items },
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/disable-many */
public async disableAddons(
serverId: string,
worldId: string,
items: Archon.Content.v1.RemoveAddonRequest[],
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/disable-many`, {
api: 'archon',
version: 1,
method: 'POST',
body: { items },
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/enable-many */
public async enableAddons(
serverId: string,
worldId: string,
items: Archon.Content.v1.RemoveAddonRequest[],
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/enable-many`, {
api: 'archon',
version: 1,
method: 'POST',
body: { items },
})
}
/** POST /v1/:server_id/worlds/:world_id/content */
public async installContent(
serverId: string,
worldId: string,
request: Archon.Content.v1.InstallWorldContent,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/content`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/content/repair */
public async repair(serverId: string, worldId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/content/repair`, {
api: 'archon',
version: 1,
method: 'POST',
})
}
/** POST /v1/:server_id/worlds/:world_id/content/unlink-modpack */
public async unlinkModpack(serverId: string, worldId: string): Promise<void> {
await this.client.request<void>(
`/servers/${serverId}/worlds/${worldId}/content/unlink-modpack`,
{
api: 'archon',
version: 1,
method: 'POST',
},
)
}
/** GET /v1/:server_id/worlds/:world_id/addons/update?filename=... */
public async getAddonUpdate(
serverId: string,
worldId: string,
filename: string,
): Promise<Archon.Content.v1.Addon> {
return this.client.request<Archon.Content.v1.Addon>(
`/servers/${serverId}/worlds/${worldId}/addons/update?filename=${encodeURIComponent(filename)}`,
{
api: 'archon',
version: 1,
method: 'GET',
},
)
}
/** POST /v1/:server_id/worlds/:world_id/addons/update */
public async updateAddon(
serverId: string,
worldId: string,
request: Archon.Content.v1.UpdateAddonRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/update`, {
api: 'archon',
version: 1,
method: 'POST',
body: request,
})
}
/** POST /v1/:server_id/worlds/:world_id/addons/update-many */
public async updateAddons(
serverId: string,
worldId: string,
addons: Archon.Content.v1.UpdateAddonRequest[],
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/update-many`, {
api: 'archon',
version: 1,
method: 'POST',
body: { addons },
})
}
/** GET /v1/:server_id/worlds/:world_id/content/modpack/update */
public async getModpackUpdate(
serverId: string,
worldId: string,
): Promise<Archon.Content.v1.ModpackFields> {
return this.client.request<Archon.Content.v1.ModpackFields>(
`/servers/${serverId}/worlds/${worldId}/content/modpack/update`,
{
api: 'archon',
version: 1,
method: 'GET',
},
)
}
/** POST /v1/:server_id/worlds/:world_id/content/modpack/update */
public async updateModpack(serverId: string, worldId: string): Promise<void> {
await this.client.request<void>(
`/servers/${serverId}/worlds/${worldId}/content/modpack/update`,
{
api: 'archon',
version: 1,
method: 'POST',
},
)
}
/** GET /v1/:server_id/worlds/:world_id/content/update-game-version?game_version=... */
public async getUpdateGameVersionPreview(
serverId: string,
worldId: string,
gameVersion: string,
signal?: AbortSignal,
): Promise<Archon.Content.v1.UpdateGameVersionPreview> {
return this.client.request<Archon.Content.v1.UpdateGameVersionPreview>(
`/servers/${serverId}/worlds/${worldId}/content/update-game-version?game_version=${encodeURIComponent(gameVersion)}`,
{
api: 'archon',
version: 1,
method: 'GET',
timeout: 1000 * 1000,
signal,
},
)
}
/** POST /v1/:server_id/worlds/:world_id/content/update-game-version?game_version=... */
public async applyGameVersionUpdate(
serverId: string,
worldId: string,
gameVersion: string,
): Promise<void> {
await this.client.request<void>(
`/servers/${serverId}/worlds/${worldId}/content/update-game-version?game_version=${encodeURIComponent(gameVersion)}`,
{
api: 'archon',
version: 1,
method: 'POST',
},
)
}
}

View File

@@ -1,6 +1,6 @@
export * from './backups/v0'
export * from './backups/v1'
export * from './content/v0'
export * from './content/v1'
export * from './properties/v1'
export * from './servers/v0'
export * from './servers/v1'
export * from './types'

View File

@@ -0,0 +1,37 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonOptionsV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_options_v1'
}
/** GET /v1/servers/:server_id/worlds/:world_id/options/startup */
public async getStartup(
serverId: string,
worldId: string,
): Promise<Archon.Content.v1.RuntimeOptions> {
return this.client.request<Archon.Content.v1.RuntimeOptions>(
`/servers/${serverId}/worlds/${worldId}/options/startup`,
{
api: 'archon',
version: 1,
method: 'GET',
},
)
}
/** PATCH /v1/servers/:server_id/worlds/:world_id/options/startup */
public async patchStartup(
serverId: string,
worldId: string,
body: Archon.Content.v1.PatchRuntimeOptions,
): Promise<void> {
await this.client.request(`/servers/${serverId}/worlds/${worldId}/options/startup`, {
api: 'archon',
version: 1,
method: 'PATCH',
body,
})
}
}

View File

@@ -0,0 +1,40 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonPropertiesV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_properties_v1'
}
/** GET /v1/servers/:server_id/worlds/:world_id/properties */
public async getProperties(
serverId: string,
worldId: string,
): Promise<Archon.Content.v1.PropertiesFields> {
return this.client.request<Archon.Content.v1.PropertiesFields>(
`/servers/${serverId}/worlds/${worldId}/properties`,
{
api: 'archon',
version: 1,
method: 'GET',
},
)
}
/** PATCH /v1/servers/:server_id/worlds/:world_id/properties */
public async patchProperties(
serverId: string,
worldId: string,
body: Archon.Content.v1.PatchPropertiesFields,
): Promise<Archon.Content.v1.PropertiesFields> {
return this.client.request<Archon.Content.v1.PropertiesFields>(
`/servers/${serverId}/worlds/${worldId}/properties`,
{
api: 'archon',
version: 1,
method: 'PATCH',
body,
},
)
}
}

View File

@@ -1,4 +1,5 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { UploadHandle, UploadProgress } from '../../../types/upload'
import type { Archon } from '../types'
export class ArchonServersV0Module extends AbstractModule {
@@ -94,4 +95,210 @@ export class ArchonServersV0Module extends AbstractModule {
body: { action },
})
}
/**
* Reinstall a server with a new loader or modpack
* POST /modrinth/v0/servers/:id/reinstall
*/
public async reinstall(
serverId: string,
request: Archon.Servers.v0.ReinstallRequest,
hardReset: boolean = false,
): Promise<void> {
await this.client.request(`/servers/${serverId}/reinstall`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
params: { hard: String(hardReset) },
body: request,
})
}
/**
* Get authentication credentials for .mrpack file upload
* GET /modrinth/v0/servers/:id/reinstallFromMrpack
*/
public async getReinstallMrpackAuth(
serverId: string,
): Promise<Archon.Servers.v0.MrpackReinstallAuth> {
return this.client.request<Archon.Servers.v0.MrpackReinstallAuth>(
`/servers/${serverId}/reinstallFromMrpack`,
{
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
},
)
}
/**
* Reinstall a server from a .mrpack file with progress tracking
*
* Two-step flow: fetches upload auth, then uploads the .mrpack file to the node.
*
* @param serverId - Server ID
* @param file - .mrpack file to upload
* @param hardReset - Whether to erase all server data
* @param options - Optional progress callback
* @returns Promise resolving to an UploadHandle with progress tracking and cancellation
*/
public async reinstallFromMrpack(
serverId: string,
file: File,
hardReset: boolean = false,
options?: {
onProgress?: (progress: UploadProgress) => void
},
): Promise<UploadHandle<void>> {
const auth = await this.getReinstallMrpackAuth(serverId)
const formData = new FormData()
formData.append('file', file)
return this.client.upload<void>('', {
api: `https://${auth.url}`,
version: 'reinstallMrpackMultiparted',
formData,
params: { hard: String(hardReset) },
headers: { Authorization: `Bearer ${auth.token}` },
skipAuth: true,
onProgress: options?.onProgress,
retry: false,
})
}
/**
* Update a server's name
* POST /modrinth/v0/servers/:id/name
*/
public async updateName(serverId: string, name: string): Promise<void> {
await this.client.request(`/servers/${serverId}/name`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
body: { name },
})
}
/**
* Get allocations for a server
* GET /modrinth/v0/servers/:id/allocations
*/
public async getAllocations(serverId: string): Promise<Archon.Servers.v0.Allocation[]> {
return this.client.request<Archon.Servers.v0.Allocation[]>(`/servers/${serverId}/allocations`, {
api: 'archon',
method: 'GET',
version: 'modrinth/v0',
})
}
/**
* Reserve a new allocation for a server
* POST /modrinth/v0/servers/:id/allocations?name=...
*/
public async reserveAllocation(
serverId: string,
name: string,
): Promise<Archon.Servers.v0.Allocation> {
return this.client.request<Archon.Servers.v0.Allocation>(`/servers/${serverId}/allocations`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
params: { name },
})
}
/**
* Update an allocation's name
* PUT /modrinth/v0/servers/:id/allocations/:port?name=...
*/
public async updateAllocation(serverId: string, port: number, name: string): Promise<void> {
await this.client.request(`/servers/${serverId}/allocations/${port}`, {
api: 'archon',
method: 'PUT',
version: 'modrinth/v0',
params: { name },
})
}
/**
* Delete an allocation
* DELETE /modrinth/v0/servers/:id/allocations/:port
*/
public async deleteAllocation(serverId: string, port: number): Promise<void> {
await this.client.request(`/servers/${serverId}/allocations/${port}`, {
api: 'archon',
method: 'DELETE',
version: 'modrinth/v0',
})
}
/**
* Check if a subdomain is available
* GET /modrinth/v0/subdomains/:subdomain/isavailable
*/
public async checkSubdomainAvailability(subdomain: string): Promise<{ available: boolean }> {
return this.client.request<{ available: boolean }>(`/subdomains/${subdomain}/isavailable`, {
api: 'archon',
method: 'GET',
version: 'modrinth/v0',
})
}
/**
* Change a server's subdomain
* POST /modrinth/v0/servers/:id/subdomain
*/
public async changeSubdomain(serverId: string, subdomain: string): Promise<void> {
await this.client.request(`/servers/${serverId}/subdomain`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
body: { subdomain },
})
}
/**
* Get startup configuration for a server
* GET /modrinth/v0/servers/:id/startup
*/
public async getStartupConfig(serverId: string): Promise<Archon.Servers.v0.StartupConfig> {
return this.client.request<Archon.Servers.v0.StartupConfig>(`/servers/${serverId}/startup`, {
api: 'archon',
method: 'GET',
version: 'modrinth/v0',
})
}
/**
* Update startup configuration for a server
* POST /modrinth/v0/servers/:id/startup
*/
public async updateStartupConfig(
serverId: string,
config: {
invocation: string | null
jdk_version: string | null
jdk_build: string | null
},
): Promise<void> {
await this.client.request(`/servers/${serverId}/startup`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
body: config,
})
}
/**
* Dismiss a server notice
* POST /modrinth/v0/servers/:id/notices/:noticeId/dismiss
*/
public async dismissNotice(serverId: string, noticeId: number): Promise<void> {
await this.client.request(`/servers/${serverId}/notices/${noticeId}/dismiss`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
})
}
}

View File

@@ -6,6 +6,30 @@ export class ArchonServersV1Module extends AbstractModule {
return 'archon_servers_v1'
}
/**
* Get list of servers for the authenticated user
* GET /v1/servers
*/
public async list(): Promise<Archon.Servers.v1.ServerFull[]> {
return this.client.request<Archon.Servers.v1.ServerFull[]>('/servers', {
api: 'archon',
version: 1,
method: 'GET',
})
}
/**
* Get full server details including worlds, backups, and content
* GET /v1/servers/:server_id
*/
public async get(serverId: string): Promise<Archon.Servers.v1.ServerFull> {
return this.client.request<Archon.Servers.v1.ServerFull>(`/servers/${serverId}`, {
api: 'archon',
version: 1,
method: 'GET',
})
}
/**
* Get available regions
* GET /v1/regions
@@ -17,4 +41,16 @@ export class ArchonServersV1Module extends AbstractModule {
method: 'GET',
})
}
/**
* End the intro flow for a server
* DELETE /v1/servers/:id/flows/intro
*/
public async endIntro(serverId: string): Promise<void> {
await this.client.request(`/servers/${serverId}/flows/intro`, {
api: 'archon',
version: 1,
method: 'DELETE',
})
}
}

View File

@@ -1,37 +1,211 @@
import type { Labrinth } from '../labrinth/types'
export namespace Archon {
export namespace Content {
export namespace v0 {
export type ContentKind = 'mod' | 'plugin'
export namespace v1 {
export type AddonKind = 'mod' | 'plugin' | 'datapack' | 'shader' | 'resourcepack'
export type Mod = {
export type ContentOwnerType = 'user' | 'organization'
export type ContentOwner = {
id: string
name: string
type: ContentOwnerType
icon_url: string | null
}
export type AddonVersion = {
id: string
name: string | null
environment?: Labrinth.Projects.v3.Environment | null
}
export type Addon = {
id: string
filename: string
project_id: string | undefined
version_id: string | undefined
name: string | undefined
version_number: string | undefined
icon_url: string | undefined
owner: string | undefined
filesize: number
disabled: boolean
installing: boolean
kind: AddonKind
from_modpack: boolean
has_update: string | null
name: string | null
project_id: string | null
version: AddonVersion | null
owner: ContentOwner | null
icon_url: string | null
}
export type InstallModRequest = {
rinth_ids: {
project_id: string
version_id: string
}
install_as: ContentKind
export type Addons = {
modloader: string | null
modloader_version: string | null
game_version: string | null
modpack: ModpackFields | null
addons: Addon[] | null
}
export type DeleteModRequest = {
path: string
export type AddAddonRequest = {
project_id: string
version_id?: string
kind?: AddonKind
}
export type UpdateModRequest = {
replace: string
export type RemoveAddonRequest = {
kind: AddonKind
filename: string
}
export type UpdateAddonRequest = {
filename: string
version_id?: string | null
}
export type Modloader =
| 'forge'
| 'neo_forge'
| 'fabric'
| 'quilt'
| 'paper'
| 'purpur'
| 'vanilla'
export type ModpackSpec = {
platform: 'modrinth'
project_id: string
version_id: string
}
export type ModpackOwner = {
id: string
name: string
type: 'user' | 'organization'
icon_url: string | null
}
export type ModpackFields = {
spec: ModpackSpec
has_update: string | null
title: string | null
description: string | null
icon_url: string | null
owner: ModpackOwner | null
version_number: string | null
date_published: string | null
downloads: number | null
followers: number | null
}
export type KnownPropertiesFields = {
allow_cheats?: string | null
allow_flight?: string | null
difficulty?: string | null
enforce_whitelist?: string | null
force_gamemode?: string | null
gamemode?: string | null
generate_structures?: string | null
generator_settings?: string | null
hardcore?: string | null
level_seed?: string | null
level_type?: string | null
max_players?: string | null
max_tick_time?: string | null
motd?: string | null
pause_when_empty_seconds?: string | null
player_idle_timeout?: string | null
require_resource_pack?: string | null
resource_pack?: string | null
resource_pack_id?: string | null
resource_pack_sha1?: string | null
simulation_distance?: string | null
spawn_protection?: string | null
sync_chunk_writes?: string | null
view_distance?: string | null
white_list?: string | null
}
export type PropertiesFields = {
known: KnownPropertiesFields
custom?: Record<string, string>
}
export type PatchPropertiesFields = {
known?: KnownPropertiesFields
custom?: Record<string, string | null>
}
export type JreVendor = 'temurin' | 'corretto' | 'graal'
export type RuntimeOptions = {
java_version: number | null
jre_vendor: JreVendor | null
original_invocation: string | null
startup_command: string | null
}
export type PatchRuntimeOptions = {
java_version?: number | null
jre_vendor?: JreVendor | null
startup_command?: string | null
}
export type InstallWorldContent =
| {
content_variant: 'modpack'
spec: ModpackSpec
soft_override: boolean
properties?: PropertiesFields | null
}
| {
content_variant: 'bare'
loader: Modloader
version: string
game_version?: string
soft_override: boolean
properties?: PropertiesFields | null
}
export type AddonDiffVersion = {
id: string
version_number: string
}
export type AddonDiffProject = {
id: string
title: string
icon_url: string | null
slug: string
}
export type AddonBaseDiffInfo = {
current_version: AddonDiffVersion | null
new_version: AddonDiffVersion | null
file_name: string | null
project_id: string | null
project: AddonDiffProject | null
}
export type AddonDiffAdded = AddonBaseDiffInfo & {
type: 'added'
new_version_id: string
}
export type AddonDiffRemoved = AddonBaseDiffInfo & {
type: 'removed'
}
export type AddonDiffUpdated = AddonBaseDiffInfo & {
type: 'updated'
current_version_id: string
new_version_id: string
}
export type AddonDiff = AddonDiffAdded | AddonDiffRemoved | AddonDiffUpdated
export type UpdateGameVersionPreview = {
addon_changes: AddonDiff[]
new_game_version: string
new_loader_version: string
has_unknown_content: boolean
}
}
}
@@ -148,9 +322,95 @@ export namespace Archon {
url: string // e.g., "node-xyz.modrinth.com/modrinth/v0/fs"
token: string // JWT token for filesystem access
}
export type ReinstallLoaderRequest = {
loader: string
loader_version?: string
game_version?: string
}
export type ReinstallModpackRequest = {
project_id: string
version_id?: string
}
export type ReinstallRequest = ReinstallLoaderRequest | ReinstallModpackRequest
export type MrpackReinstallAuth = {
url: string
token: string
}
export type Allocation = {
port: number
name: string
}
export type StartupConfig = {
invocation: string
original_invocation: string
jdk_version: 'lts8' | 'lts11' | 'lts17' | 'lts21'
jdk_build: 'corretto' | 'temurin' | 'graal'
}
}
export namespace v1 {
export type ServerFull = {
id: string
name: string
subdomain: string
specs: ServerResources
sftp_username: string
sftp_password: string
tags: string[]
location: ServerLocation
worlds: WorldFull[]
}
export type ServerResources = {
cpu: number
memory_mb: number
storage_mb: number
swap_mb: number
}
export type ServerLocation =
| {
status: 'assigned'
location_metadata: {
region: string
region_should_be_user_displayed: boolean
hostname: string
is_decommissioned_node: boolean
}
}
| {
status: 'unassigned'
}
export type WorldFull = {
id: string
name: string
created_at: string
is_active: boolean
backups: Archon.Backups.v1.Backup[]
content: WorldContentInfo | null
readiness: WorldReadiness
}
export type WorldReadiness = {
data_synchronized_fetched: boolean
}
export type WorldContentInfo = {
modloader: string
modloader_version: string
game_version: string
java_version: number
invocation: string
original_invocation: string
}
export type Region = {
shortcode: string
country_code: string
@@ -174,19 +434,18 @@ export namespace Archon {
export type Backup = {
id: string
physical_id: string
name: string
created_at: string
automated: boolean
interrupted: boolean
ongoing: boolean
locked: boolean
task?: {
file?: BackupTaskProgress
create?: BackupTaskProgress
restore?: BackupTaskProgress
}
// TODO: Uncomment when API supports these fields
// size?: number // bytes
// creator_id?: string // user ID, or 'auto' for automated backups
}
export type BackupRequest = {
@@ -319,6 +578,37 @@ export namespace Archon {
all: FilesystemOperation[]
}
export type ReadinessState =
| 'deprovisioned'
| 'waiting_active_world'
| 'waiting_world_spec_details_for_progress'
| 'pulling_world_data'
| 'migration_zfs'
| 'sync_content'
| 'container_readying'
| 'ready'
export type FlattenedPowerState = 'not_ready' | 'starting' | 'running' | 'stopping' | 'idle'
export type SyncInstallPhase = 'Analyzing' | 'InstallingPack' | 'InstallingLoader' | 'Addons'
export type SyncContentProgress = {
started_at: string
phase: SyncInstallPhase
percent: number
}
export type WSStateEvent = {
event: 'state'
debug: string
power_variant: FlattenedPowerState
exit_code?: number | null
was_oom?: boolean
target: 'start' | 'stop' | 'restart' | null
uptime: number
progress: SyncContentProgress | null
}
// Outgoing messages (client -> server)
export type WSOutgoingMessage = WSAuthMessage | WSCommandMessage
@@ -337,6 +627,7 @@ export namespace Archon {
| WSLogEvent
| WSStatsEvent
| WSPowerStateEvent
| WSStateEvent
| WSAuthExpiringEvent
| WSAuthIncorrectEvent
| WSAuthOkEvent