@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.
Files changed (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Infrastructure API Client
3
+ * Client functions for fetching infrastructure data
4
+ */
5
+
6
+ import type {
7
+ Service,
8
+ ServiceRegistryStats,
9
+ UptimeMonitor,
10
+ HealthcheckJob,
11
+ Alert,
12
+ InfrastructureStats,
13
+ FlipEvent,
14
+ ResponseTimeDataPoint,
15
+ ResponseTimeStats,
16
+ } from './types';
17
+
18
+ /**
19
+ * Fetch all services from the service registry
20
+ */
21
+ export async function fetchServices(): Promise<Service[]> {
22
+ const response = await fetch('/api/infrastructure/services');
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to fetch services: ${response.statusText}`);
25
+ }
26
+ const data = (await response.json()) as { services?: Service[] };
27
+ return data.services || [];
28
+ }
29
+
30
+ /**
31
+ * Fetch service registry statistics
32
+ */
33
+ export async function fetchServiceStats(): Promise<ServiceRegistryStats> {
34
+ const response = await fetch('/api/infrastructure/services/stats');
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to fetch service stats: ${response.statusText}`);
37
+ }
38
+ return response.json();
39
+ }
40
+
41
+ /**
42
+ * Fetch Gatus uptime monitors
43
+ */
44
+ export async function fetchUptimeMonitors(): Promise<UptimeMonitor[]> {
45
+ const response = await fetch('/api/infrastructure/uptime');
46
+ if (!response.ok) {
47
+ throw new Error(`Failed to fetch uptime monitors: ${response.statusText}`);
48
+ }
49
+ const data = (await response.json()) as { monitors?: UptimeMonitor[] };
50
+ return data.monitors || [];
51
+ }
52
+
53
+ /**
54
+ * Fetch Gatus heartbeats
55
+ */
56
+ export async function fetchHealthchecks(): Promise<HealthcheckJob[]> {
57
+ const response = await fetch('/api/infrastructure/healthchecks');
58
+ if (!response.ok) {
59
+ throw new Error(`Failed to fetch healthchecks: ${response.statusText}`);
60
+ }
61
+ const data = (await response.json()) as { checks?: HealthcheckJob[] };
62
+ return data.checks || [];
63
+ }
64
+
65
+ /**
66
+ * Flip history response from the API
67
+ */
68
+ export interface FlipsResponse {
69
+ flips: FlipEvent[];
70
+ checkId: string;
71
+ cached: boolean;
72
+ flipsToday: number;
73
+ lastFailure: number | null;
74
+ }
75
+
76
+ /**
77
+ * Fetch flip history (status changes) for a specific healthcheck
78
+ */
79
+ export async function fetchHealthcheckFlips(checkId: string): Promise<FlipsResponse> {
80
+ const response = await fetch(`/api/infrastructure/healthchecks/${checkId}/flips`);
81
+ if (!response.ok) {
82
+ throw new Error(`Failed to fetch flips: ${response.statusText}`);
83
+ }
84
+ return response.json();
85
+ }
86
+
87
+ /**
88
+ * Fetch recent alerts from D1
89
+ */
90
+ export async function fetchAlerts(limit: number = 20): Promise<Alert[]> {
91
+ const response = await fetch(`/api/infrastructure/alerts?limit=${limit}`);
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to fetch alerts: ${response.statusText}`);
94
+ }
95
+ const data = (await response.json()) as { alerts?: Alert[] };
96
+ return data.alerts || [];
97
+ }
98
+
99
+ /**
100
+ * Acknowledge an alert
101
+ */
102
+ export async function acknowledgeAlert(id: string): Promise<void> {
103
+ const response = await fetch(`/api/infrastructure/alerts/${id}/acknowledge`, {
104
+ method: 'POST',
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(`Failed to acknowledge alert: ${response.statusText}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Fetch combined infrastructure statistics
113
+ */
114
+ export async function fetchInfrastructureStats(): Promise<InfrastructureStats> {
115
+ const response = await fetch('/api/infrastructure/stats');
116
+ if (!response.ok) {
117
+ throw new Error(`Failed to fetch infrastructure stats: ${response.statusText}`);
118
+ }
119
+ return response.json();
120
+ }
121
+
122
+ /**
123
+ * Response times response from the API
124
+ */
125
+ export interface ResponseTimesResponse {
126
+ dataPoints: ResponseTimeDataPoint[];
127
+ monitorId: string;
128
+ cached: boolean;
129
+ stats: ResponseTimeStats;
130
+ }
131
+
132
+ /**
133
+ * Fetch response time history for a specific Gatus monitor
134
+ */
135
+ export async function fetchUptimeResponseTimes(monitorId: string): Promise<ResponseTimesResponse> {
136
+ const response = await fetch(`/api/infrastructure/uptime/${monitorId}/response-times`);
137
+ if (!response.ok) {
138
+ throw new Error(`Failed to fetch response times: ${response.statusText}`);
139
+ }
140
+ return response.json();
141
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Gatus API Client
3
+ * Shared fetch + KV cache layer for Gatus self-hosted monitoring
4
+ * https://{{gatusUrl}}
5
+ */
6
+
7
+ /** A single check result from Gatus */
8
+ export interface GatusResult {
9
+ status: number;
10
+ hostname: string;
11
+ duration: number; // nanoseconds
12
+ success: boolean;
13
+ timestamp: string; // ISO 8601
14
+ errors: string[];
15
+ conditionResults: Array<{
16
+ condition: string;
17
+ success: boolean;
18
+ }>;
19
+ }
20
+
21
+ /** A status transition event (only on single-endpoint responses) */
22
+ export interface GatusEvent {
23
+ type: 'START' | 'HEALTHY' | 'UNHEALTHY';
24
+ timestamp: string; // ISO 8601
25
+ }
26
+
27
+ /**
28
+ * A single endpoint status from Gatus.
29
+ * Bulk endpoint (/statuses) returns only results.
30
+ * Single endpoint (/{key}/statuses) also includes events.
31
+ */
32
+ export interface GatusEndpointStatus {
33
+ name: string;
34
+ group: string;
35
+ key: string;
36
+ results: GatusResult[];
37
+ events?: GatusEvent[]; // Only present on single-endpoint responses
38
+ }
39
+
40
+ /** Calculate uptime % from results (success count / total count * 100) */
41
+ export function calculateUptimeFromResults(results: GatusResult[]): number {
42
+ if (results.length === 0) return 0;
43
+ const successes = results.filter((r) => r.success).length;
44
+ return (successes / results.length) * 100;
45
+ }
46
+
47
+ const CACHE_KEY = 'GATUS_ALL_STATUSES';
48
+ const CACHE_TTL = 300; // 5 minutes
49
+
50
+ /**
51
+ * Fetch all endpoint statuses from Gatus, with KV caching.
52
+ * CF Access is bypassed on /api/* so no auth is needed.
53
+ */
54
+ export async function fetchAllGatusStatuses(
55
+ gatusUrl: string,
56
+ kv?: KVNamespace
57
+ ): Promise<GatusEndpointStatus[]> {
58
+ // Check cache first
59
+ if (kv) {
60
+ const cached = await kv.get(CACHE_KEY, 'json');
61
+ if (cached && Array.isArray(cached)) {
62
+ return cached as GatusEndpointStatus[];
63
+ }
64
+ }
65
+
66
+ const response = await fetch(`${gatusUrl}/api/v1/endpoints/statuses`);
67
+ if (!response.ok) {
68
+ throw new Error(`Gatus API error: ${response.status}`);
69
+ }
70
+
71
+ const data = (await response.json()) as GatusEndpointStatus[];
72
+
73
+ // Cache the result
74
+ if (kv) {
75
+ await kv.put(CACHE_KEY, JSON.stringify(data), { expirationTtl: CACHE_TTL });
76
+ }
77
+
78
+ return data;
79
+ }
80
+
81
+ /**
82
+ * Fetch a single endpoint's status from Gatus, with KV caching.
83
+ */
84
+ export async function fetchGatusEndpoint(
85
+ gatusUrl: string,
86
+ key: string,
87
+ kv?: KVNamespace
88
+ ): Promise<GatusEndpointStatus> {
89
+ const cacheKey = `GATUS_ENDPOINT:${key}`;
90
+
91
+ // Check cache first
92
+ if (kv) {
93
+ const cached = await kv.get(cacheKey, 'json');
94
+ if (cached) {
95
+ return cached as GatusEndpointStatus;
96
+ }
97
+ }
98
+
99
+ const response = await fetch(`${gatusUrl}/api/v1/endpoints/${key}/statuses`);
100
+ if (!response.ok) {
101
+ throw new Error(`Gatus API error for ${key}: ${response.status}`);
102
+ }
103
+
104
+ const data = (await response.json()) as GatusEndpointStatus;
105
+
106
+ // Cache the result
107
+ if (kv) {
108
+ await kv.put(cacheKey, JSON.stringify(data), { expirationTtl: CACHE_TTL });
109
+ }
110
+
111
+ return data;
112
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Service Binding Proxy Module
3
+ *
4
+ * Exports utilities for proxying requests from Pages Functions to Workers
5
+ * via Cloudflare Service Bindings.
6
+ *
7
+ * @module services/proxy
8
+ * @created 2025-12-04
9
+ * @task task-159 - Implement thin proxy pattern for Brand Copilot Worker APIs
10
+ */
11
+
12
+ export { proxyToService, proxyToServiceWithMetrics, createErrorResponse } from './proxy';
13
+
14
+ export type {
15
+ Fetcher,
16
+ ProxyOptions,
17
+ ProxyErrorResponse,
18
+ ProxyErrorCode,
19
+ ProxyResult,
20
+ } from './types';
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Service Binding Proxy Utility
3
+ *
4
+ * Generic proxy function for forwarding requests from Pages Functions
5
+ * to Workers via service bindings. Handles error handling, retries,
6
+ * and observability.
7
+ *
8
+ * @module services/proxy/proxy
9
+ * @created 2025-12-04
10
+ * @task task-159 - Implement thin proxy pattern for Brand Copilot Worker APIs
11
+ */
12
+
13
+ import type {
14
+ Fetcher,
15
+ ProxyOptions,
16
+ ProxyErrorResponse,
17
+ ProxyErrorCode,
18
+ ProxyResult,
19
+ } from './types';
20
+
21
+ /**
22
+ * Default proxy options
23
+ */
24
+ const DEFAULT_OPTIONS: Required<Omit<ProxyOptions, 'onError' | 'requestId'>> = {
25
+ retries: 1,
26
+ retryDelay: 100,
27
+ logTiming: true,
28
+ };
29
+
30
+ /**
31
+ * Proxy a request to a Worker via service binding
32
+ *
33
+ * @param service - The service binding (Fetcher) to call
34
+ * @param request - The incoming request to proxy
35
+ * @param options - Optional configuration
36
+ * @returns The response from the Worker
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const response = await proxyToService(
41
+ * env.BRAND_COPILOT,
42
+ * request,
43
+ * { requestId: 'abc-123' }
44
+ * );
45
+ * ```
46
+ */
47
+ export async function proxyToService(
48
+ service: Fetcher,
49
+ request: Request,
50
+ options?: ProxyOptions
51
+ ): Promise<Response> {
52
+ const result = await proxyToServiceWithMetrics(service, request, options);
53
+ return result.response;
54
+ }
55
+
56
+ /**
57
+ * Proxy a request to a Worker with full metrics
58
+ *
59
+ * @param service - The service binding (Fetcher) to call
60
+ * @param request - The incoming request to proxy
61
+ * @param options - Optional configuration
62
+ * @returns ProxyResult with response and metrics
63
+ */
64
+ export async function proxyToServiceWithMetrics(
65
+ service: Fetcher,
66
+ request: Request,
67
+ options?: ProxyOptions
68
+ ): Promise<ProxyResult> {
69
+ const startTime = performance.now();
70
+ const opts = { ...DEFAULT_OPTIONS, ...options };
71
+ const requestId = opts.requestId ?? generateRequestId();
72
+
73
+ let lastError: Error | null = null;
74
+ let retriesUsed = 0;
75
+
76
+ // Create proxy request with filtered headers
77
+ const proxyRequest = createProxyRequest(request, requestId);
78
+
79
+ for (let attempt = 0; attempt <= opts.retries; attempt++) {
80
+ try {
81
+ const response = await service.fetch(proxyRequest.clone());
82
+ const duration = performance.now() - startTime;
83
+
84
+ if (opts.logTiming) {
85
+ logProxyRequest(request, response, duration, requestId, retriesUsed);
86
+ }
87
+
88
+ return {
89
+ response,
90
+ duration_ms: Math.round(duration),
91
+ request_id: requestId,
92
+ retries_used: retriesUsed,
93
+ };
94
+ } catch (error) {
95
+ lastError = error as Error;
96
+ retriesUsed = attempt + 1;
97
+
98
+ if (attempt < opts.retries) {
99
+ await sleep(opts.retryDelay);
100
+ }
101
+ }
102
+ }
103
+
104
+ // All retries exhausted
105
+ const duration = performance.now() - startTime;
106
+
107
+ if (opts.logTiming) {
108
+ console.error(
109
+ `[PROXY] ${request.method} ${new URL(request.url).pathname} → ERROR after ${retriesUsed} attempts (${Math.round(duration)}ms): ${lastError?.message}`
110
+ );
111
+ }
112
+
113
+ // Allow custom error handling
114
+ if (options?.onError) {
115
+ const customResponse = options.onError(lastError!);
116
+ if (customResponse) {
117
+ return {
118
+ response: customResponse,
119
+ duration_ms: Math.round(duration),
120
+ request_id: requestId,
121
+ retries_used: retriesUsed,
122
+ };
123
+ }
124
+ }
125
+
126
+ // Default error response
127
+ const errorResponse = createErrorResponse(
128
+ 'SERVICE_UNAVAILABLE',
129
+ 'The backend service is temporarily unavailable',
130
+ requestId,
131
+ lastError?.message
132
+ );
133
+
134
+ return {
135
+ response: errorResponse,
136
+ duration_ms: Math.round(duration),
137
+ request_id: requestId,
138
+ retries_used: retriesUsed,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Create a proxy request with filtered headers
144
+ *
145
+ * Service bindings ignore the hostname, so we use a dummy base URL
146
+ * to satisfy the Request constructor requirement for absolute URLs.
147
+ * We preserve the original host via X-Forwarded-* headers for OAuth flows.
148
+ */
149
+ function createProxyRequest(request: Request, requestId: string): Request {
150
+ const url = new URL(request.url);
151
+ const path = url.pathname + url.search;
152
+
153
+ // Service bindings ignore hostname - use dummy base for valid URL
154
+ const proxyUrl = new URL(path, 'https://service.internal');
155
+
156
+ // Filter headers - remove Cloudflare-specific ones
157
+ const headers = new Headers();
158
+ request.headers.forEach((value, key) => {
159
+ // Skip headers that shouldn't be proxied
160
+ if (
161
+ !key.startsWith('cf-') &&
162
+ key !== 'host' &&
163
+ key !== 'x-forwarded-for' &&
164
+ key !== 'x-real-ip'
165
+ ) {
166
+ headers.set(key, value);
167
+ }
168
+ });
169
+
170
+ // Add request ID for correlation
171
+ headers.set('x-request-id', requestId);
172
+
173
+ // Preserve original host/protocol for OAuth callbacks and URL construction
174
+ // These headers allow the Worker to know the original request origin
175
+ headers.set('x-forwarded-host', url.host);
176
+ headers.set('x-forwarded-proto', url.protocol.replace(':', ''));
177
+
178
+ return new Request(proxyUrl.toString(), {
179
+ method: request.method,
180
+ headers,
181
+ body: request.body,
182
+ // Don't follow redirects - return them to the browser for OAuth flows
183
+ redirect: 'manual',
184
+ // @ts-expect-error - duplex is required for streaming bodies
185
+ duplex: 'half',
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Create a standardised error response
191
+ */
192
+ export function createErrorResponse(
193
+ code: ProxyErrorCode,
194
+ message: string,
195
+ requestId?: string,
196
+ details?: string
197
+ ): Response {
198
+ const body: ProxyErrorResponse = {
199
+ success: false,
200
+ error: message,
201
+ code,
202
+ ...(requestId && { request_id: requestId }),
203
+ ...(details && { details }),
204
+ };
205
+
206
+ return new Response(JSON.stringify(body), {
207
+ status: code === 'TIMEOUT' ? 504 : 503,
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ ...(requestId && { 'x-request-id': requestId }),
211
+ },
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Generate a unique request ID
217
+ */
218
+ function generateRequestId(): string {
219
+ return crypto.randomUUID();
220
+ }
221
+
222
+ /**
223
+ * Log a proxy request
224
+ */
225
+ function logProxyRequest(
226
+ request: Request,
227
+ response: Response,
228
+ durationMs: number,
229
+ requestId: string,
230
+ retries: number
231
+ ): void {
232
+ const url = new URL(request.url);
233
+ const retryInfo = retries > 0 ? ` (${retries} retries)` : '';
234
+ console.log(
235
+ `[PROXY] ${request.method} ${url.pathname} → ${response.status} (${Math.round(durationMs)}ms)${retryInfo} [${requestId.slice(0, 8)}]`
236
+ );
237
+ }
238
+
239
+ /**
240
+ * Sleep for a given number of milliseconds
241
+ */
242
+ function sleep(ms: number): Promise<void> {
243
+ return new Promise((resolve) => setTimeout(resolve, ms));
244
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Service Binding Proxy Types
3
+ *
4
+ * Type definitions for service binding proxy utilities.
5
+ * Used for proxying requests from Pages Functions to Workers.
6
+ *
7
+ * @module services/proxy/types
8
+ * @created 2025-12-04
9
+ * @task task-159 - Implement thin proxy pattern for Brand Copilot Worker APIs
10
+ */
11
+
12
+ /**
13
+ * Cloudflare Service Binding Fetcher interface
14
+ * Represents a service binding that can be used to call another Worker
15
+ */
16
+ export interface Fetcher {
17
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
18
+ }
19
+
20
+ /**
21
+ * Options for the proxy function
22
+ */
23
+ export interface ProxyOptions {
24
+ /**
25
+ * Maximum number of retries on failure
26
+ * @default 1
27
+ */
28
+ retries?: number;
29
+
30
+ /**
31
+ * Delay between retries in milliseconds
32
+ * @default 100
33
+ */
34
+ retryDelay?: number;
35
+
36
+ /**
37
+ * Custom error handler - return a Response to override default error handling
38
+ */
39
+ onError?: (error: Error) => Response | null;
40
+
41
+ /**
42
+ * Request ID for correlation (will be generated if not provided)
43
+ */
44
+ requestId?: string;
45
+
46
+ /**
47
+ * Log timing information
48
+ * @default true
49
+ */
50
+ logTiming?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Error response format from proxy
55
+ */
56
+ export interface ProxyErrorResponse {
57
+ success: false;
58
+ error: string;
59
+ code: ProxyErrorCode;
60
+ request_id?: string;
61
+ details?: string;
62
+ }
63
+
64
+ /**
65
+ * Proxy error codes
66
+ */
67
+ export type ProxyErrorCode =
68
+ | 'SERVICE_BINDING_ERROR'
69
+ | 'SERVICE_UNAVAILABLE'
70
+ | 'TIMEOUT'
71
+ | 'RETRY_EXHAUSTED';
72
+
73
+ /**
74
+ * Result of a proxy operation
75
+ */
76
+ export interface ProxyResult {
77
+ response: Response;
78
+ duration_ms: number;
79
+ request_id: string;
80
+ retries_used: number;
81
+ }
@@ -0,0 +1,64 @@
1
+ ---
2
+ import DashboardLayout from '../layouts/DashboardLayout.astro';
3
+
4
+ interface ProductMetricRow {
5
+ source: string;
6
+ metric_type: string;
7
+ value: number;
8
+ timestamp: number;
9
+ }
10
+
11
+ const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
12
+ const db = runtime?.env?.PLATFORM_DB;
13
+
14
+ const metricsByType: Record<string, number> = {};
15
+
16
+ if (db) {
17
+ const result = await db
18
+ .prepare(
19
+ `SELECT source, metric_type, value, timestamp
20
+ FROM product_metrics
21
+ WHERE source IN ('plausible', 'ga4')
22
+ ORDER BY timestamp DESC
23
+ LIMIT 100`
24
+ )
25
+ .all<ProductMetricRow>();
26
+
27
+ for (const row of result.results ?? []) {
28
+ const key = `${row.source}:${row.metric_type}`;
29
+ if (!(key in metricsByType)) {
30
+ metricsByType[key] = row.value ?? 0;
31
+ }
32
+ }
33
+ }
34
+
35
+ const pageviews = metricsByType['plausible:pageviews'] ?? 0;
36
+ const activeUsers = metricsByType['plausible:active_users'] ?? 0;
37
+ const installs = metricsByType['ga4:installs'] ?? 0;
38
+ ---
39
+
40
+ <DashboardLayout title="Analytics">
41
+ <div class="space-y-6">
42
+ <h2 class="text-3xl font-bold text-gray-900 dark:text-white">Product Analytics</h2>
43
+
44
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-3">
45
+ <div class="metric-card">
46
+ <div class="metric-title">Pageviews</div>
47
+ <div class="metric-value">{pageviews.toLocaleString()}</div>
48
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (Plausible)</div>
49
+ </div>
50
+
51
+ <div class="metric-card">
52
+ <div class="metric-title">Active Users</div>
53
+ <div class="metric-value">{activeUsers.toLocaleString()}</div>
54
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (Plausible)</div>
55
+ </div>
56
+
57
+ <div class="metric-card">
58
+ <div class="metric-title">Chrome Installs</div>
59
+ <div class="metric-value">{installs.toLocaleString()}</div>
60
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (GA4)</div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </DashboardLayout>