@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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -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/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -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/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -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/scripts/ops/universal-backfill.ts +147 -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/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -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 +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -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/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -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/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -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/cloudflare/costs.ts +21 -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/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -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/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.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/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -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-controls.js +141 -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/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -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/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -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 +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -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/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -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/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -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,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
|
+
};
|