@littlebearapps/platform-admin-sdk 1.4.2 → 2.0.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/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/scripts/ops/universal-backfill.ts +147 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
type: 'error' | 'warning' | 'info';
|
|
5
|
+
message: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
dismissable?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AlertBanner({ type, message, detail, dismissable = true }: Props) {
|
|
11
|
+
const [dismissed, setDismissed] = useState(false);
|
|
12
|
+
if (dismissed) return null;
|
|
13
|
+
|
|
14
|
+
const styles = {
|
|
15
|
+
error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200',
|
|
16
|
+
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200',
|
|
17
|
+
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={`border rounded-lg p-4 ${styles[type]}`}>
|
|
22
|
+
<div className="flex items-start justify-between gap-2">
|
|
23
|
+
<div>
|
|
24
|
+
<p className="font-medium">{message}</p>
|
|
25
|
+
{detail && <p className="text-sm mt-1 opacity-80">{detail}</p>}
|
|
26
|
+
</div>
|
|
27
|
+
{dismissable && (
|
|
28
|
+
<button
|
|
29
|
+
onClick={() => setDismissed(true)}
|
|
30
|
+
className="text-current opacity-50 hover:opacity-100 p-1"
|
|
31
|
+
aria-label="Dismiss"
|
|
32
|
+
>
|
|
33
|
+
x
|
|
34
|
+
</button>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface Crumb {
|
|
2
|
+
label: string;
|
|
3
|
+
href?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
items: Crumb[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Breadcrumbs({ items }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<nav className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
13
|
+
{items.map((item, i) => (
|
|
14
|
+
<span key={i} className="flex items-center gap-1.5">
|
|
15
|
+
{i > 0 && <span className="opacity-40">/</span>}
|
|
16
|
+
{item.href ? (
|
|
17
|
+
<a href={item.href} className="hover:text-gray-900 dark:hover:text-white transition-colors">
|
|
18
|
+
{item.label}
|
|
19
|
+
</a>
|
|
20
|
+
) : (
|
|
21
|
+
<span className="text-gray-900 dark:text-white font-medium">{item.label}</span>
|
|
22
|
+
)}
|
|
23
|
+
</span>
|
|
24
|
+
))}
|
|
25
|
+
</nav>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
icon?: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
action?: { label: string; href: string };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function EmptyState({ icon = '\u2205', title, description, action }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
11
|
+
<span className="text-4xl mb-3 opacity-40">{icon}</span>
|
|
12
|
+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
|
13
|
+
{description && (
|
|
14
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400 max-w-sm">{description}</p>
|
|
15
|
+
)}
|
|
16
|
+
{action && (
|
|
17
|
+
<a
|
|
18
|
+
href={action.href}
|
|
19
|
+
className="mt-4 text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
|
|
20
|
+
>
|
|
21
|
+
{action.label}
|
|
22
|
+
</a>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ReactNode, ErrorInfo } from 'react';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface State {
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
error?: Error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
15
|
+
constructor(props: Props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { hasError: false };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error: Error): State {
|
|
21
|
+
return { hasError: true, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
25
|
+
console.error('[ErrorBoundary]', error, errorInfo);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render(): ReactNode {
|
|
29
|
+
if (this.state.hasError) {
|
|
30
|
+
return (
|
|
31
|
+
this.props.fallback ?? (
|
|
32
|
+
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
33
|
+
<p className="text-sm font-medium text-red-800 dark:text-red-300">Something went wrong</p>
|
|
34
|
+
<p className="text-xs text-red-600 dark:text-red-400 mt-1">{this.state.error?.message}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.props.children;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
lines?: number;
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function LoadingSkeleton({ lines = 3, className = '' }: Props) {
|
|
7
|
+
return (
|
|
8
|
+
<div className={`animate-pulse space-y-3 ${className}`}>
|
|
9
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
10
|
+
<div
|
|
11
|
+
key={i}
|
|
12
|
+
className="h-4 bg-gray-200 dark:bg-gray-700 rounded"
|
|
13
|
+
style={{ width: `${85 - i * 15}%` }}
|
|
14
|
+
/>
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Breadcrumbs } from './Breadcrumbs';
|
|
3
|
+
|
|
4
|
+
interface PageShellProps {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
breadcrumbs?: Array<{ label: string; href?: string }>;
|
|
8
|
+
actions?: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PageShell({ title, description, breadcrumbs, actions, children }: PageShellProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-4">
|
|
15
|
+
{breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
|
|
16
|
+
<div className="flex items-start justify-between gap-4">
|
|
17
|
+
<div>
|
|
18
|
+
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">{title}</h1>
|
|
19
|
+
{description && <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{description}</p>}
|
|
20
|
+
</div>
|
|
21
|
+
{actions && <div className="shrink-0">{actions}</div>}
|
|
22
|
+
</div>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sparkline Component
|
|
3
|
+
*
|
|
4
|
+
* Inline SVG sparkline for activity trends.
|
|
5
|
+
* Minimal, industrial aesthetic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
interface SparklineProps {
|
|
11
|
+
data: number[];
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
color?: string;
|
|
15
|
+
strokeWidth?: number;
|
|
16
|
+
showFill?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Sparkline({
|
|
21
|
+
data,
|
|
22
|
+
width = 60,
|
|
23
|
+
height = 20,
|
|
24
|
+
color = '#3b82f6',
|
|
25
|
+
strokeWidth = 1.5,
|
|
26
|
+
showFill = true,
|
|
27
|
+
className = '',
|
|
28
|
+
}: SparklineProps) {
|
|
29
|
+
const pathData = useMemo(() => {
|
|
30
|
+
if (data.length < 2) return { line: '', fill: '' };
|
|
31
|
+
|
|
32
|
+
const min = Math.min(...data);
|
|
33
|
+
const max = Math.max(...data);
|
|
34
|
+
const range = max - min || 1;
|
|
35
|
+
|
|
36
|
+
// Padding to prevent clipping
|
|
37
|
+
const padding = 2;
|
|
38
|
+
const chartWidth = width - padding * 2;
|
|
39
|
+
const chartHeight = height - padding * 2;
|
|
40
|
+
|
|
41
|
+
const points = data.map((value, index) => {
|
|
42
|
+
const x = padding + (index / (data.length - 1)) * chartWidth;
|
|
43
|
+
const y = padding + chartHeight - ((value - min) / range) * chartHeight;
|
|
44
|
+
return { x, y };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Build line path
|
|
48
|
+
const linePath = points
|
|
49
|
+
.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
|
|
50
|
+
.join(' ');
|
|
51
|
+
|
|
52
|
+
// Build fill path (close at bottom)
|
|
53
|
+
const fillPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${padding} ${height - padding} Z`;
|
|
54
|
+
|
|
55
|
+
return { line: linePath, fill: fillPath };
|
|
56
|
+
}, [data, width, height]);
|
|
57
|
+
|
|
58
|
+
if (data.length < 2) {
|
|
59
|
+
return (
|
|
60
|
+
<svg
|
|
61
|
+
width={width}
|
|
62
|
+
height={height}
|
|
63
|
+
className={className}
|
|
64
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
>
|
|
67
|
+
<line
|
|
68
|
+
x1={2}
|
|
69
|
+
y1={height / 2}
|
|
70
|
+
x2={width - 2}
|
|
71
|
+
y2={height / 2}
|
|
72
|
+
stroke={color}
|
|
73
|
+
strokeWidth={strokeWidth}
|
|
74
|
+
strokeOpacity={0.3}
|
|
75
|
+
strokeDasharray="2 2"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<svg
|
|
83
|
+
width={width}
|
|
84
|
+
height={height}
|
|
85
|
+
className={className}
|
|
86
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
>
|
|
89
|
+
{/* Fill gradient */}
|
|
90
|
+
{showFill && (
|
|
91
|
+
<>
|
|
92
|
+
<defs>
|
|
93
|
+
<linearGradient
|
|
94
|
+
id={`sparkline-fill-${color.replace('#', '')}`}
|
|
95
|
+
x1="0"
|
|
96
|
+
y1="0"
|
|
97
|
+
x2="0"
|
|
98
|
+
y2="1"
|
|
99
|
+
>
|
|
100
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
|
101
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
102
|
+
</linearGradient>
|
|
103
|
+
</defs>
|
|
104
|
+
<path d={pathData.fill} fill={`url(#sparkline-fill-${color.replace('#', '')})`} />
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
{/* Line */}
|
|
108
|
+
<path
|
|
109
|
+
d={pathData.line}
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke={color}
|
|
112
|
+
strokeWidth={strokeWidth}
|
|
113
|
+
strokeLinecap="round"
|
|
114
|
+
strokeLinejoin="round"
|
|
115
|
+
/>
|
|
116
|
+
{/* End dot */}
|
|
117
|
+
<circle
|
|
118
|
+
cx={width - 2}
|
|
119
|
+
cy={pathData.line ? parseFloat(pathData.line.split(' ').slice(-1)[0]) : height / 2}
|
|
120
|
+
r={2}
|
|
121
|
+
fill={color}
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default Sparkline;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
status: 'green' | 'yellow' | 'red' | 'gray';
|
|
5
|
+
size?: 'sm' | 'md';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function StatusDot({ status, size = 'sm' }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<span
|
|
11
|
+
className={clsx(
|
|
12
|
+
'inline-block rounded-full',
|
|
13
|
+
size === 'sm' ? 'w-2 h-2' : 'w-3 h-3',
|
|
14
|
+
status === 'green' && 'bg-green-500',
|
|
15
|
+
status === 'yellow' && 'bg-yellow-500',
|
|
16
|
+
status === 'red' && 'bg-red-500',
|
|
17
|
+
status === 'gray' && 'bg-gray-400',
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
type: 'success' | 'error' | 'info';
|
|
5
|
+
message: string;
|
|
6
|
+
duration?: number;
|
|
7
|
+
onDismiss?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const styles: Record<Props['type'], string> = {
|
|
11
|
+
success: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200',
|
|
12
|
+
error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200',
|
|
13
|
+
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Toast({ type, message, duration = 5000, onDismiss }: Props) {
|
|
17
|
+
const [visible, setVisible] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (duration <= 0) return;
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
setVisible(false);
|
|
23
|
+
onDismiss?.();
|
|
24
|
+
}, duration);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}, [duration, onDismiss]);
|
|
27
|
+
|
|
28
|
+
if (!visible) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`fixed bottom-4 right-4 z-50 border rounded-lg px-4 py-3 shadow-lg max-w-sm ${styles[type]}`}>
|
|
32
|
+
<div className="flex items-center justify-between gap-3">
|
|
33
|
+
<p className="text-sm font-medium">{message}</p>
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => { setVisible(false); onDismiss?.(); }}
|
|
36
|
+
className="text-current opacity-50 hover:opacity-100 shrink-0"
|
|
37
|
+
aria-label="Dismiss"
|
|
38
|
+
>
|
|
39
|
+
x
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Sparkline } from './Sparkline';
|
|
2
|
+
export { StatusDot } from './StatusDot';
|
|
3
|
+
export { AlertBanner } from './AlertBanner';
|
|
4
|
+
export { EmptyState } from './EmptyState';
|
|
5
|
+
export { LoadingSkeleton } from './LoadingSkeleton';
|
|
6
|
+
export { Breadcrumbs } from './Breadcrumbs';
|
|
7
|
+
export { Toast } from './Toast';
|
|
8
|
+
export { ErrorBoundary } from './ErrorBoundary';
|
|
9
|
+
export { PageShell } from './PageShell';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { EmptyState } from '../ui/EmptyState';
|
|
3
|
+
import { LoadingSkeleton } from '../ui/LoadingSkeleton';
|
|
4
|
+
|
|
5
|
+
interface Anomaly {
|
|
6
|
+
id: number;
|
|
7
|
+
project: string;
|
|
8
|
+
metric: string;
|
|
9
|
+
current_value: number;
|
|
10
|
+
baseline_value: number;
|
|
11
|
+
deviation_pct: number;
|
|
12
|
+
detected_at: string;
|
|
13
|
+
status: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AnomaliesWidget() {
|
|
17
|
+
const [anomalies, setAnomalies] = useState<Anomaly[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
fetch('/api/usage/anomalies')
|
|
22
|
+
.then(res => res.json())
|
|
23
|
+
.then((data: { anomalies: Anomaly[] }) => { setAnomalies(data.anomalies); setLoading(false); })
|
|
24
|
+
.catch(() => setLoading(false));
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
if (loading) return <LoadingSkeleton lines={3} />;
|
|
28
|
+
|
|
29
|
+
if (anomalies.length === 0) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
32
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
33
|
+
Anomalies
|
|
34
|
+
</h3>
|
|
35
|
+
<EmptyState title="No anomalies detected" description="Usage patterns look normal." />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
42
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
43
|
+
Recent Anomalies
|
|
44
|
+
</h3>
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
{anomalies.map(a => (
|
|
47
|
+
<div key={a.id} className="flex items-center justify-between py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
|
48
|
+
<div>
|
|
49
|
+
<p className="text-sm text-gray-900 dark:text-white">
|
|
50
|
+
{a.metric.replace(/_/g, ' ')} — {a.project}
|
|
51
|
+
</p>
|
|
52
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
53
|
+
{new Date(a.detected_at).toLocaleDateString()}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
|
57
|
+
a.deviation_pct > 200
|
|
58
|
+
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
59
|
+
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
|
60
|
+
}`}>
|
|
61
|
+
+{a.deviation_pct}%
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Sparkline } from '../ui/Sparkline';
|
|
3
|
+
import { LoadingSkeleton } from '../ui/LoadingSkeleton';
|
|
4
|
+
|
|
5
|
+
interface Snapshot {
|
|
6
|
+
snapshot_hour: string;
|
|
7
|
+
d1_reads: number;
|
|
8
|
+
d1_writes: number;
|
|
9
|
+
kv_reads: number;
|
|
10
|
+
kv_writes: number;
|
|
11
|
+
total_cost_usd: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
hours?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function HourlyUsageChart({ hours = 24 }: Props) {
|
|
19
|
+
const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetch(`/api/usage/hourly?hours=${hours}`)
|
|
24
|
+
.then(res => res.json())
|
|
25
|
+
.then((data: { snapshots: Snapshot[] }) => { setSnapshots(data.snapshots); setLoading(false); })
|
|
26
|
+
.catch(() => setLoading(false));
|
|
27
|
+
}, [hours]);
|
|
28
|
+
|
|
29
|
+
if (loading) return <LoadingSkeleton lines={3} />;
|
|
30
|
+
|
|
31
|
+
const costData = snapshots.map(s => s.total_cost_usd);
|
|
32
|
+
const d1WriteData = snapshots.map(s => s.d1_writes);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
36
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
37
|
+
Hourly Usage ({hours}h)
|
|
38
|
+
</h3>
|
|
39
|
+
{snapshots.length === 0 ? (
|
|
40
|
+
<p className="text-sm text-gray-400">No hourly data available yet.</p>
|
|
41
|
+
) : (
|
|
42
|
+
<div className="space-y-4">
|
|
43
|
+
<div>
|
|
44
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Cost (USD)</p>
|
|
45
|
+
<Sparkline data={costData} height={40} color="blue" />
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">D1 Writes</p>
|
|
49
|
+
<Sparkline data={d1WriteData} height={40} color="amber" />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../ui/LoadingSkeleton';
|
|
3
|
+
|
|
4
|
+
interface Allowance {
|
|
5
|
+
resource: string;
|
|
6
|
+
allowance: number;
|
|
7
|
+
used: number;
|
|
8
|
+
pct: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatNumber(n: number): string {
|
|
12
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
13
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
14
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
15
|
+
return String(n);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pctColor(pct: number): string {
|
|
19
|
+
if (pct >= 90) return 'bg-red-500';
|
|
20
|
+
if (pct >= 75) return 'bg-orange-500';
|
|
21
|
+
if (pct >= 50) return 'bg-yellow-500';
|
|
22
|
+
return 'bg-green-500';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PlanAllowanceDashboard() {
|
|
26
|
+
const [allowances, setAllowances] = useState<Allowance[]>([]);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
fetch('/api/usage/allowances')
|
|
31
|
+
.then(res => res.json())
|
|
32
|
+
.then((data: { allowances: Allowance[] }) => { setAllowances(data.allowances); setLoading(false); })
|
|
33
|
+
.catch(() => setLoading(false));
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
if (loading) return <LoadingSkeleton lines={4} />;
|
|
37
|
+
|
|
38
|
+
if (allowances.length === 0) {
|
|
39
|
+
return <p className="text-sm text-gray-500 dark:text-gray-400">No allowance data available.</p>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
44
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
45
|
+
Plan Allowances (MTD)
|
|
46
|
+
</h3>
|
|
47
|
+
<div className="space-y-3">
|
|
48
|
+
{allowances.map(a => (
|
|
49
|
+
<div key={a.resource}>
|
|
50
|
+
<div className="flex items-center justify-between text-sm mb-1">
|
|
51
|
+
<span className="text-gray-700 dark:text-gray-300">{a.resource.replace(/_/g, ' ')}</span>
|
|
52
|
+
<span className="text-gray-500 dark:text-gray-400">
|
|
53
|
+
{formatNumber(a.used)} / {formatNumber(a.allowance)} ({a.pct}%)
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
57
|
+
<div
|
|
58
|
+
className={`h-full rounded-full transition-all ${pctColor(a.pct)}`}
|
|
59
|
+
style={{ width: `${Math.min(a.pct, 100)}%` }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../ui/LoadingSkeleton';
|
|
3
|
+
import { EmptyState } from '../ui/EmptyState';
|
|
4
|
+
|
|
5
|
+
interface ProjectCost {
|
|
6
|
+
project: string;
|
|
7
|
+
total_cost_usd: number;
|
|
8
|
+
d1_writes: number;
|
|
9
|
+
worker_requests: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProjectCostBreakdown() {
|
|
13
|
+
const [projects, setProjects] = useState<ProjectCost[]>([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
fetch('/api/usage/projects')
|
|
18
|
+
.then((r) => r.json())
|
|
19
|
+
.then((data: { projects: ProjectCost[] }) => {
|
|
20
|
+
setProjects(data.projects ?? []);
|
|
21
|
+
setLoading(false);
|
|
22
|
+
})
|
|
23
|
+
.catch(() => setLoading(false));
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
if (loading) return <LoadingSkeleton lines={4} />;
|
|
27
|
+
if (projects.length === 0) return <EmptyState title="No project data" description="Project cost data will appear after usage collection runs." />;
|
|
28
|
+
|
|
29
|
+
const maxCost = Math.max(...projects.map((p) => p.total_cost_usd), 0.01);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-3">
|
|
33
|
+
{projects.map((p) => (
|
|
34
|
+
<div key={p.project} className="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 p-3">
|
|
35
|
+
<div className="flex items-center justify-between mb-1">
|
|
36
|
+
<span className="text-sm font-medium text-gray-900 dark:text-white">{p.project}</span>
|
|
37
|
+
<span className="text-sm font-mono text-gray-700 dark:text-gray-300">
|
|
38
|
+
${p.total_cost_usd.toFixed(2)}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
|
42
|
+
<div
|
|
43
|
+
className="bg-blue-500 h-1.5 rounded-full transition-all"
|
|
44
|
+
style={{ width: `${Math.min((p.total_cost_usd / maxCost) * 100, 100)}%` }}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex gap-4 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
48
|
+
<span>D1 writes: {p.d1_writes.toLocaleString()}</span>
|
|
49
|
+
<span>Requests: {p.worker_requests.toLocaleString()}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/// <reference types="astro/client" />
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB: D1Database;
|
|
5
|
+
PLATFORM_CACHE: KVNamespace;
|
|
6
|
+
USAGE_API: Fetcher;
|
|
7
|
+
{{#if isStandard}}
|
|
8
|
+
ERROR_COLLECTOR_API: Fetcher;
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if isFull}}
|
|
11
|
+
PATTERN_DISCOVERY_API: Fetcher;
|
|
12
|
+
NOTIFICATIONS_API: Fetcher;
|
|
13
|
+
SETTINGS_API: Fetcher;
|
|
14
|
+
SEARCH_API: Fetcher;
|
|
15
|
+
{{/if}}
|
|
16
|
+
CF_ACCESS_AUD?: string;
|
|
17
|
+
CF_ACCESS_ISSUER?: string;
|
|
18
|
+
CF_ACCESS_TEAM_DOMAIN?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare namespace App {
|
|
22
|
+
interface Locals {
|
|
23
|
+
runtime?: {
|
|
24
|
+
env: Env;
|
|
25
|
+
};
|
|
26
|
+
user?: {
|
|
27
|
+
email?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
sub?: string;
|
|
30
|
+
audience?: unknown;
|
|
31
|
+
issuer?: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|