From f1127f1340a900d84ff80f29f1b721e06f6f3150 Mon Sep 17 00:00:00 2001
From: Alexandre <146840804+alex001x@users.noreply.github.com>
Date: Sat, 18 Oct 2025 21:49:52 +0200
Subject: [PATCH 01/16] feat: add Automatic PDU Link Planner feature
---
ajax_apply_powerplan.php | 42 +++++++++++++
ajax_generate_powerplan.php | 92 ++++++++++++++++++++++++++++
cabnavigator.php | 42 ++++++++++++-
locale/fr_FR/LC_MESSAGES/openDCIM.po | 21 +++++++
4 files changed, 195 insertions(+), 2 deletions(-)
create mode 100644 ajax_apply_powerplan.php
create mode 100644 ajax_generate_powerplan.php
diff --git a/ajax_apply_powerplan.php b/ajax_apply_powerplan.php
new file mode 100644
index 000000000..4394ae15e
--- /dev/null
+++ b/ajax_apply_powerplan.php
@@ -0,0 +1,42 @@
+require_once "db.inc.php";
+require_once "facilities.inc.php";
+
+$cabinetid = intval($_POST['cabinetid']);
+$plan = $_SESSION['autoplan_'.$cabinetid];
+
+if(!$person->WriteAccess()){
+ echo json_encode(["error"=>__("You do not have permission to apply this plan.")]);
+ exit;
+}
+
+foreach($plan as $p){
+ if(isset($p["portsA"])){
+ foreach($p["portsA"] as $port){
+ $pc=new PowerConnection();
+ $pc->DeviceID=$p["deviceid"];
+ $pc->PDUID=$pduA->PDUID;
+ $pc->PDUPosition=$port;
+ $pc->CreateConnection();
+ LogActions::Insert('Device',$pc->DeviceID,'AutoLink','PDU',$pduA->Label,$port);
+ }
+ foreach($p["portsB"] as $port){
+ $pc=new PowerConnection();
+ $pc->DeviceID=$p["deviceid"];
+ $pc->PDUID=$pduB->PDUID;
+ $pc->PDUPosition=$port;
+ $pc->CreateConnection();
+ LogActions::Insert('Device',$pc->DeviceID,'AutoLink','PDU',$pduB->Label,$port);
+ }
+ } else {
+ foreach($p["ports"] as $port){
+ $targetPDU = ($p["pdu"]==$pduA->Label)?$pduA:$pduB;
+ $pc=new PowerConnection();
+ $pc->DeviceID=$p["deviceid"];
+ $pc->PDUID=$targetPDU->PDUID;
+ $pc->PDUPosition=$port;
+ $pc->CreateConnection();
+ LogActions::Insert('Device',$pc->DeviceID,'AutoLink','PDU',$targetPDU->Label,$port);
+ }
+ }
+}
+echo json_encode(["success"=>__("Power plan applied successfully.")]);
diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php
new file mode 100644
index 000000000..d13dbfa04
--- /dev/null
+++ b/ajax_generate_powerplan.php
@@ -0,0 +1,92 @@
+require_once "db.inc.php";
+require_once "facilities.inc.php";
+
+$cabinetid = intval($_POST['cabinetid']);
+$mode = sanitize($_POST['mode']);
+
+$pdus = PowerDistribution::GetPDUbyCabinet($cabinetid);
+if(count($pdus)<2){
+ echo "
".__("This cabinet requires at least 2 PDUs for automatic planning.")."
";
+ exit;
+}
+
+$pduA = $pdus[0];
+$pduB = $pdus[1];
+
+$devices = Device::GetDevicesByCabinet($cabinetid);
+$plan = [];
+$totalPowerA = $totalPowerB = 0;
+$portA = PowerConnection::GetNextFreePort($pduA->PDUID);
+$portB = PowerConnection::GetNextFreePort($pduB->PDUID);
+
+foreach($devices as $dev){
+ if(in_array($dev->DeviceType, ["Server","Switch","Appliance","Chassis","Storage Array"])){
+ $portsNeeded = max(1, intval($dev->PowerSupplyCount)); // nombre d’alim du device
+ $power = intval($dev->Power);
+ $entry = ["device"=>$dev->Label,"deviceid"=>$dev->DeviceID];
+
+ switch($mode){
+ case "balanced":
+ $target = ($totalPowerA <= $totalPowerB) ? "A" : "B";
+ if($target=="A" && PowerConnection::HasFreePorts($pduA->PDUID, $portsNeeded)){
+ $entry["pdu"]=$pduA->Label; $entry["pduname"]="A";
+ $entry["ports"]=PowerConnection::ReservePorts($pduA->PDUID,$portsNeeded);
+ $totalPowerA += $power;
+ } elseif(PowerConnection::HasFreePorts($pduB->PDUID, $portsNeeded)) {
+ $entry["pdu"]=$pduB->Label; $entry["pduname"]="B";
+ $entry["ports"]=PowerConnection::ReservePorts($pduB->PDUID,$portsNeeded);
+ $totalPowerB += $power;
+ }
+ break;
+
+ case "dualpath":
+ if(PowerConnection::HasFreePorts($pduA->PDUID,1) && PowerConnection::HasFreePorts($pduB->PDUID,1)){
+ $entry["pdu"]="A/B";
+ $entry["portsA"]=PowerConnection::ReservePorts($pduA->PDUID,1);
+ $entry["portsB"]=PowerConnection::ReservePorts($pduB->PDUID,1);
+ $totalPowerA+=$power/2; $totalPowerB+=$power/2;
+ }
+ break;
+
+ case "intelligent":
+ // Équilibrage réel selon puissance
+ $target = ($totalPowerA <= $totalPowerB) ? "A" : "B";
+ $bestPDU = ($target=="A") ? $pduA : $pduB;
+ if(PowerConnection::HasFreePorts($bestPDU->PDUID,$portsNeeded)){
+ $entry["pdu"]=$bestPDU->Label;
+ $entry["ports"]=PowerConnection::ReservePorts($bestPDU->PDUID,$portsNeeded);
+ if($target=="A") $totalPowerA+=$power; else $totalPowerB+=$power;
+ } else {
+ // bascule si pas assez de ports
+ $altPDU = ($target=="A") ? $pduB : $pduA;
+ if(PowerConnection::HasFreePorts($altPDU->PDUID,$portsNeeded)){
+ $entry["pdu"]=$altPDU->Label;
+ $entry["ports"]=PowerConnection::ReservePorts($altPDU->PDUID,$portsNeeded);
+ if($target=="A") $totalPowerB+=$power; else $totalPowerA+=$power;
+ }
+ }
+ break;
+ }
+ $plan[]=$entry;
+ }
+}
+
+// Rendu HTML
+echo "".__("Proposed Power Connection Plan")."
";
+echo "
+| ".__("Device")." | ".__("Ports")." | ".__("PDU")." |
";
+foreach($plan as $p){
+ $ports=isset($p["portsA"]) ? "A:".implode(',',$p["portsA"])." / B:".implode(',',$p["portsB"]) : implode(',',$p["ports"]);
+ echo "| {$p['device']} | $ports | {$p['pdu']} |
";
+}
+echo "
";
+
+echo "";
+if($person->WriteAccess()){
+ echo "
";
+} else {
+ echo "
".__("Read-only mode: you can preview and print the plan but not apply changes.")."
";
+}
+echo "
";
+
+$_SESSION['autoplan_'.$cabinetid] = $plan; // stockage temporaire pour validation
diff --git a/cabnavigator.php b/cabnavigator.php
index 6e9753d81..91411a9df 100644
--- a/cabnavigator.php
+++ b/cabnavigator.php
@@ -382,8 +382,14 @@ function renderUnassignedTemplateOwnership($noTemplFlag, $noOwnerFlag, $device)
}
$body.="\t\t\n\t\n";
-
-
+ // feature automatic-pdu-link-planner
+ $body.="
+
+
+ ";
+ // end
if ($person->CanWrite($cab->AssignedTo) || $person->SiteAdmin) {
$body.="\t