@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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Quadrant - Mission Control
|
|
3
|
+
* Shows MTD spend, daily burn rate, projected monthly, sparkline, vs budget gauge
|
|
4
|
+
*/
|
|
5
|
+
import { Sparkline } from '../ui/Sparkline';
|
|
6
|
+
|
|
7
|
+
interface CostData {
|
|
8
|
+
mtdSpend: number;
|
|
9
|
+
dailyBurnRate: number;
|
|
10
|
+
projectedMonthly: number;
|
|
11
|
+
budgetPct: number;
|
|
12
|
+
monthlyBudget: number;
|
|
13
|
+
dailyTrend: number[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
data: CostData;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CostQuadrant({ data, loading }: Props) {
|
|
22
|
+
if (loading) {
|
|
23
|
+
return <QuadrantSkeleton />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const budgetStatus =
|
|
27
|
+
data.budgetPct > 150
|
|
28
|
+
? { label: 'Over Budget', colour: 'text-red-600 dark:text-red-400' }
|
|
29
|
+
: data.budgetPct > 100
|
|
30
|
+
? { label: 'Warning', colour: 'text-yellow-600 dark:text-yellow-400' }
|
|
31
|
+
: { label: 'On Track', colour: 'text-green-600 dark:text-green-400' };
|
|
32
|
+
|
|
33
|
+
const gaugeColour =
|
|
34
|
+
data.budgetPct > 150
|
|
35
|
+
? 'bg-red-500'
|
|
36
|
+
: data.budgetPct > 100
|
|
37
|
+
? 'bg-yellow-500'
|
|
38
|
+
: 'bg-green-500';
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
42
|
+
<div className="flex items-center justify-between mb-4">
|
|
43
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
44
|
+
Cost Status
|
|
45
|
+
</h3>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
49
|
+
{/* MTD Spend */}
|
|
50
|
+
<div>
|
|
51
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
52
|
+
${data.mtdSpend.toFixed(2)}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
55
|
+
MTD Spend
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Daily Burn */}
|
|
60
|
+
<div>
|
|
61
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
62
|
+
${data.dailyBurnRate.toFixed(2)}
|
|
63
|
+
</div>
|
|
64
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
65
|
+
Daily Burn
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Projected */}
|
|
70
|
+
<div>
|
|
71
|
+
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
72
|
+
${data.projectedMonthly.toFixed(2)}
|
|
73
|
+
</div>
|
|
74
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
75
|
+
Projected
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Budget Status */}
|
|
80
|
+
<div>
|
|
81
|
+
<div className={`text-lg font-semibold ${budgetStatus.colour}`}>
|
|
82
|
+
{budgetStatus.label}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
85
|
+
{data.budgetPct}% of ${data.monthlyBudget ?? 100}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* 7-day cost trend sparkline */}
|
|
91
|
+
{data.dailyTrend.length >= 2 && (
|
|
92
|
+
<div className="flex items-center gap-2 mb-3 px-1">
|
|
93
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">7d</span>
|
|
94
|
+
<Sparkline
|
|
95
|
+
data={data.dailyTrend}
|
|
96
|
+
width={120}
|
|
97
|
+
height={24}
|
|
98
|
+
color={data.budgetPct > 100 ? '#f59e0b' : '#10b981'}
|
|
99
|
+
strokeWidth={1.5}
|
|
100
|
+
showFill
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Budget gauge bar */}
|
|
106
|
+
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
107
|
+
<div
|
|
108
|
+
className={`${gaugeColour} h-2 rounded-full transition-all duration-500`}
|
|
109
|
+
style={{ width: `${Math.min(data.budgetPct, 100)}%` }}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function QuadrantSkeleton() {
|
|
117
|
+
return (
|
|
118
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
119
|
+
<div className="animate-pulse space-y-4">
|
|
120
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24" />
|
|
121
|
+
<div className="grid grid-cols-2 gap-4">
|
|
122
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
123
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
124
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
125
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
126
|
+
</div>
|
|
127
|
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full" />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors Quadrant - Mission Control
|
|
3
|
+
* Shows P0/P1 count, P2/P3 count, new today, sparkline, top 3 errors
|
|
4
|
+
*/
|
|
5
|
+
import { Sparkline } from '../ui/Sparkline';
|
|
6
|
+
|
|
7
|
+
interface ErrorData {
|
|
8
|
+
p0Count: number;
|
|
9
|
+
p1Count: number;
|
|
10
|
+
p2Count: number;
|
|
11
|
+
p3Count: number;
|
|
12
|
+
p4Count: number;
|
|
13
|
+
newToday: number;
|
|
14
|
+
dailyTrend: number[];
|
|
15
|
+
topErrors: Array<{
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
message: string;
|
|
18
|
+
script_name: string;
|
|
19
|
+
priority: string;
|
|
20
|
+
occurrence_count: number;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
data: ErrorData;
|
|
26
|
+
loading: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const priorityColours: Record<string, string> = {
|
|
30
|
+
P0: 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
|
|
31
|
+
P1: 'bg-orange-100 text-orange-800 dark:bg-orange-900/50 dark:text-orange-300',
|
|
32
|
+
P2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
33
|
+
P3: 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300',
|
|
34
|
+
P4: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function ErrorsQuadrant({ data, loading }: Props) {
|
|
38
|
+
if (loading) return <QuadrantSkeleton />;
|
|
39
|
+
|
|
40
|
+
const criticalCount = data.p0Count + data.p1Count;
|
|
41
|
+
const otherCount = data.p2Count + data.p3Count;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
45
|
+
<div className="flex items-center justify-between mb-4">
|
|
46
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
47
|
+
Active Errors
|
|
48
|
+
</h3>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="grid grid-cols-3 gap-3 mb-4">
|
|
51
|
+
<div>
|
|
52
|
+
<div className={`text-2xl font-bold ${criticalCount > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white'}`}>
|
|
53
|
+
{criticalCount}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">P0/P1</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">{otherCount}</div>
|
|
59
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">P2/P3</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">{data.newToday}</div>
|
|
63
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">Today</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
{data.dailyTrend.length >= 2 && (
|
|
67
|
+
<div className="flex items-center gap-2 mb-3 px-1">
|
|
68
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">7d</span>
|
|
69
|
+
<Sparkline data={data.dailyTrend} width={120} height={24} color={criticalCount > 0 ? '#ef4444' : '#6b7280'} strokeWidth={1.5} showFill />
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{data.topErrors.length > 0 && (
|
|
73
|
+
<div className="space-y-2 border-t border-gray-100 dark:border-gray-700 pt-3">
|
|
74
|
+
{data.topErrors.map((error) => (
|
|
75
|
+
<div key={error.fingerprint} className="flex items-center gap-2">
|
|
76
|
+
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${priorityColours[error.priority] ?? priorityColours.P4}`}>
|
|
77
|
+
{error.priority}
|
|
78
|
+
</span>
|
|
79
|
+
<span className="text-xs text-gray-600 dark:text-gray-400 truncate flex-1">
|
|
80
|
+
{error.message || error.script_name}
|
|
81
|
+
</span>
|
|
82
|
+
<span className="text-xs text-gray-400 flex-shrink-0">x{error.occurrence_count}</span>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
{data.topErrors.length === 0 && criticalCount === 0 && otherCount === 0 && (
|
|
88
|
+
<div className="text-center py-2">
|
|
89
|
+
<div className="text-green-600 dark:text-green-400 text-sm font-medium">No open errors</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function QuadrantSkeleton() {
|
|
97
|
+
return (
|
|
98
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
99
|
+
<div className="animate-pulse space-y-4">
|
|
100
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24" />
|
|
101
|
+
<div className="grid grid-cols-3 gap-3">
|
|
102
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
103
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
104
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
105
|
+
</div>
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
108
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Quadrant - Mission Control
|
|
3
|
+
* Shows uptime %, services status
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface HealthData {
|
|
7
|
+
servicesTotal: number;
|
|
8
|
+
servicesUp: number;
|
|
9
|
+
servicesDown: number;
|
|
10
|
+
uptimePct: number;
|
|
11
|
+
lastAuditScore: number | null;
|
|
12
|
+
lastAuditDate: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
data: HealthData;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function HealthQuadrant({ data, loading }: Props) {
|
|
21
|
+
if (loading) return <QuadrantSkeleton />;
|
|
22
|
+
|
|
23
|
+
const healthScore = data.lastAuditScore ?? 0;
|
|
24
|
+
const scoreColour =
|
|
25
|
+
healthScore >= 90 ? 'text-green-600 dark:text-green-400'
|
|
26
|
+
: healthScore >= 70 ? 'text-yellow-600 dark:text-yellow-400'
|
|
27
|
+
: 'text-red-600 dark:text-red-400';
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
31
|
+
<div className="flex items-center justify-between mb-4">
|
|
32
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
33
|
+
System Health
|
|
34
|
+
</h3>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="grid grid-cols-2 gap-4">
|
|
37
|
+
<div>
|
|
38
|
+
<div className={`text-3xl font-bold ${scoreColour}`}>
|
|
39
|
+
{data.lastAuditScore !== null ? data.lastAuditScore : '\u2014'}
|
|
40
|
+
</div>
|
|
41
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Audit Score</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
45
|
+
{data.uptimePct.toFixed(1)}%
|
|
46
|
+
</div>
|
|
47
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">Uptime (24h)</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
51
|
+
{data.servicesUp}/{data.servicesTotal}
|
|
52
|
+
</div>
|
|
53
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">Services Up</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
{data.servicesDown > 0 ? (
|
|
57
|
+
<>
|
|
58
|
+
<div className="text-lg font-semibold text-red-600 dark:text-red-400">{data.servicesDown}</div>
|
|
59
|
+
<div className="text-xs text-red-500 dark:text-red-400">Down</div>
|
|
60
|
+
</>
|
|
61
|
+
) : (
|
|
62
|
+
<>
|
|
63
|
+
<div className="text-lg font-semibold text-green-600 dark:text-green-400">All Clear</div>
|
|
64
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">No issues</div>
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function QuadrantSkeleton() {
|
|
74
|
+
return (
|
|
75
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
|
|
76
|
+
<div className="animate-pulse space-y-4">
|
|
77
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24" />
|
|
78
|
+
<div className="grid grid-cols-2 gap-4">
|
|
79
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
80
|
+
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
81
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
82
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Control - Overview Page Main Component
|
|
3
|
+
* Fetches /api/overview/summary and renders 4 quadrants + alert banner
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import { HealthQuadrant } from './HealthQuadrant';
|
|
7
|
+
import { ErrorsQuadrant } from './ErrorsQuadrant';
|
|
8
|
+
import { CostQuadrant } from './CostQuadrant';
|
|
9
|
+
import { ActivityFeed } from './ActivityFeed';
|
|
10
|
+
|
|
11
|
+
interface OverviewSummary {
|
|
12
|
+
health: {
|
|
13
|
+
servicesTotal: number;
|
|
14
|
+
servicesUp: number;
|
|
15
|
+
servicesDown: number;
|
|
16
|
+
uptimePct: number;
|
|
17
|
+
lastAuditScore: number | null;
|
|
18
|
+
lastAuditDate: string | null;
|
|
19
|
+
};
|
|
20
|
+
errors: {
|
|
21
|
+
p0Count: number;
|
|
22
|
+
p1Count: number;
|
|
23
|
+
p2Count: number;
|
|
24
|
+
p3Count: number;
|
|
25
|
+
p4Count: number;
|
|
26
|
+
newToday: number;
|
|
27
|
+
dailyTrend: number[];
|
|
28
|
+
topErrors: Array<{
|
|
29
|
+
fingerprint: string;
|
|
30
|
+
message: string;
|
|
31
|
+
script_name: string;
|
|
32
|
+
priority: string;
|
|
33
|
+
occurrence_count: number;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
costs: {
|
|
37
|
+
mtdSpend: number;
|
|
38
|
+
dailyBurnRate: number;
|
|
39
|
+
projectedMonthly: number;
|
|
40
|
+
budgetPct: number;
|
|
41
|
+
monthlyBudget: number;
|
|
42
|
+
dailyTrend: number[];
|
|
43
|
+
};
|
|
44
|
+
activity: {
|
|
45
|
+
notifications: Array<{
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
category: string;
|
|
49
|
+
priority: string;
|
|
50
|
+
source: string;
|
|
51
|
+
created_at: number;
|
|
52
|
+
action_url: string | null;
|
|
53
|
+
}>;
|
|
54
|
+
pendingPatterns: number;
|
|
55
|
+
};
|
|
56
|
+
alerts: {
|
|
57
|
+
hasP0P1: boolean;
|
|
58
|
+
trippedBreakers: number;
|
|
59
|
+
warningBreakers: number;
|
|
60
|
+
servicesDown: number;
|
|
61
|
+
};
|
|
62
|
+
dataQuality?: {
|
|
63
|
+
latestSnapshot: string | null;
|
|
64
|
+
snapshotAgeMinutes: number;
|
|
65
|
+
status: 'fresh' | 'stale' | 'unknown';
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function MissionControl() {
|
|
70
|
+
const [data, setData] = useState<OverviewSummary | null>(null);
|
|
71
|
+
const [loading, setLoading] = useState(true);
|
|
72
|
+
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
fetch('/api/overview/summary')
|
|
76
|
+
.then(res => {
|
|
77
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
78
|
+
return res.json();
|
|
79
|
+
})
|
|
80
|
+
.then((summary: OverviewSummary) => {
|
|
81
|
+
setData(summary);
|
|
82
|
+
setLoading(false);
|
|
83
|
+
})
|
|
84
|
+
.catch(err => {
|
|
85
|
+
setError(err instanceof Error ? err.message : 'Failed to load overview');
|
|
86
|
+
setLoading(false);
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
if (error) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<div>
|
|
95
|
+
<strong className="text-red-800 dark:text-red-200">Error loading overview</strong>
|
|
96
|
+
<p className="text-sm text-red-600 dark:text-red-300 mt-1">{error}</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dq = data?.dataQuality;
|
|
104
|
+
const dqDot = dq?.status === 'fresh' ? 'bg-green-500' : dq?.status === 'stale' ? 'bg-yellow-500' : 'bg-gray-400';
|
|
105
|
+
const dqText = dq?.status === 'fresh'
|
|
106
|
+
? `Data ${dq.snapshotAgeMinutes}m ago`
|
|
107
|
+
: dq?.status === 'stale'
|
|
108
|
+
? `Data ${Math.round((dq.snapshotAgeMinutes ?? 0) / 60)}h old`
|
|
109
|
+
: 'Data age unknown';
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="space-y-2">
|
|
113
|
+
{!loading && (
|
|
114
|
+
<div className="flex items-center justify-end gap-1.5 text-xs text-gray-400 dark:text-gray-500">
|
|
115
|
+
<span className={`inline-block w-1.5 h-1.5 rounded-full ${dqDot}`} />
|
|
116
|
+
{dqText}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
120
|
+
<HealthQuadrant
|
|
121
|
+
data={data?.health ?? { servicesTotal: 0, servicesUp: 0, servicesDown: 0, uptimePct: 100, lastAuditScore: null, lastAuditDate: null }}
|
|
122
|
+
loading={loading}
|
|
123
|
+
/>
|
|
124
|
+
<ErrorsQuadrant
|
|
125
|
+
data={data?.errors ?? { p0Count: 0, p1Count: 0, p2Count: 0, p3Count: 0, p4Count: 0, newToday: 0, dailyTrend: [], topErrors: [] }}
|
|
126
|
+
loading={loading}
|
|
127
|
+
/>
|
|
128
|
+
<CostQuadrant
|
|
129
|
+
data={data?.costs ?? { mtdSpend: 0, dailyBurnRate: 0, projectedMonthly: 0, budgetPct: 0, monthlyBudget: 100, dailyTrend: [] }}
|
|
130
|
+
loading={loading}
|
|
131
|
+
/>
|
|
132
|
+
<ActivityFeed
|
|
133
|
+
data={data?.activity ?? { notifications: [], pendingPatterns: 0 }}
|
|
134
|
+
loading={loading}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface Allowance {
|
|
2
|
+
service: string;
|
|
3
|
+
metric: string;
|
|
4
|
+
used: number;
|
|
5
|
+
included: number;
|
|
6
|
+
unit: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
allowances: Allowance[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AllowanceStatus({ allowances }: Props) {
|
|
14
|
+
if (allowances.length === 0) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
|
|
17
|
+
No allowance data available yet.
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
{allowances.map((a) => {
|
|
25
|
+
const pct = a.included > 0 ? Math.min((a.used / a.included) * 100, 100) : 0;
|
|
26
|
+
const colour = pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-yellow-500' : 'bg-green-500';
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div key={`${a.service}-${a.metric}`}>
|
|
30
|
+
<div className="flex justify-between text-sm mb-1">
|
|
31
|
+
<span className="text-gray-700 dark:text-gray-300">{a.service} — {a.metric}</span>
|
|
32
|
+
<span className="text-gray-500 dark:text-gray-400">
|
|
33
|
+
{a.used.toLocaleString()} / {a.included.toLocaleString()} {a.unit}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
37
|
+
<div className={`${colour} h-2 rounded-full transition-all`} style={{ width: `${pct}%` }} />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
interface CostRow {
|
|
2
|
+
service: string;
|
|
3
|
+
mtdCost: number;
|
|
4
|
+
pctOfTotal: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
costs: CostRow[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CostCentreOverview({ costs }: Props) {
|
|
12
|
+
if (costs.length === 0) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
|
|
15
|
+
No cost data available yet.
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="overflow-x-auto">
|
|
22
|
+
<table className="w-full text-sm">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr className="border-b border-gray-200 dark:border-gray-700">
|
|
25
|
+
<th className="text-left py-2 px-3 text-gray-500 dark:text-gray-400 font-medium">Service</th>
|
|
26
|
+
<th className="text-right py-2 px-3 text-gray-500 dark:text-gray-400 font-medium">MTD Cost</th>
|
|
27
|
+
<th className="text-right py-2 px-3 text-gray-500 dark:text-gray-400 font-medium">% of Total</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
{costs.map((row) => (
|
|
32
|
+
<tr key={row.service} className="border-b border-gray-100 dark:border-gray-800">
|
|
33
|
+
<td className="py-2 px-3 text-gray-900 dark:text-white">{row.service}</td>
|
|
34
|
+
<td className="py-2 px-3 text-right text-gray-900 dark:text-white">${row.mtdCost.toFixed(2)}</td>
|
|
35
|
+
<td className="py-2 px-3 text-right text-gray-500 dark:text-gray-400">{row.pctOfTotal}%</td>
|
|
36
|
+
</tr>
|
|
37
|
+
))}
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
type Tab = 'overview' | 'workers' | 'databases' | 'storage';
|
|
4
|
+
|
|
5
|
+
export function ResourceTabs() {
|
|
6
|
+
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
|
7
|
+
const [status, setStatus] = useState<{ latestSnapshot: string | null; trackedFeatures: number } | null>(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
fetch('/api/usage/status')
|
|
11
|
+
.then(res => res.json())
|
|
12
|
+
.then(setStatus)
|
|
13
|
+
.catch(() => {});
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
const tabs: { id: Tab; label: string }[] = [
|
|
17
|
+
{ id: 'overview', label: 'Overview' },
|
|
18
|
+
{ id: 'workers', label: 'Workers' },
|
|
19
|
+
{ id: 'databases', label: 'Databases' },
|
|
20
|
+
{ id: 'storage', label: 'Storage' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
<div className="border-b border-gray-200 dark:border-gray-700 mb-4">
|
|
26
|
+
<nav className="flex gap-4">
|
|
27
|
+
{tabs.map(tab => (
|
|
28
|
+
<button
|
|
29
|
+
key={tab.id}
|
|
30
|
+
onClick={() => setActiveTab(tab.id)}
|
|
31
|
+
className={`py-2 px-1 text-sm font-medium border-b-2 transition-colors ${
|
|
32
|
+
activeTab === tab.id
|
|
33
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
34
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
35
|
+
}`}
|
|
36
|
+
>
|
|
37
|
+
{tab.label}
|
|
38
|
+
</button>
|
|
39
|
+
))}
|
|
40
|
+
</nav>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="space-y-4">
|
|
44
|
+
{activeTab === 'overview' && (
|
|
45
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
46
|
+
<div className="metric-card">
|
|
47
|
+
<div className="metric-title">Last Snapshot</div>
|
|
48
|
+
<div className="metric-value text-lg">{status?.latestSnapshot ?? 'N/A'}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="metric-card">
|
|
51
|
+
<div className="metric-title">Tracked Features</div>
|
|
52
|
+
<div className="metric-value">{status?.trackedFeatures ?? 0}</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="metric-card">
|
|
55
|
+
<div className="metric-title">Status</div>
|
|
56
|
+
<div className="metric-value text-lg text-green-600 dark:text-green-400">Active</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
{activeTab !== 'overview' && (
|
|
61
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
62
|
+
<p>Resource details for {activeTab} will appear here once data is collected.</p>
|
|
63
|
+
<p className="text-sm mt-2">Configure collectors in services.yaml and run sync:config.</p>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function SettingsCard({ label, value, description }: Props) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
10
|
+
<div className="flex items-center justify-between">
|
|
11
|
+
<div>
|
|
12
|
+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">{label}</h3>
|
|
13
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{description}</p>
|
|
14
|
+
</div>
|
|
15
|
+
<span className="text-sm font-mono text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
|
16
|
+
{value}
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SettingsCard } from './SettingsCard';
|