@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.
- package/README.md +2 -5
- package/dist/check-upgrade.d.ts +29 -0
- package/dist/check-upgrade.js +97 -0
- package/dist/index.js +59 -4
- package/dist/manifest.d.ts +2 -0
- package/dist/scaffold.js +5 -1
- package/dist/templates.d.ts +6 -1
- package/dist/templates.js +141 -3
- package/dist/upgrade.d.ts +1 -0
- package/dist/upgrade.js +21 -2
- 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,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProjectSelect Component
|
|
4
|
+
*
|
|
5
|
+
* Project filter dropdown using nanostores for reactive state.
|
|
6
|
+
* Fetches projects dynamically from the D1 registry via /api/usage/projects.
|
|
7
|
+
* No page reload on selection.
|
|
8
|
+
*
|
|
9
|
+
* Part of task-21: Usage Dashboard Refactor Phase 3 - Integrated Filter Bar
|
|
10
|
+
* Updated: task-XX: D1-backed project registry
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Initial project from URL (for SSR hydration) */
|
|
15
|
+
initialProject?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { initialProject = 'all' } = Astro.props;
|
|
19
|
+
|
|
20
|
+
// Fallback projects shown during initial render (before API response)
|
|
21
|
+
const fallbackProjects = [{ value: 'all', label: 'All Projects' }];
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class="project-select" data-component="project-select" data-initial={initialProject}>
|
|
25
|
+
<label for="project-filter" class="select-label">Project</label>
|
|
26
|
+
<div class="select-wrapper">
|
|
27
|
+
<select id="project-filter" class="select-input">
|
|
28
|
+
{
|
|
29
|
+
fallbackProjects.map((proj) => (
|
|
30
|
+
<option value={proj.value} selected={initialProject === proj.value}>
|
|
31
|
+
{proj.label}
|
|
32
|
+
</option>
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
</select>
|
|
36
|
+
<svg
|
|
37
|
+
class="select-chevron"
|
|
38
|
+
width="12"
|
|
39
|
+
height="12"
|
|
40
|
+
viewBox="0 0 12 12"
|
|
41
|
+
fill="none"
|
|
42
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
43
|
+
>
|
|
44
|
+
<path
|
|
45
|
+
d="M3 4.5L6 7.5L9 4.5"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
stroke-width="1.5"
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
stroke-linejoin="round"></path>
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.project-select {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: 0.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.select-label {
|
|
62
|
+
font-size: 0.625rem;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
color: var(--usage-text-muted);
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.05em;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.select-wrapper {
|
|
70
|
+
position: relative;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.select-input {
|
|
76
|
+
appearance: none;
|
|
77
|
+
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
|
78
|
+
font-size: 0.8125rem;
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
color: var(--usage-text-primary);
|
|
82
|
+
background-color: var(--usage-bg-tertiary);
|
|
83
|
+
border: 1px solid var(--usage-border-primary);
|
|
84
|
+
border-radius: 0.5rem;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: all 0.15s ease;
|
|
87
|
+
min-width: 140px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.select-input:hover {
|
|
91
|
+
border-color: var(--usage-border-hover);
|
|
92
|
+
background-color: var(--usage-bg-secondary);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.select-input:focus {
|
|
96
|
+
outline: none;
|
|
97
|
+
border-color: var(--usage-accent-primary);
|
|
98
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--usage-accent-primary) 25%, transparent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.select-chevron {
|
|
102
|
+
position: absolute;
|
|
103
|
+
right: 0.75rem;
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
color: var(--usage-text-secondary);
|
|
106
|
+
transition: transform 0.15s ease;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.select-wrapper:has(.select-input:focus) .select-chevron {
|
|
110
|
+
transform: rotate(180deg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (max-width: 640px) {
|
|
114
|
+
.select-input {
|
|
115
|
+
padding: 0.375rem 1.75rem 0.375rem 0.5rem;
|
|
116
|
+
font-size: 0.75rem;
|
|
117
|
+
min-width: 120px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.select-chevron {
|
|
121
|
+
right: 0.5rem;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Dark mode overrides */
|
|
126
|
+
:global([data-theme='dark']) .select-label,
|
|
127
|
+
:global(.dark) .select-label {
|
|
128
|
+
color: #8b949e;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:global([data-theme='dark']) .select-input,
|
|
132
|
+
:global(.dark) .select-input {
|
|
133
|
+
background-color: #21262d;
|
|
134
|
+
border-color: #30363d;
|
|
135
|
+
color: #e6edf3;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:global([data-theme='dark']) .select-input:hover,
|
|
139
|
+
:global(.dark) .select-input:hover {
|
|
140
|
+
background-color: #161b22;
|
|
141
|
+
border-color: #484f58;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
:global([data-theme='dark']) .select-input option,
|
|
145
|
+
:global(.dark) .select-input option {
|
|
146
|
+
background-color: #161b22;
|
|
147
|
+
color: #e6edf3;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:global([data-theme='dark']) .select-chevron,
|
|
151
|
+
:global(.dark) .select-chevron {
|
|
152
|
+
color: #8b949e;
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
155
|
+
|
|
156
|
+
<script>
|
|
157
|
+
import { $project } from '../state/usageStore';
|
|
158
|
+
import { setProject } from '../state/usageActions';
|
|
159
|
+
|
|
160
|
+
interface ProjectOption {
|
|
161
|
+
value: string;
|
|
162
|
+
label: string;
|
|
163
|
+
resourceCount?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Cache fetched projects
|
|
167
|
+
let projectsCache: ProjectOption[] | null = null;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Fetch projects from the D1 registry API
|
|
171
|
+
*/
|
|
172
|
+
async function fetchProjects(): Promise<ProjectOption[]> {
|
|
173
|
+
if (projectsCache) return projectsCache;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Include credentials to pass Cloudflare Access JWT cookie
|
|
177
|
+
const response = await fetch('/api/usage/projects', {
|
|
178
|
+
credentials: 'include',
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
console.warn('[ProjectSelect] Failed to fetch projects, using fallback');
|
|
182
|
+
return getFallbackProjects();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
if (!data.success || !Array.isArray(data.projects)) {
|
|
187
|
+
return getFallbackProjects();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Map to options: "All Projects" first, then sorted by resource count
|
|
191
|
+
const options: ProjectOption[] = [
|
|
192
|
+
{ value: 'all', label: 'All Projects', resourceCount: data.totalResources },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
for (const project of data.projects) {
|
|
196
|
+
options.push({
|
|
197
|
+
value: project.projectId,
|
|
198
|
+
label: `${project.displayName} (${project.resourceCount})`,
|
|
199
|
+
resourceCount: project.resourceCount,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
projectsCache = options;
|
|
204
|
+
return options;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('[ProjectSelect] Error fetching projects:', error);
|
|
207
|
+
return getFallbackProjects();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fallback projects if API fails
|
|
213
|
+
*/
|
|
214
|
+
function getFallbackProjects(): ProjectOption[] {
|
|
215
|
+
return [
|
|
216
|
+
{ value: 'all', label: 'All Projects' },
|
|
217
|
+
{ value: 'brand-copilot', label: 'Brand Copilot' },
|
|
218
|
+
{ value: 'scout', label: 'Scout' },
|
|
219
|
+
{ value: 'platform', label: 'Platform' },
|
|
220
|
+
{ value: 'aus-history', label: 'Australian History MCP' },
|
|
221
|
+
{ value: 'cloakpipe', label: 'CloakPipe' },
|
|
222
|
+
{ value: 'shared', label: 'Shared' },
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Populate the select element with project options
|
|
228
|
+
*/
|
|
229
|
+
function populateSelect(
|
|
230
|
+
select: HTMLSelectElement,
|
|
231
|
+
projects: ProjectOption[],
|
|
232
|
+
currentValue: string
|
|
233
|
+
): void {
|
|
234
|
+
// Clear existing options
|
|
235
|
+
select.innerHTML = '';
|
|
236
|
+
|
|
237
|
+
// Add options
|
|
238
|
+
for (const proj of projects) {
|
|
239
|
+
const option = document.createElement('option');
|
|
240
|
+
option.value = proj.value;
|
|
241
|
+
option.textContent = proj.label;
|
|
242
|
+
if (proj.value === currentValue) {
|
|
243
|
+
option.selected = true;
|
|
244
|
+
}
|
|
245
|
+
select.appendChild(option);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function initProjectSelect(): Promise<void> {
|
|
250
|
+
const container = document.querySelector('[data-component="project-select"]');
|
|
251
|
+
if (!container) return;
|
|
252
|
+
|
|
253
|
+
const select = container.querySelector<HTMLSelectElement>('#project-filter');
|
|
254
|
+
if (!select) return;
|
|
255
|
+
|
|
256
|
+
const initialProject = container.getAttribute('data-initial') || 'all';
|
|
257
|
+
|
|
258
|
+
// Fetch and populate projects from D1 registry
|
|
259
|
+
const projects = await fetchProjects();
|
|
260
|
+
populateSelect(select, projects, initialProject);
|
|
261
|
+
|
|
262
|
+
// Handle selection changes
|
|
263
|
+
select.addEventListener('change', () => {
|
|
264
|
+
setProject(select.value);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Subscribe to store changes and update UI
|
|
268
|
+
$project.subscribe((project) => {
|
|
269
|
+
if (select.value !== project) {
|
|
270
|
+
select.value = project;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Initialize on DOM ready
|
|
276
|
+
if (document.readyState === 'loading') {
|
|
277
|
+
document.addEventListener('DOMContentLoaded', () => initProjectSelect());
|
|
278
|
+
} else {
|
|
279
|
+
initProjectSelect();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Re-initialize on Astro page transitions
|
|
283
|
+
document.addEventListener('astro:page-load', () => initProjectSelect());
|
|
284
|
+
</script>
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tab Controller
|
|
3
|
+
*
|
|
4
|
+
* Handles AI Gateway and Workers AI data loading and rendering.
|
|
5
|
+
* Extracted from index.astro for task-22.5 (slim to <300 lines)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { formatAINumber, formatAICurrency } from './formatters';
|
|
9
|
+
|
|
10
|
+
// ========== Types ==========
|
|
11
|
+
|
|
12
|
+
export interface AIGatewayData {
|
|
13
|
+
totalRequests: number;
|
|
14
|
+
totalCachedRequests: number;
|
|
15
|
+
cacheHitRate: number;
|
|
16
|
+
tokensIn: number;
|
|
17
|
+
tokensOut: number;
|
|
18
|
+
totalCostUsd: number;
|
|
19
|
+
byProvider: Record<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
requests: number;
|
|
23
|
+
cachedRequests: number;
|
|
24
|
+
tokensIn: number;
|
|
25
|
+
tokensOut: number;
|
|
26
|
+
costUsd: number;
|
|
27
|
+
}
|
|
28
|
+
>;
|
|
29
|
+
byModel: Record<
|
|
30
|
+
string,
|
|
31
|
+
{
|
|
32
|
+
requests: number;
|
|
33
|
+
cachedRequests: number;
|
|
34
|
+
tokensIn: number;
|
|
35
|
+
tokensOut: number;
|
|
36
|
+
costUsd: number;
|
|
37
|
+
}
|
|
38
|
+
>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkersAIData {
|
|
42
|
+
totalRequests: number;
|
|
43
|
+
totalInputTokens: number;
|
|
44
|
+
totalOutputTokens: number;
|
|
45
|
+
totalCostUsd: number;
|
|
46
|
+
byProject: Record<string, { requests: number; costUsd: number; isEstimated: boolean }>;
|
|
47
|
+
byModel: Record<string, { requests: number; costUsd: number }>;
|
|
48
|
+
aiGateway?: AIGatewayData;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ========== State ==========
|
|
52
|
+
|
|
53
|
+
let aiDataLoaded = false;
|
|
54
|
+
let aiDataCache: WorkersAIData | null = null;
|
|
55
|
+
|
|
56
|
+
// ========== DOM Helpers ==========
|
|
57
|
+
|
|
58
|
+
function clearElement(el: HTMLElement): void {
|
|
59
|
+
while (el.firstChild) {
|
|
60
|
+
el.removeChild(el.firstChild);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createLoadingIndicator(message: string): HTMLElement {
|
|
65
|
+
const div = document.createElement('div');
|
|
66
|
+
div.className = 'ai-loading-indicator';
|
|
67
|
+
div.textContent = message;
|
|
68
|
+
return div;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createErrorState(message: string): HTMLElement {
|
|
72
|
+
const div = document.createElement('div');
|
|
73
|
+
div.className = 'ai-error-state';
|
|
74
|
+
div.textContent = message;
|
|
75
|
+
return div;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createEmptyState(message: string): HTMLElement {
|
|
79
|
+
const div = document.createElement('div');
|
|
80
|
+
div.className = 'ai-empty-state';
|
|
81
|
+
div.textContent = message;
|
|
82
|
+
return div;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ========== Data Loading ==========
|
|
86
|
+
|
|
87
|
+
export async function loadAITabData(period: string = '7d'): Promise<void> {
|
|
88
|
+
if (aiDataLoaded && aiDataCache) {
|
|
89
|
+
renderAIData(aiDataCache);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Show loading state
|
|
94
|
+
const containers = ['ai-gateway-content', 'ai-by-project', 'ai-by-model'];
|
|
95
|
+
containers.forEach((id) => {
|
|
96
|
+
const el = document.getElementById(id);
|
|
97
|
+
if (el) {
|
|
98
|
+
clearElement(el);
|
|
99
|
+
el.appendChild(createLoadingIndicator('Loading AI data...'));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Include credentials to pass Cloudflare Access JWT cookie
|
|
105
|
+
const response = await fetch(`/api/usage/workersai?period=${period}`, {
|
|
106
|
+
credentials: 'include',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const json = await response.json();
|
|
114
|
+
|
|
115
|
+
if (!json.success || !json.data) {
|
|
116
|
+
throw new Error(json.error || 'Invalid response');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
aiDataCache = json.data;
|
|
120
|
+
aiDataLoaded = true;
|
|
121
|
+
renderAIData(json.data);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Failed to load AI data:', error);
|
|
124
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
125
|
+
containers.forEach((id) => {
|
|
126
|
+
const el = document.getElementById(id);
|
|
127
|
+
if (el) {
|
|
128
|
+
clearElement(el);
|
|
129
|
+
el.appendChild(createErrorState('Failed to load AI data: ' + errorMsg));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const statIds = ['ai-total-requests', 'ai-total-cost', 'ai-input-tokens', 'ai-output-tokens'];
|
|
134
|
+
statIds.forEach((id) => {
|
|
135
|
+
const el = document.getElementById(id);
|
|
136
|
+
if (el) el.textContent = 'Error';
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ========== Rendering ==========
|
|
142
|
+
|
|
143
|
+
function renderAIData(data: WorkersAIData): void {
|
|
144
|
+
// Update stats
|
|
145
|
+
const totalRequestsEl = document.getElementById('ai-total-requests');
|
|
146
|
+
const totalCostEl = document.getElementById('ai-total-cost');
|
|
147
|
+
const inputTokensEl = document.getElementById('ai-input-tokens');
|
|
148
|
+
const outputTokensEl = document.getElementById('ai-output-tokens');
|
|
149
|
+
|
|
150
|
+
if (totalRequestsEl) totalRequestsEl.textContent = formatAINumber(data.totalRequests);
|
|
151
|
+
if (totalCostEl) totalCostEl.textContent = formatAICurrency(data.totalCostUsd);
|
|
152
|
+
if (inputTokensEl) inputTokensEl.textContent = formatAINumber(data.totalInputTokens);
|
|
153
|
+
if (outputTokensEl) outputTokensEl.textContent = formatAINumber(data.totalOutputTokens);
|
|
154
|
+
|
|
155
|
+
// Render AI Gateway content with real data if available
|
|
156
|
+
renderAIGatewaySection(data.aiGateway);
|
|
157
|
+
|
|
158
|
+
// Render by project table
|
|
159
|
+
renderByProjectTable(data.byProject);
|
|
160
|
+
|
|
161
|
+
// Render by model table
|
|
162
|
+
renderByModelTable(data.byModel);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderAIGatewaySection(aiGateway?: AIGatewayData): void {
|
|
166
|
+
const gatewayEl = document.getElementById('ai-gateway-content');
|
|
167
|
+
if (!gatewayEl) return;
|
|
168
|
+
|
|
169
|
+
clearElement(gatewayEl);
|
|
170
|
+
|
|
171
|
+
// If no AI Gateway data, show placeholder
|
|
172
|
+
if (!aiGateway) {
|
|
173
|
+
const emptyDiv = createEmptyState('No AI Gateway data available for this period');
|
|
174
|
+
gatewayEl.appendChild(emptyDiv);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create metrics grid
|
|
179
|
+
const metricsGrid = document.createElement('div');
|
|
180
|
+
metricsGrid.className = 'ai-gateway-metrics-grid';
|
|
181
|
+
|
|
182
|
+
const metrics = [
|
|
183
|
+
{ value: formatAINumber(aiGateway.totalRequests), label: 'Gateway Requests' },
|
|
184
|
+
{ value: `${aiGateway.cacheHitRate.toFixed(1)}%`, label: 'Cache Hit Rate' },
|
|
185
|
+
{ value: formatAINumber(aiGateway.tokensIn), label: 'Tokens In' },
|
|
186
|
+
{ value: formatAINumber(aiGateway.tokensOut), label: 'Tokens Out' },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
metrics.forEach((metric) => {
|
|
190
|
+
const metricDiv = document.createElement('div');
|
|
191
|
+
metricDiv.className = 'ai-gateway-metric';
|
|
192
|
+
|
|
193
|
+
const valueDiv = document.createElement('div');
|
|
194
|
+
valueDiv.className = 'metric-value';
|
|
195
|
+
valueDiv.textContent = metric.value;
|
|
196
|
+
metricDiv.appendChild(valueDiv);
|
|
197
|
+
|
|
198
|
+
const labelDiv = document.createElement('div');
|
|
199
|
+
labelDiv.className = 'metric-label';
|
|
200
|
+
labelDiv.textContent = metric.label;
|
|
201
|
+
metricDiv.appendChild(labelDiv);
|
|
202
|
+
|
|
203
|
+
metricsGrid.appendChild(metricDiv);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
gatewayEl.appendChild(metricsGrid);
|
|
207
|
+
|
|
208
|
+
// Add provider breakdown if available
|
|
209
|
+
const providers = Object.entries(aiGateway.byProvider);
|
|
210
|
+
if (providers.length > 0) {
|
|
211
|
+
const providerSection = document.createElement('div');
|
|
212
|
+
providerSection.className = 'ai-gateway-providers';
|
|
213
|
+
|
|
214
|
+
const providerHeader = document.createElement('h4');
|
|
215
|
+
providerHeader.textContent = 'By Provider';
|
|
216
|
+
providerSection.appendChild(providerHeader);
|
|
217
|
+
|
|
218
|
+
const providerTable = document.createElement('table');
|
|
219
|
+
providerTable.className = 'ai-table ai-table-compact';
|
|
220
|
+
|
|
221
|
+
// Header
|
|
222
|
+
const thead = document.createElement('thead');
|
|
223
|
+
const headerRow = document.createElement('tr');
|
|
224
|
+
['Provider', 'Requests', 'Cached', 'Cost'].forEach((text) => {
|
|
225
|
+
const th = document.createElement('th');
|
|
226
|
+
th.textContent = text;
|
|
227
|
+
headerRow.appendChild(th);
|
|
228
|
+
});
|
|
229
|
+
thead.appendChild(headerRow);
|
|
230
|
+
providerTable.appendChild(thead);
|
|
231
|
+
|
|
232
|
+
// Body
|
|
233
|
+
const tbody = document.createElement('tbody');
|
|
234
|
+
providers.sort((a, b) => b[1].requests - a[1].requests);
|
|
235
|
+
providers.forEach(([provider, data]) => {
|
|
236
|
+
const row = document.createElement('tr');
|
|
237
|
+
|
|
238
|
+
const nameCell = document.createElement('td');
|
|
239
|
+
nameCell.className = 'provider-name';
|
|
240
|
+
nameCell.textContent = provider;
|
|
241
|
+
row.appendChild(nameCell);
|
|
242
|
+
|
|
243
|
+
const requestsCell = document.createElement('td');
|
|
244
|
+
requestsCell.textContent = formatAINumber(data.requests);
|
|
245
|
+
row.appendChild(requestsCell);
|
|
246
|
+
|
|
247
|
+
const cachedCell = document.createElement('td');
|
|
248
|
+
cachedCell.textContent = formatAINumber(data.cachedRequests);
|
|
249
|
+
row.appendChild(cachedCell);
|
|
250
|
+
|
|
251
|
+
const costCell = document.createElement('td');
|
|
252
|
+
costCell.className = 'cost-cell';
|
|
253
|
+
costCell.textContent = formatAICurrency(data.costUsd);
|
|
254
|
+
row.appendChild(costCell);
|
|
255
|
+
|
|
256
|
+
tbody.appendChild(row);
|
|
257
|
+
});
|
|
258
|
+
providerTable.appendChild(tbody);
|
|
259
|
+
providerSection.appendChild(providerTable);
|
|
260
|
+
gatewayEl.appendChild(providerSection);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderByProjectTable(
|
|
265
|
+
byProject: Record<string, { requests: number; costUsd: number; isEstimated: boolean }>
|
|
266
|
+
): void {
|
|
267
|
+
const containerEl = document.getElementById('ai-by-project');
|
|
268
|
+
if (!containerEl) return;
|
|
269
|
+
|
|
270
|
+
clearElement(containerEl);
|
|
271
|
+
|
|
272
|
+
const projects = Object.entries(byProject);
|
|
273
|
+
if (projects.length === 0) {
|
|
274
|
+
containerEl.appendChild(createEmptyState('No Workers AI usage data available'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Sort by cost descending
|
|
279
|
+
projects.sort((a, b) => b[1].costUsd - a[1].costUsd);
|
|
280
|
+
|
|
281
|
+
const table = document.createElement('table');
|
|
282
|
+
table.className = 'ai-table';
|
|
283
|
+
|
|
284
|
+
// Header
|
|
285
|
+
const thead = document.createElement('thead');
|
|
286
|
+
const headerRow = document.createElement('tr');
|
|
287
|
+
['Project', 'Requests', 'Est. Cost'].forEach((text) => {
|
|
288
|
+
const th = document.createElement('th');
|
|
289
|
+
th.textContent = text;
|
|
290
|
+
headerRow.appendChild(th);
|
|
291
|
+
});
|
|
292
|
+
thead.appendChild(headerRow);
|
|
293
|
+
table.appendChild(thead);
|
|
294
|
+
|
|
295
|
+
// Body
|
|
296
|
+
const tbody = document.createElement('tbody');
|
|
297
|
+
let hasEstimated = false;
|
|
298
|
+
|
|
299
|
+
projects.forEach(([projectName, projectData]) => {
|
|
300
|
+
const row = document.createElement('tr');
|
|
301
|
+
|
|
302
|
+
// Project name cell
|
|
303
|
+
const nameCell = document.createElement('td');
|
|
304
|
+
nameCell.className = 'project-name';
|
|
305
|
+
nameCell.textContent = projectName;
|
|
306
|
+
|
|
307
|
+
if (projectData.isEstimated) {
|
|
308
|
+
hasEstimated = true;
|
|
309
|
+
const badge = document.createElement('span');
|
|
310
|
+
badge.className = 'estimated-badge';
|
|
311
|
+
badge.textContent = '~estimated';
|
|
312
|
+
nameCell.appendChild(badge);
|
|
313
|
+
}
|
|
314
|
+
row.appendChild(nameCell);
|
|
315
|
+
|
|
316
|
+
// Requests cell
|
|
317
|
+
const requestsCell = document.createElement('td');
|
|
318
|
+
requestsCell.textContent = formatAINumber(projectData.requests);
|
|
319
|
+
row.appendChild(requestsCell);
|
|
320
|
+
|
|
321
|
+
// Cost cell
|
|
322
|
+
const costCell = document.createElement('td');
|
|
323
|
+
costCell.className = 'cost-cell';
|
|
324
|
+
costCell.textContent = formatAICurrency(projectData.costUsd);
|
|
325
|
+
row.appendChild(costCell);
|
|
326
|
+
|
|
327
|
+
tbody.appendChild(row);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
table.appendChild(tbody);
|
|
331
|
+
containerEl.appendChild(table);
|
|
332
|
+
|
|
333
|
+
if (hasEstimated) {
|
|
334
|
+
const note = document.createElement('p');
|
|
335
|
+
note.className = 'ai-note';
|
|
336
|
+
note.textContent =
|
|
337
|
+
'* Estimated costs are calculated from request counts and average token usage.';
|
|
338
|
+
containerEl.appendChild(note);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderByModelTable(byModel: Record<string, { requests: number; costUsd: number }>): void {
|
|
343
|
+
const containerEl = document.getElementById('ai-by-model');
|
|
344
|
+
if (!containerEl) return;
|
|
345
|
+
|
|
346
|
+
clearElement(containerEl);
|
|
347
|
+
|
|
348
|
+
const models = Object.entries(byModel);
|
|
349
|
+
if (models.length === 0) {
|
|
350
|
+
containerEl.appendChild(createEmptyState('No model-level data available'));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Sort by cost descending
|
|
355
|
+
models.sort((a, b) => b[1].costUsd - a[1].costUsd);
|
|
356
|
+
|
|
357
|
+
const table = document.createElement('table');
|
|
358
|
+
table.className = 'ai-table';
|
|
359
|
+
|
|
360
|
+
// Header
|
|
361
|
+
const thead = document.createElement('thead');
|
|
362
|
+
const headerRow = document.createElement('tr');
|
|
363
|
+
['Model', 'Requests', 'Est. Cost'].forEach((text) => {
|
|
364
|
+
const th = document.createElement('th');
|
|
365
|
+
th.textContent = text;
|
|
366
|
+
headerRow.appendChild(th);
|
|
367
|
+
});
|
|
368
|
+
thead.appendChild(headerRow);
|
|
369
|
+
table.appendChild(thead);
|
|
370
|
+
|
|
371
|
+
// Body
|
|
372
|
+
const tbody = document.createElement('tbody');
|
|
373
|
+
|
|
374
|
+
models.forEach(([modelName, modelData]) => {
|
|
375
|
+
const row = document.createElement('tr');
|
|
376
|
+
|
|
377
|
+
// Model name cell (formatted)
|
|
378
|
+
const nameCell = document.createElement('td');
|
|
379
|
+
nameCell.className = 'model-name';
|
|
380
|
+
nameCell.textContent = modelName.replace('@cf/', '').replace('meta/', '');
|
|
381
|
+
row.appendChild(nameCell);
|
|
382
|
+
|
|
383
|
+
// Requests cell
|
|
384
|
+
const requestsCell = document.createElement('td');
|
|
385
|
+
requestsCell.textContent = formatAINumber(modelData.requests);
|
|
386
|
+
row.appendChild(requestsCell);
|
|
387
|
+
|
|
388
|
+
// Cost cell
|
|
389
|
+
const costCell = document.createElement('td');
|
|
390
|
+
costCell.className = 'cost-cell';
|
|
391
|
+
costCell.textContent = formatAICurrency(modelData.costUsd);
|
|
392
|
+
row.appendChild(costCell);
|
|
393
|
+
|
|
394
|
+
tbody.appendChild(row);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
table.appendChild(tbody);
|
|
398
|
+
containerEl.appendChild(table);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ========== Reset State ==========
|
|
402
|
+
|
|
403
|
+
export function resetAITabState(): void {
|
|
404
|
+
aiDataLoaded = false;
|
|
405
|
+
aiDataCache = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ========== Event Listener Setup ==========
|
|
409
|
+
|
|
410
|
+
export function setupAITabEventListeners(): void {
|
|
411
|
+
// Reload AI data when period changes
|
|
412
|
+
document.addEventListener('period-change', () => {
|
|
413
|
+
resetAITabState();
|
|
414
|
+
const aiPanel = document.getElementById('tab-ai');
|
|
415
|
+
if (aiPanel && aiPanel.style.display !== 'none') {
|
|
416
|
+
loadAITabData();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|