@littlebearapps/platform-admin-sdk 1.5.0 → 2.1.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/README.md +2 -2
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +197 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +3 -0
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -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/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -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/ready-for-review.ts +39 -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/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/scripts/ops/universal-backfill.ts +147 -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/security.yml +33 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -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/reports/ReportInfoButton.tsx +98 -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/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +6 -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/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -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/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/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/user/identity.ts +11 -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/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 +5 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -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/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -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/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/index.ts +2 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -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/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch with Request Deduplication
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate API calls by:
|
|
5
|
+
* 1. Deduplicating concurrent requests to the same URL
|
|
6
|
+
* 2. Caching responses for a short TTL
|
|
7
|
+
*
|
|
8
|
+
* This is used by the unified dashboard components to prevent
|
|
9
|
+
* multiple components from making redundant API calls.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface CacheEntry<T> {
|
|
13
|
+
data: T;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// In-flight request tracking (prevents concurrent duplicate requests)
|
|
18
|
+
const pendingRequests = new Map<string, Promise<Response>>();
|
|
19
|
+
|
|
20
|
+
// Response cache with TTL
|
|
21
|
+
const responseCache = new Map<string, CacheEntry<unknown>>();
|
|
22
|
+
|
|
23
|
+
// Default cache TTL: 5 seconds (short enough to stay fresh, long enough to dedupe)
|
|
24
|
+
const DEFAULT_CACHE_TTL = 5000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch with automatic request deduplication and short-term caching
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchWithDedup<T>(
|
|
30
|
+
url: string,
|
|
31
|
+
options: RequestInit = {},
|
|
32
|
+
cacheTtl: number = DEFAULT_CACHE_TTL
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
const cacheKey = `${options.method || 'GET'}:${url}`;
|
|
35
|
+
|
|
36
|
+
// Check response cache first
|
|
37
|
+
const cached = responseCache.get(cacheKey);
|
|
38
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
39
|
+
return cached.data as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for in-flight request
|
|
43
|
+
const pending = pendingRequests.get(cacheKey);
|
|
44
|
+
if (pending) {
|
|
45
|
+
const response = await pending;
|
|
46
|
+
const data = await response.clone().json();
|
|
47
|
+
return data as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create new request
|
|
51
|
+
const request = fetch(url, {
|
|
52
|
+
credentials: 'include',
|
|
53
|
+
...options,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
pendingRequests.set(cacheKey, request);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await request;
|
|
60
|
+
pendingRequests.delete(cacheKey);
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`HTTP ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
// Cache the response
|
|
69
|
+
responseCache.set(cacheKey, {
|
|
70
|
+
data,
|
|
71
|
+
expiresAt: Date.now() + cacheTtl,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return data as T;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
pendingRequests.delete(cacheKey);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear the response cache (useful for forced refresh)
|
|
83
|
+
*/
|
|
84
|
+
export function clearFetchCache(urlPattern?: string): void {
|
|
85
|
+
if (urlPattern) {
|
|
86
|
+
for (const key of responseCache.keys()) {
|
|
87
|
+
if (key.includes(urlPattern)) {
|
|
88
|
+
responseCache.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
responseCache.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a URL is currently being fetched
|
|
98
|
+
*/
|
|
99
|
+
export function isFetching(url: string): boolean {
|
|
100
|
+
return pendingRequests.has(`GET:${url}`);
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { CF_PAID_ALLOWANCES, CF_PRICING } from '../../../lib/cloudflare/costs';
|
|
3
|
+
|
|
4
|
+
interface Env {
|
|
5
|
+
PLATFORM_DB?: D1Database;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CostLine {
|
|
9
|
+
resource: string;
|
|
10
|
+
used: number;
|
|
11
|
+
allowance: number;
|
|
12
|
+
overage: number;
|
|
13
|
+
cost: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
17
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
18
|
+
const db = env?.PLATFORM_DB;
|
|
19
|
+
|
|
20
|
+
const overview = {
|
|
21
|
+
lines: [] as CostLine[],
|
|
22
|
+
totalCost: 0,
|
|
23
|
+
period: 'mtd',
|
|
24
|
+
billingStart: '',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const now = new Date();
|
|
28
|
+
overview.billingStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
|
29
|
+
|
|
30
|
+
if (db) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await db
|
|
33
|
+
.prepare(
|
|
34
|
+
`SELECT SUM(d1_reads) as d1_reads, SUM(d1_writes) as d1_writes,
|
|
35
|
+
SUM(kv_reads) as kv_reads, SUM(kv_writes) as kv_writes,
|
|
36
|
+
SUM(worker_requests) as worker_requests,
|
|
37
|
+
SUM(r2_reads) as r2_reads, SUM(r2_writes) as r2_writes
|
|
38
|
+
FROM daily_usage_rollups
|
|
39
|
+
WHERE project = 'all' AND snapshot_date >= ?
|
|
40
|
+
LIMIT 1`
|
|
41
|
+
)
|
|
42
|
+
.bind(overview.billingStart)
|
|
43
|
+
.first<Record<string, number>>();
|
|
44
|
+
|
|
45
|
+
if (result) {
|
|
46
|
+
for (const [resource, allowance] of Object.entries(CF_PAID_ALLOWANCES)) {
|
|
47
|
+
const used = result[resource] ?? 0;
|
|
48
|
+
const overage = Math.max(0, used - allowance);
|
|
49
|
+
const unitPrice = CF_PRICING[resource] ?? 0;
|
|
50
|
+
const cost = Math.round(overage * unitPrice * 100) / 100;
|
|
51
|
+
|
|
52
|
+
overview.lines.push({ resource, used, allowance, overage, cost });
|
|
53
|
+
overview.totalCost += cost;
|
|
54
|
+
}
|
|
55
|
+
overview.totalCost = Math.round(overview.totalCost * 100) / 100;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Table may not exist
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response(JSON.stringify(overview), {
|
|
63
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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 providers: Array<{
|
|
12
|
+
provider: string;
|
|
13
|
+
mtd_cost: number;
|
|
14
|
+
latest_date: string;
|
|
15
|
+
}> = [];
|
|
16
|
+
|
|
17
|
+
if (db) {
|
|
18
|
+
try {
|
|
19
|
+
const monthStart = new Date();
|
|
20
|
+
monthStart.setDate(1);
|
|
21
|
+
const cutoff = monthStart.toISOString().slice(0, 10);
|
|
22
|
+
|
|
23
|
+
const result = await db
|
|
24
|
+
.prepare(
|
|
25
|
+
`SELECT provider,
|
|
26
|
+
SUM(cost_usd) as mtd_cost,
|
|
27
|
+
MAX(snapshot_date) as latest_date
|
|
28
|
+
FROM third_party_usage
|
|
29
|
+
WHERE snapshot_date >= ?
|
|
30
|
+
GROUP BY provider
|
|
31
|
+
ORDER BY mtd_cost DESC
|
|
32
|
+
LIMIT 20`
|
|
33
|
+
)
|
|
34
|
+
.bind(cutoff)
|
|
35
|
+
.all();
|
|
36
|
+
if (result.results) {
|
|
37
|
+
providers.push(...(result.results as typeof providers));
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Table may not exist
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Response(JSON.stringify({ providers }), {
|
|
45
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
|
|
46
|
+
});
|
|
47
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
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
|
+
const kv = env?.PLATFORM_CACHE;
|
|
12
|
+
|
|
13
|
+
let services: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
project: string;
|
|
17
|
+
status: string;
|
|
18
|
+
}> = [];
|
|
19
|
+
|
|
20
|
+
// Try KV first
|
|
21
|
+
if (kv) {
|
|
22
|
+
try {
|
|
23
|
+
const registry = await kv.get('SERVICE_REGISTRY', 'json');
|
|
24
|
+
if (registry && Array.isArray(registry)) {
|
|
25
|
+
services = registry as typeof services;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// KV may not be available
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fall back to D1
|
|
33
|
+
if (services.length === 0 && db) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await db
|
|
36
|
+
.prepare(
|
|
37
|
+
`SELECT resource_name as name, resource_type as type,
|
|
38
|
+
project_id as project, 'deployed' as status
|
|
39
|
+
FROM resource_project_mapping
|
|
40
|
+
ORDER BY project_id, resource_type
|
|
41
|
+
LIMIT 200`
|
|
42
|
+
)
|
|
43
|
+
.all();
|
|
44
|
+
if (result.results) {
|
|
45
|
+
services = result.results as typeof services;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Table may not exist
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Response(JSON.stringify({ services }), {
|
|
53
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' },
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
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
|
+
const kv = env?.PLATFORM_CACHE;
|
|
12
|
+
|
|
13
|
+
const stats = {
|
|
14
|
+
services: {
|
|
15
|
+
total: 0,
|
|
16
|
+
byStatus: { deployed: 0, development: 0, deprecated: 0, paused: 0 },
|
|
17
|
+
byType: {} as Record<string, number>,
|
|
18
|
+
byProject: {} as Record<string, number>,
|
|
19
|
+
},
|
|
20
|
+
alerts: {
|
|
21
|
+
total: 0,
|
|
22
|
+
unacknowledged: 0,
|
|
23
|
+
critical: 0,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Try KV first (faster, populated by sync-config)
|
|
28
|
+
if (kv) {
|
|
29
|
+
try {
|
|
30
|
+
const services = await kv.get('SERVICE_REGISTRY', 'json');
|
|
31
|
+
if (services && Array.isArray(services)) {
|
|
32
|
+
stats.services.total = services.length;
|
|
33
|
+
for (const s of services as Array<{ status: string; type: string; project: string }>) {
|
|
34
|
+
const status = s.status as keyof typeof stats.services.byStatus;
|
|
35
|
+
if (status in stats.services.byStatus) {
|
|
36
|
+
stats.services.byStatus[status]++;
|
|
37
|
+
}
|
|
38
|
+
stats.services.byType[s.type] = (stats.services.byType[s.type] || 0) + 1;
|
|
39
|
+
stats.services.byProject[s.project] = (stats.services.byProject[s.project] || 0) + 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// KV may not be available
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fall back to D1 if KV had nothing
|
|
48
|
+
if (stats.services.total === 0 && db) {
|
|
49
|
+
try {
|
|
50
|
+
const rows = await db
|
|
51
|
+
.prepare(
|
|
52
|
+
`SELECT resource_type, project_id, COUNT(*) as cnt
|
|
53
|
+
FROM resource_project_mapping
|
|
54
|
+
GROUP BY resource_type, project_id
|
|
55
|
+
LIMIT 200`
|
|
56
|
+
)
|
|
57
|
+
.all<{ resource_type: string; project_id: string; cnt: number }>();
|
|
58
|
+
if (rows.results) {
|
|
59
|
+
for (const row of rows.results) {
|
|
60
|
+
stats.services.total += row.cnt;
|
|
61
|
+
stats.services.byStatus.deployed += row.cnt;
|
|
62
|
+
stats.services.byType[row.resource_type] = (stats.services.byType[row.resource_type] || 0) + row.cnt;
|
|
63
|
+
stats.services.byProject[row.project_id] = (stats.services.byProject[row.project_id] || 0) + row.cnt;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Table may not exist
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Alert stats from notifications
|
|
72
|
+
if (db) {
|
|
73
|
+
try {
|
|
74
|
+
const alertResult = await db
|
|
75
|
+
.prepare(
|
|
76
|
+
`SELECT
|
|
77
|
+
COUNT(*) as total,
|
|
78
|
+
SUM(CASE WHEN category IN ('error', 'warning') THEN 1 ELSE 0 END) as unacknowledged,
|
|
79
|
+
SUM(CASE WHEN priority IN ('critical', 'high') AND category = 'error' THEN 1 ELSE 0 END) as critical
|
|
80
|
+
FROM notifications
|
|
81
|
+
WHERE created_at > unixepoch() - 86400 * 7
|
|
82
|
+
AND (expires_at IS NULL OR expires_at > unixepoch())
|
|
83
|
+
LIMIT 1`
|
|
84
|
+
)
|
|
85
|
+
.first<{ total: number; unacknowledged: number; critical: number }>();
|
|
86
|
+
if (alertResult) {
|
|
87
|
+
stats.alerts.total = alertResult.total || 0;
|
|
88
|
+
stats.alerts.unacknowledged = alertResult.unacknowledged || 0;
|
|
89
|
+
stats.alerts.critical = alertResult.critical || 0;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Table may not exist
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Response(JSON.stringify(stats), {
|
|
97
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' },
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -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,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,11 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
4
|
+
// Extract identity from CF Access JWT headers
|
|
5
|
+
const email = request.headers.get('cf-access-authenticated-user-email') ?? 'unknown';
|
|
6
|
+
const name = email.split('@')[0] ?? 'User';
|
|
7
|
+
|
|
8
|
+
return new Response(JSON.stringify({ email, name }), {
|
|
9
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' },
|
|
10
|
+
});
|
|
11
|
+
};
|