feat: Add one-click Oxide/uMod installer — backend + frontend
POST /servers/install-oxide endpoint, NATS bridge for oxide.status, server store installOxide method, ServerView Install Oxide card with progress tracker matching the Deploy card pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<DeploymentConfig>({
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -544,6 +589,82 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install Oxide/uMod -->
|
||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||
<div class="flex items-center gap-2 mb-5">
|
||||
<Puzzle class="w-4 h-4 text-oxide-400" />
|
||||
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Install Oxide / uMod</h2>
|
||||
</div>
|
||||
|
||||
<!-- Installation Progress Tracker -->
|
||||
<div v-if="oxideStatus || isInstallingOxide" class="mb-6">
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="stage in oxideStages"
|
||||
:key="stage.key"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<!-- Stage indicator -->
|
||||
<div class="w-6 h-6 rounded-full flex items-center justify-center shrink-0"
|
||||
:class="{
|
||||
'bg-neutral-800 text-neutral-600': getOxideStageState(stage.key) === 'pending',
|
||||
'bg-amber-500/20 text-amber-400': getOxideStageState(stage.key) === 'active',
|
||||
'bg-green-500/20 text-green-400': getOxideStageState(stage.key) === 'complete',
|
||||
'bg-red-500/20 text-red-400': getOxideStageState(stage.key) === 'failed',
|
||||
}"
|
||||
>
|
||||
<Loader2 v-if="getOxideStageState(stage.key) === 'active'" class="w-3.5 h-3.5 animate-spin" />
|
||||
<Check v-else-if="getOxideStageState(stage.key) === 'complete'" class="w-3.5 h-3.5" />
|
||||
<AlertTriangle v-else-if="getOxideStageState(stage.key) === 'failed'" class="w-3.5 h-3.5" />
|
||||
<span v-else class="w-1.5 h-1.5 rounded-full bg-neutral-600" />
|
||||
</div>
|
||||
<!-- Stage label -->
|
||||
<span
|
||||
class="text-sm"
|
||||
:class="{
|
||||
'text-neutral-600': getOxideStageState(stage.key) === 'pending',
|
||||
'text-amber-300 font-medium': getOxideStageState(stage.key) === 'active',
|
||||
'text-green-400': getOxideStageState(stage.key) === 'complete',
|
||||
'text-red-400': getOxideStageState(stage.key) === 'failed',
|
||||
}"
|
||||
>{{ stage.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status message -->
|
||||
<div v-if="oxideStatus?.message" class="mt-4 px-3 py-2 bg-neutral-800/50 rounded-lg">
|
||||
<p class="text-xs text-neutral-400">{{ oxideStatus.message }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error display -->
|
||||
<div v-if="oxideStatus?.error" class="mt-3 px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg">
|
||||
<p class="text-xs text-red-400">{{ oxideStatus.error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Retry button on failure -->
|
||||
<button
|
||||
v-if="oxideStatus?.stage === 'failed'"
|
||||
@click="oxideStatus = null; installOxide()"
|
||||
class="mt-3 flex items-center gap-2 px-4 py-2 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<RotateCcw class="w-4 h-4" />
|
||||
Retry Installation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Install Button (shown when not installing) -->
|
||||
<div v-else class="text-center py-4">
|
||||
<p class="text-sm text-neutral-400 mb-4">Install or update Oxide/uMod. Required for all plugins including CorrosionCompanion.</p>
|
||||
<button
|
||||
@click="installOxide()"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<Puzzle class="w-4 h-4" />
|
||||
Install Oxide
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
|
||||
Reference in New Issue
Block a user