@littlebearapps/platform-admin-sdk 1.4.2 → 2.0.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 (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,60 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ interface Stats {
4
+ pending: number;
5
+ shadow: number;
6
+ approved: number;
7
+ rejected: number;
8
+ }
9
+
10
+ export function PatternStats() {
11
+ const [stats, setStats] = useState<Stats | null>(null);
12
+
13
+ useEffect(() => {
14
+ Promise.all([
15
+ fetch('/api/patterns?status=pending').then(r => r.json()),
16
+ fetch('/api/patterns?status=shadow').then(r => r.json()),
17
+ fetch('/api/patterns?status=approved').then(r => r.json()),
18
+ ])
19
+ .then(([pending, shadow, approved]) => {
20
+ setStats({
21
+ pending: (pending as { suggestions: unknown[] }).suggestions?.length ?? 0,
22
+ shadow: (shadow as { suggestions: unknown[] }).suggestions?.length ?? 0,
23
+ approved: (approved as { suggestions: unknown[] }).suggestions?.length ?? 0,
24
+ rejected: 0,
25
+ });
26
+ })
27
+ .catch(() => {});
28
+ }, []);
29
+
30
+ if (!stats) {
31
+ return (
32
+ <div className="grid grid-cols-4 gap-4">
33
+ {Array.from({ length: 4 }).map((_, i) => (
34
+ <div key={i} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 animate-pulse">
35
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-12 mb-2" />
36
+ <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-8" />
37
+ </div>
38
+ ))}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ const items = [
44
+ { label: 'Pending', value: stats.pending, colour: 'text-yellow-600 dark:text-yellow-400' },
45
+ { label: 'Shadow', value: stats.shadow, colour: 'text-purple-600 dark:text-purple-400' },
46
+ { label: 'Approved', value: stats.approved, colour: 'text-green-600 dark:text-green-400' },
47
+ { label: 'Rejected', value: stats.rejected, colour: 'text-red-600 dark:text-red-400' },
48
+ ];
49
+
50
+ return (
51
+ <div className="grid grid-cols-4 gap-4">
52
+ {items.map(item => (
53
+ <div key={item.label} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
54
+ <div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">{item.label}</div>
55
+ <div className={`text-2xl font-bold mt-1 ${item.colour}`}>{item.value}</div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,116 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { EmptyState } from '../../components/ui/EmptyState';
3
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
4
+
5
+ type Tab = 'pending' | 'shadow' | 'approved' | 'rejected';
6
+
7
+ interface Pattern {
8
+ id: number;
9
+ name: string;
10
+ description: string;
11
+ match_rule: string;
12
+ status: string;
13
+ source: string;
14
+ created_at: string;
15
+ match_count?: number;
16
+ }
17
+
18
+ export function PatternTabs() {
19
+ const [activeTab, setActiveTab] = useState<Tab>('pending');
20
+ const [patterns, setPatterns] = useState<Pattern[]>([]);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ useEffect(() => {
24
+ setLoading(true);
25
+ fetch(`/api/patterns?status=${activeTab}`)
26
+ .then(res => res.json())
27
+ .then((data: { patterns: Pattern[] }) => { setPatterns(data.patterns ?? []); setLoading(false); })
28
+ .catch(() => setLoading(false));
29
+ }, [activeTab]);
30
+
31
+ const tabs: { id: Tab; label: string }[] = [
32
+ { id: 'pending', label: 'Pending' },
33
+ { id: 'shadow', label: 'Shadow' },
34
+ { id: 'approved', label: 'Approved' },
35
+ { id: 'rejected', label: 'Rejected' },
36
+ ];
37
+
38
+ const handleAction = async (id: number, action: 'approve' | 'reject') => {
39
+ const endpoint = action === 'approve' ? '/api/patterns/approve' : '/api/patterns/reject';
40
+ try {
41
+ const res = await fetch(endpoint, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ id }),
45
+ });
46
+ if (res.ok) {
47
+ setPatterns(prev => prev.filter(p => p.id !== id));
48
+ }
49
+ } catch {
50
+ // Silent failure — user can retry
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div>
56
+ <div className="border-b border-gray-200 dark:border-gray-700 mb-4">
57
+ <nav className="flex gap-4">
58
+ {tabs.map(tab => (
59
+ <button
60
+ key={tab.id}
61
+ onClick={() => setActiveTab(tab.id)}
62
+ className={`py-2 px-1 text-sm font-medium border-b-2 transition-colors ${
63
+ activeTab === tab.id
64
+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
65
+ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
66
+ }`}
67
+ >
68
+ {tab.label}
69
+ </button>
70
+ ))}
71
+ </nav>
72
+ </div>
73
+
74
+ {loading ? (
75
+ <LoadingSkeleton lines={4} />
76
+ ) : patterns.length === 0 ? (
77
+ <EmptyState title={`No ${activeTab} patterns`} description="Patterns will appear here as they are discovered." />
78
+ ) : (
79
+ <div className="space-y-3">
80
+ {patterns.map(p => (
81
+ <div key={p.id} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
82
+ <div className="flex items-start justify-between gap-3">
83
+ <div className="min-w-0">
84
+ <p className="text-sm font-medium text-gray-900 dark:text-white">{p.name}</p>
85
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{p.description}</p>
86
+ <p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-mono truncate">{p.match_rule}</p>
87
+ <div className="flex items-center gap-3 mt-2 text-xs text-gray-500 dark:text-gray-400">
88
+ <span>Source: {p.source}</span>
89
+ {p.match_count !== undefined && <span>Matches: {p.match_count}</span>}
90
+ <span>{new Date(p.created_at).toLocaleDateString()}</span>
91
+ </div>
92
+ </div>
93
+ {(activeTab === 'pending' || activeTab === 'shadow') && (
94
+ <div className="flex gap-2 shrink-0">
95
+ <button
96
+ onClick={() => handleAction(p.id, 'approve')}
97
+ className="text-xs px-3 py-1 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-900/50"
98
+ >
99
+ Approve
100
+ </button>
101
+ <button
102
+ onClick={() => handleAction(p.id, 'reject')}
103
+ className="text-xs px-3 py-1 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50"
104
+ >
105
+ Reject
106
+ </button>
107
+ </div>
108
+ )}
109
+ </div>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,115 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ interface PatternSuggestion {
4
+ id: number;
5
+ pattern_type: string;
6
+ pattern_value: string;
7
+ error_type: string;
8
+ priority: string;
9
+ status: string;
10
+ match_count: number;
11
+ source: string;
12
+ created_at: string;
13
+ reviewed_at: string | null;
14
+ reviewer_notes: string | null;
15
+ }
16
+
17
+ export function SuggestionsQueue() {
18
+ const [suggestions, setSuggestions] = useState<PatternSuggestion[]>([]);
19
+ const [filter, setFilter] = useState<string>('pending');
20
+ const [actioningId, setActioningId] = useState<number | null>(null);
21
+
22
+ const loadSuggestions = useCallback(() => {
23
+ fetch(`/api/patterns?status=${filter}`)
24
+ .then(res => res.json())
25
+ .then((data: { suggestions: PatternSuggestion[] }) => setSuggestions(data.suggestions ?? []))
26
+ .catch(() => {});
27
+ }, [filter]);
28
+
29
+ useEffect(() => { loadSuggestions(); }, [loadSuggestions]);
30
+
31
+ async function handleAction(id: number, action: 'approve' | 'reject') {
32
+ setActioningId(id);
33
+ try {
34
+ const res = await fetch(`/api/patterns/${action}`, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({ id }),
38
+ });
39
+ if (res.ok) {
40
+ setSuggestions(prev => prev.filter(s => s.id !== id));
41
+ }
42
+ } catch {
43
+ // Silently fail
44
+ }
45
+ setActioningId(null);
46
+ }
47
+
48
+ return (
49
+ <div className="space-y-4">
50
+ <div className="flex gap-2">
51
+ {['pending', 'shadow', 'approved', 'rejected'].map(s => (
52
+ <button
53
+ key={s}
54
+ onClick={() => setFilter(s)}
55
+ className={`text-sm px-3 py-1 rounded-full transition-colors ${
56
+ filter === s
57
+ ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
58
+ : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
59
+ }`}
60
+ >
61
+ {s.charAt(0).toUpperCase() + s.slice(1)}
62
+ </button>
63
+ ))}
64
+ </div>
65
+
66
+ {suggestions.length === 0 ? (
67
+ <div className="text-center py-8 text-gray-500 dark:text-gray-400">
68
+ No {filter} patterns.
69
+ </div>
70
+ ) : (
71
+ <div className="space-y-3">
72
+ {suggestions.map(s => (
73
+ <div key={s.id} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
74
+ <div className="flex items-start justify-between gap-4">
75
+ <div className="flex-1 min-w-0">
76
+ <div className="flex items-center gap-2 mb-1">
77
+ <span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-mono">
78
+ {s.pattern_type}
79
+ </span>
80
+ <span className="text-xs text-gray-500 dark:text-gray-400">
81
+ {s.source} · {s.match_count} matches
82
+ </span>
83
+ </div>
84
+ <p className="text-sm text-gray-900 dark:text-white font-mono truncate">{s.pattern_value}</p>
85
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
86
+ {s.error_type} · {s.priority} · {new Date(s.created_at).toLocaleDateString('en-AU')}
87
+ </p>
88
+ </div>
89
+
90
+ {filter === 'pending' && (
91
+ <div className="flex gap-2 flex-shrink-0">
92
+ <button
93
+ onClick={() => handleAction(s.id, 'approve')}
94
+ disabled={actioningId === s.id}
95
+ className="text-xs px-3 py-1.5 rounded bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/60 disabled:opacity-50"
96
+ >
97
+ Approve
98
+ </button>
99
+ <button
100
+ onClick={() => handleAction(s.id, 'reject')}
101
+ disabled={actioningId === s.id}
102
+ className="text-xs px-3 py-1.5 rounded bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60 disabled:opacity-50"
103
+ >
104
+ Reject
105
+ </button>
106
+ </div>
107
+ )}
108
+ </div>
109
+ </div>
110
+ ))}
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,52 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
3
+
4
+ interface PatternStats {
5
+ byStatus: Record<string, number>;
6
+ total: number;
7
+ }
8
+
9
+ export function SystemPatterns() {
10
+ const [stats, setStats] = useState<PatternStats | null>(null);
11
+ const [loading, setLoading] = useState(true);
12
+
13
+ useEffect(() => {
14
+ fetch('/api/patterns/stats')
15
+ .then((r) => r.json())
16
+ .then((data: PatternStats) => {
17
+ setStats(data);
18
+ setLoading(false);
19
+ })
20
+ .catch(() => setLoading(false));
21
+ }, []);
22
+
23
+ if (loading) return <LoadingSkeleton lines={2} />;
24
+ if (!stats) return null;
25
+
26
+ const statusLabels: Record<string, { label: string; colour: string }> = {
27
+ approved: { label: 'Approved', colour: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' },
28
+ pending: { label: 'Pending', colour: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' },
29
+ shadow: { label: 'Shadow', colour: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
30
+ rejected: { label: 'Rejected', colour: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' },
31
+ stale: { label: 'Stale', colour: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' },
32
+ };
33
+
34
+ return (
35
+ <div>
36
+ <div className="flex items-center gap-2 mb-3">
37
+ <span className="text-sm font-medium text-gray-900 dark:text-white">Pattern Overview</span>
38
+ <span className="text-xs text-gray-500 dark:text-gray-400">({stats.total} total)</span>
39
+ </div>
40
+ <div className="flex flex-wrap gap-2">
41
+ {Object.entries(stats.byStatus).map(([status, count]) => {
42
+ const meta = statusLabels[status] ?? { label: status, colour: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' };
43
+ return (
44
+ <span key={status} className={`text-xs px-2.5 py-1 rounded-full font-medium ${meta.colour}`}>
45
+ {meta.label}: {count}
46
+ </span>
47
+ );
48
+ })}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,5 @@
1
+ export { SuggestionsQueue } from './SuggestionsQueue';
2
+ export { PatternStats } from './PatternStats';
3
+ export { PatternTabs } from './PatternTabs';
4
+ export { ActivePatterns } from './ActivePatterns';
5
+ export { SystemPatterns } from './SystemPatterns';
@@ -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,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';
@@ -0,0 +1,108 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ interface SearchResult {
4
+ type: string;
5
+ id: string;
6
+ title: string;
7
+ subtitle: string;
8
+ }
9
+
10
+ export function SearchModal() {
11
+ const [open, setOpen] = useState(false);
12
+ const [query, setQuery] = useState('');
13
+ const [results, setResults] = useState<SearchResult[]>([]);
14
+ const [loading, setLoading] = useState(false);
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+ const debounceRef = useRef<ReturnType<typeof setTimeout>>();
17
+
18
+ // Cmd+K / Ctrl+K listener
19
+ useEffect(() => {
20
+ function handleKeyDown(e: KeyboardEvent) {
21
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
22
+ e.preventDefault();
23
+ setOpen(prev => !prev);
24
+ }
25
+ if (e.key === 'Escape') {
26
+ setOpen(false);
27
+ }
28
+ }
29
+ document.addEventListener('keydown', handleKeyDown);
30
+ return () => document.removeEventListener('keydown', handleKeyDown);
31
+ }, []);
32
+
33
+ useEffect(() => {
34
+ if (open && inputRef.current) {
35
+ inputRef.current.focus();
36
+ }
37
+ }, [open]);
38
+
39
+ const search = useCallback((q: string) => {
40
+ if (q.length < 2) {
41
+ setResults([]);
42
+ return;
43
+ }
44
+ setLoading(true);
45
+ fetch(`/api/search?q=${encodeURIComponent(q)}`)
46
+ .then(res => res.json())
47
+ .then((data: { results: SearchResult[] }) => {
48
+ setResults(data.results ?? []);
49
+ setLoading(false);
50
+ })
51
+ .catch(() => setLoading(false));
52
+ }, []);
53
+
54
+ function handleInput(value: string) {
55
+ setQuery(value);
56
+ clearTimeout(debounceRef.current);
57
+ debounceRef.current = setTimeout(() => search(value), 300);
58
+ }
59
+
60
+ if (!open) return null;
61
+
62
+ return (
63
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]" onClick={() => setOpen(false)}>
64
+ <div className="fixed inset-0 bg-black/50" />
65
+ <div
66
+ className="relative w-full max-w-lg bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700"
67
+ onClick={(e) => e.stopPropagation()}
68
+ >
69
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
70
+ <span className="text-gray-400 text-sm">Search</span>
71
+ <input
72
+ ref={inputRef}
73
+ type="text"
74
+ value={query}
75
+ onChange={(e) => handleInput(e.target.value)}
76
+ placeholder="Search errors, notifications..."
77
+ className="flex-1 bg-transparent text-gray-900 dark:text-white placeholder-gray-400 outline-none text-sm"
78
+ />
79
+ <kbd className="hidden md:inline text-xs text-gray-400 border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5">
80
+ ESC
81
+ </kbd>
82
+ </div>
83
+
84
+ <div className="max-h-80 overflow-y-auto p-2">
85
+ {loading && (
86
+ <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">Searching...</div>
87
+ )}
88
+ {!loading && results.length === 0 && query.length >= 2 && (
89
+ <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">No results found.</div>
90
+ )}
91
+ {!loading && results.map((r) => (
92
+ <a
93
+ key={`${r.type}-${r.id}`}
94
+ href={r.type === 'error' ? `/errors` : `/notifications`}
95
+ className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
96
+ >
97
+ <span className="text-xs font-mono text-gray-400 dark:text-gray-500 uppercase">{r.type}</span>
98
+ <div className="flex-1 min-w-0">
99
+ <div className="text-sm text-gray-900 dark:text-white truncate">{r.title}</div>
100
+ <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{r.subtitle}</div>
101
+ </div>
102
+ </a>
103
+ ))}
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,37 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ params, locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ const id = params.id;
14
+ if (!id) {
15
+ return new Response(JSON.stringify({ error: 'Missing notification id' }), {
16
+ status: 400,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ });
19
+ }
20
+
21
+ try {
22
+ await db
23
+ .prepare(`UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`)
24
+ .bind(id)
25
+ .run();
26
+
27
+ return new Response(
28
+ JSON.stringify({ ok: true }),
29
+ { headers: { 'Content-Type': 'application/json' } }
30
+ );
31
+ } catch {
32
+ return new Response(JSON.stringify({ error: 'Failed to mark as read' }), {
33
+ status: 500,
34
+ headers: { 'Content-Type': 'application/json' },
35
+ });
36
+ }
37
+ };