spine-framework-cortex 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
- package/components/CliInstancesCard.tsx +144 -0
- package/components/CortexSidebar.tsx +27 -53
- package/hooks/useTypeRegistry.ts +74 -0
- package/index.tsx +13 -24
- package/manifest.json +1 -13
- package/package.json +11 -20
- package/pages/courses/CoursesPage.tsx +14 -4
- package/pages/crm/AccountDetailPage.tsx +149 -194
- package/pages/crm/ContactsPage.tsx +7 -7
- package/pages/intelligence/IntelligencePage.tsx +24 -31
- package/pages/kb/KBEditorPage.tsx +9 -2
- package/pages/operations/AuditFunnelPage.tsx +378 -0
- package/pages/operations/InstallFunnelPage.tsx +410 -0
- package/pages/operations/OperationsDashboard.tsx +275 -0
- package/pages/support/RedactionReview.tsx +11 -2
- package/seed/link-types.json +8 -42
- package/seed/package.json +27 -0
- package/seed/roles.json +1 -1
- package/seed/types.json +2711 -596
- package/CHANGELOG.md +0 -42
- package/LICENSE.md +0 -223
- package/README.md +0 -69
- package/functions/custom_anonymous-sessions.ts +0 -356
- package/functions/custom_case_analysis.ts +0 -507
- package/functions/custom_community-escalation.ts +0 -234
- package/functions/custom_cortex-chunks.ts +0 -52
- package/functions/custom_funnel-scoring.ts +0 -256
- package/functions/custom_funnel-signal.ts +0 -430
- package/functions/custom_funnel-timers.ts +0 -449
- package/functions/custom_kb-chunker-test.ts +0 -364
- package/functions/custom_kb-chunker.ts +0 -576
- package/functions/custom_kb-embeddings.ts +0 -481
- package/functions/custom_kb-ingestion.ts +0 -448
- package/functions/custom_support-triage.ts +0 -649
- package/functions/custom_tag_management.ts +0 -314
- package/functions/webhook-handlers.ts +0 -29
- package/lib/resolveTypeId.ts +0 -16
- package/pages/crm/ContactDetailPage.tsx +0 -184
- package/pages/ops/AuditFunnelPage.tsx +0 -191
- package/pages/ops/CommandCenterPage.tsx +0 -377
- package/pages/ops/InstallFunnelPage.tsx +0 -226
- package/seed/accounts.json +0 -9
- package/seed/integrations.json +0 -24
- package/seed/pipelines.json +0 -59
- package/seed/triggers.json +0 -125
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@core/components/ui/card'
|
|
3
|
+
import { Badge } from '@core/components/ui/badge'
|
|
4
|
+
import { Button } from '@core/components/ui/button'
|
|
5
|
+
import { Progress } from '@core/components/ui/progress'
|
|
6
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@core/components/ui/tabs'
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@core/components/ui/table'
|
|
8
|
+
import { useApi } from '@core/hooks/useApi'
|
|
9
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
10
|
+
import { Download, TrendingUp, Users, Package } from 'lucide-react'
|
|
11
|
+
|
|
12
|
+
interface InstallMetrics {
|
|
13
|
+
total_downloads: number
|
|
14
|
+
unique_installers: number
|
|
15
|
+
active_instances: number
|
|
16
|
+
conversion_funnel: {
|
|
17
|
+
downloads: number
|
|
18
|
+
installations: number
|
|
19
|
+
first_use: number
|
|
20
|
+
claimed: number
|
|
21
|
+
}
|
|
22
|
+
install_trends: Array<{
|
|
23
|
+
date: string
|
|
24
|
+
downloads: number
|
|
25
|
+
installations: number
|
|
26
|
+
}>
|
|
27
|
+
recent_installs: Array<{
|
|
28
|
+
id: string
|
|
29
|
+
serial: string
|
|
30
|
+
app_version: string
|
|
31
|
+
environment: string
|
|
32
|
+
first_seen_at: string
|
|
33
|
+
last_signal_at: string
|
|
34
|
+
claimed_by?: string
|
|
35
|
+
claimed_at?: string
|
|
36
|
+
account_name?: string
|
|
37
|
+
}>
|
|
38
|
+
top_versions: Array<{
|
|
39
|
+
version: string
|
|
40
|
+
count: number
|
|
41
|
+
percentage: number
|
|
42
|
+
}>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function InstallFunnelPage() {
|
|
46
|
+
const [metrics, setMetrics] = useState<InstallMetrics | null>(null)
|
|
47
|
+
const [loading, setLoading] = useState(true)
|
|
48
|
+
const { apiFetch } = useApi()
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
loadMetrics()
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const loadMetrics = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const data = await apiFetch('/admin/install-funnel/metrics')
|
|
57
|
+
setMetrics(data)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Failed to load install metrics:', error)
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const exportData = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const response = await apiFetch('/admin/install-funnel/exportData')
|
|
68
|
+
|
|
69
|
+
// Create download link
|
|
70
|
+
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' })
|
|
71
|
+
const url = window.URL.createObjectURL(blob)
|
|
72
|
+
const a = document.createElement('a')
|
|
73
|
+
a.href = url
|
|
74
|
+
a.download = `install-funnel-${new Date().toISOString().split('T')[0]}.json`
|
|
75
|
+
document.body.appendChild(a)
|
|
76
|
+
a.click()
|
|
77
|
+
window.URL.revokeObjectURL(url)
|
|
78
|
+
document.body.removeChild(a)
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Failed to export data:', error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (loading) {
|
|
85
|
+
return <div className="p-6">Loading install funnel data...</div>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const conversionRates = metrics ? {
|
|
89
|
+
download_to_install: (metrics.conversion_funnel.installations / metrics.conversion_funnel.downloads * 100) || 0,
|
|
90
|
+
install_to_use: (metrics.conversion_funnel.first_use / metrics.conversion_funnel.installations * 100) || 0,
|
|
91
|
+
use_to_claim: (metrics.conversion_funnel.claimed / metrics.conversion_funnel.first_use * 100) || 0
|
|
92
|
+
} : { download_to_install: 0, install_to_use: 0, use_to_claim: 0 }
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="p-6 space-y-6">
|
|
96
|
+
<div className="flex justify-between items-center">
|
|
97
|
+
<div>
|
|
98
|
+
<h1 className="text-3xl font-bold">Install Funnel</h1>
|
|
99
|
+
<p className="text-muted-foreground">
|
|
100
|
+
Track downloads, installations, and adoption metrics
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex space-x-2">
|
|
104
|
+
<Button variant="outline" onClick={exportData}>
|
|
105
|
+
<Download className="w-4 h-4 mr-2" />
|
|
106
|
+
Export Data
|
|
107
|
+
</Button>
|
|
108
|
+
<Button onClick={loadMetrics}>Refresh</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Key Metrics */}
|
|
113
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader className="pb-2">
|
|
116
|
+
<CardTitle className="text-sm font-medium flex items-center">
|
|
117
|
+
<Download className="w-4 h-4 mr-2" />
|
|
118
|
+
Total Downloads
|
|
119
|
+
</CardTitle>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<CardContent>
|
|
122
|
+
<div className="text-2xl font-bold">{metrics?.total_downloads || 0}</div>
|
|
123
|
+
<p className="text-xs text-muted-foreground">
|
|
124
|
+
All-time downloads
|
|
125
|
+
</p>
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader className="pb-2">
|
|
131
|
+
<CardTitle className="text-sm font-medium flex items-center">
|
|
132
|
+
<Users className="w-4 h-4 mr-2" />
|
|
133
|
+
Unique Installers
|
|
134
|
+
</CardTitle>
|
|
135
|
+
</CardHeader>
|
|
136
|
+
<CardContent>
|
|
137
|
+
<div className="text-2xl font-bold">{metrics?.unique_installers || 0}</div>
|
|
138
|
+
<p className="text-xs text-muted-foreground">
|
|
139
|
+
Distinct installations
|
|
140
|
+
</p>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
|
|
144
|
+
<Card>
|
|
145
|
+
<CardHeader className="pb-2">
|
|
146
|
+
<CardTitle className="text-sm font-medium flex items-center">
|
|
147
|
+
<Package className="w-4 h-4 mr-2" />
|
|
148
|
+
Active Instances
|
|
149
|
+
</CardTitle>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent>
|
|
152
|
+
<div className="text-2xl font-bold">{metrics?.active_instances || 0}</div>
|
|
153
|
+
<p className="text-xs text-muted-foreground">
|
|
154
|
+
Used in last 7 days
|
|
155
|
+
</p>
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
|
|
159
|
+
<Card>
|
|
160
|
+
<CardHeader className="pb-2">
|
|
161
|
+
<CardTitle className="text-sm font-medium flex items-center">
|
|
162
|
+
<TrendingUp className="w-4 h-4 mr-2" />
|
|
163
|
+
Overall Conversion
|
|
164
|
+
</CardTitle>
|
|
165
|
+
</CardHeader>
|
|
166
|
+
<CardContent>
|
|
167
|
+
<div className="text-2xl font-bold">
|
|
168
|
+
{conversionRates.download_to_install.toFixed(1)}%
|
|
169
|
+
</div>
|
|
170
|
+
<p className="text-xs text-muted-foreground">
|
|
171
|
+
Download → Install
|
|
172
|
+
</p>
|
|
173
|
+
</CardContent>
|
|
174
|
+
</Card>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<Tabs defaultValue="funnel" className="space-y-4">
|
|
178
|
+
<TabsList>
|
|
179
|
+
<TabsTrigger value="funnel">Conversion Funnel</TabsTrigger>
|
|
180
|
+
<TabsTrigger value="instances">Instance Details</TabsTrigger>
|
|
181
|
+
<TabsTrigger value="trends">Installation Trends</TabsTrigger>
|
|
182
|
+
<TabsTrigger value="versions">Version Analytics</TabsTrigger>
|
|
183
|
+
</TabsList>
|
|
184
|
+
|
|
185
|
+
<TabsContent value="funnel" className="space-y-4">
|
|
186
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
187
|
+
<Card>
|
|
188
|
+
<CardHeader>
|
|
189
|
+
<CardTitle>Conversion Funnel</CardTitle>
|
|
190
|
+
<CardDescription>User progression from download to claim</CardDescription>
|
|
191
|
+
</CardHeader>
|
|
192
|
+
<CardContent>
|
|
193
|
+
<div className="space-y-4">
|
|
194
|
+
{metrics?.conversion_funnel && (
|
|
195
|
+
<>
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
<div className="flex justify-between">
|
|
198
|
+
<span>Downloads</span>
|
|
199
|
+
<span className="font-medium">{metrics.conversion_funnel.downloads}</span>
|
|
200
|
+
</div>
|
|
201
|
+
<Progress value={100} className="h-2" />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="space-y-2">
|
|
205
|
+
<div className="flex justify-between">
|
|
206
|
+
<span>Installations</span>
|
|
207
|
+
<span className="font-medium">{metrics.conversion_funnel.installations}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<Progress value={conversionRates.download_to_install} className="h-2" />
|
|
210
|
+
<p className="text-xs text-muted-foreground">
|
|
211
|
+
{conversionRates.download_to_install.toFixed(1)}% conversion
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
<div className="flex justify-between">
|
|
217
|
+
<span>First Use</span>
|
|
218
|
+
<span className="font-medium">{metrics.conversion_funnel.first_use}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<Progress value={conversionRates.install_to_use} className="h-2" />
|
|
221
|
+
<p className="text-xs text-muted-foreground">
|
|
222
|
+
{conversionRates.install_to_use.toFixed(1)}% conversion
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="space-y-2">
|
|
227
|
+
<div className="flex justify-between">
|
|
228
|
+
<span>Claimed</span>
|
|
229
|
+
<span className="font-medium">{metrics.conversion_funnel.claimed}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<Progress value={conversionRates.use_to_claim} className="h-2" />
|
|
232
|
+
<p className="text-xs text-muted-foreground">
|
|
233
|
+
{conversionRates.use_to_claim.toFixed(1)}% conversion
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
236
|
+
</>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</CardContent>
|
|
240
|
+
</Card>
|
|
241
|
+
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle>Conversion Insights</CardTitle>
|
|
245
|
+
<CardDescription>Key metrics and drop-off points</CardDescription>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent>
|
|
248
|
+
<div className="space-y-4">
|
|
249
|
+
<div className="flex justify-between items-center p-3 bg-blue-50 rounded-lg">
|
|
250
|
+
<div>
|
|
251
|
+
<div className="font-medium">Download to Install</div>
|
|
252
|
+
<div className="text-sm text-muted-foreground">
|
|
253
|
+
{conversionRates.download_to_install.toFixed(1)}% conversion
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
<Badge variant={conversionRates.download_to_install > 50 ? 'default' : 'destructive'}>
|
|
257
|
+
{conversionRates.download_to_install > 50 ? 'Good' : 'Needs Attention'}
|
|
258
|
+
</Badge>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className="flex justify-between items-center p-3 bg-green-50 rounded-lg">
|
|
262
|
+
<div>
|
|
263
|
+
<div className="font-medium">Install to Use</div>
|
|
264
|
+
<div className="text-sm text-muted-foreground">
|
|
265
|
+
{conversionRates.install_to_use.toFixed(1)}% conversion
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<Badge variant={conversionRates.install_to_use > 70 ? 'default' : 'destructive'}>
|
|
269
|
+
{conversionRates.install_to_use > 70 ? 'Good' : 'Needs Attention'}
|
|
270
|
+
</Badge>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div className="flex justify-between items-center p-3 bg-purple-50 rounded-lg">
|
|
274
|
+
<div>
|
|
275
|
+
<div className="font-medium">Use to Claim</div>
|
|
276
|
+
<div className="text-sm text-muted-foreground">
|
|
277
|
+
{conversionRates.use_to_claim.toFixed(1)}% conversion
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<Badge variant={conversionRates.use_to_claim > 30 ? 'default' : 'destructive'}>
|
|
281
|
+
{conversionRates.use_to_claim > 30 ? 'Good' : 'Needs Attention'}
|
|
282
|
+
</Badge>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</CardContent>
|
|
286
|
+
</Card>
|
|
287
|
+
</div>
|
|
288
|
+
</TabsContent>
|
|
289
|
+
|
|
290
|
+
<TabsContent value="instances" className="space-y-4">
|
|
291
|
+
<Card>
|
|
292
|
+
<CardHeader>
|
|
293
|
+
<CardTitle>Recent Installations</CardTitle>
|
|
294
|
+
<CardDescription>Latest instance installations and their status</CardDescription>
|
|
295
|
+
</CardHeader>
|
|
296
|
+
<CardContent>
|
|
297
|
+
<Table>
|
|
298
|
+
<TableHeader>
|
|
299
|
+
<TableRow>
|
|
300
|
+
<TableHead>Instance ID</TableHead>
|
|
301
|
+
<TableHead>Version</TableHead>
|
|
302
|
+
<TableHead>Environment</TableHead>
|
|
303
|
+
<TableHead>First Seen</TableHead>
|
|
304
|
+
<TableHead>Last Activity</TableHead>
|
|
305
|
+
<TableHead>Status</TableHead>
|
|
306
|
+
<TableHead>Owner</TableHead>
|
|
307
|
+
</TableRow>
|
|
308
|
+
</TableHeader>
|
|
309
|
+
<TableBody>
|
|
310
|
+
{metrics?.recent_installs?.map((instance) => (
|
|
311
|
+
<TableRow key={instance.id}>
|
|
312
|
+
<TableCell>
|
|
313
|
+
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
|
314
|
+
{instance.serial.slice(0, 12)}...
|
|
315
|
+
</code>
|
|
316
|
+
</TableCell>
|
|
317
|
+
<TableCell>
|
|
318
|
+
<Badge variant="outline">{instance.app_version}</Badge>
|
|
319
|
+
</TableCell>
|
|
320
|
+
<TableCell>
|
|
321
|
+
<Badge
|
|
322
|
+
variant={instance.environment === 'production' ? 'default' : 'secondary'}
|
|
323
|
+
>
|
|
324
|
+
{instance.environment}
|
|
325
|
+
</Badge>
|
|
326
|
+
</TableCell>
|
|
327
|
+
<TableCell>
|
|
328
|
+
<div className="text-sm">
|
|
329
|
+
{formatDistanceToNow(new Date(instance.first_seen_at), { addSuffix: true })}
|
|
330
|
+
</div>
|
|
331
|
+
</TableCell>
|
|
332
|
+
<TableCell>
|
|
333
|
+
<div className="text-sm">
|
|
334
|
+
{formatDistanceToNow(new Date(instance.last_signal_at), { addSuffix: true })}
|
|
335
|
+
</div>
|
|
336
|
+
</TableCell>
|
|
337
|
+
<TableCell>
|
|
338
|
+
<Badge variant={instance.claimed_by ? 'default' : 'secondary'}>
|
|
339
|
+
{instance.claimed_by ? 'Claimed' : 'Unclaimed'}
|
|
340
|
+
</Badge>
|
|
341
|
+
</TableCell>
|
|
342
|
+
<TableCell>
|
|
343
|
+
{instance.account_name ? (
|
|
344
|
+
<div>
|
|
345
|
+
<div className="font-medium text-sm">{instance.account_name}</div>
|
|
346
|
+
<div className="text-xs text-muted-foreground">{instance.claimed_by}</div>
|
|
347
|
+
</div>
|
|
348
|
+
) : (
|
|
349
|
+
<span className="text-sm text-muted-foreground">Unclaimed</span>
|
|
350
|
+
)}
|
|
351
|
+
</TableCell>
|
|
352
|
+
</TableRow>
|
|
353
|
+
))}
|
|
354
|
+
</TableBody>
|
|
355
|
+
</Table>
|
|
356
|
+
</CardContent>
|
|
357
|
+
</Card>
|
|
358
|
+
</TabsContent>
|
|
359
|
+
|
|
360
|
+
<TabsContent value="trends" className="space-y-4">
|
|
361
|
+
<Card>
|
|
362
|
+
<CardHeader>
|
|
363
|
+
<CardTitle>Installation Trends</CardTitle>
|
|
364
|
+
<CardDescription>Daily download and installation activity</CardDescription>
|
|
365
|
+
</CardHeader>
|
|
366
|
+
<CardContent>
|
|
367
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
368
|
+
<TrendingUp className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
369
|
+
<p>Chart visualization would be implemented here</p>
|
|
370
|
+
<p className="text-sm">Showing {metrics?.install_trends?.length || 0} days of data</p>
|
|
371
|
+
</div>
|
|
372
|
+
</CardContent>
|
|
373
|
+
</Card>
|
|
374
|
+
</TabsContent>
|
|
375
|
+
|
|
376
|
+
<TabsContent value="versions" className="space-y-4">
|
|
377
|
+
<Card>
|
|
378
|
+
<CardHeader>
|
|
379
|
+
<CardTitle>Version Distribution</CardTitle>
|
|
380
|
+
<CardDescription>Active instances by version</CardDescription>
|
|
381
|
+
</CardHeader>
|
|
382
|
+
<CardContent>
|
|
383
|
+
<div className="space-y-3">
|
|
384
|
+
{metrics?.top_versions?.map((version) => (
|
|
385
|
+
<div key={version.version} className="space-y-1">
|
|
386
|
+
<div className="flex justify-between items-center">
|
|
387
|
+
<div className="flex items-center space-x-2">
|
|
388
|
+
<Badge variant="outline">{version.version}</Badge>
|
|
389
|
+
<span className="text-sm text-muted-foreground">
|
|
390
|
+
{version.count} instances
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<span className="font-medium">{version.percentage.toFixed(1)}%</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="w-full bg-secondary rounded-full h-2">
|
|
396
|
+
<div
|
|
397
|
+
className="bg-primary h-2 rounded-full"
|
|
398
|
+
style={{ width: `${version.percentage}%` }}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
</CardContent>
|
|
405
|
+
</Card>
|
|
406
|
+
</TabsContent>
|
|
407
|
+
</Tabs>
|
|
408
|
+
</div>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@core/components/ui/card'
|
|
3
|
+
import { Badge } from '@core/components/ui/badge'
|
|
4
|
+
import { Button } from '@core/components/ui/button'
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@core/components/ui/tabs'
|
|
6
|
+
import { useApi } from '@core/hooks/useApi'
|
|
7
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
8
|
+
|
|
9
|
+
interface SignalMetrics {
|
|
10
|
+
total_signals: number
|
|
11
|
+
signals_today: number
|
|
12
|
+
signals_this_week: number
|
|
13
|
+
conversion_rate: number
|
|
14
|
+
top_sources: Array<{ source: string; count: number }>
|
|
15
|
+
recent_signals: Array<{
|
|
16
|
+
id: string
|
|
17
|
+
action_type: string
|
|
18
|
+
source: string
|
|
19
|
+
stage: string
|
|
20
|
+
created_at: string
|
|
21
|
+
account_name?: string
|
|
22
|
+
}>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FunnelMetrics {
|
|
26
|
+
anonymous_visitors: number
|
|
27
|
+
identified_users: number
|
|
28
|
+
installed_instances: number
|
|
29
|
+
conversion_rates: {
|
|
30
|
+
anonymous_to_identified: number
|
|
31
|
+
identified_to_installed: number
|
|
32
|
+
}
|
|
33
|
+
stage_distribution: Array<{
|
|
34
|
+
stage: string
|
|
35
|
+
count: number
|
|
36
|
+
percentage: number
|
|
37
|
+
}>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function OperationsDashboard() {
|
|
41
|
+
const [signalMetrics, setSignalMetrics] = useState<SignalMetrics | null>(null)
|
|
42
|
+
const [funnelMetrics, setFunnelMetrics] = useState<FunnelMetrics | null>(null)
|
|
43
|
+
const [loading, setLoading] = useState(true)
|
|
44
|
+
const { apiFetch } = useApi()
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
loadMetrics()
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const loadMetrics = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const [signals, funnel] = await Promise.all([
|
|
53
|
+
apiFetch('/admin/signals/metrics'),
|
|
54
|
+
apiFetch('/admin/funnel/metrics')
|
|
55
|
+
])
|
|
56
|
+
setSignalMetrics(signals)
|
|
57
|
+
setFunnelMetrics(funnel)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Failed to load metrics:', error)
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (loading) {
|
|
66
|
+
return <div className="p-6">Loading operations dashboard...</div>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="p-6 space-y-6">
|
|
71
|
+
<div className="flex justify-between items-center">
|
|
72
|
+
<div>
|
|
73
|
+
<h1 className="text-3xl font-bold">Operations Dashboard</h1>
|
|
74
|
+
<p className="text-muted-foreground">
|
|
75
|
+
Monitor signal tracking, funnel performance, and system health
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
<Button onClick={loadMetrics}>Refresh</Button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Key Metrics */}
|
|
82
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
83
|
+
<Card>
|
|
84
|
+
<CardHeader className="pb-2">
|
|
85
|
+
<CardTitle className="text-sm font-medium">Total Signals</CardTitle>
|
|
86
|
+
</CardHeader>
|
|
87
|
+
<CardContent>
|
|
88
|
+
<div className="text-2xl font-bold">{signalMetrics?.total_signals || 0}</div>
|
|
89
|
+
<p className="text-xs text-muted-foreground">
|
|
90
|
+
+{signalMetrics?.signals_today || 0} today
|
|
91
|
+
</p>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
|
|
95
|
+
<Card>
|
|
96
|
+
<CardHeader className="pb-2">
|
|
97
|
+
<CardTitle className="text-sm font-medium">Conversion Rate</CardTitle>
|
|
98
|
+
</CardHeader>
|
|
99
|
+
<CardContent>
|
|
100
|
+
<div className="text-2xl font-bold">{(signalMetrics?.conversion_rate || 0).toFixed(1)}%</div>
|
|
101
|
+
<p className="text-xs text-muted-foreground">
|
|
102
|
+
Last 30 days
|
|
103
|
+
</p>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
|
|
107
|
+
<Card>
|
|
108
|
+
<CardHeader className="pb-2">
|
|
109
|
+
<CardTitle className="text-sm font-medium">Active Users</CardTitle>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent>
|
|
112
|
+
<div className="text-2xl font-bold">{funnelMetrics?.identified_users || 0}</div>
|
|
113
|
+
<p className="text-xs text-muted-foreground">
|
|
114
|
+
Identified users
|
|
115
|
+
</p>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
|
|
119
|
+
<Card>
|
|
120
|
+
<CardHeader className="pb-2">
|
|
121
|
+
<CardTitle className="text-sm font-medium">Installations</CardTitle>
|
|
122
|
+
</CardHeader>
|
|
123
|
+
<CardContent>
|
|
124
|
+
<div className="text-2xl font-bold">{funnelMetrics?.installed_instances || 0}</div>
|
|
125
|
+
<p className="text-xs text-muted-foreground">
|
|
126
|
+
Active instances
|
|
127
|
+
</p>
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<Tabs defaultValue="signals" className="space-y-4">
|
|
133
|
+
<TabsList>
|
|
134
|
+
<TabsTrigger value="signals">Signal Analytics</TabsTrigger>
|
|
135
|
+
<TabsTrigger value="funnel">Funnel Performance</TabsTrigger>
|
|
136
|
+
<TabsTrigger value="health">System Health</TabsTrigger>
|
|
137
|
+
</TabsList>
|
|
138
|
+
|
|
139
|
+
<TabsContent value="signals" className="space-y-4">
|
|
140
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
141
|
+
<Card>
|
|
142
|
+
<CardHeader>
|
|
143
|
+
<CardTitle>Top Signal Sources</CardTitle>
|
|
144
|
+
<CardDescription>Most active signal sources</CardDescription>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
<CardContent>
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
{signalMetrics?.top_sources?.map((source, index) => (
|
|
149
|
+
<div key={source.source} className="flex justify-between items-center">
|
|
150
|
+
<div className="flex items-center space-x-2">
|
|
151
|
+
<Badge variant="outline">{index + 1}</Badge>
|
|
152
|
+
<span className="capitalize">{source.source}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<span className="font-medium">{source.count}</span>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</CardContent>
|
|
159
|
+
</Card>
|
|
160
|
+
|
|
161
|
+
<Card>
|
|
162
|
+
<CardHeader>
|
|
163
|
+
<CardTitle>Recent Signals</CardTitle>
|
|
164
|
+
<CardDescription>Latest signal activity</CardDescription>
|
|
165
|
+
</CardHeader>
|
|
166
|
+
<CardContent>
|
|
167
|
+
<div className="space-y-2">
|
|
168
|
+
{signalMetrics?.recent_signals?.slice(0, 10).map((signal) => (
|
|
169
|
+
<div key={signal.id} className="flex justify-between items-center text-sm">
|
|
170
|
+
<div>
|
|
171
|
+
<span className="font-medium">{signal.action_type}</span>
|
|
172
|
+
<div className="text-muted-foreground">
|
|
173
|
+
{signal.account_name || 'Anonymous'} • {signal.source}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="text-right">
|
|
177
|
+
<Badge variant="outline" className="text-xs">
|
|
178
|
+
{signal.stage}
|
|
179
|
+
</Badge>
|
|
180
|
+
<div className="text-xs text-muted-foreground">
|
|
181
|
+
{formatDistanceToNow(new Date(signal.created_at), { addSuffix: true })}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</CardContent>
|
|
188
|
+
</Card>
|
|
189
|
+
</div>
|
|
190
|
+
</TabsContent>
|
|
191
|
+
|
|
192
|
+
<TabsContent value="funnel" className="space-y-4">
|
|
193
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader>
|
|
196
|
+
<CardTitle>Funnel Stages</CardTitle>
|
|
197
|
+
<CardDescription>User progression through funnel</CardDescription>
|
|
198
|
+
</CardHeader>
|
|
199
|
+
<CardContent>
|
|
200
|
+
<div className="space-y-3">
|
|
201
|
+
{funnelMetrics?.stage_distribution?.map((stage) => (
|
|
202
|
+
<div key={stage.stage} className="space-y-1">
|
|
203
|
+
<div className="flex justify-between text-sm">
|
|
204
|
+
<span className="capitalize">{stage.stage}</span>
|
|
205
|
+
<span>{stage.count} ({stage.percentage.toFixed(1)}%)</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="w-full bg-secondary rounded-full h-2">
|
|
208
|
+
<div
|
|
209
|
+
className="bg-primary h-2 rounded-full"
|
|
210
|
+
style={{ width: `${stage.percentage}%` }}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</CardContent>
|
|
217
|
+
</Card>
|
|
218
|
+
|
|
219
|
+
<Card>
|
|
220
|
+
<CardHeader>
|
|
221
|
+
<CardTitle>Conversion Rates</CardTitle>
|
|
222
|
+
<CardDescription>Stage-to-stage conversion</CardDescription>
|
|
223
|
+
</CardHeader>
|
|
224
|
+
<CardContent>
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
<div className="text-center">
|
|
227
|
+
<div className="text-2xl font-bold text-green-600">
|
|
228
|
+
{(funnelMetrics?.conversion_rates.anonymous_to_identified || 0).toFixed(1)}%
|
|
229
|
+
</div>
|
|
230
|
+
<p className="text-sm text-muted-foreground">
|
|
231
|
+
Anonymous → Identified
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="text-center">
|
|
235
|
+
<div className="text-2xl font-bold text-blue-600">
|
|
236
|
+
{(funnelMetrics?.conversion_rates.identified_to_installed || 0).toFixed(1)}%
|
|
237
|
+
</div>
|
|
238
|
+
<p className="text-sm text-muted-foreground">
|
|
239
|
+
Identified → Installed
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
</div>
|
|
246
|
+
</TabsContent>
|
|
247
|
+
|
|
248
|
+
<TabsContent value="health" className="space-y-4">
|
|
249
|
+
<Card>
|
|
250
|
+
<CardHeader>
|
|
251
|
+
<CardTitle>System Health</CardTitle>
|
|
252
|
+
<CardDescription>Overall system status and performance</CardDescription>
|
|
253
|
+
</CardHeader>
|
|
254
|
+
<CardContent>
|
|
255
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
256
|
+
<div className="text-center">
|
|
257
|
+
<div className="text-2xl font-bold text-green-600">Healthy</div>
|
|
258
|
+
<p className="text-sm text-muted-foreground">Signal Processing</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="text-center">
|
|
261
|
+
<div className="text-2xl font-bold text-green-600">Operational</div>
|
|
262
|
+
<p className="text-sm text-muted-foreground">Database</p>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="text-center">
|
|
265
|
+
<div className="text-2xl font-bold text-green-600">Normal</div>
|
|
266
|
+
<p className="text-sm text-muted-foreground">API Response</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</CardContent>
|
|
270
|
+
</Card>
|
|
271
|
+
</TabsContent>
|
|
272
|
+
</Tabs>
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|