fix(proxmox): resolve 7 dashboard and AI chat issues #129

Merged
sarman merged 6 commits from fix/proxmox-issues-v2 into beta 2026-06-21 21:35:29 +00:00
5 changed files with 29 additions and 69 deletions
Showing only changes of commit 58b4d59e6d - Show all commits

View File

@ -709,13 +709,13 @@ pub async fn list_proxmox_datastores(
let storage_name = obj.get("storage").and_then(|v| v.as_str()).unwrap_or("");
let node_name = obj.get("node").and_then(|v| v.as_str()).unwrap_or("");
normalized.insert(
"id".to_string(),
serde_json::Value::String(format!(
"storage/{}/{}",
node_name, storage_name
)),
);
// Avoid double-slash when cluster/resources omits "node" for shared storage
let storage_id = if node_name.is_empty() {
format!("storage/{}", storage_name)
} else {
format!("storage/{}/{}", node_name, storage_name)
};
normalized.insert("id".to_string(), serde_json::Value::String(storage_id));
normalized.insert(
"storage".to_string(),
serde_json::Value::String(storage_name.to_string()),

View File

@ -80,6 +80,8 @@ pub async fn list_firewall_rules(
}
/// Add firewall rule — uses correct PVE API field names (proto, enable, dest).
/// `rule.rule_num` is intentionally not sent: PVE assigns the position (pos) automatically
/// on creation. rule_num is only used for update/delete operations on existing rules.
pub async fn add_rule(
client: &crate::proxmox::client::ProxmoxClient,
node: &str,

View File

@ -122,6 +122,8 @@ export function VMList({
}));
}, [rawVms]);
// clusterId comes from props (not captured via closure over state), so it is always
// current when an action fires even if the user switches clusters mid-session.
const handleVMAction = useCallback(async (vm: VMInfo, action: string) => {
if (!clusterId) {
toast.error('No cluster selected');
@ -746,6 +748,8 @@ function MigrationDialog({
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
{/* Disabled when: no target node typed/selected,
OR same-cluster migration with no enumerated nodes to choose from */}
<Button
onClick={onSubmit}
disabled={!targetNode || (!isCrossCluster && availableTargets.length === 0)}

View File

@ -80,26 +80,14 @@ export function ProxmoxBackupPage() {
}, [selectedClusterId, loadJobs]);
const handleNewJob = () => {
setJobName('');
setJobNode('');
setJobSchedule('');
setJobVms('');
setShowNewJobDialog(true);
toast.warning(
'Backup job creation requires additional backend implementation (POST cluster/backup) and is not yet available.',
);
};
const handleSubmitNewJob = async () => {
if (!jobName || !jobNode || !jobSchedule) {
toast.error('Job name, node, and schedule are required');
return;
}
try {
toast.info(`Creating backup job ${jobName} - implementation pending`);
setShowNewJobDialog(false);
} catch (error) {
console.error('Failed to create backup job:', error);
toast.error(`Failed to create backup job: ${error}`);
}
toast.warning('Backup job creation is not yet available.');
setShowNewJobDialog(false);
};
if (clusters.length === 0 && !isLoading) {

View File

@ -17,7 +17,7 @@ export function ProxmoxNetworkPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showAddDialog, setShowAddDialog] = useState(false);
const [editingInterface, setEditingInterface] = useState<NetworkInterface | null>(null);
const [editingInterface] = useState<NetworkInterface | null>(null);
// Form state
const [ifaceName, setIfaceName] = useState('');
@ -52,58 +52,24 @@ export function ProxmoxNetworkPage() {
.catch(console.error);
}, [loadInterfaces, nodeId]);
const NOT_IMPLEMENTED_MSG =
'Network interface management requires additional backend implementation (POST/PUT/DELETE nodes/{node}/network) and is not yet available.';
const handleAddInterface = () => {
setEditingInterface(null);
setIfaceName('');
setIfaceType('eth');
setAddress('');
setNetmask('');
setGateway('');
setActive(true);
setShowAddDialog(true);
toast.warning(NOT_IMPLEMENTED_MSG);
};
const handleEditInterface = (iface: NetworkInterface) => {
setEditingInterface(iface);
setIfaceName(iface.iface);
setIfaceType(iface.type);
setAddress(iface.address || '');
setNetmask(iface.netmask || '');
setGateway(iface.gateway || '');
setActive(iface.active);
setShowAddDialog(true);
const handleEditInterface = (_iface: NetworkInterface) => {
toast.warning(NOT_IMPLEMENTED_MSG);
};
const handleSubmit = async () => {
if (!ifaceName || !ifaceType) {
toast.error('Interface name and type are required');
return;
}
try {
if (editingInterface) {
toast.info(`Updating interface ${ifaceName} - implementation pending`);
} else {
toast.info(`Creating interface ${ifaceName} - implementation pending`);
}
setShowAddDialog(false);
} catch (error) {
console.error('Failed to save interface:', error);
toast.error(`Failed to save interface: ${error}`);
}
toast.warning(NOT_IMPLEMENTED_MSG);
setShowAddDialog(false);
};
const handleDeleteInterface = async (iface: NetworkInterface) => {
if (!confirm(`Are you sure you want to delete interface ${iface.iface}?`)) {
return;
}
try {
toast.info(`Deleting interface ${iface.iface} - implementation pending`);
} catch (error) {
console.error('Failed to delete interface:', error);
toast.error(`Failed to delete interface: ${error}`);
}
const handleDeleteInterface = async (_iface: NetworkInterface) => {
toast.warning(NOT_IMPLEMENTED_MSG);
};
return (