@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +2 -5
  2. package/dist/check-upgrade.d.ts +29 -0
  3. package/dist/check-upgrade.js +97 -0
  4. package/dist/index.js +59 -4
  5. package/dist/manifest.d.ts +2 -0
  6. package/dist/scaffold.js +5 -1
  7. package/dist/templates.d.ts +6 -1
  8. package/dist/templates.js +141 -3
  9. package/dist/upgrade.d.ts +1 -0
  10. package/dist/upgrade.js +21 -2
  11. package/package.json +1 -1
  12. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  13. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  14. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  15. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  16. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  17. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  18. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  19. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  20. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  21. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  22. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  23. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  24. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  25. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  26. package/templates/full/dashboard/src/pages/map.astro +561 -0
  27. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  28. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  29. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  30. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  31. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  32. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  33. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  34. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  35. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  36. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  37. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  38. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  39. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  40. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  41. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  42. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  43. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  44. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  45. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  46. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  47. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  48. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  49. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  50. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  51. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  52. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  53. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  54. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  55. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  56. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  57. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  58. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  59. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  60. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  61. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  62. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  63. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  64. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  65. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  66. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  67. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  68. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  69. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  70. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  71. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  72. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  73. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  74. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  75. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  76. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  77. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  78. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  79. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  80. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  81. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  82. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  83. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  84. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  85. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  86. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  87. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  88. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  89. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  90. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  91. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  92. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  93. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  94. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  95. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  96. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  97. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  98. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  99. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  100. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  101. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  102. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  103. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  104. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  105. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  107. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  108. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  109. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  110. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  111. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  112. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  113. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  114. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  115. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  116. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  117. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  118. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  119. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  120. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  121. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  122. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,431 @@
