@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.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 (122) hide show
  1. package/README.md +2 -5
  2. package/dist/check-upgrade.d.ts +29 -0
  3. package/dist/check-upgrade.js +97 -0
  4. package/dist/index.js +59 -4
  5. package/dist/manifest.d.ts +2 -0
  6. package/dist/scaffold.js +5 -1
  7. package/dist/templates.d.ts +6 -1
  8. package/dist/templates.js +141 -3
  9. package/dist/upgrade.d.ts +1 -0
  10. package/dist/upgrade.js +21 -2
  11. package/package.json +1 -1
  12. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  13. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  14. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  15. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  16. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  17. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  18. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  19. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  20. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  21. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  22. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  23. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  24. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  25. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  26. package/templates/full/dashboard/src/pages/map.astro +561 -0
  27. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  28. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  29. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  30. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  31. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  32. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  33. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  34. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  35. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  36. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  37. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  38. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  39. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  40. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  41. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  42. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  43. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  44. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  45. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  46. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  47. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  48. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  49. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  50. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  51. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  52. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  53. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  54. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  55. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  56. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  57. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  58. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  59. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  60. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  61. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  62. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  63. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  64. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  65. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  66. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  67. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  68. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  69. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  70. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  71. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  72. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  73. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  74. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  75. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  76. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  77. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  78. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  79. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  80. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  81. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  82. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  83. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  84. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  85. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  86. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  87. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  88. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  89. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  90. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  91. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  92. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  93. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  94. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  95. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  96. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  97. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  98. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  99. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  100. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  101. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  102. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  103. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  104. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  105. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  107. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  108. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  109. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  110. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  111. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  112. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  113. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  114. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  115. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  116. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  117. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  118. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  119. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  120. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  121. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  122. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,561 @@
