spine-framework-cortex 0.1.19 → 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 -46
- 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 -446
- 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,378 @@
|
|
|
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 { Input } from '@core/components/ui/input'
|
|
6
|
+
import { Label } from '@core/components/ui/label'
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@core/components/ui/select'
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@core/components/ui/table'
|
|
9
|
+
import { useApi } from '@core/hooks/useApi'
|
|
10
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
11
|
+
import { Search, Filter, Download } from 'lucide-react'
|
|
12
|
+
|
|
13
|
+
interface SignalRecord {
|
|
14
|
+
id: string
|
|
15
|
+
action_type: string
|
|
16
|
+
action_value: number
|
|
17
|
+
stage: string
|
|
18
|
+
source: string
|
|
19
|
+
created_at: string
|
|
20
|
+
account_name?: string
|
|
21
|
+
person_email?: string
|
|
22
|
+
anonymous_id?: string
|
|
23
|
+
session_id?: string
|
|
24
|
+
rating: number
|
|
25
|
+
raw_score: number
|
|
26
|
+
referrer_domain?: string
|
|
27
|
+
utm_source?: string
|
|
28
|
+
utm_medium?: string
|
|
29
|
+
utm_campaign?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AuditFilters {
|
|
33
|
+
date_from?: string
|
|
34
|
+
date_to?: string
|
|
35
|
+
source?: string
|
|
36
|
+
stage?: string
|
|
37
|
+
action_type?: string
|
|
38
|
+
min_rating?: number
|
|
39
|
+
account_id?: string
|
|
40
|
+
search?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function AuditFunnelPage() {
|
|
44
|
+
const [signals, setSignals] = useState<SignalRecord[]>([])
|
|
45
|
+
const [loading, setLoading] = useState(false)
|
|
46
|
+
const [filters, setFilters] = useState<AuditFilters>({})
|
|
47
|
+
const [totalCount, setTotalCount] = useState(0)
|
|
48
|
+
const [page, setPage] = useState(1)
|
|
49
|
+
const { apiFetch } = useApi()
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
loadSignals()
|
|
53
|
+
}, [page, filters])
|
|
54
|
+
|
|
55
|
+
const loadSignals = async () => {
|
|
56
|
+
setLoading(true)
|
|
57
|
+
try {
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
page: page.toString(),
|
|
60
|
+
limit: '50',
|
|
61
|
+
...Object.fromEntries(
|
|
62
|
+
Object.entries(filters).filter(([_, v]) => v != null && v !== '')
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const response = await apiFetch(`/admin/signals/audit?${params}`)
|
|
67
|
+
setSignals(response.signals || [])
|
|
68
|
+
setTotalCount(response.total || 0)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to load signals:', error)
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const exportData = async () => {
|
|
77
|
+
try {
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
export: 'true',
|
|
80
|
+
...Object.fromEntries(
|
|
81
|
+
Object.entries(filters).filter(([_, v]) => v != null && v !== '')
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const response = await apiFetch(`/admin/signals/audit?${params}`)
|
|
86
|
+
|
|
87
|
+
// Create download link
|
|
88
|
+
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' })
|
|
89
|
+
const url = window.URL.createObjectURL(blob)
|
|
90
|
+
const a = document.createElement('a')
|
|
91
|
+
a.href = url
|
|
92
|
+
a.download = `signals-audit-${new Date().toISOString().split('T')[0]}.json`
|
|
93
|
+
document.body.appendChild(a)
|
|
94
|
+
a.click()
|
|
95
|
+
window.URL.revokeObjectURL(url)
|
|
96
|
+
document.body.removeChild(a)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to export data:', error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const updateFilter = (key: keyof AuditFilters, value: any) => {
|
|
103
|
+
setFilters(prev => ({ ...prev, [key]: value }))
|
|
104
|
+
setPage(1) // Reset to first page when filtering
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const getRatingColor = (rating: number) => {
|
|
108
|
+
if (rating >= 4) return 'bg-green-100 text-green-800'
|
|
109
|
+
if (rating >= 3) return 'bg-yellow-100 text-yellow-800'
|
|
110
|
+
return 'bg-red-100 text-red-800'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const getSourceColor = (source: string) => {
|
|
114
|
+
const colors = {
|
|
115
|
+
mar: 'bg-blue-100 text-blue-800',
|
|
116
|
+
int: 'bg-purple-100 text-purple-800',
|
|
117
|
+
use: 'bg-green-100 text-green-800',
|
|
118
|
+
manual: 'bg-gray-100 text-gray-800'
|
|
119
|
+
}
|
|
120
|
+
return colors[source as keyof typeof colors] || 'bg-gray-100 text-gray-800'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="p-6 space-y-6">
|
|
125
|
+
<div className="flex justify-between items-center">
|
|
126
|
+
<div>
|
|
127
|
+
<h1 className="text-3xl font-bold">Funnel Audit</h1>
|
|
128
|
+
<p className="text-muted-foreground">
|
|
129
|
+
Detailed audit trail of all funnel signals and conversions
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex space-x-2">
|
|
133
|
+
<Button variant="outline" onClick={exportData}>
|
|
134
|
+
<Download className="w-4 h-4 mr-2" />
|
|
135
|
+
Export
|
|
136
|
+
</Button>
|
|
137
|
+
<Button onClick={loadSignals}>Refresh</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Filters */}
|
|
142
|
+
<Card>
|
|
143
|
+
<CardHeader>
|
|
144
|
+
<CardTitle className="flex items-center">
|
|
145
|
+
<Filter className="w-4 h-4 mr-2" />
|
|
146
|
+
Filters
|
|
147
|
+
</CardTitle>
|
|
148
|
+
</CardHeader>
|
|
149
|
+
<CardContent>
|
|
150
|
+
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
151
|
+
<div>
|
|
152
|
+
<Label htmlFor="search">Search</Label>
|
|
153
|
+
<div className="relative">
|
|
154
|
+
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
155
|
+
<Input
|
|
156
|
+
id="search"
|
|
157
|
+
placeholder="Email, domain..."
|
|
158
|
+
className="pl-8"
|
|
159
|
+
value={filters.search || ''}
|
|
160
|
+
onChange={(e) => updateFilter('search', e.target.value)}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div>
|
|
166
|
+
<Label htmlFor="date_from">From Date</Label>
|
|
167
|
+
<Input
|
|
168
|
+
id="date_from"
|
|
169
|
+
type="date"
|
|
170
|
+
value={filters.date_from || ''}
|
|
171
|
+
onChange={(e) => updateFilter('date_from', e.target.value)}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div>
|
|
176
|
+
<Label htmlFor="date_to">To Date</Label>
|
|
177
|
+
<Input
|
|
178
|
+
id="date_to"
|
|
179
|
+
type="date"
|
|
180
|
+
value={filters.date_to || ''}
|
|
181
|
+
onChange={(e) => updateFilter('date_to', e.target.value)}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div>
|
|
186
|
+
<Label htmlFor="source">Source</Label>
|
|
187
|
+
<Select value={filters.source || ''} onValueChange={(value) => updateFilter('source', value)}>
|
|
188
|
+
<SelectTrigger>
|
|
189
|
+
<SelectValue placeholder="All sources" />
|
|
190
|
+
</SelectTrigger>
|
|
191
|
+
<SelectContent>
|
|
192
|
+
<SelectItem value="">All sources</SelectItem>
|
|
193
|
+
<SelectItem value="mar">Marketing</SelectItem>
|
|
194
|
+
<SelectItem value="int">Internal</SelectItem>
|
|
195
|
+
<SelectItem value="use">Usage</SelectItem>
|
|
196
|
+
<SelectItem value="manual">Manual</SelectItem>
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div>
|
|
202
|
+
<Label htmlFor="stage">Stage</Label>
|
|
203
|
+
<Select value={filters.stage || ''} onValueChange={(value) => updateFilter('stage', value)}>
|
|
204
|
+
<SelectTrigger>
|
|
205
|
+
<SelectValue placeholder="All stages" />
|
|
206
|
+
</SelectTrigger>
|
|
207
|
+
<SelectContent>
|
|
208
|
+
<SelectItem value="">All stages</SelectItem>
|
|
209
|
+
<SelectItem value="anonymous">Anonymous</SelectItem>
|
|
210
|
+
<SelectItem value="identified">Identified</SelectItem>
|
|
211
|
+
<SelectItem value="installed">Installed</SelectItem>
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div>
|
|
217
|
+
<Label htmlFor="min_rating">Min Rating</Label>
|
|
218
|
+
<Select value={filters.min_rating?.toString() || ''} onValueChange={(value) => updateFilter('min_rating', value ? parseInt(value) : undefined)}>
|
|
219
|
+
<SelectTrigger>
|
|
220
|
+
<SelectValue placeholder="Any rating" />
|
|
221
|
+
</SelectTrigger>
|
|
222
|
+
<SelectContent>
|
|
223
|
+
<SelectItem value="">Any rating</SelectItem>
|
|
224
|
+
<SelectItem value="4">4+ (High)</SelectItem>
|
|
225
|
+
<SelectItem value="3">3+ (Medium)</SelectItem>
|
|
226
|
+
<SelectItem value="2">2+ (Low)</SelectItem>
|
|
227
|
+
<SelectItem value="1">1+ (All)</SelectItem>
|
|
228
|
+
</SelectContent>
|
|
229
|
+
</Select>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Results Summary */}
|
|
236
|
+
<Card>
|
|
237
|
+
<CardHeader>
|
|
238
|
+
<CardTitle>Results</CardTitle>
|
|
239
|
+
<CardDescription>
|
|
240
|
+
Showing {signals.length} of {totalCount} signals
|
|
241
|
+
</CardDescription>
|
|
242
|
+
</CardHeader>
|
|
243
|
+
<CardContent>
|
|
244
|
+
<Table>
|
|
245
|
+
<TableHeader>
|
|
246
|
+
<TableRow>
|
|
247
|
+
<TableHead>Timestamp</TableHead>
|
|
248
|
+
<TableHead>Action</TableHead>
|
|
249
|
+
<TableHead>User</TableHead>
|
|
250
|
+
<TableHead>Source</TableHead>
|
|
251
|
+
<TableHead>Stage</TableHead>
|
|
252
|
+
<TableHead>Rating</TableHead>
|
|
253
|
+
<TableHead>Score</TableHead>
|
|
254
|
+
<TableHead>Referrer</TableHead>
|
|
255
|
+
<TableHead>UTM</TableHead>
|
|
256
|
+
</TableRow>
|
|
257
|
+
</TableHeader>
|
|
258
|
+
<TableBody>
|
|
259
|
+
{loading ? (
|
|
260
|
+
<TableRow>
|
|
261
|
+
<TableCell colSpan={9} className="text-center py-8">
|
|
262
|
+
Loading signals...
|
|
263
|
+
</TableCell>
|
|
264
|
+
</TableRow>
|
|
265
|
+
) : signals.length === 0 ? (
|
|
266
|
+
<TableRow>
|
|
267
|
+
<TableCell colSpan={9} className="text-center py-8">
|
|
268
|
+
No signals found matching the current filters
|
|
269
|
+
</TableCell>
|
|
270
|
+
</TableRow>
|
|
271
|
+
) : (
|
|
272
|
+
signals.map((signal) => (
|
|
273
|
+
<TableRow key={signal.id}>
|
|
274
|
+
<TableCell>
|
|
275
|
+
<div>
|
|
276
|
+
<div className="text-sm">
|
|
277
|
+
{formatDistanceToNow(new Date(signal.created_at), { addSuffix: true })}
|
|
278
|
+
</div>
|
|
279
|
+
<div className="text-xs text-muted-foreground">
|
|
280
|
+
{new Date(signal.created_at).toLocaleString()}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</TableCell>
|
|
284
|
+
<TableCell>
|
|
285
|
+
<div>
|
|
286
|
+
<div className="font-medium">{signal.action_type}</div>
|
|
287
|
+
<div className="text-sm text-muted-foreground">
|
|
288
|
+
Value: {signal.action_value}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</TableCell>
|
|
292
|
+
<TableCell>
|
|
293
|
+
<div>
|
|
294
|
+
{signal.account_name ? (
|
|
295
|
+
<div>
|
|
296
|
+
<div className="font-medium">{signal.account_name}</div>
|
|
297
|
+
<div className="text-sm text-muted-foreground">{signal.person_email}</div>
|
|
298
|
+
</div>
|
|
299
|
+
) : signal.anonymous_id ? (
|
|
300
|
+
<div className="text-sm text-muted-foreground">
|
|
301
|
+
Anonymous: {signal.anonymous_id.slice(0, 8)}...
|
|
302
|
+
</div>
|
|
303
|
+
) : (
|
|
304
|
+
<div className="text-sm text-muted-foreground">Unknown</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</TableCell>
|
|
308
|
+
<TableCell>
|
|
309
|
+
<Badge className={getSourceColor(signal.source)}>
|
|
310
|
+
{signal.source}
|
|
311
|
+
</Badge>
|
|
312
|
+
</TableCell>
|
|
313
|
+
<TableCell>
|
|
314
|
+
<Badge variant="outline">{signal.stage}</Badge>
|
|
315
|
+
</TableCell>
|
|
316
|
+
<TableCell>
|
|
317
|
+
<Badge className={getRatingColor(signal.rating)}>
|
|
318
|
+
{signal.rating}
|
|
319
|
+
</Badge>
|
|
320
|
+
</TableCell>
|
|
321
|
+
<TableCell>
|
|
322
|
+
<span className="font-mono text-sm">{signal.raw_score}</span>
|
|
323
|
+
</TableCell>
|
|
324
|
+
<TableCell>
|
|
325
|
+
<div className="max-w-32 truncate text-sm">
|
|
326
|
+
{signal.referrer_domain || 'Direct'}
|
|
327
|
+
</div>
|
|
328
|
+
</TableCell>
|
|
329
|
+
<TableCell>
|
|
330
|
+
<div className="text-sm space-y-1">
|
|
331
|
+
{signal.utm_source && (
|
|
332
|
+
<div>Source: {signal.utm_source}</div>
|
|
333
|
+
)}
|
|
334
|
+
{signal.utm_medium && (
|
|
335
|
+
<div>Medium: {signal.utm_medium}</div>
|
|
336
|
+
)}
|
|
337
|
+
{signal.utm_campaign && (
|
|
338
|
+
<div>Campaign: {signal.utm_campaign}</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</TableCell>
|
|
342
|
+
</TableRow>
|
|
343
|
+
))
|
|
344
|
+
)}
|
|
345
|
+
</TableBody>
|
|
346
|
+
</Table>
|
|
347
|
+
|
|
348
|
+
{/* Pagination */}
|
|
349
|
+
{totalCount > 50 && (
|
|
350
|
+
<div className="flex justify-between items-center mt-4">
|
|
351
|
+
<div className="text-sm text-muted-foreground">
|
|
352
|
+
Page {page} of {Math.ceil(totalCount / 50)}
|
|
353
|
+
</div>
|
|
354
|
+
<div className="flex space-x-2">
|
|
355
|
+
<Button
|
|
356
|
+
variant="outline"
|
|
357
|
+
size="sm"
|
|
358
|
+
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
359
|
+
disabled={page === 1}
|
|
360
|
+
>
|
|
361
|
+
Previous
|
|
362
|
+
</Button>
|
|
363
|
+
<Button
|
|
364
|
+
variant="outline"
|
|
365
|
+
size="sm"
|
|
366
|
+
onClick={() => setPage(p => p + 1)}
|
|
367
|
+
disabled={page >= Math.ceil(totalCount / 50)}
|
|
368
|
+
>
|
|
369
|
+
Next
|
|
370
|
+
</Button>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
</CardContent>
|
|
375
|
+
</Card>
|
|
376
|
+
</div>
|
|
377
|
+
)
|
|
378
|
+
}
|