Fix ui stuff

This commit is contained in:
soaxelbrooke 2025-07-12 11:58:54 -07:00
parent fea216d6c3
commit 7bb999ab47
4 changed files with 480 additions and 158 deletions

View file

@ -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<string, any>(),
logsExpanded: {} as Record<string, boolean>,
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')
])
])
])
]);
}
};

View file

@ -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`)}`;
}

View file

@ -45,46 +45,85 @@ impl SqliteBuildEventLog {
})
}
fn row_to_build_event(row: &Row) -> rusqlite::Result<BuildEvent> {
// Proper event reconstruction from joined query results
fn row_to_build_event_from_join(row: &Row) -> rusqlite::Result<BuildEvent> {
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::<i32>().unwrap_or(0);
let requested_partitions: Vec<PartitionRef> = 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::<i32>().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<String> = row.get(9).ok();
let manifests_json: String = row.get(10)?;
let status = status_str.parse::<i32>().unwrap_or(0);
let target_partitions: Vec<PartitionRef> = serde_json::from_str(&target_partitions_json)
.unwrap_or_default();
let config: Option<JobConfig> = config_json
.and_then(|json| serde_json::from_str(&json).ok());
let manifests: Vec<PartitionManifest> = 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<Vec<BuildEvent>> {
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<i64>
_partition_ref: &str,
_since: Option<i64>
) -> Result<Vec<BuildEvent>> {
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<Vec<BuildEvent>> {
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<Vec<BuildEvent>> {
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<QueryResult> {

View file

@ -84,45 +84,117 @@ pub async fn get_build_status(
State(service): State<ServiceState>,
Path(BuildStatusRequest { build_request_id }): Path<BuildStatusRequest>,
) -> Result<Json<BuildStatusResponse>, (StatusCode, Json<ErrorResponse>)> {
// 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<BuildRequestStatus> = 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<BuildEventSummary> = 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<crate::build_event::EventType>) -> 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<crate::build_event::EventType>) -> 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
}
}