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 " +"; +foreach($plan as $p){ + $ports=isset($p["portsA"]) ? "A:".implode(',',$p["portsA"])." / B:".implode(',',$p["portsB"]) : implode(',',$p["ports"]); + echo ""; +} +echo "
".__("Device")."".__("Ports")."".__("PDU")."
{$p['device']}$ports{$p['pdu']}
"; + +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
\n"; if ($person->CanWrite($cab->AssignedTo) ) { @@ -818,6 +824,38 @@ function flippyfloppy(){ +// feature automatic-pdu-link-planner JS modal + req. Ajax +$('#btnAutoPlanner').click(function(){ + const html = ` + `; + $('body').append(html); + $('#pduPlannerModal').modal('show'); +}); + +$(document).on('click','#btnGeneratePlan',function(){ + const mode = $('input[name="planmode"]:checked').val(); + $.ajax({ + url: 'ajax_generate_powerplan.php', + type: 'POST', + data: { cabinetid: cabinetID, mode: mode }, + success: function(resp){ + $('#pduPlannerModal').modal('hide'); + $('#autoPlanResult').html(resp); + } + }); +}); +// end diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 9263f60e8..d9cedb69d 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6203,3 +6203,24 @@ msgstr "" #~ msgid "MailTo" #~ msgstr "Envoyé à " + +msgid "Automatic PDU Link Planner" +msgstr "Planificateur automatique de liaison PDU" + +msgid "Mode 1 – Load Balanced" +msgstr "Mode 1 – Répartition équilibrée" + +msgid "Mode 2 – Dual Power Path" +msgstr "Mode 2 – Voie double" + +msgid "Mode 3 – Intelligent Power Planner" +msgstr "Mode 3 – Planificateur intelligent" + +msgid "Proposed Power Connection Plan" +msgstr "Plan de raccordement proposé" + +msgid "Print Power Plan" +msgstr "Imprimer le plan d'alimentation" + +msgid "You do not have permission to apply this plan." +msgstr "Vous n’avez pas les droits pour appliquer ce plan." From 599030fbd7f9e7deb8644831a9b4893edd33c897 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:17:48 +0200 Subject: [PATCH 02/16] feat: add Automatic PDU Link Planner feature --- cabnavigator.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cabnavigator.php b/cabnavigator.php index 91411a9df..3864e40aa 100644 --- a/cabnavigator.php +++ b/cabnavigator.php @@ -383,12 +383,12 @@ function renderUnassignedTemplateOwnership($noTemplFlag, $noOwnerFlag, $device) $body.="\t\t\n\t
\n"; // feature automatic-pdu-link-planner - $body.="
- -
-
"; + $body .= '
+ +
+
'; // end if ($person->CanWrite($cab->AssignedTo) || $person->SiteAdmin) { $body.="\t
\n"; @@ -824,13 +824,14 @@ function flippyfloppy(){ +var cabinetID = CabinetID); ?>; // feature automatic-pdu-link-planner JS modal + req. Ajax $('#btnAutoPlanner').click(function(){ const html = ` \n\t
\n"; $body.='
@@ -382,14 +402,7 @@ 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
\n"; if ($person->CanWrite($cab->AssignedTo) ) { @@ -825,37 +838,41 @@ function flippyfloppy(){ } ?> var cabinetID = CabinetID); ?>; -// feature automatic-pdu-link-planner JS modal + req. Ajax -$('#btnAutoPlanner').click(function(){ - const html = ` - `; - $(document).on('hidden.bs.modal', '#pduPlannerModal', function() { - $(this).remove(); -}); - -}); - -$(document).on('click','#btnGeneratePlan',function(){ - const mode = $('input[name="planmode"]:checked').val(); - $.ajax({ - url: 'ajax_generate_powerplan.php', - type: 'POST', - data: { cabinetid: cabinetID, mode: mode }, - success: function(resp){ - $('#pduPlannerModal').modal('hide'); - $('#autoPlanResult').html(resp); - } +// feature automatic-pdu-link-planner JS modal + req. Ajax => 25.01 +$(document).ready(function(){ + var cabinetID = CabinetID); ?>; + var i18n = { + planner : "", + select : "", + mode1 : "", + mode2 : "", + mode3 : "", + generate: "", + cancel : "" + }; + + $('#btnAutoPlanner').on('click', function(){ + const html = ` +
+

${i18n.select}

