@littlebearapps/platform-admin-sdk 2.1.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 +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- 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/FeatureUsageReport.tsx +339 -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/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/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/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/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -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-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -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/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
|
@@ -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>
|