0 ) { $this->render_detail( $incident_id ); return; } $this->render_list(); } private function render_list(): void { $incidents = ( new IncidentRepository() )->all(); Menu::header( __( 'Incidents', 'aftercare' ) ); if ( empty( $incidents ) ) { echo '

' . esc_html__( 'No incidents recorded. When a metric breaches its budget or regresses more than 20% against its 28-day baseline, an incident opens here and an alert email goes out.', 'aftercare' ) . '

'; Menu::footer(); return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $incidents as $incident ) { $metric = (string) $incident['metric']; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in helper. echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Metric', 'aftercare' ) . '' . esc_html__( 'URL', 'aftercare' ) . '' . esc_html__( 'Breach', 'aftercare' ) . '' . esc_html__( 'Budget', 'aftercare' ) . '' . esc_html__( 'Baseline', 'aftercare' ) . '' . esc_html__( 'Status', 'aftercare' ) . '' . esc_html__( 'Opened', 'aftercare' ) . '
' . esc_html( IncidentRepository::label( $incident ) ) . '' . esc_html( (string) $incident['url'] ) . '' . esc_html( Util::format_metric( $metric, (float) $incident['breach_p75'] ) ) . '' . ( IncidentRepository::is_vitals( $incident ) ? esc_html( Util::format_metric( $metric, (float) $incident['budget_value'] ) ) : '—' ) . '' . ( null !== $incident['baseline_p75'] ? esc_html( Util::format_metric( $metric, (float) $incident['baseline_p75'] ) ) : '—' ) . '' . self::status_pill( (string) $incident['status'] ) . '' . esc_html( human_time_diff( strtotime( $incident['opened_at'] . ' UTC' ) ) . ' ' . __( 'ago', 'aftercare' ) ) . '' . esc_html__( 'Details', 'aftercare' ) . '
'; Menu::footer(); } private function render_detail( int $id ): void { $repo = new IncidentRepository(); $incident = $repo->find( $id ); Menu::header( __( 'Incident detail', 'aftercare' ) ); if ( ! $incident ) { echo '

' . esc_html__( 'Incident not found.', 'aftercare' ) . '

'; Menu::footer(); return; } $metric = (string) $incident['metric']; $url = (string) $incident['url']; $is_lab = IncidentRepository::SOURCE_LAB === ( $incident['sample_source'] ?? '' ); echo '

← ' . esc_html__( 'All incidents', 'aftercare' ) . '

'; echo '
'; echo '

' . esc_html( IncidentRepository::label( $incident ) ) . ' — ' . esc_html( $url ) . ' ' . self::status_pill( (string) $incident['status'] ) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! IncidentRepository::is_vitals( $incident ) ) { $this->render_problem_stats( $incident ); } else { echo '
'; echo '
' . ( $is_lab ? esc_html__( 'Lab reading', 'aftercare' ) : esc_html__( 'Breach p75', 'aftercare' ) ) . '' . esc_html( Util::format_metric( $metric, (float) $incident['breach_p75'] ) ) . '
'; echo '
' . esc_html__( 'Budget', 'aftercare' ) . '' . esc_html( Util::format_metric( $metric, (float) $incident['budget_value'] ) ) . '
'; echo '
' . esc_html__( '28-day baseline', 'aftercare' ) . '' . ( null !== $incident['baseline_p75'] ? esc_html( Util::format_metric( $metric, (float) $incident['baseline_p75'] ) ) : '—' ) . '
'; echo '
' . esc_html__( 'Opened', 'aftercare' ) . '' . esc_html( (string) $incident['opened_at'] ) . ' UTC
'; echo '
'; if ( $is_lab ) { echo '

' . esc_html__( 'Opened by a Lighthouse lab check: the reading was more than 20% worse than the 28-day lab baseline, by more than normal run-to-run variation. Lab incidents never open from the budget alone.', 'aftercare' ) . '

'; } } $triggers = ( new LedgerRepository() )->find_many( IncidentRepository::trigger_ids( $incident ) ); if ( $triggers ) { echo '

' . esc_html__( 'Caught by the lab re-test that ran after these changes:', 'aftercare' ) . '

'; echo ''; } if ( IncidentRepository::is_vitals( $incident ) ) { // 60-day trend with the breach visible, drawn from the same kind // of data that opened the incident. $samples = new SampleRepository(); $series = $is_lab ? $samples->lab_series( $url, $metric, 60 ) : $samples->series( $url, $metric, 60 ); echo '
'; } // Status actions. echo '
'; echo ''; echo ''; wp_nonce_field( 'aftercare_incident_status_' . (int) $incident['id'] ); foreach ( array( 'acknowledged' => __( 'Acknowledge', 'aftercare' ), 'resolved' => __( 'Mark resolved', 'aftercare' ), 'dismissed' => __( 'Dismiss', 'aftercare' ), ) as $status => $label ) { if ( $status === $incident['status'] ) { continue; } echo ' '; } echo '
'; echo '
'; // Ranked cause attribution comes from the separate add-on. Without it // the page shows the raw ledger window on its own, which is the same // evidence in unranked form. echo '
'; if ( class_exists( Attribution::class ) ) { echo '
'; echo '

' . esc_html__( 'Probable cause', 'aftercare' ) . '

'; $causes = json_decode( (string) ( $incident['causes'] ?? '' ), true ); if ( is_array( $causes ) && $causes ) { echo '
    '; foreach ( $causes as $cause ) { $confidence = (string) ( $cause['confidence'] ?? 'low' ); echo '
  1. '; echo '' . esc_html( self::confidence_label( $confidence ) ) . ' '; echo esc_html( (string) ( $cause['label'] ?? '' ) ); if ( ! empty( $cause['occurred_at'] ) ) { echo ' ' . esc_html( (string) $cause['occurred_at'] ) . ' UTC'; } echo '
  2. '; } echo '
'; } else { echo '

' . esc_html__( 'No attribution data recorded for this incident.', 'aftercare' ) . '

'; } echo '
'; } // Raw ledger window. echo '
'; echo '

' . esc_html__( 'Changes in the 72 hours before the breach', 'aftercare' ) . '

'; $from = gmdate( 'Y-m-d H:i:s', strtotime( $incident['opened_at'] . ' UTC' ) - 72 * HOUR_IN_SECONDS ); $events = ( new LedgerRepository() )->in_window( $from, (string) $incident['opened_at'] ); if ( empty( $events ) ) { echo '

' . esc_html__( 'No site changes were recorded in this window — an external factor (hosting, traffic, third-party scripts) is likely.', 'aftercare' ) . '

'; } else { echo ''; } echo '
'; echo '
'; Menu::footer(); } /** * Stats for incidents that are not about vitals: a fatal error, or a page * that failed the availability check. * * @param array $incident */ private function render_problem_stats( array $incident ): void { $stat = static function ( string $label, string $value, string $class = '' ): void { echo '
' . esc_html( $label ) . '' . esc_html( $value ) . '
'; }; if ( IncidentRepository::SOURCE_ERROR === $incident['sample_source'] ) { $error = ( new ErrorRepository() )->find_by_location( (string) $incident['url'] ); if ( $error ) { echo '

' . esc_html( (string) $error['message'] ) . '

'; } echo '
'; if ( $error ) { $stat( __( 'Occurrences', 'aftercare' ), Util::format_metric( 'ERROR', (float) $error['occurrences'] ), 'aftercare-fail' ); $stat( __( 'Likely source', 'aftercare' ), ErrorRepository::culprit_label( (string) $error['culprit'] ) ); $stat( __( 'First seen', 'aftercare' ), (string) $error['first_seen'] . ' UTC' ); $stat( __( 'Last seen', 'aftercare' ), (string) $error['last_seen'] . ' UTC' ); } $stat( __( 'Opened', 'aftercare' ), (string) $incident['opened_at'] . ' UTC' ); echo '
'; echo '

' . esc_html__( 'Resolves on its own once the error has not happened for 24 hours.', 'aftercare' ) . '

'; return; } echo '
'; $stat( __( 'Response', 'aftercare' ), Util::format_metric( 'HTTP', (float) $incident['breach_p75'] ), 'aftercare-fail' ); $stat( __( 'Opened', 'aftercare' ), (string) $incident['opened_at'] . ' UTC' ); echo '
'; echo '

' . esc_html__( 'The page failed two availability checks in a row: a server error, a missing page or a page cut off before it finished rendering. Resolves on its own at the first healthy check.', 'aftercare' ) . '

'; } public static function status_pill( string $status ): string { $labels = array( 'open' => __( 'Open', 'aftercare' ), 'acknowledged' => __( 'Acknowledged', 'aftercare' ), 'resolved' => __( 'Resolved', 'aftercare' ), 'dismissed' => __( 'Dismissed', 'aftercare' ), ); $class = in_array( $status, array( 'resolved', 'dismissed' ), true ) ? 'pass' : ( 'acknowledged' === $status ? 'warn' : 'fail' ); return '' . esc_html( $labels[ $status ] ?? $status ) . ''; } private static function confidence_label( string $confidence ): string { $labels = array( 'high' => __( 'High confidence', 'aftercare' ), 'medium' => __( 'Medium confidence', 'aftercare' ), 'low' => __( 'Low confidence', 'aftercare' ), ); return $labels[ $confidence ] ?? $confidence; } public static function handle_status_change(): void { if ( ! current_user_can( Menu::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'aftercare' ) ); } // The incident ID is part of the nonce action, so it must be read first. $id = isset( $_POST['incident'] ) ? absint( wp_unslash( $_POST['incident'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified on the next line. check_admin_referer( 'aftercare_incident_status_' . $id ); $status = sanitize_key( wp_unslash( $_POST['status'] ?? '' ) ); ( new IncidentRepository() )->set_status( $id, $status ); wp_safe_redirect( admin_url( 'admin.php?page=aftercare-incidents&incident=' . $id ) ); exit; } }