@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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.
Files changed (111) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +121 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  9. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  10. package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
  11. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  12. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  13. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  14. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  15. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  16. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  17. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  18. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  19. package/templates/full/migrations/008_auditor.sql +99 -0
  20. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  21. package/templates/full/migrations/011_multi_account.sql +51 -0
  22. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  23. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  24. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  25. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  26. package/templates/full/workers/lib/auditor/index.ts +9 -0
  27. package/templates/full/workers/lib/auditor/types.ts +167 -0
  28. package/templates/full/workers/platform-auditor.ts +1071 -0
  29. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  30. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  31. package/templates/shared/config/observability.yaml.hbs +276 -0
  32. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  33. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  34. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  35. package/templates/shared/dashboard/astro.config.mjs +21 -0
  36. package/templates/shared/dashboard/package.json.hbs +29 -0
  37. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  38. package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
  39. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  40. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  41. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  42. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  43. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  44. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  45. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  46. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  47. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  48. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  49. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  50. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  51. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  52. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  53. package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
  54. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  55. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  56. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  57. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  58. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  59. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  60. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  61. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  62. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  63. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  64. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  65. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  66. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  67. package/templates/shared/dashboard/src/styles/global.css +29 -0
  68. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  69. package/templates/shared/dashboard/tsconfig.json +9 -0
  70. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  71. package/templates/shared/package.json.hbs +12 -1
  72. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  73. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  74. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  75. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  76. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  77. package/templates/shared/scripts/validate-schemas.js +61 -0
  78. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  79. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  80. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  81. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  82. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  83. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  84. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  85. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  86. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  87. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  88. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  89. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  90. package/templates/shared/workers/platform-usage.ts +98 -8
  91. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  92. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  93. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  94. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  95. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  96. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  97. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  98. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  99. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  100. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  101. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  102. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  103. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  104. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  105. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  106. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  107. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  108. package/templates/standard/workers/platform-mapper.ts +482 -0
  109. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  110. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  111. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,29 @@
