self::STATUS_EXPIRED, 'progress' => 0, 'phase' => '', ]; } $terminal = self::find_terminal_event( $sse_events ); $progress = self::derive_progress( $sse_events, $session_data ); $phase = self::derive_phase( $sse_events, $session_data ); if ( $terminal && 'complete' === $terminal['action'] ) { return [ 'status' => self::STATUS_COMPLETE, 'progress' => 100, 'phase' => 'finalizer', 'summary' => $terminal['results'] ?? [], ]; } if ( $terminal && 'error' === $terminal['action'] ) { $retry = ! empty( $terminal['retry'] ); return [ 'status' => $retry ? self::STATUS_NEEDS_RETRY : self::STATUS_FAILED, 'progress' => $progress, 'phase' => $phase, // Message + code only — never a stack trace (FR-006; the trace is // already stripped server-side in RunsImport::import()). 'message' => isset( $terminal['message'] ) ? (string) $terminal['message'] : '', ]; } // A slice ran (or is between chunks) with no terminal event: still running. return [ 'status' => self::STATUS_RUNNING, 'progress' => $progress, 'phase' => $phase, ]; } /** * The last complete/error event in the slice, if any (terminal wins over * intermediate `continue`/`updateLog` events). * * @param array $sse_events * @return array|null */ private static function find_terminal_event( array $sse_events ): ?array { $found = null; foreach ( $sse_events as $event ) { if ( ! is_array( $event ) ) { continue; } $action = $event['action'] ?? ''; if ( 'complete' === $action || 'error' === $action ) { $found = $event; // keep the last one } } return $found; } /** * Coarse 0-100 progress: prefer an explicit `progress` from the latest * updateLog event; otherwise approximate from completed session steps. * * @param array $sse_events * @param array $session_data * @return int */ private static function derive_progress( array $sse_events, array $session_data ): int { $explicit = null; foreach ( $sse_events as $event ) { if ( is_array( $event ) && isset( $event['progress'] ) && is_numeric( $event['progress'] ) ) { $explicit = (int) $event['progress']; } } if ( null !== $explicit ) { return max( 0, min( 100, $explicit ) ); } // Fallback: fraction of known early setup steps completed. $steps = [ 'create_log_dir', 'check_writing_permission', 'download_zip' ]; $progress = $session_data['progress'] ?? []; $done = 0; foreach ( $steps as $step ) { if ( ! empty( $progress[ $step ] ) ) { $done++; } } // Cap the setup-only fallback well below 100 — content import follows. return (int) round( ( $done / count( $steps ) ) * 40 ); } /** * Current phase label from the latest informative event, else the last * completed session step. * * @param array $sse_events * @param array $session_data * @return string */ private static function derive_phase( array $sse_events, array $session_data ): string { $phase = ''; foreach ( $sse_events as $event ) { if ( ! is_array( $event ) ) { continue; } if ( ! empty( $event['info'] ) ) { $phase = (string) $event['info']; } elseif ( ! empty( $event['type'] ) && ! in_array( $event['type'], [ 'eventLog', 'updateLog', 'start' ], true ) ) { $phase = (string) $event['type']; } } if ( '' === $phase ) { $progress = $session_data['progress'] ?? []; if ( is_array( $progress ) && ! empty( $progress ) ) { $keys = array_keys( array_filter( $progress ) ); $phase = end( $keys ) ?: ''; } } return $phase; } }