tftsr-devops_investigation/src/pages/Proxmox/RemotesPage.tsx

240 lines
8.0 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react';
import { RemotesList } from '@/components/Proxmox';
import { AddRemoteForm } from '@/components/Proxmox';
import { EditRemoteForm } from '@/components/Proxmox';
import { RemoveRemoteDialog } from '@/components/Proxmox';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index';
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster, updateProxmoxCluster, connectProxmoxCluster, disconnectProxmoxCluster } from '@/lib/proxmoxClient';
import { ClusterType } from '@/lib/domain';
import { toast } from 'sonner';
interface RemoteInfo {
id: string;
name: string;
url: string;
username: string;
type: 'pve' | 'pbs';
status: 'connected' | 'disconnected' | 'error';
}
export function ProxmoxRemotesPage() {
const [remotes, setRemotes] = useState<RemoteInfo[]>([]);
const [showAddDialog, setShowAddDialog] = useState(false);
const [editingRemote, setEditingRemote] = useState<RemoteInfo | null>(null);
const [removingRemote, setRemovingRemote] = useState<RemoteInfo | null>(null);
const loadRemotes = async () => {
try {
const clusters = await listProxmoxClusters();
// TODO: Implement actual status checking via backend connection test
const remotesList: RemoteInfo[] = clusters.map((c) => ({
id: c.id,
name: c.name,
url: c.url,
username: c.username,
type: c.clusterType === 've' ? 'pve' : 'pbs',
fix(proxmox): restore broken client retrieval across all commands Half-completed refactor left 68 Tauri command functions with orphaned .ok_or_else() chains after the old clusters.get() pattern was removed without inserting the replacement helper call. Also fixed two bugs in the new get_proxmox_client_for_cluster helper: undeclared `clusters` variable in the early-return check, and client_arc going out of scope before return. fix(ai): enforce system-message-first ordering for strict LLM providers Qwen3.5-122b (and other models via LiteLLM) reject requests where system messages appear after user/assistant turns. Moved tool-calling format and iteration-budget system messages to before history is appended. Changed mid-loop iteration warning and forced-stop instruction from system role to user role so they can safely appear mid-conversation. fix(proxmox): Remotes actions menu and connect/disconnect behaviour Replaced the non-functional "..." toast placeholder with a proper ActionsMenu dropdown (Edit / Test Connection / Delete). Removed inline emoji buttons folded into the menu. Connect now calls getProxmoxCluster as a live connection test and reflects real status; disconnect marks the remote disconnected locally. Remote status now maps correctly from the backend ClusterInfoWithHealth.connected field instead of hardcoding 'connected' for every entry. fix(proxmox): Ceph page no longer shows HEALTH_OK on non-Ceph clusters Page now fetches real health data on mount. If getCephHealth fails the page renders an informational notice rather than fake HEALTH_OK. When Ceph is present, pools and OSDs are loaded and displayed live.
2026-06-20 03:13:48 +00:00
status: (c.connected ? 'connected' : 'disconnected') as RemoteInfo['status'],
}));
setRemotes(remotesList);
} catch (err) {
console.error('Failed to load remotes:', err);
}
};
useEffect(() => {
void loadRemotes();
}, []);
const generateId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
/**
* Helper function to parse a Proxmox URL and extract hostname and port.
* Handles URLs with or without explicit port numbers.
*
* @param url - The full URL (e.g., "https://proxmox-server:8006" or "https://pve.example.com")
* @param type - The cluster type ('pve' or 'pbs') to determine default port
* @returns Object with hostname (stripped of protocol and port) and port number
*/
const parseRemoteUrl = (url: string, type: 'pve' | 'pbs'): { hostname: string; port: number } => {
let hostname = url.replace(/^https?:\/\//, '');
let port = type === 'pve' ? 8006 : 8007;
const portMatch = hostname.match(/:(\d+)$/);
if (portMatch) {
port = parseInt(portMatch[1], 10);
hostname = hostname.replace(/:\d+$/, '');
}
return { hostname, port };
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleAddRemote = async (config: any) => {
try {
const clusterType = config.type === 'pve' ? 've' : 'pbs';
const { hostname, port } = parseRemoteUrl(config.url, config.type);
const id = config.id || generateId();
await addProxmoxCluster(
id,
config.name,
clusterType as ClusterType,
{ url: hostname, port },
config.username,
config.password || ''
);
await loadRemotes();
setShowAddDialog(false);
} catch (err) {
console.error('Failed to add remote:', err);
toast.error('Failed to add remote: ' + String(err));
throw err;
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleEditRemote = async (config: any) => {
try {
const clusterType = config.type === 'pve' ? 've' : 'pbs';
const { hostname, port } = parseRemoteUrl(config.url, config.type);
await updateProxmoxCluster(
config.id,
config.name,
clusterType as ClusterType,
{ url: hostname, port },
config.username,
config.password || ''
);
await loadRemotes();
setEditingRemote(null);
} catch (err) {
console.error('Failed to edit remote:', err);
toast.error('Failed to edit remote: ' + String(err));
throw err;
}
};
const handleRemoveRemote = async () => {
if (removingRemote) {
try {
await removeProxmoxCluster(removingRemote.id);
await loadRemotes();
setRemovingRemote(null);
} catch (err) {
console.error('Failed to remove remote:', err);
toast.error('Failed to remove remote: ' + String(err));
}
}
};
fix(proxmox): restore broken client retrieval across all commands Half-completed refactor left 68 Tauri command functions with orphaned .ok_or_else() chains after the old clusters.get() pattern was removed without inserting the replacement helper call. Also fixed two bugs in the new get_proxmox_client_for_cluster helper: undeclared `clusters` variable in the early-return check, and client_arc going out of scope before return. fix(ai): enforce system-message-first ordering for strict LLM providers Qwen3.5-122b (and other models via LiteLLM) reject requests where system messages appear after user/assistant turns. Moved tool-calling format and iteration-budget system messages to before history is appended. Changed mid-loop iteration warning and forced-stop instruction from system role to user role so they can safely appear mid-conversation. fix(proxmox): Remotes actions menu and connect/disconnect behaviour Replaced the non-functional "..." toast placeholder with a proper ActionsMenu dropdown (Edit / Test Connection / Delete). Removed inline emoji buttons folded into the menu. Connect now calls getProxmoxCluster as a live connection test and reflects real status; disconnect marks the remote disconnected locally. Remote status now maps correctly from the backend ClusterInfoWithHealth.connected field instead of hardcoding 'connected' for every entry. fix(proxmox): Ceph page no longer shows HEALTH_OK on non-Ceph clusters Page now fetches real health data on mount. If getCephHealth fails the page renders an informational notice rather than fake HEALTH_OK. When Ceph is present, pools and OSDs are loaded and displayed live.
2026-06-20 03:13:48 +00:00
const handleConnectRemote = async (remote: RemoteInfo) => {
try {
toast.info(`Connecting to ${remote.name}...`);
await connectProxmoxCluster(remote.id);
toast.success(`Connected to ${remote.name}`);
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'connected' } : r))
);
fix(proxmox): restore broken client retrieval across all commands Half-completed refactor left 68 Tauri command functions with orphaned .ok_or_else() chains after the old clusters.get() pattern was removed without inserting the replacement helper call. Also fixed two bugs in the new get_proxmox_client_for_cluster helper: undeclared `clusters` variable in the early-return check, and client_arc going out of scope before return. fix(ai): enforce system-message-first ordering for strict LLM providers Qwen3.5-122b (and other models via LiteLLM) reject requests where system messages appear after user/assistant turns. Moved tool-calling format and iteration-budget system messages to before history is appended. Changed mid-loop iteration warning and forced-stop instruction from system role to user role so they can safely appear mid-conversation. fix(proxmox): Remotes actions menu and connect/disconnect behaviour Replaced the non-functional "..." toast placeholder with a proper ActionsMenu dropdown (Edit / Test Connection / Delete). Removed inline emoji buttons folded into the menu. Connect now calls getProxmoxCluster as a live connection test and reflects real status; disconnect marks the remote disconnected locally. Remote status now maps correctly from the backend ClusterInfoWithHealth.connected field instead of hardcoding 'connected' for every entry. fix(proxmox): Ceph page no longer shows HEALTH_OK on non-Ceph clusters Page now fetches real health data on mount. If getCephHealth fails the page renders an informational notice rather than fake HEALTH_OK. When Ceph is present, pools and OSDs are loaded and displayed live.
2026-06-20 03:13:48 +00:00
} catch (err) {
console.error('Failed to connect remote:', err);
toast.error('Connection failed: ' + String(err));
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'error' } : r))
);
}
};
const handleDisconnectRemote = async (remote: RemoteInfo) => {
try {
await disconnectProxmoxCluster(remote.id);
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'disconnected' } : r))
);
toast.info(`Disconnected from ${remote.name}`);
} catch (err) {
console.error('Failed to disconnect remote:', err);
toast.error('Disconnect failed: ' + String(err));
}
fix(proxmox): restore broken client retrieval across all commands Half-completed refactor left 68 Tauri command functions with orphaned .ok_or_else() chains after the old clusters.get() pattern was removed without inserting the replacement helper call. Also fixed two bugs in the new get_proxmox_client_for_cluster helper: undeclared `clusters` variable in the early-return check, and client_arc going out of scope before return. fix(ai): enforce system-message-first ordering for strict LLM providers Qwen3.5-122b (and other models via LiteLLM) reject requests where system messages appear after user/assistant turns. Moved tool-calling format and iteration-budget system messages to before history is appended. Changed mid-loop iteration warning and forced-stop instruction from system role to user role so they can safely appear mid-conversation. fix(proxmox): Remotes actions menu and connect/disconnect behaviour Replaced the non-functional "..." toast placeholder with a proper ActionsMenu dropdown (Edit / Test Connection / Delete). Removed inline emoji buttons folded into the menu. Connect now calls getProxmoxCluster as a live connection test and reflects real status; disconnect marks the remote disconnected locally. Remote status now maps correctly from the backend ClusterInfoWithHealth.connected field instead of hardcoding 'connected' for every entry. fix(proxmox): Ceph page no longer shows HEALTH_OK on non-Ceph clusters Page now fetches real health data on mount. If getCephHealth fails the page renders an informational notice rather than fake HEALTH_OK. When Ceph is present, pools and OSDs are loaded and displayed live.
2026-06-20 03:13:48 +00:00
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Remotes</h1>
<p className="text-muted-foreground">Manage Proxmox VE and Backup Server connections</p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm" onClick={() => { void loadRemotes(); }}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={() => setShowAddDialog(true)}>
<span className="mr-2 h-4 w-4">+</span>
Add Remote
</Button>
</div>
</div>
<RemotesList
remotes={remotes}
onRefresh={() => { void loadRemotes(); }}
onEdit={(remote) => {
setEditingRemote(remote as RemoteInfo | null);
}}
onDelete={(remote) => {
setRemovingRemote(remote as RemoteInfo | null);
}}
onConnect={(remote) => { void handleConnectRemote(remote as RemoteInfo); }}
onDisconnect={(remote) => { void handleDisconnectRemote(remote as RemoteInfo); }}
/>
{showAddDialog && (
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Add New Remote</DialogTitle>
</DialogHeader>
<AddRemoteForm onAdd={handleAddRemote} onCancel={() => setShowAddDialog(false)} />
</DialogContent>
</Dialog>
)}
{editingRemote !== null && (
<Dialog open={true} onOpenChange={() => setEditingRemote(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Remote</DialogTitle>
</DialogHeader>
<EditRemoteForm
remote={editingRemote}
onSave={handleEditRemote}
onCancel={() => setEditingRemote(null)}
/>
</DialogContent>
</Dialog>
)}
{removingRemote !== null && (
<Dialog open={true} onOpenChange={() => setRemovingRemote(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Remove Remote</DialogTitle>
</DialogHeader>
<RemoveRemoteDialog
remote={removingRemote}
onConfirm={handleRemoveRemote}
onCancel={() => setRemovingRemote(null)}
/>
</DialogContent>
</Dialog>
)}
</div>
);
}