1
+ ---
2
+ ---
3
+
4
+ <header class="sticky top-0 z-10 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 lg:px-6 py-3">
5
+ <div class="flex items-center justify-between">
6
+ <div class="lg:hidden">
7
+ <h1 class="text-lg font-bold text-gray-900 dark:text-white">Platform</h1>
8
+ </div>
9
+
10
+ <div class="flex items-center gap-3 ml-auto">
11
+ <!-- Dark mode toggle -->
12
+ <button
13
+ id="theme-toggle"
14
+ class="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
15
+ aria-label="Toggle dark mode"
16
+ >
17
+ <span id="theme-light" class="hidden dark:inline">Sun</span>
18
+ <span id="theme-dark" class="dark:hidden">Moon</span>
19
+ </button>
20
+ </div>
21
+ </div>
22
+ </header>
23
+
24
+ <script is:inline>
25
+ document.getElementById('theme-toggle')?.addEventListener('click', () => {
26
+ const isDark = document.documentElement.classList.toggle('dark');
27
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
28
+ });
29
+ </script>
@@ -0,0 +1,57 @@
1
+ ---
2
+ const pathname = Astro.url.pathname;
3
+
4
+ interface NavItem {
5
+ href: string;
6
+ label: string;
7
+ icon: string;
8
+ }
9
+
10
+ const navItems: NavItem[] = [
11
+ { href: '/dashboard', label: 'Overview', icon: 'home' },
12
+ { href: '/resources', label: 'Resources', icon: 'server' },
13
+ {{#if isStandard}}
14
+ { href: '/health', label: 'Health', icon: 'activity' },
15
+ { href: '/errors', label: 'Errors', icon: 'alert-triangle' },
16
+ {{/if}}
17
+ {{#if isFull}}
18
+ { href: '/notifications', label: 'Notifications', icon: 'bell' },
19
+ {{/if}}
20
+ { href: '/settings', label: 'Settings', icon: 'settings' },
21
+ ];
22
+
23
+ function isActive(href: string): boolean {
24
+ return pathname === href || pathname.startsWith(href + '/');
25
+ }
26
+ ---
27
+
28
+ <aside class="hidden lg:flex lg:flex-col lg:w-56 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
29
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
30
+ <h1 class="text-lg font-bold text-gray-900 dark:text-white">Platform</h1>
31
+ <p class="text-xs text-gray-500 dark:text-gray-400">Dashboard</p>
32
+ </div>
33
+
34
+ <nav class="flex-1 p-3 space-y-1">
35
+ {navItems.map((item) => (
36
+ <a
37
+ href={item.href}
38
+ class:list={[
39
+ 'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
40
+ isActive(item.href)
41
+ ? 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 font-medium'
42
+ : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700/50',
43
+ ]}
44
+ >
45
+ <span class="w-5 h-5 flex items-center justify-center text-current opacity-60">
46
+ {item.icon === 'home' && '\u2302'}
47
+ {item.icon === 'server' && '\u229E'}
48
+ {item.icon === 'activity' && '\u2661'}
49
+ {item.icon === 'alert-triangle' && '\u25B3'}
50
+ {item.icon === 'bell' && '\uD83D\uDD14'}
51
+ {item.icon === 'settings' && '\u2699'}
52
+ </span>
53
+ {item.label}
54
+ </a>
55
+ ))}
56
+ </nav>
57
+ </aside>
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Activity Feed Quadrant - Mission Control
3
+ * Shows last 5 notifications, pending patterns count, recent alerts
4
+ */
5
+
6
+ interface ActivityData {
7
+ notifications: Array<{
8
+ id: string;
9
+ title: string;
10
+ category: string;
11
+ priority: string;
12
+ source: string;
13
+ created_at: number;
14
+ action_url: string | null;
15
+ }>;
16
+ pendingPatterns: number;
17
+ }
18
+
19
+ interface Props {
20
+ data: ActivityData;
21
+ loading: boolean;
22
+ }
23
+
24
+ const categoryLabels: Record<string, string> = {
25
+ error: '[ERR]',
26
+ warning: '[WARN]',
27
+ info: '[INFO]',
28
+ success: '[OK]',
29
+ };
30
+
31
+ const sourceLabels: Record<string, string> = {
32
+ 'error-collector': 'Error',
33
+ 'pattern-discovery': 'Pattern',
34
+ 'circuit-breaker': 'CB',
35
+ usage: 'Usage',
36
+ gatus: 'Monitor',
37
+ system: 'System',
38
+ };
39
+
40
+ function formatTimestamp(ts: number): string {
41
+ if (!ts) return '';
42
+ const now = Date.now();
43
+ const diffMs = now - ts * 1000;
44
+ const diffMins = Math.floor(diffMs / 60000);
45
+ const diffHours = Math.floor(diffMs / 3600000);
46
+ const diffDays = Math.floor(diffMs / 86400000);
47
+
48
+ if (diffMins < 1) return 'Just now';
49
+ if (diffMins < 60) return `${diffMins}m`;
50
+ if (diffHours < 24) return `${diffHours}h`;
51
+ if (diffDays < 7) return `${diffDays}d`;
52
+ return new Date(ts * 1000).toLocaleDateString('en-AU', { day: 'numeric', month: 'short' });
53
+ }
54
+
55
+ export function ActivityFeed({ data, loading }: Props) {
56
+ if (loading) {
57
+ return <QuadrantSkeleton />;
58
+ }
59
+
60
+ return (
61
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
62
+ <div className="flex items-center justify-between mb-4">
63
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
64
+ Activity
65
+ </h3>
66
+ {data.pendingPatterns > 0 && (
67
+ <span className="text-xs px-2 py-1 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">
68
+ {data.pendingPatterns} pattern{data.pendingPatterns !== 1 ? 's' : ''} pending
69
+ </span>
70
+ )}
71
+ </div>
72
+
73
+ {data.notifications.length > 0 ? (
74
+ <div className="space-y-3">
75
+ {data.notifications.map((n) => (
76
+ <div key={n.id} className="flex items-start gap-2">
77
+ <span className="text-xs font-mono flex-shrink-0 mt-0.5 text-gray-500 dark:text-gray-400">
78
+ {categoryLabels[n.category] ?? '[INFO]'}
79
+ </span>
80
+ <div className="flex-1 min-w-0">
81
+ <div className="text-sm text-gray-900 dark:text-white truncate">
82
+ {n.action_url ? (
83
+ <a
84
+ href={n.action_url}
85
+ className="hover:text-blue-600 dark:hover:text-blue-400"
86
+ onClick={(e) => e.stopPropagation()}
87
+ >
88
+ {n.title}
89
+ </a>
90
+ ) : (
91
+ n.title
92
+ )}
93
+ </div>
94
+ <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mt-0.5">
95
+ <span>{sourceLabels[n.source] ?? n.source}</span>
96
+ <span>&middot;</span>
97
+ <span>{formatTimestamp(n.created_at)}</span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ ) : (
104
+ <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
105
+ No recent activity
106
+ </div>
107
+ )}
108
+
109
+ <div className="mt-4 pt-3 border-t border-gray-100 dark:border-gray-700">
110
+ <a
111
+ href="/notifications"
112
+ className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
113
+ >
114
+ View all notifications &rarr;
115
+ </a>
116
+ </div>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function QuadrantSkeleton() {
122
+ return (
123
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-5">
124
+ <div className="animate-pulse space-y-3">
125
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16" />
126
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
127
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
128
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
129
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3" />
130
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
131
+ </div>
132
+ </div>
133
+ );
134
+ }
@@ -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
+ }