1
+ ---
2
+ /**
3
+ * Infrastructure Map Dashboard
4
+ *
5
+ * Interactive topology visualization powered by Cytoscape.js
6
+ * Shows all Little Bear Apps services, connections, and health status
7
+ * Auto-refreshes every 60 seconds
8
+ */
9
+
10
+ import DashboardLayout from '../layouts/DashboardLayout.astro';
11
+
12
+ const title = 'Infrastructure Map';
13
+ ---
14
+
15
+ <DashboardLayout title={title}>
16
+ <div class="infrastructure-map-container">
17
+ <div class="toolbar">
18
+ <h2>Infrastructure Topology</h2>
19
+
20
+ <div class="toolbar-controls">
21
+ <button id="btn-fit" class="btn">Fit to Screen</button>
22
+ <button id="btn-reset" class="btn">Reset Layout</button>
23
+
24
+ <select id="layout-select" class="select">
25
+ <option value="dagre">Hierarchical (Default)</option>
26
+ <option value="cose">Force-Directed</option>
27
+ <option value="breadthfirst">Breadth-First</option>
28
+ <option value="circle">Circle</option>
29
+ </select>
30
+ </div>
31
+
32
+ <div class="legend">
33
+ <div class="legend-title">Health Status:</div>
34
+ <span class="legend-item"><span class="status-dot up"></span> Up</span>
35
+ <span class="legend-item"><span class="status-dot degraded"></span> Degraded</span>
36
+ <span class="legend-item"><span class="status-dot down"></span> Down</span>
37
+ <span class="legend-item"><span class="status-dot unknown"></span> Unknown</span>
38
+ </div>
39
+
40
+ <div class="health-summary">
41
+ <span id="health-summary-text">Loading...</span>
42
+ </div>
43
+ </div>
44
+
45
+ <div id="cy" class="cytoscape-container"></div>
46
+
47
+ <div id="service-details" class="details-panel hidden">
48
+ <div class="details-header">
49
+ <h3>Service Details</h3>
50
+ <button id="close-details" class="btn-close">×</button>
51
+ </div>
52
+ <div id="details-content"></div>
53
+ </div>
54
+ </div>
55
+ </DashboardLayout>
56
+
57
+ <script>
58
+ // @ts-ignore - Loaded via CDN
59
+ import cytoscape from 'https://cdn.jsdelivr.net/npm/cytoscape@3.28.1/dist/cytoscape.esm.min.js';
60
+ // @ts-ignore - Dagre layout
61
+ import dagre from 'https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js';
62
+ // @ts-ignore - Cytoscape dagre
63
+ import cytoscapeDagre from 'https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.js';
64
+
65
+ // @ts-ignore
66
+ cytoscape.use(cytoscapeDagre);
67
+
68
+ let cy: any;
69
+ let refreshInterval: any;
70
+
71
+ async function loadTopology() {
72
+ const response = await fetch('/api/topology');
73
+
74
+ if (!response.ok) {
75
+ console.error('Failed to load topology:', response.status);
76
+ throw new Error('Topology not available');
77
+ }
78
+
79
+ const topology = await response.json();
80
+ return topology;
81
+ }
82
+
83
+ async function initializeMap() {
84
+ try {
85
+ const topology = await loadTopology();
86
+
87
+ const elements = {
88
+ nodes: topology.services.map((service: any) => ({
89
+ data: {
90
+ id: service.id,
91
+ label: service.name,
92
+ type: service.type,
93
+ tier: service.tier,
94
+ status: service.status,
95
+ health: service.health_status || 'unknown',
96
+ version: service.version,
97
+ metadata: service.metadata,
98
+ },
99
+ })),
100
+ edges: topology.connections.map((conn: any) => ({
101
+ data: {
102
+ id: `${conn.from_service}-${conn.to_service}`,
103
+ source: conn.from_service,
104
+ target: conn.to_service,
105
+ type: conn.connection_type,
106
+ status: conn.status,
107
+ },
108
+ })),
109
+ };
110
+
111
+ cy = cytoscape({
112
+ container: document.getElementById('cy'),
113
+ elements: elements,
114
+
115
+ style: [
116
+ {
117
+ selector: 'node',
118
+ style: {
119
+ label: 'data(label)',
120
+ 'text-valign': 'center',
121
+ 'text-halign': 'center',
122
+ 'text-wrap': 'wrap',
123
+ 'text-max-width': '80px',
124
+ 'font-size': '10px',
125
+ width: '60px',
126
+ height: '60px',
127
+ 'background-color': (ele: any) => getNodeColor(ele.data('health')),
128
+ 'border-width': 3,
129
+ 'border-color': (ele: any) => getTierColor(ele.data('tier')),
130
+ },
131
+ },
132
+ {
133
+ selector: 'edge',
134
+ style: {
135
+ width: 2,
136
+ 'line-color': (ele: any) => getEdgeColor(ele.data('status')),
137
+ 'target-arrow-color': (ele: any) => getEdgeColor(ele.data('status')),
138
+ 'target-arrow-shape': 'triangle',
139
+ 'curve-style': 'bezier',
140
+ 'line-style': (ele: any) => (ele.data('status') === 'planned' ? 'dashed' : 'solid'),
141
+ },
142
+ },
143
+ {
144
+ selector: ':selected',
145
+ style: {
146
+ 'border-width': 5,
147
+ 'border-color': '#0066cc',
148
+ },
149
+ },
150
+ ],
151
+
152
+ layout: {
153
+ name: 'dagre',
154
+ rankDir: 'TB', // Top to bottom
155
+ spacingFactor: 1.5,
156
+ nodeSep: 50,
157
+ rankSep: 100,
158
+ },
159
+ });
160
+
161
+ // Click handler for service details
162
+ cy.on('tap', 'node', (evt: any) => {
163
+ const node = evt.target;
164
+ showServiceDetails(node.data());
165
+ });
166
+
167
+ // Click outside to close details
168
+ cy.on('tap', (evt: any) => {
169
+ if (evt.target === cy) {
170
+ hideServiceDetails();
171
+ }
172
+ });
173
+
174
+ // Update health summary
175
+ updateHealthSummary(topology);
176
+
177
+ // Auto-refresh every 60 seconds
178
+ refreshInterval = setInterval(refreshTopology, 60000);
179
+
180
+ console.log('✅ Infrastructure Map initialized:', {
181
+ services: topology.services.length,
182
+ connections: topology.connections.length,
183
+ });
184
+ } catch (error) {
185
+ console.error('Failed to initialize map:', error);
186
+ document.getElementById('cy')!.innerHTML = `
187
+ <div style="padding: 40px; text-align: center; color: #ef4444;">
188
+ <h3>Failed to load infrastructure map</h3>
189
+ <p>Topology discovery worker may not have run yet. Please try again in 15 minutes.</p>
190
+ </div>
191
+ `;
192
+ }
193
+ }
194
+
195
+ function getNodeColor(health: string): string {
196
+ const colors: Record<string, string> = {
197
+ up: '#10b981', // Green
198
+ degraded: '#f59e0b', // Orange
199
+ down: '#ef4444', // Red
200
+ unknown: '#9ca3af', // Gray
201
+ };
202
+ return colors[health] || colors.unknown;
203
+ }
204
+
205
+ function getTierColor(tier: number): string {
206
+ const colors: Record<number, string> = {
207
+ 0: '#dc2626', // Critical (red border)
208
+ 1: '#f59e0b', // High (orange border)
209
+ 2: '#6b7280', // Medium (gray border)
210
+ };
211
+ return colors[tier] || colors[2];
212
+ }
213
+
214
+ function getEdgeColor(status: string): string {
215
+ const colors: Record<string, string> = {
216
+ active: '#10b981', // Green
217
+ planned: '#9ca3af', // Gray
218
+ 'not-integrated': '#f59e0b', // Orange
219
+ broken: '#ef4444', // Red
220
+ };
221
+ return colors[status] || colors.planned;
222
+ }
223
+
224
+ function showServiceDetails(data: any) {
225
+ const panel = document.getElementById('service-details')!;
226
+ const content = document.getElementById('details-content')!;
227
+
228
+ const tierLabels = ['Critical', 'High Priority', 'Medium Priority'];
229
+
230
+ content.innerHTML = `
231
+ <div class="detail-row">
232
+ <strong>ID:</strong>
233
+ <span>${data.id}</span>
234
+ </div>
235
+ <div class="detail-row">
236
+ <strong>Name:</strong>
237
+ <span>${data.label}</span>
238
+ </div>
239
+ <div class="detail-row">
240
+ <strong>Type:</strong>
241
+ <span>${data.type}</span>
242
+ </div>
243
+ <div class="detail-row">
244
+ <strong>Status:</strong>
245
+ <span class="status-badge ${data.health}">${data.health}</span>
246
+ </div>
247
+ <div class="detail-row">
248
+ <strong>Version:</strong>
249
+ <span>${data.version || 'N/A'}</span>
250
+ </div>
251
+ <div class="detail-row">
252
+ <strong>Tier:</strong>
253
+ <span>${data.tier} (${tierLabels[data.tier] || 'Unknown'})</span>
254
+ </div>
255
+ ${
256
+ data.metadata
257
+ ? `
258
+ <div class="detail-row full-width">
259
+ <strong>Metadata:</strong>
260
+ <pre>${JSON.stringify(data.metadata, null, 2)}</pre>
261
+ </div>
262
+ `
263
+ : ''
264
+ }
265
+ `;
266
+
267
+ panel.classList.remove('hidden');
268
+ }
269
+
270
+ function hideServiceDetails() {
271
+ const panel = document.getElementById('service-details')!;
272
+ panel.classList.add('hidden');
273
+ }
274
+
275
+ async function refreshTopology() {
276
+ try {
277
+ const topology = await loadTopology();
278
+
279
+ // Update node health status
280
+ topology.services.forEach((service: any) => {
281
+ const node = cy.getElementById(service.id);
282
+ if (node.length) {
283
+ node.data('health', service.health_status || 'unknown');
284
+ node.data('version', service.version);
285
+ }
286
+ });
287
+
288
+ // Update health summary
289
+ updateHealthSummary(topology);
290
+
291
+ console.log('🔄 Topology refreshed');
292
+ } catch (error) {
293
+ console.error('Failed to refresh topology:', error);
294
+ }
295
+ }
296
+
297
+ function updateHealthSummary(topology: any) {
298
+ const summary = document.getElementById('health-summary-text')!;
299
+
300
+ const healthCounts: Record<string, number> = {
301
+ up: 0,
302
+ down: 0,
303
+ degraded: 0,
304
+ unknown: 0,
305
+ };
306
+
307
+ topology.services.forEach((s: any) => {
308
+ healthCounts[s.health_status || 'unknown']++;
309
+ });
310
+
311
+ summary.textContent = `${healthCounts.up} up, ${healthCounts.degraded} degraded, ${healthCounts.down} down, ${healthCounts.unknown} unknown`;
312
+ }
313
+
314
+ // Toolbar button handlers
315
+ document.getElementById('btn-fit')!.addEventListener('click', () => {
316
+ cy.fit();
317
+ });
318
+
319
+ document.getElementById('btn-reset')!.addEventListener('click', () => {
320
+ const layoutName = (document.getElementById('layout-select')! as HTMLSelectElement).value;
321
+ cy.layout({ name: layoutName, rankDir: 'TB', spacingFactor: 1.5 }).run();
322
+ });
323
+
324
+ document.getElementById('layout-select')!.addEventListener('change', (evt: any) => {
325
+ const layoutName = evt.target.value;
326
+ cy.layout({ name: layoutName, rankDir: 'TB', spacingFactor: 1.5 }).run();
327
+ });
328
+
329
+ document.getElementById('close-details')!.addEventListener('click', () => {
330
+ hideServiceDetails();
331
+ });
332
+
333
+ // Initialize on page load
334
+ initializeMap();
335
+
336
+ // Cleanup on page unload
337
+ window.addEventListener('beforeunload', () => {
338
+ if (refreshInterval) {
339
+ clearInterval(refreshInterval);
340
+ }
341
+ });
342
+ </script>
343
+
344
+ <style>
345
+ .infrastructure-map-container {
346
+ padding: 20px;
347
+ height: calc(100vh - 120px);
348
+ display: flex;
349
+ flex-direction: column;
350
+ }
351
+
352
+ .toolbar {
353
+ display: flex;
354
+ gap: 20px;
355
+ align-items: center;
356
+ margin-bottom: 15px;
357
+ padding: 15px;
358
+ background-color: #f9fafb;
359
+ border-radius: 8px;
360
+ flex-wrap: wrap;
361
+ }
362
+
363
+ .toolbar h2 {
364
+ margin: 0;
365
+ font-size: 20px;
366
+ color: #111827;
367
+ }
368
+
369
+ .toolbar-controls {
370
+ display: flex;
371
+ gap: 10px;
372
+ align-items: center;
373
+ }
374
+
375
+ .btn {
376
+ padding: 8px 16px;
377
+ border: 1px solid #d1d5db;
378
+ border-radius: 6px;
379
+ background-color: white;
380
+ cursor: pointer;
381
+ font-size: 14px;
382
+ transition: all 0.2s;
383
+ }
384
+
385
+ .btn:hover {
386
+ background-color: #f3f4f6;
387
+ border-color: #9ca3af;
388
+ }
389
+
390
+ .select {
391
+ padding: 8px 12px;
392
+ border: 1px solid #d1d5db;
393
+ border-radius: 6px;
394
+ background-color: white;
395
+ cursor: pointer;
396
+ font-size: 14px;
397
+ }
398
+
399
+ .legend {
400
+ display: flex;
401
+ gap: 15px;
402
+ align-items: center;
403
+ margin-left: auto;
404
+ }
405
+
406
+ .legend-title {
407
+ font-weight: 600;
408
+ color: #4b5563;
409
+ font-size: 14px;
410
+ }
411
+
412
+ .legend-item {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 6px;
416
+ font-size: 13px;
417
+ color: #6b7280;
418
+ }
419
+
420
+ .status-dot {
421
+ width: 12px;
422
+ height: 12px;
423
+ border-radius: 50%;
424
+ }
425
+
426
+ .status-dot.up {
427
+ background-color: #10b981;
428
+ }
429
+ .status-dot.degraded {
430
+ background-color: #f59e0b;
431
+ }
432
+ .status-dot.down {
433
+ background-color: #ef4444;
434
+ }
435
+ .status-dot.unknown {
436
+ background-color: #9ca3af;
437
+ }
438
+
439
+ .health-summary {
440
+ padding: 8px 14px;
441
+ background-color: white;
442
+ border: 1px solid #d1d5db;
443
+ border-radius: 6px;
444
+ font-size: 14px;
445
+ font-weight: 500;
446
+ color: #374151;
447
+ }
448
+
449
+ .cytoscape-container {
450
+ flex: 1;
451
+ border: 2px solid #e5e7eb;
452
+ border-radius: 8px;
453
+ background-color: #ffffff;
454
+ position: relative;
455
+ }
456
+
457
+ .details-panel {
458
+ position: fixed;
459
+ right: 30px;
460
+ top: 120px;
461
+ width: 350px;
462
+ max-height: 600px;
463
+ overflow-y: auto;
464
+ background: white;
465
+ border: 2px solid #e5e7eb;
466
+ border-radius: 8px;
467
+ padding: 20px;
468
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
469
+ }
470
+
471
+ .details-panel.hidden {
472
+ display: none;
473
+ }
474
+
475
+ .details-header {
476
+ display: flex;
477
+ justify-content: space-between;
478
+ align-items: center;
479
+ margin-bottom: 15px;
480
+ padding-bottom: 10px;
481
+ border-bottom: 2px solid #e5e7eb;
482
+ }
483
+
484
+ .details-header h3 {
485
+ margin: 0;
486
+ font-size: 18px;
487
+ color: #111827;
488
+ }
489
+
490
+ .btn-close {
491
+ background: none;
492
+ border: none;
493
+ font-size: 28px;
494
+ cursor: pointer;
495
+ color: #6b7280;
496
+ line-height: 1;
497
+ padding: 0;
498
+ width: 30px;
499
+ height: 30px;
500
+ }
501
+
502
+ .btn-close:hover {
503
+ color: #111827;
504
+ }
505
+
506
+ .detail-row {
507
+ display: flex;
508
+ justify-content: space-between;
509
+ padding: 8px 0;
510
+ border-bottom: 1px solid #f3f4f6;
511
+ }
512
+
513
+ .detail-row.full-width {
514
+ flex-direction: column;
515
+ gap: 8px;
516
+ }
517
+
518
+ .detail-row strong {
519
+ color: #374151;
520
+ font-size: 13px;
521
+ }
522
+
523
+ .detail-row span {
524
+ color: #6b7280;
525
+ font-size: 13px;
526
+ }
527
+
528
+ .detail-row pre {
529
+ background-color: #f9fafb;
530
+ padding: 10px;
531
+ border-radius: 4px;
532
+ font-size: 11px;
533
+ overflow-x: auto;
534
+ margin: 0;
535
+ }
536
+
537
+ .status-badge {
538
+ padding: 3px 8px;
539
+ border-radius: 4px;
540
+ font-size: 11px;
541
+ font-weight: bold;
542
+ text-transform: uppercase;
543
+ }
544
+
545
+ .status-badge.up {
546
+ background-color: #d1fae5;
547
+ color: #065f46;
548
+ }
549
+ .status-badge.degraded {
550
+ background-color: #fed7aa;
551
+ color: #92400e;
552
+ }
553
+ .status-badge.down {
554
+ background-color: #fee2e2;
555
+ color: #991b1b;
556
+ }
557
+ .status-badge.unknown {
558
+ background-color: #f3f4f6;
559
+ color: #4b5563;
560
+ }
561
+ </style>
@@ -0,0 +1,72 @@
1
+ ---
2
+ import DashboardLayout from '../layouts/DashboardLayout.astro';
3
+
4
+ interface RevenueRow {
5
+ metric_type: string;
6
+ value: number;
7
+ timestamp: number;
8
+ }
9
+
10
+ const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
11
+ const db = runtime?.env?.PLATFORM_DB;
12
+
13
+ let latestMetrics: Record<string, number> = {};
14
+
15
+ if (db) {
16
+ const result = await db
17
+ .prepare(
18
+ `SELECT metric_type, value, timestamp
19
+ FROM revenue_metrics
20
+ WHERE metric_type IN ('mrr', 'arr', 'churn')
21
+ ORDER BY timestamp DESC
22
+ LIMIT 100`
23
+ )
24
+ .all<RevenueRow>();
25
+
26
+ for (const row of result.results ?? []) {
27
+ if (!(row.metric_type in latestMetrics)) {
28
+ latestMetrics[row.metric_type] = row.value ?? 0;
29
+ }
30
+ }
31
+ }
32
+
33
+ const mrr = latestMetrics.mrr ?? 0;
34
+ const arr = latestMetrics.arr ?? 0;
35
+ const churn = latestMetrics.churn ?? 0;
36
+ ---
37
+
38
+ <DashboardLayout title="Revenue">
39
+ <div class="space-y-6">
40
+ <h2 class="text-3xl font-bold text-gray-900 dark:text-white">Revenue Metrics</h2>
41
+
42
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-3">
43
+ <div class="metric-card">
44
+ <div class="metric-title">Monthly Recurring Revenue</div>
45
+ <div class="metric-value">${mrr.toFixed(2)}</div>
46
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">MRR (current)</div>
47
+ </div>
48
+
49
+ <div class="metric-card">
50
+ <div class="metric-title">Annual Recurring Revenue</div>
51
+ <div class="metric-value">${arr.toFixed(2)}</div>
52
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">ARR (annualized)</div>
53
+ </div>
54
+
55
+ <div class="metric-card">
56
+ <div class="metric-title">Churn Rate</div>
57
+ <div class="metric-value">{(churn * 100).toFixed(1)}%</div>
58
+ <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Monthly churn</div>
59
+ </div>
60
+ </div>
61
+
62
+ <section class="metric-card">
63
+ <h3 class="mb-4 text-xl font-semibold text-gray-800 dark:text-gray-200">Revenue Trends</h3>
64
+ <div
65
+ class="rounded border border-dashed border-gray-300 bg-gray-50 py-12 text-center text-gray-600 dark:border-gray-600 dark:bg-gray-900/50 dark:text-gray-400"
66
+ >
67
+ Charts coming soon — integrate Chart.js or a similar library to visualize MRR, ARR, and
68
+ churn trends.
69
+ </div>
70
+ </section>
71
+ </div>
72
+ </DashboardLayout>