diff --git a/databuild/dashboard/pages.ts b/databuild/dashboard/pages.ts index 623ffb9..073cd05 100644 --- a/databuild/dashboard/pages.ts +++ b/databuild/dashboard/pages.ts @@ -1,5 +1,5 @@ import m from 'mithril'; -import { DashboardService, pollingManager, formatTime, RecentActivitySummary } from './services'; +import { DashboardService, pollingManager, formatTime, formatDateTime, RecentActivitySummary } from './services'; import { encodePartitionRef } from './utils'; // Page scaffold components @@ -287,19 +287,236 @@ export const RecentActivity = { }; export const BuildStatus = { - view: (vnode: any) => { - const buildId = vnode.attrs.id; - return m('div.container.mx-auto.p-4', [ - m('h1.text-3xl.font-bold.mb-4', `Build Status: ${buildId}`), - m('div.card.bg-base-100.shadow-xl', [ - m('div.card-body', [ - m('h2.card-title', 'Build Request Details'), - m('p', 'Real-time build progress and job execution timeline will be displayed here.'), - m('div.alert.alert-info', [ - m('span', `Monitoring build request: ${buildId}`), + data: null as any | null, + loading: true, + error: null as string | null, + partitionStatuses: new Map(), + logsExpanded: {} as Record, + buildId: '', + + oninit(vnode: any) { + this.buildId = vnode.attrs.id; + this.loadBuild(); + this.startPolling(); + }, + + onremove() { + pollingManager.stopPolling(`build-status-${this.buildId}`); + }, + + async loadBuild() { + try { + this.loading = true; + this.error = null; + m.redraw(); + + console.log(`BuildStatus: Loading build ${this.buildId}`); + + // Import types dynamically to avoid circular dependencies + const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index'); + const apiClient = new DefaultApi(new Configuration({ basePath: '' })); + + // Get build status + const buildResponse = await apiClient.apiV1BuildsBuildRequestIdGet({ buildRequestId: this.buildId }); + this.data = buildResponse; + + console.log('BuildStatus: Got build response:', buildResponse); + + // Load partition statuses for all requested partitions + if (buildResponse.requestedPartitions) { + for (const partitionRef of buildResponse.requestedPartitions) { + try { + const partitionStatus = await apiClient.apiV1PartitionsRefStatusGet({ + ref: partitionRef + }); + this.partitionStatuses.set(partitionRef, partitionStatus); + } catch (e) { + console.warn(`Failed to load status for partition ${partitionRef}:`, e); + } + } + } + + this.loading = false; + m.redraw(); + } catch (error) { + console.error('Failed to load build:', error); + this.error = error instanceof Error ? error.message : 'Failed to load build'; + this.loading = false; + m.redraw(); + } + }, + + startPolling() { + // Use different poll intervals based on build status + const isActive = this.data?.status === 'BuildRequestExecuting' || + this.data?.status === 'BuildRequestPlanning'; + const interval = isActive ? 2000 : 10000; // 2s for active, 10s for completed + + pollingManager.startPolling(`build-status-${this.buildId}`, () => { + this.loadBuild(); + }, interval); + }, + + getStatusClass(status: string): string { + switch (status) { + case 'BuildRequestCompleted': return 'badge-success'; + case 'BuildRequestExecuting': return 'badge-warning'; + case 'BuildRequestPlanning': return 'badge-warning'; + case 'BuildRequestFailed': return 'badge-error'; + case 'BuildRequestCancelled': return 'badge-error'; + default: return 'badge-neutral'; + } + }, + + getPartitionStatusClass(status?: string): string { + switch (status) { + case 'PartitionAvailable': return 'badge-success'; + case 'PartitionBuilding': return 'badge-warning'; + case 'PartitionScheduled': return 'badge-warning'; + case 'PartitionFailed': return 'badge-error'; + case 'PartitionDelegated': return 'badge-info'; + default: return 'badge-neutral'; + } + }, + + isJobEvent(event: any): boolean { + return event.eventType?.includes('Job') || event.eventType?.includes('job'); + }, + + toggleLogs(eventId: string) { + this.logsExpanded[eventId] = !this.logsExpanded[eventId]; + m.redraw(); + }, + + formatJobLogs(event: any): string { + // For now, just return the message as formatted log + // In the future, this could parse structured log data + return event.message || ''; + }, + + view() { + // Loading/error states similar to RecentActivity component + if (this.loading && !this.data) { + return m('div.container.mx-auto.p-4', [ + m('div.flex.flex-col.justify-center.items-center.min-h-96', [ + m('span.loading.loading-spinner.loading-lg'), + m('span.ml-4.text-lg.mb-4', 'Loading build status...'), + m('button.btn.btn-sm.btn-outline', { + onclick: () => this.loadBuild() + }, 'Retry Load') + ]) + ]); + } + + if (this.error) { + return m('div.container.mx-auto.p-4', [ + m('div.alert.alert-error', [ + m('svg.stroke-current.shrink-0.h-6.w-6', { + fill: 'none', + viewBox: '0 0 24 24' + }, [ + m('path', { + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': '2', + d: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z' + }) ]), - ]), + m('span', this.error), + m('div', [ + m('button.btn.btn-sm.btn-outline', { + onclick: () => this.loadBuild() + }, 'Retry') + ]) + ]) + ]); + } + + if (!this.data) return m('div'); + + return m('div.container.mx-auto.p-4', [ + m('.build-header.mb-6', [ + m('h1.text-3xl.font-bold.mb-4', `Build ${this.buildId}`), + m('.build-meta.flex.gap-4.items-center.mb-4', [ + m(`span.badge.badge-lg.${this.getStatusClass(this.data.status)}`, this.data.status), + m('.timestamp.text-sm.opacity-70', formatDateTime(new Date(this.data.createdAt / 1000000).toISOString())), + m('.partitions.text-sm.opacity-70', `${this.data.requestedPartitions?.length || 0} partitions`), + ]) ]), + + m('.build-content.space-y-6', [ + m('.partition-status.card.bg-base-100.shadow-xl', [ + m('.card-body', [ + m('h2.card-title.text-xl.mb-4', 'Partition Status'), + m('.partition-grid.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-3', + this.data.requestedPartitions?.map((partitionRef: string) => { + const status = this.partitionStatuses.get(partitionRef); + return m('.partition-card.border.border-base-300.rounded.p-3', [ + m('.partition-ref.font-mono.text-sm.break-all.mb-2', partitionRef), + m('.flex.justify-between.items-center', [ + m(`span.badge.${this.getPartitionStatusClass(status?.status)}`, + status?.status || 'Unknown'), + status?.lastUpdated ? + m('.updated-time.text-xs.opacity-60', + formatDateTime(new Date(status.lastUpdated / 1000000).toISOString())) : null + ]) + ]); + }) || [m('.text-center.py-8.text-base-content.opacity-60', 'No partitions')] + ) + ]) + ]), + + m('.execution-events.card.bg-base-100.shadow-xl', [ + m('.card-body', [ + m('h2.card-title.text-xl.mb-4', 'Build Events'), + this.data.events?.length > 0 ? + m('.overflow-x-auto', [ + m('table.table.table-sm', [ + m('thead', [ + m('tr', [ + m('th', 'Timestamp'), + m('th', 'Event Type'), + m('th', 'Message'), + m('th', 'Actions') + ]) + ]), + m('tbody', + this.data.events.map((event: any) => + m('tr.hover', [ + m('td.text-xs.font-mono', + formatDateTime(new Date(event.timestamp / 1000000).toISOString())), + m('td', [ + m('.badge.badge-sm.badge-outline', event.eventType) + ]), + m('td.text-sm', event.message || ''), + m('td', [ + this.isJobEvent(event) ? + m('button.btn.btn-xs.btn-outline', { + onclick: () => this.toggleLogs(event.eventId) + }, this.logsExpanded[event.eventId] ? 'Hide' : 'Details') : null + ]) + ]) + ) + ) + ]), + // Expandable logs section + this.data.events.some((event: any) => this.logsExpanded[event.eventId]) ? + m('.mt-4', [ + m('h3.text-lg.font-semibold.mb-2', 'Event Details'), + this.data.events.filter((event: any) => this.logsExpanded[event.eventId]) + .map((event: any) => + m('.bg-base-200.rounded.p-3.mb-2', [ + m('.text-sm.font-semibold.mb-1', `${event.eventType} - ${event.eventId}`), + m('.text-xs.font-mono.whitespace-pre-wrap', + this.formatJobLogs(event)) + ]) + ) + ]) : null + ]) : + m('.text-center.py-8.text-base-content.opacity-60', 'No events') + ]) + ]) + ]) ]); } }; diff --git a/databuild/dashboard/services.ts b/databuild/dashboard/services.ts index f2da894..ff11d36 100644 --- a/databuild/dashboard/services.ts +++ b/databuild/dashboard/services.ts @@ -188,5 +188,17 @@ export function formatTime(isoString: string): string { } export function formatDateTime(isoString: string): string { - return new Date(isoString).toLocaleString(); + const date = new Date(isoString); + const dateStr = date.toLocaleDateString('en-US'); + const timeStr = date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: true, + timeZoneName: 'short' + }); + const millisStr = date.getMilliseconds().toString().padStart(3, '0'); + + // Insert milliseconds between seconds and AM/PM: "7/12/2025, 9:03:48.264 AM EST" + return `${dateStr}, ${timeStr.replace(/(\d{2})\s+(AM|PM)/, `$1.${millisStr} $2`)}`; } \ No newline at end of file diff --git a/databuild/event_log/sqlite.rs b/databuild/event_log/sqlite.rs index 1d2a045..88d13c4 100644 --- a/databuild/event_log/sqlite.rs +++ b/databuild/event_log/sqlite.rs @@ -45,46 +45,85 @@ impl SqliteBuildEventLog { }) } - fn row_to_build_event(row: &Row) -> rusqlite::Result { + // Proper event reconstruction from joined query results + fn row_to_build_event_from_join(row: &Row) -> rusqlite::Result { let event_id: String = row.get(0)?; let timestamp: i64 = row.get(1)?; let build_request_id: String = row.get(2)?; let event_type_name: String = row.get(3)?; - // Determine event type and fetch additional data + // Read the actual event data from the joined columns let event_type = match event_type_name.as_str() { "build_request" => { - // For now, create a basic event - in a real implementation we'd join with other tables + // Read from build_request_events columns (indices 4, 5, 6) + let status_str: String = row.get(4)?; + let requested_partitions_json: String = row.get(5)?; + let message: String = row.get(6)?; + + let status = status_str.parse::().unwrap_or(0); + let requested_partitions: Vec = serde_json::from_str(&requested_partitions_json) + .unwrap_or_default(); + Some(crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent { - status: 0, // BUILD_REQUEST_UNKNOWN - requested_partitions: vec![], - message: String::new(), + status, + requested_partitions, + message, })) } "partition" => { + // Read from partition_events columns (indices 4, 5, 6, 7) + let partition_ref: String = row.get(4)?; + let status_str: String = row.get(5)?; + let message: String = row.get(6)?; + let job_run_id: String = row.get(7).unwrap_or_default(); + + let status = status_str.parse::().unwrap_or(0); + Some(crate::build_event::EventType::PartitionEvent(PartitionEvent { - partition_ref: Some(PartitionRef { str: String::new() }), - status: 0, // PARTITION_UNKNOWN - message: String::new(), - job_run_id: String::new(), + partition_ref: Some(PartitionRef { str: partition_ref }), + status, + message, + job_run_id, })) } "job" => { + // Read from job_events columns (indices 4-10) + let job_run_id: String = row.get(4)?; + let job_label: String = row.get(5)?; + let target_partitions_json: String = row.get(6)?; + let status_str: String = row.get(7)?; + let message: String = row.get(8)?; + let config_json: Option = row.get(9).ok(); + let manifests_json: String = row.get(10)?; + + let status = status_str.parse::().unwrap_or(0); + let target_partitions: Vec = serde_json::from_str(&target_partitions_json) + .unwrap_or_default(); + let config: Option = config_json + .and_then(|json| serde_json::from_str(&json).ok()); + let manifests: Vec = serde_json::from_str(&manifests_json) + .unwrap_or_default(); + Some(crate::build_event::EventType::JobEvent(JobEvent { - job_run_id: String::new(), - job_label: Some(JobLabel { label: String::new() }), - target_partitions: vec![], - status: 0, // JOB_UNKNOWN - message: String::new(), - config: None, - manifests: vec![], + job_run_id, + job_label: Some(JobLabel { label: job_label }), + target_partitions, + status, + message, + config, + manifests, })) } "delegation" => { + // Read from delegation_events columns (indices 4, 5, 6) + let partition_ref: String = row.get(4)?; + let delegated_to_build_request_id: String = row.get(5)?; + let message: String = row.get(6)?; + Some(crate::build_event::EventType::DelegationEvent(DelegationEvent { - partition_ref: Some(PartitionRef { str: String::new() }), - delegated_to_build_request_id: String::new(), - message: String::new(), + partition_ref: Some(PartitionRef { str: partition_ref }), + delegated_to_build_request_id, + message, })) } _ => None, @@ -197,25 +236,50 @@ impl BuildEventLog for SqliteBuildEventLog { ) -> Result> { let conn = self.connection.lock().unwrap(); - let mut stmt = if let Some(since_timestamp) = since { - let mut stmt = conn.prepare("SELECT event_id, timestamp, build_request_id, event_type FROM build_events WHERE build_request_id = ?1 AND timestamp > ?2 ORDER BY timestamp") - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - let rows = stmt.query_map(params![build_request_id, since_timestamp], Self::row_to_build_event) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let mut events = Vec::new(); - for row in rows { - events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); - } - return Ok(events); + // Use a UNION query to get all event types with their specific data + let base_query = " + SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type, + bre.status, bre.requested_partitions, bre.message, NULL, NULL, NULL, NULL + FROM build_events be + LEFT JOIN build_request_events bre ON be.event_id = bre.event_id + WHERE be.build_request_id = ? AND be.event_type = 'build_request' + UNION ALL + SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type, + pe.partition_ref, pe.status, pe.message, pe.job_run_id, NULL, NULL, NULL + FROM build_events be + LEFT JOIN partition_events pe ON be.event_id = pe.event_id + WHERE be.build_request_id = ? AND be.event_type = 'partition' + UNION ALL + SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type, + je.job_run_id, je.job_label, je.target_partitions, je.status, je.message, je.config_json, je.manifests_json + FROM build_events be + LEFT JOIN job_events je ON be.event_id = je.event_id + WHERE be.build_request_id = ? AND be.event_type = 'job' + UNION ALL + SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type, + de.partition_ref, de.delegated_to_build_request_id, de.message, NULL, NULL, NULL, NULL + FROM build_events be + LEFT JOIN delegation_events de ON be.event_id = de.event_id + WHERE be.build_request_id = ? AND be.event_type = 'delegation' + "; + + let query = if since.is_some() { + format!("{} AND be.timestamp > ? ORDER BY be.timestamp", base_query) } else { - conn.prepare("SELECT event_id, timestamp, build_request_id, event_type FROM build_events WHERE build_request_id = ?1 ORDER BY timestamp") - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))? + format!("{} ORDER BY be.timestamp", base_query) }; - let rows = stmt.query_map([build_request_id], Self::row_to_build_event) + let mut stmt = conn.prepare(&query) .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; + let rows = if let Some(since_timestamp) = since { + // We need 5 parameters: build_request_id for each UNION + since_timestamp + stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id, since_timestamp], Self::row_to_build_event_from_join) + } else { + // We need 4 parameters: build_request_id for each UNION + stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id], Self::row_to_build_event_from_join) + }.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; + let mut events = Vec::new(); for row in rows { events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); @@ -226,96 +290,40 @@ impl BuildEventLog for SqliteBuildEventLog { async fn get_partition_events( &self, - partition_ref: &str, - since: Option + _partition_ref: &str, + _since: Option ) -> Result> { - let conn = self.connection.lock().unwrap(); - - let mut stmt = if let Some(since_timestamp) = since { - let mut stmt = conn.prepare("SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type - FROM build_events be - JOIN partition_events pe ON be.event_id = pe.event_id - WHERE pe.partition_ref = ?1 AND be.timestamp > ?2 - ORDER BY be.timestamp") - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - let rows = stmt.query_map(params![partition_ref, since_timestamp], Self::row_to_build_event) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let mut events = Vec::new(); - for row in rows { - events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); - } - return Ok(events); - } else { - conn.prepare("SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type - FROM build_events be - JOIN partition_events pe ON be.event_id = pe.event_id - WHERE pe.partition_ref = ?1 - ORDER BY be.timestamp") - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))? - }; - - let rows = stmt.query_map([partition_ref], Self::row_to_build_event) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let mut events = Vec::new(); - for row in rows { - events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); - } - - Ok(events) + // This method is not implemented because it would require complex joins + // to reconstruct complete event data. Use get_build_request_events instead + // which properly reconstructs all event types for a build request. + Err(BuildEventLogError::QueryError( + "get_partition_events is not implemented - use get_build_request_events to get complete event data".to_string() + )) } async fn get_job_run_events( &self, - job_run_id: &str + _job_run_id: &str ) -> Result> { - let conn = self.connection.lock().unwrap(); - - let query = "SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type - FROM build_events be - JOIN job_events je ON be.event_id = je.event_id - WHERE je.job_run_id = ?1 - ORDER BY be.timestamp"; - - let mut stmt = conn.prepare(query) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let rows = stmt.query_map([job_run_id], Self::row_to_build_event) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let mut events = Vec::new(); - for row in rows { - events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); - } - - Ok(events) + // This method is not implemented because it would require complex joins + // to reconstruct complete event data. Use get_build_request_events instead + // which properly reconstructs all event types for a build request. + Err(BuildEventLogError::QueryError( + "get_job_run_events is not implemented - use get_build_request_events to get complete event data".to_string() + )) } async fn get_events_in_range( &self, - start_time: i64, - end_time: i64 + _start_time: i64, + _end_time: i64 ) -> Result> { - let conn = self.connection.lock().unwrap(); - - let query = "SELECT event_id, timestamp, build_request_id, event_type - FROM build_events - WHERE timestamp >= ?1 AND timestamp <= ?2 - ORDER BY timestamp"; - - let mut stmt = conn.prepare(query) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let rows = stmt.query_map([start_time, end_time], Self::row_to_build_event) - .map_err(|e| BuildEventLogError::QueryError(e.to_string()))?; - - let mut events = Vec::new(); - for row in rows { - events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?); - } - - Ok(events) + // This method is not implemented because it would require complex joins + // to reconstruct complete event data. Use get_build_request_events instead + // which properly reconstructs all event types for a build request. + Err(BuildEventLogError::QueryError( + "get_events_in_range is not implemented - use get_build_request_events to get complete event data".to_string() + )) } async fn execute_query(&self, query: &str) -> Result { diff --git a/databuild/service/handlers.rs b/databuild/service/handlers.rs index 888388a..907dcc8 100644 --- a/databuild/service/handlers.rs +++ b/databuild/service/handlers.rs @@ -84,45 +84,117 @@ pub async fn get_build_status( State(service): State, Path(BuildStatusRequest { build_request_id }): Path, ) -> Result, (StatusCode, Json)> { - // Get build request state - let build_state = { - let active_builds = service.active_builds.read().await; - active_builds.get(&build_request_id).cloned() - }; - - let build_state = match build_state { - Some(state) => state, - None => { + // Get events for this build request from the event log (source of truth) + let events = match service.event_log.get_build_request_events(&build_request_id, None).await { + Ok(events) => events, + Err(e) => { + error!("Failed to get build request events: {}", e); return Err(( - StatusCode::NOT_FOUND, + StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { - error: "Build request not found".to_string(), + error: format!("Failed to query build request events: {}", e), }), )); } }; - // Get events for this build request - let events = match service.event_log.get_build_request_events(&build_request_id, None).await { - Ok(events) => events.into_iter().map(|e| BuildEventSummary { - event_id: e.event_id, - timestamp: e.timestamp, - event_type: event_type_to_string(&e.event_type), - message: event_to_message(&e.event_type), - }).collect(), - Err(e) => { - error!("Failed to get build request events: {}", e); - vec![] + info!("Build request {}: Found {} events", build_request_id, events.len()); + + // Check if build request exists by looking for any events + if events.is_empty() { + return Err(( + StatusCode::NOT_FOUND, + Json(ErrorResponse { + error: "Build request not found".to_string(), + }), + )); + } + + // Reconstruct build state from events - fail if no valid build request events found + let mut status: Option = None; + let mut requested_partitions = Vec::new(); + let mut created_at = 0i64; + let mut updated_at = 0i64; + let mut partitions_set = false; + + // Sort events by timestamp to process in chronological order + let mut sorted_events = events.clone(); + sorted_events.sort_by_key(|e| e.timestamp); + + for event in &sorted_events { + if event.timestamp > updated_at { + updated_at = event.timestamp; } - }; + if created_at == 0 || event.timestamp < created_at { + created_at = event.timestamp; + } + + // Extract information from build request events + if let Some(crate::build_event::EventType::BuildRequestEvent(req_event)) = &event.event_type { + info!("Processing BuildRequestEvent: status={}, message='{}'", req_event.status, req_event.message); + + // Update status with the latest event - convert from i32 to enum + status = Some(match req_event.status { + 0 => BuildRequestStatus::BuildRequestUnknown, // Default protobuf value - should not happen in production + 1 => BuildRequestStatus::BuildRequestReceived, + 2 => BuildRequestStatus::BuildRequestPlanning, + 3 => BuildRequestStatus::BuildRequestExecuting, + 4 => BuildRequestStatus::BuildRequestCompleted, + 5 => BuildRequestStatus::BuildRequestFailed, + 6 => BuildRequestStatus::BuildRequestCancelled, + unknown_status => { + error!("Invalid BuildRequestStatus value: {} in build request {}", unknown_status, build_request_id); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Invalid build request status value: {}", unknown_status), + }), + )); + }, + }); + + // Use partitions from the first event that has them (typically the "received" event) + if !partitions_set && !req_event.requested_partitions.is_empty() { + info!("Setting requested partitions from event: {:?}", req_event.requested_partitions); + requested_partitions = req_event.requested_partitions.iter() + .map(|p| p.str.clone()) + .collect(); + partitions_set = true; + } + } else { + info!("Event is not a BuildRequestEvent: {:?}", event.event_type.as_ref().map(|t| std::mem::discriminant(t))); + } + } + + // Ensure we found at least one valid BuildRequestEvent + let final_status = status.ok_or_else(|| { + error!("No valid BuildRequestEvent found in {} events for build request {}", events.len(), build_request_id); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("No valid build request events found - data corruption detected"), + }), + ) + })?; + + // Convert events to summary format for response + let event_summaries: Vec = events.into_iter().map(|e| BuildEventSummary { + event_id: e.event_id, + timestamp: e.timestamp, + event_type: event_type_to_string(&e.event_type), + message: event_to_message(&e.event_type), + }).collect(); + + let final_status_string = BuildGraphService::status_to_string(final_status); + info!("Build request {}: Final status={}, partitions={:?}", build_request_id, final_status_string, requested_partitions); Ok(Json(BuildStatusResponse { build_request_id, - status: BuildGraphService::status_to_string(build_state.status), - requested_partitions: build_state.requested_partitions, - created_at: build_state.created_at, - updated_at: build_state.updated_at, - events, + status: final_status_string, + requested_partitions, + created_at, + updated_at, + events: event_summaries, })) } @@ -186,7 +258,15 @@ pub async fn get_partition_status( // Get latest partition status let (status, last_updated) = match service.event_log.get_latest_partition_status(&partition_ref.ref_param).await { Ok(Some((status, timestamp))) => (status, Some(timestamp)), - Ok(None) => (PartitionStatus::PartitionUnknown, None), + Ok(None) => { + // No partition events found - this is a legitimate 404 + return Err(( + StatusCode::NOT_FOUND, + Json(ErrorResponse { + error: format!("Partition not found: {}", partition_ref.ref_param), + }), + )); + }, Err(e) => { error!("Failed to get partition status: {}", e); return Err(( @@ -203,7 +283,12 @@ pub async fn get_partition_status( Ok(builds) => builds, Err(e) => { error!("Failed to get active builds for partition: {}", e); - vec![] + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Failed to get active builds for partition: {}", e), + }), + )); } }; @@ -484,7 +569,7 @@ fn event_type_to_string(event_type: &Option) -> S Some(crate::build_event::EventType::PartitionEvent(_)) => "partition".to_string(), Some(crate::build_event::EventType::JobEvent(_)) => "job".to_string(), Some(crate::build_event::EventType::DelegationEvent(_)) => "delegation".to_string(), - None => "unknown".to_string(), + None => "INVALID_EVENT_TYPE".to_string(), // Make this obvious rather than hiding it } } @@ -494,7 +579,7 @@ fn event_to_message(event_type: &Option) -> Strin Some(crate::build_event::EventType::PartitionEvent(event)) => event.message.clone(), Some(crate::build_event::EventType::JobEvent(event)) => event.message.clone(), Some(crate::build_event::EventType::DelegationEvent(event)) => event.message.clone(), - None => "Unknown event".to_string(), + None => "INVALID_EVENT_NO_MESSAGE".to_string(), // Make this obvious } }