@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.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 +4 -7
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +206 -4
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -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/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -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/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -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/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -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/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -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/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -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/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -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/dedup.test.ts +350 -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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/patterns/:id/approve - Proxy to pattern-discovery /suggestions/:id?action=approve
|
|
3
|
+
* Approves a pending pattern suggestion
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
PATTERN_DISCOVERY_API?: Fetcher;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const POST: APIRoute = async ({ locals, params, url }) => {
|
|
12
|
+
const api = (locals.runtime?.env as Env)?.PATTERN_DISCOVERY_API;
|
|
13
|
+
|
|
14
|
+
if (!api) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Pattern Discovery API not configured' }), {
|
|
16
|
+
status: 503,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { id } = params;
|
|
22
|
+
if (!id) {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'Missing suggestion ID' }), {
|
|
24
|
+
status: 400,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const by = url.searchParams.get('by') || 'dashboard';
|
|
31
|
+
|
|
32
|
+
const response = await api.fetch(
|
|
33
|
+
`https://pattern-discovery.littlebearapps.workers.dev/suggestions/${id}?action=approve&by=${by}`,
|
|
34
|
+
{ method: 'POST' }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return new Response(JSON.stringify(data), {
|
|
39
|
+
status: response.status,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error approving suggestion:', error);
|
|
44
|
+
return new Response(JSON.stringify({ error: 'Failed to approve suggestion' }), {
|
|
45
|
+
status: 500,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/patterns/:id/reject - Proxy to pattern-discovery /suggestions/:id?action=reject
|
|
3
|
+
* Rejects a pending pattern suggestion
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
PATTERN_DISCOVERY_API?: Fetcher;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const POST: APIRoute = async ({ locals, params, url }) => {
|
|
12
|
+
const api = (locals.runtime?.env as Env)?.PATTERN_DISCOVERY_API;
|
|
13
|
+
|
|
14
|
+
if (!api) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Pattern Discovery API not configured' }), {
|
|
16
|
+
status: 503,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { id } = params;
|
|
22
|
+
if (!id) {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'Missing suggestion ID' }), {
|
|
24
|
+
status: 400,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const by = url.searchParams.get('by') || 'dashboard';
|
|
31
|
+
const reason = url.searchParams.get('reason') || 'Rejected via dashboard';
|
|
32
|
+
|
|
33
|
+
const response = await api.fetch(
|
|
34
|
+
`https://pattern-discovery.littlebearapps.workers.dev/suggestions/${id}?action=reject&by=${by}&reason=${encodeURIComponent(reason)}`,
|
|
35
|
+
{ method: 'POST' }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
return new Response(JSON.stringify(data), {
|
|
40
|
+
status: response.status,
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error rejecting suggestion:', error);
|
|
45
|
+
return new Response(JSON.stringify({ error: 'Failed to reject suggestion' }), {
|
|
46
|
+
status: 500,
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/reports/digests/stats - Proxy to error-collector /digests/stats
|
|
3
|
+
*/
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
|
|
6
|
+
interface Env {
|
|
7
|
+
ERROR_COLLECTOR_API?: Fetcher;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
11
|
+
const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
|
|
12
|
+
|
|
13
|
+
if (!api) {
|
|
14
|
+
return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
|
|
15
|
+
status: 503,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await api.fetch(
|
|
22
|
+
'https://error-collector.littlebearapps.workers.dev/digests/stats',
|
|
23
|
+
{ method: 'GET' }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return new Response(JSON.stringify(data), {
|
|
28
|
+
status: response.status,
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error fetching digest stats:', error);
|
|
33
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch digest stats' }), {
|
|
34
|
+
status: 500,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/reports/digests - Proxy to error-collector /digests
|
|
3
|
+
*/
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
|
|
6
|
+
interface Env {
|
|
7
|
+
ERROR_COLLECTOR_API?: Fetcher;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
11
|
+
const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
|
|
12
|
+
|
|
13
|
+
if (!api) {
|
|
14
|
+
return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
|
|
15
|
+
status: 503,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(request.url);
|
|
22
|
+
const response = await api.fetch(
|
|
23
|
+
`https://error-collector.littlebearapps.workers.dev/digests${url.search}`,
|
|
24
|
+
{ method: 'GET' }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return new Response(JSON.stringify(data), {
|
|
29
|
+
status: response.status,
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error fetching digests:', error);
|
|
34
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch digests' }), {
|
|
35
|
+
status: 500,
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reindex API Proxy
|
|
3
|
+
*
|
|
4
|
+
* Proxies reindex requests to platform-search worker.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/pages/api/search/reindex
|
|
7
|
+
* @created 2026-02-03
|
|
8
|
+
* @task task-303.3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { APIRoute } from 'astro';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/search/reindex/:type - Reindex all documents of a content type
|
|
15
|
+
*/
|
|
16
|
+
export const POST: APIRoute = async ({ params, locals }) => {
|
|
17
|
+
const api = (locals.runtime?.env as { SEARCH_API?: Fetcher })?.SEARCH_API;
|
|
18
|
+
const { type } = params;
|
|
19
|
+
|
|
20
|
+
if (!api) {
|
|
21
|
+
return new Response(JSON.stringify({ error: 'Search API not available' }), {
|
|
22
|
+
status: 503,
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!type) {
|
|
28
|
+
return new Response(JSON.stringify({ error: 'Content type required' }), {
|
|
29
|
+
status: 400,
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await api.fetch(
|
|
36
|
+
new Request(
|
|
37
|
+
`https://platform-search.littlebearapps.workers.dev/search/reindex/${encodeURIComponent(type)}`,
|
|
38
|
+
{
|
|
39
|
+
method: 'POST',
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return new Response(JSON.stringify(data), {
|
|
46
|
+
status: response.status,
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Reindex error:', error);
|
|
51
|
+
return new Response(JSON.stringify({ error: 'Failed to reindex content type' }), {
|
|
52
|
+
status: 500,
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Reports API Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Serves HTML test reports from KV storage
|
|
5
|
+
* Reports are stored by platform-ingest-tester worker
|
|
6
|
+
* TTL: 7 days (passes) or 90 days (failures)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const GET = async ({ params, locals }: any) => {
|
|
10
|
+
const { id: runId } = params;
|
|
11
|
+
const env = locals.runtime.env;
|
|
12
|
+
|
|
13
|
+
if (!runId) {
|
|
14
|
+
return new Response(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
error: 'Missing run ID',
|
|
17
|
+
message: 'Run ID is required',
|
|
18
|
+
}),
|
|
19
|
+
{
|
|
20
|
+
status: 400,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// First, get the run from D1 to find the KV key
|
|
28
|
+
const run = await env.PLATFORM_DB.prepare(
|
|
29
|
+
`SELECT id, project, kv_report_key, timestamp, pass_rate
|
|
30
|
+
FROM runs
|
|
31
|
+
WHERE id = ?`
|
|
32
|
+
)
|
|
33
|
+
.bind(runId)
|
|
34
|
+
.first();
|
|
35
|
+
|
|
36
|
+
if (!run) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
error: 'Run not found',
|
|
40
|
+
message: `No test run found with ID: ${runId}`,
|
|
41
|
+
}),
|
|
42
|
+
{
|
|
43
|
+
status: 404,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!run.kv_report_key) {
|
|
50
|
+
return new Response(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
error: 'Report not available',
|
|
53
|
+
message: 'HTML report was not uploaded for this run',
|
|
54
|
+
}),
|
|
55
|
+
{
|
|
56
|
+
status: 404,
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fetch HTML report from KV
|
|
63
|
+
const htmlReport = await env.PLATFORM_CACHE.get(run.kv_report_key);
|
|
64
|
+
|
|
65
|
+
if (!htmlReport) {
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
error: 'Report expired',
|
|
69
|
+
message: 'HTML report has expired or been deleted (TTL: 7d for passes, 90d for failures)',
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
status: 410, // Gone
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Return HTML report
|
|
79
|
+
return new Response(htmlReport, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
82
|
+
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
|
|
83
|
+
'X-Run-ID': runId,
|
|
84
|
+
'X-Project': run.project as string,
|
|
85
|
+
'X-Timestamp': run.timestamp as string,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error('Failed to fetch test report:', error);
|
|
90
|
+
|
|
91
|
+
return new Response(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
error: 'Internal server error',
|
|
94
|
+
message: error.message,
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
status: 500,
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
};
|