failures(); // Keyed, so a migration retrying every admin load updates its entry // instead of appending a new one each time. $failures[ $table . '@' . $version ] = array( 'table' => $table, 'version' => $version, 'error' => substr( (string) $error, 0, 500 ), 'at' => gmdate( 'Y-m-d H:i:s' ), ); if ( count( $failures ) > self::MAX_FAILURES ) { $failures = array_slice( $failures, -self::MAX_FAILURES, null, true ); } update_option( self::OPTION_FAILURES, $failures, false ); } catch ( \Throwable $e ) { unset( $e ); } } /** * Forget every recorded failure. * * Called once reconciliation finds that NO table still needs an upgrade, * which is the normal outcome: an unstamped version means the migration * retries on the next admin load, so most failures here are transient and * must stop being reported when they resolve. * * All-or-nothing rather than per table because BerlinDB's `Table` exposes * no public accessor for its own name, so a stored failure cannot be * matched back to a specific Table object from outside. "Nothing needs * upgrading" is the stronger statement anyway, and it is the one worth * acting on. */ public function clear_all(): void { try { if ( array() !== $this->failures() ) { update_option( self::OPTION_FAILURES, array(), false ); } } catch ( \Throwable $e ) { unset( $e ); } } /** * Register the Site Health test. * * @param array|mixed $tests Site Health tests. * @return array|mixed */ public function add_test( $tests ) { if ( ! is_array( $tests ) ) { return $tests; } if ( ! isset( $tests['direct'] ) || ! is_array( $tests['direct'] ) ) { $tests['direct'] = array(); } $tests['direct'][ self::TEST_KEY ] = array( 'label' => __( 'AcrossAI MCP database upgrades', 'acrossai-mcp-manager' ), 'test' => array( $this, 'run_self_check' ), ); return $tests; } /** * Report any migration that has not been able to apply. * * @return array */ public function run_self_check(): array { $failures = $this->failures(); if ( empty( $failures ) ) { return $this->build_result( 'good', __( 'AcrossAI MCP database tables are up to date', 'acrossai-mcp-manager' ), '

' . esc_html__( 'Every database change this plugin needed has been applied successfully.', 'acrossai-mcp-manager' ) . '

' ); } $items = ''; foreach ( $failures as $failure ) { $items .= '
  • ' . sprintf( /* translators: 1: database table name, 2: schema version, 3: UTC timestamp. */ esc_html__( '%1$s could not be upgraded to version %2$s (last attempt %3$s UTC)', 'acrossai-mcp-manager' ), '' . esc_html( (string) ( $failure['table'] ?? '' ) ) . '', '' . esc_html( (string) ( $failure['version'] ?? '' ) ) . '', esc_html( (string) ( $failure['at'] ?? '' ) ) ); if ( ! empty( $failure['error'] ) ) { $items .= '
    ' . esc_html( (string) $failure['error'] ) . ''; } $items .= '
  • '; } return $this->build_result( 'critical', __( 'An AcrossAI MCP database upgrade could not be applied', 'acrossai-mcp-manager' ), '

    ' . esc_html__( 'One or more database changes this plugin needed did not apply. The affected table is in a shape the plugin does not expect, which can mean saved data is silently incomplete.', 'acrossai-mcp-manager' ) . '

    ' . '
      ' . $items . '
    ' . '

    ' . esc_html__( 'The upgrade is retried on each admin page load, so this often clears on its own. If it persists, the database user most likely lacks ALTER permission on these tables.', 'acrossai-mcp-manager' ) . '

    ' ); } /** * Stored failures, normalised to an array. * * @return array> */ private function failures(): array { $stored = get_option( self::OPTION_FAILURES, array() ); return is_array( $stored ) ? $stored : array(); } /** * Shape a Site Health result. * * @param string $status One of `good`, `recommended` or `critical`. * @param string $label Short headline. * @param string $description Already-escaped HTML. * @return array */ private function build_result( string $status, string $label, string $description ): array { return array( 'label' => $label, 'status' => $status, 'badge' => array( 'label' => __( 'Database', 'acrossai-mcp-manager' ), 'color' => 'blue', ), 'description' => $description, 'actions' => '', 'test' => self::TEST_KEY, ); } }