From 84ddf75afef2c452381e344e227affa0004938fc Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Fri, 12 Jun 2026 21:55:01 -0500 Subject: [PATCH] feat(proxmox): implement notes system, resource search, and administration panel (phases 12-13) - NotesPage: load/display/edit cluster notes with draft/save/cancel flow - SearchPage: cross-cluster resource search grouped by type with icon decorators - AdminPage: tabbed node admin (status, apt updates, repositories, syslog, tasks) with cluster/node selector; imports ClusterInfo from domain.ts --- src/pages/Proxmox/AdminPage.tsx | 355 +++++++++++++++++++++++++++++++ src/pages/Proxmox/NotesPage.tsx | 105 +++++++++ src/pages/Proxmox/SearchPage.tsx | 127 +++++++++++ 3 files changed, 587 insertions(+) create mode 100644 src/pages/Proxmox/AdminPage.tsx create mode 100644 src/pages/Proxmox/NotesPage.tsx create mode 100644 src/pages/Proxmox/SearchPage.tsx diff --git a/src/pages/Proxmox/AdminPage.tsx b/src/pages/Proxmox/AdminPage.tsx new file mode 100644 index 00000000..ce65aad4 --- /dev/null +++ b/src/pages/Proxmox/AdminPage.tsx @@ -0,0 +1,355 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/index'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; +import { Button } from '@/components/ui/index'; +import { Input } from '@/components/ui/index'; +import { RefreshCw, Power, RotateCcw } from 'lucide-react'; +import { + listProxmoxClusters, + getNodeStatus, + listAptUpdates, + listAptRepositories, + getSyslog, + listClusterTasks, +} from '@/lib/proxmoxClient'; +import type { + NodeStatus, + AptPackage, + AptRepository, + SyslogEntry, + ClusterTask, +} from '@/lib/proxmoxClient'; +import type { ClusterInfo } from '@/lib/domain'; + +export function ProxmoxAdminPage() { + const [clusters, setClusters] = useState([]); + const [clusterId, setClusterId] = useState(''); + const [nodeId, setNodeId] = useState('localhost'); + const [nodeInputValue, setNodeInputValue] = useState('localhost'); + const [nodeStatus, setNodeStatus] = useState(null); + const [aptUpdates, setAptUpdates] = useState([]); + const [aptRepos, setAptRepos] = useState([]); + const [syslog, setSyslog] = useState([]); + const [tasks, setTasks] = useState([]); + const [activeTab, setActiveTab] = useState('status'); + const [tabError, setTabError] = useState(null); + + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + setClusters(cls); + if (cls.length > 0) setClusterId(cls[0].id); + }) + .catch((err: unknown) => console.error('Failed to load clusters:', err)); + }, []); + + const loadTabData = useCallback( + async (tab: string, cId: string, nId: string) => { + if (!cId) return; + setTabError(null); + try { + switch (tab) { + case 'status': + setNodeStatus(await getNodeStatus(cId, nId)); + break; + case 'updates': + setAptUpdates(await listAptUpdates(cId, nId)); + break; + case 'repositories': + setAptRepos(await listAptRepositories(cId, nId)); + break; + case 'syslog': + setSyslog(await getSyslog(cId, nId)); + break; + case 'tasks': + setTasks(await listClusterTasks(cId)); + break; + } + } catch (e) { + setTabError(String(e)); + } + }, + [] + ); + + useEffect(() => { + void loadTabData(activeTab, clusterId, nodeId); + }, [activeTab, clusterId, nodeId, loadTabData]); + + const applyNodeId = () => { + setNodeId(nodeInputValue.trim() || 'localhost'); + }; + + const formatBytes = (bytes: number) => + bytes >= 1073741824 + ? `${(bytes / 1073741824).toFixed(1)} GB` + : `${Math.round(bytes / 1048576)} MB`; + + const formatUptime = (seconds: number) => { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + return d > 0 ? `${d}d ${h}h ${m}m` : `${h}h ${m}m`; + }; + + return ( +
+
+
+

Administration

+

Node management, updates, and system monitoring

