connection_count() ) { return [ 'key' => 'connect', 'title' => __( 'Connect your first site', 'shiftora' ), 'note' => __( 'Shiftora moves content between two WordPress sites. Connecting them is the first step, and nothing else works until it is done.', 'shiftora' ), 'url' => admin_url( 'admin.php?page=shiftora-connections' ), 'cta' => __( 'Add a connection', 'shiftora' ), ]; } if ( 0 === $this->active_connection_count() ) { return [ 'key' => 'reactivate', 'title' => __( 'Reconnect your sites', 'shiftora' ), 'note' => __( 'Your connections exist but are not active, so a migration would be refused before it started.', 'shiftora' ), 'url' => admin_url( 'admin.php?page=shiftora-connections' ), 'cta' => __( 'Review connections', 'shiftora' ), ]; } $running = $this->running_migration(); if ( null !== $running ) { return [ 'key' => 'watch', 'title' => __( 'A migration is running', 'shiftora' ), 'note' => sprintf( /* translators: %d: migration number */ __( 'Migration %d is still going. You can close this page: Shiftora keeps working in the background and will finish without you.', 'shiftora' ), $running['id'] ), 'url' => admin_url( 'admin.php?page=shiftora-migrate' ), 'cta' => __( 'Watch progress', 'shiftora' ), ]; } if ( [] === $this->recent_migrations() ) { return [ 'key' => 'first-migration', 'title' => __( 'Run your first migration', 'shiftora' ), 'note' => __( 'Your sites are connected. The next step asks what you want to move, shows you how much of it there is, and only then starts anything.', 'shiftora' ), 'url' => admin_url( 'admin.php?page=shiftora-migrate' ), 'cta' => __( 'Start a migration', 'shiftora' ), ]; } $failed = $this->most_recent_failure(); if ( null !== $failed ) { // Built from the outcome, not the status, so a recovered pull and an abandoned push are not described alike. $outcome = MigrationOutcome::describe( $failed ); return [ 'key' => 'recover', 'title' => __( 'A migration needs attention', 'shiftora' ), 'note' => sprintf( /* translators: 1: migration number, 2: what happened to the data */ __( 'Migration %1$d: %2$s', 'shiftora' ), $failed['id'], $outcome['headline'] ), 'url' => admin_url( 'admin.php?page=shiftora-migrate' ), 'cta' => __( 'Review it', 'shiftora' ), ]; } return [ 'key' => 'new', 'title' => __( 'Ready when you are', 'shiftora' ), 'note' => __( 'Your sites are connected and nothing is running. Start a migration whenever you need one.', 'shiftora' ), 'url' => admin_url( 'admin.php?page=shiftora-migrate' ), 'cta' => __( 'Start a migration', 'shiftora' ), ]; } public function connection_count(): int { return count( $this->connections->list() ); } public function active_connection_count(): int { return (int) $this->connections->count_active(); } /** * The migration currently running, if any. At most one, because the engine * takes a worker lock; read from the database so a run that finished in the * background is not reported as running. * * @return array{id:int,direction:string,status:string}|null */ public function running_migration(): ?array { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id, direction, status FROM %i WHERE status = %s ORDER BY id DESC LIMIT 1', $wpdb->prefix . 'shiftora_migrations', MigrationStatus::Running->value ), ARRAY_A ); if ( ! is_array( $rows ) || [] === $rows ) { return null; } $row = (array) $rows[0]; return [ 'id' => (int) ( $row['id'] ?? 0 ), 'direction' => (string) ( $row['direction'] ?? '' ), 'status' => (string) ( $row['status'] ?? '' ), ]; } /** * Recent migrations, newest first. `tables_json` is decoded here, not in the * template, so a malformed value cannot break the page. * * @return array */ public function recent_migrations(): array { $wpdb = $this->wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id, direction, status, connection_id, created_at, updated_at, tables_json FROM %i ORDER BY id DESC LIMIT %d', $wpdb->prefix . 'shiftora_migrations', self::RECENT_LIMIT ), ARRAY_A ); if ( ! is_array( $rows ) ) { return []; } $out = []; foreach ( $rows as $row ) { $row = (array) $row; $tables = json_decode( (string) ( $row['tables_json'] ?? '[]' ), true ); $tables = is_array( $tables ) ? array_values( array_filter( array_map( 'strval', $tables ) ) ) : []; $created = (string) ( $row['created_at'] ?? '' ); $updated = (string) ( $row['updated_at'] ?? '' ); $finished = in_array( (string) ( $row['status'] ?? '' ), MigrationStatus::terminal_values(), true ) ? $updated : null; $out[] = [ 'id' => (int) ( $row['id'] ?? 0 ), 'direction' => (string) ( $row['direction'] ?? '' ), 'status' => (string) ( $row['status'] ?? '' ), 'connection_id' => (int) ( $row['connection_id'] ?? 0 ), // Lets a row say which site it refers to when there are several connections. 'remote_url' => $this->remote_url_for( (int) ( $row['connection_id'] ?? 0 ) ), 'created_at' => $created, 'finished_at' => $finished, 'tables' => $tables, 'elapsed' => $this->elapsed_seconds( $created, $finished ), ]; } return $out; } /** * The newest migration that ended badly. Only `failed` counts; a completed one needs no nudge. * * @return array{id:int,direction:string,status:string}|null */ public function most_recent_failure(): ?array { // A recovered run is resolved, not an outstanding problem; it still shows in the recent list. foreach ( $this->recent_migrations() as $migration ) { if ( MigrationStatus::Failed->value === $migration['status'] ) { return $migration; } } return null; } /** A connection's host, for a row that does not otherwise say which site it was. */ private function remote_url_for( int $connection_id ): string { if ( $connection_id <= 0 ) { return ''; } $connection = $this->connections->find_by_id( $connection_id ); if ( ! $connection ) { return ''; } $url = (string) ( $connection->remote_url ?? '' ); if ( '' === $url ) { return ''; } $host = wp_parse_url( $url, PHP_URL_HOST ); return is_string( $host ) && '' !== $host ? $host : $url; } /** How long a migration took in whole seconds, or null while in progress (the live page shows elapsed time). */ private function elapsed_seconds( string $created, ?string $finished ): ?int { if ( null === $finished || '' === $created || '' === $finished ) { return null; } $start = strtotime( $created ); $end = strtotime( $finished ); if ( ! is_int( $start ) || ! is_int( $end ) || $end < $start ) { return null; } return $end - $start; } }