"2026-08-21 / 09:00:47". $format_log_time = static function ( ?string $ts ): string { return $ts ? str_replace( ' ', ' / ', $ts ) : '—'; }; $active_tab = sanitize_key( wp_unslash( $_GET['tab'] ?? 'sessions' ) ); // ── Sessions tab data ────────────────────────────────────────────────── $graph_offset = absint( wp_unslash( $_GET['graph_offset'] ?? 0 ) ); $graph_to = gmdate( 'Y-m-d', strtotime( "-{$graph_offset} days" ) ); $graph_from = gmdate( 'Y-m-d', strtotime( '-' . ( $graph_offset + 29 ) . ' days' ) ); $filter_date = sanitize_text_field( wp_unslash( $_GET['session_date'] ?? '' ) ); $highlight_session = absint( wp_unslash( $_GET['highlight_session'] ?? 0 ) ); $daily = AICOM_Audit_Logger::get_daily_sessions( $graph_from, $graph_to ); $max_count = max( 1, max( array_column( $daily, 'total' ) ?: [ 1 ] ) ); $has_older = AICOM_Audit_Logger::has_logs_before( $graph_from ); $class_order = [ 'admin_sensitive', 'destructive', 'write', 'read', 'discovery', 'public' ]; $class_labels = [ 'public' => __( 'Public', 'aicom' ), 'discovery' => __( 'Discovery', 'aicom' ), 'read' => __( 'Read', 'aicom' ), 'write' => __( 'Write', 'aicom' ), 'destructive' => __( 'Destructive', 'aicom' ), 'admin_sensitive' => __( 'Admin Sensitive', 'aicom' ), ]; // Collect which classes actually appear in this window for the legend. $classes_present = []; foreach ( $daily as $day ) { foreach ( array_keys( $day['classes'] ) as $cls ) { $classes_present[ $cls ] = true; } } if ( $filter_date ) { $sessions = AICOM_Sessions::get_all( [ 'log_date' => $filter_date ] ); } else { $sessions = AICOM_Sessions::get_all( [ 'date_from' => $graph_from, 'date_to' => $graph_to ] ); } $session_base_url = add_query_arg( [ 'page' => 'aicom-audit-logs', 'tab' => 'sessions' ], admin_url( 'admin.php' ) ); // ── Logs tab data ────────────────────────────────────────────────────── $filter_tool = sanitize_text_field( wp_unslash( $_GET['tool_name'] ?? '' ) ); $filter_status = sanitize_key( wp_unslash( $_GET['status'] ?? '' ) ); $filter_key_id = absint( wp_unslash( $_GET['api_key_id'] ?? 0 ) ); $filter_ip = sanitize_text_field( wp_unslash( $_GET['remote_ip'] ?? '' ) ); $filter_session_id = absint( wp_unslash( $_GET['session_id'] ?? 0 ) ); $filter_period = sanitize_key( wp_unslash( $_GET['period'] ?? 'today' ) ); $filter_from = sanitize_text_field( wp_unslash( $_GET['date_from'] ?? '' ) ); $filter_to = sanitize_text_field( wp_unslash( $_GET['date_to'] ?? '' ) ); $page_num = max( 1, absint( wp_unslash( $_GET['paged'] ?? 1 ) ) ); $per_page = 50; if ( $filter_period !== 'custom' ) { $now = current_time( 'mysql', true ); // created_at is stored in UTC (current_time('mysql', true) at insert // time), but "today"/"7 days"/etc. are calendar boundaries in the // site's LOCAL timezone. Building $filter_from from a local calendar // date (current_time('Y-m-d') / gmdate on a local-shifted value) and // then comparing it directly against the UTC column was wrong: for any // non-zero UTC offset it silently mis-bounds the window by that many // hours, and for "today" specifically it can even flip $filter_from // past $filter_to during the first few hours after local midnight // (whenever local calendar date has already advanced but UTC hasn't), // making the query return zero rows regardless of what's in the table. // Fix: compute the boundary as local midnight, then convert THAT to UTC. $tz = wp_timezone(); $to_utc = static function ( DateTime $local_midnight ): string { $local_midnight->setTimezone( new DateTimeZone( 'UTC' ) ); return $local_midnight->format( 'Y-m-d H:i:s' ); }; switch ( $filter_period ) { case 'today': $filter_from = $to_utc( new DateTime( 'today', $tz ) ); $filter_to = $now; break; case '7days': $filter_from = $to_utc( ( new DateTime( 'today', $tz ) )->modify( '-7 days' ) ); $filter_to = $now; break; case '30days': $filter_from = $to_utc( ( new DateTime( 'today', $tz ) )->modify( '-30 days' ) ); $filter_to = $now; break; case 'year': $jan1 = new DateTime( ( new DateTime( 'today', $tz ) )->format( 'Y' ) . '-01-01 00:00:00', $tz ); $filter_from = $to_utc( $jan1 ); $filter_to = $now; break; default: $filter_from = ''; $filter_to = ''; } } $filters = array_filter( [ 'tool_name' => $filter_tool, 'status' => $filter_status, 'api_key_id' => $filter_key_id ?: null, 'remote_ip' => $filter_ip, 'session_id' => $filter_session_id ?: null, 'date_from' => $filter_from, 'date_to' => $filter_to, ] ); $result = AICOM_Audit_Logger::query( $filters, $per_page, $page_num ); $logs = $result['items']; $total = $result['total']; $num_pages = (int) ceil( $total / $per_page ); $api_keys = $wpdb->get_results( "SELECT id, label, key_prefix FROM {$wpdb->prefix}aicom_api_keys ORDER BY label", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $status_options = [ '', 'success', 'error', 'blocked_soft_lock', 'blocked_hard_lock', 'denied_scope', 'denied_allowlist', 'validation_failed', 'auth_failed', 'dependency_missing', 'rate_limited', 'blocked_working_hours' ]; $period_options = [ 'today' => __( 'Today', 'aicom' ), '7days' => __( 'Last 7 Days', 'aicom' ), '30days' => __( 'Last 30 Days', 'aicom' ), 'year' => __( 'This Year', 'aicom' ), 'custom' => __( 'Custom Range', 'aicom' ), 'all' => __( 'All Time', 'aicom' ), ]; $base_url = admin_url( 'admin.php?page=aicom-audit-logs' ); $active_tab_norm = ( $active_tab === 'filters' ) ? 'logs' : $active_tab; ?>

