2026-03-04 15:56:58 +01:00
< ? php
namespace MintyPHP\Repository\Stats ;
2026-03-05 11:17:42 +01:00
use MintyPHP\DB ;
2026-03-04 15:56:58 +01:00
use MintyPHP\Domain\Taxonomy\ImportAuditStatus ;
use MintyPHP\Domain\Taxonomy\MailLogStatus ;
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus ;
use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType ;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome ;
use MintyPHP\Domain\Taxonomy\TenantStatus ;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction ;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus ;
2026-03-05 08:26:51 +01:00
class AdminStatsRepository implements AdminStatsRepositoryInterface
2026-03-04 15:56:58 +01:00
{
/**
* @ return array < string , mixed >
*/
public function buildViewData () : array
{
$tenantActiveStatus = TenantStatus :: Active -> value ;
$tenantInactiveStatus = TenantStatus :: Inactive -> value ;
$importStatusRunning = ImportAuditStatus :: Running -> value ;
$importStatusSuccess = ImportAuditStatus :: Success -> value ;
$importStatusPartial = ImportAuditStatus :: Partial -> value ;
$importStatusFailed = ImportAuditStatus :: Failed -> value ;
$systemAuditOutcomeFailed = SystemAuditOutcome :: Failed -> value ;
$systemAuditOutcomeDenied = SystemAuditOutcome :: Denied -> value ;
$lifecycleStatusFailed = UserLifecycleStatus :: Failed -> value ;
$lifecycleStatusSkipped = UserLifecycleStatus :: Skipped -> value ;
$lifecycleActionRestore = UserLifecycleAction :: Restore -> value ;
$schedulerTriggerScheduler = ScheduledJobTriggerType :: Scheduler -> value ;
$schedulerRunStatusFailed = ScheduledJobRunStatus :: Failed -> value ;
$mailStatusFailed = MailLogStatus :: Failed -> value ;
$mailStatusSent = MailLogStatus :: Sent -> value ;
$activeUserCount = ( int ) ( DB :: selectValue ( 'select count(*) from users where active = 1' ) ? ? 0 );
$inactiveUserCount = ( int ) ( DB :: selectValue ( 'select count(*) from users where active = 0' ) ? ? 0 );
$activeTenantCount = ( int ) ( DB :: selectValue ( 'select count(*) from tenants where status = ?' , $tenantActiveStatus ) ? ? 0 );
$inactiveTenantCount = ( int ) ( DB :: selectValue ( 'select count(*) from tenants where status = ?' , $tenantInactiveStatus ) ? ? 0 );
$activeDepartmentCount = ( int ) ( DB :: selectValue ( 'select count(*) from departments where active = 1' ) ? ? 0 );
$inactiveDepartmentCount = ( int ) ( DB :: selectValue ( 'select count(*) from departments where active = 0' ) ? ? 0 );
$activeRoleCount = ( int ) ( DB :: selectValue ( 'select count(*) from roles where active = 1' ) ? ? 0 );
$inactiveRoleCount = ( int ) ( DB :: selectValue ( 'select count(*) from roles where active = 0' ) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$rolesWithoutPermissionsCount = ( int ) ( DB :: selectValue (
'select count(*) from roles r left join role_permissions rp on rp.role_id = r.id where r.active = 1 and rp.role_id is null'
) ? ? 0 );
$permissionsWithoutRolesCount = ( int ) ( DB :: selectValue (
'select count(*) from permissions p left join role_permissions rp on rp.permission_id = p.id where p.active = 1 and rp.permission_id is null'
) ? ? 0 );
$usersWithoutRolesCount = ( int ) ( DB :: selectValue (
'select count(*) from users u left join user_roles ur on ur.user_id = u.id where u.active = 1 and ur.user_id is null'
) ? ? 0 );
$usersWithoutTenantsCount = ( int ) ( DB :: selectValue (
'select count(*) from users u left join user_tenants ut on ut.user_id = u.id where u.active = 1 and ut.user_id is null'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$flattenStatsRow = static function ( $row ) : array {
if ( ! is_array ( $row )) {
return [];
}
if ( isset ( $row [ '' ]) && is_array ( $row [ '' ])) {
return $row [ '' ];
}
$flat = [];
foreach ( $row as $key => $value ) {
if ( is_array ( $value )) {
$flat = array_merge ( $flat , $value );
} elseif ( is_string ( $key )) {
$flat [ $key ] = $value ;
}
}
return $flat ;
};
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$ssoStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables where table_schema = database() and table_name = 'tenant_auth_microsoft' "
) ? ? 0 ) === 1 ;
$ssoEnabledTenantCount = 0 ;
$ssoEnforcedTenantCount = 0 ;
$ssoEnabledTenantPercent = 0.0 ;
$usersMicrosoftOnlyCount = 0 ;
$usersMicrosoftSyncGapsCount = 0 ;
$ssoTenantRows = [];
$usersMicrosoftOnlyRows = [];
$usersMicrosoftSyncGapRows = [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $ssoStatsAvailable ) {
$ssoEnabledTenantCount = ( int ) ( DB :: selectValue (
" select count(*) from tenants t " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where t.status = ? and coalesce(tam.enabled, 0) = 1 " ,
$tenantActiveStatus
) ? ? 0 );
$ssoEnforcedTenantCount = ( int ) ( DB :: selectValue (
" select count(*) from tenants t " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where t.status = ? and coalesce(tam.enabled, 0) = 1 and coalesce(tam.enforce_microsoft_login, 0) = 1 " ,
$tenantActiveStatus
) ? ? 0 );
if ( $activeTenantCount > 0 ) {
$ssoEnabledTenantPercent = round ((( float ) $ssoEnabledTenantCount * 100 ) / ( float ) $activeTenantCount , 1 );
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$usersMicrosoftOnlyCount = ( int ) ( DB :: selectValue (
" select count(*) from users u " .
" where u.active = 1 " .
" and exists ( " .
" select 1 from user_tenants ut " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" where ut.user_id = u.id " .
" ) " .
" and not exists ( " .
" select 1 from user_tenants ut " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where ut.user_id = u.id " .
" and (coalesce(tam.enabled, 0) = 0 or coalesce(tam.enforce_microsoft_login, 0) = 0) " .
" ) " ,
$tenantActiveStatus ,
$tenantActiveStatus
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$usersMicrosoftSyncGapsCount = ( int ) ( DB :: selectValue (
" select count(*) from ( " .
" select u.id, " .
" trim(coalesce(u.first_name, '')) as first_name_value, " .
" trim(coalesce(u.last_name, '')) as last_name_value, " .
" trim(coalesce(u.phone, '')) as phone_value, " .
" trim(coalesce(u.mobile, '')) as mobile_value, " .
" max(find_in_set('first_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_first_name, " .
" max(find_in_set('last_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_last_name, " .
" max(find_in_set('phone', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_phone, " .
" max(find_in_set('mobile', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_mobile " .
" from users u " .
" join user_tenants ut on ut.user_id = u.id " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where u.active = 1 and u.last_login_provider = 'microsoft' and tam.enabled = 1 and tam.sync_profile_on_login = 1 " .
" group by u.id, u.first_name, u.last_name, u.phone, u.mobile " .
" ) x " .
" where (x.need_first_name = 1 and x.first_name_value = '') " .
" or (x.need_last_name = 1 and x.last_name_value = '') " .
" or (x.need_phone = 1 and x.phone_value = '') " .
" or (x.need_mobile = 1 and x.mobile_value = '') " ,
$tenantActiveStatus
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$ssoTenantRowsRaw = DB :: select (
" select " .
" t.uuid as tenant_uuid, " .
" t.description as tenant_description, " .
" coalesce(tam.enabled, 0) as sso_enabled, " .
" coalesce(tam.enforce_microsoft_login, 0) as enforce_microsoft_login, " .
" coalesce(tam.sync_profile_on_login, 0) as sync_profile_on_login, " .
" coalesce(nullif(tam.sync_profile_fields, ''), '-') as sync_profile_fields " .
" from tenants t " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where t.status = ? " .
" order by sso_enabled desc, enforce_microsoft_login desc, t.description asc " .
" limit 10 " ,
$tenantActiveStatus
);
$ssoTenantRows = is_array ( $ssoTenantRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$tenantUuid = trim (( string ) ( $flat [ 'tenant_uuid' ] ? ? ( $flat [ 'uuid' ] ? ? '' )));
$tenantDescription = trim (( string ) ( $flat [ 'tenant_description' ] ? ? ( $flat [ 'description' ] ? ? '' )));
if ( $tenantUuid === '' || $tenantDescription === '' ) {
return null ;
}
return [
'tenant_uuid' => $tenantUuid ,
'tenant_description' => $tenantDescription ,
'sso_enabled' => ( int ) ( $flat [ 'sso_enabled' ] ? ? ( $flat [ 'enabled' ] ? ? 0 )),
'enforce_microsoft_login' => ( int ) ( $flat [ 'enforce_microsoft_login' ] ? ? 0 ),
'sync_profile_on_login' => ( int ) ( $flat [ 'sync_profile_on_login' ] ? ? 0 ),
'sync_profile_fields' => trim (( string ) ( $flat [ 'sync_profile_fields' ] ? ? '-' )),
];
},
$ssoTenantRowsRaw
))) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$usersMicrosoftOnlyRowsRaw = DB :: select (
" select " .
" u.uuid as user_uuid, " .
" u.display_name as display_name, " .
" u.email as email, " .
" group_concat(distinct t.description order by t.description separator ', ') as tenant_labels " .
" from users u " .
" join user_tenants ut on ut.user_id = u.id " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" left join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where u.active = 1 " .
" group by u.id, u.uuid, u.display_name, u.email " .
" having sum(case when coalesce(tam.enabled, 0) = 0 or coalesce(tam.enforce_microsoft_login, 0) = 0 then 1 else 0 end) = 0 " .
" order by u.display_name asc " .
" limit 10 " ,
$tenantActiveStatus
);
$usersMicrosoftOnlyRows = is_array ( $usersMicrosoftOnlyRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$userUuid = trim (( string ) ( $flat [ 'user_uuid' ] ? ? ( $flat [ 'uuid' ] ? ? '' )));
$email = trim (( string ) ( $flat [ 'user_email' ] ? ? ( $flat [ 'email' ] ? ? '' )));
if ( $userUuid === '' || $email === '' ) {
return null ;
}
return [
'user_uuid' => $userUuid ,
'display_name' => trim (( string ) ( $flat [ 'user_display_name' ] ? ? ( $flat [ 'display_name' ] ? ? '' ))),
'email' => $email ,
'tenant_labels' => trim (( string ) ( $flat [ 'tenant_labels' ] ? ? ( $flat [ 'description' ] ? ? '' ))),
];
},
$usersMicrosoftOnlyRowsRaw
))) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$usersMicrosoftSyncGapRowsRaw = DB :: select (
" select " .
" x.user_uuid, x.user_display_name, x.user_email, " .
" trim(concat_ws(', ', " .
" if(x.need_first_name = 1, 'first_name', null), " .
" if(x.need_last_name = 1, 'last_name', null), " .
" if(x.need_phone = 1, 'phone', null), " .
" if(x.need_mobile = 1, 'mobile', null) " .
" )) as configured_fields, " .
" trim(concat_ws(', ', " .
" if(x.need_first_name = 1 and x.first_name_value = '', 'first_name', null), " .
" if(x.need_last_name = 1 and x.last_name_value = '', 'last_name', null), " .
" if(x.need_phone = 1 and x.phone_value = '', 'phone', null), " .
" if(x.need_mobile = 1 and x.mobile_value = '', 'mobile', null) " .
" )) as missing_fields " .
" from ( " .
" select " .
" u.id, u.uuid as user_uuid, u.display_name as user_display_name, u.email as user_email, " .
" trim(coalesce(u.first_name, '')) as first_name_value, " .
" trim(coalesce(u.last_name, '')) as last_name_value, " .
" trim(coalesce(u.phone, '')) as phone_value, " .
" trim(coalesce(u.mobile, '')) as mobile_value, " .
" max(find_in_set('first_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_first_name, " .
" max(find_in_set('last_name', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_last_name, " .
" max(find_in_set('phone', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_phone, " .
" max(find_in_set('mobile', coalesce(nullif(tam.sync_profile_fields, ''), 'first_name,last_name')) > 0) as need_mobile " .
" from users u " .
" join user_tenants ut on ut.user_id = u.id " .
" join tenants t on t.id = ut.tenant_id and t.status = ? " .
" join tenant_auth_microsoft tam on tam.tenant_id = t.id " .
" where u.active = 1 and u.last_login_provider = 'microsoft' and tam.enabled = 1 and tam.sync_profile_on_login = 1 " .
" group by u.id, u.uuid, u.display_name, u.email, u.first_name, u.last_name, u.phone, u.mobile " .
" ) x " .
" where (x.need_first_name = 1 and x.first_name_value = '') " .
" or (x.need_last_name = 1 and x.last_name_value = '') " .
" or (x.need_phone = 1 and x.phone_value = '') " .
" or (x.need_mobile = 1 and x.mobile_value = '') " .
" order by x.user_display_name asc " .
" limit 10 " ,
$tenantActiveStatus
);
$usersMicrosoftSyncGapRows = is_array ( $usersMicrosoftSyncGapRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$userUuid = trim (( string ) ( $flat [ 'user_uuid' ] ? ? ( $flat [ 'uuid' ] ? ? '' )));
$email = trim (( string ) ( $flat [ 'user_email' ] ? ? ( $flat [ 'email' ] ? ? '' )));
if ( $userUuid === '' || $email === '' ) {
return null ;
}
return [
'user_uuid' => $userUuid ,
'display_name' => trim (( string ) ( $flat [ 'user_display_name' ] ? ? ( $flat [ 'display_name' ] ? ? '' ))),
'email' => $email ,
'configured_fields' => trim (( string ) ( $flat [ 'configured_fields' ] ? ? '' )),
'missing_fields' => trim (( string ) ( $flat [ 'missing_fields' ] ? ? '' )),
];
},
$usersMicrosoftSyncGapRowsRaw
))) : [];
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$customFieldStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() " .
" and table_name in ('tenant_custom_field_definitions', 'tenant_custom_field_options', 'user_custom_field_values') "
) ? ? 0 ) === 3 ;
$customFieldsSelectionWithoutOptionsCount = 0 ;
$customFieldTenantsWithSelectionWithoutOptionsCount = 0 ;
$customFieldsInactiveWithValuesCount = 0 ;
$customFieldsUnusedActiveCount = 0 ;
$customFieldsSelectionWithoutOptionsHref = 'admin/tenants' ;
$customFieldTenantsWithSelectionWithoutOptionsHref = 'admin/tenants' ;
$customFieldsInactiveWithValuesHref = 'admin/tenants' ;
$customFieldsUnusedActiveHref = 'admin/tenants' ;
$customFieldIssueTenantRows = [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $customFieldStatsAvailable ) {
$loadCustomFieldIssueTenants = static function ( string $query ) : array {
$rows = DB :: select ( $query );
if ( ! is_array ( $rows )) {
return [];
}
$items = [];
foreach ( $rows as $row ) {
$tenant = $row [ 'tenants' ] ? ? [];
$meta = $row [ '' ] ? ? [];
$tenantUuid = trim (( string ) ( $tenant [ 'uuid' ] ? ? '' ));
$tenantDescription = trim (( string ) ( $tenant [ 'description' ] ? ? '' ));
$issueCount = ( int ) ( $meta [ 'issue_count' ] ? ? 0 );
if ( $tenantUuid === '' || $tenantDescription === '' || $issueCount <= 0 ) {
continue ;
}
$items [] = [
'tenant_uuid' => $tenantUuid ,
'tenant_description' => $tenantDescription ,
'issue_count' => $issueCount ,
];
}
return $items ;
};
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$customFieldsSelectionWithoutOptionsCount = ( int ) ( DB :: selectValue (
" select count(*) from ( " .
" select d.id " .
" from tenant_custom_field_definitions d " .
" left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
" where d.active = 1 and d.type in ('select', 'multiselect') " .
" group by d.id " .
" having count(o.id) = 0 " .
') missing_options'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$customFieldTenantsWithSelectionWithoutOptionsCount = ( int ) ( DB :: selectValue (
" select count(distinct missing_options.tenant_id) from ( " .
" select d.tenant_id " .
" from tenant_custom_field_definitions d " .
" left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
" where d.active = 1 and d.type in ('select', 'multiselect') " .
" group by d.id, d.tenant_id " .
" having count(o.id) = 0 " .
') missing_options'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$customFieldsInactiveWithValuesCount = ( int ) ( DB :: selectValue (
'select count(distinct d.id) from tenant_custom_field_definitions d ' .
'inner join user_custom_field_values v on v.definition_id = d.id ' .
'where d.active = 0'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$customFieldsUnusedActiveCount = ( int ) ( DB :: selectValue (
" select count(*) from ( " .
" select d.id " .
" from tenant_custom_field_definitions d " .
" left join user_custom_field_values v on v.definition_id = d.id " .
" where d.active = 1 " .
" group by d.id " .
" having count(v.id) = 0 " .
') unused_active'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$selectionWithoutOptionsTenantUuid = ( string ) ( DB :: selectValue (
" select t.uuid from tenants t " .
" inner join ( " .
" select d.tenant_id " .
" from tenant_custom_field_definitions d " .
" left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
" where d.active = 1 and d.type in ('select', 'multiselect') " .
" group by d.id, d.tenant_id " .
" having count(o.id) = 0 " .
" order by d.tenant_id asc " .
" limit 1 " .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ? ? '' );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$inactiveWithValuesTenantUuid = ( string ) ( DB :: selectValue (
" select t.uuid from tenants t " .
" inner join ( " .
" select d.tenant_id " .
" from tenant_custom_field_definitions d " .
" inner join user_custom_field_values v on v.definition_id = d.id " .
" where d.active = 0 " .
" group by d.tenant_id " .
" order by d.tenant_id asc " .
" limit 1 " .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ? ? '' );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$unusedActiveTenantUuid = ( string ) ( DB :: selectValue (
" select t.uuid from tenants t " .
" inner join ( " .
" select d.tenant_id " .
" from tenant_custom_field_definitions d " .
" left join user_custom_field_values v on v.definition_id = d.id " .
" where d.active = 1 " .
" group by d.id, d.tenant_id " .
" having count(v.id) = 0 " .
" order by d.tenant_id asc " .
" limit 1 " .
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ? ? '' );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $selectionWithoutOptionsTenantUuid !== '' ) {
$customFieldsSelectionWithoutOptionsHref = 'admin/tenants/edit/' . $selectionWithoutOptionsTenantUuid . '?tab=custom_fields' ;
$customFieldTenantsWithSelectionWithoutOptionsHref = $customFieldsSelectionWithoutOptionsHref ;
}
if ( $inactiveWithValuesTenantUuid !== '' ) {
$customFieldsInactiveWithValuesHref = 'admin/tenants/edit/' . $inactiveWithValuesTenantUuid . '?tab=custom_fields' ;
}
if ( $unusedActiveTenantUuid !== '' ) {
$customFieldsUnusedActiveHref = 'admin/tenants/edit/' . $unusedActiveTenantUuid . '?tab=custom_fields' ;
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$selectionIssueTenants = $loadCustomFieldIssueTenants (
" select t.uuid, t.description, count(*) as issue_count " .
" from tenants t " .
" inner join ( " .
" select d.id, d.tenant_id " .
" from tenant_custom_field_definitions d " .
" left join tenant_custom_field_options o on o.definition_id = d.id and o.active = 1 " .
" where d.active = 1 and d.type in ('select', 'multiselect') " .
" group by d.id, d.tenant_id " .
" having count(o.id) = 0 " .
" ) issue on issue.tenant_id = t.id " .
" group by t.id, t.uuid, t.description " .
" order by issue_count desc, t.description asc " .
" limit 5 "
);
foreach ( $selectionIssueTenants as $row ) {
$customFieldIssueTenantRows [] = [
'issue' => t ( 'Selection fields without options' ),
'tenant_uuid' => $row [ 'tenant_uuid' ],
'tenant_description' => $row [ 'tenant_description' ],
'issue_count' => $row [ 'issue_count' ],
];
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$inactiveIssueTenants = $loadCustomFieldIssueTenants (
" select t.uuid, t.description, count(distinct d.id) as issue_count " .
" from tenants t " .
" inner join tenant_custom_field_definitions d on d.tenant_id = t.id and d.active = 0 " .
" inner join user_custom_field_values v on v.definition_id = d.id " .
" group by t.id, t.uuid, t.description " .
" order by issue_count desc, t.description asc " .
" limit 5 "
);
foreach ( $inactiveIssueTenants as $row ) {
$customFieldIssueTenantRows [] = [
'issue' => t ( 'Inactive custom fields with values' ),
'tenant_uuid' => $row [ 'tenant_uuid' ],
'tenant_description' => $row [ 'tenant_description' ],
'issue_count' => $row [ 'issue_count' ],
];
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$unusedIssueTenants = $loadCustomFieldIssueTenants (
" select t.uuid, t.description, count(*) as issue_count " .
" from tenants t " .
" inner join ( " .
" select d.id, d.tenant_id " .
" from tenant_custom_field_definitions d " .
" left join user_custom_field_values v on v.definition_id = d.id " .
" where d.active = 1 " .
" group by d.id, d.tenant_id " .
" having count(v.id) = 0 " .
" ) issue on issue.tenant_id = t.id " .
" group by t.id, t.uuid, t.description " .
" order by issue_count desc, t.description asc " .
" limit 5 "
);
foreach ( $unusedIssueTenants as $row ) {
$customFieldIssueTenantRows [] = [
'issue' => t ( 'Unused active custom fields' ),
'tenant_uuid' => $row [ 'tenant_uuid' ],
'tenant_description' => $row [ 'tenant_description' ],
'issue_count' => $row [ 'issue_count' ],
];
}
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$apiStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() and table_name in ('api_audit_log', 'user_api_tokens') "
) ? ? 0 ) === 2 ;
$apiRequests24hCount = 0 ;
$apiRequestsPrev24hCount = 0 ;
$apiRequestTrendPercent = 0.0 ;
$apiErrors4xx24hCount = 0 ;
$apiErrors5xx24hCount = 0 ;
$apiP95DurationMs = 0 ;
$apiTokensExpiring7dCount = 0 ;
$apiTopErrorRows = [];
$apiSlowEndpointRows = [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $apiStatsAvailable ) {
$apiRequests24hCount = ( int ) ( DB :: selectValue (
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR)'
) ? ? 0 );
$apiRequestsPrev24hCount = ( int ) ( DB :: selectValue (
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 48 HOUR) and created_at < (NOW() - INTERVAL 24 HOUR)'
) ? ? 0 );
if ( $apiRequestsPrev24hCount > 0 ) {
$apiRequestTrendPercent = round ((( $apiRequests24hCount - $apiRequestsPrev24hCount ) * 100 ) / $apiRequestsPrev24hCount , 1 );
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$apiErrors4xx24hCount = ( int ) ( DB :: selectValue (
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 400 and 499'
) ? ? 0 );
$apiErrors5xx24hCount = ( int ) ( DB :: selectValue (
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 500 and 599'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$durationCount = ( int ) ( DB :: selectValue (
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null'
) ? ? 0 );
if ( $durationCount > 0 ) {
$offset = ( int ) floor (( $durationCount - 1 ) * 0.95 );
$apiP95DurationMs = ( int ) ( DB :: selectValue (
'select duration_ms from api_audit_log ' .
'where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null ' .
'order by duration_ms asc limit ' . $offset . ', 1'
) ? ? 0 );
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$apiTokensExpiring7dCount = ( int ) ( DB :: selectValue (
'select count(*) from user_api_tokens ' .
'where revoked_at is null and expires_at is not null and expires_at > NOW() and expires_at <= (NOW() + INTERVAL 7 DAY)'
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$apiTopErrorRowsRaw = DB :: select (
" select " .
" coalesce(nullif(error_code, ''), concat('http_', status_code)) as error_label, " .
" count(*) as hit_count " .
" from api_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code >= 400 " .
" group by error_label " .
" order by hit_count desc, error_label asc " .
" limit 5 "
);
$apiTopErrorRows = is_array ( $apiTopErrorRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$label = trim (( string ) ( $flat [ 'error_label' ] ? ? '' ));
$hits = ( int ) ( $flat [ 'hit_count' ] ? ? 0 );
if ( $label === '' || $hits <= 0 ) {
return null ;
}
return [
'error_label' => $label ,
'hit_count' => $hits ,
];
},
$apiTopErrorRowsRaw
))) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$apiSlowEndpointRowsRaw = DB :: select (
" select " .
" path, " .
" count(*) as hit_count, " .
" round(avg(duration_ms), 1) as avg_duration_ms, " .
" max(duration_ms) as max_duration_ms " .
" from api_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null " .
" group by path " .
" having count(*) >= 3 " .
" order by avg(duration_ms) desc, max(duration_ms) desc " .
" limit 5 "
);
$apiSlowEndpointRows = is_array ( $apiSlowEndpointRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$path = trim (( string ) ( $flat [ 'path' ] ? ? '' ));
$hits = ( int ) ( $flat [ 'hit_count' ] ? ? 0 );
if ( $path === '' || $hits <= 0 ) {
return null ;
}
return [
'path' => $path ,
'hit_count' => $hits ,
'avg_duration_ms' => ( float ) ( $flat [ 'avg_duration_ms' ] ? ? 0 ),
'max_duration_ms' => ( int ) ( $flat [ 'max_duration_ms' ] ? ? 0 ),
];
},
$apiSlowEndpointRowsRaw
))) : [];
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$importStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() and table_name = 'import_audit_runs' "
) ? ? 0 ) === 1 ;
$importRuns7dCount = 0 ;
$importRunsPrev7dCount = 0 ;
$importRunTrendPercent = 0.0 ;
$importRowsCreated7dCount = 0 ;
$importRowsFailed7dCount = 0 ;
$importPartialOrFailedRuns7dCount = 0 ;
$importSuccessRate7dPercent = 0.0 ;
$importAverageDurationMs7d = 0 ;
$importRecentRunRows = [];
$importProfileRows = [];
$importTopErrorCodeRows = [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $importStatsAvailable ) {
$importRuns7dCount = ( int ) ( DB :: selectValue (
" select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
$importRunsPrev7dCount = ( int ) ( DB :: selectValue (
" select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 14 DAY) and started_at < (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
if ( $importRunsPrev7dCount > 0 ) {
$importRunTrendPercent = round ((( $importRuns7dCount - $importRunsPrev7dCount ) * 100 ) / $importRunsPrev7dCount , 1 );
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$importRowsCreated7dCount = ( int ) ( DB :: selectValue (
" select coalesce(sum(created_count), 0) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
$importRowsFailed7dCount = ( int ) ( DB :: selectValue (
" select coalesce(sum(failed_count), 0) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
$importPartialOrFailedRuns7dCount = ( int ) ( DB :: selectValue (
" select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) and status in (???) " ,
[ $importStatusPartial , $importStatusFailed ]
) ? ? 0 );
$importAverageDurationMs7d = ( int ) ( DB :: selectValue (
" select round(avg(duration_ms), 0) from import_audit_runs " .
" where started_at >= (NOW() - INTERVAL 7 DAY) and duration_ms is not null "
) ? ? 0 );
$importSuccessRuns7dCount = ( int ) ( DB :: selectValue (
" select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY) and status = ? " ,
$importStatusSuccess
) ? ? 0 );
if ( $importRuns7dCount > 0 ) {
$importSuccessRate7dPercent = round (( $importSuccessRuns7dCount * 100 ) / $importRuns7dCount , 1 );
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$importRecentRunRowsRaw = DB :: select (
" select " .
" import_audit_runs.id, " .
" import_audit_runs.started_at, " .
" import_audit_runs.profile_key, " .
" import_audit_runs.status, " .
" import_audit_runs.rows_total, " .
" import_audit_runs.created_count, " .
" import_audit_runs.failed_count, " .
" import_audit_runs.duration_ms, " .
" users.uuid as user_uuid, " .
" users.display_name as user_display_name, " .
" users.email as user_email " .
" from import_audit_runs " .
" left join users on users.id = import_audit_runs.user_id " .
" order by import_audit_runs.started_at desc " .
" limit 10 "
);
$importRecentRunRows = is_array ( $importRecentRunRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$id = ( int ) ( $flat [ 'id' ] ? ? 0 );
$startedAt = trim (( string ) ( $flat [ 'started_at' ] ? ? '' ));
$profileKey = trim (( string ) ( $flat [ 'profile_key' ] ? ? '' ));
$status = trim (( string ) ( $flat [ 'status' ] ? ? '' ));
if ( $id <= 0 || $startedAt === '' || $profileKey === '' || $status === '' ) {
return null ;
}
return [
'id' => $id ,
'started_at' => $startedAt ,
'profile_key' => $profileKey ,
'status' => $status ,
'rows_total' => ( int ) ( $flat [ 'rows_total' ] ? ? 0 ),
'created_count' => ( int ) ( $flat [ 'created_count' ] ? ? 0 ),
'failed_count' => ( int ) ( $flat [ 'failed_count' ] ? ? 0 ),
'duration_ms' => ( int ) ( $flat [ 'duration_ms' ] ? ? 0 ),
'user_uuid' => trim (( string ) ( $flat [ 'user_uuid' ] ? ? '' )),
'user_display_name' => trim (( string ) ( $flat [ 'user_display_name' ] ? ? '' )),
'user_email' => trim (( string ) ( $flat [ 'user_email' ] ? ? '' )),
];
},
$importRecentRunRowsRaw
))) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$importProfileRowsRaw = DB :: select (
" select " .
" profile_key, " .
" count(*) as run_count, " .
" sum(case when status = ? then 1 else 0 end) as success_count, " .
" coalesce(sum(created_count), 0) as created_count_sum, " .
" coalesce(sum(failed_count), 0) as failed_count_sum " .
" from import_audit_runs " .
" where started_at >= (NOW() - INTERVAL 7 DAY) " .
" group by profile_key " .
" order by run_count desc, profile_key asc " .
" limit 10 " ,
$importStatusSuccess
);
$importProfileRows = is_array ( $importProfileRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$profileKey = trim (( string ) ( $flat [ 'profile_key' ] ? ? '' ));
$runCount = ( int ) ( $flat [ 'run_count' ] ? ? 0 );
if ( $profileKey === '' || $runCount <= 0 ) {
return null ;
}
$successCount = ( int ) ( $flat [ 'success_count' ] ? ? 0 );
$successRate = round (( $successCount * 100 ) / $runCount , 1 );
return [
'profile_key' => $profileKey ,
'run_count' => $runCount ,
'created_count_sum' => ( int ) ( $flat [ 'created_count_sum' ] ? ? 0 ),
'failed_count_sum' => ( int ) ( $flat [ 'failed_count_sum' ] ? ? 0 ),
'success_rate' => $successRate ,
];
},
$importProfileRowsRaw
))) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$importTopErrorRowsRaw = DB :: select (
" select error_codes_json from import_audit_runs " .
" where started_at >= (NOW() - INTERVAL 7 DAY) " .
" and error_codes_json is not null and error_codes_json <> '' " .
" order by started_at desc " .
" limit 1000 "
);
$errorCodeCounts = [];
if ( is_array ( $importTopErrorRowsRaw )) {
foreach ( $importTopErrorRowsRaw as $row ) {
$flat = $flattenStatsRow ( $row );
$json = ( string ) ( $flat [ 'error_codes_json' ] ? ? '' );
if ( $json === '' ) {
continue ;
}
$decoded = json_decode ( $json , true );
if ( ! is_array ( $decoded )) {
continue ;
}
foreach ( $decoded as $code => $count ) {
$codeKey = trim (( string ) $code );
$countValue = ( int ) $count ;
if ( $codeKey === '' || $countValue <= 0 ) {
continue ;
}
$errorCodeCounts [ $codeKey ] = ( int ) ( $errorCodeCounts [ $codeKey ] ? ? 0 ) + $countValue ;
}
}
}
if ( ! empty ( $errorCodeCounts )) {
arsort ( $errorCodeCounts );
foreach ( array_slice ( $errorCodeCounts , 0 , 5 , true ) as $code => $count ) {
$importTopErrorCodeRows [] = [
'error_code' => $code ,
'count' => ( int ) $count ,
];
}
}
}
$auditNow = new \DateTimeImmutable ( 'now' );
$systemAuditCreatedFrom = $auditNow -> modify ( '-1 day' ) -> format ( 'Y-m-d' );
$systemAuditCreatedTo = $auditNow -> format ( 'Y-m-d' );
$lifecycleCreatedFrom = $auditNow -> modify ( '-7 days' ) -> format ( 'Y-m-d' );
$lifecycleCreatedTo = $auditNow -> format ( 'Y-m-d' );
$systemAuditStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() and table_name = 'system_audit_log' "
) ? ? 0 ) === 1 ;
$userLifecycleAuditStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() and table_name = 'user_lifecycle_audit_log' "
) ? ? 0 ) === 1 ;
$auditStatsAvailable = $systemAuditStatsAvailable || $userLifecycleAuditStatsAvailable ;
$systemAuditEvents24hCount = 0 ;
$systemAuditFailed24hCount = 0 ;
$systemAuditDenied24hCount = 0 ;
$systemAuditEventsPrev24hCount = 0 ;
$systemAuditTrendPercent = 0.0 ;
$systemAuditTopEventTypeRows = [];
$systemAuditRecentRiskRows = [];
$systemAuditAll24hHref = 'admin/system-audit' ;
$systemAuditFailed24hHref = 'admin/system-audit' ;
$systemAuditDenied24hHref = 'admin/system-audit' ;
if ( $systemAuditStatsAvailable ) {
$systemAuditEvents24hCount = ( int ) ( DB :: selectValue (
" select count(*) from system_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) "
) ? ? 0 );
$systemAuditFailed24hCount = ( int ) ( DB :: selectValue (
" select count(*) from system_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome = ? " ,
$systemAuditOutcomeFailed
) ? ? 0 );
$systemAuditDenied24hCount = ( int ) ( DB :: selectValue (
" select count(*) from system_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome = ? " ,
$systemAuditOutcomeDenied
) ? ? 0 );
$systemAuditEventsPrev24hCount = ( int ) ( DB :: selectValue (
" select count(*) from system_audit_log " .
" where created_at >= (NOW() - INTERVAL 48 HOUR) and created_at < (NOW() - INTERVAL 24 HOUR) "
) ? ? 0 );
if ( $systemAuditEventsPrev24hCount > 0 ) {
$systemAuditTrendPercent = round (
(( $systemAuditEvents24hCount - $systemAuditEventsPrev24hCount ) * 100 ) / $systemAuditEventsPrev24hCount ,
1
);
}
$systemAuditAll24hHref = 'admin/system-audit?' . http_build_query ([
'created_from' => $systemAuditCreatedFrom ,
'created_to' => $systemAuditCreatedTo ,
]);
$systemAuditFailed24hHref = 'admin/system-audit?' . http_build_query ([
'outcome' => $systemAuditOutcomeFailed ,
'created_from' => $systemAuditCreatedFrom ,
'created_to' => $systemAuditCreatedTo ,
]);
$systemAuditDenied24hHref = 'admin/system-audit?' . http_build_query ([
'outcome' => $systemAuditOutcomeDenied ,
'created_from' => $systemAuditCreatedFrom ,
'created_to' => $systemAuditCreatedTo ,
]);
$systemAuditTopEventTypeRowsRaw = DB :: select (
" select " .
" event_type, " .
" count(*) as hit_count, " .
" sum(case when outcome in (???) then 1 else 0 end) as failed_or_denied_count " .
" from system_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) " .
" group by event_type " .
" order by failed_or_denied_count desc, hit_count desc, event_type asc " .
" limit 5 " ,
[ $systemAuditOutcomeFailed , $systemAuditOutcomeDenied ]
);
$systemAuditTopEventTypeRows = is_array ( $systemAuditTopEventTypeRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$eventType = trim (( string ) ( $flat [ 'event_type' ] ? ? '' ));
$hitCount = ( int ) ( $flat [ 'hit_count' ] ? ? 0 );
if ( $eventType === '' || $hitCount <= 0 ) {
return null ;
}
return [
'event_type' => $eventType ,
'hit_count' => $hitCount ,
'failed_or_denied_count' => ( int ) ( $flat [ 'failed_or_denied_count' ] ? ? 0 ),
];
},
$systemAuditTopEventTypeRowsRaw
))) : [];
$systemAuditRecentRiskRowsRaw = DB :: select (
" select " .
" id, event_type, outcome, channel, created_at " .
" from system_audit_log " .
" where created_at >= (NOW() - INTERVAL 24 HOUR) and outcome in (???) " .
" order by created_at desc " .
" limit 10 " ,
[ $systemAuditOutcomeFailed , $systemAuditOutcomeDenied ]
);
$systemAuditRecentRiskRows = is_array ( $systemAuditRecentRiskRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$id = ( int ) ( $flat [ 'id' ] ? ? 0 );
$eventType = trim (( string ) ( $flat [ 'event_type' ] ? ? '' ));
$outcome = trim (( string ) ( $flat [ 'outcome' ] ? ? '' ));
if ( $id <= 0 || $eventType === '' || $outcome === '' ) {
return null ;
}
return [
'id' => $id ,
'event_type' => $eventType ,
'outcome' => $outcome ,
'channel' => trim (( string ) ( $flat [ 'channel' ] ? ? '' )),
'created_at' => trim (( string ) ( $flat [ 'created_at' ] ? ? '' )),
];
},
$systemAuditRecentRiskRowsRaw
))) : [];
}
$lifecycleRuns7dCount = 0 ;
$lifecycleRunsPrev7dCount = 0 ;
$lifecycleRunTrendPercent = 0.0 ;
$lifecycleFailed7dCount = 0 ;
$lifecycleSkipped7dCount = 0 ;
$lifecycleRestore7dCount = 0 ;
$lifecycleTopReasonRows = [];
$lifecycleRecentRiskRows = [];
$lifecycleAll7dHref = 'admin/user-lifecycle-audit' ;
$lifecycleFailed7dHref = 'admin/user-lifecycle-audit' ;
$lifecycleRestore7dHref = 'admin/user-lifecycle-audit' ;
if ( $userLifecycleAuditStatsAvailable ) {
$lifecycleRuns7dCount = ( int ) ( DB :: selectValue (
" select count(*) from user_lifecycle_audit_log where created_at >= (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
$lifecycleRunsPrev7dCount = ( int ) ( DB :: selectValue (
" select count(*) from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 14 DAY) and created_at < (NOW() - INTERVAL 7 DAY) "
) ? ? 0 );
if ( $lifecycleRunsPrev7dCount > 0 ) {
$lifecycleRunTrendPercent = round (
(( $lifecycleRuns7dCount - $lifecycleRunsPrev7dCount ) * 100 ) / $lifecycleRunsPrev7dCount ,
1
);
}
$lifecycleFailed7dCount = ( int ) ( DB :: selectValue (
" select count(*) from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 7 DAY) and status = ? " ,
$lifecycleStatusFailed
) ? ? 0 );
$lifecycleSkipped7dCount = ( int ) ( DB :: selectValue (
" select count(*) from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 7 DAY) and status = ? " ,
$lifecycleStatusSkipped
) ? ? 0 );
$lifecycleRestore7dCount = ( int ) ( DB :: selectValue (
" select count(*) from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 7 DAY) and action = ? " ,
$lifecycleActionRestore
) ? ? 0 );
$lifecycleAll7dHref = 'admin/user-lifecycle-audit?' . http_build_query ([
'created_from' => $lifecycleCreatedFrom ,
'created_to' => $lifecycleCreatedTo ,
]);
$lifecycleFailed7dHref = 'admin/user-lifecycle-audit?' . http_build_query ([
'statuses' => $lifecycleStatusFailed . ',' . $lifecycleStatusSkipped ,
'created_from' => $lifecycleCreatedFrom ,
'created_to' => $lifecycleCreatedTo ,
]);
$lifecycleRestore7dHref = 'admin/user-lifecycle-audit?' . http_build_query ([
'actions' => $lifecycleActionRestore ,
'created_from' => $lifecycleCreatedFrom ,
'created_to' => $lifecycleCreatedTo ,
]);
$lifecycleTopReasonRowsRaw = DB :: select (
" select " .
" coalesce(nullif(reason_code, ''), '-') as reason_code, " .
" count(*) as hit_count " .
" from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 7 DAY) " .
" group by reason_code " .
" order by hit_count desc, reason_code asc " .
" limit 5 "
);
$lifecycleTopReasonRows = is_array ( $lifecycleTopReasonRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$reasonCode = trim (( string ) ( $flat [ 'reason_code' ] ? ? '-' ));
$hitCount = ( int ) ( $flat [ 'hit_count' ] ? ? 0 );
if ( $hitCount <= 0 ) {
return null ;
}
if ( $reasonCode === '' ) {
$reasonCode = '-' ;
}
return [
'reason_code' => $reasonCode ,
'hit_count' => $hitCount ,
];
},
$lifecycleTopReasonRowsRaw
))) : [];
$lifecycleRecentRiskRowsRaw = DB :: select (
" select " .
" id, action, status, trigger_type, target_user_uuid, target_user_email, created_at " .
" from user_lifecycle_audit_log " .
" where created_at >= (NOW() - INTERVAL 7 DAY) " .
" and (status in (???) or action = ?) " .
" order by created_at desc " .
" limit 10 " ,
[ $lifecycleStatusFailed , $lifecycleStatusSkipped ],
$lifecycleActionRestore
);
$lifecycleRecentRiskRows = is_array ( $lifecycleRecentRiskRowsRaw ) ? array_values ( array_filter ( array_map (
static function ( $row ) use ( $flattenStatsRow ) : ? array {
$flat = $flattenStatsRow ( $row );
$id = ( int ) ( $flat [ 'id' ] ? ? 0 );
$action = trim (( string ) ( $flat [ 'action' ] ? ? '' ));
$status = trim (( string ) ( $flat [ 'status' ] ? ? '' ));
if ( $id <= 0 || $action === '' || $status === '' ) {
return null ;
}
return [
'id' => $id ,
'action' => $action ,
'status' => $status ,
'trigger_type' => trim (( string ) ( $flat [ 'trigger_type' ] ? ? '' )),
'target_user_uuid' => trim (( string ) ( $flat [ 'target_user_uuid' ] ? ? '' )),
'target_user_email' => trim (( string ) ( $flat [ 'target_user_email' ] ? ? '' )),
'created_at' => trim (( string ) ( $flat [ 'created_at' ] ? ? '' )),
];
},
$lifecycleRecentRiskRowsRaw
))) : [];
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$scheduledStatsAvailable = ( int ) ( DB :: selectValue (
" select count(*) from information_schema.tables " .
" where table_schema = database() and table_name in ('scheduled_jobs', 'scheduled_job_runs', 'scheduler_runtime_status') "
) ? ? 0 ) === 3 ;
$scheduledEnabledJobsCount = 0 ;
$scheduledOverdueJobsCount = 0 ;
$scheduledRuns24hCount = 0 ;
$scheduledFailedRuns24hCount = 0 ;
$schedulerRunnerLastHeartbeatAt = '' ;
$schedulerRunnerLastResult = '' ;
$schedulerRunnerLastErrorCode = '' ;
$schedulerRunnerIsActive = false ;
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
if ( $scheduledStatsAvailable ) {
$scheduledEnabledJobsCount = ( int ) ( DB :: selectValue (
" select count(*) from scheduled_jobs where enabled = 1 "
) ? ? 0 );
$scheduledOverdueJobsCount = ( int ) ( DB :: selectValue (
" select count(*) from scheduled_jobs " .
" where enabled = 1 and next_run_at is not null and next_run_at <= NOW() "
) ? ? 0 );
$scheduledRuns24hCount = ( int ) ( DB :: selectValue (
" select count(*) from scheduled_job_runs " .
" where trigger_type = ? and started_at >= (NOW() - INTERVAL 24 HOUR) " ,
$schedulerTriggerScheduler
) ? ? 0 );
$scheduledFailedRuns24hCount = ( int ) ( DB :: selectValue (
" select count(*) from scheduled_job_runs " .
" where trigger_type = ? and status = ? and started_at >= (NOW() - INTERVAL 24 HOUR) " ,
$schedulerTriggerScheduler ,
$schedulerRunStatusFailed
) ? ? 0 );
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$schedulerRunnerIsActive = ( int ) ( DB :: selectValue (
" select case when last_heartbeat_at >= (NOW() - INTERVAL 3 MINUTE) then 1 else 0 end " .
" from scheduler_runtime_status where id = 1 "
) ? ? 0 ) === 1 ;
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$schedulerRuntimeRowRaw = DB :: selectOne (
" select last_heartbeat_at, last_result, last_error_code from scheduler_runtime_status where id = 1 limit 1 "
);
$schedulerRuntimeRow = $flattenStatsRow ( $schedulerRuntimeRowRaw );
$schedulerRunnerLastHeartbeatAt = trim (( string ) ( $schedulerRuntimeRow [ 'last_heartbeat_at' ] ? ? '' ));
$schedulerRunnerLastResult = trim (( string ) ( $schedulerRuntimeRow [ 'last_result' ] ? ? '' ));
$schedulerRunnerLastErrorCode = trim (( string ) ( $schedulerRuntimeRow [ 'last_error_code' ] ? ? '' ));
}
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$usersUnverifiedCount = ( int ) ( DB :: selectValue (
'select count(*) from users where active = 1 and email_verified_at is null'
) ? ? 0 );
$usersNeverLoggedCount = ( int ) ( DB :: selectValue (
'select count(*) from users where active = 1 and last_login_at is null'
) ? ? 0 );
$recentLogins = DB :: select (
'select id, uuid, first_name, last_name, email, last_login_at from users where active = 1 and last_login_at is not null order by last_login_at desc limit 10'
);
$recentLogins = is_array ( $recentLogins ) ? array_map (
static fn ( $row ) => $row [ 'users' ] ? ? $row ,
$recentLogins
) : [];
$neverLoggedUsers = DB :: select (
'select id, uuid, first_name, last_name, email, created from users where active = 1 and last_login_at is null order by created desc limit 10'
);
$neverLoggedUsers = is_array ( $neverLoggedUsers ) ? array_map (
static fn ( $row ) => $row [ 'users' ] ? ? $row ,
$neverLoggedUsers
) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$mailLogFailedCount = ( int ) ( DB :: selectValue ( 'select count(*) from mail_log where status = ?' , $mailStatusFailed ) ? ? 0 );
$mailLogSentCount = ( int ) ( DB :: selectValue ( 'select count(*) from mail_log where status = ?' , $mailStatusSent ) ? ? 0 );
$mailLogLastSentAt = ( string ) ( DB :: selectValue ( 'select max(sent_at) from mail_log where status = ?' , $mailStatusSent ) ? ? '' );
$mailLogRecentFailed = DB :: select (
'select id, to_email, subject, error_message, created_at from mail_log where status = ? order by created_at desc limit 5' ,
$mailStatusFailed
);
$mailLogRecentFailed = is_array ( $mailLogRecentFailed ) ? array_map (
static fn ( $row ) => $row [ 'mail_log' ] ? ? $row ,
$mailLogRecentFailed
) : [];
2026-03-05 11:17:42 +01:00
2026-03-04 15:56:58 +01:00
$vars = get_defined_vars ();
unset ( $vars [ 'this' ]);
return $vars ;
}
}