1
+ ---
2
+ import DashboardLayout from '../layouts/DashboardLayout.astro';
3
+
4
+ interface TestRun {
5
+ id: string;
6
+ project: string;
7
+ timestamp: string;
8
+ duration_ms: number;
9
+ total_tests: number;
10
+ passed_tests: number;
11
+ failed_tests: number;
12
+ skipped_tests: number;
13
+ pass_rate: number;
14
+ branch: string | null;
15
+ commit_sha: string | null;
16
+ commit_message: string | null;
17
+ environment: string;
18
+ trigger: string;
19
+ kv_report_key: string | null;
20
+ }
21
+
22
+ interface FlakyTest {
23
+ test_name: string;
24
+ failure_count: number;
25
+ total_count: number;
26
+ failure_rate: number;
27
+ last_failed_at: string | null;
28
+ }
29
+
30
+ interface Anomaly {
31
+ run_id: string;
32
+ project: string;
33
+ timestamp: string;
34
+ pass_rate: number;
35
+ baseline_pass_rate: number;
36
+ drop_percentage: number;
37
+ }
38
+
39
+ const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
40
+ const db = runtime?.env?.PLATFORM_DB;
41
+ const kv = runtime?.env?.PLATFORM_CACHE;
42
+
43
+ let recentRuns: TestRun[] = [];
44
+ let flakyTests: FlakyTest[] = [];
45
+ let anomalies: Anomaly[] = [];
46
+ let totalRuns = 0;
47
+ let avgPassRate = 0;
48
+ let totalTests = 0;
49
+ let flakyTestCount = 0;
50
+
51
+ if (db && kv) {
52
+ // Get recent test runs
53
+ const cachedRuns = await kv.get('tests:recent:runs');
54
+ if (cachedRuns) {
55
+ recentRuns = JSON.parse(cachedRuns);
56
+ } else {
57
+ const result = await db
58
+ .prepare(
59
+ `SELECT
60
+ id, project, timestamp, duration_ms, total_tests, passed_tests,
61
+ failed_tests, skipped_tests, pass_rate, branch, commit_sha,
62
+ commit_message, environment, trigger, kv_report_key
63
+ FROM runs
64
+ ORDER BY timestamp DESC
65
+ LIMIT 20`
66
+ )
67
+ .all<TestRun>();
68
+
69
+ recentRuns = result.results ?? [];
70
+
71
+ // Cache for 5 minutes
72
+ await kv.put('tests:recent:runs', JSON.stringify(recentRuns), {
73
+ expirationTtl: 300,
74
+ });
75
+ }
76
+
77
+ // Get flaky tests (failure rate > 20%)
78
+ const cachedFlaky = await kv.get('tests:flaky:all');
79
+ if (cachedFlaky) {
80
+ flakyTests = JSON.parse(cachedFlaky);
81
+ } else {
82
+ const result = await db
83
+ .prepare(
84
+ `SELECT
85
+ test_name,
86
+ failure_count,
87
+ total_count,
88
+ failure_rate,
89
+ last_failed_at
90
+ FROM test_flakiness
91
+ WHERE is_flaky = 1
92
+ ORDER BY failure_rate DESC
93
+ LIMIT 20`
94
+ )
95
+ .all<FlakyTest>();
96
+
97
+ flakyTests = result.results ?? [];
98
+
99
+ await kv.put('tests:flaky:all', JSON.stringify(flakyTests), {
100
+ expirationTtl: 300,
101
+ });
102
+ }
103
+
104
+ // Calculate anomalies (runs with >10% drop from 7-day baseline)
105
+ // For each recent run, compare to baseline
106
+ const anomaliesTemp: Anomaly[] = [];
107
+
108
+ for (const run of recentRuns.slice(0, 10)) {
109
+ const sevenDaysAgo = new Date(run.timestamp);
110
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
111
+
112
+ const baseline = await db
113
+ .prepare(
114
+ `SELECT AVG(pass_rate) as avg_pass_rate
115
+ FROM runs
116
+ WHERE project = ?
117
+ AND timestamp >= ?
118
+ AND timestamp < ?
119
+ AND id != ?`
120
+ )
121
+ .bind(run.project, sevenDaysAgo.toISOString(), run.timestamp, run.id)
122
+ .first<{ avg_pass_rate: number | null }>();
123
+
124
+ if (baseline && baseline.avg_pass_rate) {
125
+ const dropPercentage =
126
+ ((baseline.avg_pass_rate - run.pass_rate) / baseline.avg_pass_rate) * 100;
127
+
128
+ if (dropPercentage > 10) {
129
+ anomaliesTemp.push({
130
+ run_id: run.id,
131
+ project: run.project,
132
+ timestamp: run.timestamp,
133
+ pass_rate: run.pass_rate,
134
+ baseline_pass_rate: baseline.avg_pass_rate,
135
+ drop_percentage: dropPercentage,
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ anomalies = anomaliesTemp;
142
+
143
+ // Get summary statistics
144
+ const stats = await db
145
+ .prepare(
146
+ `SELECT
147
+ COUNT(*) as total_runs,
148
+ AVG(pass_rate) as avg_pass_rate,
149
+ SUM(total_tests) as total_tests
150
+ FROM runs`
151
+ )
152
+ .first<{ total_runs: number; avg_pass_rate: number; total_tests: number }>();
153
+
154
+ totalRuns = stats?.total_runs ?? 0;
155
+ avgPassRate = stats?.avg_pass_rate ?? 0;
156
+ totalTests = stats?.total_tests ?? 0;
157
+
158
+ flakyTestCount = flakyTests.length;
159
+ }
160
+
161
+ function formatDate(dateString: string): string {
162
+ const date = new Date(dateString);
163
+ const now = new Date();
164
+ const diffMs = now.getTime() - date.getTime();
165
+ const diffMins = Math.floor(diffMs / 60000);
166
+ const diffHours = Math.floor(diffMs / 3600000);
167
+ const diffDays = Math.floor(diffMs / 86400000);
168
+
169
+ if (diffMins < 60) return `${diffMins}m ago`;
170
+ if (diffHours < 24) return `${diffHours}h ago`;
171
+ if (diffDays < 7) return `${diffDays}d ago`;
172
+ return date.toLocaleDateString();
173
+ }
174
+
175
+ function formatDuration(ms: number): string {
176
+ if (ms < 1000) return `${ms}ms`;
177
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
178
+ return `${(ms / 60000).toFixed(1)}m`;
179
+ }
180
+
181
+ function getPassRateBadgeColor(passRate: number): string {
182
+ if (passRate >= 0.95) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
183
+ if (passRate >= 0.8)
184
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
185
+ if (passRate >= 0.6)
186
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300';
187
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
188
+ }
189
+
190
+ function getEnvironmentBadgeColor(environment: string): string {
191
+ if (environment === 'ci') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
192
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
193
+ }
194
+ ---
195
+
196
+ <DashboardLayout title="Tests">
197
+ <div class="space-y-6">
198
+ <h2 class="text-3xl font-bold text-gray-900 dark:text-white">Test Results</h2>
199
+
200
+ <!-- Summary Cards -->
201
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-4">
202
+ <div class="metric-card">
203
+ <div class="metric-title">Total Runs</div>
204
+ <div class="metric-value">{totalRuns}</div>
205
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">All time</div>
206
+ </div>
207
+
208
+ <div class="metric-card">
209
+ <div class="metric-title">Average Pass Rate</div>
210
+ <div class="metric-value">{(avgPassRate * 100).toFixed(1)}%</div>
211
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Across all runs</div>
212
+ </div>
213
+
214
+ <div class="metric-card">
215
+ <div class="metric-title">Flaky Tests</div>
216
+ <div class="metric-value">{flakyTestCount}</div>
217
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">&gt;20% failure rate</div>
218
+ </div>
219
+
220
+ <div class="metric-card">
221
+ <div class="metric-title">Anomalies</div>
222
+ <div class="metric-value">{anomalies.length}</div>
223
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Recent drops</div>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Anomaly Alerts (if any) -->
228
+ {
229
+ anomalies.length > 0 && (
230
+ <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
231
+ <h3 class="text-xl font-semibold text-red-900 dark:text-red-300 mb-4 flex items-center gap-2">
232
+ <span>🚨</span>
233
+ <span>Anomalies Detected ({anomalies.length})</span>
234
+ </h3>
235
+
236
+ <div class="space-y-3">
237
+ {anomalies.map((anomaly) => (
238
+ <div class="bg-white dark:bg-gray-800 border border-red-300 dark:border-red-700 rounded-lg p-4">
239
+ <div class="flex items-start justify-between">
240
+ <div class="flex-1">
241
+ <div class="flex items-center gap-2 mb-2">
242
+ <span class="font-mono text-sm text-gray-900 dark:text-white">
243
+ {anomaly.project}
244
+ </span>
245
+ <span class="px-2 py-1 rounded text-xs font-bold bg-red-500 text-white">
246
+ -{anomaly.drop_percentage.toFixed(1)}%
247
+ </span>
248
+ </div>
249
+
250
+ <div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
251
+ <div>
252
+ Current:{' '}
253
+ <span class="font-semibold">{(anomaly.pass_rate * 100).toFixed(1)}%</span> |
254
+ Baseline:{' '}
255
+ <span class="font-semibold">
256
+ {(anomaly.baseline_pass_rate * 100).toFixed(1)}%
257
+ </span>
258
+ </div>
259
+ <div>{formatDate(anomaly.timestamp)}</div>
260
+ </div>
261
+
262
+ {recentRuns.find((r) => r.id === anomaly.run_id)?.kv_report_key && (
263
+ <div class="mt-2">
264
+ <a
265
+ href={`/api/test-reports/${anomaly.run_id}`}
266
+ target="_blank"
267
+ rel="noopener noreferrer"
268
+ class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
269
+ >
270
+ View HTML Report →
271
+ </a>
272
+ </div>
273
+ )}
274
+ </div>
275
+ </div>
276
+ </div>
277
+ ))}
278
+ </div>
279
+ </div>
280
+ )
281
+ }
282
+
283
+ <!-- Flaky Tests -->
284
+ {
285
+ flakyTests.length > 0 && (
286
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
287
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
288
+ Flaky Tests ({flakyTests.length})
289
+ </h3>
290
+
291
+ <div class="space-y-3">
292
+ {flakyTests.map((test) => (
293
+ <div class="border border-yellow-200 dark:border-yellow-700 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
294
+ <div class="flex items-start justify-between">
295
+ <div class="flex-1">
296
+ <div class="flex items-center gap-2 mb-2">
297
+ <span class="px-2 py-1 rounded text-xs font-bold bg-yellow-500 text-white">
298
+ {(test.failure_rate * 100).toFixed(1)}% failure rate
299
+ </span>
300
+ </div>
301
+
302
+ <div class="text-sm font-mono text-gray-900 dark:text-white mb-2">
303
+ {test.test_name}
304
+ </div>
305
+
306
+ <div class="text-xs text-gray-600 dark:text-gray-400">
307
+ <span>{test.failure_count} failures</span>
308
+ <span class="mx-2">•</span>
309
+ <span>{test.total_count} total runs</span>
310
+ {test.last_failed_at && (
311
+ <>
312
+ <span class="mx-2">•</span>
313
+ <span>Last failed: {formatDate(test.last_failed_at)}</span>
314
+ </>
315
+ )}
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ ))}
321
+ </div>
322
+ </div>
323
+ )
324
+ }
325
+
326
+ <!-- Recent Test Runs -->
327
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
328
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Recent Test Runs</h3>
329
+
330
+ {
331
+ recentRuns.length === 0 ? (
332
+ <p class="text-gray-600 dark:text-gray-400">No test runs yet.</p>
333
+ ) : (
334
+ <div class="overflow-x-auto">
335
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
336
+ <thead class="bg-gray-50 dark:bg-gray-900">
337
+ <tr>
338
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
339
+ Project
340
+ </th>
341
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
342
+ Time
343
+ </th>
344
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
345
+ Pass Rate
346
+ </th>
347
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
348
+ Tests
349
+ </th>
350
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
351
+ Duration
352
+ </th>
353
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
354
+ Branch
355
+ </th>
356
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
357
+ Env
358
+ </th>
359
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
360
+ Report
361
+ </th>
362
+ </tr>
363
+ </thead>
364
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
365
+ {recentRuns.map((run) => (
366
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
367
+ <td class="px-4 py-3 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
368
+ {run.project}
369
+ </td>
370
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
371
+ {formatDate(run.timestamp)}
372
+ </td>
373
+ <td class="px-4 py-3 whitespace-nowrap">
374
+ <span
375
+ class={`px-2 py-1 rounded text-xs font-medium ${getPassRateBadgeColor(run.pass_rate)}`}
376
+ >
377
+ {(run.pass_rate * 100).toFixed(1)}%
378
+ </span>
379
+ </td>
380
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
381
+ <span class="text-green-600 dark:text-green-400">{run.passed_tests}</span>
382
+ <span class="mx-1">/</span>
383
+ <span>{run.total_tests}</span>
384
+ {run.failed_tests > 0 && (
385
+ <span class="ml-2 text-red-600 dark:text-red-400">
386
+ ({run.failed_tests} failed)
387
+ </span>
388
+ )}
389
+ </td>
390
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
391
+ {formatDuration(run.duration_ms)}
392
+ </td>
393
+ <td class="px-4 py-3 whitespace-nowrap text-sm font-mono text-gray-600 dark:text-gray-400">
394
+ {run.branch || '-'}
395
+ {run.commit_sha && (
396
+ <span class="ml-1 text-xs text-gray-500">
397
+ ({run.commit_sha.substring(0, 7)})
398
+ </span>
399
+ )}
400
+ </td>
401
+ <td class="px-4 py-3 whitespace-nowrap">
402
+ <span
403
+ class={`px-2 py-1 rounded text-xs font-medium ${getEnvironmentBadgeColor(run.environment)}`}
404
+ >
405
+ {run.environment}
406
+ </span>
407
+ </td>
408
+ <td class="px-4 py-3 whitespace-nowrap text-sm">
409
+ {run.kv_report_key ? (
410
+ <a
411
+ href={`/api/test-reports/${run.id}`}
412
+ target="_blank"
413
+ rel="noopener noreferrer"
414
+ class="text-blue-600 dark:text-blue-400 hover:underline"
415
+ >
416
+ View
417
+ </a>
418
+ ) : (
419
+ <span class="text-gray-400 dark:text-gray-600">-</span>
420
+ )}
421
+ </td>
422
+ </tr>
423
+ ))}
424
+ </tbody>
425
+ </table>
426
+ </div>
427
+ )
428
+ }
429
+ </div>
430
+ </div>
431
+ </DashboardLayout>