Fix ui stuff
This commit is contained in:
parent
fea216d6c3
commit
7bb999ab47
4 changed files with 480 additions and 158 deletions
|
|
@ -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')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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`)}`;
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue