@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,311 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ PLATFORM_CACHE?: KVNamespace;
6
+ USAGE_API?: Fetcher;
7
+ }
8
+
9
+ export const GET: APIRoute = async ({ locals }) => {
10
+ const env = locals.runtime?.env as Env | undefined;
11
+ const db = env?.PLATFORM_DB;
12
+ const kv = env?.PLATFORM_CACHE;
13
+
14
+ const summary = {
15
+ health: {
16
+ servicesTotal: 0,
17
+ servicesUp: 0,
18
+ servicesDown: 0,
19
+ uptimePct: 100,
20
+ lastAuditScore: null as number | null,
21
+ lastAuditDate: null as string | null,
22
+ },
23
+ errors: {
24
+ p0Count: 0,
25
+ p1Count: 0,
26
+ p2Count: 0,
27
+ p3Count: 0,
28
+ p4Count: 0,
29
+ newToday: 0,
30
+ dailyTrend: [] as number[],
31
+ topErrors: [] as Array<{
32
+ fingerprint: string;
33
+ message: string;
34
+ script_name: string;
35
+ priority: string;
36
+ occurrence_count: number;
37
+ }>,
38
+ },
39
+ costs: {
40
+ mtdSpend: 0,
41
+ dailyBurnRate: 0,
42
+ projectedMonthly: 0,
43
+ budgetPct: 0,
44
+ monthlyBudget: 100,
45
+ dailyTrend: [] as number[],
46
+ },
47
+ activity: {
48
+ notifications: [] as Array<{
49
+ id: string;
50
+ title: string;
51
+ category: string;
52
+ priority: string;
53
+ source: string;
54
+ created_at: number;
55
+ action_url: string | null;
56
+ }>,
57
+ pendingPatterns: 0,
58
+ },
59
+ alerts: {
60
+ hasP0P1: false,
61
+ trippedBreakers: 0,
62
+ warningBreakers: 0,
63
+ servicesDown: 0,
64
+ },
65
+ dataQuality: {
66
+ latestSnapshot: null as string | null,
67
+ snapshotAgeMinutes: -1,
68
+ status: 'unknown' as 'fresh' | 'stale' | 'unknown',
69
+ },
70
+ };
71
+
72
+ const promises: Promise<void>[] = [];
73
+
74
+ // 1. Service health from D1 project_registry
75
+ promises.push(
76
+ (async () => {
77
+ if (!db) return;
78
+ try {
79
+ const services = await db
80
+ .prepare(
81
+ `SELECT status FROM resource_project_mapping
82
+ WHERE resource_type = 'worker'
83
+ LIMIT 100`
84
+ )
85
+ .all<{ status: string }>();
86
+ if (services.results) {
87
+ summary.health.servicesTotal = services.results.length;
88
+ summary.health.servicesUp = services.results.filter((s) => s.status !== 'inactive').length;
89
+ summary.health.servicesDown = services.results.filter((s) => s.status === 'inactive').length;
90
+ summary.alerts.servicesDown = summary.health.servicesDown;
91
+ }
92
+ } catch {
93
+ // Table may not exist
94
+ }
95
+ })()
96
+ );
97
+
98
+ // 2. Error counts from D1
99
+ promises.push(
100
+ (async () => {
101
+ if (!db) return;
102
+ try {
103
+ const stats = await db
104
+ .prepare(
105
+ `SELECT priority, COUNT(*) as cnt
106
+ FROM error_occurrences
107
+ WHERE status = 'open'
108
+ GROUP BY priority
109
+ LIMIT 10`
110
+ )
111
+ .all<{ priority: string; cnt: number }>();
112
+ if (stats.results) {
113
+ for (const row of stats.results) {
114
+ if (row.priority === 'P0') summary.errors.p0Count = row.cnt;
115
+ else if (row.priority === 'P1') summary.errors.p1Count = row.cnt;
116
+ else if (row.priority === 'P2') summary.errors.p2Count = row.cnt;
117
+ else if (row.priority === 'P3') summary.errors.p3Count = row.cnt;
118
+ else if (row.priority === 'P4') summary.errors.p4Count = row.cnt;
119
+ }
120
+ }
121
+ summary.alerts.hasP0P1 = summary.errors.p0Count > 0 || summary.errors.p1Count > 0;
122
+ } catch {
123
+ // Table may not exist
124
+ }
125
+
126
+ // Top 3 errors
127
+ try {
128
+ const topErrors = await db
129
+ .prepare(
130
+ `SELECT fingerprint, normalized_message as message, script_name, priority, occurrence_count
131
+ FROM error_occurrences
132
+ WHERE status = 'open'
133
+ ORDER BY
134
+ CASE priority WHEN 'P0' THEN 1 WHEN 'P1' THEN 2 WHEN 'P2' THEN 3 WHEN 'P3' THEN 4 ELSE 5 END,
135
+ occurrence_count DESC
136
+ LIMIT 3`
137
+ )
138
+ .all();
139
+ summary.errors.topErrors = (topErrors.results ?? []) as typeof summary.errors.topErrors;
140
+ } catch {
141
+ // Table may not exist
142
+ }
143
+
144
+ // 7-day error trend
145
+ try {
146
+ const trend = await db
147
+ .prepare(
148
+ `SELECT DATE(first_seen_at) as day, COUNT(*) as count
149
+ FROM error_occurrences
150
+ WHERE first_seen_at >= DATE('now', '-7 days')
151
+ GROUP BY DATE(first_seen_at)
152
+ ORDER BY day ASC
153
+ LIMIT 7`
154
+ )
155
+ .all<{ day: string; count: number }>();
156
+ if (trend.results) {
157
+ const today = new Date();
158
+ const dailyCounts: number[] = [];
159
+ for (let i = 6; i >= 0; i--) {
160
+ const d = new Date(today);
161
+ d.setDate(d.getDate() - i);
162
+ const key = d.toISOString().slice(0, 10);
163
+ const match = trend.results.find((r) => r.day === key);
164
+ dailyCounts.push(match?.count ?? 0);
165
+ }
166
+ summary.errors.dailyTrend = dailyCounts;
167
+ }
168
+ } catch {
169
+ // Table may not exist
170
+ }
171
+ })()
172
+ );
173
+
174
+ // 3. Cost data from D1
175
+ promises.push(
176
+ (async () => {
177
+ if (!db) return;
178
+ try {
179
+ const now = new Date();
180
+ const billingStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
181
+
182
+ const costResult = await db
183
+ .prepare(
184
+ `SELECT SUM(total_cost_usd) as mtd_cost, COUNT(*) as days_tracked
185
+ FROM daily_usage_rollups
186
+ WHERE project = 'all' AND snapshot_date >= ?
187
+ LIMIT 1`
188
+ )
189
+ .bind(billingStart)
190
+ .first<{ mtd_cost: number; days_tracked: number }>();
191
+
192
+ if (costResult?.mtd_cost) {
193
+ summary.costs.mtdSpend = Math.round(costResult.mtd_cost * 100) / 100;
194
+ }
195
+
196
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
197
+ const daysSoFar = now.getDate();
198
+ summary.costs.dailyBurnRate = daysSoFar > 0 ? Math.round((summary.costs.mtdSpend / daysSoFar) * 100) / 100 : 0;
199
+ summary.costs.projectedMonthly = Math.round(summary.costs.dailyBurnRate * daysInMonth * 100) / 100;
200
+ summary.costs.budgetPct = summary.costs.monthlyBudget > 0
201
+ ? Math.round((summary.costs.projectedMonthly / summary.costs.monthlyBudget) * 100)
202
+ : 0;
203
+
204
+ // 7-day cost trend
205
+ const sevenDaysAgo = new Date();
206
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
207
+ const trendStart = sevenDaysAgo.toISOString().slice(0, 10);
208
+ const costTrend = await db
209
+ .prepare(
210
+ `SELECT snapshot_date as day, total_cost_usd as daily_cost
211
+ FROM daily_usage_rollups
212
+ WHERE project = 'all' AND snapshot_date >= ?
213
+ ORDER BY snapshot_date ASC
214
+ LIMIT 7`
215
+ )
216
+ .bind(trendStart)
217
+ .all<{ day: string; daily_cost: number }>();
218
+ if (costTrend.results) {
219
+ const today = new Date();
220
+ const dailyCosts: number[] = [];
221
+ for (let i = 6; i >= 0; i--) {
222
+ const d = new Date(today);
223
+ d.setDate(d.getDate() - i);
224
+ const key = d.toISOString().slice(0, 10);
225
+ const match = costTrend.results.find((r) => r.day === key);
226
+ dailyCosts.push(Math.round((match?.daily_cost ?? 0) * 100) / 100);
227
+ }
228
+ summary.costs.dailyTrend = dailyCosts;
229
+ }
230
+ } catch {
231
+ // Table may not exist
232
+ }
233
+ })()
234
+ );
235
+
236
+ // 4. Activity data
237
+ promises.push(
238
+ (async () => {
239
+ if (!db) return;
240
+ try {
241
+ const notifications = await db
242
+ .prepare(
243
+ `SELECT id, title, category, priority, source, created_at, action_url
244
+ FROM notifications
245
+ WHERE created_at >= unixepoch() - (7 * 24 * 60 * 60)
246
+ ORDER BY created_at DESC
247
+ LIMIT 5`
248
+ )
249
+ .all();
250
+ summary.activity.notifications = (notifications.results ?? []) as typeof summary.activity.notifications;
251
+ } catch {
252
+ // Table may not exist
253
+ }
254
+ })()
255
+ );
256
+
257
+ // 5. Circuit breaker status from KV
258
+ promises.push(
259
+ (async () => {
260
+ if (!kv) return;
261
+ try {
262
+ const cbKeys = await kv.list({ prefix: 'cb:' });
263
+ let tripped = 0;
264
+ let warning = 0;
265
+ for (const key of cbKeys.keys) {
266
+ const state = (await kv.get(key.name, 'json')) as { status?: string } | null;
267
+ if (state?.status === 'stopped' || state?.status === 'paused') tripped++;
268
+ else if (state?.status === 'warning') warning++;
269
+ }
270
+ summary.alerts.trippedBreakers = tripped;
271
+ summary.alerts.warningBreakers = warning;
272
+ } catch {
273
+ // KV may not be available
274
+ }
275
+ })()
276
+ );
277
+
278
+ // 6. Data quality
279
+ promises.push(
280
+ (async () => {
281
+ if (!db) return;
282
+ try {
283
+ const quality = await db
284
+ .prepare(
285
+ `SELECT MAX(snapshot_hour) as latest
286
+ FROM hourly_usage_snapshots
287
+ WHERE snapshot_hour >= datetime('now', '-24 hours') AND project = 'all'
288
+ LIMIT 1`
289
+ )
290
+ .first<{ latest: string | null }>();
291
+ if (quality?.latest) {
292
+ summary.dataQuality.latestSnapshot = quality.latest;
293
+ const ageMs = Date.now() - new Date(quality.latest + 'Z').getTime();
294
+ summary.dataQuality.snapshotAgeMinutes = Math.round(ageMs / 60000);
295
+ summary.dataQuality.status = summary.dataQuality.snapshotAgeMinutes < 120 ? 'fresh' : 'stale';
296
+ }
297
+ } catch {
298
+ // Table may not exist
299
+ }
300
+ })()
301
+ );
302
+
303
+ await Promise.all(promises);
304
+
305
+ return new Response(JSON.stringify(summary), {
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ 'Cache-Control': 'max-age=60',
309
+ },
310
+ });
311
+ };
@@ -0,0 +1,56 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { CF_PAID_ALLOWANCES } from '../../../lib/cloudflare/costs';
3
+
4
+ interface Env {
5
+ PLATFORM_DB?: D1Database;
6
+ }
7
+
8
+ export const GET: APIRoute = async ({ locals }) => {
9
+ const env = locals.runtime?.env as Env | undefined;
10
+ const db = env?.PLATFORM_DB;
11
+
12
+ const allowances: Array<{
13
+ resource: string;
14
+ allowance: number;
15
+ used: number;
16
+ pct: number;
17
+ }> = [];
18
+
19
+ if (db) {
20
+ try {
21
+ const monthStart = new Date();
22
+ monthStart.setDate(1);
23
+ const cutoff = monthStart.toISOString().slice(0, 10);
24
+
25
+ const result = await db
26
+ .prepare(
27
+ `SELECT SUM(d1_reads) as d1_reads, SUM(d1_writes) as d1_writes,
28
+ SUM(kv_reads) as kv_reads, SUM(kv_writes) as kv_writes,
29
+ SUM(worker_requests) as worker_requests
30
+ FROM daily_usage_rollups
31
+ WHERE project = 'all' AND snapshot_date >= ?
32
+ LIMIT 1`
33
+ )
34
+ .bind(cutoff)
35
+ .first<Record<string, number>>();
36
+
37
+ if (result) {
38
+ for (const [key, limit] of Object.entries(CF_PAID_ALLOWANCES)) {
39
+ const used = result[key] ?? 0;
40
+ allowances.push({
41
+ resource: key,
42
+ allowance: limit,
43
+ used,
44
+ pct: limit > 0 ? Math.round((used / limit) * 100) : 0,
45
+ });
46
+ }
47
+ }
48
+ } catch {
49
+ // Table may not exist
50
+ }
51
+ }
52
+
53
+ return new Response(JSON.stringify({ allowances }), {
54
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
55
+ });
56
+ };
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ }
6
+
7
+ export const GET: APIRoute = async ({ locals }) => {
8
+ const env = locals.runtime?.env as Env | undefined;
9
+ const db = env?.PLATFORM_DB;
10
+
11
+ const anomalies: Array<{
12
+ id: number;
13
+ project: string;
14
+ metric: string;
15
+ current_value: number;
16
+ baseline_value: number;
17
+ deviation_pct: number;
18
+ detected_at: string;
19
+ status: string;
20
+ }> = [];
21
+
22
+ if (db) {
23
+ try {
24
+ const result = await db
25
+ .prepare(
26
+ `SELECT id, project, metric, current_value, baseline_value,
27
+ deviation_pct, detected_at, status
28
+ FROM usage_anomalies
29
+ WHERE detected_at >= datetime('now', '-7 days')
30
+ ORDER BY detected_at DESC
31
+ LIMIT 20`
32
+ )
33
+ .all();
34
+ if (result.results) {
35
+ anomalies.push(...(result.results as typeof anomalies));
36
+ }
37
+ } catch {
38
+ // Table may not exist
39
+ }
40
+ }
41
+
42
+ return new Response(JSON.stringify({ anomalies }), {
43
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' },
44
+ });
45
+ };
@@ -0,0 +1,53 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ }
6
+
7
+ export const GET: APIRoute = async ({ locals }) => {
8
+ const env = locals.runtime?.env as Env | undefined;
9
+ const db = env?.PLATFORM_DB;
10
+
11
+ const now = new Date();
12
+ const billingStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
13
+ const billingEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
14
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
15
+ const daysSoFar = now.getDate();
16
+
17
+ const billing = {
18
+ cycleStart: billingStart,
19
+ cycleEnd: billingEnd,
20
+ daysInMonth,
21
+ daysSoFar,
22
+ currentSpend: 0,
23
+ dailyBurnRate: 0,
24
+ projectedMonthly: 0,
25
+ plan: 'workers_paid' as string,
26
+ };
27
+
28
+ if (db) {
29
+ try {
30
+ const result = await db
31
+ .prepare(
32
+ `SELECT SUM(total_cost_usd) as spend
33
+ FROM daily_usage_rollups
34
+ WHERE project = 'all' AND snapshot_date >= ?
35
+ LIMIT 1`
36
+ )
37
+ .bind(billingStart)
38
+ .first<{ spend: number | null }>();
39
+
40
+ if (result?.spend) {
41
+ billing.currentSpend = Math.round(result.spend * 100) / 100;
42
+ billing.dailyBurnRate = daysSoFar > 0 ? Math.round((billing.currentSpend / daysSoFar) * 100) / 100 : 0;
43
+ billing.projectedMonthly = Math.round(billing.dailyBurnRate * daysInMonth * 100) / 100;
44
+ }
45
+ } catch {
46
+ // Table may not exist
47
+ }
48
+ }
49
+
50
+ return new Response(JSON.stringify(billing), {
51
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
52
+ });
53
+ };
@@ -0,0 +1,44 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const kv = (locals.runtime?.env as { PLATFORM_CACHE?: KVNamespace } | undefined)?.PLATFORM_CACHE;
5
+
6
+ if (!kv) {
7
+ return new Response(JSON.stringify({ breakers: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const cbKeys = await kv.list({ prefix: 'cb:' });
14
+ const breakers = [];
15
+
16
+ for (const key of cbKeys.keys) {
17
+ const state = (await kv.get(key.name, 'json')) as {
18
+ status?: string;
19
+ feature?: string;
20
+ reason?: string;
21
+ trippedAt?: string;
22
+ } | null;
23
+ if (state) {
24
+ breakers.push({
25
+ key: key.name,
26
+ feature: state.feature ?? key.name.replace('cb:', ''),
27
+ status: state.status ?? 'unknown',
28
+ reason: state.reason ?? null,
29
+ trippedAt: state.trippedAt ?? null,
30
+ });
31
+ }
32
+ }
33
+
34
+ return new Response(JSON.stringify({ breakers }), {
35
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
36
+ });
37
+ } catch (error) {
38
+ console.error('[circuit-breakers] Error:', error);
39
+ return new Response(JSON.stringify({ breakers: [], error: 'Failed to fetch circuit breakers' }), {
40
+ status: 500,
41
+ headers: { 'Content-Type': 'application/json' },
42
+ });
43
+ }
44
+ };
@@ -0,0 +1,50 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ }
6
+
7
+ export const GET: APIRoute = async ({ locals, url }) => {
8
+ const env = locals.runtime?.env as Env | undefined;
9
+ const db = env?.PLATFORM_DB;
10
+ const project = url.searchParams.get('project') ?? 'all';
11
+ const days = Math.min(Number(url.searchParams.get('days') ?? '7'), 30);
12
+
13
+ const rows: Array<{
14
+ snapshot_date: string;
15
+ d1_reads: number;
16
+ d1_writes: number;
17
+ kv_reads: number;
18
+ kv_writes: number;
19
+ r2_reads: number;
20
+ r2_writes: number;
21
+ worker_requests: number;
22
+ total_cost_usd: number;
23
+ }> = [];
24
+
25
+ if (db) {
26
+ try {
27
+ const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
28
+ const result = await db
29
+ .prepare(
30
+ `SELECT snapshot_date, d1_reads, d1_writes, kv_reads, kv_writes,
31
+ r2_reads, r2_writes, worker_requests, total_cost_usd
32
+ FROM daily_usage_rollups
33
+ WHERE project = ? AND snapshot_date >= ?
34
+ ORDER BY snapshot_date ASC
35
+ LIMIT 30`
36
+ )
37
+ .bind(project, cutoff)
38
+ .all();
39
+ if (result.results) {
40
+ rows.push(...(result.results as typeof rows));
41
+ }
42
+ } catch {
43
+ // Table may not exist
44
+ }
45
+ }
46
+
47
+ return new Response(JSON.stringify({ rows, project, days }), {
48
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' },
49
+ });
50
+ };
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ }
6
+
7
+ export const GET: APIRoute = async ({ locals, url }) => {
8
+ const env = locals.runtime?.env as Env | undefined;
9
+ const db = env?.PLATFORM_DB;
10
+ const hours = Math.min(Number(url.searchParams.get('hours') ?? '24'), 168);
11
+
12
+ const snapshots: Array<{
13
+ snapshot_hour: string;
14
+ d1_reads: number;
15
+ d1_writes: number;
16
+ kv_reads: number;
17
+ kv_writes: number;
18
+ total_cost_usd: number;
19
+ }> = [];
20
+
21
+ if (db) {
22
+ try {
23
+ const cutoff = new Date(Date.now() - hours * 3600000).toISOString().slice(0, 19).replace('T', ' ');
24
+ const result = await db
25
+ .prepare(
26
+ `SELECT snapshot_hour, d1_reads, d1_writes, kv_reads, kv_writes, total_cost_usd
27
+ FROM hourly_usage_snapshots
28
+ WHERE project = 'all' AND snapshot_hour >= ?
29
+ ORDER BY snapshot_hour ASC
30
+ LIMIT 168`
31
+ )
32
+ .bind(cutoff)
33
+ .all();
34
+ if (result.results) {
35
+ snapshots.push(...(result.results as typeof snapshots));
36
+ }
37
+ } catch {
38
+ // Table may not exist
39
+ }
40
+ }
41
+
42
+ return new Response(JSON.stringify({ snapshots, hours }), {
43
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' },
44
+ });
45
+ };
@@ -0,0 +1,51 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ interface Env {
4
+ PLATFORM_DB?: D1Database;
5
+ }
6
+
7
+ export const GET: APIRoute = async ({ locals }) => {
8
+ const env = locals.runtime?.env as Env | undefined;
9
+ const db = env?.PLATFORM_DB;
10
+
11
+ const projects: Array<{
12
+ project: string;
13
+ total_cost: number;
14
+ d1_writes: number;
15
+ worker_requests: number;
16
+ latest_date: string;
17
+ }> = [];
18
+
19
+ if (db) {
20
+ try {
21
+ const monthStart = new Date();
22
+ monthStart.setDate(1);
23
+ const cutoff = monthStart.toISOString().slice(0, 10);
24
+
25
+ const result = await db
26
+ .prepare(
27
+ `SELECT project,
28
+ SUM(total_cost_usd) as total_cost,
29
+ SUM(d1_writes) as d1_writes,
30
+ SUM(worker_requests) as worker_requests,
31
+ MAX(snapshot_date) as latest_date
32
+ FROM daily_usage_rollups
33
+ WHERE project != 'all' AND snapshot_date >= ?
34
+ GROUP BY project
35
+ ORDER BY total_cost DESC
36
+ LIMIT 20`
37
+ )
38
+ .bind(cutoff)
39
+ .all();
40
+ if (result.results) {
41
+ projects.push(...(result.results as typeof projects));
42
+ }
43
+ } catch {
44
+ // Table may not exist
45
+ }
46
+ }
47
+
48
+ return new Response(JSON.stringify({ projects }), {
49
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
50
+ });
51
+ };
@@ -0,0 +1,42 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ status: 'no_database' }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const latestSnapshot = await db
14
+ .prepare(
15
+ `SELECT snapshot_hour, project, total_cost_usd
16
+ FROM hourly_usage_snapshots
17
+ WHERE project = 'all'
18
+ ORDER BY snapshot_hour DESC
19
+ LIMIT 1`
20
+ )
21
+ .first<{ snapshot_hour: string; project: string; total_cost_usd: number }>();
22
+
23
+ const featureCount = await db
24
+ .prepare(`SELECT COUNT(DISTINCT feature_id) as count FROM feature_usage_daily LIMIT 1`)
25
+ .first<{ count: number }>();
26
+
27
+ return new Response(
28
+ JSON.stringify({
29
+ status: 'active',
30
+ latestSnapshot: latestSnapshot?.snapshot_hour ?? null,
31
+ latestCost: latestSnapshot?.total_cost_usd ?? 0,
32
+ trackedFeatures: featureCount?.count ?? 0,
33
+ }),
34
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
35
+ );
36
+ } catch {
37
+ return new Response(JSON.stringify({ status: 'error' }), {
38
+ status: 500,
39
+ headers: { 'Content-Type': 'application/json' },
40
+ });
41
+ }
42
+ };