import m from 'mithril'; import { DashboardService, pollingManager, formatTime, formatDateTime, formatDuration, formatDate, RecentActivitySummary } from './services'; import { encodePartitionRef, decodePartitionRef, encodeJobLabel, decodeJobLabel, BuildStatusBadge, PartitionStatusBadge, EventTypeBadge } from './utils'; // Page scaffold components export const RecentActivity = { data: null as RecentActivitySummary | null, loading: true, error: null as string | null, pollInterval: null as NodeJS.Timeout | null, loadData() { this.loading = true; this.error = null; m.redraw(); // Redraw to show loading state const service = DashboardService.getInstance(); return service.getRecentActivity() .then(data => { this.data = data; this.loading = false; m.redraw(); // Explicitly redraw after data loads }) .catch(error => { console.error('RecentActivity: Error in loadData:', error); this.error = error instanceof Error ? error.message : 'Failed to load data'; this.loading = false; m.redraw(); // Redraw after error }); }, oninit() { // Load initial data - Mithril will automatically redraw after promise resolves this.loadData(); // Set up polling for real-time updates (5 second interval) if (pollingManager.isVisible()) { pollingManager.startPolling('recent-activity', () => { this.loadData(); }, 5000); } }, onremove() { // Clean up polling when component is removed pollingManager.stopPolling('recent-activity'); }, view: function() { 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 dashboard...'), m('button.btn.btn-sm.btn-outline', { onclick: () => this.loadData() }, '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.loadData() }, 'Retry') ]) ]) ]); } const data = this.data; if (!data) return m('div'); return m('div.container.mx-auto.p-4', [ // Dashboard Header m('div.dashboard-header.mb-6', [ m('div.flex.justify-between.items-center.mb-4', [ m('h1.text-3xl.font-bold', 'DataBuild Dashboard'), m('div.badge.badge-primary.badge-lg', data.graphName) ]), // Statistics m('div.stats.shadow.w-full.bg-base-100', [ m('div.stat', [ m('div.stat-figure.text-primary', [ m('svg.w-8.h-8', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [ m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M13 10V3L4 14h7v7l9-11h-7z' }) ]) ]), m('div.stat-title', 'Active Builds'), m('div.stat-value.text-primary', data.activeBuilds), m('div.stat-desc', 'Currently running') ]), m('div.stat', [ m('div.stat-figure.text-secondary', [ m('svg.w-8.h-8', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [ m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z' }) ]) ]), m('div.stat-title', 'Recent Builds'), m('div.stat-value.text-secondary', data.recentBuilds.length), m('div.stat-desc', 'In the last hour') ]), m('div.stat', [ m('div.stat-figure.text-accent', [ m('svg.w-8.h-8', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [ m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 5l8 4' }) ]) ]), m('div.stat-title', 'Total Partitions'), m('div.stat-value.text-accent', data.totalPartitions), m('div.stat-desc', 'Managed partitions') ]) ]) ]), // Dashboard Content Grid m('div.dashboard-content.grid.grid-cols-1.lg:grid-cols-2.gap-6', [ // Recent Build Requests m('div.recent-builds.card.bg-base-100.shadow-xl', [ m('div.card-body', [ m('h2.card-title.text-xl.mb-4', [ m('svg.w-6.h-6.mr-2', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [ m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M13 10V3L4 14h7v7l9-11h-7z' }) ]), 'Recent Build Requests' ]), data.recentBuilds.length === 0 ? m('div.text-center.py-8.text-base-content.opacity-60', 'No recent builds') : m('div.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Build ID'), m('th', 'Status'), m('th', 'Created'), ]) ]), m('tbody', data.recentBuilds.map(build => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm', { href: `/builds/${build.id}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${build.id}`); } }, build.id) ]), m('td', [ m(BuildStatusBadge, { status: build.status }) ]), m('td.text-sm.opacity-70', formatTime(build.createdAt)), ]) ) ) ]) ]) ]) ]), // Recent Partition Builds m('div.recent-partitions.card.bg-base-100.shadow-xl', [ m('div.card-body', [ m('h2.card-title.text-xl.mb-4', [ m('svg.w-6.h-6.mr-2', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, [ m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 5l8 4' }) ]), 'Recent Partition Builds' ]), data.recentPartitions.length === 0 ? m('div.text-center.py-8.text-base-content.opacity-60', 'No recent partitions') : m('div.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Partition Reference'), m('th', 'Status'), m('th', 'Updated'), ]) ]), m('tbody', data.recentPartitions.map(partition => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm.break-all', { href: `/partitions/${encodePartitionRef(partition.ref)}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/partitions/${encodePartitionRef(partition.ref)}`); }, title: partition.ref }, partition.ref) ]), m('td', [ m(PartitionStatusBadge, { status: partition.status }) ]), m('td.text-sm.opacity-70', formatTime(partition.updatedAt)), ]) ) ) ]) ]) ]) ]) ]) ]); } }; export const BuildStatus = { data: null as any | null, loading: true, error: null as string | null, partitionStatuses: new Map(), 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(); // 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; // 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); }, getEventLink(event: any): { href: string; text: string } | null { const eventType = event.eventType; switch (eventType) { case 'job': if (event.jobLabel) { return { href: `/jobs/${encodeURIComponent(event.jobLabel)}`, text: 'Job Details' }; } return null; case 'partition': if (event.partitionRef) { return { href: `/partitions/${encodePartitionRef(event.partitionRef)}`, text: 'Partition Status' }; } return null; case 'delegation': if (event.delegatedBuildId) { return { href: `/builds/${event.delegatedBuildId}`, text: 'Delegated Build' }; } return null; case 'build_request': // Self-referential, no additional link needed return null; default: return null; } }, 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(BuildStatusBadge, { status: this.data.status, size: 'lg' }), m('.timestamp.text-sm.opacity-70', formatDateTime(new Date(this.data.createdAt).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('a.partition-ref.font-mono.text-sm.break-all.mb-2.link.link-primary', { href: `/partitions/${encodePartitionRef(partitionRef)}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/partitions/${encodePartitionRef(partitionRef)}`); }, title: `View details for partition: ${partitionRef}` }, partitionRef), m('.flex.justify-between.items-center', [ m(PartitionStatusBadge, { status: status?.status || 'Unknown' }), status?.lastUpdated ? m('.updated-time.text-xs.opacity-60', formatDateTime(new Date(status.lastUpdated).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', 'Link') ]) ]), m('tbody', this.data.events.map((event: any) => m('tr.hover', [ m('td.text-xs.font-mono', formatDateTime(new Date(event.timestamp).toISOString())), m('td', [ m(EventTypeBadge, { eventType: event.eventType }) ]), m('td.text-sm', event.message || ''), m('td', [ (() => { const link = this.getEventLink(event); return link ? m(m.route.Link, { href: link.href, class: 'link link-primary text-sm' }, link.text) : m('span.text-xs.opacity-50', '—'); })() ]) ]) ) ) ]) ]) : m('.text-center.py-8.text-base-content.opacity-60', 'No events') ]) ]) ]) ]); } }; export const PartitionsList = { data: null as any | null, loading: true, error: null as string | null, searchTerm: '', async loadPartitions() { try { this.loading = true; this.error = null; m.redraw(); const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index'); const apiClient = new DefaultApi(new Configuration({ basePath: '' })); const response = await apiClient.apiV1PartitionsGet(); this.data = response; this.loading = false; m.redraw(); } catch (error) { console.error('Failed to load partitions:', error); this.error = error instanceof Error ? error.message : 'Failed to load partitions'; this.loading = false; m.redraw(); } }, async buildPartition(partitionRef: string) { try { const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index'); const apiClient = new DefaultApi(new Configuration({ basePath: '' })); const buildRequest = { partitions: [partitionRef] }; const response = await apiClient.apiV1BuildsPost({ buildRequest }); // Redirect to build status page m.route.set(`/builds/${response.buildRequestId}`); } catch (error) { console.error('Failed to start build:', error); alert(`Failed to start build: ${error instanceof Error ? error.message : 'Unknown error'}`); } }, filteredPartitions() { if (!this.data?.partitions) return []; if (!this.searchTerm) return this.data.partitions; const search = this.searchTerm.toLowerCase(); return this.data.partitions.filter((partition: any) => partition.partition_ref.toLowerCase().includes(search) ); }, oninit() { this.loadPartitions(); }, view() { 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 partitions...'), m('button.btn.btn-sm.btn-outline', { onclick: () => this.loadPartitions() }, '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.loadPartitions() }, 'Retry') ]) ]) ]); } const filteredPartitions = this.filteredPartitions(); return m('div.container.mx-auto.p-4', [ m('.partitions-header.mb-6', [ m('div.flex.justify-between.items-center.mb-4', [ m('h1.text-3xl.font-bold', 'Partitions'), m('.badge.badge-primary.badge-lg', `${this.data?.totalCount || 0} total`) ]), m('div.form-control.mb-4', [ m('input.input.input-bordered.w-full.max-w-md', { placeholder: 'Search partitions...', value: this.searchTerm, oninput: (e: Event) => { this.searchTerm = (e.target as HTMLInputElement).value; m.redraw(); } }) ]) ]), m('.partitions-content', [ filteredPartitions.length === 0 ? m('div.card.bg-base-100.shadow-xl', [ m('div.card-body.text-center', [ m('h2.card-title.justify-center', 'No Partitions Found'), m('p.text-base-content.opacity-60', this.searchTerm ? 'No partitions match your search criteria.' : 'No partitions have been built yet.') ]) ]) : m('div.card.bg-base-100.shadow-xl', [ m('div.card-body', [ m('h2.card-title.mb-4', `Showing ${filteredPartitions.length} partitions`), m('div.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Partition Reference'), m('th', 'Status'), m('th', 'Last Updated'), m('th', 'Actions'), ]) ]), m('tbody', filteredPartitions.map((partition: any) => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm.break-all', { href: `/partitions/${encodePartitionRef(partition.partition_ref)}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/partitions/${encodePartitionRef(partition.partition_ref)}`); }, title: partition.partition_ref }, partition.partition_ref) ]), m('td', [ m(PartitionStatusBadge, { status: partition.status }) ]), m('td.text-sm.opacity-70', formatTime(new Date(partition.updated_at).toISOString())), m('td', [ m('button.btn.btn-sm.btn-primary', { onclick: () => this.buildPartition(partition.partition_ref) }, 'Build'), partition.build_request_id ? m('a.btn.btn-sm.btn-outline.ml-2', { href: `/builds/${partition.build_request_id}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${partition.build_request_id}`); } }, 'View Build') : null ]) ]) ) ) ]) ]) ]) ]) ]) ]); } }; export const PartitionStatus = { data: null as any | null, events: null as any | null, loading: true, error: null as string | null, partitionRef: '', buildHistory: [] as any[], async loadPartition() { try { this.loading = true; this.error = null; m.redraw(); const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index'); const apiClient = new DefaultApi(new Configuration({ basePath: '' })); // Load partition status const statusResponse = await apiClient.apiV1PartitionsRefStatusGet({ ref: this.partitionRef }); this.data = statusResponse; // Load partition events for build history const eventsResponse = await apiClient.apiV1PartitionsRefEventsGet({ ref: this.partitionRef }); this.events = eventsResponse; // Create build history from events this.buildHistory = this.extractBuildHistory(eventsResponse.events); this.loading = false; m.redraw(); } catch (error) { console.error('Failed to load partition:', error); this.error = error instanceof Error ? error.message : 'Failed to load partition'; this.loading = false; m.redraw(); } }, extractBuildHistory(events: any[]): any[] { // Group events by build request ID to create build history entries const buildRequests = new Map(); events.forEach(event => { if (event.buildRequestId) { if (!buildRequests.has(event.buildRequestId)) { buildRequests.set(event.buildRequestId, { id: event.buildRequestId, status: 'Unknown', startedAt: event.timestamp, completedAt: null, events: [] }); } const build = buildRequests.get(event.buildRequestId); build.events.push(event); // Update status based on event type if (event.eventType === 'build_request') { if (event.message?.includes('completed') || event.message?.includes('successful')) { build.status = 'Completed'; build.completedAt = event.timestamp; } else if (event.message?.includes('failed') || event.message?.includes('error')) { build.status = 'Failed'; build.completedAt = event.timestamp; } else if (event.message?.includes('executing') || event.message?.includes('running')) { build.status = 'Executing'; } else if (event.message?.includes('planning')) { build.status = 'Planning'; } } } }); // Convert to array and sort by start time (newest first) return Array.from(buildRequests.values()).sort((a, b) => b.startedAt - a.startedAt); }, async buildPartition(forceRebuild: boolean = false) { try { const { DefaultApi, Configuration } = await import('../client/typescript_generated/src/index'); const apiClient = new DefaultApi(new Configuration({ basePath: '' })); const buildRequest = { partitions: [this.partitionRef] }; const response = await apiClient.apiV1BuildsPost({ buildRequest }); // Redirect to build status page m.route.set(`/builds/${response.buildRequestId}`); } catch (error) { console.error('Failed to start build:', error); alert(`Failed to start build: ${error instanceof Error ? error.message : 'Unknown error'}`); } }, oninit(vnode: any) { this.partitionRef = decodePartitionRef(vnode.attrs.base64_ref); this.loadPartition(); }, view() { 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 partition...'), m('button.btn.btn-sm.btn-outline', { onclick: () => this.loadPartition() }, '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.loadPartition() }, 'Retry') ]) ]) ]); } if (!this.data) return m('div'); return m('div.container.mx-auto.p-4', [ // Partition Header m('.partition-header.mb-6', [ m('div.flex.justify-between.items-start.mb-4', [ m('div.flex-1', [ m('h1.text-3xl.font-bold.mb-2', 'Partition Status'), m('div.font-mono.text-lg.break-all.bg-base-200.p-3.rounded', this.partitionRef) ]), m('div.flex.flex-col.gap-2', [ m('button.btn.btn-primary', { onclick: () => this.buildPartition(false) }, 'Build Now'), m('button.btn.btn-secondary', { onclick: () => this.buildPartition(true) }, 'Force Rebuild'), ]) ]), m('div.partition-meta.flex.gap-4.items-center.mb-4', [ m(PartitionStatusBadge, { status: this.data?.status || 'Unknown', size: 'lg' }), this.data?.lastUpdated ? m('.timestamp.text-sm.opacity-70', `Last updated: ${formatDateTime(new Date(this.data.lastUpdated).toISOString())}`) : null, ]) ]), // Main Content m('.partition-content.space-y-6', [ // Build History m('.build-history.card.bg-base-100.shadow-xl', [ m('.card-body', [ m('h2.card-title.text-xl.mb-4', `Build History (${this.buildHistory?.length || 0} builds)`), !this.buildHistory || this.buildHistory.length === 0 ? m('.text-center.py-8.text-base-content.opacity-60', 'No build history available') : m('.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Build Request'), m('th', 'Status'), m('th', 'Started'), m('th', 'Completed'), m('th', 'Events'), ]) ]), m('tbody', this.buildHistory.map((build: any) => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm', { href: `/builds/${build.id}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${build.id}`); } }, build.id) ]), m('td', [ m(BuildStatusBadge, { status: build.status }) ]), m('td.text-sm.opacity-70', formatDateTime(new Date(build.startedAt).toISOString())), m('td.text-sm.opacity-70', build.completedAt ? formatDateTime(new Date(build.completedAt).toISOString()) : '—'), m('td.text-sm.opacity-70', `${build.events?.length || 0} events`) ]) ) ) ]) ]) ]) ]), // Related Build Requests this.data?.buildRequests && this.data.buildRequests.length > 0 ? m('.related-builds.card.bg-base-100.shadow-xl', [ m('.card-body', [ m('h2.card-title.text-xl.mb-4', 'Related Build Requests'), m('.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-3', this.data.buildRequests.map((buildId: string) => m('.build-card.border.border-base-300.rounded.p-3', [ m('a.link.link-primary.font-mono.text-sm', { href: `/builds/${buildId}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${buildId}`); } }, buildId) ]) ) ) ]) ]) : null, // Raw Events this.events?.events && this.events.events.length > 0 ? m('.partition-events.card.bg-base-100.shadow-xl', [ m('.card-body', [ m('h2.card-title.text-xl.mb-4', `All Events (${this.events.events.length})`), m('.overflow-x-auto', [ m('table.table.table-xs', [ m('thead', [ m('tr', [ m('th', 'Timestamp'), m('th', 'Event Type'), m('th', 'Build Request'), m('th', 'Message'), ]) ]), m('tbody', this.events.events.slice(0, 20).map((event: any) => // Show first 20 events m('tr.hover', [ m('td.text-xs.font-mono', formatDateTime(new Date(event.timestamp).toISOString())), m('td', [ m(EventTypeBadge, { eventType: event.eventType, size: 'xs' }) ]), m('td', event.buildRequestId ? m('a.link.link-primary.font-mono.text-xs', { href: `/builds/${event.buildRequestId}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${event.buildRequestId}`); } }, event.buildRequestId) : '—'), m('td.text-xs', event.message || ''), ]) ) ) ]) ]), this.events.events.length > 20 ? m('.text-center.mt-4', [ m('.text-sm.opacity-60', `Showing first 20 of ${this.events.events.length} events`) ]) : null ]) ]) : null ]) ]); } }; export const JobsList = { jobs: [] as any[], searchTerm: '', loading: false, error: null as string | null, searchTimeout: null as NodeJS.Timeout | null, oninit(vnode: any) { JobsList.loadJobs(); }, async loadJobs() { JobsList.loading = true; JobsList.error = null; try { const service = DashboardService.getInstance(); JobsList.jobs = await service.getJobs(JobsList.searchTerm || undefined); } catch (error) { console.error('Failed to load jobs:', error); JobsList.error = 'Failed to load jobs. Please try again.'; } finally { JobsList.loading = false; m.redraw(); } }, filteredJobs() { if (!JobsList.searchTerm) { return JobsList.jobs; } const search = JobsList.searchTerm.toLowerCase(); return JobsList.jobs.filter((job: any) => job.job_label.toLowerCase().includes(search) ); }, view: () => { if (JobsList.loading) { return m('div.container.mx-auto.p-4', [ m('div.flex.justify-center.items-center.h-64', [ m('div.loading.loading-spinner.loading-lg') ]) ]); } if (JobsList.error) { return m('div.container.mx-auto.p-4', [ m('div.alert.alert-error', [ m('span', JobsList.error), m('div', [ m('button.btn.btn-sm.btn-outline', { onclick: () => JobsList.loadJobs() }, 'Retry') ]) ]) ]); } return m('div.container.mx-auto.p-4', [ // Jobs Header m('.jobs-header.mb-6', [ m('h1.text-3xl.font-bold.mb-4', 'Jobs'), m('div.flex.gap-4.items-center.mb-4', [ m('input.input.input-bordered.flex-1[placeholder="Search jobs..."]', { value: JobsList.searchTerm, oninput: (e: Event) => { JobsList.searchTerm = (e.target as HTMLInputElement).value; // Debounce search if (JobsList.searchTimeout) clearTimeout(JobsList.searchTimeout); JobsList.searchTimeout = setTimeout(() => JobsList.loadJobs(), 300); } }), m('button.btn.btn-outline', { onclick: () => JobsList.loadJobs() }, 'Refresh') ]) ]), // Jobs Table JobsList.filteredJobs().length === 0 ? m('div.text-center.py-8.text-base-content.opacity-60', 'No jobs found') : m('.jobs-table.card.bg-base-100.shadow-xl', [ m('.card-body.p-0', [ m('.overflow-x-auto', [ m('table.table.table-zebra', [ m('thead', [ m('tr', [ m('th', 'Job Label'), m('th', 'Success Rate'), m('th', 'Avg Duration'), m('th', 'Recent Runs'), m('th', 'Last Run'), ]) ]), m('tbody', JobsList.filteredJobs().map((job: any) => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm', { href: `/jobs/${encodeJobLabel(job.job_label)}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/jobs/${encodeJobLabel(job.job_label)}`); } }, job.job_label) ]), m('td', [ m(`span.badge.${job.success_rate >= 0.9 ? 'badge-success' : job.success_rate >= 0.7 ? 'badge-warning' : 'badge-error'}`, `${Math.round(job.success_rate * 100)}%`) ]), m('td', formatDuration(job.avg_duration_ms)), m('td', (job.recent_runs || 0).toString()), m('td.text-sm.opacity-70', job.last_run ? formatTime(new Date(job.last_run).toISOString()) : '—'), ]) )) ]) ]) ]) ]) ]); } }; export const JobMetrics = { jobLabel: '', metrics: null as any, loading: false, error: null as string | null, oninit(vnode: any) { JobMetrics.jobLabel = decodeJobLabel(vnode.attrs.label); JobMetrics.loadJobMetrics(); }, async loadJobMetrics() { JobMetrics.loading = true; JobMetrics.error = null; try { const service = DashboardService.getInstance(); JobMetrics.metrics = await service.getJobMetrics(JobMetrics.jobLabel); if (!JobMetrics.metrics) { JobMetrics.error = 'Job not found or no metrics available'; } } catch (error) { console.error('Failed to load job metrics:', error); JobMetrics.error = 'Failed to load job metrics. Please try again.'; } finally { JobMetrics.loading = false; m.redraw(); } }, view: () => { if (JobMetrics.loading) { return m('div.container.mx-auto.p-4', [ m('div.flex.justify-center.items-center.h-64', [ m('div.loading.loading-spinner.loading-lg') ]) ]); } if (JobMetrics.error) { return m('div.container.mx-auto.p-4', [ m('div.alert.alert-error', [ m('span', JobMetrics.error), m('div', [ m('button.btn.btn-sm.btn-outline', { onclick: () => JobMetrics.loadJobMetrics() }, 'Retry') ]) ]) ]); } if (!JobMetrics.metrics) { return m('div.container.mx-auto.p-4', [ m('div.text-center.py-8.text-base-content.opacity-60', 'No metrics available') ]); } return m('div.container.mx-auto.p-4', [ // Job Header m('.job-header.mb-6', [ m('h1.text-3xl.font-bold.mb-4', [ 'Job Metrics: ', m('span.font-mono.text-2xl', JobMetrics.jobLabel) ]), m('.job-stats.grid.grid-cols-1.md:grid-cols-3.gap-4.mb-6', [ m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [ m('.stat-title', 'Success Rate'), m('.stat-value.text-3xl', [ m(`span.${JobMetrics.metrics.success_rate >= 0.9 ? 'text-success' : JobMetrics.metrics.success_rate >= 0.7 ? 'text-warning' : 'text-error'}`, `${Math.round(JobMetrics.metrics.success_rate * 100)}%`) ]), ]), m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [ m('.stat-title', 'Avg Duration'), m('.stat-value.text-3xl', formatDuration(JobMetrics.metrics.avg_duration_ms)), ]), m('.stat.bg-base-100.shadow-xl.rounded-lg.p-4', [ m('.stat-title', 'Total Runs'), m('.stat-value.text-3xl', JobMetrics.metrics.total_runs), ]), ]) ]), // Main Content m('.job-content.space-y-6', [ // Performance Trends JobMetrics.metrics.daily_stats?.length > 0 && m('.performance-trends.card.bg-base-100.shadow-xl', [ m('.card-body', [ m('h2.card-title.text-xl.mb-4', 'Performance Trends (Last 30 Days)'), m('.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Date'), m('th', 'Success Rate'), m('th', 'Avg Duration'), m('th', 'Total Runs'), ]) ]), m('tbody', JobMetrics.metrics.daily_stats.map((stat: any) => m('tr.hover', [ m('td', formatDate(stat.date)), m('td', [ m(`span.badge.${stat.success_rate >= 0.9 ? 'badge-success' : stat.success_rate >= 0.7 ? 'badge-warning' : 'badge-error'}`, `${Math.round(stat.success_rate * 100)}%`) ]), m('td', formatDuration(stat.avg_duration_ms)), m('td', stat.total_runs), ]) )) ]) ]) ]) ]), // Recent Runs m('.recent-runs.card.bg-base-100.shadow-xl', [ m('.card-body', [ m('h2.card-title.text-xl.mb-4', `Recent Runs (${JobMetrics.metrics.recent_runs?.length || 0})`), !JobMetrics.metrics.recent_runs || JobMetrics.metrics.recent_runs.length === 0 ? m('.text-center.py-8.text-base-content.opacity-60', 'No recent runs available') : m('.overflow-x-auto', [ m('table.table.table-sm', [ m('thead', [ m('tr', [ m('th', 'Build Request'), m('th', 'Partitions'), m('th', 'Status'), m('th', 'Duration'), m('th', 'Started'), ]) ]), m('tbody', JobMetrics.metrics.recent_runs.map((run: any) => m('tr.hover', [ m('td', [ m('a.link.link-primary.font-mono.text-sm', { href: `/builds/${run.build_request_id}`, onclick: (e: Event) => { e.preventDefault(); m.route.set(`/builds/${run.build_request_id}`); } }, run.build_request_id) ]), m('td.text-sm', [ m('span.font-mono', run.partitions.slice(0, 3).join(', ')), run.partitions.length > 3 && m('span.opacity-60', ` +${run.partitions.length - 3} more`) ]), m('td', [ m(`span.badge.${run.status === 'completed' ? 'badge-success' : run.status === 'failed' ? 'badge-error' : run.status === 'running' ? 'badge-warning' : 'badge-info'}`, run.status) ]), m('td', formatDuration(run.duration_ms)), m('td.text-sm.opacity-70', formatTime(new Date(run.started_at).toISOString())), ]) )) ]) ]) ]) ]) ]) ]); } }; export const GraphAnalysis = { view: () => m('div.container.mx-auto.p-4', [ m('h1.text-3xl.font-bold.mb-4', 'Graph Analysis'), m('div.card.bg-base-100.shadow-xl', [ m('div.card-body', [ m('h2.card-title', 'Interactive Build Graph'), m('p', 'Analyze partition dependencies and execution plans.'), m('div.form-control.mb-4', [ m('label.label', [ m('span.label-text', 'Partition References'), ]), m('textarea.textarea.textarea-bordered[placeholder="Enter partition references to analyze..."]'), ]), m('div.card-actions.justify-end', [ m('button.btn.btn-primary', 'Analyze Graph'), ]), ]), ]), ]) };