' . esc_html( Design::label( $event['code'] ) ) . ( '
' . esc_html( self::review_prompt( $event['code'] ) ) . '
'; } self::peek( $event, $time, $url, $list_url ); echo '__( 'Comment', 'logged-ai' ), 'core' => __( 'Core', 'logged-ai' ), 'coupon' => __( 'Coupon', 'logged-ai' ), 'network' => __( 'Network', 'logged-ai' ), 'order' => __( 'Order', 'logged-ai' ), 'plugin' => __( 'Plugin', 'logged-ai' ), 'post' => __( 'Post', 'logged-ai' ), 'product' => __( 'Product', 'logged-ai' ), 'refund' => __( 'Refund', 'logged-ai' ), 'site' => __( 'Site', 'logged-ai' ), 'term' => __( 'Term', 'logged-ai' ), 'theme' => __( 'Theme', 'logged-ai' ), 'user' => __( 'User', 'logged-ai' ), 'object' => __( 'Object', 'logged-ai' ) )[ $object_type ] ?? ucfirst( str_replace( '_', ' ', $object_type ) );
if ( 'post' === $object_type && in_array( $event['code'] ?? '', array( 'content.post_created', 'content.post_changed', 'content.post_deleted' ), true ) ) {
$recorded_type = $event['detail']['facts']['post_type'] ?? '';
$type = array( 'post' => __( 'Post', 'logged-ai' ), 'page' => __( 'Page', 'logged-ai' ), 'attachment' => __( 'Media item', 'logged-ai' ) )[ $recorded_type ] ?? __( 'Content', 'logged-ai' );
}
$id = $event['object_id'] ?? null;
return empty( $id ) ? sprintf( /* translators: %s: object type. */ __( '%s (identifier not captured)', 'logged-ai' ), $type ) : sprintf( /* translators: 1: object type, 2: recorded object identifier. */ __( '%1$s #%2$s', 'logged-ai' ), $type, $id );
}
public static function actor( array $event ): string {
return null !== ( $event['actor_id'] ?? null ) ? sprintf( /* translators: %s: recorded user identifier. */ __( 'User #%s', 'logged-ai' ), $event['actor_id'] ) : ( 'system' === ( $event['actor_kind'] ?? '' ) ? __( 'WordPress background task', 'logged-ai' ) : __( 'Actor not recorded', 'logged-ai' ) );
}
public static function outcome( string $outcome ): string {
return array( 'stored_change' => __( 'Change recorded', 'logged-ai' ), 'completed' => __( 'Completed', 'logged-ai' ), 'failed' => __( 'Operation failed', 'logged-ai' ), 'attempted' => __( 'Attempt recorded', 'logged-ai' ), 'unknown' => __( 'Result unknown', 'logged-ai' ) )[ $outcome ] ?? ucfirst( str_replace( '_', ' ', $outcome ) );
}
private static function review_prompt( string $code ): string {
return match ( $code ) {
'identity.roles_changed' => __( 'Confirm that this access change was intentional.', 'logged-ai' ),
'identity.application_password_created' => __( 'Check that you recognise this application access.', 'logged-ai' ),
'configuration.plugin_updated', 'configuration.plugin_deactivated' => __( 'Compare this change with your planned maintenance.', 'logged-ai' ),
'configuration.option_changed' => __( 'Check this against your recent site configuration changes.', 'logged-ai' ),
'identity.authentication_failed_burst', 'identity.account_auth_failed_burst' => __( 'Review the captured sign-in failures and their context.', 'logged-ai' ),
default => __( 'Open the captured evidence and check whether this was expected.', 'logged-ai' ),
};
}
/** Counts only the supplied records; never silently presents a full-site trend. */
public static function chart( array $events, string $id = 'activity', bool $compact = false ): void {
$categories = array(); $hours = array(); $severity = array();
foreach ( $events as $event ) {
$category = (string) ( $event['category'] ?? 'other' );
$categories[ $category ] = ( $categories[ $category ] ?? 0 ) + 1;
$hour = substr( (string) $event['occurred_at'], 0, 13 ) . ':00';
$hours[ $hour ] = ( $hours[ $hour ] ?? 0 ) + 1;
$level = (string) ( $event['severity'] ?? 'unknown' );
$severity[ $level ] = ( $severity[ $level ] ?? 0 ) + 1;
}
arsort( $categories ); ksort( $hours ); arsort( $severity );
$total = count( $events );
$names = array( 'identity' => __( 'Users & access', 'logged-ai' ), 'content' => __( 'Content', 'logged-ai' ), 'configuration' => __( 'Site & plugins', 'logged-ai' ), 'commerce' => __( 'Commerce', 'logged-ai' ), 'privacy' => __( 'Privacy', 'logged-ai' ), 'other' => __( 'Other activity', 'logged-ai' ) );
$leading = array_key_first( $categories );
echo ' ' . esc_html__( 'ACTIVITY ANALYSIS', 'logged-ai' ) . ' ' ) . esc_html( $total ? sprintf( /* translators: 1: leading activity category, 2: category records, 3: total sample records. */ _n( '%1$s leads this sample, with %2$d of %3$d record.', '%1$s leads this sample, with %2$d of %3$d records.', $total, 'logged-ai' ), $names[ $leading ] ?? ucfirst( $leading ), $categories[ $leading ], $total ) : __( 'No retained events in this selection.', 'logged-ai' ) ) . ' ' . esc_html__( 'This selection only. Recorded hours show occupied UTC buckets, not a continuous traffic trend. Missing activity is not counted.', 'logged-ai' ) . '' . esc_html__( 'See the pattern.', 'logged-ai' ) . '
records', 'logged-ai' ), array( 'br' => array() ) ) . '
' . esc_html( implode( ' · ', $changes ) ) . '
'; } if ( ( $detail || 'available' !== ( $target['state'] ?? '' ) ) && '' !== ( $target['message'] ?? '' ) ) { echo '' . esc_html( $target['message'] ) . '
'; } if ( '' !== ( $target['url'] ?? '' ) || '' !== ( $target['revision_url'] ?? '' ) ) { echo '' . esc_html( $target['revision_message'] ) . '
'; } if ( ! empty( $target['revision_url'] ) && array_intersect( array( 'post_name', 'post_date', 'post_date_gmt', 'comment_status', 'ping_status' ), array_column( $event['detail']['diffs'] ?? array(), 'field' ) ) ) { echo '' . esc_html__( 'WordPress revisions may omit URL slug, publication date and discussion settings. Check the captured field differences below; those values remain redacted.', 'logged-ai' ) . '
'; } echo '' . esc_html__( 'Current item details are shown only with your WordPress access. They can differ from this recorded event. Logged.ai does not store the title or article text here.', 'logged-ai' ) . '
'; } echo '' . esc_html__( 'View item activity', 'logged-ai' ) . '
'; } } /** * Summarize supported content changes without exposing redacted values. * * @param array $event Authorized retained event. * @return array Human-readable changes, taken only from retained field names/statuses. */ private static function content_changes( array $event ): array { if ( 'content.post_changed' !== ( $event['code'] ?? '' ) ) { return array(); } $names = array( 'post_title' => __( 'Title changed', 'logged-ai' ), 'post_content' => __( 'Content changed', 'logged-ai' ), 'post_excerpt' => __( 'Excerpt changed', 'logged-ai' ), 'post_author' => __( 'Author changed', 'logged-ai' ), 'post_password' => __( 'Password protection changed', 'logged-ai' ), 'post_parent' => __( 'Parent changed', 'logged-ai' ), 'menu_order' => __( 'Menu order changed', 'logged-ai' ), 'post_name' => __( 'URL slug changed', 'logged-ai' ), 'post_date' => __( 'Publication date changed', 'logged-ai' ), 'post_date_gmt' => __( 'Publication date changed', 'logged-ai' ), 'comment_status' => __( 'Comment settings changed', 'logged-ai' ), 'ping_status' => __( 'Pingback settings changed', 'logged-ai' ), ); $changes = array(); foreach ( $event['detail']['diffs'] ?? array() as $diff ) { $field = $diff['field'] ?? ''; if ( isset( $names[ $field ] ) ) { $changes[ $field ] = $names[ $field ]; } elseif ( 'post_status' === $field && is_string( $diff['before'] ?? null ) && is_string( $diff['after'] ?? null ) && 'redacted' !== ( $diff['availability'] ?? '' ) ) { /* translators: 1: recorded prior post status, 2: recorded new post status. */ $changes[ $field ] = sprintf( __( 'Status: %1$s → %2$s', 'logged-ai' ), self::post_status( $diff['before'] ), self::post_status( $diff['after'] ) ); } } return array_values( array_unique( $changes ) ); } /** * Label a captured content field without changing its stored identifier. * * @param string $field Retained field identifier. * @return string Translated label, or the original identifier for other schemas. */ public static function content_field( string $field ): string { return array( 'post_title' => __( 'Title', 'logged-ai' ), 'post_content' => __( 'Content', 'logged-ai' ), 'post_excerpt' => __( 'Excerpt', 'logged-ai' ), 'post_status' => __( 'Status', 'logged-ai' ), 'post_author' => __( 'Author', 'logged-ai' ), 'post_password' => __( 'Password protection', 'logged-ai' ), 'post_parent' => __( 'Parent', 'logged-ai' ), 'menu_order' => __( 'Menu order', 'logged-ai' ), 'post_name' => __( 'URL slug', 'logged-ai' ), 'post_date' => __( 'Publication date (site time)', 'logged-ai' ), 'post_date_gmt' => __( 'Publication date (UTC)', 'logged-ai' ), 'comment_status' => __( 'Comment settings', 'logged-ai' ), 'ping_status' => __( 'Pingback settings', 'logged-ai' ), )[ $field ] ?? $field; } /** * Label common recorded statuses; preserve custom status identifiers. * * @param string $status Retained status identifier. * @return string Human-readable status. */ private static function post_status( string $status ): string { return array( 'draft' => __( 'Draft', 'logged-ai' ), 'publish' => __( 'Published', 'logged-ai' ), 'pending' => __( 'Pending review', 'logged-ai' ), 'private' => __( 'Private', 'logged-ai' ), 'future' => __( 'Scheduled', 'logged-ai' ), 'trash' => __( 'Trash', 'logged-ai' ) )[ $status ] ?? $status; } private static function source_label( string $channel ): string { return array( 'wp_admin' => __( 'WordPress admin', 'logged-ai' ), 'cron' => __( 'Scheduled task', 'logged-ai' ), 'xmlrpc' => 'XML-RPC', 'rest' => 'REST API', 'cli' => __( 'Command line', 'logged-ai' ), 'frontend' => __( 'Site front end', 'logged-ai' ) )[ $channel ] ?? ucfirst( str_replace( '_', ' ', $channel ) ); } public static function severity( string $level ): string { return array( 'info' => __( 'Info', 'logged-ai' ), 'notice' => __( 'Notice', 'logged-ai' ), 'warning' => __( 'Warning', 'logged-ai' ), 'high' => __( 'High', 'logged-ai' ), 'critical' => __( 'Critical', 'logged-ai' ), 'unknown' => __( 'Unknown', 'logged-ai' ) )[ $level ] ?? ucfirst( $level ); } private static function review( array $event ): bool { return in_array( $event['severity'], array( 'warning', 'high', 'critical' ), true ); } private static function row_attributes( array $event ): void { echo ' data-lai-activity-row data-actor-id="' . esc_attr( (string) ( $event['actor_id'] ?? '' ) ) . '" data-review="' . ( self::review( $event ) ? '1' : '0' ) . '" data-failed="' . ( 'failed' === $event['outcome'] ? '1' : '0' ) . '" data-accounts="' . ( 'identity' === $event['category'] ? '1' : '0' ) . '"'; } private static function peek( array $event, TimeDisplay $time, string $url, string $list_url ): void { echo ( '' . esc_html__( 'Try a wider date range or clear a filter. Collection gaps can also produce an empty result.', 'logged-ai' ) . '
' . esc_html( self::review_prompt( $event['code'] ) ) . '
'; } self::peek( $event, $time, $url, $list_url ); echo '| ' . esc_html__( 'What happened', 'logged-ai' ) . ' | ' . esc_html__( 'Who', 'logged-ai' ) . ' | ' . esc_html__( 'Affected object', 'logged-ai' ) . ' | ' . esc_html__( 'When', 'logged-ai' ) . ' | ' . esc_html__( 'Result', 'logged-ai' ) . ' | ' . esc_html__( 'Source / severity', 'logged-ai' ) . ' |
|---|---|---|---|---|---|
| ' . esc_html( Design::label( $event['code'] ) ) . '' . esc_html( $event['code'] . ' / #' . $event['id'] ) . ''; self::peek( $event, $time, $url, $list_url ); echo ' | ' . wp_kses( self::person( $event ), Design::allowed_markup() ) . ' | ' . ( ! empty( $event['subject_id'] ) ? wp_kses( self::person( $event, true ), Design::allowed_markup() ) : esc_html( self::object( $event ) ) ); self::content_target( $event ); echo ' | ' . esc_html( self::review( $event ) ? sprintf( /* translators: %s: recorded outcome. */ __( 'Review: %s', 'logged-ai' ), self::outcome( $event['outcome'] ) ) : self::outcome( $event['outcome'] ) ) . ' | ' . esc_html( $event['channel'] . ' / ' . $event['severity'] ) . ' |
' . esc_html__( 'Try All activity, or change the search filters above.', 'logged-ai' ) . '
' . esc_html( sprintf( /* translators: %s: display timezone. */ __( 'Names show current WordPress profiles when your access permits; recorded user IDs remain the evidence. Initials are generated locally without loading profile photos. Times: %s. Observations do not establish intent.', 'logged-ai' ), $time->zone() ) ) . '
' ); } public static function request_chart( array $groups ): void { if ( ! $groups ) { return; } $counts = array(); foreach ( $groups as $group ) { $family = $group['family']; $counts[ $family ] = ( $counts[ $family ] ?? 0 ) + (int) $group['admitted_count']; } arsort( $counts ); $max = max( $counts ); echo ( '' . esc_html__( 'OBSERVED REQUEST PATTERNS', 'logged-ai' ) . '
' . esc_html__( 'Compare admitted findings in the retained minute groups below.', 'logged-ai' ) . '
' . esc_html__( 'Bar lengths are relative to the largest family in this selection. Findings are lower bounds, not visitor counts or proof of an attack.', 'logged-ai' ) . '