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 '| ' . esc_html__( 'Metric', 'aftercare' ) . ' | '; echo '' . esc_html__( 'URL', 'aftercare' ) . ' | '; echo '' . esc_html__( 'Breach', 'aftercare' ) . ' | '; echo '' . esc_html__( 'Budget', 'aftercare' ) . ' | '; echo '' . esc_html__( 'Baseline', 'aftercare' ) . ' | '; echo '' . esc_html__( 'Status', 'aftercare' ) . ' | '; echo '' . esc_html__( 'Opened', 'aftercare' ) . ' | '; echo ''; echo ' |
|---|---|---|---|---|---|---|---|
| ' . esc_html( IncidentRepository::label( $incident ) ) . ' | '; echo '' . esc_html( (string) $incident['url'] ) . ' | '; echo '' . esc_html( Util::format_metric( $metric, (float) $incident['breach_p75'] ) ) . ' | '; echo '' . ( IncidentRepository::is_vitals( $incident ) ? esc_html( Util::format_metric( $metric, (float) $incident['budget_value'] ) ) : '—' ) . ' | '; echo '' . ( null !== $incident['baseline_p75'] ? esc_html( Util::format_metric( $metric, (float) $incident['baseline_p75'] ) ) : '—' ) . ' | '; echo '' . self::status_pill( (string) $incident['status'] ) . ' | '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in helper. echo '' . esc_html( human_time_diff( strtotime( $incident['opened_at'] . ' UTC' ) ) . ' ' . __( 'ago', 'aftercare' ) ) . ' | '; echo '' . esc_html__( 'Details', 'aftercare' ) . ' | '; 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 '' . 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 '' . esc_html__( 'No attribution data recorded for this incident.', 'aftercare' ) . '
'; } echo '' . esc_html__( 'No site changes were recorded in this window — an external factor (hosting, traffic, third-party scripts) is likely.', 'aftercare' ) . '
'; } else { echo '' . esc_html( (string) $error['message'] ) . '
' . esc_html__( 'Resolves on its own once the error has not happened for 24 hours.', 'aftercare' ) . '
'; return; } 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; } }