@littlebearapps/platform-admin-sdk 1.5.0 → 2.1.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 (157) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +197 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  6. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  7. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  8. package/templates/full/dashboard/src/components/patterns/index.ts +3 -0
  9. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  10. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  11. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  12. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  13. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  14. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  15. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  18. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  19. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  20. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  21. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  22. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  23. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  24. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  25. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  26. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  27. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  28. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  29. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  30. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  31. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  32. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  33. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  34. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  35. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  36. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  37. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  38. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  39. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  40. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  41. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  42. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  43. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  44. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  45. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  46. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  47. package/templates/shared/.github/workflows/security.yml +33 -0
  48. package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -0
  49. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  50. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  51. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  52. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  53. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  54. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  55. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  56. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  57. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  58. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  59. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  60. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  61. package/templates/shared/dashboard/src/components/ui/index.ts +6 -0
  62. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  63. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  67. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  68. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  69. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  70. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  71. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  72. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  73. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  74. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  75. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  76. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  77. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  78. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  79. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  80. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  81. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  82. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  83. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  84. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  85. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  86. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  87. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  88. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  89. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  90. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  91. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  92. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  93. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  94. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  95. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  96. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  97. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  99. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  100. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  101. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  102. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  103. package/templates/shared/docs/architecture.md +89 -0
  104. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  105. package/templates/shared/docs/troubleshooting.md +91 -0
  106. package/templates/shared/package.json.hbs +5 -0
  107. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  108. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  109. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  110. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  111. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  112. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  113. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  114. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  115. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  116. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  117. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  118. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  119. package/templates/shared/tests/unit/billing.test.ts +331 -0
  120. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  121. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  122. package/templates/shared/tests/unit/control.test.ts +226 -0
  123. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  124. package/templates/shared/tests/unit/economics.test.ts +365 -0
  125. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  126. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  127. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  128. package/templates/shared/vitest.config.ts +18 -0
  129. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  130. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  131. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  132. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  133. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  134. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  135. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  136. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  137. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  138. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  139. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  140. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  141. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  142. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  143. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  144. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  145. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  146. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  147. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  148. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  149. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  150. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  151. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  152. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  153. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  154. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  155. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  156. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  157. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Digest Statistics Cards Component