+
+
+ +
`; + $(html).dialog({ + modal: true, width: 460, title: i18n.planner, + buttons: [ + { text: i18n.generate, click: function(){ + const mode = $('input[name="planmode"]:checked').val(); + $.post('ajax_generate_powerplan.php', { cabinetid: cabinetID, mode: mode }, function(resp){ + $('#autoPlanResult').html(resp); + }); + $(this).dialog('close'); + }}, + { text: i18n.cancel, click: function(){ $(this).dialog('close'); } } + ], + close: function(){ $(this).remove(); } + }); }); }); // end diff --git a/classes/PowerPorts.class.php b/classes/PowerPorts.class.php index a5a8979bc..854f5338b 100644 --- a/classes/PowerPorts.class.php +++ b/classes/PowerPorts.class.php @@ -50,19 +50,24 @@ function MakeDisplay(){ $this->Notes=stripslashes(trim($this->Notes)); } - static function RowToObject($dbRow){ - $pp=new PowerPorts(); - $pp->DeviceID=$dbRow['DeviceID']; - $pp->PortNumber=$dbRow['PortNumber']; - $pp->Label=$dbRow['Label']; - $pp->ConnectedDeviceID=$dbRow['ConnectedDeviceID']; - $pp->ConnectedPort=$dbRow['ConnectedPort']; - $pp->Notes=$dbRow['Notes']; - - $pp->MakeDisplay(); - - return $pp; - } + function RowToObject($dbRow){ + $pp = new PowerPorts(); + $pp->DeviceID = $dbRow['DeviceID']; + $pp->PortNumber = $dbRow['PortNumber']; + $pp->Label = $dbRow['Label']; + // ➜ new 25.01 + if(array_key_exists('ConnectorID',$dbRow)) { $pp->ConnectorID = $dbRow['ConnectorID']; } + if(array_key_exists('PhaseID',$dbRow)) { $pp->PhaseID = $dbRow['PhaseID']; } + if(array_key_exists('VoltageID',$dbRow)) { $pp->VoltageID = $dbRow['VoltageID']; } + // exist + $pp->ConnectedDeviceID = $dbRow['ConnectedDeviceID']; + $pp->ConnectedPort = $dbRow['ConnectedPort']; + $pp->Notes = $dbRow['Notes']; + + $pp->MakeDisplay(); + return $pp; +} + function getPort(){ global $dbh; diff --git a/css/inventory.php b/css/inventory.php index 0faa03b06..946207db9 100644 --- a/css/inventory.php +++ b/css/inventory.php @@ -1477,5 +1477,7 @@ p.errormsg {padding: 20px; background-color: #DDDDDD; font-size: 120%; font-weight: bold; color: red;} - - +.phase-load-table td { + padding: 3px 5px; + vertical-align: middle; +} \ No newline at end of file diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index d9cedb69d..0ebd0d9a5 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6205,22 +6205,76 @@ msgstr "" #~ msgstr "Envoyé à " msgid "Automatic PDU Link Planner" -msgstr "Planificateur automatique de liaison PDU" +msgstr "Planificateur automatique de liaisons PDU" + +msgid "Select a power distribution mode for this cabinet" +msgstr "Sélectionnez un mode de distribution électrique pour cette baie" msgid "Mode 1 – Load Balanced" msgstr "Mode 1 – Répartition équilibrée" msgid "Mode 2 – Dual Power Path" -msgstr "Mode 2 – Voie double" +msgstr "Mode 2 – Double alimentation" msgid "Mode 3 – Intelligent Power Planner" msgstr "Mode 3 – Planificateur intelligent" -msgid "Proposed Power Connection Plan" -msgstr "Plan de raccordement proposé" +msgid "⚠ No PDU detected in this cabinet." +msgstr "⚠ Aucun PDU détecté dans cette baie." + +msgid "⚠ Dual Power Path mode requires at least two PDUs." +msgstr "⚠ Le mode double alimentation nécessite au moins deux PDU." + +msgid "ℹ No devices detected in this cabinet." +msgstr "ℹ Aucun équipement détecté dans cette baie." + +msgid "⚠ No available outlet found for this device." +msgstr "⚠ Aucune prise disponible pour cet équipement." + +msgid "Proposed Power Distribution Plan" +msgstr "Plan de distribution électrique proposé" + +msgid "Phase Load Summary" +msgstr "Synthèse de charge par phase" + +msgid "Read-only mode: preview and print only." +msgstr "Mode lecture seule : aperçu et impression uniquement." + +msgid "Apply and Save" +msgstr "Appliquer et enregistrer" msgid "Print Power Plan" msgstr "Imprimer le plan d'alimentation" +msgid "⚠ Single-power devices detected. Installing a Static Transfer Switch (STS) is recommended to enhance power redundancy." +msgstr "⚠ Des équipements mono-alimentés ont été détectés. L’installation d’un STS (commutateur statique de transfert) est recommandée pour améliorer la redondance d'alimentation." + msgid "You do not have permission to apply this plan." -msgstr "Vous n’avez pas les droits pour appliquer ce plan." +msgstr "Vous n’avez pas l’autorisation d’appliquer ce plan." + +msgid "no free power inlet on device" +msgstr "aucune entrée d’alimentation libre sur l’équipement" + +msgid "target PDU port no longer available" +msgstr "le port du PDU cible n’est plus disponible" + +msgid "failed to make power connection" +msgstr "échec de la création de la connexion d’alimentation" + +msgid "⚠ No compatible outlet found for this device feed (%s, %s)." +msgstr "⚠ Aucune prise compatible trouvée pour cette alimentation de l’équipement (%s, %s)." + +msgid "any connector" +msgstr "n'importe quel connecteur" + +msgid "any voltage" +msgstr "n'importe quelle tension" + +msgid "no free compatible power inlet on device" +msgstr "aucune entrée d'alimentation compatible libre sur l’équipement" + +msgid "Feature unavailable" +msgstr "Fonctionnalité indisponible" + +msgid "The Automatic PDU Link Planner requires OpenDCIM version 25.01 or newer. Please update your installation to access this feature." +msgstr "Le planificateur automatique de liaisons PDU nécessite OpenDCIM version 25.01 ou plus récente. Veuillez mettre à jour votre installation pour accéder à cette fonctionnalité." From 86d1c59cedbf068f003de6244daad0d8b0021c70 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:52:03 +0200 Subject: [PATCH 05/16] feat: add Automatic PDU Link Planner feature --- cabnavigator.php | 7 +++++++ css/inventory.php | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cabnavigator.php b/cabnavigator.php index d376a9091..2a98c755a 100644 --- a/cabnavigator.php +++ b/cabnavigator.php @@ -861,6 +861,13 @@ function flippyfloppy(){ `; $(html).dialog({ modal: true, width: 460, title: i18n.planner, + appendTo: "body", // ensures it’s not confined inside .main or .page + dialogClass: "autoPlannerModal", + draggable: false, + resizable: false, + open: function() { + $(".ui-widget-overlay").css("opacity", "0.6"); // enforce overlay opacity + }, buttons: [ { text: i18n.generate, click: function(){ const mode = $('input[name="planmode"]:checked').val(); diff --git a/css/inventory.php b/css/inventory.php index 946207db9..fdf7b4b3d 100644 --- a/css/inventory.php +++ b/css/inventory.php @@ -1477,7 +1477,50 @@ p.errormsg {padding: 20px; background-color: #DDDDDD; font-size: 120%; font-weight: bold; color: red;} +/* feature automatic pdu link planer */ .phase-load-table td { padding: 3px 5px; vertical-align: middle; -} \ No newline at end of file +} + /* --- Fix for Auto Planner modal visibility --- */ + .ui-dialog { + z-index: 99999 !important; /* ensures it's above the rack view */ + background: #fff !important; /* opaque white background for clarity */ + border-radius: 6px; + box-shadow: 0 0 25px rgba(0,0,0,0.4); + } + + .ui-widget-overlay { + background: rgba(0,0,0,0.6) !important; /* dark transparent overlay */ + position: fixed !important; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 99998 !important; + } + + /* Slight fade animation for nicer UX */ + .ui-dialog { + animation: fadeInModal 0.25s ease-in-out; + } + @keyframes fadeInModal { + from {opacity:0; transform: scale(0.97);} + to {opacity:1; transform: scale(1);} + } + + /* Optional: better spacing inside modal */ + #pduPlannerDialog label { + display: block; + margin: 6px 0; + cursor: pointer; + } + .ui-dialog-buttonpane .ui-button { + background-color: #007bff !important; + border: none !important; + color: white !important; + border-radius: 4px; + padding: 4px 10px; + font-weight: bold; +} +.ui-dialog-buttonpane .ui-button:hover { + background-color: #0056b3 !important; +} + From 7c59d80a2225a91b0e0d681ee3b8a59aea13ccef Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:26:11 +0200 Subject: [PATCH 06/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 10 ++++++++-- cabnavigator.php | 5 ++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index c5d076bbb..4b1cbdc90 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -10,7 +10,10 @@ $person = new People(); $person->GetUserRights(); -$pduList = PowerDistribution::GetPDUbyCabinet($cabinetid); +// Get PDU List (instance method) +$pdu = new PowerDistribution(); +$pdu->CabinetID = $cabinetid; +$pduList = $pdu->GetPDUbyCabinet(); $pduCount = is_array($pduList) ? count($pduList) : 0; if ($pduCount == 0) { @@ -22,7 +25,10 @@ exit; } -$devices = Device::GetDevicesByCabinet($cabinetid); +// --- Get devices --- +$dev = new Device(); +$dev->Cabinet = $cabinetid; +$devices = $dev->ViewDevicesByCabinet(); if (empty($devices)) { echo '
'.__("ℹ No devices detected in this cabinet.").'
'; exit; diff --git a/cabnavigator.php b/cabnavigator.php index 2a98c755a..9b0671ebf 100644 --- a/cabnavigator.php +++ b/cabnavigator.php @@ -378,12 +378,11 @@ function renderUnassignedTemplateOwnership($noTemplFlag, $noOwnerFlag, $device) } else { // Display the button if user has at least read access if ($person->CanRead($cab->AssignedTo)) { - $body .= '
+ $body .= '
-
-
'; +
'; } } // end From 01c4e57c6cb194fd524b2dee4c1ac925d9caf37f Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:38:37 +0200 Subject: [PATCH 07/16] feat: add Automatic PDU Link Planner feature --- classes/PowerPorts.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/PowerPorts.class.php b/classes/PowerPorts.class.php index 854f5338b..8a76d849e 100644 --- a/classes/PowerPorts.class.php +++ b/classes/PowerPorts.class.php @@ -50,7 +50,7 @@ function MakeDisplay(){ $this->Notes=stripslashes(trim($this->Notes)); } - function RowToObject($dbRow){ + public static function RowToObject($dbRow){ $pp = new PowerPorts(); $pp->DeviceID = $dbRow['DeviceID']; $pp->PortNumber = $dbRow['PortNumber']; @@ -376,7 +376,8 @@ static function getPortList($DeviceID){ $portList=array(); foreach($dbh->query($sql) as $row){ - $portList[$row['PortNumber']]=PowerPorts::RowToObject($row); + $port = new PowerPorts(); + $portList[$row["PortNumber"]] = $port->RowToObject($row); } if( sizeof($portList)==0 && $dev->DeviceType!="Physical Infrastructure" ){ From ae10b8c58e509065aaf05ea82a68bc88b776a3a6 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:49:19 +0200 Subject: [PATCH 08/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index 4b1cbdc90..3d090577f 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -258,7 +258,7 @@ function phaseColor($percent){ echo "
"; // person write access echo "
"; -if($person->WriteAccess()){ +if($person->SiteAdmin || $person->CanWrite($cab->AssignedTo)){ echo ""; } else { echo "
".__("Read-only mode: preview and print only.")."
"; From 9ccef7b2ed9722b665ff5abb41930ba74f9fb0e2 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:58:24 +0200 Subject: [PATCH 09/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index 3d090577f..35056bd18 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -2,6 +2,12 @@ require_once "db.inc.php"; require_once "facilities.inc.php"; +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +$person = People::Current(); + header('Content-Type: text/html; charset=utf-8'); $cabinetid = intval($_POST['cabinetid'] ?? 0); @@ -258,7 +264,9 @@ function phaseColor($percent){ echo ""; // person write access echo "
"; -if($person->SiteAdmin || $person->CanWrite($cab->AssignedTo)){ +if($person->SiteAdmin || (isset($cab) && $person->CanWrite($cab->AssignedTo))){ + // DEBUG TEMP + error_log("Planner:: user={$person->UserID} admin={$person->SiteAdmin}"); echo ""; } else { echo "
".__("Read-only mode: preview and print only.")."
"; From 6a7f3f8a9c5ed2997c77076fc752c25f152ae155 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:17:26 +0200 Subject: [PATCH 10/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index 35056bd18..66cb20a44 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -1,21 +1,36 @@ UserID == ""){ + // Sometimes AJAX context fails to restore People::Current() + // -> fallback to People object manually + if(isset($_COOKIE['openDCIMUser'])){ + $person = new People(); + $person->UserID = $_COOKIE['openDCIMUser']; + $person->GetPersonByUserID(); + } +} + +if(!$person || $person->UserID == ""){ + echo '
' + .__("Session expired or user not found. Please reload the page.") + .'
'; + exit; +} + header('Content-Type: text/html; charset=utf-8'); $cabinetid = intval($_POST['cabinetid'] ?? 0); $mode = sanitize($_POST['mode'] ?? 'balanced'); -$person = new People(); -$person->GetUserRights(); - // Get PDU List (instance method) $pdu = new PowerDistribution(); $pdu->CabinetID = $cabinetid; @@ -262,10 +277,14 @@ function phaseColor($percent){ } echo ""; +// person write access +$cab = new Cabinet(); +$cab->CabinetID = $cabinetid; +$cab->GetCabinet(); + // person write access echo "
"; -if($person->SiteAdmin || (isset($cab) && $person->CanWrite($cab->AssignedTo))){ - // DEBUG TEMP +if($person->SiteAdmin || ($cab->AssignedTo && $person->CanWrite($cab->AssignedTo))){ error_log("Planner:: user={$person->UserID} admin={$person->SiteAdmin}"); echo ""; } else { @@ -273,5 +292,6 @@ function phaseColor($percent){ } echo "
"; + $_SESSION["auto_plan_$cabinetid"] = $planRows; From 2b037b5c144b71d9779e71cf863e43a9a2e6b534 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:24:33 +0200 Subject: [PATCH 11/16] feat: add Automatic PDU Link Planner feature --- ajax_apply_powerplan.php | 165 +++++++++++++++++++++++++-------------- cabnavigator.php | 21 +++++ 2 files changed, 129 insertions(+), 57 deletions(-) diff --git a/ajax_apply_powerplan.php b/ajax_apply_powerplan.php index e4f8672e6..62f94a626 100644 --- a/ajax_apply_powerplan.php +++ b/ajax_apply_powerplan.php @@ -1,73 +1,124 @@ UserID == ""){ + if(isset($_COOKIE['openDCIMUser'])){ + $person = new People(); + $person->UserID = $_COOKIE['openDCIMUser']; + $person->GetPersonByUserID(); + } +} + +if(!$person || $person->UserID == ""){ + echo '
' + .__("Session expired or user not found. Please reload the page.") + .'
'; + exit; +} +// --- Retrieve CabinetID and plan data --- $cabinetid = intval($_POST['cabinetid'] ?? 0); -$plan = $_SESSION["auto_plan_$cabinetid"] ?? []; +if($cabinetid <= 0){ + echo '
'.__("Invalid cabinet ID.").'
'; + exit; +} + +$cab = new Cabinet(); +$cab->CabinetID = $cabinetid; +if(!$cab->GetCabinet()){ + echo '
'.__("Cabinet not found.").'
'; + exit; +} + +// --- Check permissions --- +if(!$person->SiteAdmin && !$person->CanWrite($cab->AssignedTo)){ + echo '
' + .__("You do not have sufficient rights to apply this power plan.") + .'
'; + exit; +} -$person = new People(); -$person->GetUserRights(); +// --- Retrieve plan from session --- +$plan = $_SESSION["auto_plan_$cabinetid"] ?? []; -if (!$person->WriteAccess()) { - echo json_encode(["error" => __("You do not have permission to apply this plan.")]); - exit; +if(empty($plan)){ + echo '
'.__("No power plan in memory. Please generate one first.").'
'; + exit; } -$pp = new PowerPorts(); -$dp = new DevicePorts(); +echo "

".__("Applying Power Distribution Plan")."

"; -$ok = 0; $err = []; +// --- Apply connections --- +$success = 0; +$failed = 0; +$total = 0; + +$pc = new PowerConnection(); foreach($plan as $row){ - if(isset($row['Error'])){ continue; } - - // 1) Read PDU chosen port to know ConnectorID/VoltageID constraints - $pduPorts = $pp->getPortList($row['PDUID']); - if(!isset($pduPorts[$row['Port']])){ - $err[] = $row['Device']." – ".__("target PDU port no longer available"); - continue; - } - $pduPortObj = $pduPorts[$row['Port']]; - $needConnID = property_exists($pduPortObj,'ConnectorID') ? intval($pduPortObj->ConnectorID) : null; - $needVoltID = property_exists($pduPortObj,'VoltageID') ? intval($pduPortObj->VoltageID) : null; - - // 2) Pick a FREE device inlet compatible with PDU port - $devPorts = $dp->getPortList($row['DeviceID']); - $devInletObj = null; - foreach($devPorts as $n => $dport){ - // only power inlets - if(property_exists($dport,'PortType') && stripos($dport->PortType,'power')===false) continue; - if(intval($dport->ConnectedDeviceID)!=0) continue; - - // strict match if device exposes ConnectorID/VoltageID, else accept - $okConn = true; $okVolt = true; - if($needConnID && property_exists($dport,'ConnectorID') && $dport->ConnectorID){ - $okConn = (intval($dport->ConnectorID) === $needConnID); - } - if($needVoltID && property_exists($dport,'VoltageID') && $dport->VoltageID){ - $okVolt = (intval($dport->VoltageID) === $needVoltID); - } - if($okConn && $okVolt){ $devInletObj = $dport; break; } - } - if(!$devInletObj){ - $err[] = $row['Device']." – ".__("no free compatible power inlet on device"); - continue; - } - - // 3) Make the power connection (device inlet <-> selected PDU port) - if(!$pp->makeConnection($devInletObj, $pduPortObj)){ - $err[] = $row['Device']." – ".__("failed to make power connection"); - continue; - } - - LogActions::Insert('Device', $row['DeviceID'], 'AutoLink', 'PDU', '', $row['PDUID']); - $ok++; + if( + !isset($row['PDUID']) || + !isset($row['Port']) || + !isset($row['DeviceID']) + ){ + continue; + } + + $total++; + + $pc->PDUID = intval($row['PDUID']); + $pc->PDUPosition = intval($row['Port']); + $pc->DeviceID = intval($row['DeviceID']); + + // Remove any existing connection before creating a new one (avoid duplicate) + $existing = new PowerConnection(); + $existing->PDUID = $pc->PDUID; + $existing->PDUPosition = $pc->PDUPosition; + $existing->RemoveConnection(); + + if($pc->CreateConnection()){ + $success++; + } else { + $failed++; + } } -if($err){ - echo json_encode(["partial"=>true, "applied"=>$ok, "errors"=>$err]); -} else { - echo json_encode(["success"=>true, "applied"=>$ok]); +// --- Output summary --- +echo '
' + .sprintf(__("✅ %d power connections successfully created."), $success) + .'
'; + +if($failed > 0){ + echo '
' + .sprintf(__("⚠ %d connections failed."), $failed) + .'
'; } + +if($success == 0 && $failed == 0){ + echo '
'.__("No valid connections to apply.").'
'; +} + +// --- Final note --- +echo "

" + .sprintf(__("Processed %d total entries."), $total) + ."

"; + +// --- Cleanup temporary session --- +unset($_SESSION["auto_plan_$cabinetid"]); + +?> diff --git a/cabnavigator.php b/cabnavigator.php index 9b0671ebf..f9030dcf9 100644 --- a/cabnavigator.php +++ b/cabnavigator.php @@ -882,6 +882,27 @@ function flippyfloppy(){ }); }); // end +// === Apply and Save button handler === +$(document).on('click','#btnApplyPowerPlan',function(){ + const btn = $(this); + btn.prop('disabled', true).text(""); + $('#autoPlanResult').html('
'); + $.ajax({ + url: 'ajax_apply_powerplan.php', + type: 'POST', + data: { cabinetid: cabinetID }, + success: function(resp){ + $('#autoPlanResult').html(resp); + }, + error: function(){ + alert(""); + }, + complete: function(){ + btn.prop('disabled', false).text(""); + } + }); +}); +//end From 4a94485b1b32a919a680c4c5fa5ebafb89048f2a Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:32:11 +0200 Subject: [PATCH 12/16] feat: add Automatic PDU Link Planner feature --- ajax_apply_powerplan.php | 58 +++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/ajax_apply_powerplan.php b/ajax_apply_powerplan.php index 62f94a626..253dc643f 100644 --- a/ajax_apply_powerplan.php +++ b/ajax_apply_powerplan.php @@ -31,7 +31,9 @@ exit; } -// --- Retrieve CabinetID and plan data --- +header('Content-Type: text/html; charset=utf-8'); + +// --- Retrieve CabinetID --- $cabinetid = intval($_POST['cabinetid'] ?? 0); if($cabinetid <= 0){ echo '
'.__("Invalid cabinet ID.").'
'; @@ -53,55 +55,68 @@ exit; } -// --- Retrieve plan from session --- +// --- Retrieve generated plan --- $plan = $_SESSION["auto_plan_$cabinetid"] ?? []; - if(empty($plan)){ echo '
'.__("No power plan in memory. Please generate one first.").'
'; exit; } +// ----------------------------------------------------------------------------- +// APPLY POWER CONNECTIONS +// ----------------------------------------------------------------------------- echo "

".__("Applying Power Distribution Plan")."

"; -// --- Apply connections --- $success = 0; $failed = 0; $total = 0; -$pc = new PowerConnection(); - foreach($plan as $row){ if( - !isset($row['PDUID']) || - !isset($row['Port']) || - !isset($row['DeviceID']) + empty($row['PDUID']) || + empty($row['Port']) || + empty($row['DeviceID']) ){ continue; } $total++; - $pc->PDUID = intval($row['PDUID']); - $pc->PDUPosition = intval($row['Port']); - $pc->DeviceID = intval($row['DeviceID']); + // On détermine le numéro d’entrée sur le device (DeviceConnNumber) + // Ici, on suppose 1ère alim = 1, 2e alim = 2, etc. + static $feedCount = []; + $devID = intval($row['DeviceID']); + if(!isset($feedCount[$devID])) $feedCount[$devID] = 1; + else $feedCount[$devID]++; - // Remove any existing connection before creating a new one (avoid duplicate) + $conn = new PowerConnection(); + $conn->PDUID = intval($row['PDUID']); + $conn->PDUPosition = intval($row['Port']); + $conn->DeviceID = $devID; + $conn->DeviceConnNumber = $feedCount[$devID]; // important ! + + // Nettoyage éventuel avant création $existing = new PowerConnection(); - $existing->PDUID = $pc->PDUID; - $existing->PDUPosition = $pc->PDUPosition; + $existing->PDUID = $conn->PDUID; + $existing->PDUPosition = $conn->PDUPosition; $existing->RemoveConnection(); - if($pc->CreateConnection()){ + if($conn->CreateConnection()){ $success++; } else { + error_log("❌ Failed to create connection for Device {$conn->DeviceID} Port {$conn->PDUPosition}"); $failed++; } } -// --- Output summary --- -echo '
' - .sprintf(__("✅ %d power connections successfully created."), $success) - .'
'; +// ----------------------------------------------------------------------------- +// RESULT OUTPUT +// ----------------------------------------------------------------------------- +if($success > 0){ + echo '
' + .sprintf(__("✅ %d power connections successfully created."), $success) + .'
'; +} if($failed > 0){ echo '
' @@ -113,12 +128,11 @@ echo '
'.__("No valid connections to apply.").'
'; } -// --- Final note --- echo "

" .sprintf(__("Processed %d total entries."), $total) ."

"; -// --- Cleanup temporary session --- +// --- Cleanup session --- unset($_SESSION["auto_plan_$cabinetid"]); ?> From bc8306fe525b237e84eb26bb0d82349e57624afd Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:47:10 +0200 Subject: [PATCH 13/16] feat: add Automatic PDU Link Planner feature --- ajax_apply_powerplan.php | 64 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/ajax_apply_powerplan.php b/ajax_apply_powerplan.php index 253dc643f..cf80af042 100644 --- a/ajax_apply_powerplan.php +++ b/ajax_apply_powerplan.php @@ -15,7 +15,6 @@ // --- Restore user context --- $person = People::Current(); - if(!$person || $person->UserID == ""){ if(isset($_COOKIE['openDCIMUser'])){ $person = new People(); @@ -23,7 +22,6 @@ $person->GetPersonByUserID(); } } - if(!$person || $person->UserID == ""){ echo '
' .__("Session expired or user not found. Please reload the page.") @@ -62,49 +60,67 @@ exit; } -// ----------------------------------------------------------------------------- -// APPLY POWER CONNECTIONS -// ----------------------------------------------------------------------------- echo "

".__("Applying Power Distribution Plan")."

"; $success = 0; $failed = 0; $total = 0; +global $dbh; + foreach($plan as $row){ - if( - empty($row['PDUID']) || - empty($row['Port']) || - empty($row['DeviceID']) - ){ + if(empty($row['PDUID']) || empty($row['Port']) || empty($row['DeviceID'])){ continue; } $total++; + $PDUID = intval($row['PDUID']); + $portNum = intval($row['Port']); + $deviceID = intval($row['DeviceID']); // On détermine le numéro d’entrée sur le device (DeviceConnNumber) - // Ici, on suppose 1ère alim = 1, 2e alim = 2, etc. static $feedCount = []; - $devID = intval($row['DeviceID']); - if(!isset($feedCount[$devID])) $feedCount[$devID] = 1; - else $feedCount[$devID]++; + if(!isset($feedCount[$deviceID])) $feedCount[$deviceID] = 1; + else $feedCount[$deviceID]++; $conn = new PowerConnection(); - $conn->PDUID = intval($row['PDUID']); - $conn->PDUPosition = intval($row['Port']); - $conn->DeviceID = $devID; - $conn->DeviceConnNumber = $feedCount[$devID]; // important ! + $conn->PDUID = $PDUID; + $conn->PDUPosition = $portNum; + $conn->DeviceID = $deviceID; + $conn->DeviceConnNumber = $feedCount[$deviceID]; - // Nettoyage éventuel avant création + // Supprimer ancienne liaison éventuelle $existing = new PowerConnection(); - $existing->PDUID = $conn->PDUID; - $existing->PDUPosition = $conn->PDUPosition; + $existing->PDUID = $PDUID; + $existing->PDUPosition = $portNum; $existing->RemoveConnection(); + // Crée la connexion (fac_PowerConnection) if($conn->CreateConnection()){ + // --- Synchroniser fac_PowerPorts --- + + // Récupérer port côté PDU + $pp = new PowerPorts(); + $pduPorts = $pp->getPortList($PDUID); + if(isset($pduPorts[$portNum])){ + $pduPort = $pduPorts[$portNum]; + $pduPort->ConnectedDeviceID = $deviceID; + $pduPort->ConnectedPort = $feedCount[$deviceID]; + $pduPort->UpdatePort(); + } + + // Récupérer port côté DEVICE + $devPorts = $pp->getPortList($deviceID); + if(isset($devPorts[$feedCount[$deviceID]])){ + $devPort = $devPorts[$feedCount[$deviceID]]; + $devPort->ConnectedDeviceID = $PDUID; + $devPort->ConnectedPort = $portNum; + $devPort->UpdatePort(); + } + $success++; } else { - error_log("❌ Failed to create connection for Device {$conn->DeviceID} Port {$conn->PDUPosition}"); + error_log("❌ Failed to create connection for Device $deviceID Port $portNum"); $failed++; } } @@ -114,16 +130,14 @@ // ----------------------------------------------------------------------------- if($success > 0){ echo '
' - .sprintf(__("✅ %d power connections successfully created."), $success) + .sprintf(__("✅ %d power connections successfully created and synchronized."), $success) .'
'; } - if($failed > 0){ echo '
' .sprintf(__("⚠ %d connections failed."), $failed) .'
'; } - if($success == 0 && $failed == 0){ echo '
'.__("No valid connections to apply.").'
'; } From 152fe0f21f06bf85b97cbfb06c3b3cab74447fc8 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:15:52 +0200 Subject: [PATCH 14/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 222 ++++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 87 deletions(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index 66cb20a44..fc798f5ff 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -1,8 +1,9 @@ UserID == ""){ - // Sometimes AJAX context fails to restore People::Current() - // -> fallback to People object manually - if(isset($_COOKIE['openDCIMUser'])){ - $person = new People(); - $person->UserID = $_COOKIE['openDCIMUser']; - $person->GetPersonByUserID(); - } + if(isset($_COOKIE['openDCIMUser'])){ + $person = new People(); + $person->UserID = $_COOKIE['openDCIMUser']; + $person->GetPersonByUserID(); + } } if(!$person || $person->UserID == ""){ - echo '
' - .__("Session expired or user not found. Please reload the page.") - .'
'; - exit; + echo '
' + .__("Session expired or user not found. Please reload the page.") + .'
'; + exit; } header('Content-Type: text/html; charset=utf-8'); @@ -31,7 +30,7 @@ $cabinetid = intval($_POST['cabinetid'] ?? 0); $mode = sanitize($_POST['mode'] ?? 'balanced'); -// Get PDU List (instance method) +// Get PDU List $pdu = new PowerDistribution(); $pdu->CabinetID = $cabinetid; $pduList = $pdu->GetPDUbyCabinet(); @@ -45,6 +44,10 @@ echo '
'.__("⚠ Dual Power Path mode requires at least two PDUs.").'
'; exit; } +if ($mode === 'intelligent' && $pduCount < 2) { + echo '
'.__("⚠ Intelligent Power Planner requires at least two PDUs.").'
'; + exit; +} // --- Get devices --- $dev = new Device(); @@ -56,121 +59,185 @@ } // ---------- Helpers ---------- - -// a) free power ports on a PDU, including ConnectorID/PhaseID/VoltageID function getFreePowerPortsForPDUID($pduid){ $pp = new PowerPorts(); - $ports = $pp->getPortList($pduid); // PowerPorts[] indexed by PortNumber + $ports = $pp->getPortList($pduid); $free = []; foreach($ports as $n => $port){ if(intval($port->ConnectedDeviceID) === 0){ - $free[$n] = $port; // keep full object with ConnectorID/PhaseID/VoltageID + $free[$n] = $port; } } return $free; } -// b) get device inlet requirements (array of device power inlets still free, with ConnectorID/VoltageID) -// We read on the DEVICE side to know what connector/voltage it expects. function getDeviceFreeInlets($deviceID){ - $dp = new DevicePorts(); // class provided in your repo - $ports = $dp->getPortList($deviceID); // returns array of ports incl. power inlets + $dp = new DevicePorts(); + $ports = $dp->getPortList($deviceID); $free = []; foreach($ports as $n => $port){ - // We only consider POWER inlets, not data; rely on class typing if present, else heuristic: if(property_exists($port,'PortType') && stripos($port->PortType,'power')===false){ - continue; // not a power inlet + continue; } if(intval($port->ConnectedDeviceID) === 0){ - $free[$n] = $port; // should carry ConnectorID/VoltageID if schema supports + $free[$n] = $port; } } - // Fallback: if DevicePorts doesn't carry ConnectorID/VoltageID, return empty -> no strict filter on device side return $free; } -// c) choose best (PDU,Port) strictly compatible with given device inlet requirements, -// and balancing the phase total load (lower is better). function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $requiredVoltageID, &$phaseLoad){ $best = null; $bestScore = PHP_INT_MAX; - $chosenPhase = 0; - foreach($targetsPDU as $pdu){ $PDUID = $pdu->PDUID; if(!isset($pduState[$PDUID])) continue; foreach($pduState[$PDUID]['free'] as $pn => $portObj){ - // Strict filter: connector + voltage must match if device specified something if($requiredConnectorID && intval($portObj->ConnectorID) !== intval($requiredConnectorID)) continue; if($requiredVoltageID && intval($portObj->VoltageID) !== intval($requiredVoltageID)) continue; - $ph = intval($portObj->PhaseID) ?: 0; $score = intval($phaseLoad[$ph] ?? 0); if($score < $bestScore){ - $bestScore = $score; - $best = ['pdu'=>$pdu,'port'=>$pn,'phase'=>$ph]; + $bestScore = $score; + $best = ['pdu'=>$pdu,'port'=>$pn,'phase'=>$ph]; } } } - return $best; // null if none + return $best; } -// ---------- Build PDU state (free ports) ---------- -$pduState = []; // PDUID => ['obj'=>PowerDistribution, 'free'=>[port#=>PowerPorts]] +// ---------- Build PDU state ---------- +$pduState = []; +$missingConnectorOrPhase = false; + foreach($pduList as $pdu){ + $freePorts = getFreePowerPortsForPDUID($pdu->PDUID); $pduState[$pdu->PDUID] = [ 'obj' => $pdu, - 'free' => getFreePowerPortsForPDUID($pdu->PDUID) + 'free' => $freePorts ]; + + // Vérification des données critiques + foreach($freePorts as $port){ + if(empty($port->ConnectorID) || empty($port->PhaseID)){ + $missingConnectorOrPhase = true; + break 2; // on sort dès qu’un port incomplet est détecté + } + } +} + +// ⚠️ Alerte si ConnectorID ou PhaseID manquant +if($missingConnectorOrPhase && $mode === 'intelligent'){ + echo '
' + .__("⚠ Warning: One or more PDU ports have missing ConnectorID or PhaseID values.
+ Automatic balancing by connector/phase cannot be guaranteed.

+ The planner will proceed in generic mode (balanced only).") + .'
'; } -// Phase load accumulator (unknown phase = 0 bucket) -$phaseLoad = []; // phaseID => watts +$phaseLoad = []; $phaseLabels = [1=>'A',2=>'B',3=>'C']; $planRows = []; $hasMonoFeed = false; -// ---------- Main loop over devices ---------- +// ---------- Main loop ---------- foreach($devices as $dev){ if(!in_array($dev->DeviceType, ['Server','Switch','Appliance','Chassis','Storage Array'])){ continue; } $feeds = max(1, intval($dev->PowerSupplyCount)); $power = intval($dev->Power); $hasMonoFeed = $hasMonoFeed || ($feeds == 1); - - // read FREE inlets on the DEVICE to know expected Connector/Voltage (strict) $deviceFreeInlets = getDeviceFreeInlets($dev->DeviceID); - // Resolve how many feeds we request in this mode - $requestedFeeds = ($mode === 'dualpath') ? min(2, $feeds) : $feeds; + // Mode 3: Intelligent Power Planner + if($mode === 'intelligent'){ + $pdus = array_values($pduList); + if(count($pdus) < 2) continue; + + $pduA = $pdus[0]; + $pduB = $pdus[1]; + + // Carte PDU / Phase / Connecteur + $pduPhaseMap = []; + foreach($pduState as $pid => $pdata){ + foreach($pdata['free'] as $pn => $port){ + $ph = intval($port->PhaseID) ?: 0; + $conn = intval($port->ConnectorID) ?: 0; + if(!isset($pduPhaseMap[$pid][$ph][$conn])) $pduPhaseMap[$pid][$ph][$conn] = []; + $pduPhaseMap[$pid][$ph][$conn][] = $pn; + } + } - // What PDUs are eligible for each feed (depends on mode) - $targets = array_values($pduList); // default: all - if($mode === 'balanced' && count($targets) >= 2){ - // We'll alternate [0],[1],[0],[1] ... - } elseif($mode === 'dualpath' && count($targets) >= 2){ - // feed 1 -> [0], feed 2 -> [1] - } else { - // intelligent: consider all targets each time, scoring by phaseLoad + $assigned = []; + for($f=1; $f<=min($feeds,2); $f++){ + $reqConnectorID = null; + $reqVoltageID = null; + if(!empty($deviceFreeInlets)){ + $inletKey = array_key_first($deviceFreeInlets); + $inlet = $deviceFreeInlets[$inletKey]; + unset($deviceFreeInlets[$inletKey]); + if(property_exists($inlet,'ConnectorID') && $inlet->ConnectorID) $reqConnectorID = intval($inlet->ConnectorID); + if(property_exists($inlet,'VoltageID') && $inlet->VoltageID) $reqVoltageID = intval($inlet->VoltageID); + } + + $targetPDU = ($f == 1) ? $pduA : $pduB; + $best = null; + $bestScore = PHP_INT_MAX; + + foreach($pduPhaseMap[$targetPDU->PDUID] as $phase => $conns){ + if($reqConnectorID && !isset($conns[$reqConnectorID])) continue; + $connKey = $reqConnectorID ?: array_key_first($conns); + if(empty($conns[$connKey])) continue; + $score = $phaseLoad[$phase] ?? 0; + if($score < $bestScore){ + $bestScore = $score; + $best = [ + 'pdu'=>$targetPDU, + 'phase'=>$phase, + 'conn'=>$connKey, + 'port'=>array_shift($pduPhaseMap[$targetPDU->PDUID][$phase][$connKey]) + ]; + } + } + + if(!$best){ + $planRows[] = [ + 'Device'=>$dev->Label, + 'Error'=>sprintf(__("⚠ No compatible port found on %s (connector %d)."), $targetPDU->Label, $reqConnectorID) + ]; + continue; + } + + $planRows[] = [ + 'Device'=>$dev->Label, + 'DeviceID'=>$dev->DeviceID, + 'PDU'=>$best['pdu']->Label, + 'PDUID'=>$best['pdu']->PDUID, + 'Port'=>$best['port'] + ]; + unset($pduState[$best['pdu']->PDUID]['free'][$best['port']]); + $phaseLoad[$best['phase']] = ($phaseLoad[$best['phase']] ?? 0) + ($power / min($feeds,2)); + } + continue; } + // Modes 1 & 2 + $requestedFeeds = ($mode === 'dualpath') ? min(2, $feeds) : $feeds; + $targets = array_values($pduList); $assigned = []; + for($f=0; $f<$requestedFeeds; $f++){ - // Pick a device FREE inlet compatible (for this feed) $reqConnectorID = null; $reqVoltageID = null; if(!empty($deviceFreeInlets)){ - // take one free inlet and use its requirements $inletKey = array_key_first($deviceFreeInlets); $inlet = $deviceFreeInlets[$inletKey]; unset($deviceFreeInlets[$inletKey]); if(property_exists($inlet,'ConnectorID') && $inlet->ConnectorID){ $reqConnectorID = intval($inlet->ConnectorID); } if(property_exists($inlet,'VoltageID') && $inlet->VoltageID){ $reqVoltageID = intval($inlet->VoltageID); } } - // If device side doesn't expose requirements, req* stay null -> no strict filter on that axis. - // Determine eligible PDU set for this feed based on mode $eligiblePDU = $targets; if($mode === 'balanced' && count($targets) >= 2){ $eligiblePDU = [$targets[$f % 2]]; @@ -179,23 +246,16 @@ function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $re $eligiblePDU = [$targets[min($f,1)]]; } - // Pick best PDU/Port strictly matching connector/voltage + min phase load $best = pickBestPduPortStrict($eligiblePDU, $pduState, $reqConnectorID, $reqVoltageID, $phaseLoad); - if($best){ $assigned[] = $best; - // reserve the chosen port unset($pduState[$best['pdu']->PDUID]['free'][$best['port']]); - // update phase load (W divided across feeds of this device) $ph = intval($best['phase']) ?: 0; $phaseLoad[$ph] = ($phaseLoad[$ph] ?? 0) + ($power / max(1,$requestedFeeds)); }else{ - // No compatible outlet found for this feed - $niceConn = ($reqConnectorID) ? sprintf(__("connector #%d"), $reqConnectorID) : __("any connector"); - $niceVolt = ($reqVoltageID) ? sprintf(__("voltage #%d"), $reqVoltageID) : __("any voltage"); $planRows[] = [ 'Device'=>$dev->Label, - 'Error'=>sprintf(__("⚠ No compatible outlet found for this device feed (%s, %s)."), $niceConn, $niceVolt) + 'Error'=>__("⚠ No compatible outlet found for this device feed.") ]; } } @@ -203,17 +263,17 @@ function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $re if(!empty($assigned)){ foreach($assigned as $a){ $planRows[] = [ - 'Device' => $dev->Label, - 'DeviceID' => $dev->DeviceID, - 'PDU' => $a['pdu']->Label, - 'PDUID' => $a['pdu']->PDUID, - 'Port' => $a['port'] + 'Device'=>$dev->Label, + 'DeviceID'=>$dev->DeviceID, + 'PDU'=>$a['pdu']->Label, + 'PDUID'=>$a['pdu']->PDUID, + 'Port'=>$a['port'] ]; } } } -// ---------- HTML Render ---------- +// ---------- HTML Output ---------- echo "

".__("Proposed Power Distribution Plan")."

"; if ($hasMonoFeed) { @@ -240,25 +300,19 @@ function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $re } echo ""; -// ---------- Phase Load Visual Summary ---------- $totalPower = array_sum($phaseLoad); $maxPhaseLoad = ($totalPower > 0) ? max($phaseLoad) : 0; - -// Définir couleurs dynamiques selon % de charge max function phaseColor($percent){ - if($percent > 80){ return "#f44336"; } // rouge - if($percent > 60){ return "#ffc107"; } // jaune - return "#4caf50"; // vert + if($percent > 80){ return "#f44336"; } + if($percent > 60){ return "#ffc107"; } + return "#4caf50"; } - echo "
".__("Visual Load Summary").""; echo ""; - foreach($phaseLoad as $ph => $load){ $label = $phaseLabels[$ph] ?? __("Unknown"); $percent = ($maxPhaseLoad > 0) ? round(($load / $maxPhaseLoad) * 100) : 0; $color = phaseColor($percent); - echo ""; } - echo "
Phase $label @@ -267,31 +321,25 @@ function phaseColor($percent){ ".number_format($load,0)." W
"; - if($totalPower > 0){ echo "

" .sprintf(__("Total Estimated Power: %.1f W"), $totalPower) ."

"; } - echo "
"; -// person write access + $cab = new Cabinet(); $cab->CabinetID = $cabinetid; $cab->GetCabinet(); -// person write access echo "
"; if($person->SiteAdmin || ($cab->AssignedTo && $person->CanWrite($cab->AssignedTo))){ - error_log("Planner:: user={$person->UserID} admin={$person->SiteAdmin}"); echo ""; } else { echo "
".__("Read-only mode: preview and print only.")."
"; } echo "
"; - $_SESSION["auto_plan_$cabinetid"] = $planRows; - +?> From 5645a5468c26806b4d995e6fd540a5ce603c1fc4 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:06:25 +0200 Subject: [PATCH 15/16] feat: add Automatic PDU Link Planner feature --- ajax_generate_powerplan.php | 469 +++++++++++++++------------ locale/fr_FR/LC_MESSAGES/openDCIM.po | 24 ++ 2 files changed, 286 insertions(+), 207 deletions(-) diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php index fc798f5ff..6f2086ffa 100644 --- a/ajax_generate_powerplan.php +++ b/ajax_generate_powerplan.php @@ -3,13 +3,13 @@ session_name("openDCIMSession"); session_start(); } - require_once "db.inc.php"; require_once "facilities.inc.php"; -// --- Ensure $person is loaded --- -$person = People::Current(); +header('Content-Type: text/html; charset=utf-8'); +// --- Ensure user session --- +$person = People::Current(); if(!$person || $person->UserID == ""){ if(isset($_COOKIE['openDCIMUser'])){ $person = new People(); @@ -17,23 +17,24 @@ $person->GetPersonByUserID(); } } - if(!$person || $person->UserID == ""){ - echo '
' - .__("Session expired or user not found. Please reload the page.") - .'
'; + echo '
'.__("Session expired or user not found. Please reload the page.").'
'; exit; } -header('Content-Type: text/html; charset=utf-8'); - $cabinetid = intval($_POST['cabinetid'] ?? 0); -$mode = sanitize($_POST['mode'] ?? 'balanced'); +$mode = sanitize($_POST['mode'] ?? 'balanced'); // balanced | dualpath | intelligent +$force = intval($_POST['force'] ?? 0); // continue mode if metadata issues detected -// Get PDU List -$pdu = new PowerDistribution(); -$pdu->CabinetID = $cabinetid; -$pduList = $pdu->GetPDUbyCabinet(); +// --- Load cabinet --- +$cab = new Cabinet(); +$cab->CabinetID = $cabinetid; +$cab->GetCabinet(); + +// --- Load PDUs in cabinet --- +$pduObj = new PowerDistribution(); +$pduObj->CabinetID = $cabinetid; +$pduList = $pduObj->GetPDUbyCabinet(); $pduCount = is_array($pduList) ? count($pduList) : 0; if ($pduCount == 0) { @@ -45,11 +46,11 @@ exit; } if ($mode === 'intelligent' && $pduCount < 2) { - echo '
'.__("⚠ Intelligent Power Planner requires at least two PDUs.").'
'; + echo '
'.__("⚠ Intelligent Power Planner mode requires at least two PDUs.").'
'; exit; } -// --- Get devices --- +// --- Load devices --- $dev = new Device(); $dev->Cabinet = $cabinetid; $devices = $dev->ViewDevicesByCabinet(); @@ -58,12 +59,33 @@ exit; } -// ---------- Helpers ---------- +/* ========================================================== + Helper functions + ========================================================== */ + +/** + * Get nominal power for a device. + */ +function getDeviceWatts(Device $d){ + if (intval($d->NominalWatts) > 0) return intval($d->NominalWatts); + if (intval($d->TemplateID) > 0) { + $t = new DeviceTemplate(); + $t->TemplateID = $d->TemplateID; + if($t->GetTemplateByID()){ + return intval($t->Wattage); + } + } + return 0; +} + +/** + * Retrieve free power ports from a PDU, including ConnectorID / PhaseID / VoltageID. + */ function getFreePowerPortsForPDUID($pduid){ $pp = new PowerPorts(); $ports = $pp->getPortList($pduid); $free = []; - foreach($ports as $n => $port){ + foreach((array)$ports as $n => $port){ if(intval($port->ConnectedDeviceID) === 0){ $free[$n] = $port; } @@ -71,11 +93,48 @@ function getFreePowerPortsForPDUID($pduid){ return $free; } +/** + * Build a connector/phase map for each PDU: + * $map[PDUID][ConnectorID][PhaseID] => [PortNumbers...] + */ +function buildPduConnectorPhaseMap($pduList){ + $map = []; + $flatFree = []; + $metaIssues = ['missingConnector'=>false, 'missingPhase'=>false]; + + foreach($pduList as $p){ + $PDUID = $p->PDUID; + $map[$PDUID] = []; + $flatFree[$PDUID] = []; + $freePorts = getFreePowerPortsForPDUID($PDUID); + foreach($freePorts as $pn => $port){ + $cid = isset($port->ConnectorID) ? intval($port->ConnectorID) : null; + $ph = isset($port->PhaseID) ? intval($port->PhaseID) : null; + + if (empty($cid)) $metaIssues['missingConnector'] = true; + if (empty($ph)) $metaIssues['missingPhase'] = true; + + $cidKey = $cid ?: 'null'; + $phKey = $ph ?: 'null'; + + if(!isset($map[$PDUID][$cidKey])) $map[$PDUID][$cidKey] = []; + if(!isset($map[$PDUID][$cidKey][$phKey])) $map[$PDUID][$cidKey][$phKey] = []; + $map[$PDUID][$cidKey][$phKey][] = $pn; + + $flatFree[$PDUID][$pn] = $port; + } + } + return [$map, $flatFree, $metaIssues]; +} + +/** + * Get device power inlets still free. + */ function getDeviceFreeInlets($deviceID){ $dp = new DevicePorts(); $ports = $dp->getPortList($deviceID); $free = []; - foreach($ports as $n => $port){ + foreach((array)$ports as $n => $port){ if(property_exists($port,'PortType') && stripos($port->PortType,'power')===false){ continue; } @@ -86,194 +145,188 @@ function getDeviceFreeInlets($deviceID){ return $free; } -function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $requiredVoltageID, &$phaseLoad){ +/** + * Select the best (PDU, Phase, Port) based on: + * - required connector + * - lowest current phase load + */ +function pickBestByConnectorPhase( + $eligiblePDUs, + &$pduMap, + &$phaseLoad, + $reqConnectorID, + $allowNullMeta +){ $best = null; - $bestScore = PHP_INT_MAX; - foreach($targetsPDU as $pdu){ - $PDUID = $pdu->PDUID; - if(!isset($pduState[$PDUID])) continue; - foreach($pduState[$PDUID]['free'] as $pn => $portObj){ - if($requiredConnectorID && intval($portObj->ConnectorID) !== intval($requiredConnectorID)) continue; - if($requiredVoltageID && intval($portObj->VoltageID) !== intval($requiredVoltageID)) continue; - $ph = intval($portObj->PhaseID) ?: 0; - $score = intval($phaseLoad[$ph] ?? 0); - if($score < $bestScore){ - $bestScore = $score; - $best = ['pdu'=>$pdu,'port'=>$pn,'phase'=>$ph]; + $bestLoad = PHP_INT_MAX; + + foreach($eligiblePDUs as $p){ + $PDUID = $p->PDUID; + if(!isset($pduMap[$PDUID])) continue; + + $connectorKeys = []; + if(!is_null($reqConnectorID) && isset($pduMap[$PDUID][$reqConnectorID])){ + $connectorKeys[] = $reqConnectorID; + } elseif($allowNullMeta && isset($pduMap[$PDUID]['null'])) { + $connectorKeys[] = 'null'; + } elseif(is_null($reqConnectorID)){ + $connectorKeys = array_keys($pduMap[$PDUID]); + } + + foreach($connectorKeys as $cidKey){ + foreach($pduMap[$PDUID][$cidKey] as $phKey => $ports){ + if (empty($ports)) continue; + if ($phKey === 'null' && !$allowNullMeta) continue; + $ph = ($phKey === 'null') ? 0 : intval($phKey); + $load = intval($phaseLoad[$PDUID][$ph] ?? 0); + if ($load < $bestLoad){ + $bestLoad = $load; + $best = [ + 'PDUID' => $PDUID, + 'Phase' => $ph, + 'PhaseKey' => $phKey, + 'ConnectorKey' => $cidKey, + 'Port' => $ports[0] + ]; + } } } } return $best; } -// ---------- Build PDU state ---------- -$pduState = []; -$missingConnectorOrPhase = false; - -foreach($pduList as $pdu){ - $freePorts = getFreePowerPortsForPDUID($pdu->PDUID); - $pduState[$pdu->PDUID] = [ - 'obj' => $pdu, - 'free' => $freePorts - ]; - - // Vérification des données critiques - foreach($freePorts as $port){ - if(empty($port->ConnectorID) || empty($port->PhaseID)){ - $missingConnectorOrPhase = true; - break 2; // on sort dès qu’un port incomplet est détecté - } +/* ========================================================== + Build PDU map + ========================================================== */ + +list($pduMap, $pduFree, $metaIssues) = buildPduConnectorPhaseMap($pduList); + +// Warn user if ConnectorID or PhaseID are missing +if (($metaIssues['missingConnector'] || $metaIssues['missingPhase']) && !$force) { + echo "
"; + if ($metaIssues['missingConnector'] && $metaIssues['missingPhase']) { + echo __("Unable to determine connectors and phases for some PDU ports (ConnectorID and PhaseID are null)."); + } elseif ($metaIssues['missingConnector']) { + echo __("Unable to determine connectors for some PDU ports (ConnectorID is null)."); + } else { + echo __("Unable to determine phases for some PDU ports (PhaseID is null)."); } + echo " ".__("The planner will proceed with a simplified balanced mode without connector/phase distinction."); + echo "
".__("Do you want to continue?"); + echo "
"; + ?> +
+ + +
+ + ' - .__("⚠ Warning: One or more PDU ports have missing ConnectorID or PhaseID values.
- Automatic balancing by connector/phase cannot be guaranteed.

- The planner will proceed in generic mode (balanced only).") - .'
'; -} +/* ========================================================== + Power plan computation + ========================================================== */ -$phaseLoad = []; $phaseLabels = [1=>'A',2=>'B',3=>'C']; -$planRows = []; +$phaseLoad = []; +foreach($pduList as $p){ + $phaseLoad[$p->PDUID] = [0=>0,1=>0,2=>0,3=>0]; +} + +$planRows = []; $hasMonoFeed = false; +$pduArray = array_values($pduList); +$pduA = $pduArray[0] ?? null; +$pduB = $pduArray[1] ?? null; -// ---------- Main loop ---------- -foreach($devices as $dev){ - if(!in_array($dev->DeviceType, ['Server','Switch','Appliance','Chassis','Storage Array'])){ continue; } +foreach($devices as $dv){ + if(!in_array($dv->DeviceType, ['Server','Switch','Appliance','Chassis','Storage Array'])) continue; - $feeds = max(1, intval($dev->PowerSupplyCount)); - $power = intval($dev->Power); + $feeds = max(1, intval($dv->PowerSupplyCount)); + $power = getDeviceWatts($dv); $hasMonoFeed = $hasMonoFeed || ($feeds == 1); - $deviceFreeInlets = getDeviceFreeInlets($dev->DeviceID); - - // Mode 3: Intelligent Power Planner - if($mode === 'intelligent'){ - $pdus = array_values($pduList); - if(count($pdus) < 2) continue; - - $pduA = $pdus[0]; - $pduB = $pdus[1]; - - // Carte PDU / Phase / Connecteur - $pduPhaseMap = []; - foreach($pduState as $pid => $pdata){ - foreach($pdata['free'] as $pn => $port){ - $ph = intval($port->PhaseID) ?: 0; - $conn = intval($port->ConnectorID) ?: 0; - if(!isset($pduPhaseMap[$pid][$ph][$conn])) $pduPhaseMap[$pid][$ph][$conn] = []; - $pduPhaseMap[$pid][$ph][$conn][] = $pn; - } - } - - $assigned = []; - for($f=1; $f<=min($feeds,2); $f++){ - $reqConnectorID = null; - $reqVoltageID = null; - if(!empty($deviceFreeInlets)){ - $inletKey = array_key_first($deviceFreeInlets); - $inlet = $deviceFreeInlets[$inletKey]; - unset($deviceFreeInlets[$inletKey]); - if(property_exists($inlet,'ConnectorID') && $inlet->ConnectorID) $reqConnectorID = intval($inlet->ConnectorID); - if(property_exists($inlet,'VoltageID') && $inlet->VoltageID) $reqVoltageID = intval($inlet->VoltageID); - } - - $targetPDU = ($f == 1) ? $pduA : $pduB; - $best = null; - $bestScore = PHP_INT_MAX; - - foreach($pduPhaseMap[$targetPDU->PDUID] as $phase => $conns){ - if($reqConnectorID && !isset($conns[$reqConnectorID])) continue; - $connKey = $reqConnectorID ?: array_key_first($conns); - if(empty($conns[$connKey])) continue; - $score = $phaseLoad[$phase] ?? 0; - if($score < $bestScore){ - $bestScore = $score; - $best = [ - 'pdu'=>$targetPDU, - 'phase'=>$phase, - 'conn'=>$connKey, - 'port'=>array_shift($pduPhaseMap[$targetPDU->PDUID][$phase][$connKey]) - ]; - } - } - - if(!$best){ - $planRows[] = [ - 'Device'=>$dev->Label, - 'Error'=>sprintf(__("⚠ No compatible port found on %s (connector %d)."), $targetPDU->Label, $reqConnectorID) - ]; - continue; - } - - $planRows[] = [ - 'Device'=>$dev->Label, - 'DeviceID'=>$dev->DeviceID, - 'PDU'=>$best['pdu']->Label, - 'PDUID'=>$best['pdu']->PDUID, - 'Port'=>$best['port'] - ]; - unset($pduState[$best['pdu']->PDUID]['free'][$best['port']]); - $phaseLoad[$best['phase']] = ($phaseLoad[$best['phase']] ?? 0) + ($power / min($feeds,2)); - } - continue; - } - - // Modes 1 & 2 + $deviceFreeInlets = getDeviceFreeInlets($dv->DeviceID); $requestedFeeds = ($mode === 'dualpath') ? min(2, $feeds) : $feeds; - $targets = array_values($pduList); - $assigned = []; for($f=0; $f<$requestedFeeds; $f++){ $reqConnectorID = null; - $reqVoltageID = null; - if(!empty($deviceFreeInlets)){ $inletKey = array_key_first($deviceFreeInlets); - $inlet = $deviceFreeInlets[$inletKey]; + $inlet = $deviceFreeInlets[$inletKey]; unset($deviceFreeInlets[$inletKey]); - if(property_exists($inlet,'ConnectorID') && $inlet->ConnectorID){ $reqConnectorID = intval($inlet->ConnectorID); } - if(property_exists($inlet,'VoltageID') && $inlet->VoltageID){ $reqVoltageID = intval($inlet->VoltageID); } + if(property_exists($inlet,'ConnectorID') && $inlet->ConnectorID!==''){ + $reqConnectorID = intval($inlet->ConnectorID); + } } - $eligiblePDU = $targets; - if($mode === 'balanced' && count($targets) >= 2){ - $eligiblePDU = [$targets[$f % 2]]; - } - if($mode === 'dualpath' && count($targets) >= 2){ - $eligiblePDU = [$targets[min($f,1)]]; + if($mode === 'balanced' && count($pduArray) >= 2){ + $eligible = [ $pduArray[$f % 2] ]; + } elseif ($mode === 'dualpath' && $pduA && $pduB){ + $eligible = [ ($f==0 ? $pduA : $pduB) ]; + } elseif ($mode === 'intelligent'){ + if ($feeds >= 2 && $pduA && $pduB){ + $eligible = [ ($f==0 ? $pduA : $pduB) ]; + } else { + $eligible = $pduArray; + } + } else { + $eligible = $pduArray; } - $best = pickBestPduPortStrict($eligiblePDU, $pduState, $reqConnectorID, $reqVoltageID, $phaseLoad); + $best = pickBestByConnectorPhase($eligible, $pduMap, $phaseLoad, $reqConnectorID, (bool)$force); + if($best){ - $assigned[] = $best; - unset($pduState[$best['pdu']->PDUID]['free'][$best['port']]); - $ph = intval($best['phase']) ?: 0; - $phaseLoad[$ph] = ($phaseLoad[$ph] ?? 0) + ($power / max(1,$requestedFeeds)); - }else{ + $PDUID = $best['PDUID']; + $port = $best['Port']; + $phKey = $best['PhaseKey']; + $ph = $best['Phase']; + + // Reserve the port + $bucket =& $pduMap[$PDUID][$best['ConnectorKey']][$phKey]; + $idx = array_search($port, $bucket); + if($idx !== false) array_splice($bucket, $idx, 1); + + // Update load + $phaseLoad[$PDUID][$ph] = ($phaseLoad[$PDUID][$ph] ?? 0) + ($power / max(1,$requestedFeeds)); + + $pduLabel = ''; + foreach($pduList as $p){ if($p->PDUID == $PDUID){ $pduLabel = $p->Label; break; } } + $planRows[] = [ - 'Device'=>$dev->Label, - 'Error'=>__("⚠ No compatible outlet found for this device feed.") + 'Device'=>$dv->Label, + 'DeviceID'=>$dv->DeviceID, + 'PDU'=>$pduLabel ?: "PDU-$PDUID", + 'PDUID'=>$PDUID, + 'Port'=>$port ]; - } - } - - if(!empty($assigned)){ - foreach($assigned as $a){ + }else{ $planRows[] = [ - 'Device'=>$dev->Label, - 'DeviceID'=>$dev->DeviceID, - 'PDU'=>$a['pdu']->Label, - 'PDUID'=>$a['pdu']->PDUID, - 'Port'=>$a['port'] + 'Device'=>$dv->Label, + 'Error'=>sprintf(__("⚠ No compatible outlet found for this device feed (connector %s)."), + $reqConnectorID ?: __("any")) ]; } } } -// ---------- HTML Output ---------- +/* ========================================================== + Render HTML + ========================================================== */ + echo "

".__("Proposed Power Distribution Plan")."

"; if ($hasMonoFeed) { @@ -282,8 +335,7 @@ function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $re . '
'; } -echo " - "; +echo "
".__("Device")."".__("PDU")."".__("Port")."
"; foreach($planRows as $r){ if(isset($r['Error'])){ echo ""; @@ -293,53 +345,56 @@ function pickBestPduPortStrict($targetsPDU, $pduState, $requiredConnectorID, $re } echo "
".__("Device")."".__("PDU")."".__("Port")."
{$r['Device']}{$r['Error']}
"; +$phaseLabels = [1=>'A',2=>'B',3=>'C']; echo "
".__("Phase Load Summary")."
    "; -foreach($phaseLoad as $ph => $load){ - $label = $phaseLabels[$ph] ?? __("Unknown"); - echo "
  • ".sprintf(__("Phase %s"), $label).": ".number_format($load,1)." W
  • "; +$totalPower = 0; +foreach($phaseLoad as $PDUID => $loads){ + $pduName = ''; + foreach($pduList as $p){ if($p->PDUID==$PDUID){ $pduName=$p->Label; break; } } + if(!$pduName) $pduName = "PDU-$PDUID"; + $totalPower += array_sum($loads); + echo "
  • $pduName: "; + $tmp = []; + foreach([1,2,3] as $ph){ + $tmp[] = sprintf("Phase %s: %.1f W", $phaseLabels[$ph], $loads[$ph] ?? 0); + } + if(($loads[0] ?? 0) > 0){ + $tmp[] = sprintf("%s: %.1f W", __("Unknown"), $loads[0]); + } + echo implode(" • ", $tmp)."
  • "; } echo "
"; -$totalPower = array_sum($phaseLoad); -$maxPhaseLoad = ($totalPower > 0) ? max($phaseLoad) : 0; -function phaseColor($percent){ - if($percent > 80){ return "#f44336"; } - if($percent > 60){ return "#ffc107"; } - return "#4caf50"; -} +function phaseColor($p){ if($p>80) return "#f44336"; if($p>60) return "#ffc107"; return "#4caf50"; } + echo "
".__("Visual Load Summary").""; -echo ""; -foreach($phaseLoad as $ph => $load){ - $label = $phaseLabels[$ph] ?? __("Unknown"); - $percent = ($maxPhaseLoad > 0) ? round(($load / $maxPhaseLoad) * 100) : 0; - $color = phaseColor($percent); - echo " - - - - "; -} -echo "
Phase $label -
-
".number_format($load,0)." W
"; -if($totalPower > 0){ - echo "

" - .sprintf(__("Total Estimated Power: %.1f W"), $totalPower) - ."

"; +foreach($phaseLoad as $PDUID => $loads){ + $pduName = ''; + foreach($pduList as $p){ if($p->PDUID==$PDUID){ $pduName=$p->Label; break; } } + if(!$pduName) $pduName = "PDU-$PDUID"; + echo "
$pduName
"; + $maxLoad = max($loads) ?: 1; + echo ""; + foreach([1,2,3,0] as $ph){ + if(($loads[$ph] ?? 0) <= 0) continue; + $label = ($ph==0) ? __("Unknown") : $phaseLabels[$ph]; + $perc = round(($loads[$ph]/$maxLoad)*100); + $col = phaseColor($perc); + echo " + + "; + } + echo "
Phase $label
".number_format($loads[$ph],0)." W
"; } echo "
"; -$cab = new Cabinet(); -$cab->CabinetID = $cabinetid; -$cab->GetCabinet(); - echo "
"; if($person->SiteAdmin || ($cab->AssignedTo && $person->CanWrite($cab->AssignedTo))){ echo ""; -} else { +}else{ echo "
".__("Read-only mode: preview and print only.")."
"; } echo "
"; $_SESSION["auto_plan_$cabinetid"] = $planRows; -?> +?> \ No newline at end of file diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 0ebd0d9a5..12b0cc4bc 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6278,3 +6278,27 @@ msgstr "Fonctionnalité indisponible" msgid "The Automatic PDU Link Planner requires OpenDCIM version 25.01 or newer. Please update your installation to access this feature." msgstr "Le planificateur automatique de liaisons PDU nécessite OpenDCIM version 25.01 ou plus récente. Veuillez mettre à jour votre installation pour accéder à cette fonctionnalité." + +msgid "Unable to determine connectors and phases for some PDU ports (ConnectorID and PhaseID are null)." +msgstr "Impossible de déterminer les connecteurs et les + +msgid "Unable to determine connectors for some PDU ports (ConnectorID is null)." +msgstr "Impossible de déterminer les connecteurs de certains ports PDU (ConnectorID est nul)." + +msgid "Unable to determine phases for some PDU ports (PhaseID is null)." +msgstr "Impossible de déterminer les phases de certains ports PDU (PhaseID est nul)." + +msgid "The planner will proceed with a simplified balanced mode without connector/phase distinction." +msgstr "Le planificateur sera exécuté en mode équilibrage simple sans distinction de connecteur ou de phase." + +msgid "Do you want to continue?" +msgstr "Voulez-vous continuer ?" + +msgid "Operation cancelled." +msgstr "Opération annulée." + +msgid "⚠ No compatible outlet found for this device feed (connector %s)." +msgstr "⚠ Aucune prise compatible trouvée pour cette alimentation de l'équipement (connecteur %s)." + +msgid "any" +msgstr "n'importe lequel" \ No newline at end of file From 4a49429d453850f7477ce24dff8ed15567ee8f9e Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:20:31 +0200 Subject: [PATCH 16/16] feat: add Automatic PDU Link Planner feature --- locale/fr_FR/LC_MESSAGES/openDCIM.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 12b0cc4bc..a90fade33 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6280,7 +6280,7 @@ msgid "The Automatic PDU Link Planner requires OpenDCIM version 25.01 or newer. msgstr "Le planificateur automatique de liaisons PDU nécessite OpenDCIM version 25.01 ou plus récente. Veuillez mettre à jour votre installation pour accéder à cette fonctionnalité." msgid "Unable to determine connectors and phases for some PDU ports (ConnectorID and PhaseID are null)." -msgstr "Impossible de déterminer les connecteurs et les +msgstr "Impossible de déterminer les connecteurs et les phases de certains ports PDU (ConnectorID et PhaseID sont nuls)." msgid "Unable to determine connectors for some PDU ports (ConnectorID is null)." msgstr "Impossible de déterminer les connecteurs de certains ports PDU (ConnectorID est nul)."