diff --git a/ajax_apply_powerplan.php b/ajax_apply_powerplan.php
new file mode 100644
index 000000000..cf80af042
--- /dev/null
+++ b/ajax_apply_powerplan.php
@@ -0,0 +1,152 @@
+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;
+}
+
+header('Content-Type: text/html; charset=utf-8');
+
+// --- Retrieve CabinetID ---
+$cabinetid = intval($_POST['cabinetid'] ?? 0);
+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;
+}
+
+// --- Retrieve generated plan ---
+$plan = $_SESSION["auto_plan_$cabinetid"] ?? [];
+if(empty($plan)){
+ echo ''.__("No power plan in memory. Please generate one first.").'
';
+ exit;
+}
+
+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'])){
+ 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)
+ static $feedCount = [];
+ if(!isset($feedCount[$deviceID])) $feedCount[$deviceID] = 1;
+ else $feedCount[$deviceID]++;
+
+ $conn = new PowerConnection();
+ $conn->PDUID = $PDUID;
+ $conn->PDUPosition = $portNum;
+ $conn->DeviceID = $deviceID;
+ $conn->DeviceConnNumber = $feedCount[$deviceID];
+
+ // Supprimer ancienne liaison éventuelle
+ $existing = new PowerConnection();
+ $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 $deviceID Port $portNum");
+ $failed++;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// RESULT OUTPUT
+// -----------------------------------------------------------------------------
+if($success > 0){
+ echo ''
+ .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.").'
';
+}
+
+echo ""
+ .sprintf(__("Processed %d total entries."), $total)
+ ."
";
+
+// --- Cleanup session ---
+unset($_SESSION["auto_plan_$cabinetid"]);
+
+?>
diff --git a/ajax_generate_powerplan.php b/ajax_generate_powerplan.php
new file mode 100644
index 000000000..6f2086ffa
--- /dev/null
+++ b/ajax_generate_powerplan.php
@@ -0,0 +1,400 @@
+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;
+}
+
+$cabinetid = intval($_POST['cabinetid'] ?? 0);
+$mode = sanitize($_POST['mode'] ?? 'balanced'); // balanced | dualpath | intelligent
+$force = intval($_POST['force'] ?? 0); // continue mode if metadata issues detected
+
+// --- 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) {
+ echo ''.__("⚠ No PDU detected in this cabinet.").'
';
+ exit;
+}
+if ($mode === 'dualpath' && $pduCount < 2) {
+ echo ''.__("⚠ Dual Power Path mode requires at least two PDUs.").'
';
+ exit;
+}
+if ($mode === 'intelligent' && $pduCount < 2) {
+ echo ''.__("⚠ Intelligent Power Planner mode requires at least two PDUs.").'
';
+ exit;
+}
+
+// --- Load devices ---
+$dev = new Device();
+$dev->Cabinet = $cabinetid;
+$devices = $dev->ViewDevicesByCabinet();
+if (empty($devices)) {
+ echo ''.__("ℹ No devices detected in this cabinet.").'
';
+ exit;
+}
+
+/* ==========================================================
+ 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((array)$ports as $n => $port){
+ if(intval($port->ConnectedDeviceID) === 0){
+ $free[$n] = $port;
+ }
+ }
+ 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((array)$ports as $n => $port){
+ if(property_exists($port,'PortType') && stripos($port->PortType,'power')===false){
+ continue;
+ }
+ if(intval($port->ConnectedDeviceID) === 0){
+ $free[$n] = $port;
+ }
+ }
+ return $free;
+}
+
+/**
+ * Select the best (PDU, Phase, Port) based on:
+ * - required connector
+ * - lowest current phase load
+ */
+function pickBestByConnectorPhase(
+ $eligiblePDUs,
+ &$pduMap,
+ &$phaseLoad,
+ $reqConnectorID,
+ $allowNullMeta
+){
+ $best = null;
+ $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 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 "
";
+ ?>
+
+
+
+
+
+ 'A',2=>'B',3=>'C'];
+$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;
+
+foreach($devices as $dv){
+ if(!in_array($dv->DeviceType, ['Server','Switch','Appliance','Chassis','Storage Array'])) continue;
+
+ $feeds = max(1, intval($dv->PowerSupplyCount));
+ $power = getDeviceWatts($dv);
+ $hasMonoFeed = $hasMonoFeed || ($feeds == 1);
+ $deviceFreeInlets = getDeviceFreeInlets($dv->DeviceID);
+ $requestedFeeds = ($mode === 'dualpath') ? min(2, $feeds) : $feeds;
+
+ for($f=0; $f<$requestedFeeds; $f++){
+ $reqConnectorID = 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($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 = pickBestByConnectorPhase($eligible, $pduMap, $phaseLoad, $reqConnectorID, (bool)$force);
+
+ if($best){
+ $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'=>$dv->Label,
+ 'DeviceID'=>$dv->DeviceID,
+ 'PDU'=>$pduLabel ?: "PDU-$PDUID",
+ 'PDUID'=>$PDUID,
+ 'Port'=>$port
+ ];
+ }else{
+ $planRows[] = [
+ 'Device'=>$dv->Label,
+ 'Error'=>sprintf(__("⚠ No compatible outlet found for this device feed (connector %s)."),
+ $reqConnectorID ?: __("any"))
+ ];
+ }
+ }
+}
+
+/* ==========================================================
+ Render HTML
+ ========================================================== */
+
+echo "".__("Proposed Power Distribution Plan")." ";
+
+if ($hasMonoFeed) {
+ echo ''
+ . __("⚠ Single-power devices detected. Installing a Static Transfer Switch (STS) is recommended to enhance power redundancy.")
+ . '
';
+}
+
+echo "".__("Device")." ".__("PDU")." ".__("Port")." ";
+foreach($planRows as $r){
+ if(isset($r['Error'])){
+ echo "{$r['Device']} {$r['Error']} ";
+ } else {
+ echo "{$r['Device']} {$r['PDU']} {$r['Port']} ";
+ }
+}
+echo "
";
+
+$phaseLabels = [1=>'A',2=>'B',3=>'C'];
+echo "".__("Phase Load Summary")." ";
+$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 " ";
+
+function phaseColor($p){ if($p>80) return "#f44336"; if($p>60) return "#ffc107"; return "#4caf50"; }
+
+echo "".__("Visual Load Summary")." ";
+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 "Phase $label
+
+ ".number_format($loads[$ph],0)." W ";
+ }
+ echo "
";
+}
+echo " ";
+
+echo "";
+if($person->SiteAdmin || ($cab->AssignedTo && $person->CanWrite($cab->AssignedTo))){
+ echo "
".__("Apply and Save")." ";
+}else{
+ echo "
".__("Read-only mode: preview and print only.")."
";
+}
+echo "
".__("Print Power Plan")." ";
+
+$_SESSION["auto_plan_$cabinetid"] = $planRows;
+?>
\ No newline at end of file
diff --git a/cabnavigator.php b/cabnavigator.php
index 6e9753d81..f9030dcf9 100644
--- a/cabnavigator.php
+++ b/cabnavigator.php
@@ -366,7 +366,26 @@ function renderUnassignedTemplateOwnership($noTemplFlag, $noOwnerFlag, $device)
if($person->CanWrite($cab->AssignedTo)){
$body.="\n\t\t\n";
}
-
+ // --- Feature Guard: Require OpenDCIM 25.01 or later ---
+ //feature automatic-pdu-link-planner
+ $currentVersion = floatval($config->ParameterArray["Version"]);
+
+ if ($currentVersion < 25.01) {
+ $body .= '
+ '.__("Feature unavailable").': '
+ .__("The Automatic PDU Link Planner requires OpenDCIM version 25.01 or newer. Please update your installation to access this feature.")
+ .'
';
+ } else {
+ // Display the button if user has at least read access
+ if ($person->CanRead($cab->AssignedTo)) {
+ $body .= '
+ '
+ . __('Automatic PDU Link Planner') .
+ '
+
';
+ }
+ }
+ // end
$body.="\t\t\n\t\n";
$body.='
@@ -383,7 +402,6 @@ function renderUnassignedTemplateOwnership($noTemplFlag, $noOwnerFlag, $device)
$body.="\t\t\n\t \n";
-
if ($person->CanWrite($cab->AssignedTo) || $person->SiteAdmin) {
$body.="\t\n";
if ($person->CanWrite($cab->AssignedTo) ) {
@@ -818,6 +836,73 @@ function flippyfloppy(){
+var cabinetID = CabinetID); ?>;
+// 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 = `
+ `;
+ $(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();
+ $.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
+// === 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