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,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>
|
||||
|
||||
Reference in New Issue
Block a user