@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.
Files changed (111) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +121 -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/PatternStats.tsx +60 -0
  9. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  10. package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
  11. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  12. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  13. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  14. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  15. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  16. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  17. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  18. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  19. package/templates/full/migrations/008_auditor.sql +99 -0
  20. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  21. package/templates/full/migrations/011_multi_account.sql +51 -0
  22. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  23. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  24. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  25. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  26. package/templates/full/workers/lib/auditor/index.ts +9 -0
  27. package/templates/full/workers/lib/auditor/types.ts +167 -0
  28. package/templates/full/workers/platform-auditor.ts +1071 -0
  29. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  30. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  31. package/templates/shared/config/observability.yaml.hbs +276 -0
  32. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  33. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  34. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  35. package/templates/shared/dashboard/astro.config.mjs +21 -0
  36. package/templates/shared/dashboard/package.json.hbs +29 -0
  37. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  38. package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
  39. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  40. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  41. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  42. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  43. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  44. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  45. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  46. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  47. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  48. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  49. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  50. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  51. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  52. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  53. package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
  54. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  55. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  56. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  57. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  58. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  59. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  60. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  61. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  62. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  63. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  64. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  65. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  66. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  67. package/templates/shared/dashboard/src/styles/global.css +29 -0
  68. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  69. package/templates/shared/dashboard/tsconfig.json +9 -0
  70. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  71. package/templates/shared/package.json.hbs +12 -1
  72. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  73. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  74. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  75. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  76. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  77. package/templates/shared/scripts/validate-schemas.js +61 -0
  78. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  79. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  80. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  81. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  82. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  83. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  84. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  85. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  86. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  87. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  88. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  89. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  90. package/templates/shared/workers/platform-usage.ts +98 -8
  91. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  92. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  93. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  94. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  95. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  96. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  97. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  98. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  99. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  100. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  101. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  102. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  103. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  104. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  105. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  106. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  107. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  108. package/templates/standard/workers/platform-mapper.ts +482 -0
  109. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  110. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  111. 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,3 @@
1
+ ---
2
+ return Astro.redirect('/dashboard');
3
+ ---
@@ -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,9 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./src/**/*.{astro,html,js,jsx,ts,tsx}'],
4
+ darkMode: 'class',
5
+ theme: {
6
+ extend: {},
7
+ },
8
+ plugins: [],
9
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/*": ["src/*"]
7
+ }
8
+ }
9
+ }
@@ -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"