feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Frontend:
- Wire Dashboard quick actions (start/stop/trigger wipe) + next wipe schedule
- Wire Console WebSocket streaming for real-time output
- Implement TOTP 2FA challenge flow in LoginView
- Wire Plugin load/unload toggle + uninstall buttons with confirmations
- Wire WipesView profile selector, disable trigger when no profiles
- Build full WipeProfiles create/edit modal with all config fields
- Wire MapsView file upload with multipart FormData
- Fix SettingsView empty catch blocks → toast error messages
- Fix stale localStorage token reads in CSV exports → auth store
- Fix auth store hardcoded permissions → JWT-decoded role permissions
- Fix wipe store onMounted lifecycle bug → explicit subscribe action
- Update EarlyAccessView from countdown to "Now Live" state
Backend:
- Wire wipe trigger to publish NATS cmd (corrosion.{id}.cmd.wipe)
- Wire plugin reload/uninstall to publish NATS cmd
- Expand NatsBridgeService: add files, wipe status, server status subs
- Add PATCH schedules/:id/toggle endpoint for task toggling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useWipeStore } from '@/stores/wipe'
|
||||
import { FileText, Plus, ChevronDown, ChevronRight } from 'lucide-vue-next'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { WipeProfile } from '@/types'
|
||||
import { FileText, Plus, ChevronDown, ChevronRight, Edit2, Trash2, X } from 'lucide-vue-next'
|
||||
|
||||
const wipeStore = useWipeStore()
|
||||
const toast = useToastStore()
|
||||
|
||||
const expandedId = ref<string | null>(null)
|
||||
const showModal = ref(false)
|
||||
const editingProfile = ref<WipeProfile | null>(null)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const defaultForm = () => ({
|
||||
profile_name: '',
|
||||
description: '',
|
||||
pre_wipe_config: {
|
||||
enabled: true,
|
||||
backup_before_wipe: true,
|
||||
countdown_warnings: [30, 10, 5],
|
||||
countdown_unit: 'minutes',
|
||||
countdown_messages: {} as Record<string, string>,
|
||||
kick_players_before_wipe: false,
|
||||
kick_message: 'Server is wiping, back soon!',
|
||||
run_final_save: true,
|
||||
discord_pre_announce: false,
|
||||
pushbullet_notify: false,
|
||||
custom_commands_before: [] as string[],
|
||||
},
|
||||
post_wipe_config: {
|
||||
enabled: true,
|
||||
verify_server_started: true,
|
||||
verify_correct_map: true,
|
||||
verify_plugins_loaded: true,
|
||||
verify_player_slots_open: false,
|
||||
max_restart_attempts: 3,
|
||||
health_check_timeout_seconds: 120,
|
||||
discord_post_announce: false,
|
||||
pushbullet_notify: false,
|
||||
rollback_on_failure: true,
|
||||
post_wipe_commands: [] as string[],
|
||||
},
|
||||
})
|
||||
|
||||
const form = ref(defaultForm())
|
||||
|
||||
function toggle(id: string) {
|
||||
expandedId.value = expandedId.value === id ? null : id
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingProfile.value = null
|
||||
form.value = defaultForm()
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function openEditModal(profile: WipeProfile) {
|
||||
editingProfile.value = profile
|
||||
form.value = {
|
||||
profile_name: profile.profile_name,
|
||||
description: profile.description || '',
|
||||
pre_wipe_config: {
|
||||
enabled: profile.pre_wipe_config.enabled,
|
||||
backup_before_wipe: profile.pre_wipe_config.backup_before_wipe,
|
||||
countdown_warnings: [...profile.pre_wipe_config.countdown_warnings],
|
||||
countdown_unit: profile.pre_wipe_config.countdown_unit,
|
||||
countdown_messages: { ...profile.pre_wipe_config.countdown_messages },
|
||||
kick_players_before_wipe: profile.pre_wipe_config.kick_players_before_wipe,
|
||||
kick_message: profile.pre_wipe_config.kick_message,
|
||||
run_final_save: profile.pre_wipe_config.run_final_save,
|
||||
discord_pre_announce: profile.pre_wipe_config.discord_pre_announce,
|
||||
pushbullet_notify: profile.pre_wipe_config.pushbullet_notify,
|
||||
custom_commands_before: [...profile.pre_wipe_config.custom_commands_before],
|
||||
},
|
||||
post_wipe_config: {
|
||||
enabled: profile.post_wipe_config.enabled,
|
||||
verify_server_started: profile.post_wipe_config.verify_server_started,
|
||||
verify_correct_map: profile.post_wipe_config.verify_correct_map,
|
||||
verify_plugins_loaded: profile.post_wipe_config.verify_plugins_loaded,
|
||||
verify_player_slots_open: profile.post_wipe_config.verify_player_slots_open,
|
||||
max_restart_attempts: profile.post_wipe_config.max_restart_attempts,
|
||||
health_check_timeout_seconds: profile.post_wipe_config.health_check_timeout_seconds,
|
||||
discord_post_announce: profile.post_wipe_config.discord_post_announce,
|
||||
pushbullet_notify: profile.post_wipe_config.pushbullet_notify,
|
||||
rollback_on_failure: profile.post_wipe_config.rollback_on_failure,
|
||||
post_wipe_commands: [...profile.post_wipe_config.post_wipe_commands],
|
||||
},
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingProfile.value = null
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
if (!form.value.profile_name.trim()) {
|
||||
toast.error('Profile name is required')
|
||||
return
|
||||
}
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (editingProfile.value) {
|
||||
await wipeStore.updateProfile(editingProfile.value.id, form.value)
|
||||
toast.success(`Profile "${form.value.profile_name}" updated`)
|
||||
} else {
|
||||
await wipeStore.createProfile(form.value)
|
||||
toast.success(`Profile "${form.value.profile_name}" created`)
|
||||
}
|
||||
closeModal()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to save profile')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProfile(profile: WipeProfile) {
|
||||
if (!confirm(`Delete profile "${profile.profile_name}"? This cannot be undone.`)) return
|
||||
try {
|
||||
await wipeStore.deleteProfile(profile.id)
|
||||
toast.success(`Profile "${profile.profile_name}" deleted`)
|
||||
if (expandedId.value === profile.id) expandedId.value = null
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to delete profile')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
wipeStore.fetchProfiles()
|
||||
})
|
||||
@@ -23,7 +142,10 @@ onMounted(() => {
|
||||
<FileText class="w-5 h-5 text-oxide-500" />
|
||||
<h1 class="text-2xl font-bold text-neutral-100">Wipe Profiles</h1>
|
||||
</div>
|
||||
<button class="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">
|
||||
<button
|
||||
@click="openCreateModal"
|
||||
class="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"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
New Profile
|
||||
</button>
|
||||
@@ -42,16 +164,34 @@ onMounted(() => {
|
||||
:key="profile.id"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<button
|
||||
@click="toggle(profile.id)"
|
||||
class="w-full flex items-center justify-between p-4 text-left hover:bg-neutral-800/50 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-neutral-100">{{ profile.profile_name }}</h3>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">{{ profile.description || 'No description' }}</p>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
@click="toggle(profile.id)"
|
||||
class="flex-1 flex items-center justify-between p-4 text-left hover:bg-neutral-800/50 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-neutral-100">{{ profile.profile_name }}</h3>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">{{ profile.description || 'No description' }}</p>
|
||||
</div>
|
||||
<component :is="expandedId === profile.id ? ChevronDown : ChevronRight" class="w-4 h-4 text-neutral-500" />
|
||||
</button>
|
||||
<div class="flex items-center gap-1 pr-4">
|
||||
<button
|
||||
@click="openEditModal(profile)"
|
||||
class="p-1.5 text-neutral-500 hover:text-oxide-400 rounded transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteProfile(profile)"
|
||||
class="p-1.5 text-neutral-500 hover:text-red-400 rounded transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<component :is="expandedId === profile.id ? ChevronDown : ChevronRight" class="w-4 h-4 text-neutral-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedId === profile.id" class="border-t border-neutral-800 p-4">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
@@ -119,4 +259,164 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create / Edit Modal -->
|
||||
<div
|
||||
v-if="showModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<!-- Modal header -->
|
||||
<div class="sticky top-0 bg-neutral-900 border-b border-neutral-800 px-6 py-4 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-neutral-100">{{ editingProfile ? 'Edit Profile' : 'New Profile' }}</h2>
|
||||
<button @click="closeModal" class="p-2 text-neutral-400 hover:text-neutral-200 rounded-lg transition-colors">
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Basic Info -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Basic Information</h3>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Profile Name</label>
|
||||
<input
|
||||
v-model="form.profile_name"
|
||||
type="text"
|
||||
placeholder="Default Wipe Profile"
|
||||
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Description (optional)</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="2"
|
||||
placeholder="Standard wipe configuration for monthly force wipes"
|
||||
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pre-Wipe Config -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Pre-Wipe</h3>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.enabled" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.backup_before_wipe" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Backup before wipe
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.run_final_save" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Run final save
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.kick_players_before_wipe" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Kick players before wipe
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.discord_pre_announce" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Discord pre-announce
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.pre_wipe_config.pushbullet_notify" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Pushbullet notify
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="form.pre_wipe_config.kick_players_before_wipe">
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Kick Message</label>
|
||||
<input
|
||||
v-model="form.pre_wipe_config.kick_message"
|
||||
type="text"
|
||||
placeholder="Server is wiping, back soon!"
|
||||
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post-Wipe Config -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Post-Wipe</h3>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.enabled" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.verify_server_started" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Verify server started
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.verify_correct_map" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Verify correct map
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.verify_plugins_loaded" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Verify plugins loaded
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.verify_player_slots_open" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Verify player slots open
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.rollback_on_failure" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Rollback on failure
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-neutral-300 cursor-pointer">
|
||||
<input v-model="form.post_wipe_config.discord_post_announce" type="checkbox" class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50" />
|
||||
Discord post-announce
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Max Restart Attempts</label>
|
||||
<input
|
||||
v-model.number="form.post_wipe_config.max_restart_attempts"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-neutral-300 mb-2">Health Check Timeout (seconds)</label>
|
||||
<input
|
||||
v-model.number="form.post_wipe_config.health_check_timeout_seconds"
|
||||
type="number"
|
||||
min="30"
|
||||
max="600"
|
||||
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal footer -->
|
||||
<div class="sticky bottom-0 bg-neutral-900 border-t border-neutral-800 px-6 py-4 flex items-center justify-end gap-3">
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveProfile"
|
||||
:disabled="isSaving"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 rounded-lg transition-colors"
|
||||
>
|
||||
{{ isSaving ? 'Saving...' : (editingProfile ? 'Save Changes' : 'Create Profile') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user