fix: Wire automation toggles, browse uMod, and error feedback across admin views
All checks were successful
Test Asgard Runner / test (push) Successful in 2s

- ServerView: automation toggles (crash recovery, auto-update, force wipe eligible)
  now call updateConfig() on click with toast success/error; all catch blocks get
  toast feedback instead of silent swallow
- PluginsView: Browse uMod tab wired to /plugins/search backend endpoint with
  debounced search, results table, and Install button that calls installPlugin();
  shows install state and marks already-installed plugins
- WipesView: dry-run results now displayed in a collapsible panel (would_delete /
  would_preserve lists + estimated duration); schedule enable/disable toggle wired
  to PUT /wipes/schedules/:id with loading state and toast feedback
- AnalyticsView: catch blocks now show toast errors instead of console.log;
  Player Retention placeholder replaced with intentional placeholder cards
- TeamView, ChatLogView, PlayersView, NotificationsView, MapsView: all empty
  catch blocks and '// API not wired yet' comments replaced with toast.error()
  calls; Notifications save now shows success toast

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-21 16:04:34 -05:00
parent cbb3ba6586
commit 38e6d28248
9 changed files with 290 additions and 48 deletions

View File

@@ -1,15 +1,18 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { usePluginStore } from '@/stores/plugins'
import { useToastStore } from '@/stores/toast'
import type { PluginEntry } from '@/types'
import { Puzzle, Search, Download, RefreshCw, Power, PowerOff, Trash2 } from 'lucide-vue-next'
import { Puzzle, Search, Download, RefreshCw, Power, PowerOff, Trash2, Loader2 } from 'lucide-vue-next'
const pluginStore = usePluginStore()
const toast = useToastStore()
const searchQuery = ref('')
const tab = ref<'installed' | 'browse'>('installed')
const browseQuery = ref('')
const browseDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
const installing = ref<string | null>(null)
const filteredPlugins = computed(() => {
let result = pluginStore.plugins
@@ -59,6 +62,32 @@ async function handleUninstall(plugin: PluginEntry) {
}
}
async function handleBrowseSearch() {
if (!browseQuery.value.trim()) return
try {
await pluginStore.searchPlugins(browseQuery.value.trim())
} catch {
toast.error('Failed to search uMod plugins')
}
}
function scheduleBrowseSearch() {
if (browseDebounce.value) clearTimeout(browseDebounce.value)
browseDebounce.value = setTimeout(handleBrowseSearch, 400)
}
async function installFromBrowse(result: { name: string }) {
installing.value = result.name
try {
await pluginStore.installPlugin({ plugin_name: result.name, source: 'umod' })
toast.success(`${result.name} installed`)
} catch {
toast.error(`Failed to install ${result.name}`)
} finally {
installing.value = null
}
}
onMounted(() => {
pluginStore.fetchPlugins()
})
@@ -105,12 +134,23 @@ onMounted(() => {
Browse uMod
</button>
</div>
<div class="relative flex-1 max-w-sm">
<div v-if="tab === 'installed'" class="relative flex-1 max-w-sm">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
<input
v-model="searchQuery"
type="text"
:placeholder="tab === 'installed' ? 'Search installed plugins...' : 'Search uMod...'"
placeholder="Search installed plugins..."
class="w-full pl-10 pr-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 v-if="tab === 'browse'" class="relative flex-1 max-w-sm">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
<input
v-model="browseQuery"
type="text"
placeholder="Search uMod plugins..."
@input="scheduleBrowseSearch"
@keydown.enter="handleBrowseSearch"
class="w-full pl-10 pr-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>
@@ -187,11 +227,69 @@ onMounted(() => {
</table>
</div>
<!-- Browse uMod (placeholder) -->
<div v-if="tab === 'browse'" class="bg-neutral-900 border border-neutral-800 rounded-lg p-12 text-center">
<Download class="w-10 h-10 text-neutral-600 mx-auto mb-3" />
<h3 class="text-lg font-medium text-neutral-300 mb-1">uMod Plugin Browser</h3>
<p class="text-sm text-neutral-500">Search and install plugins directly from uMod. Coming soon.</p>
<!-- Browse uMod -->
<div v-if="tab === 'browse'">
<!-- Empty state: no search yet -->
<div v-if="!browseQuery.trim() && pluginStore.searchResults.length === 0" class="bg-neutral-900 border border-neutral-800 rounded-lg p-12 text-center">
<Search class="w-10 h-10 text-neutral-600 mx-auto mb-3" />
<h3 class="text-lg font-medium text-neutral-300 mb-1">Search uMod</h3>
<p class="text-sm text-neutral-500">Type a plugin name above to search the uMod plugin directory.</p>
</div>
<!-- Loading -->
<div v-else-if="pluginStore.isLoading" class="bg-neutral-900 border border-neutral-800 rounded-lg p-12 text-center">
<Loader2 class="w-8 h-8 text-neutral-500 animate-spin mx-auto mb-3" />
<p class="text-sm text-neutral-500">Searching uMod...</p>
</div>
<!-- No results -->
<div v-else-if="browseQuery.trim() && pluginStore.searchResults.length === 0" class="bg-neutral-900 border border-neutral-800 rounded-lg p-12 text-center">
<Download class="w-10 h-10 text-neutral-600 mx-auto mb-3" />
<h3 class="text-lg font-medium text-neutral-300 mb-1">No plugins found</h3>
<p class="text-sm text-neutral-500">No uMod plugins matched "{{ browseQuery }}". Try a different search term.</p>
</div>
<!-- Results -->
<div v-else class="bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-neutral-800 text-left">
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Plugin</th>
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Author</th>
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Version</th>
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-800">
<tr
v-for="result in pluginStore.searchResults"
:key="result.name"
class="hover:bg-neutral-800/50 transition-colors"
>
<td class="px-4 py-3">
<p class="text-sm font-medium text-neutral-100">{{ result.name }}</p>
<p v-if="result.description" class="text-xs text-neutral-500 mt-0.5 truncate max-w-xs">{{ result.description }}</p>
</td>
<td class="px-4 py-3 text-sm text-neutral-400">{{ result.author ?? '\u2014' }}</td>
<td class="px-4 py-3 text-sm text-neutral-400 font-mono">{{ result.latest_version ?? '\u2014' }}</td>
<td class="px-4 py-3 text-right">
<button
@click="installFromBrowse(result)"
:disabled="installing === result.name || pluginStore.plugins.some((p: PluginEntry) => p.plugin_name === result.name)"
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ml-auto"
:class="pluginStore.plugins.some((p: PluginEntry) => p.plugin_name === result.name)
? 'bg-neutral-700 text-neutral-500 cursor-not-allowed'
: 'bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 text-white'"
>
<Loader2 v-if="installing === result.name" class="w-3.5 h-3.5 animate-spin" />
<Download v-else class="w-3.5 h-3.5" />
{{ pluginStore.plugins.some((p: PluginEntry) => p.plugin_name === result.name) ? 'Installed' : installing === result.name ? 'Installing...' : 'Install' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>