@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,633 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AnomalyAlerts Component
|
|
4
|
+
*
|
|
5
|
+
* Displays usage anomalies detected by the platform-usage worker.
|
|
6
|
+
* Shows metric spikes with deviation factors and resolution status.
|
|
7
|
+
*
|
|
8
|
+
* Part of Enhancement #2: Anomaly alerts visualization.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Lists recent anomalies with severity indicators
|
|
12
|
+
* - Shows deviation factor (how many stddevs above average)
|
|
13
|
+
* - Visual indicators for resolved/unresolved status
|
|
14
|
+
* - Filters by time range and resolved status
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface AnomalyData {
|
|
18
|
+
id: string;
|
|
19
|
+
detectedAt: string;
|
|
20
|
+
metric: string;
|
|
21
|
+
project: string;
|
|
22
|
+
currentValue: number;
|
|
23
|
+
rollingAvg: number;
|
|
24
|
+
deviationFactor: number;
|
|
25
|
+
alertSent: boolean;
|
|
26
|
+
alertChannel: string | null;
|
|
27
|
+
resolved: boolean;
|
|
28
|
+
resolvedAt: string | null;
|
|
29
|
+
resolvedBy: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
/** Initial anomalies (if server-rendered) */
|
|
34
|
+
anomalies?: AnomalyData[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { anomalies = [] } = Astro.props;
|
|
38
|
+
|
|
39
|
+
// Format metric names for display
|
|
40
|
+
function formatMetric(metric: string): string {
|
|
41
|
+
const map: Record<string, string> = {
|
|
42
|
+
workers_requests: 'Workers Requests',
|
|
43
|
+
d1_rows_read: 'D1 Reads',
|
|
44
|
+
d1_rows_written: 'D1 Writes',
|
|
45
|
+
kv_reads: 'KV Reads',
|
|
46
|
+
kv_writes: 'KV Writes',
|
|
47
|
+
r2_class_a: 'R2 Class A',
|
|
48
|
+
r2_class_b: 'R2 Class B',
|
|
49
|
+
total_cost_usd: 'Total Cost',
|
|
50
|
+
ai_requests: 'AI Requests',
|
|
51
|
+
ai_tokens: 'AI Tokens',
|
|
52
|
+
};
|
|
53
|
+
return map[metric] ?? metric.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Format large numbers
|
|
57
|
+
function formatNumber(num: number): string {
|
|
58
|
+
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
|
|
59
|
+
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
|
|
60
|
+
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
|
|
61
|
+
return num.toLocaleString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get severity class based on deviation factor
|
|
65
|
+
function getSeverity(deviationFactor: number): 'critical' | 'high' | 'medium' | 'low' {
|
|
66
|
+
if (deviationFactor >= 4) return 'critical';
|
|
67
|
+
if (deviationFactor >= 3) return 'high';
|
|
68
|
+
if (deviationFactor >= 2) return 'medium';
|
|
69
|
+
return 'low';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Format relative time
|
|
73
|
+
function formatRelativeTime(dateStr: string): string {
|
|
74
|
+
const date = new Date(dateStr);
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const diffMs = now.getTime() - date.getTime();
|
|
77
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
78
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
79
|
+
|
|
80
|
+
if (diffDays > 0) return `${diffDays}d ago`;
|
|
81
|
+
if (diffHours > 0) return `${diffHours}h ago`;
|
|
82
|
+
return 'Just now';
|
|
83
|
+
}
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<section class="anomaly-alerts-section" data-component="anomaly-alerts">
|
|
87
|
+
<div class="section-header">
|
|
88
|
+
<h3 class="section-title">
|
|
89
|
+
<span class="title-icon">⚠️</span>
|
|
90
|
+
Usage Anomalies
|
|
91
|
+
</h3>
|
|
92
|
+
<p class="section-description">Detected usage spikes based on 7-day rolling averages</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="anomaly-controls">
|
|
96
|
+
<div class="filter-group">
|
|
97
|
+
<label for="anomaly-days">Lookback:</label>
|
|
98
|
+
<select id="anomaly-days" class="filter-select">
|
|
99
|
+
<option value="7">7 days</option>
|
|
100
|
+
<option value="14">14 days</option>
|
|
101
|
+
<option value="30">30 days</option>
|
|
102
|
+
</select>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="filter-group">
|
|
105
|
+
<label for="anomaly-resolved">Status:</label>
|
|
106
|
+
<select id="anomaly-resolved" class="filter-select">
|
|
107
|
+
<option value="all">All</option>
|
|
108
|
+
<option value="false">Unresolved</option>
|
|
109
|
+
<option value="true">Resolved</option>
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
<button type="button" id="anomaly-refresh" class="refresh-btn" title="Refresh"> 🔄 </button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="anomaly-list-container" id="anomaly-list-container">
|
|
116
|
+
<!-- Loading state -->
|
|
117
|
+
<div class="loading-state" id="anomaly-loading">
|
|
118
|
+
<div class="loading-spinner"></div>
|
|
119
|
+
<span>Loading anomalies...</span>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Empty state -->
|
|
123
|
+
<div class="empty-state" id="anomaly-empty" style="display: none;">
|
|
124
|
+
<span class="empty-icon">✅</span>
|
|
125
|
+
<span class="empty-text">No anomalies detected in this period</span>
|
|
126
|
+
<span class="empty-subtext">Your usage is within normal ranges</span>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Anomaly cards -->
|
|
130
|
+
<div class="anomaly-cards" id="anomaly-cards" style="display: none;">
|
|
131
|
+
<!-- Cards populated by JavaScript -->
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.anomaly-alerts-section {
|
|
138
|
+
margin-bottom: 1.5rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.section-header {
|
|
142
|
+
margin-bottom: 1rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.section-title {
|
|
146
|
+
font-size: 1.1rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
margin: 0 0 0.25rem;
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: 0.5rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.title-icon {
|
|
155
|
+
font-size: 1.25rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.section-description {
|
|
159
|
+
color: var(--text-secondary, #666);
|
|
160
|
+
font-size: 0.875rem;
|
|
161
|
+
margin: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.anomaly-controls {
|
|
165
|
+
display: flex;
|
|
166
|
+
gap: 1rem;
|
|
167
|
+
align-items: center;
|
|
168
|
+
margin-bottom: 1rem;
|
|
169
|
+
flex-wrap: wrap;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.filter-group {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.filter-group label {
|
|
179
|
+
font-size: 0.875rem;
|
|
180
|
+
color: var(--text-secondary, #666);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.filter-select {
|
|
184
|
+
padding: 0.375rem 0.75rem;
|
|
185
|
+
border: 1px solid var(--border-color, #ddd);
|
|
186
|
+
border-radius: 4px;
|
|
187
|
+
background: var(--bg-secondary, #fff);
|
|
188
|
+
font-size: 0.875rem;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.refresh-btn {
|
|
193
|
+
padding: 0.375rem 0.5rem;
|
|
194
|
+
border: 1px solid var(--border-color, #ddd);
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
background: var(--bg-secondary, #fff);
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
font-size: 1rem;
|
|
199
|
+
line-height: 1;
|
|
200
|
+
transition: background-color 0.2s;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.refresh-btn:hover {
|
|
204
|
+
background: var(--bg-hover, #f5f5f5);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.refresh-btn:disabled {
|
|
208
|
+
opacity: 0.5;
|
|
209
|
+
cursor: not-allowed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.anomaly-list-container {
|
|
213
|
+
min-height: 150px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.loading-state,
|
|
217
|
+
.empty-state {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
align-items: center;
|
|
221
|
+
justify-content: center;
|
|
222
|
+
padding: 2rem;
|
|
223
|
+
color: var(--text-secondary, #666);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.loading-spinner {
|
|
227
|
+
width: 24px;
|
|
228
|
+
height: 24px;
|
|
229
|
+
border: 2px solid var(--border-color, #ddd);
|
|
230
|
+
border-top-color: var(--primary-color, #3b82f6);
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
animation: spin 0.8s linear infinite;
|
|
233
|
+
margin-bottom: 0.5rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@keyframes spin {
|
|
237
|
+
to {
|
|
238
|
+
transform: rotate(360deg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.empty-icon {
|
|
243
|
+
font-size: 2rem;
|
|
244
|
+
margin-bottom: 0.5rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.empty-text {
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
margin-bottom: 0.25rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.empty-subtext {
|
|
253
|
+
font-size: 0.8rem;
|
|
254
|
+
opacity: 0.8;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.anomaly-cards {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
gap: 0.75rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.anomaly-card {
|
|
264
|
+
display: grid;
|
|
265
|
+
grid-template-columns: auto 1fr auto auto;
|
|
266
|
+
gap: 1rem;
|
|
267
|
+
align-items: center;
|
|
268
|
+
padding: 1rem;
|
|
269
|
+
border: 1px solid var(--border-color, #ddd);
|
|
270
|
+
border-radius: 8px;
|
|
271
|
+
background: var(--bg-secondary, #fff);
|
|
272
|
+
transition: box-shadow 0.2s;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.anomaly-card:hover {
|
|
276
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.anomaly-card.resolved {
|
|
280
|
+
opacity: 0.7;
|
|
281
|
+
border-left: 3px solid #10b981;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.anomaly-card.unresolved {
|
|
285
|
+
border-left: 3px solid #ef4444;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.severity-badge {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
width: 40px;
|
|
293
|
+
height: 40px;
|
|
294
|
+
border-radius: 50%;
|
|
295
|
+
font-weight: 600;
|
|
296
|
+
font-size: 0.875rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.severity-badge.critical {
|
|
300
|
+
background: #fee2e2;
|
|
301
|
+
color: #dc2626;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.severity-badge.high {
|
|
305
|
+
background: #ffedd5;
|
|
306
|
+
color: #ea580c;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.severity-badge.medium {
|
|
310
|
+
background: #fef3c7;
|
|
311
|
+
color: #d97706;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.severity-badge.low {
|
|
315
|
+
background: #dbeafe;
|
|
316
|
+
color: #2563eb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.anomaly-info {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-direction: column;
|
|
322
|
+
gap: 0.25rem;
|
|
323
|
+
min-width: 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.anomaly-metric {
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
font-size: 0.95rem;
|
|
329
|
+
white-space: nowrap;
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
text-overflow: ellipsis;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.anomaly-details {
|
|
335
|
+
font-size: 0.8rem;
|
|
336
|
+
color: var(--text-secondary, #666);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.anomaly-project {
|
|
340
|
+
font-weight: 500;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.anomaly-values {
|
|
344
|
+
text-align: right;
|
|
345
|
+
font-size: 0.875rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.anomaly-current {
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.anomaly-avg {
|
|
353
|
+
font-size: 0.75rem;
|
|
354
|
+
color: var(--text-secondary, #666);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.anomaly-time {
|
|
358
|
+
font-size: 0.8rem;
|
|
359
|
+
color: var(--text-secondary, #666);
|
|
360
|
+
text-align: right;
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.anomaly-status {
|
|
365
|
+
font-size: 0.7rem;
|
|
366
|
+
padding: 0.125rem 0.375rem;
|
|
367
|
+
border-radius: 3px;
|
|
368
|
+
margin-top: 0.25rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.anomaly-status.resolved {
|
|
372
|
+
background: #dcfce7;
|
|
373
|
+
color: #166534;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.anomaly-status.unresolved {
|
|
377
|
+
background: #fee2e2;
|
|
378
|
+
color: #991b1b;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@media (max-width: 768px) {
|
|
382
|
+
.anomaly-card {
|
|
383
|
+
grid-template-columns: auto 1fr;
|
|
384
|
+
grid-template-rows: auto auto;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.anomaly-values,
|
|
388
|
+
.anomaly-time {
|
|
389
|
+
grid-column: 2;
|
|
390
|
+
text-align: left;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
</style>
|
|
394
|
+
|
|
395
|
+
<script>
|
|
396
|
+
// Type definitions
|
|
397
|
+
interface AnomalyData {
|
|
398
|
+
id: string;
|
|
399
|
+
detectedAt: string;
|
|
400
|
+
metric: string;
|
|
401
|
+
project: string;
|
|
402
|
+
currentValue: number;
|
|
403
|
+
rollingAvg: number;
|
|
404
|
+
deviationFactor: number;
|
|
405
|
+
alertSent: boolean;
|
|
406
|
+
alertChannel: string | null;
|
|
407
|
+
resolved: boolean;
|
|
408
|
+
resolvedAt: string | null;
|
|
409
|
+
resolvedBy: string | null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Global update function
|
|
413
|
+
declare global {
|
|
414
|
+
interface Window {
|
|
415
|
+
updateAnomalyAlerts?: (anomalies: AnomalyData[]) => void;
|
|
416
|
+
refreshAnomalyAlerts?: () => Promise<void>;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Format metric names for display
|
|
421
|
+
function formatMetric(metric: string): string {
|
|
422
|
+
const map: Record<string, string> = {
|
|
423
|
+
workers_requests: 'Workers Requests',
|
|
424
|
+
d1_rows_read: 'D1 Reads',
|
|
425
|
+
d1_rows_written: 'D1 Writes',
|
|
426
|
+
kv_reads: 'KV Reads',
|
|
427
|
+
kv_writes: 'KV Writes',
|
|
428
|
+
r2_class_a: 'R2 Class A',
|
|
429
|
+
r2_class_b: 'R2 Class B',
|
|
430
|
+
total_cost_usd: 'Total Cost',
|
|
431
|
+
ai_requests: 'AI Requests',
|
|
432
|
+
ai_tokens: 'AI Tokens',
|
|
433
|
+
};
|
|
434
|
+
return map[metric] ?? metric.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Format large numbers
|
|
438
|
+
function formatNumber(num: number): string {
|
|
439
|
+
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
|
|
440
|
+
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
|
|
441
|
+
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
|
|
442
|
+
return num.toLocaleString();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Get severity class based on deviation factor
|
|
446
|
+
function getSeverity(deviationFactor: number): string {
|
|
447
|
+
if (deviationFactor >= 4) return 'critical';
|
|
448
|
+
if (deviationFactor >= 3) return 'high';
|
|
449
|
+
if (deviationFactor >= 2) return 'medium';
|
|
450
|
+
return 'low';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Format relative time
|
|
454
|
+
function formatRelativeTime(dateStr: string): string {
|
|
455
|
+
const date = new Date(dateStr);
|
|
456
|
+
const now = new Date();
|
|
457
|
+
const diffMs = now.getTime() - date.getTime();
|
|
458
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
459
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
460
|
+
|
|
461
|
+
if (diffDays > 0) return `${diffDays}d ago`;
|
|
462
|
+
if (diffHours > 0) return `${diffHours}h ago`;
|
|
463
|
+
return 'Just now';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Create anomaly card element using safe DOM methods
|
|
467
|
+
function createAnomalyCard(anomaly: AnomalyData): HTMLElement {
|
|
468
|
+
const card = document.createElement('div');
|
|
469
|
+
card.className = `anomaly-card ${anomaly.resolved ? 'resolved' : 'unresolved'}`;
|
|
470
|
+
|
|
471
|
+
// Severity badge
|
|
472
|
+
const severity = getSeverity(anomaly.deviationFactor);
|
|
473
|
+
const badge = document.createElement('div');
|
|
474
|
+
badge.className = `severity-badge ${severity}`;
|
|
475
|
+
badge.textContent = `${anomaly.deviationFactor}x`;
|
|
476
|
+
badge.title = `${anomaly.deviationFactor}x standard deviation`;
|
|
477
|
+
card.appendChild(badge);
|
|
478
|
+
|
|
479
|
+
// Info section
|
|
480
|
+
const info = document.createElement('div');
|
|
481
|
+
info.className = 'anomaly-info';
|
|
482
|
+
|
|
483
|
+
const metricEl = document.createElement('div');
|
|
484
|
+
metricEl.className = 'anomaly-metric';
|
|
485
|
+
metricEl.textContent = formatMetric(anomaly.metric);
|
|
486
|
+
info.appendChild(metricEl);
|
|
487
|
+
|
|
488
|
+
const details = document.createElement('div');
|
|
489
|
+
details.className = 'anomaly-details';
|
|
490
|
+
const projectSpan = document.createElement('span');
|
|
491
|
+
projectSpan.className = 'anomaly-project';
|
|
492
|
+
projectSpan.textContent = anomaly.project === 'all' ? 'All Projects' : anomaly.project;
|
|
493
|
+
details.appendChild(projectSpan);
|
|
494
|
+
info.appendChild(details);
|
|
495
|
+
|
|
496
|
+
card.appendChild(info);
|
|
497
|
+
|
|
498
|
+
// Values section
|
|
499
|
+
const values = document.createElement('div');
|
|
500
|
+
values.className = 'anomaly-values';
|
|
501
|
+
|
|
502
|
+
const current = document.createElement('div');
|
|
503
|
+
current.className = 'anomaly-current';
|
|
504
|
+
current.textContent = formatNumber(anomaly.currentValue);
|
|
505
|
+
values.appendChild(current);
|
|
506
|
+
|
|
507
|
+
const avg = document.createElement('div');
|
|
508
|
+
avg.className = 'anomaly-avg';
|
|
509
|
+
avg.textContent = `avg: ${formatNumber(anomaly.rollingAvg)}`;
|
|
510
|
+
values.appendChild(avg);
|
|
511
|
+
|
|
512
|
+
card.appendChild(values);
|
|
513
|
+
|
|
514
|
+
// Time section
|
|
515
|
+
const time = document.createElement('div');
|
|
516
|
+
time.className = 'anomaly-time';
|
|
517
|
+
|
|
518
|
+
const timeText = document.createElement('div');
|
|
519
|
+
timeText.textContent = formatRelativeTime(anomaly.detectedAt);
|
|
520
|
+
time.appendChild(timeText);
|
|
521
|
+
|
|
522
|
+
const status = document.createElement('div');
|
|
523
|
+
status.className = `anomaly-status ${anomaly.resolved ? 'resolved' : 'unresolved'}`;
|
|
524
|
+
status.textContent = anomaly.resolved ? 'Resolved' : 'Active';
|
|
525
|
+
time.appendChild(status);
|
|
526
|
+
|
|
527
|
+
card.appendChild(time);
|
|
528
|
+
|
|
529
|
+
return card;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Update anomaly alerts display
|
|
533
|
+
function updateAnomalyAlerts(anomalies: AnomalyData[]): void {
|
|
534
|
+
const loading = document.getElementById('anomaly-loading');
|
|
535
|
+
const empty = document.getElementById('anomaly-empty');
|
|
536
|
+
const cards = document.getElementById('anomaly-cards');
|
|
537
|
+
|
|
538
|
+
if (!loading || !empty || !cards) return;
|
|
539
|
+
|
|
540
|
+
loading.style.display = 'none';
|
|
541
|
+
|
|
542
|
+
if (!anomalies || anomalies.length === 0) {
|
|
543
|
+
empty.style.display = 'flex';
|
|
544
|
+
cards.style.display = 'none';
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
empty.style.display = 'none';
|
|
549
|
+
cards.style.display = 'flex';
|
|
550
|
+
|
|
551
|
+
// Clear existing cards
|
|
552
|
+
cards.replaceChildren();
|
|
553
|
+
|
|
554
|
+
// Create new cards using safe DOM methods
|
|
555
|
+
for (const anomaly of anomalies) {
|
|
556
|
+
const card = createAnomalyCard(anomaly);
|
|
557
|
+
cards.appendChild(card);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Fetch anomalies from API
|
|
562
|
+
async function fetchAnomalies(): Promise<void> {
|
|
563
|
+
const loading = document.getElementById('anomaly-loading');
|
|
564
|
+
const daysSelect = document.getElementById('anomaly-days') as HTMLSelectElement | null;
|
|
565
|
+
const resolvedSelect = document.getElementById('anomaly-resolved') as HTMLSelectElement | null;
|
|
566
|
+
const refreshBtn = document.getElementById('anomaly-refresh') as HTMLButtonElement | null;
|
|
567
|
+
|
|
568
|
+
if (loading) loading.style.display = 'flex';
|
|
569
|
+
if (refreshBtn) refreshBtn.disabled = true;
|
|
570
|
+
|
|
571
|
+
const days = daysSelect?.value ?? '7';
|
|
572
|
+
const resolved = resolvedSelect?.value ?? 'all';
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const response = await fetch(
|
|
576
|
+
`/api/usage/anomalies?days=${days}&resolved=${resolved}&limit=50`,
|
|
577
|
+
{ credentials: 'include' }
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
if (!response.ok) {
|
|
581
|
+
throw new Error(`HTTP ${response.status}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const data = await response.json();
|
|
585
|
+
|
|
586
|
+
if (data.success && data.anomalies) {
|
|
587
|
+
updateAnomalyAlerts(data.anomalies);
|
|
588
|
+
} else {
|
|
589
|
+
updateAnomalyAlerts([]);
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error('[AnomalyAlerts] Fetch error:', error);
|
|
593
|
+
updateAnomalyAlerts([]);
|
|
594
|
+
} finally {
|
|
595
|
+
if (refreshBtn) refreshBtn.disabled = false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Initialize component
|
|
600
|
+
function init(): void {
|
|
601
|
+
// Export global functions
|
|
602
|
+
window.updateAnomalyAlerts = updateAnomalyAlerts;
|
|
603
|
+
window.refreshAnomalyAlerts = fetchAnomalies;
|
|
604
|
+
|
|
605
|
+
// Add event listeners for filters
|
|
606
|
+
const daysSelect = document.getElementById('anomaly-days');
|
|
607
|
+
const resolvedSelect = document.getElementById('anomaly-resolved');
|
|
608
|
+
const refreshBtn = document.getElementById('anomaly-refresh');
|
|
609
|
+
|
|
610
|
+
daysSelect?.addEventListener('change', fetchAnomalies);
|
|
611
|
+
resolvedSelect?.addEventListener('change', fetchAnomalies);
|
|
612
|
+
refreshBtn?.addEventListener('click', fetchAnomalies);
|
|
613
|
+
|
|
614
|
+
// Fetch initial data when Alerts tab is shown
|
|
615
|
+
const alertsTab = document.querySelector('[data-tab="alerts"]');
|
|
616
|
+
if (alertsTab) {
|
|
617
|
+
alertsTab.addEventListener('click', () => {
|
|
618
|
+
// Only fetch if we haven't loaded yet (loading state is visible)
|
|
619
|
+
const loading = document.getElementById('anomaly-loading');
|
|
620
|
+
if (loading && loading.style.display !== 'none') {
|
|
621
|
+
fetchAnomalies();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Run on DOM ready
|
|
628
|
+
if (document.readyState === 'loading') {
|
|
629
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
630
|
+
} else {
|
|
631
|
+
init();
|
|
632
|
+
}
|
|
633
|
+
</script>
|