diff --git a/plans/webapp_v1/chunk-5-build-status.md b/plans/webapp_v1/chunk-5-build-status.md index 8707bc0..3aac839 100644 --- a/plans/webapp_v1/chunk-5-build-status.md +++ b/plans/webapp_v1/chunk-5-build-status.md @@ -27,106 +27,186 @@ Implement the core operational build request status page with real-time updates, ## Technical Approach ### Data Sources -From Build Graph Service API `/api/v1/builds/:id`: -- Build request metadata (ID, status, timestamps) -- Requested partitions -- Build events with job execution details -- Delegation information +From Build Graph Service API: +- `/api/v1/builds/:id` - Individual build request details and events +- `/api/v1/partitions/:ref/status` - Individual partition status +- `/api/v1/partitions/:ref/events` - Partition-specific events + +Available TypeScript types from generated client: +- `BuildStatusResponse` - Build request metadata, status, events +- `PartitionStatusResponse` - Individual partition status +- `PartitionEventsResponse` - Partition build events +- `BuildRequestStatus` enum - Status values (Received, Planning, Executing, Completed, Failed, Cancelled) +- `PartitionStatus` enum - Status values (Requested, Scheduled, Building, Available, Failed, Delegated) + +**Note**: Timestamps are in nanoseconds and require conversion via `/1000000` for JavaScript Date objects. ### Component Structure ```typescript +import { DefaultApi, Configuration, BuildStatusResponse, PartitionStatusResponse } from '../client/typescript_generated/src/index'; +import { pollingManager, formatTime } from './services'; + const BuildStatus = { + data: null as BuildStatusResponse | null, + loading: true, + error: null as string | null, + partitionStatuses: new Map(), + logsExpanded: {} as Record, + oninit: (vnode) => { this.buildId = vnode.attrs.id; - this.build = null; - this.pollInterval = null; this.loadBuild(); this.startPolling(); }, onremove: () => { - this.stopPolling(); + pollingManager.stopPolling(`build-status-${this.buildId}`); + }, + + async loadBuild() { + try { + this.loading = true; + this.error = null; + m.redraw(); + + const apiClient = new DefaultApi(new Configuration({ basePath: '' })); + + // Get build status + const buildResponse = await apiClient.apiV1BuildsBuildRequestIdGet({ buildRequestId: this.buildId }); + this.data = buildResponse; + + // Load partition statuses for all requested partitions + if (buildResponse.requestedPartitions) { + for (const partition of buildResponse.requestedPartitions) { + try { + const partitionStatus = await apiClient.apiV1PartitionsRefStatusGet({ + ref: partition.str + }); + this.partitionStatuses.set(partition.str, partitionStatus); + } catch (e) { + console.warn(`Failed to load status for partition ${partition.str}:`, 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); }, view: () => [ - m('.build-header', [ - m('h1', `Build ${this.buildId}`), - m('.build-meta', [ - m('.badge', this.build?.status), - m('.timestamp', formatTime(this.build?.created_at)), - m('.partitions', `${this.build?.requested_partitions.length} partitions`), - ]) - ]), + // Loading/error states similar to RecentActivity component + this.loading && !this.data ? m('.loading-state', '...') : null, + this.error ? m('.error-state', this.error) : null, - m('.build-content', [ - m('.partition-status', [ - m('h2', 'Partition Status'), - m('.partition-grid', - this.build?.requested_partitions.map(partition => - m('.partition-card', [ - m('.partition-ref', partition), - m('.partition-status', this.getPartitionStatus(partition)), - m('.partition-job', this.getPartitionJob(partition)), - ]) - ) - ) + this.data ? [ + m('.build-header', [ + m('h1', `Build ${this.buildId}`), + m('.build-meta', [ + m(`span.badge.${this.getStatusClass(this.data.status)}`, this.data.status), + m('.timestamp', formatTime(new Date(this.data.createdAt / 1000000).toISOString())), + m('.partitions', `${this.data.requestedPartitions?.length || 0} partitions`), + ]) ]), - m('.execution-timeline', [ - m('h2', 'Execution Timeline'), - m('.timeline', this.build?.events.map(event => - m('.timeline-item', [ - m('.timestamp', formatTime(event.timestamp)), - m('.event-type', event.event_type), - m('.message', event.message), - this.isJobEvent(event) ? m('.expandable-logs', [ - m('button', { onclick: () => this.toggleLogs(event) }, 'Show Logs'), - this.logsExpanded[event.event_id] ? - m('.logs', this.formatJobLogs(event)) : null - ]) : null - ]) - )) + m('.build-content', [ + m('.partition-status', [ + m('h2', 'Partition Status'), + m('.partition-grid', + this.data.requestedPartitions?.map(partition => { + const status = this.partitionStatuses.get(partition.str); + return m('.partition-card', [ + m('.partition-ref', partition.str), + m(`span.badge.${this.getPartitionStatusClass(status?.status)}`, + status?.status || 'Unknown'), + status?.updatedAt ? + m('.updated-time', formatTime(new Date(status.updatedAt / 1000000).toISOString())) : null + ]); + }) || [] + ) + ]), + + m('.execution-timeline', [ + m('h2', 'Execution Timeline'), + m('.timeline', this.data.events?.map(event => + m('.timeline-item', [ + m('.timestamp', formatTime(new Date(event.timestamp / 1000000).toISOString())), + m('.event-type', event.eventType), + m('.message', event.message || ''), + // Add expandable logs for job events + this.isJobEvent(event) ? m('.expandable-logs', [ + m('button.btn.btn-sm', { + onclick: () => this.toggleLogs(event.eventId) + }, this.logsExpanded[event.eventId] ? 'Hide Logs' : 'Show Logs'), + this.logsExpanded[event.eventId] ? + m('.logs', this.formatJobLogs(event)) : null + ]) : null + ]) + ) || []) + ]) ]) - ]) + ] : null ] }; ``` ### Real-time Updates -- Poll every 2 seconds when build is active -- Poll every 10 seconds when build is completed -- Stop polling when tab is not visible -- Visual indicators for live updates +- Use existing `pollingManager` from `services.ts` with Page Visibility API +- Poll every 2 seconds when build status is `BuildRequestExecuting` or `BuildRequestPlanning` +- Poll every 10 seconds when build is `BuildRequestCompleted`, `BuildRequestFailed`, or `BuildRequestCancelled` +- Automatic polling pause when tab is not visible +- Explicit `m.redraw()` calls after async data loading ### Status Visualization -- Color-coded status badges (green/yellow/red) -- Progress indicators for running builds -- Delegation indicators with links to other builds -- Timeline visualization for build events +- Color-coded status badges using DaisyUI classes: + - `badge-success` for `BuildRequestCompleted` / `PartitionAvailable` + - `badge-warning` for `BuildRequestExecuting` / `PartitionBuilding` + - `badge-error` for `BuildRequestFailed` / `PartitionFailed` + - `badge-neutral` for other states +- Timeline visualization for build events with timestamp formatting +- Delegation indicators for `PartitionDelegated` status with build request links ## Implementation Strategy -1. **Create Data Layer** - - API integration for build status - - Event parsing and categorization - - Polling manager with different intervals +1. **Extend Existing Infrastructure** + - Use established `DefaultApi` client pattern from `services.ts` + - Leverage existing `pollingManager` with Page Visibility API + - Follow `RecentActivity` component patterns for loading/error states + - Import generated TypeScript types from client 2. **Build Status Components** - - Build header with metadata - - Partition status grid - - Execution timeline - - Expandable log sections + - Build header with metadata using `BuildStatusResponse` + - Partition status grid using `PartitionStatusResponse` for each partition + - Execution timeline parsing `events` array from build response + - Expandable log sections for job events with state management 3. **Real-time Updates** - - Intelligent polling based on build state - - Page Visibility API integration - - Loading states and error handling + - Intelligent polling based on `BuildRequestStatus` enum values + - Reuse existing `pollingManager` infrastructure + - Loading states and error handling following established patterns + - Proper `m.redraw()` calls after async operations 4. **Status Visualization** - - Color-coded status indicators - - Progress bars for active builds - - Timeline layout for events - - Delegation indicators + - Status badge classes using established DaisyUI patterns + - Timeline layout similar to existing dashboard components + - Partition delegation links using build request IDs + - Timestamp formatting using existing `formatTime` utility ## Deliverables