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 ""; +foreach($planRows as $r){ + if(isset($r['Error'])){ + echo ""; + } else { + echo ""; + } +} +echo "
".__("Device")."".__("PDU")."".__("Port")."
{$r['Device']}{$r['Error']}
{$r['Device']}{$r['PDU']}{$r['Port']}
"; + +$phaseLabels = [1=>'A',2=>'B',3=>'C']; +echo "
".__("Phase Load Summary")."
"; + +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 " + + "; + } + echo "
Phase $label
".number_format($loads[$ph],0)." W
"; +} +echo "
"; + +echo "
"; +if($person->SiteAdmin || ($cab->AssignedTo && $person->CanWrite($cab->AssignedTo))){ + echo ""; +}else{ + echo "
".__("Read-only mode: preview and print only.")."
"; +} +echo "
"; + +$_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 .= '
+ +
'; + } + } + // 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 = ` +
+

${i18n.select}

+
+
+ +
`; + $(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 diff --git a/classes/PowerPorts.class.php b/classes/PowerPorts.class.php index 08fa3067d..1ac6a45c7 100644 --- a/classes/PowerPorts.class.php +++ b/classes/PowerPorts.class.php @@ -56,22 +56,22 @@ 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->ConnectorID=$dbRow['ConnectorID']; - $pp->PhaseID=$dbRow['PhaseID']; - $pp->VoltageID=$dbRow['VoltageID']; - $pp->ConnectedDeviceID=$dbRow['ConnectedDeviceID']; - $pp->ConnectedPort=$dbRow['ConnectedPort']; - $pp->Notes=$dbRow['Notes']; - - $pp->MakeDisplay(); - - return $pp; - } + public static function RowToObject($dbRow){ + $pp = new PowerPorts(); + $pp->DeviceID = $dbRow['DeviceID']; + $pp->PortNumber = $dbRow['PortNumber']; + $pp->Label = $dbRow['Label']; + // ➜ new 25.01 Optional fields (backward compatibility) + 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']; } + $pp->ConnectedDeviceID = $dbRow['ConnectedDeviceID']; + $pp->ConnectedPort = $dbRow['ConnectedPort']; + $pp->Notes = $dbRow['Notes']; + + $pp->MakeDisplay(); + return $pp; +} function getPort(){ global $dbh; @@ -384,7 +384,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" ){ diff --git a/css/inventory.php b/css/inventory.php index 0b4dd74c7..8bc1820fe 100644 --- a/css/inventory.php +++ b/css/inventory.php @@ -1482,5 +1482,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; +} + /* --- 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; +} diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 4b1d3370d..e21259004 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6359,3 +6359,102 @@ msgstr "" #~ msgid "MailTo" #~ msgstr "Envoyé à " + +msgid "Automatic PDU Link Planner" +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 – Double alimentation" + +msgid "Mode 3 – Intelligent Power Planner" +msgstr "Mode 3 – Planificateur intelligent" + +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 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é." + +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 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)." + +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