diff --git a/backend-nest/src/modules/servers/servers.controller.ts b/backend-nest/src/modules/servers/servers.controller.ts index 0100d77..457aedd 100644 --- a/backend-nest/src/modules/servers/servers.controller.ts +++ b/backend-nest/src/modules/servers/servers.controller.ts @@ -73,4 +73,11 @@ export class ServersController { ) { return await this.serversService.deployServer(licenseId, dto); } + + @Post('install-oxide') + @RequirePermission('server.manage') + @ApiOperation({ summary: 'Install Oxide/uMod via companion agent' }) + async installOxide(@CurrentTenant() licenseId: string) { + return await this.serversService.installOxide(licenseId); + } } diff --git a/backend-nest/src/modules/servers/servers.service.ts b/backend-nest/src/modules/servers/servers.service.ts index 5643120..da20754 100644 --- a/backend-nest/src/modules/servers/servers.service.ts +++ b/backend-nest/src/modules/servers/servers.service.ts @@ -103,4 +103,12 @@ export class ServersService { await this.natsService.sendDeployCommand(licenseId, { ...dto }); return { message: 'Deployment started' }; } + + /** + * Install Oxide/uMod via companion agent + */ + async installOxide(licenseId: string) { + await this.natsService.sendOxideInstallCommand(licenseId); + return { message: 'Oxide installation started' }; + } } diff --git a/backend-nest/src/services/nats-bridge.service.ts b/backend-nest/src/services/nats-bridge.service.ts index babfe78..801c471 100644 --- a/backend-nest/src/services/nats-bridge.service.ts +++ b/backend-nest/src/services/nats-bridge.service.ts @@ -39,6 +39,11 @@ export class NatsBridgeService implements OnModuleInit { this.emit(licenseId, 'deploy_status', data); }); + this.nats.subscribe('corrosion.*.oxide.status', (data, subject) => { + const licenseId = subject.split('.')[1]; + this.emit(licenseId, 'oxide_status', data); + }); + this.logger.log('NATS bridge subscriptions initialized'); } diff --git a/backend-nest/src/services/nats.service.ts b/backend-nest/src/services/nats.service.ts index 5c1db54..3f14f14 100644 --- a/backend-nest/src/services/nats.service.ts +++ b/backend-nest/src/services/nats.service.ts @@ -79,4 +79,12 @@ export class NatsService implements OnModuleInit, OnModuleDestroy { timestamp: new Date().toISOString(), }); } + + /** Publish an Oxide install command to a specific license's companion agent */ + async sendOxideInstallCommand(licenseId: string): Promise { + await this.publish(`corrosion.${licenseId}.cmd.oxide`, { + action: 'install_oxide', + timestamp: new Date().toISOString(), + }); + } } diff --git a/frontend/src/stores/server.ts b/frontend/src/stores/server.ts index fec656f..ecf9542 100644 --- a/frontend/src/stores/server.ts +++ b/frontend/src/stores/server.ts @@ -64,6 +64,15 @@ export const useServerStore = defineStore('server', () => { } } + async function installOxide() { + try { + await api.post('/servers/install-oxide') + } catch (e) { + console.error('Failed to start Oxide installation:', e) + throw e + } + } + function updateDeploymentStatus(status: DeploymentStatus) { deploymentStatus.value = status if (status.stage === 'online' || status.stage === 'failed') { @@ -94,6 +103,7 @@ export const useServerStore = defineStore('server', () => { stopServer, restartServer, deployServer, + installOxide, updateDeploymentStatus, clearDeploymentStatus, updateStats, diff --git a/frontend/src/views/admin/ServerView.vue b/frontend/src/views/admin/ServerView.vue index 4e8dbea..388d683 100644 --- a/frontend/src/views/admin/ServerView.vue +++ b/frontend/src/views/admin/ServerView.vue @@ -18,6 +18,7 @@ import { Rocket, AlertTriangle, Check, + Puzzle, } from 'lucide-vue-next' import type { DeploymentConfig, DeploymentStatus } from '@/types' import { useWebSocket } from '@/composables/useWebSocket' @@ -34,6 +35,8 @@ const setupTab = ref<'linux' | 'windows'>('linux') const windowsCopied = ref(false) const showDeployForm = ref(false) const deployLoading = ref(false) +const oxideStatus = ref<{ stage: string; progress: number; message: string; error?: string } | null>(null) +const isInstallingOxide = ref(false) const deployForm = ref({ server_name: 'My Rust Server', @@ -141,6 +144,42 @@ function getStageState(stageKey: string): 'pending' | 'active' | 'complete' | 'f return 'pending' } +const oxideStages = [ + { key: 'fetching_release', label: 'Check Latest Release' }, + { key: 'downloading', label: 'Download Oxide' }, + { key: 'installing', label: 'Extract Files' }, + { key: 'restarting', label: 'Restart Server' }, + { key: 'complete', label: 'Complete' }, +] as const + +function getOxideStageState(stageKey: string): 'pending' | 'active' | 'complete' | 'failed' { + if (!oxideStatus.value) return 'pending' + const status = oxideStatus.value + if (status.stage === 'failed') { + const currentStages = oxideStages + const idx = currentStages.findIndex(s => s.key === stageKey) + // Find which stage was active when failure occurred — approximate from message + // For failed state, mark all stages before current as complete + return idx === 0 ? 'failed' : 'pending' + } + const currentIdx = oxideStages.findIndex(s => s.key === status.stage) + const thisIdx = oxideStages.findIndex(s => s.key === stageKey) + if (thisIdx < currentIdx) return 'complete' + if (thisIdx === currentIdx) return status.stage === 'complete' ? 'complete' : 'active' + return 'pending' +} + +async function installOxide() { + isInstallingOxide.value = true + oxideStatus.value = null + try { + await server.installOxide() + } catch { + toast.error('Failed to start Oxide installation') + isInstallingOxide.value = false + } +} + const form = ref({ server_name: '', max_players: 0, @@ -207,6 +246,12 @@ onMounted(async () => { if (msg.type === 'event' && msg.event === 'deploy_status') { server.updateDeploymentStatus(msg.data as DeploymentStatus) } + if (msg.type === 'event' && msg.event === 'oxide_status') { + oxideStatus.value = msg.data as { stage: string; progress: number; message: string; error?: string } + if (msg.data && (msg.data as any).stage === 'complete' || (msg.data as any).stage === 'failed') { + isInstallingOxide.value = false + } + } }) }) @@ -544,6 +589,82 @@ onMounted(async () => { + +
+
+ +

Install Oxide / uMod

+
+ + +
+
+
+ +
+ + + + +
+ + {{ stage.label }} +
+
+ + +
+

{{ oxideStatus.message }}

+
+ + +
+

{{ oxideStatus.error }}

+
+ + + +
+ + +
+

Install or update Oxide/uMod. Required for all plugins including CorrosionCompanion.

+ +
+
+