+
+
+ + {/* Cluster / Node selector bar */} +
+
+ Cluster: + +
+
+ Node: + setNodeInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') applyNodeId(); + }} + placeholder="localhost" + /> + +
+ +
+ + {tabError &&
{tabError}
} + + + + Node Status + Updates + Repositories + System Log + Tasks + + + {/* ── Node Status ─────────────────────────────────────────────────── */} + + + + Node Status +
+ + +
+
+ + {nodeStatus ? ( +
+
+ CPU:{' '} + {(nodeStatus.cpu * 100).toFixed(1)}% +
+
+ Memory:{' '} + {formatBytes(nodeStatus.memory.used)} / {formatBytes(nodeStatus.memory.total)} +
+
+ Swap:{' '} + {formatBytes(nodeStatus.swap.used)} / {formatBytes(nodeStatus.swap.total)} +
+
+ Disk:{' '} + {formatBytes(nodeStatus.disk.used)} / {formatBytes(nodeStatus.disk.total)} +
+
+ Uptime:{' '} + {formatUptime(nodeStatus.uptime)} +
+
+ Version:{' '} + {nodeStatus.version} +
+ {nodeStatus.loadAvg.length > 0 && ( +
+ Load Avg:{' '} + {nodeStatus.loadAvg.map((v) => v.toFixed(2)).join(' / ')} +
+ )} +
+ ) : ( +
Loading node status...
+ )} +
+
+
+ + {/* ── APT Updates ─────────────────────────────────────────────────── */} + + + + Available Updates ({aptUpdates.length}) + + + {aptUpdates.length === 0 ? ( +
No updates available
+ ) : ( +
+ {aptUpdates.map((pkg, i) => ( +
+ {pkg.package} + + {pkg.version} + {pkg.newVersion ? ` → ${pkg.newVersion}` : ''} + + {pkg.description && ( + + {pkg.description} + + )} +
+ ))} +
+ )} +
+
+
+ + {/* ── APT Repositories ────────────────────────────────────────────── */} + + + + APT Repositories + + + {aptRepos.length === 0 ? ( +
No repositories found
+ ) : ( +
+ {aptRepos.map((repo, i) => ( +
+
+ {repo.types.join(' ')} {repo.uris.join(' ')} {repo.suites.join(' ')}{' '} + {repo.components.join(' ')} +
+
+ + {repo.enabled ? 'Enabled' : 'Disabled'} + + {repo.comment && ( + {repo.comment} + )} +
+
+ ))} +
+ )} +
+
+
+ + {/* ── Syslog ──────────────────────────────────────────────────────── */} + + + + System Log + + + +
+ {syslog.length === 0 ? ( +
No log entries
+ ) : ( + syslog.map((entry) => ( +
+ {entry.t} {entry.msg} +
+ )) + )} +
+
+
+
+ + {/* ── Tasks ───────────────────────────────────────────────────────── */} + + + + Recent Tasks + + + {tasks.length === 0 ? ( +
No tasks found
+ ) : ( +
+ {tasks.map((t) => ( +
+ + {t.upid} + + {t.type} + {t.node} + + {t.exitstatus ?? 'running'} + +
+ ))} +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/pages/Proxmox/NotesPage.tsx b/src/pages/Proxmox/NotesPage.tsx new file mode 100644 index 00000000..2c3dfcdd --- /dev/null +++ b/src/pages/Proxmox/NotesPage.tsx @@ -0,0 +1,105 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; +import { Button } from '@/components/ui/index'; +import { Textarea } from '@/components/ui/index'; +import { Edit, Save, X } from 'lucide-react'; +import { getClusterNotes, updateClusterNotes, listProxmoxClusters } from '@/lib/proxmoxClient'; + +export function ProxmoxNotesPage() { + const [notes, setNotes] = useState(''); + const [editMode, setEditMode] = useState(false); + const [draft, setDraft] = useState(''); + const [clusterId, setClusterId] = useState(''); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const init = async () => { + try { + const clusters = await listProxmoxClusters(); + if (clusters.length > 0) { + setClusterId(clusters[0].id); + const n = await getClusterNotes(clusters[0].id); + setNotes(n); + } + } catch (e) { + setError(String(e)); + } + }; + void init(); + }, []); + + const handleEdit = () => { + setDraft(notes); + setEditMode(true); + }; + + const handleCancel = () => setEditMode(false); + + const handleSave = async () => { + setSaving(true); + try { + await updateClusterNotes(clusterId, draft); + setNotes(draft); + setEditMode(false); + } catch (e) { + setError(String(e)); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+

Notes

+

Cluster notes and documentation

+
+ {!editMode ? ( + + ) : ( +
+ + +
+ )} +
+ + {error &&
{error}
} + + + + Cluster Notes + + + {!editMode ? ( +
+              {notes || (
+                
+                  No notes yet. Click Edit to add notes.
+                
+              )}
+            
+ ) : ( +