@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-Party Provider Stubs
|
|
3
|
+
*
|
|
4
|
+
* Interface definitions and fetcher stubs for third-party service usage tracking.
|
|
5
|
+
* These will be implemented incrementally as integrations are built.
|
|
6
|
+
*
|
|
7
|
+
* Supported providers (planned):
|
|
8
|
+
* - GitHub (Actions minutes, Advanced Security seats, storage)
|
|
9
|
+
* - OpenAI (API usage, tokens)
|
|
10
|
+
* - Apify (Actor runs, compute units)
|
|
11
|
+
* - Anthropic (API usage, tokens)
|
|
12
|
+
* - Resend (emails sent)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider identifier
|
|
17
|
+
*/
|
|
18
|
+
export type ProviderId = 'github' | 'openai' | 'apify' | 'anthropic' | 'resend';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Usage record from a third-party provider
|
|
22
|
+
*/
|
|
23
|
+
export interface ThirdPartyUsageRecord {
|
|
24
|
+
/** Provider identifier */
|
|
25
|
+
provider: ProviderId;
|
|
26
|
+
/** Type of resource (e.g., 'actions_minutes', 'api_tokens') */
|
|
27
|
+
resourceType: string;
|
|
28
|
+
/** Optional specific resource name */
|
|
29
|
+
resourceName?: string;
|
|
30
|
+
/** Numeric usage value */
|
|
31
|
+
usageValue: number;
|
|
32
|
+
/** Unit of measurement */
|
|
33
|
+
usageUnit: string;
|
|
34
|
+
/** Estimated cost in USD */
|
|
35
|
+
costUsd: number;
|
|
36
|
+
/** When this data was collected */
|
|
37
|
+
collectedAt: Date;
|
|
38
|
+
/** Period this usage covers */
|
|
39
|
+
periodStart: Date;
|
|
40
|
+
periodEnd: Date;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provider configuration
|
|
45
|
+
*/
|
|
46
|
+
export interface ProviderConfig {
|
|
47
|
+
/** Provider identifier */
|
|
48
|
+
id: ProviderId;
|
|
49
|
+
/** Human-readable name */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Whether the provider is currently enabled */
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
/** API key environment variable name */
|
|
54
|
+
apiKeyEnvVar: string;
|
|
55
|
+
/** Pricing model description */
|
|
56
|
+
pricingModel: string;
|
|
57
|
+
/** Documentation URL */
|
|
58
|
+
docsUrl: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Provider interface for fetching usage data
|
|
63
|
+
*/
|
|
64
|
+
export interface Provider {
|
|
65
|
+
/** Provider configuration */
|
|
66
|
+
config: ProviderConfig;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetch current usage data from the provider
|
|
70
|
+
* @param apiKey - API key for authentication
|
|
71
|
+
* @param options - Additional fetch options
|
|
72
|
+
* @returns Array of usage records
|
|
73
|
+
*/
|
|
74
|
+
fetchUsage(
|
|
75
|
+
apiKey: string,
|
|
76
|
+
options?: {
|
|
77
|
+
startDate?: Date;
|
|
78
|
+
endDate?: Date;
|
|
79
|
+
includeBreakdown?: boolean;
|
|
80
|
+
}
|
|
81
|
+
): Promise<ThirdPartyUsageRecord[]>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if the provider credentials are valid
|
|
85
|
+
* @param apiKey - API key to validate
|
|
86
|
+
*/
|
|
87
|
+
validateCredentials(apiKey: string): Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Provider configurations
|
|
92
|
+
*/
|
|
93
|
+
export const PROVIDER_CONFIGS: Record<ProviderId, ProviderConfig> = {
|
|
94
|
+
github: {
|
|
95
|
+
id: 'github',
|
|
96
|
+
name: 'GitHub',
|
|
97
|
+
enabled: true,
|
|
98
|
+
apiKeyEnvVar: 'GITHUB_TOKEN',
|
|
99
|
+
pricingModel: 'Actions minutes, Advanced Security seats, storage',
|
|
100
|
+
docsUrl: 'https://docs.github.com/en/billing',
|
|
101
|
+
},
|
|
102
|
+
openai: {
|
|
103
|
+
id: 'openai',
|
|
104
|
+
name: 'OpenAI',
|
|
105
|
+
enabled: false,
|
|
106
|
+
apiKeyEnvVar: 'OPENAI_ADMIN_API_KEY',
|
|
107
|
+
pricingModel: 'Per-token pricing for API usage',
|
|
108
|
+
docsUrl: 'https://platform.openai.com/docs/api-reference/usage',
|
|
109
|
+
},
|
|
110
|
+
apify: {
|
|
111
|
+
id: 'apify',
|
|
112
|
+
name: 'Apify',
|
|
113
|
+
enabled: false,
|
|
114
|
+
apiKeyEnvVar: 'APIFY_API_KEY',
|
|
115
|
+
pricingModel: 'Compute units for actor runs',
|
|
116
|
+
docsUrl: 'https://docs.apify.com/platform/billing',
|
|
117
|
+
},
|
|
118
|
+
anthropic: {
|
|
119
|
+
id: 'anthropic',
|
|
120
|
+
name: 'Anthropic',
|
|
121
|
+
enabled: false,
|
|
122
|
+
apiKeyEnvVar: 'ANTHROPIC_ADMIN_API_KEY',
|
|
123
|
+
pricingModel: 'Per-token pricing for Claude API',
|
|
124
|
+
docsUrl: 'https://docs.anthropic.com/en/api/admin-api',
|
|
125
|
+
},
|
|
126
|
+
resend: {
|
|
127
|
+
id: 'resend',
|
|
128
|
+
name: 'Resend',
|
|
129
|
+
enabled: false,
|
|
130
|
+
apiKeyEnvVar: 'RESEND_API_KEY',
|
|
131
|
+
pricingModel: 'Per-email pricing',
|
|
132
|
+
docsUrl: 'https://resend.com/docs/api-reference',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// PROVIDER FETCHER STUBS
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Each stub returns an empty array with a TODO comment.
|
|
140
|
+
// Implement these as integrations are built.
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* GitHub Usage Fetcher
|
|
145
|
+
*
|
|
146
|
+
* TODO: Implement GitHub billing API integration
|
|
147
|
+
* - GET /orgs/{org}/settings/billing/actions
|
|
148
|
+
* - GET /orgs/{org}/settings/billing/advanced-security
|
|
149
|
+
* - GET /orgs/{org}/settings/billing/shared-storage
|
|
150
|
+
*
|
|
151
|
+
* @see https://docs.github.com/en/rest/billing
|
|
152
|
+
*/
|
|
153
|
+
export async function fetchGitHubUsage(
|
|
154
|
+
_apiKey: string,
|
|
155
|
+
_options?: {
|
|
156
|
+
organization?: string;
|
|
157
|
+
startDate?: Date;
|
|
158
|
+
endDate?: Date;
|
|
159
|
+
}
|
|
160
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
161
|
+
// TODO: Implement GitHub billing API calls
|
|
162
|
+
// 1. Fetch Actions minutes usage
|
|
163
|
+
// 2. Fetch Advanced Security seats
|
|
164
|
+
// 3. Fetch shared storage usage
|
|
165
|
+
// 4. Calculate costs based on GitHub pricing
|
|
166
|
+
console.log('[providers] GitHub usage fetcher not yet implemented');
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* OpenAI Usage Fetcher
|
|
172
|
+
*
|
|
173
|
+
* TODO: Implement OpenAI Usage API integration
|
|
174
|
+
* - GET /v1/usage (requires admin API key: sk-admin-...)
|
|
175
|
+
*
|
|
176
|
+
* @see https://platform.openai.com/docs/api-reference/usage
|
|
177
|
+
*/
|
|
178
|
+
export async function fetchOpenAIUsage(
|
|
179
|
+
_apiKey: string,
|
|
180
|
+
_options?: {
|
|
181
|
+
startDate?: Date;
|
|
182
|
+
endDate?: Date;
|
|
183
|
+
}
|
|
184
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
185
|
+
// TODO: Implement OpenAI Usage API calls
|
|
186
|
+
// 1. Fetch token usage by model
|
|
187
|
+
// 2. Calculate costs based on model pricing
|
|
188
|
+
// 3. Group by date for time-series data
|
|
189
|
+
console.log('[providers] OpenAI usage fetcher not yet implemented');
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Apify Usage Fetcher
|
|
195
|
+
*
|
|
196
|
+
* TODO: Implement Apify billing API integration
|
|
197
|
+
* - GET /v2/users/{userId}/usage
|
|
198
|
+
*
|
|
199
|
+
* @see https://docs.apify.com/api/v2#/reference/users/account-usage
|
|
200
|
+
*/
|
|
201
|
+
export async function fetchApifyUsage(
|
|
202
|
+
_apiKey: string,
|
|
203
|
+
_options?: {
|
|
204
|
+
startDate?: Date;
|
|
205
|
+
endDate?: Date;
|
|
206
|
+
}
|
|
207
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
208
|
+
// TODO: Implement Apify usage API calls
|
|
209
|
+
// 1. Fetch compute unit usage
|
|
210
|
+
// 2. Fetch actor run counts
|
|
211
|
+
// 3. Calculate costs based on Apify pricing
|
|
212
|
+
console.log('[providers] Apify usage fetcher not yet implemented');
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Anthropic Usage Fetcher
|
|
218
|
+
*
|
|
219
|
+
* TODO: Implement Anthropic Admin API integration
|
|
220
|
+
* - GET /v1/organizations/{organization_id}/usage (Admin API)
|
|
221
|
+
*
|
|
222
|
+
* @see https://docs.anthropic.com/en/api/admin-api
|
|
223
|
+
*/
|
|
224
|
+
export async function fetchAnthropicUsage(
|
|
225
|
+
_apiKey: string,
|
|
226
|
+
_options?: {
|
|
227
|
+
organizationId?: string;
|
|
228
|
+
startDate?: Date;
|
|
229
|
+
endDate?: Date;
|
|
230
|
+
}
|
|
231
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
232
|
+
// TODO: Implement Anthropic Admin API calls
|
|
233
|
+
// 1. Fetch token usage by model
|
|
234
|
+
// 2. Calculate costs based on Claude pricing
|
|
235
|
+
// 3. Group by date for time-series data
|
|
236
|
+
console.log('[providers] Anthropic usage fetcher not yet implemented');
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resend Usage Fetcher
|
|
242
|
+
*
|
|
243
|
+
* TODO: Implement Resend API integration
|
|
244
|
+
* - No direct usage endpoint; track via webhook or count emails
|
|
245
|
+
*
|
|
246
|
+
* @see https://resend.com/docs/api-reference
|
|
247
|
+
*/
|
|
248
|
+
export async function fetchResendUsage(
|
|
249
|
+
_apiKey: string,
|
|
250
|
+
_options?: {
|
|
251
|
+
startDate?: Date;
|
|
252
|
+
endDate?: Date;
|
|
253
|
+
}
|
|
254
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
255
|
+
// TODO: Implement Resend usage tracking
|
|
256
|
+
// 1. Query sent emails count (if available)
|
|
257
|
+
// 2. Or track via our own email logs
|
|
258
|
+
// 3. Calculate costs based on Resend pricing
|
|
259
|
+
console.log('[providers] Resend usage fetcher not yet implemented');
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// AGGREGATION UTILITIES
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Fetch usage from all enabled providers
|
|
269
|
+
*/
|
|
270
|
+
export async function fetchAllProviderUsage(
|
|
271
|
+
apiKeys: Partial<Record<ProviderId, string>>,
|
|
272
|
+
options?: {
|
|
273
|
+
startDate?: Date;
|
|
274
|
+
endDate?: Date;
|
|
275
|
+
}
|
|
276
|
+
): Promise<ThirdPartyUsageRecord[]> {
|
|
277
|
+
const results: ThirdPartyUsageRecord[] = [];
|
|
278
|
+
|
|
279
|
+
// Only fetch from providers that have API keys configured
|
|
280
|
+
const fetchPromises: Promise<ThirdPartyUsageRecord[]>[] = [];
|
|
281
|
+
|
|
282
|
+
if (apiKeys.github) {
|
|
283
|
+
fetchPromises.push(fetchGitHubUsage(apiKeys.github, options));
|
|
284
|
+
}
|
|
285
|
+
if (apiKeys.openai) {
|
|
286
|
+
fetchPromises.push(fetchOpenAIUsage(apiKeys.openai, options));
|
|
287
|
+
}
|
|
288
|
+
if (apiKeys.apify) {
|
|
289
|
+
fetchPromises.push(fetchApifyUsage(apiKeys.apify, options));
|
|
290
|
+
}
|
|
291
|
+
if (apiKeys.anthropic) {
|
|
292
|
+
fetchPromises.push(fetchAnthropicUsage(apiKeys.anthropic, options));
|
|
293
|
+
}
|
|
294
|
+
if (apiKeys.resend) {
|
|
295
|
+
fetchPromises.push(fetchResendUsage(apiKeys.resend, options));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fetch all in parallel, handle individual failures gracefully
|
|
299
|
+
const settledResults = await Promise.allSettled(fetchPromises);
|
|
300
|
+
|
|
301
|
+
for (const result of settledResults) {
|
|
302
|
+
if (result.status === 'fulfilled') {
|
|
303
|
+
results.push(...result.value);
|
|
304
|
+
} else {
|
|
305
|
+
console.error('[providers] Failed to fetch from provider:', result.reason);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return results;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Calculate total third-party costs from usage records
|
|
314
|
+
*/
|
|
315
|
+
export function calculateThirdPartyCosts(records: ThirdPartyUsageRecord[]): {
|
|
316
|
+
byProvider: Record<ProviderId, number>;
|
|
317
|
+
total: number;
|
|
318
|
+
} {
|
|
319
|
+
const byProvider: Partial<Record<ProviderId, number>> = {};
|
|
320
|
+
let total = 0;
|
|
321
|
+
|
|
322
|
+
for (const record of records) {
|
|
323
|
+
byProvider[record.provider] = (byProvider[record.provider] ?? 0) + record.costUsd;
|
|
324
|
+
total += record.costUsd;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
byProvider: byProvider as Record<ProviderId, number>,
|
|
329
|
+
total,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ params, locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ error: 'Database not available' }), {
|
|
8
|
+
status: 503,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const id = params.id;
|
|
14
|
+
if (!id) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Missing notification id' }), {
|
|
16
|
+
status: 400,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await db
|
|
23
|
+
.prepare(`UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`)
|
|
24
|
+
.bind(id)
|
|
25
|
+
.run();
|
|
26
|
+
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({ ok: true }),
|
|
29
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return new Response(JSON.stringify({ error: 'Failed to mark as read' }), {
|
|
33
|
+
status: 500,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: 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({ error: 'Database not available' }), {
|
|
8
|
+
status: 503,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await db
|
|
15
|
+
.prepare(`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`)
|
|
16
|
+
.run();
|
|
17
|
+
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({ ok: true, updated: result.meta?.changes ?? 0 }),
|
|
20
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
21
|
+
);
|
|
22
|
+
} catch {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'Failed to mark all as read' }), {
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
const kv = (locals.runtime?.env as { PLATFORM_CACHE?: KVNamespace } | undefined)?.PLATFORM_CACHE;
|
|
6
|
+
|
|
7
|
+
if (!db || !kv) {
|
|
8
|
+
return new Response(JSON.stringify({ error: 'Bindings not available' }), {
|
|
9
|
+
status: 503,
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const approved = await db
|
|
16
|
+
.prepare(
|
|
17
|
+
`SELECT id, pattern_type, pattern_value, error_type, priority
|
|
18
|
+
FROM transient_pattern_suggestions
|
|
19
|
+
WHERE status = 'approved'
|
|
20
|
+
ORDER BY match_count DESC
|
|
21
|
+
LIMIT 500`
|
|
22
|
+
)
|
|
23
|
+
.all();
|
|
24
|
+
|
|
25
|
+
const patterns = approved.results ?? [];
|
|
26
|
+
await kv.put('PATTERNS:DYNAMIC:APPROVED', JSON.stringify(patterns), { expirationTtl: 86400 });
|
|
27
|
+
|
|
28
|
+
return new Response(
|
|
29
|
+
JSON.stringify({ refreshed: true, count: patterns.length }),
|
|
30
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
31
|
+
);
|
|
32
|
+
} catch {
|
|
33
|
+
return new Response(JSON.stringify({ error: 'Cache refresh failed' }), {
|
|
34
|
+
status: 500,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: 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({ error: 'Database not available' }), {
|
|
8
|
+
status: 503,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const unclassified = await db
|
|
15
|
+
.prepare(
|
|
16
|
+
`SELECT fingerprint, script_name, error_message, priority, occurrence_count
|
|
17
|
+
FROM error_occurrences
|
|
18
|
+
WHERE fingerprint NOT IN (
|
|
19
|
+
SELECT pattern_value FROM transient_pattern_suggestions WHERE status = 'approved'
|
|
20
|
+
)
|
|
21
|
+
ORDER BY occurrence_count DESC
|
|
22
|
+
LIMIT 50`
|
|
23
|
+
)
|
|
24
|
+
.all();
|
|
25
|
+
|
|
26
|
+
return new Response(
|
|
27
|
+
JSON.stringify({ discovered: unclassified.results?.length ?? 0, errors: unclassified.results ?? [] }),
|
|
28
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return new Response(JSON.stringify({ error: 'Discovery failed' }), {
|
|
32
|
+
status: 500,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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({ patterns: [], count: 0 }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const patterns = await db
|
|
14
|
+
.prepare(
|
|
15
|
+
`SELECT s.id, s.pattern_type, s.pattern_value, s.error_type, s.priority,
|
|
16
|
+
s.match_count, s.source, s.created_at,
|
|
17
|
+
COUNT(e.id) as evidence_count
|
|
18
|
+
FROM transient_pattern_suggestions s
|
|
19
|
+
LEFT JOIN pattern_match_evidence e ON e.suggestion_id = s.id
|
|
20
|
+
WHERE s.status = 'shadow'
|
|
21
|
+
AND s.match_count >= 3
|
|
22
|
+
AND s.created_at < datetime('now', '-3 days')
|
|
23
|
+
GROUP BY s.id
|
|
24
|
+
ORDER BY s.match_count DESC
|
|
25
|
+
LIMIT 20`
|
|
26
|
+
)
|
|
27
|
+
.all();
|
|
28
|
+
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({ patterns: patterns.results ?? [], count: patterns.results?.length ?? 0 }),
|
|
31
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
return new Response(JSON.stringify({ patterns: [], count: 0 }), {
|
|
35
|
+
status: 500,
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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({ byStatus: {}, total: 0 }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const counts = await db
|
|
14
|
+
.prepare(
|
|
15
|
+
`SELECT status, COUNT(*) as count
|
|
16
|
+
FROM transient_pattern_suggestions
|
|
17
|
+
GROUP BY status
|
|
18
|
+
LIMIT 10`
|
|
19
|
+
)
|
|
20
|
+
.all<{ status: string; count: number }>();
|
|
21
|
+
|
|
22
|
+
const byStatus: Record<string, number> = {};
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const row of counts.results ?? []) {
|
|
25
|
+
byStatus[row.status] = row.count;
|
|
26
|
+
total += row.count;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({ byStatus, total }),
|
|
31
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
return new Response(JSON.stringify({ byStatus: {}, total: 0 }), {
|
|
35
|
+
status: 500,
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const status = url.searchParams.get('status') ?? 'pending';
|
|
13
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '50'), 100);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const suggestions = await db
|
|
17
|
+
.prepare(
|
|
18
|
+
`SELECT id, pattern_type, pattern_value, error_type, priority, status,
|
|
19
|
+
match_count, source, created_at, reviewed_at, reviewer_notes, is_protected
|
|
20
|
+
FROM transient_pattern_suggestions
|
|
21
|
+
WHERE status = ?
|
|
22
|
+
ORDER BY match_count DESC, created_at DESC
|
|
23
|
+
LIMIT ?`
|
|
24
|
+
)
|
|
25
|
+
.bind(status, limit)
|
|
26
|
+
.all();
|
|
27
|
+
|
|
28
|
+
const total = await db
|
|
29
|
+
.prepare(`SELECT COUNT(*) as count FROM transient_pattern_suggestions WHERE status = ? LIMIT 1`)
|
|
30
|
+
.bind(status)
|
|
31
|
+
.first<{ count: number }>();
|
|
32
|
+
|
|
33
|
+
return new Response(
|
|
34
|
+
JSON.stringify({ suggestions: suggestions.results ?? [], total: total?.count ?? 0 }),
|
|
35
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
|
|
39
|
+
status: 500,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ reports: [] }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const project = url.searchParams.get('project');
|
|
13
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 50);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const query = project
|
|
17
|
+
? `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
|
|
18
|
+
cost_protection_score, security_score, scan_date, focused_dimensions
|
|
19
|
+
FROM audit_results
|
|
20
|
+
WHERE project = ?
|
|
21
|
+
ORDER BY scan_date DESC
|
|
22
|
+
LIMIT ?`
|
|
23
|
+
: `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
|
|
24
|
+
cost_protection_score, security_score, scan_date, focused_dimensions
|
|
25
|
+
FROM audit_results
|
|
26
|
+
ORDER BY scan_date DESC
|
|
27
|
+
LIMIT ?`;
|
|
28
|
+
|
|
29
|
+
const stmt = project
|
|
30
|
+
? db.prepare(query).bind(project, limit)
|
|
31
|
+
: db.prepare(query).bind(limit);
|
|
32
|
+
|
|
33
|
+
const reports = await stmt.all();
|
|
34
|
+
|
|
35
|
+
return new Response(
|
|
36
|
+
JSON.stringify({ reports: reports.results ?? [] }),
|
|
37
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' } }
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return new Response(JSON.stringify({ reports: [] }), {
|
|
41
|
+
status: 500,
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ daily: [], monthly: [] }), {
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const days = Math.min(parseInt(url.searchParams.get('days') ?? '30'), 90);
|
|
13
|
+
const startDate = new Date();
|
|
14
|
+
startDate.setDate(startDate.getDate() - days);
|
|
15
|
+
const startStr = startDate.toISOString().slice(0, 10);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const [daily, monthly] = await Promise.all([
|
|
19
|
+
db
|
|
20
|
+
.prepare(
|
|
21
|
+
`SELECT snapshot_date, d1_reads, d1_writes, kv_reads, kv_writes,
|
|
22
|
+
r2_reads, r2_writes, worker_requests, total_cost_usd
|
|
23
|
+
FROM daily_usage_rollups
|
|
24
|
+
WHERE project = 'all' AND snapshot_date >= ?
|
|
25
|
+
ORDER BY snapshot_date ASC
|
|
26
|
+
LIMIT 90`
|
|
27
|
+
)
|
|
28
|
+
.bind(startStr)
|
|
29
|
+
.all(),
|
|
30
|
+
db
|
|
31
|
+
.prepare(
|
|
32
|
+
`SELECT snapshot_month, d1_reads, d1_writes, kv_reads, kv_writes,
|
|
33
|
+
r2_reads, r2_writes, worker_requests, total_cost_usd
|
|
34
|
+
FROM monthly_usage_rollups
|
|
35
|
+
WHERE project = 'all'
|
|
36
|
+
ORDER BY snapshot_month DESC
|
|
37
|
+
LIMIT 12`
|
|
38
|
+
)
|
|
39
|
+
.all(),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
return new Response(
|
|
43
|
+
JSON.stringify({ daily: daily.results ?? [], monthly: monthly.results ?? [] }),
|
|
44
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' } }
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
return new Response(JSON.stringify({ daily: [], monthly: [] }), {
|
|
48
|
+
status: 500,
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|