@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.
Files changed (157) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +197 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  6. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  7. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  8. package/templates/full/dashboard/src/components/patterns/index.ts +3 -0
  9. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  10. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  11. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  12. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  13. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  14. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  15. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  18. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  19. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  20. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  21. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  22. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  23. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  24. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  25. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  26. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  27. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  28. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  29. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  30. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  31. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  32. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  33. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  34. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  35. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  36. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  37. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  38. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  39. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  40. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  41. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  42. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  43. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  44. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  45. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  46. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  47. package/templates/shared/.github/workflows/security.yml +33 -0
  48. package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -0
  49. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  50. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  51. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  52. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  53. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  54. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  55. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  56. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  57. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  58. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  59. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  60. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  61. package/templates/shared/dashboard/src/components/ui/index.ts +6 -0
  62. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  63. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  67. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  68. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  69. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  70. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  71. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  72. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  73. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  74. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  75. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  76. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  77. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  78. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  79. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  80. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  81. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  82. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  83. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  84. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  85. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  86. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  87. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  88. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  89. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  90. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  91. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  92. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  93. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  94. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  95. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  96. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  97. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  99. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  100. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  101. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  102. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  103. package/templates/shared/docs/architecture.md +89 -0
  104. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  105. package/templates/shared/docs/troubleshooting.md +91 -0
  106. package/templates/shared/package.json.hbs +5 -0
  107. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  108. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  109. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  110. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  111. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  112. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  113. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  114. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  115. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  116. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  117. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  118. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  119. package/templates/shared/tests/unit/billing.test.ts +331 -0
  120. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  121. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  122. package/templates/shared/tests/unit/control.test.ts +226 -0
  123. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  124. package/templates/shared/tests/unit/economics.test.ts +365 -0
  125. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  126. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  127. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  128. package/templates/shared/vitest.config.ts +18 -0
  129. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  130. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  131. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  132. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  133. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  134. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  135. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  136. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  137. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  138. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  139. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  140. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  141. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  142. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  143. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  144. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  145. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  146. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  147. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  148. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  149. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  150. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  151. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  152. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  153. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  154. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  155. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  156. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  157. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Unit Tests for Budget Enforcement