( )

__( 'Sessions', 'aicom' ), 'logs' => __( 'Logs', 'aicom' ), ]; foreach ( $tabs as $slug => $label ) : $is_active = $active_tab_norm === $slug; $tab_url = add_query_arg( [ 'page' => 'aicom-audit-logs', 'tab' => $slug ], admin_url( 'admin.php' ) ); ?>

← ← 0 ) : ?> →
$graph_offset, 'session_date' => $day['date'] ], $session_base_url ) ); $clear_url = esc_url( add_query_arg( [ 'graph_offset' => $graph_offset, 'session_date' => '' ], $session_base_url ) ); ?>

· ·
0 ) : ?>
▼

—

>

1 ) : $pagination_args = array_filter( [ 'page' => 'aicom-audit-logs', 'tab' => 'logs', 'tool_name' => $filter_tool, 'status' => $filter_status, 'api_key_id' => $filter_key_id ?: null, 'remote_ip' => $filter_ip, 'period' => $filter_period, 'date_from' => $filter_from, 'date_to' => $filter_to, ] ); $base = admin_url( 'admin.php' ) . '?' . http_build_query( $pagination_args ); $page_url = fn( $p ) => $base . '&paged=' . (int) $p; // Build the page list with ellipsis. Always show first + last, // current and its immediate neighbours; collapse gaps with "…". $pages_to_show = []; if ( $num_pages <= 7 ) { for ( $i = 1; $i <= $num_pages; $i++ ) { $pages_to_show[] = $i; } } else { $pages_to_show[] = 1; if ( $page_num - 1 > 2 ) { $pages_to_show[] = '…'; } for ( $i = max( 2, $page_num - 1 ); $i <= min( $num_pages - 1, $page_num + 1 ); $i++ ) { $pages_to_show[] = $i; } if ( $page_num + 1 < $num_pages - 1 ) { $pages_to_show[] = '…'; } $pages_to_show[] = $num_pages; } ?>