3
+ * Shows warning digest summary stats and trends
4
+ */
5
+ import { useState, useEffect } from 'react';
6
+ import type { DigestStats as DigestStatsType } from '../../lib/reports/types';
7
+
8
+ interface StatCardProps {
9
+ label: string;
10
+ value: number | string;
11
+ subValue?: string;
12
+ colour: string;
13
+ icon: string;
14
+ }
15
+
16
+ function StatCard({ label, value, subValue, colour, icon }: StatCardProps) {
17
+ return (
18
+ <div className={`bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 ${colour}`}>
19
+ <div className="flex items-center justify-between">
20
+ <div>
21
+ <p className="text-sm font-medium text-gray-600 dark:text-gray-400">{label}</p>
22
+ <p className="text-2xl font-bold mt-1">{value}</p>
23
+ {subValue && (
24
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{subValue}</p>
25
+ )}
26
+ </div>
27
+ <span className="text-2xl">{icon}</span>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ export function DigestStats() {
34
+ const [stats, setStats] = useState<DigestStatsType | null>(null);
35
+ const [loading, setLoading] = useState(true);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ useEffect(() => {
39
+ async function fetchStats() {
40
+ try {
41
+ const response = await fetch('/api/reports/digests/stats');
42
+ if (!response.ok) throw new Error('Failed to fetch stats');
43
+ const data = await response.json();
44
+ setStats(data);
45
+ } catch (e) {
46
+ setError(e instanceof Error ? e.message : 'Unknown error');
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }
51
+ fetchStats();
52
+ }, []);
53
+
54
+ if (loading) {
55
+ return (
56
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
57
+ {[...Array(6)].map((_, i) => (
58
+ <div key={i} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 animate-pulse">
59
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"></div>
60
+ <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-12"></div>
61
+ </div>
62
+ ))}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (error) {
68
+ return (
69
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
70
+ Error loading stats: {error}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ if (!stats) return null;
76
+
77
+ // Calculate trend (comparing today vs yesterday)
78
+ const todayOcc = stats.todayDigests.occurrences;
79
+ const yesterdayOcc = stats.yesterdayDigests.occurrences;
80
+ const trend = yesterdayOcc > 0
81
+ ? Math.round(((todayOcc - yesterdayOcc) / yesterdayOcc) * 100)
82
+ : todayOcc > 0 ? 100 : 0;
83
+ const trendText = trend > 0 ? `+${trend}%` : trend < 0 ? `${trend}%` : 'Same';
84
+
85
+ return (
86
+ <div className="space-y-4">
87
+ {/* Main Stats */}
88
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
89
+ <StatCard
90
+ label="Today's Warnings"
91
+ value={stats.todayDigests.occurrences}
92
+ subValue={`${stats.todayDigests.count} digest(s)`}
93
+ colour="border-l-4 border-l-yellow-500"
94
+ icon="⚠️"
95
+ />
96
+ <StatCard
97
+ label="Yesterday"
98
+ value={stats.yesterdayDigests.occurrences}
99
+ subValue={`${stats.yesterdayDigests.count} digest(s)`}
100
+ colour="border-l-4 border-l-gray-400"
101
+ icon="📅"
102
+ />
103
+ <StatCard
104
+ label="Daily Trend"
105
+ value={trendText}
106
+ subValue={trend > 0 ? 'Increase' : trend < 0 ? 'Decrease' : 'No change'}
107
+ colour={`border-l-4 ${trend > 20 ? 'border-l-red-500' : trend < -20 ? 'border-l-green-500' : 'border-l-blue-500'}`}
108
+ icon={trend > 0 ? '📈' : trend < 0 ? '📉' : '➡️'}
109
+ />
110
+ <StatCard
111
+ label="Total Digests"
112
+ value={stats.totalDigests}
113
+ colour="border-l-4 border-l-purple-500"
114
+ icon="📊"
115
+ />
116
+ <StatCard
117
+ label="Total Occurrences"
118
+ value={stats.totalOccurrences.toLocaleString()}
119
+ colour="border-l-4 border-l-indigo-500"
120
+ icon="🔢"
121
+ />
122
+ <StatCard
123
+ label="Active Scripts"
124
+ value={stats.byScript.length}
125
+ colour="border-l-4 border-l-teal-500"
126
+ icon="📜"
127
+ />
128
+ </div>
129
+
130
+ {/* Top Warnings Summary */}
131
+ {stats.topWarnings.length > 0 && (
132
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
133
+ <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Top Warning Types</h3>
134
+ <div className="space-y-2">
135
+ {stats.topWarnings.slice(0, 3).map((warning, i) => (
136
+ <div key={i} className="flex items-center justify-between text-sm">
137
+ <span className="text-gray-600 dark:text-gray-400 truncate max-w-md">
138
+ {warning.normalized_message.slice(0, 60)}
139
+ {warning.normalized_message.length > 60 && '...'}
140
+ </span>
141
+ <span className="text-gray-900 dark:text-white font-medium ml-2">
142
+ {warning.occurrences.toLocaleString()} ({warning.days_occurred}d)
143
+ </span>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ )}
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
3
+ import { EmptyState } from '../../components/ui/EmptyState';
4
+
5
+ interface UsageReport {
6
+ snapshot_date: string;
7
+ d1_reads: number;
8
+ d1_writes: number;
9
+ kv_reads: number;
10
+ kv_writes: number;
11
+ worker_requests: number;
12
+ total_cost_usd: number;
13
+ }
14
+
15
+ export function GapDetectionReport() {
16
+ const [daily, setDaily] = useState<UsageReport[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ fetch('/api/reports/usage?days=14')
21
+ .then((r) => r.json())
22
+ .then((data: { daily: UsageReport[] }) => {
23
+ setDaily(data.daily ?? []);
24
+ setLoading(false);
25
+ })
26
+ .catch(() => setLoading(false));
27
+ }, []);
28
+
29
+ if (loading) return <LoadingSkeleton lines={5} />;
30
+ if (daily.length === 0) return <EmptyState title="No usage data" description="Daily rollup data will appear after collection." />;
31
+
32
+ const gapDays = daily.filter((d) => d.d1_reads === 0 && d.d1_writes === 0 && d.worker_requests === 0);
33
+
34
+ return (
35
+ <div className="space-y-3">
36
+ <div className="flex items-center gap-3">
37
+ <span className="text-sm font-medium text-gray-900 dark:text-white">14-Day Coverage</span>
38
+ <span className={`text-xs px-2 py-0.5 rounded-full ${
39
+ gapDays.length === 0
40
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
41
+ : 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
42
+ }`}>
43
+ {daily.length - gapDays.length}/{daily.length} days
44
+ </span>
45
+ </div>
46
+
47
+ <div className="flex gap-0.5">
48
+ {daily.map((d) => {
49
+ const hasData = d.d1_reads > 0 || d.d1_writes > 0 || d.worker_requests > 0;
50
+ return (
51
+ <div
52
+ key={d.snapshot_date}
53
+ title={`${d.snapshot_date}: $${d.total_cost_usd.toFixed(2)}`}
54
+ className={`h-6 flex-1 rounded-sm ${
55
+ hasData ? 'bg-green-500 dark:bg-green-600' : 'bg-red-300 dark:bg-red-700'
56
+ }`}
57
+ />
58
+ );
59
+ })}
60
+ </div>
61
+
62
+ {gapDays.length > 0 && (
63
+ <p className="text-xs text-yellow-600 dark:text-yellow-400">
64
+ {gapDays.length} day(s) with zero data: {gapDays.map((d) => d.snapshot_date).join(', ')}
65
+ </p>
66
+ )}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Health Trends Report Component
3
+ * Shows project health score trends over time
4
+ */
5
+ import { useState, useEffect } from 'react';
6
+
7
+ interface HealthTrend {
8
+ project: string;
9
+ audit_date: string;
10
+ composite_score: number;
11
+ sdk_score: number;
12
+ observability_score: number;
13
+ cost_score: number;
14
+ security_score: number;
15
+ trend: 'improving' | 'stable' | 'declining';
16
+ score_delta: number;
17
+ }
18
+
19
+ function StatCard({ label, value, colour, subValue }: { label: string; value: number | string; colour: string; subValue?: string }) {
20
+ return (
21
+ <div className={`bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 ${colour}`}>
22
+ <p className="text-sm font-medium text-gray-600 dark:text-gray-400">{label}</p>
23
+ <p className="text-2xl font-bold mt-1">{value}</p>
24
+ {subValue && <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{subValue}</p>}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ function TrendBadge({ trend, delta }: { trend: string; delta: number }) {
30
+ const config: Record<string, { colour: string; icon: string }> = {
31
+ improving: { colour: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', icon: '↑' },
32
+ stable: { colour: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300', icon: '→' },
33
+ declining: { colour: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', icon: '↓' },
34
+ };
35
+ const { colour, icon } = config[trend] || config.stable;
36
+ return (
37
+ <span className={`px-2 py-0.5 text-xs font-semibold rounded inline-flex items-center gap-1 ${colour}`}>
38
+ {icon} {delta > 0 ? '+' : ''}{delta}
39
+ </span>
40
+ );
41
+ }
42
+
43
+ function ScoreBar({ score, max = 5, label }: { score: number; max?: number; label: string }) {
44
+ const percentage = (score / max) * 100;
45
+ const colour = percentage >= 80 ? 'bg-green-500' : percentage >= 60 ? 'bg-yellow-500' : 'bg-red-500';
46
+ return (
47
+ <div className="flex items-center gap-2">
48
+ <span className="w-24 text-xs text-gray-600 dark:text-gray-400">{label}</span>
49
+ <div className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
50
+ <div className={`h-full ${colour}`} style={{ width: `${percentage}%` }} />
51
+ </div>
52
+ <span className="w-8 text-xs text-gray-900 dark:text-white text-right">{score}/{max}</span>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function HealthTrendsReport() {
58
+ const [trends, setTrends] = useState<HealthTrend[]>([]);
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState<string | null>(null);
61
+ const [days, setDays] = useState(30);
62
+
63
+ useEffect(() => {
64
+ async function fetchData() {
65
+ try {
66
+ setLoading(true);
67
+ const response = await fetch(`/api/usage/health-trends?days=${days}`);
68
+ if (!response.ok) throw new Error('Failed to fetch health trends');
69
+ const data = await response.json();
70
+
71
+ // API returns { success, data: [{ project, trends: [{ date, compositeScore, rubricScores, trend, delta }] }] }
72
+ // Flatten nested project→trends structure into flat array
73
+ const apiData = data.data || [];
74
+ const flatTrends: HealthTrend[] = [];
75
+ for (const proj of apiData) {
76
+ for (const t of proj.trends || []) {
77
+ flatTrends.push({
78
+ project: proj.project,
79
+ audit_date: t.date,
80
+ composite_score: t.compositeScore,
81
+ sdk_score: t.rubricScores?.sdk ?? 0,
82
+ observability_score: t.rubricScores?.observability ?? 0,
83
+ cost_score: t.rubricScores?.cost ?? 0,
84
+ security_score: t.rubricScores?.security ?? 0,
85
+ trend: t.trend,
86
+ score_delta: t.delta,
87
+ });
88
+ }
89
+ }
90
+ setTrends(flatTrends);
91
+ } catch (e) {
92
+ setError(e instanceof Error ? e.message : 'Unknown error');
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ }
97
+ fetchData();
98
+ }, [days]);
99
+
100
+ if (loading) {
101
+ return (
102
+ <div className="space-y-4">
103
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
104
+ {[...Array(4)].map((_, i) => (
105
+ <div key={i} className="bg-white dark:bg-gray-800 rounded-lg border p-4 animate-pulse">
106
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"></div>
107
+ <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-12"></div>
108
+ </div>
109
+ ))}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ if (error) {
116
+ return (
117
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
118
+ Error loading health trends: {error}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ const improving = trends.filter(t => t.trend === 'improving').length;
124
+ const stable = trends.filter(t => t.trend === 'stable').length;
125
+ const declining = trends.filter(t => t.trend === 'declining').length;
126
+ const avgScore = trends.length > 0
127
+ ? Math.round(trends.reduce((sum, t) => sum + t.composite_score, 0) / trends.length)
128
+ : 0;
129
+
130
+ // Group by project for latest trends
131
+ const latestByProject = new Map<string, HealthTrend>();
132
+ trends.forEach(t => {
133
+ if (!latestByProject.has(t.project) || t.audit_date > latestByProject.get(t.project)!.audit_date) {
134
+ latestByProject.set(t.project, t);
135
+ }
136
+ });
137
+ const latestTrends = Array.from(latestByProject.values()).sort((a, b) => b.composite_score - a.composite_score);
138
+
139
+ return (
140
+ <div className="space-y-6">
141
+ {/* Filter */}
142
+ <div className="flex items-center gap-4">
143
+ <label className="text-sm text-gray-600 dark:text-gray-400">Time range:</label>
144
+ <select
145
+ value={days}
146
+ onChange={(e) => setDays(Number(e.target.value))}
147
+ className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-sm"
148
+ >
149
+ <option value={7}>Last 7 days</option>
150
+ <option value={30}>Last 30 days</option>
151
+ <option value={90}>Last 90 days</option>
152
+ </select>
153
+ </div>
154
+
155
+ {/* Stats */}
156
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
157
+ <StatCard label="Improving" value={improving} colour="border-l-4 border-l-green-500" />
158
+ <StatCard label="Stable" value={stable} colour="border-l-4 border-l-gray-400" />
159
+ <StatCard label="Declining" value={declining} colour="border-l-4 border-l-red-500" />
160
+ <StatCard label="Avg Score" value={avgScore} colour="border-l-4 border-l-purple-500" />
161
+ </div>
162
+
163
+ {/* Project Health Cards */}
164
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
165
+ {latestTrends.map((t) => (
166
+ <div key={t.project} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
167
+ <div className="flex items-center justify-between mb-3">
168
+ <h3 className="font-semibold text-gray-900 dark:text-white">{t.project}</h3>
169
+ <TrendBadge trend={t.trend} delta={t.score_delta} />
170
+ </div>
171
+ <div className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
172
+ {t.composite_score}<span className="text-sm font-normal text-gray-500">/100</span>
173
+ </div>
174
+ <div className="space-y-2">
175
+ <ScoreBar score={t.sdk_score} label="SDK" />
176
+ <ScoreBar score={t.observability_score} label="Observability" />
177
+ <ScoreBar score={t.cost_score} label="Cost" />
178
+ <ScoreBar score={t.security_score} label="Security" />
179
+ </div>
180
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-3">Last audit: {t.audit_date}</p>
181
+ </div>
182
+ ))}
183
+ </div>
184
+
185
+ {latestTrends.length === 0 && (
186
+ <div className="bg-gray-50 dark:bg-gray-800 rounded-lg border p-8 text-center text-gray-500 dark:text-gray-400">
187
+ No health trend data available. Run the platform auditor to generate health scores.
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,72 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
3
+ import { EmptyState } from '../../components/ui/EmptyState';
4
+
5
+ interface AuditReport {
6
+ id: number;
7
+ project: string;
8
+ scan_type: string;
9
+ ai_judge_score: number;
10
+ sdk_score: number;
11
+ observability_score: number;
12
+ cost_protection_score: number;
13
+ security_score: number;
14
+ scan_date: string;
15
+ }
16
+
17
+ export function SdkAuditReport() {
18
+ const [reports, setReports] = useState<AuditReport[]>([]);
19
+ const [loading, setLoading] = useState(true);
20
+
21
+ useEffect(() => {
22
+ fetch('/api/reports/audit?limit=20')
23
+ .then((r) => r.json())
24
+ .then((data: { reports: AuditReport[] }) => {
25
+ setReports(data.reports ?? []);
26
+ setLoading(false);
27
+ })
28
+ .catch(() => setLoading(false));
29
+ }, []);
30
+
31
+ if (loading) return <LoadingSkeleton lines={5} />;
32
+ if (reports.length === 0) return <EmptyState title="No audit reports" description="Run the platform auditor to generate reports." />;
33
+
34
+ const scoreColour = (score: number): string => {
35
+ if (score >= 90) return 'text-green-600 dark:text-green-400';
36
+ if (score >= 70) return 'text-yellow-600 dark:text-yellow-400';
37
+ return 'text-red-600 dark:text-red-400';
38
+ };
39
+
40
+ return (
41
+ <div className="overflow-x-auto">
42
+ <table className="w-full text-sm">
43
+ <thead>
44
+ <tr className="border-b border-gray-200 dark:border-gray-700">
45
+ <th className="text-left py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Project</th>
46
+ <th className="text-left py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Type</th>
47
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Overall</th>
48
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">SDK</th>
49
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Obs</th>
50
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Cost</th>
51
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Sec</th>
52
+ <th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Date</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ {reports.map((r) => (
57
+ <tr key={r.id} className="border-b border-gray-100 dark:border-gray-800">
58
+ <td className="py-1.5 px-2 text-gray-900 dark:text-white">{r.project}</td>
59
+ <td className="py-1.5 px-2 text-gray-500 dark:text-gray-400">{r.scan_type}</td>
60
+ <td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.ai_judge_score)}`}>{r.ai_judge_score}</td>
61
+ <td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.sdk_score)}`}>{r.sdk_score}</td>
62
+ <td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.observability_score)}`}>{r.observability_score}</td>
63
+ <td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.cost_protection_score)}`}>{r.cost_protection_score}</td>
64
+ <td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.security_score)}`}>{r.security_score}</td>
65
+ <td className="py-1.5 px-2 text-right text-gray-400 dark:text-gray-500">{new Date(r.scan_date).toLocaleDateString()}</td>
66
+ </tr>
67
+ ))}
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,2 @@
1
+ export { SdkAuditReport } from './SdkAuditReport';
2
+ export { GapDetectionReport } from './GapDetectionReport';