*/ public function buildViewData(): array { $tenantActiveStatus = TenantStatus::Active->value; $tenantInactiveStatus = TenantStatus::Inactive->value; $importStatusRunning = 'running'; $importStatusSuccess = 'success'; $importStatusPartial = 'partial'; $importStatusFailed = 'failed'; $systemAuditOutcomeFailed = 'failed'; $systemAuditOutcomeDenied = 'denied'; $lifecycleStatusFailed = 'failed'; $lifecycleStatusSkipped = 'skipped'; $lifecycleActionRestore = 'restore'; $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); $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); $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; }; $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 = []; 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); } $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); $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); $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 ))) : []; $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 ))) : []; $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 ))) : []; } $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 = []; 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; }; $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); $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); $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); $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); $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' ) ?? ''); $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' ) ?? ''); $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' ) ?? ''); 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'; } $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'], ]; } $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'], ]; } $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'], ]; } } $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 = []; 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); } $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); $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); } $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); $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 ))) : []; $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 ))) : []; } $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 = []; 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); } $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); } $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 ))) : []; $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 ))) : []; $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 ))) : []; } $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; 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); $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; $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'] ?? '')); } $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 ) : []; $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 ) : []; $vars = get_defined_vars(); /** @phpstan-ignore unset.offset (get_defined_vars() includes $this in object context; must remove before returning) */ unset($vars['this']); return $vars; } }