count_open(); Menu::header( __( 'Aftercare — Dashboard', 'aftercare' ) ); if ( isset( $_GET['aftercare_ran'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

' . esc_html__( 'Daily checks ran. Vitals, breach detection and retention are up to date.', 'aftercare' ) . '

'; } if ( '' === (string) Options::get( 'api_key' ) ) { echo '

'; printf( /* translators: %s: settings page URL */ wp_kses_post( __( 'Add your Google API key in Aftercare Settings to start pulling Core Web Vitals from the Chrome UX Report. First data appears within minutes.', 'aftercare' ) ), esc_url( admin_url( 'admin.php?page=aftercare-settings' ) ) ); echo '

'; } echo '

'; printf( /* translators: %s: URL being monitored */ esc_html__( 'Monitoring %s (p75, field data)', 'aftercare' ), esc_html( $primary_url ) ); echo '

'; echo '
'; foreach ( Options::METRICS as $metric ) { $this->render_metric_card( $samples->series( $primary_url, $metric, 30 ), $primary_url, $metric ); } echo '
'; if ( LabClient::enabled() ) { $this->render_lab_section( $samples, $primary_url ); } echo '
'; // Open incidents summary. echo '
'; echo '

' . esc_html__( 'Incidents', 'aftercare' ) . '

'; if ( $open_count > 0 ) { echo '

'; printf( /* translators: %d: number of open incidents */ esc_html( _n( '%d open incident needs attention.', '%d open incidents need attention.', $open_count, 'aftercare' ) ), (int) $open_count ); echo '

'; } else { echo '

' . esc_html__( 'No open incidents. All metrics within budget.', 'aftercare' ) . '

'; } echo '

' . esc_html__( 'View incidents', 'aftercare' ) . '

'; echo '

' . esc_html__( 'Maintenance', 'aftercare' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'aftercare_run_now' ); echo ''; echo '
'; echo '
'; // Recent ledger events. echo '
'; echo '

' . esc_html__( 'Recent changes', 'aftercare' ) . '

'; $events = $ledger->query( array(), 10 ); if ( empty( $events ) ) { echo '

' . esc_html__( 'No changes recorded yet. Plugin updates, theme switches, publishes and settings changes will appear here within seconds of happening.', 'aftercare' ) . '

'; } else { echo ''; echo '

' . esc_html__( 'Full ledger', 'aftercare' ) . '

'; } echo '
'; echo '
'; Menu::footer(); } /** * Lab cards sit under their own heading and are never merged into the * field cards above: a Lighthouse run is a synthetic load, not a visitor. */ private function render_lab_section( SampleRepository $samples, string $url ): void { echo '

' . esc_html__( 'Lab checks', 'aftercare' ) . '

'; echo '

' . esc_html__( 'Lighthouse, mobile profile, median of 3 runs per day through the PageSpeed Insights API. Useful when field data is thin; not a measure of real visitors.', 'aftercare' ) . '

'; $error = LabClient::last_error(); if ( $error ) { echo '

'; printf( /* translators: 1: URL, 2: error message from the PageSpeed Insights API */ esc_html__( 'The last lab check of %1$s failed: %2$s', 'aftercare' ), esc_html( $error['url'] ), esc_html( $error['message'] ) ); echo '

'; } echo '
'; foreach ( Options::LAB_METRICS as $metric ) { $this->render_metric_card( $samples->lab_series( $url, $metric, 30 ), $url, $metric ); } echo '
'; } /** * @param array $series */ private function render_metric_card( array $series, string $url, string $metric ): void { $latest = ! empty( $series ) ? end( $series )['value'] : null; $budget = Options::budget( $metric, $url ); $status = \Aftercare\Vitals\Status::evaluate( $latest, $budget ); $labels = array( 'pass' => __( 'Pass', 'aftercare' ), 'warn' => __( 'Warn', 'aftercare' ), 'fail' => __( 'Fail', 'aftercare' ), 'empty' => __( 'No data', 'aftercare' ), ); echo '
'; echo '
'; echo '' . esc_html( $metric ) . ''; echo '' . esc_html( $labels[ $status ] ) . ''; echo '
'; echo '
' . ( null !== $latest ? esc_html( Util::format_metric( $metric, $latest ) ) : '—' ) . '
'; echo '
'; printf( /* translators: %s: budget value */ esc_html__( 'Budget %s', 'aftercare' ), esc_html( Util::format_metric( $metric, $budget ) ) ); echo '
'; echo '
'; echo '
'; } }