feat: add /nodes route, AppSidebar nav item, and NodeManagementView
This commit is contained in:
parent
1cd9c5d455
commit
4c225b94f5
3 changed files with 106 additions and 0 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' } },
|
||||
|
|
|
|||
93
web/src/views/NodeManagementView.vue
Normal file
93
web/src/views/NodeManagementView.vue
Normal 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>
|
||||
Loading…
Reference in a new issue