@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/package.json.hbs +12 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +2 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- 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,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,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
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../layouts/DashboardLayout.astro';
|
|
3
|
+
import { MissionControl } from '../components/overview/MissionControl';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<DashboardLayout title="Overview">
|
|
7
|
+
<div class="max-w-7xl mx-auto">
|
|
8
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Mission Control</h2>
|
|
9
|
+
<MissionControl client:load />
|
|
10
|
+
</div>
|
|
11
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../layouts/DashboardLayout.astro';
|
|
3
|
+
import { ResourceTabs } from '../components/resources/ResourceTabs';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<DashboardLayout title="Resources">
|
|
7
|
+
<div class="max-w-7xl mx-auto">
|
|
8
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Resources</h2>
|
|
9
|
+
<ResourceTabs client:load />
|
|
10
|
+
</div>
|
|
11
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
|
3
|
+
import { SettingsCard } from '../../components/settings/SettingsCard';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<DashboardLayout title="Settings">
|
|
7
|
+
<div class="max-w-3xl mx-auto space-y-4">
|
|
8
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Settings</h2>
|
|
9
|
+
<SettingsCard
|
|
10
|
+
client:load
|
|
11
|
+
label="Monthly Budget"
|
|
12
|
+
description="Soft budget limit for cost alerts. Configure in budgets.yaml."
|
|
13
|
+
value="$100"
|
|
14
|
+
/>
|
|
15
|
+
<SettingsCard
|
|
16
|
+
client:load
|
|
17
|
+
label="Circuit Breakers"
|
|
18
|
+
description="Automatic feature pause when budgets are exceeded."
|
|
19
|
+
value="Enabled"
|
|
20
|
+
/>
|
|
21
|
+
<SettingsCard
|
|
22
|
+
client:load
|
|
23
|
+
label="Data Collection"
|
|
24
|
+
description="Hourly Cloudflare GraphQL snapshots and SDK telemetry."
|
|
25
|
+
value="Active"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer components {
|
|
6
|
+
.nav-link {
|
|
7
|
+
@apply text-gray-700 transition-colors hover:text-blue-600 dark:text-gray-300 dark:hover:text-blue-400 font-medium;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.metric-card {
|
|
11
|
+
@apply rounded-lg bg-white p-6 shadow dark:bg-gray-800;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.metric-title {
|
|
15
|
+
@apply text-sm font-medium uppercase tracking-wide text-gray-600 dark:text-gray-400;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.metric-value {
|
|
19
|
+
@apply mt-2 text-3xl font-bold text-gray-900 dark:text-white;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.alert-critical {
|
|
23
|
+
@apply rounded border-l-4 border-red-500 bg-red-50 p-4 dark:bg-red-900/20;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.alert-warning {
|
|
27
|
+
@apply rounded border-l-4 border-yellow-500 bg-yellow-50 p-4 dark:bg-yellow-900/20;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "{{projectSlug}}-dashboard",
|
|
4
|
+
"pages_build_output_dir": "dist",
|
|
5
|
+
"compatibility_date": "2026-01-01",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"upload_source_maps": true,
|
|
8
|
+
"d1_databases": [
|
|
9
|
+
{
|
|
10
|
+
"binding": "PLATFORM_DB",
|
|
11
|
+
"database_name": "YOUR_DATABASE_NAME",
|
|
12
|
+
"database_id": "YOUR_DATABASE_ID"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"kv_namespaces": [
|
|
16
|
+
{
|
|
17
|
+
"binding": "PLATFORM_CACHE",
|
|
18
|
+
"id": "YOUR_PLATFORM_CACHE_KV_ID"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"services": [
|
|
22
|
+
{
|
|
23
|
+
"binding": "USAGE_API",
|
|
24
|
+
"service": "{{projectSlug}}-usage"
|
|
25
|
+
}{{#if isStandard}},
|
|
26
|
+
{
|
|
27
|
+
"binding": "ERROR_COLLECTOR_API",
|
|
28
|
+
"service": "{{projectSlug}}-error-collector"
|
|
29
|
+
}{{/if}}{{#if isFull}},
|
|
30
|
+
{
|
|
31
|
+
"binding": "PATTERN_DISCOVERY_API",
|
|
32
|
+
"service": "{{projectSlug}}-pattern-discovery"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"binding": "NOTIFICATIONS_API",
|
|
36
|
+
"service": "{{projectSlug}}-notifications"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"binding": "SETTINGS_API",
|
|
40
|
+
"service": "{{projectSlug}}-settings"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"binding": "SEARCH_API",
|
|
44
|
+
"service": "{{projectSlug}}-search"
|
|
45
|
+
}{{/if}}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"typecheck": "tsc --noEmit",
|
|
8
8
|
"sync:config": "npx tsx scripts/sync-config.ts",
|
|
9
|
-
"deploy:usage": "wrangler deploy -c wrangler.{{projectSlug}}-usage.jsonc"
|
|
9
|
+
"deploy:usage": "wrangler deploy -c wrangler.{{projectSlug}}-usage.jsonc",
|
|
10
|
+
"backfill": "npx tsx scripts/ops/backfill-cloudflare-hourly.ts",
|
|
11
|
+
"reset-cb": "npx tsx scripts/ops/reset-budget-state.ts",
|
|
12
|
+
"verify": "npx tsx scripts/ops/verify-account-completeness.ts",
|
|
13
|
+
"validate:pipeline": "npx tsx scripts/ops/validate-pipeline.ts",
|
|
14
|
+
"validate:schemas": "node scripts/validate-schemas.js",
|
|
15
|
+
"deploy:auditor": "wrangler deploy -c wrangler.{{projectSlug}}-auditor.jsonc",
|
|
16
|
+
"deploy:mapper": "wrangler deploy -c wrangler.{{projectSlug}}-mapper.jsonc",
|
|
17
|
+
"deploy:test-client": "wrangler deploy -c wrangler.{{projectSlug}}-sdk-test-client.jsonc",
|
|
18
|
+
"discover:datasets": "npx tsx scripts/ops/discover-graphql-datasets.ts"
|
|
10
19
|
},
|
|
11
20
|
"dependencies": {
|
|
12
21
|
"@littlebearapps/platform-consumer-sdk": "^{{sdkVersion}}",
|
|
@@ -14,6 +23,8 @@
|
|
|
14
23
|
},
|
|
15
24
|
"devDependencies": {
|
|
16
25
|
"@cloudflare/workers-types": "^4.20250214.0",
|
|
26
|
+
"ajv": "^8.17.0",
|
|
27
|
+
"ajv-formats": "^3.0.0",
|
|
17
28
|
"tsx": "^4.19.0",
|
|
18
29
|
"typescript": "^5.7.3",
|
|
19
30
|
"wrangler": "^3.100.0"
|