feat: add /nodes route, AppSidebar nav item, and NodeManagementView

This commit is contained in:
pyr0ball 2026-05-05 21:24:27 -07:00
parent 1cd9c5d455
commit 4c225b94f5
3 changed files with 106 additions and 0 deletions

View file

@ -53,6 +53,17 @@
<span v-if="!stowed" class="nav-label">Fleet</span>
</RouterLink>
</li>
<li>
<RouterLink
to="/nodes"
class="nav-item"
:title="stowed ? 'Nodes' : ''"
@click="isMobile && stow()"
>
<span class="nav-icon" aria-hidden="true">🖥</span>
<span v-if="!stowed" class="nav-label">Nodes</span>
</RouterLink>
</li>
<!-- Data section -->
<li>

View file

@ -12,12 +12,14 @@ const TrainJobsView = () => import('../views/TrainJobsView.vue')
const TrainResultsView = () => import('../views/TrainResultsView.vue')
const ModelsView = () => import('../views/ModelsView.vue')
const SettingsView = () => import('../views/SettingsView.vue')
const NodeManagementView = () => import('../views/NodeManagementView.vue')
export const routes = [
// ── Top-level ────────────────────────────────────────────
{ path: '/', component: DashboardView, meta: { title: 'Dashboard' } },
{ path: '/fleet', component: ModelsView, meta: { title: 'Fleet' } },
{ path: '/settings', component: SettingsView, meta: { title: 'Settings' } },
{ path: '/nodes', component: NodeManagementView, meta: { title: 'Nodes' } },
// ── Data domain ──────────────────────────────────────────
{ path: '/data/label', component: LabelView, meta: { title: 'Label' } },

View file

@ -0,0 +1,93 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import NodeCard from '../components/nodes/NodeCard.vue'
interface GpuEntry {
gpu_id: number
card: string
vram_total_mb: number
vram_used_mb: number
vram_free_mb: number
temp_c: number | null
utilization_pct: number | null
compute_cap: number | null
services_assigned: string[]
services_running: string[]
}
interface ServiceInfo {
min_compute_cap: number
max_mb: number
catalog_size: number
}
interface NodeSummary {
node_id: string
online: boolean
agent_url: string
gpus: GpuEntry[]
profile_loaded: boolean
services_catalog: Record<string, ServiceInfo>
}
const nodes = ref<NodeSummary[]>([])
const loading = ref(true)
const error = ref('')
async function fetchNodes() {
loading.value = true
error.value = ''
try {
const r = await fetch('/api/nodes-mgmt/nodes')
if (!r.ok) throw new Error(`HTTP ${r.status}`)
nodes.value = await r.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load nodes'
} finally {
loading.value = false
}
}
onMounted(fetchNodes)
</script>
<template>
<main class="nodes-page">
<header class="nodes-header">
<h1>Nodes</h1>
<button class="btn-secondary" @click="fetchNodes" :disabled="loading">Refresh</button>
</header>
<div v-if="loading" class="nodes-status" aria-live="polite">Loading nodes...</div>
<div v-else-if="error" class="nodes-status nodes-error" role="alert">{{ error }}</div>
<div v-else-if="nodes.length === 0" class="nodes-status">
No nodes found. Check <code>coordinator_url</code> in config.
</div>
<div v-else class="nodes-grid">
<NodeCard
v-for="node in nodes"
:key="node.node_id"
:node="node"
@updated="fetchNodes"
/>
</div>
</main>
</template>
<style scoped>
.nodes-page { padding: 1.5rem; }
.nodes-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
}
.nodes-header h1 { margin: 0; font-size: 1.5rem; }
.nodes-grid { display: flex; flex-direction: column; gap: 1.5rem; }
.nodes-status {
color: var(--text-secondary, #888);
padding: 2rem;
text-align: center;
}
.nodes-error { color: var(--color-error, #fc8181); }
</style>