feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
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:
Vantz Stockwell
2026-02-21 13:34:09 -05:00
parent a181ed7ded
commit 8bb6cc0890
19 changed files with 776 additions and 139 deletions

View File

@@ -1,14 +1,20 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import { useAuthStore } from '@/stores/auth'
import { useToastStore } from '@/stores/toast'
import type { MapEntry } from '@/types'
import { Map, Upload, Trash2, RefreshCw } from 'lucide-vue-next'
import { Map, Upload, Trash2, RefreshCw, Loader2 } from 'lucide-vue-next'
import { safeFileSize } from '@/utils/formatters'
const api = useApi()
const auth = useAuthStore()
const toast = useToastStore()
const maps = ref<MapEntry[]>([])
const isLoading = ref(false)
const isUploading = ref(false)
const fileInputRef = ref<HTMLInputElement | null>(null)
function formatSize(bytes: number): string {
return safeFileSize(bytes)
@@ -35,8 +41,48 @@ async function deleteMap(map: MapEntry) {
try {
await api.del(`/maps/${map.id}`)
await fetchMaps()
} catch {
// Handle error
toast.success(`${map.display_name} deleted`)
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to delete map')
}
}
function triggerFileInput() {
fileInputRef.value?.click()
}
async function handleFileSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
// Reset the input so the same file can be re-selected if needed
input.value = ''
isUploading.value = true
try {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/maps', {
method: 'POST',
headers: {
Authorization: `Bearer ${auth.accessToken}`,
},
body: formData,
})
if (!response.ok) {
const err = await response.json().catch(() => ({ message: 'Upload failed' }))
throw new Error(err.message || `HTTP ${response.status}`)
}
toast.success(`${file.name} uploaded successfully`)
await fetchMaps()
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Upload failed')
} finally {
isUploading.value = false
}
}
@@ -64,9 +110,21 @@ onMounted(() => {
>
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
</button>
<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">
<Upload class="w-4 h-4" />
Upload Map
<input
ref="fileInputRef"
type="file"
accept=".map"
class="hidden"
@change="handleFileSelected"
/>
<button
@click="triggerFileInput"
:disabled="isUploading"
class="flex items-center gap-2 px-4 py-2 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors"
>
<Loader2 v-if="isUploading" class="w-4 h-4 animate-spin" />
<Upload v-else class="w-4 h-4" />
{{ isUploading ? 'Uploading...' : 'Upload Map' }}
</button>
</div>
</div>