From f34373be6d2b1d3cd72f8c96d187696790a84a67 Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Fri, 16 Jan 2026 15:30:32 +0100 Subject: [PATCH 1/5] =?UTF-8?q?N=C2=B07909=20-=20Missing=20spacing=20betwe?= =?UTF-8?q?en=20fields=20when=20columns=20collapse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/backoffice/layout/multi-column/_multi-column.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/css/backoffice/layout/multi-column/_multi-column.scss b/css/backoffice/layout/multi-column/_multi-column.scss index 41edcfe67..998d65e91 100644 --- a/css/backoffice/layout/multi-column/_multi-column.scss +++ b/css/backoffice/layout/multi-column/_multi-column.scss @@ -5,9 +5,11 @@ $ibo-multi-column--margin-x: -16px !default; /* This is to compensate columns padding and make the whole multicolumn align with the parent borders (cf. Bootstrap rows / cols) */ $ibo-multi-column--margin-y: $ibo-spacing-0 !default; +$ibo-multi-column--row-gap: $ibo-spacing-800 !default; .ibo-multi-column { display: flex; flex-wrap: wrap; margin: $ibo-multi-column--margin-y $ibo-multi-column--margin-x; + row-gap: $ibo-multi-column--row-gap; } \ No newline at end of file From 0e0c09c420228d9578c175f3b9429085a53574ea Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 23 Jan 2026 15:55:59 +0100 Subject: [PATCH 2/5] =?UTF-8?q?N=C2=B09027=20-=20Add=20right=20on=20WorkOr?= =?UTF-8?q?der=20transition=20to=20SuperUser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml index 4643c9073..083e045dd 100755 --- a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml +++ b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml @@ -238,6 +238,11 @@ allow + + + allow + + allow From 643752f8e7ec9f94446045bd2f0a0146a483c111 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 23 Jan 2026 16:24:19 +0100 Subject: [PATCH 3/5] =?UTF-8?q?N=C2=B08378=20-=20Missing=20rights=20on=20i?= =?UTF-8?q?ncident=20for=20SuperUser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml index 083e045dd..9011eb298 100755 --- a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml +++ b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml @@ -259,8 +259,10 @@ allow + allow allow allow + allow allow allow From 01adaadfadea9b907195bc7c02fe0537767b4a4c Mon Sep 17 00:00:00 2001 From: v-dumas Date: Mon, 2 Feb 2026 14:56:53 +0100 Subject: [PATCH 4/5] =?UTF-8?q?N=C2=B08492=20-=20Missing=20accent=20for=20?= =?UTF-8?q?'Categorie'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.x/itop-faq-light/dictionaries/fr.dict.itop-faq-light.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodels/2.x/itop-faq-light/dictionaries/fr.dict.itop-faq-light.php b/datamodels/2.x/itop-faq-light/dictionaries/fr.dict.itop-faq-light.php index 9c7ad2e3d..f795b6cdc 100644 --- a/datamodels/2.x/itop-faq-light/dictionaries/fr.dict.itop-faq-light.php +++ b/datamodels/2.x/itop-faq-light/dictionaries/fr.dict.itop-faq-light.php @@ -19,7 +19,7 @@ Dict::Add('FR FR', 'French', 'Français', [ 'Class:FAQ/Attribute:summary+' => '', 'Class:FAQ/Attribute:description' => 'Description', 'Class:FAQ/Attribute:description+' => '', - 'Class:FAQ/Attribute:category_id' => 'Categorie', + 'Class:FAQ/Attribute:category_id' => 'Catégorie', 'Class:FAQ/Attribute:category_id+' => '', 'Class:FAQ/Attribute:category_name' => 'Nom catégorie', 'Class:FAQ/Attribute:category_name+' => '', From 985db46960780562714e77d16164572e1a0075b6 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Thu, 5 Feb 2026 14:57:54 +0100 Subject: [PATCH 5/5] =?UTF-8?q?N=C2=B09193=20-=20Start=20the=20KPI=20logs?= =?UTF-8?q?=20at=20the=20beginning=20of=20the=20http=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/kpi.class.inc.php | 232 +++++++++++++++++++++----------- sources/Core/Kpi/KpiLogData.php | 18 ++- 2 files changed, 168 insertions(+), 82 deletions(-) diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php index fd6f83feb..977b3da44 100644 --- a/core/kpi.class.inc.php +++ b/core/kpi.class.inc.php @@ -1,18 +1,20 @@ 0) { @@ -71,6 +76,7 @@ class ExecutionKPI return true; } } + return false; } @@ -97,7 +103,7 @@ class ExecutionKPI $sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'"; $sSlowQueries = ''; if (self::$m_fSlowQueries > 0) { - $sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s"; + $sSlowQueries = '. Slow Queries: '.self::$m_fSlowQueries.'s'; } $aExtensions = []; @@ -127,7 +133,7 @@ class ExecutionKPI $sRequest .= ' operation: '.$_POST['operation']; } - $fStop = MyHelpers::getmicrotime(); + $fStop = microtime(true); if (($fStop - $fItopStarted) > self::$m_fSlowQueries) { // Invoke extensions to log the KPI operation /** @var \iKPILoggerExtension $oExtensionInstance */ @@ -151,17 +157,17 @@ class ExecutionKPI $sTableStyle = 'background-color: #ccc; margin: 10px;'; - $sHtml = "
"; + $sHtml = '
'; $sHtml .= "
"; $sHtml .= "

