Display job execution start and end time (#5123)

added start and end times in the job execution details

<img width="350" height="241" alt="image" src="https://github.com/user-attachments/assets/39bd7a41-0d2b-48af-a61d-160f0ea1b5fe" />

fix: https://github.com/flutter/flutter/issues/190633
diff --git a/dashboard/lib/service/data_seeder.dart b/dashboard/lib/service/data_seeder.dart
index f5460a4..f2708aa 100644
--- a/dashboard/lib/service/data_seeder.dart
+++ b/dashboard/lib/service/data_seeder.dart
@@ -668,8 +668,10 @@
         .neutral =>
           '[INFO] Starting task $jobName...\n[INFO] Test neutral: Dummy Tests',
       },
-      startTime: creationTime + 30000,
-      endTime: creationTime + 60000,
+      startTime: status != TaskStatus.waitingForBackfill
+          ? creationTime + 30000
+          : null,
+      endTime: status.isComplete ? creationTime + 60000 : null,
       logAnalysis: switch (status) {
         .failed =>
           '''
diff --git a/dashboard/lib/views/presubmit_view.dart b/dashboard/lib/views/presubmit_view.dart
index 61a75b9..de3db57 100644
--- a/dashboard/lib/views/presubmit_view.dart
+++ b/dashboard/lib/views/presubmit_view.dart
@@ -12,6 +12,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:gpt_markdown/gpt_markdown.dart';
+import 'package:intl/intl.dart';
 import 'package:provider/provider.dart';
 import 'package:url_launcher/url_launcher.dart';
 
@@ -620,6 +621,7 @@
                 ],
               ),
             ),
+            _buildExecutionDates(selectedJob, isDark),
             Expanded(
               child: Container(
                 margin: const EdgeInsets.all(8.0),
@@ -796,6 +798,51 @@
         '${job.jobName} is not yet scheduled for execution.\n"View more details on LUCI UI" button will become enabled once the job is scheduled.',
     };
   }
+
+  Widget _buildExecutionDates(PresubmitJobResponse job, bool isDark) {
+    final start = job.startTime;
+    final end = job.endTime;
+    final created = job.creationTime;
+
+    final parts = <String>[];
+    if (start != null && start != 0 && end != null && end != 0) {
+      final startTime = DateTime.fromMillisecondsSinceEpoch(start);
+      final endTime = DateTime.fromMillisecondsSinceEpoch(end);
+      parts.add(formatDuration(endTime.difference(startTime)));
+    }
+    if (start != null && start != 0) {
+      parts.add('Start: ${_formatDateTime(start)}');
+    }
+    if (end != null && end != 0) {
+      parts.add('End: ${_formatDateTime(end)}');
+    }
+    if (parts.isEmpty && created != 0) {
+      parts.add('Created: ${_formatDateTime(created)}');
+    }
+
+    if (parts.isEmpty) return const SizedBox.shrink();
+
+    return Container(
+      padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 0),
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Text(
+            parts.join('    '),
+            style: TextStyle(
+              fontSize: 12,
+              color: isDark ? const Color(0xFF8B949E) : const Color(0xFF6B7280),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  String _formatDateTime(int millis) {
+    final dt = DateTime.fromMillisecondsSinceEpoch(millis);
+    return DateFormat('dd/MM/yy HH:mm:ss').format(dt);
+  }
 }
 
 class _JobsSidebar extends StatefulWidget {
@@ -1072,3 +1119,43 @@
     }
   }
 }
+
+/// Formats a [Duration] into a human-readable string.
+///
+/// If hours, minutes, or seconds are non-zero, they are included (e.g., "1h 12m 34s").
+/// Zero hours or minutes are omitted.
+/// If all are zero, falls back to milliseconds (e.g., "12ms").
+/// If milliseconds are zero, falls back to microseconds (e.g., "34µs").
+/// If even microseconds are zero, returns "Planck time".
+@visibleForTesting
+String formatDuration(Duration d) {
+  final absD = d.abs();
+  final parts = <String>[];
+  if (absD.inHours > 0) {
+    parts.add('${absD.inHours}h');
+  }
+  final minutes = absD.inMinutes % 60;
+  if (minutes > 0) {
+    parts.add('${minutes}m');
+  }
+  final seconds = absD.inSeconds % 60;
+  if (seconds > 0) {
+    parts.add('${seconds}s');
+  }
+
+  if (parts.isNotEmpty) {
+    return 'Duration: ${parts.join(' ')}';
+  }
+
+  final millis = absD.inMilliseconds;
+  if (millis > 0) {
+    return 'Duration: ${millis}ms';
+  }
+
+  final micros = absD.inMicroseconds;
+  if (micros > 0) {
+    return 'Duration: $microsµs';
+  }
+
+  return 'Duration: Planck time';
+}
diff --git a/dashboard/test/views/presubmit_view_test.dart b/dashboard/test/views/presubmit_view_test.dart
index 7b28f1b..7dbd413 100644
--- a/dashboard/test/views/presubmit_view_test.dart
+++ b/dashboard/test/views/presubmit_view_test.dart
@@ -1459,5 +1459,257 @@
       expect(find.text('Filter jobs'), findsOneWidget);
       expect(find.text('Re-run failed'), findsOneWidget);
     });
+
+    testWidgets(
+      'PreSubmitView displays job dates at top of job details content area for different attempts',
+      (WidgetTester tester) async {
+        tester.view.physicalSize = const Size(2000, 1080);
+        tester.view.devicePixelRatio = 1.0;
+        addTearDown(tester.view.resetPhysicalSize);
+        addTearDown(tester.view.resetDevicePixelRatio);
+
+        const mockSha = 'decaf_3_real_sha';
+        const guardResponse = PresubmitGuardResponse(
+          prNum: 123,
+          author: 'dash',
+          guardStatus: GuardStatus.succeeded,
+          checkRunId: 456,
+          stages: [
+            PresubmitGuardStage(
+              name: 'Engine',
+              createdAt: 0,
+              jobs: {'Mac mac_host_engine 1': TaskStatus.succeeded},
+            ),
+          ],
+        );
+
+        when(
+          mockCocoonService.fetchPresubmitGuard(
+            repo: anyNamed('repo'),
+            sha: mockSha,
+          ),
+        ).thenAnswer((_) async => const CocoonResponse.data(guardResponse));
+
+        when(
+          mockCocoonService.fetchPresubmitJobDetails(
+            checkRunId: anyNamed('checkRunId'),
+            jobName: argThat(contains('mac_host_engine'), named: 'jobName'),
+          ),
+        ).thenAnswer(
+          (_) async => CocoonResponse.data([
+            PresubmitJobResponse(
+              attemptNumber: 1,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(2026, 8, 5, 10, 5, 0).millisecondsSinceEpoch,
+              endTime: DateTime(2026, 8, 5, 11, 20, 34).millisecondsSinceEpoch,
+              status: TaskStatus.succeeded,
+              summary: 'Attempt 1 passed',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 2,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(2026, 8, 5, 10, 20).millisecondsSinceEpoch,
+              status: TaskStatus.inProgress,
+              summary: 'Attempt 2 in progress',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 3,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 30).millisecondsSinceEpoch,
+              status: TaskStatus.waitingForBackfill,
+              summary: 'Attempt 3 queued',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 4,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(2026, 8, 5, 10, 5, 0).millisecondsSinceEpoch,
+              endTime: DateTime(2026, 8, 5, 10, 5, 34).millisecondsSinceEpoch,
+              status: TaskStatus.succeeded,
+              summary: 'Attempt 4 passed',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 5,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(2026, 8, 5, 10, 5, 0).millisecondsSinceEpoch,
+              endTime: DateTime(2026, 8, 5, 11, 5, 0).millisecondsSinceEpoch,
+              status: TaskStatus.succeeded,
+              summary: 'Attempt 5 passed',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 6,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(
+                2026,
+                8,
+                5,
+                10,
+                5,
+                0,
+                0,
+              ).millisecondsSinceEpoch,
+              endTime: DateTime(
+                2026,
+                8,
+                5,
+                10,
+                5,
+                0,
+                123,
+              ).millisecondsSinceEpoch,
+              status: TaskStatus.succeeded,
+              summary: 'Attempt 6 passed',
+            ),
+            PresubmitJobResponse(
+              attemptNumber: 7,
+              jobName: 'Mac mac_host_engine 1',
+              creationTime: DateTime(2026, 8, 5, 10, 0).millisecondsSinceEpoch,
+              startTime: DateTime(
+                2026,
+                8,
+                5,
+                10,
+                5,
+                0,
+                0,
+              ).millisecondsSinceEpoch,
+              endTime: DateTime(2026, 8, 5, 10, 5, 0, 0).millisecondsSinceEpoch,
+              status: TaskStatus.succeeded,
+              summary: 'Attempt 7 passed',
+            ),
+          ]),
+        );
+
+        await tester.runAsync(() async {
+          await tester.pumpWidget(
+            createPreSubmitView({'repo': 'flutter', 'sha': mockSha}),
+          );
+          for (var i = 0; i < 50; i++) {
+            await tester.pump();
+            await Future<void>.delayed(const Duration(milliseconds: 50));
+            if (find
+                .textContaining('Execution Details')
+                .evaluate()
+                .isNotEmpty) {
+              break;
+            }
+          }
+        });
+        await tester.pumpAndSettle();
+
+        await tester.tap(find.textContaining('mac_host_engine').first);
+        await tester.pumpAndSettle();
+
+        expect(find.text('Execution Details'), findsOneWidget);
+
+        // Attempt 1: Both Start and End
+        expect(
+          find.textContaining(
+            'Duration: 1h 15m 34s    Start: 05/08/26 10:05:00    End: 05/08/26 11:20:34',
+          ),
+          findsOneWidget,
+        );
+
+        // Switch to Attempt 2: Only Start
+        await tester.tap(find.text('#2'));
+        await tester.pumpAndSettle();
+        expect(find.textContaining('Start: 05/08/26 10:20:00'), findsOneWidget);
+        expect(find.textContaining('End:'), findsNothing);
+
+        // Switch to Attempt 3: Only Creation
+        await tester.tap(find.text('#3'));
+        await tester.pumpAndSettle();
+        expect(
+          find.textContaining('Created: 05/08/26 10:30:00'),
+          findsOneWidget,
+        );
+        expect(find.textContaining('Start:'), findsNothing);
+
+        // Switch to Attempt 4: Only seconds duration
+        await tester.tap(find.text('#4'));
+        await tester.pumpAndSettle();
+        expect(
+          find.textContaining(
+            'Duration: 34s    Start: 05/08/26 10:05:00    End: 05/08/26 10:05:34',
+          ),
+          findsOneWidget,
+        );
+
+        // Switch to Attempt 5: Only hours duration
+        await tester.tap(find.text('#5'));
+        await tester.pumpAndSettle();
+        expect(
+          find.textContaining(
+            'Duration: 1h    Start: 05/08/26 10:05:00    End: 05/08/26 11:05:00',
+          ),
+          findsOneWidget,
+        );
+
+        // Switch to Attempt 6: Milliseconds duration
+        await tester.tap(find.text('#6'));
+        await tester.pumpAndSettle();
+        expect(
+          find.textContaining(
+            'Duration: 123ms    Start: 05/08/26 10:05:00    End: 05/08/26 10:05:00',
+          ),
+          findsOneWidget,
+        );
+
+        // Switch to Attempt 7: Planck time duration
+        await tester.tap(find.text('#7'));
+        await tester.pumpAndSettle();
+        expect(
+          find.textContaining(
+            'Duration: Planck time    Start: 05/08/26 10:05:00    End: 05/08/26 10:05:00',
+          ),
+          findsOneWidget,
+        );
+      },
+    );
+
+    group('formatDuration', () {
+      test('formats hours, minutes, and seconds correctly', () {
+        expect(
+          formatDuration(const Duration(hours: 1, minutes: 12, seconds: 34)),
+          'Duration: 1h 12m 34s',
+        );
+        expect(
+          formatDuration(const Duration(hours: 2, seconds: 5)),
+          'Duration: 2h 5s',
+        );
+        expect(
+          formatDuration(const Duration(minutes: 45, seconds: 12)),
+          'Duration: 45m 12s',
+        );
+        expect(formatDuration(const Duration(minutes: 30)), 'Duration: 30m');
+        expect(formatDuration(const Duration(seconds: 15)), 'Duration: 15s');
+      });
+
+      test('falls back to milliseconds when h/m/s are zero', () {
+        expect(
+          formatDuration(const Duration(milliseconds: 123)),
+          'Duration: 123ms',
+        );
+        expect(
+          formatDuration(const Duration(hours: 0, milliseconds: 456)),
+          'Duration: 456ms',
+        );
+      });
+
+      test('falls back to microseconds when ms is zero', () {
+        expect(
+          formatDuration(const Duration(microseconds: 789)),
+          'Duration: 789µs',
+        );
+      });
+
+      test('returns Planck time when all are zero', () {
+        expect(formatDuration(Duration.zero), 'Duration: Planck time');
+      });
+    });
   });
 }