3
+ *
4
+ * Covers:
5
+ * - Feature-level budget warnings at 70%/90% thresholds
6
+ * - Circuit breaker trip at 100%
7
+ * - Monthly budget tracking
8
+ * - Dedup via KV BUDGET_WARN: prefix
9
+ */
10
+
11
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
12
+ import { createMockKV } from '../../helpers/mock-kv';
13
+ import { MockD1Database } from '../../helpers/mock-d1';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Module mocks — must be before imports that resolve these modules
17
+ // ---------------------------------------------------------------------------
18
+
19
+ vi.mock('@littlebearapps/platform-consumer-sdk', async (importOriginal) => {
20
+ const actual = await importOriginal();
21
+ return {
22
+ ...actual,
23
+ createLoggerFromEnv: () => ({
24
+ info: vi.fn(),
25
+ warn: vi.fn(),
26
+ error: vi.fn(),
27
+ debug: vi.fn(),
28
+ }),
29
+ };
30
+ });
31
+
32
+ vi.mock('../../../workers/lib/circuit-breaker-middleware', () => ({
33
+ CB_STATUS: { CLOSED: 'active', WARNING: 'warning', OPEN: 'paused' },
34
+ }));
35
+
36
+ vi.mock('../../../workers/lib/platform-settings', () => ({
37
+ getPlatformSettings: vi.fn().mockResolvedValue({}),
38
+ getProjectSetting: vi.fn().mockResolvedValue(null),
39
+ DEFAULT_PLATFORM_SETTINGS: {},
40
+ }));
41
+
42
+ const mockFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }));
43
+
44
+ import {
45
+ checkAndUpdateBudgetStatus,
46
+ checkMonthlyBudgets,
47
+ } from '../../../workers/lib/usage/queue/budget-enforcement';
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Helpers
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function createTestEnv(kvData: Record<string, string> = {}, slackUrl?: string) {
54
+ const db = new MockD1Database();
55
+ db.queueRunResult({ success: true });
56
+ db.queueRunResult({ success: true });
57
+
58
+ return {
59
+ env: {
60
+ PLATFORM_CACHE: createMockKV(kvData),
61
+ PLATFORM_DB: db,
62
+ SLACK_WEBHOOK_URL: slackUrl,
63
+ NOTIFICATIONS_API: undefined,
64
+ PLATFORM_TELEMETRY: undefined,
65
+ TELEMETRY_QUEUE: undefined,
66
+ } as unknown,
67
+ db,
68
+ };
69
+ }
70
+
71
+ // ===========================================================================
72
+ // Section A: Feature-Level Budget Warnings
73
+ // ===========================================================================
74
+
75
+ describe('checkAndUpdateBudgetStatus', () => {
76
+ beforeEach(() => {
77
+ vi.stubGlobal('fetch', mockFetch);
78
+ mockFetch.mockClear();
79
+ });
80
+
81
+ afterEach(() => {
82
+ vi.unstubAllGlobals();
83
+ });
84
+
85
+ it('fires 70% warning when metric is between 70-89% of budget', async () => {
86
+ const { env } = createTestEnv(
87
+ {
88
+ 'CONFIG:FEATURE:test:api:endpoint:BUDGET': JSON.stringify({
89
+ d1_rows_written: 100_000,
90
+ }),
91
+ },
92
+ 'https://hooks.slack.com/test'
93
+ );
94
+
95
+ await checkAndUpdateBudgetStatus(
96
+ 'test:api:endpoint',
97
+ { d1RowsWritten: 72_000 } as never,
98
+ env as never
99
+ );
100
+
101
+ const kv = (env as Record<string, unknown>).PLATFORM_CACHE as ReturnType<typeof createMockKV>;
102
+ expect(kv.put).toHaveBeenCalledWith(
103
+ 'BUDGET_WARN:test:api:endpoint:d1RowsWritten:70',
104
+ '1',
105
+ { expirationTtl: 3600 }
106
+ );
107
+ });
108
+
109
+ it('fires 90% critical warning when metric >= 90% of budget', async () => {
110
+ const { env } = createTestEnv(
111
+ {
112
+ 'CONFIG:FEATURE:test:api:endpoint:BUDGET': JSON.stringify({
113
+ d1_rows_written: 100_000,
114
+ }),
115
+ },
116
+ 'https://hooks.slack.com/test'
117
+ );
118
+
119
+ await checkAndUpdateBudgetStatus(
120
+ 'test:api:endpoint',
121
+ { d1RowsWritten: 92_000 } as never,
122
+ env as never
123
+ );
124
+
125
+ const kv = (env as Record<string, unknown>).PLATFORM_CACHE as ReturnType<typeof createMockKV>;
126
+ expect(kv.put).toHaveBeenCalledWith(
127
+ 'BUDGET_WARN:test:api:endpoint:d1RowsWritten:90',
128
+ '1',
129
+ { expirationTtl: 3600 }
130
+ );
131
+ });
132
+
133
+ it('does not fire warning when metric is below 70%', async () => {
134
+ const { env } = createTestEnv(
135
+ {
136
+ 'CONFIG:FEATURE:test:api:endpoint:BUDGET': JSON.stringify({
137
+ d1_rows_written: 100_000,
138
+ }),
139
+ },
140
+ 'https://hooks.slack.com/test'
141
+ );
142
+
143
+ await checkAndUpdateBudgetStatus(
144
+ 'test:api:endpoint',
145
+ { d1RowsWritten: 50_000 } as never,
146
+ env as never
147
+ );
148
+
149
+ expect(mockFetch).not.toHaveBeenCalled();
150
+ });
151
+
152
+ it('skips warning when dedup key already exists', async () => {
153
+ const { env } = createTestEnv(
154
+ {
155
+ 'CONFIG:FEATURE:test:api:endpoint:BUDGET': JSON.stringify({
156
+ d1_rows_written: 100_000,
157
+ }),
158
+ 'BUDGET_WARN:test:api:endpoint:d1RowsWritten:70': '1',
159
+ },
160
+ 'https://hooks.slack.com/test'
161
+ );
162
+
163
+ await checkAndUpdateBudgetStatus(
164
+ 'test:api:endpoint',
165
+ { d1RowsWritten: 75_000 } as never,
166
+ env as never
167
+ );
168
+
169
+ expect(mockFetch).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('does nothing when no budget config exists', async () => {
173
+ const { env } = createTestEnv({}, 'https://hooks.slack.com/test');
174
+
175
+ await checkAndUpdateBudgetStatus(
176
+ 'test:api:endpoint',
177
+ { d1RowsWritten: 99_000 } as never,
178
+ env as never
179
+ );
180
+
181
+ expect(mockFetch).not.toHaveBeenCalled();
182
+ });
183
+ });
184
+
185
+ // ===========================================================================
186
+ // Section B: Monthly Budget Tracking
187
+ // ===========================================================================
188
+
189
+ describe('checkMonthlyBudgets', () => {
190
+ beforeEach(() => {
191
+ vi.stubGlobal('fetch', mockFetch);
192
+ mockFetch.mockClear();
193
+ });
194
+
195
+ afterEach(() => {
196
+ vi.unstubAllGlobals();
197
+ });
198
+
199
+ it('queries daily_usage_rollups for current month', async () => {
200
+ const { env, db } = createTestEnv({});
201
+ db.queueAllResult([]);
202
+
203
+ await checkMonthlyBudgets(env as never);
204
+
205
+ expect(db.statements.some((s: string) => s.includes('daily_usage_rollups'))).toBe(true);
206
+ });
207
+
208
+ it('handles missing tables gracefully', async () => {
209
+ const { env } = createTestEnv({});
210
+
211
+ // Should not throw even with empty DB
212
+ await expect(checkMonthlyBudgets(env as never)).resolves.not.toThrow();
213
+ });
214
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ include: ['workers/**/*.ts'],
11
+ },
12
+ },
13
+ resolve: {
14
+ alias: {
15
+ '@': './src',
16
+ },
17
+ },
18
+ });
@@ -0,0 +1,69 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { EmptyState } from '../../components/ui/EmptyState';
3
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
4
+
5
+ interface CBEvent {
6
+ id: number;
7
+ feature_key: string;
8
+ event_type: string;
9
+ old_status: string;
10
+ new_status: string;
11
+ reason: string | null;
12
+ timestamp: string;
13
+ }
14
+
15
+ export function CircuitBreakerEvents() {
16
+ const [events, setEvents] = useState<CBEvent[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ fetch('/api/usage/circuit-breakers')
21
+ .then(res => res.json())
22
+ .then((data: { events?: CBEvent[] }) => { setEvents(data.events ?? []); setLoading(false); })
23
+ .catch(() => setLoading(false));
24
+ }, []);
25
+
26
+ if (loading) return <LoadingSkeleton lines={3} />;
27
+
28
+ if (events.length === 0) {
29
+ return (
30
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
31
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
32
+ CB Event History
33
+ </h3>
34
+ <EmptyState title="No events" description="No circuit breaker state changes recorded." />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ return (
40
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
41
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
42
+ CB Event History
43
+ </h3>
44
+ <div className="space-y-2">
45
+ {events.map(ev => (
46
+ <div key={ev.id} className="flex items-start gap-3 py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
47
+ <div className={`mt-1 w-2 h-2 rounded-full shrink-0 ${
48
+ ev.new_status === 'active' ? 'bg-green-500'
49
+ : ev.new_status === 'warning' ? 'bg-yellow-500'
50
+ : 'bg-red-500'
51
+ }`} />
52
+ <div className="min-w-0">
53
+ <p className="text-sm text-gray-900 dark:text-white">
54
+ <span className="font-medium">{ev.feature_key}</span>
55
+ {' '}{ev.old_status} &rarr; {ev.new_status}
56
+ </p>
57
+ {ev.reason && (
58
+ <p className="text-xs text-gray-500 dark:text-gray-400 truncate">{ev.reason}</p>
59
+ )}
60
+ <p className="text-xs text-gray-400 dark:text-gray-500">
61
+ {new Date(ev.timestamp).toLocaleString()}
62
+ </p>
63
+ </div>
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,97 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
3
+
4
+ interface CircuitBreaker {
5
+ key: string;
6
+ feature: string;
7
+ status: string;
8
+ reason: string | null;
9
+ level: 'global' | 'project' | 'feature';
10
+ }
11
+
12
+ export function CircuitBreakerPanel() {
13
+ const [breakers, setBreakers] = useState<CircuitBreaker[]>([]);
14
+ const [loading, setLoading] = useState(true);
15
+
16
+ const loadBreakers = useCallback(() => {
17
+ fetch('/api/usage/circuit-breakers')
18
+ .then(res => res.json())
19
+ .then((data: { breakers: CircuitBreaker[] }) => {
20
+ const enriched = (data.breakers ?? []).map(cb => ({
21
+ ...cb,
22
+ level: (cb.key.includes(':global:') ? 'global'
23
+ : cb.key.includes(':project:') ? 'project'
24
+ : 'feature') as CircuitBreaker['level'],
25
+ }));
26
+ setBreakers(enriched);
27
+ setLoading(false);
28
+ })
29
+ .catch(() => setLoading(false));
30
+ }, []);
31
+
32
+ useEffect(() => { loadBreakers(); }, [loadBreakers]);
33
+
34
+ if (loading) return <LoadingSkeleton lines={4} />;
35
+
36
+ const grouped = {
37
+ global: breakers.filter(b => b.level === 'global'),
38
+ project: breakers.filter(b => b.level === 'project'),
39
+ feature: breakers.filter(b => b.level === 'feature'),
40
+ };
41
+
42
+ const statusBadge = (status: string) => {
43
+ const styles = status === 'active'
44
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
45
+ : status === 'warning'
46
+ ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
47
+ : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
48
+ return <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${styles}`}>{status}</span>;
49
+ };
50
+
51
+ const renderGroup = (title: string, items: CircuitBreaker[]) => {
52
+ if (items.length === 0) return null;
53
+ return (
54
+ <div>
55
+ <h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">{title}</h4>
56
+ <div className="space-y-1.5">
57
+ {items.map(cb => (
58
+ <div key={cb.key} className="flex items-center justify-between py-1.5">
59
+ <div>
60
+ <span className="text-sm text-gray-900 dark:text-white">{cb.feature}</span>
61
+ {cb.reason && (
62
+ <p className="text-xs text-gray-500 dark:text-gray-400">{cb.reason}</p>
63
+ )}
64
+ </div>
65
+ {statusBadge(cb.status)}
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ );
71
+ };
72
+
73
+ return (
74
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
75
+ <div className="flex items-center justify-between mb-4">
76
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
77
+ Circuit Breakers
78
+ </h3>
79
+ <button
80
+ onClick={loadBreakers}
81
+ className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
82
+ >
83
+ Refresh
84
+ </button>
85
+ </div>
86
+ {breakers.length === 0 ? (
87
+ <p className="text-sm text-gray-500 dark:text-gray-400">No circuit breakers configured.</p>
88
+ ) : (
89
+ <div className="space-y-4">
90
+ {renderGroup('Global', grouped.global)}
91
+ {renderGroup('Project', grouped.project)}
92
+ {renderGroup('Feature', grouped.feature)}
93
+ </div>
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -1,2 +1,4 @@
1
1
  export { HealthTabs } from './HealthTabs';
2
2
  export { DlqStatusCard } from './DlqStatusCard';
3
+ export { CircuitBreakerPanel } from './CircuitBreakerPanel';
4
+ export { CircuitBreakerEvents } from './CircuitBreakerEvents';