KPIs - $sRequest

"; $oStarted = DateTime::createFromFormat('U.u', $fItopStarted); $sHtml .= '

'.$oStarted->format('Y-m-d H:i:s.u').'

'; - $sHtml .= "

log_kpi_user_id: ".UserRights::GetUserId()."

"; - $sHtml .= "
"; + $sHtml .= '

log_kpi_user_id: '.UserRights::GetUserId().'

'; + $sHtml .= '
'; $sHtml .= ""; - $sHtml .= ""; - $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; + $sHtml .= ' '; + $sHtml .= ''; foreach (self::$m_aExecData as $aOpStats) { $sOperation = $aOpStats['op']; $sBegin = round($aOpStats['time_begin'], 3); @@ -180,12 +186,12 @@ class ExecutionKPI } } - $sHtml .= ""; + $sHtml .= ''; $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; } - $sHtml .= "
OperationBeginEndDurationMemory startMemory endMemory peak
OperationBeginEndDurationMemory startMemory endMemory peak
$sOperation$sBegin$sEnd$sDuration$sMemBegin$sMemEnd$sMemPeak
"; - $sHtml .= "
"; + $sHtml .= ''; + $sHtml .= '
'; $aConsolidatedStats = []; foreach (self::$m_aStats as $sOperation => $aOpStats) { @@ -208,20 +214,20 @@ class ExecutionKPI } } $aConsolidatedStats[$sOperation] = [ - 'count' => $iTotalOp, + 'count' => $iTotalOp, 'duration' => $fTotalOp, - 'min' => $fMinOp, - 'max' => $fMaxOp, - 'avg' => $fTotalOp / $iTotalOp, + 'min' => $fMinOp, + 'max' => $fMaxOp, + 'avg' => $fTotalOp / $iTotalOp, 'max_args' => $sMaxOpArguments, ]; } - $sHtml .= "
"; + $sHtml .= '
'; $sHtml .= ""; - $sHtml .= ""; - $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; + $sHtml .= ' '; + $sHtml .= ''; foreach ($aConsolidatedStats as $sOperation => $aOpStats) { $sOperation = ''.$sOperation.''; $sCount = $aOpStats['count']; @@ -230,14 +236,14 @@ class ExecutionKPI $sMax = ''.round($aOpStats['max'], 3).''; $sAvg = round($aOpStats['avg'], 3); - $sHtml .= ""; + $sHtml .= ''; $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; } - $sHtml .= "
OperationCountDurationMinMaxAvg
OperationCountDurationMinMaxAvg
$sOperation$sCount$sDuration$sMin$sMax$sAvg
"; - $sHtml .= "
"; + $sHtml .= ''; + $sHtml .= '
'; - $sHtml .= "
"; + $sHtml .= ''; $sHtml .= "

Next page stats

"; @@ -287,18 +293,18 @@ class ExecutionKPI $sOperationHtml = ''.$sOperation.''; $sHtml .= "

$sOperationHtml

"; $sHtml .= ""; - $sHtml .= ""; - $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; + $sHtml .= ' '; + $sHtml .= ''; $bDisplayHeader = false; } - $sHtml .= ""; + $sHtml .= ''; $sHtml .= " "; - $sHtml .= ""; + $sHtml .= ''; } } if (!$bDisplayHeader) { - $sHtml .= "
Operation details (+ blame caller if log_kpi_duration = 2)CountDurationMinMax
Operation details (+ blame caller if log_kpi_duration = 2)CountDurationMinMax
$sHtmlArguments$iCountInter$sTotalInter$sMinInter$sMaxInter
"; + $sHtml .= ''; $sHtml .= "

Back to page stats

"; } self::Report($sHtml); @@ -333,39 +339,50 @@ class ExecutionKPI $aNewEntry = null; - $fStarted = $this->m_fStarted; - $fStopped = $this->m_fStarted; - if (self::$m_bEnabled_Duration) { - $fStopped = MyHelpers::getmicrotime(); - $aNewEntry = [ - 'op' => $sOperationDesc, - 'time_begin' => $this->m_fStarted - $fItopStarted, - 'time_end' => $fStopped - $fItopStarted, - ]; - // Reset for the next operation (if the object is recycled) - $this->m_fStarted = $fStopped; + if (is_null(static::$fLastReportTime)) { + static::$fLastReportTime = $fItopStarted; } - $iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory; - $iCurrentMemory = 0; - $iPeakMemory = 0; + if (is_null(static::$iLastReportMemory)) { + global $iItopInitialMemory; + static::$iLastReportMemory = $iItopInitialMemory; + } + + $fStarted = static::$fLastReportTime; + $fStopped = microtime(true); + if (self::$m_bEnabled_Duration) { + $aNewEntry = [ + 'op' => $sOperationDesc, + 'time_begin' => $fStarted - $fItopStarted, + 'time_end' => $fStopped - $fItopStarted, + ]; + } + static::$fLastReportTime = $fStopped; + + $iInitialMemory = static::$iLastReportMemory; + $iCurrentMemory = $iInitialMemory; + $iPeakMemory = $iInitialMemory; if (self::$m_bEnabled_Memory) { $iCurrentMemory = self::memory_get_usage(); if (is_null($aNewEntry)) { $aNewEntry = ['op' => $sOperationDesc]; } - $aNewEntry['mem_begin'] = $this->m_iInitialMemory; + $aNewEntry['mem_begin'] = $iInitialMemory; $aNewEntry['mem_end'] = $iCurrentMemory; $iPeakMemory = self::memory_get_peak_usage(); $aNewEntry['mem_peak'] = $iPeakMemory; // Reset for the next operation (if the object is recycled) - $this->m_iInitialMemory = $iCurrentMemory; + static::$iLastReportMemory = $iCurrentMemory; } if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) { - // Invoke extensions to log the KPI operation - /** @var \iKPILoggerExtension $oExtensionInstance */ - foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $aCallstack = ['callstack' => $this->GetCallStack()]; + if (static::$bMetamodelStarted) { + foreach (static::$aBootstrapOperations as $oLog) { + $this->LogOperation($oLog); + } + static::$aBootstrapOperations = []; + // Invoke extensions to log the KPI operation $sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1); $oKPILogData = new KpiLogData( KpiLogData::TYPE_REPORT, @@ -376,9 +393,24 @@ class ExecutionKPI $sExtension, $iInitialMemory, $iCurrentMemory, - $iPeakMemory + $iPeakMemory, + $aCallstack ); - $oExtensionInstance->LogOperation($oKPILogData); + $this->LogOperation($oKPILogData); + } else { + $oKPILogData = new KpiLogData( + KpiLogData::TYPE_REPORT, + 'Step', + $sOperationDesc, + $fStarted, + $fStopped, + '', + $iInitialMemory, + $iCurrentMemory, + $iPeakMemory, + $aCallstack + ); + static::$aBootstrapOperations[] = $oKPILogData; } } @@ -388,13 +420,21 @@ class ExecutionKPI $this->ResetCounters(); } + private function LogOperation(KpiLogData $oKPILogData): void + { + /** @var \iKPILoggerExtension $oExtensionInstance */ + foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { + $oExtensionInstance->LogOperation($oKPILogData); + } + } + /** * Compute statistics for a call to an extension * Note: not working in dev mode (with links to env-production) * * @param object|string $object object called - * @param string $sMethod method called on the object - * @param string $sMessage additional message + * @param string $sMethod method called on the object + * @param string $sMessage additional message * * @return bool true if an extension was found for this object::method * @throws \ReflectionException @@ -423,21 +463,23 @@ class ExecutionKPI $fDuration = 0; if (self::$m_bEnabled_Duration) { - $fStopped = MyHelpers::getmicrotime(); + $fStopped = microtime(true); $fDuration = $fStopped - $this->m_fStarted; $aCallstack = []; if (self::$m_bGenerateLegacyReport) { if (self::$m_bBlameCaller) { $aCallstack = MyHelpers::get_callstack(1); self::$m_aStats[$sOperation][$sArguments][] = [ - 'time' => $fDuration, - 'callers' => $aCallstack, + 'time' => $fDuration, + 'callers' => $aCallstack, ]; } else { self::$m_aStats[$sOperation][$sArguments][] = [ - 'time' => $fDuration, + 'time' => $fDuration, ]; } + } else { + $aCallstack = ['callstack' => $this->GetCallStack()]; } $iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory; @@ -448,33 +490,45 @@ class ExecutionKPI $iPeakMemory = self::memory_get_peak_usage(); } - // Invoke extensions to log the KPI operation - /** @var \iKPILoggerExtension $oExtensionInstance */ - foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) { - //$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1); - $sExtension = ''; + if (static::$bMetamodelStarted) { + foreach (static::$aBootstrapOperations as $oLog) { + $this->LogOperation($oLog); + } + static::$aBootstrapOperations = []; $oKPILogData = new KpiLogData( KpiLogData::TYPE_STATS, $sOperation, $sArguments, $this->m_fStarted, $fStopped, - $sExtension, + '', $iInitialMemory, $iCurrentMemory, $iPeakMemory, $aCallstack ); - $oExtensionInstance->LogOperation($oKPILogData); + $this->LogOperation($oKPILogData); + } else { + $oKPILogData = new KpiLogData( + KpiLogData::TYPE_STATS, + $sOperation, + $sArguments, + $this->m_fStarted, + $fStopped, + '', + $iInitialMemory, + $iCurrentMemory, + $iPeakMemory, + $aCallstack + ); + static::$aBootstrapOperations[] = $oKPILogData; } } } protected function ResetCounters() { - if (self::$m_bEnabled_Duration) { - $this->m_fStarted = microtime(true); - } + $this->m_fStarted = microtime(true); if (self::$m_bEnabled_Memory) { $this->m_iInitialMemory = self::memory_get_usage(); @@ -503,7 +557,33 @@ class ExecutionKPI if (function_exists('memory_get_peak_usage')) { return memory_get_peak_usage($bRealUsage); } + // PHP > 5.2.1 - this verb depends on a compilation option return 0; } + + /* + * ModuleHandlerApiInterface methods + */ + + public static function OnMetaModelStarted() + { + static::$bMetamodelStarted = true; + } + + public function RegisterEventsAndListeners() + { + EventService::RegisterListener(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED, [$this, 'OnMetaModelStarted']); + } + + private function GetCallStack(): string + { + $aCallStack = MyHelpers::get_callstack(2); + $sCallStack = "Call stack:\n"; + foreach ($aCallStack as $index => $aLine) { + $sCallStack .= "#$index ".$aLine['File'].'('.$aLine['Line'].'): '.$aLine['Function']."\n"; + } + + return $sCallStack; + } } diff --git a/sources/Core/Kpi/KpiLogData.php b/sources/Core/Kpi/KpiLogData.php index 2701be80a..9494ea47b 100644 --- a/sources/Core/Kpi/KpiLogData.php +++ b/sources/Core/Kpi/KpiLogData.php @@ -9,8 +9,8 @@ namespace Combodo\iTop\Core\Kpi; class KpiLogData { - public const TYPE_REPORT = 'report'; - public const TYPE_STATS = 'stats'; + public const TYPE_REPORT = 'report'; + public const TYPE_STATS = 'stats'; public const TYPE_REQUEST = 'request'; /** @var string */ @@ -33,6 +33,8 @@ class KpiLogData public $iPeakMemory; /** @var array */ public $aData; + // Computed + public string $sDuration; /** * @param string $sType @@ -43,9 +45,10 @@ class KpiLogData * @param string $sExtension * @param int $iInitialMemory * @param int $iCurrentMemory + * @param int $iPeakMemory * @param array $aData */ - public function __construct($sType, $sOperation, $sArguments, $fStartTime, $fStopTime, $sExtension, $iInitialMemory = 0, $iCurrentMemory = 0, $iPeakMemory = 0, $aData = []) + public function __construct($sType, $sOperation, $sArguments, float $fStartTime, float $fStopTime, $sExtension, $iInitialMemory = 0, $iCurrentMemory = 0, $iPeakMemory = 0, $aData = []) { $this->sType = $sType; $this->sOperation = $sOperation; @@ -57,6 +60,7 @@ class KpiLogData $this->iCurrentMemory = $iCurrentMemory; $this->iPeakMemory = $iPeakMemory; $this->aData = $aData; + $this->sDuration = sprintf('%01.3f', $fStopTime - $fStartTime); } /** @@ -66,21 +70,22 @@ class KpiLogData */ public static function GetCSVHeader() { - return "Type,Operation,Arguments,StartTime,StopTime,Duration,Extension,InitialMemory,CurrentMemory,PeakMemory"; + return 'Type,Operation,Arguments,StartTime,StopTime,Duration,Extension,InitialMemory,CurrentMemory,PeakMemory'; } /** * Return the CSV line for the values + * * @return string */ public function GetCSV() { - $fDuration = sprintf('%01.4f', $this->fStopTime - $this->fStartTime); $sType = $this->RemoveQuotes($this->sType); $sOperation = $this->RemoveQuotes($this->sOperation); $sArguments = $this->RemoveQuotes($this->sArguments); $sExtension = $this->RemoveQuotes($this->sExtension); - return "\"$sType\",\"$sOperation\",\"$sArguments\",$this->fStartTime,$this->fStopTime,$fDuration,\"$sExtension\",$this->iInitialMemory,$this->iCurrentMemory,$this->iPeakMemory"; + + return "\"$sType\",\"$sOperation\",\"$sArguments\",$this->fStartTime,$this->fStopTime,$this->sDuration,\"$sExtension\",$this->iInitialMemory,$this->iCurrentMemory,$this->iPeakMemory"; } private function RemoveQuotes(string $sEntry): string @@ -98,6 +103,7 @@ class KpiLogData if ($oOther->fStartTime > $this->fStartTime) { return -1; } + return 1; }