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.
Files changed (46) hide show
  1. package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
  2. package/components/CliInstancesCard.tsx +144 -0
  3. package/components/CortexSidebar.tsx +27 -53
  4. package/hooks/useTypeRegistry.ts +74 -0
  5. package/index.tsx +13 -24
  6. package/manifest.json +1 -13
  7. package/package.json +11 -20
  8. package/pages/courses/CoursesPage.tsx +14 -4
  9. package/pages/crm/AccountDetailPage.tsx +149 -194
  10. package/pages/crm/ContactsPage.tsx +7 -7
  11. package/pages/intelligence/IntelligencePage.tsx +24 -31
  12. package/pages/kb/KBEditorPage.tsx +9 -2
  13. package/pages/operations/AuditFunnelPage.tsx +378 -0
  14. package/pages/operations/InstallFunnelPage.tsx +410 -0
  15. package/pages/operations/OperationsDashboard.tsx +275 -0
  16. package/pages/support/RedactionReview.tsx +11 -2
  17. package/seed/link-types.json +8 -42
  18. package/seed/package.json +27 -0
  19. package/seed/roles.json +1 -1
  20. package/seed/types.json +2711 -596
  21. package/CHANGELOG.md +0 -42
  22. package/LICENSE.md +0 -223
  23. package/README.md +0 -69
  24. package/functions/custom_anonymous-sessions.ts +0 -356
  25. package/functions/custom_case_analysis.ts +0 -507
  26. package/functions/custom_community-escalation.ts +0 -234
  27. package/functions/custom_cortex-chunks.ts +0 -52
  28. package/functions/custom_funnel-scoring.ts +0 -256
  29. package/functions/custom_funnel-signal.ts +0 -430
  30. package/functions/custom_funnel-timers.ts +0 -449
  31. package/functions/custom_kb-chunker-test.ts +0 -364
  32. package/functions/custom_kb-chunker.ts +0 -576
  33. package/functions/custom_kb-embeddings.ts +0 -481
  34. package/functions/custom_kb-ingestion.ts +0 -448
  35. package/functions/custom_support-triage.ts +0 -649
  36. package/functions/custom_tag_management.ts +0 -314
  37. package/functions/webhook-handlers.ts +0 -29
  38. package/lib/resolveTypeId.ts +0 -16
  39. package/pages/crm/ContactDetailPage.tsx +0 -184
  40. package/pages/ops/AuditFunnelPage.tsx +0 -191
  41. package/pages/ops/CommandCenterPage.tsx +0 -377
  42. package/pages/ops/InstallFunnelPage.tsx +0 -226
  43. package/seed/accounts.json +0 -9
  44. package/seed/integrations.json +0 -24
  45. package/seed/pipelines.json +0 -59
  46. package/seed/triggers.json +0 -125
@@ -1,314 +0,0 @@
1
- import { createHandler } from './_shared/middleware'
2
- import { adminDb } from './_shared/db'
3
-
4
- /**
5
- * Custom Tag Management Handler
6
- *
7
- * Actions:
8
- * - POST ?action=create_or_get_tag - Get existing tag or create new one
9
- * - POST ?action=list_tags - List tags with filtering
10
- * - POST ?action=update_tag_usage - Increment tag usage count
11
- * - POST ?action=merge_tags - Merge duplicate tags
12
- *
13
- * Provides centralized tag management for case analysis and other systems.
14
- */
15
-
16
- // ─── CONSTANTS ────────────────────────────────────────────────────────────────
17
-
18
- const TAG_TYPE_ID = 'tag' // Will be looked up by slug
19
-
20
- // ─── TYPES ────────────────────────────────────────────────────────────────────
21
-
22
- interface TagRequest {
23
- slug: string
24
- name: string
25
- purpose?: string
26
- category: 'bug_classification' | 'knowledge_value' | 'process_type' | 'sentiment'
27
- applicable_to?: string[]
28
- }
29
-
30
- interface TagResponse {
31
- id: string
32
- slug: string
33
- name: string
34
- purpose?: string
35
- category: string
36
- applicable_to: string[]
37
- usage_count: number
38
- created_at: string
39
- updated_at: string
40
- }
41
-
42
- interface ListTagsRequest {
43
- category?: string
44
- applicable_to?: string
45
- limit?: number
46
- offset?: number
47
- }
48
-
49
- // ─── HELPERS ──────────────────────────────────────────────────────────────────
50
-
51
- async function getTagTypeId(): Promise<string> {
52
- const { data: tagType } = await adminDb
53
- .from('types')
54
- .select('id')
55
- .eq('slug', 'tag')
56
- .single()
57
-
58
- if (!tagType) throw new Error('Tag type not found')
59
- return tagType.id
60
- }
61
-
62
- async function findExistingTag(slug: string): Promise<TagResponse | null> {
63
- const { data: tag } = await adminDb
64
- .from('items')
65
- .select('*')
66
- .eq('type_id', await getTagTypeId())
67
- .eq('data->>slug', slug)
68
- .single()
69
-
70
- if (!tag) return null
71
-
72
- return {
73
- id: tag.id,
74
- slug: tag.data?.slug || '',
75
- name: tag.title,
76
- purpose: tag.description,
77
- category: tag.data?.category || '',
78
- applicable_to: tag.data?.applicable_to || ['ticket'],
79
- usage_count: tag.data?.usage_count || 0,
80
- created_at: tag.created_at,
81
- updated_at: tag.updated_at
82
- }
83
- }
84
-
85
- async function createTag(
86
- tagData: TagRequest,
87
- accountId: string
88
- ): Promise<TagResponse> {
89
- const tagTypeId = await getTagTypeId()
90
-
91
- const { data: tag, error } = await adminDb
92
- .from('items')
93
- .insert({
94
- type_id: tagTypeId,
95
- account_id: accountId,
96
- title: tagData.name,
97
- description: tagData.purpose,
98
- data: {
99
- slug: tagData.slug,
100
- name: tagData.name,
101
- purpose: tagData.purpose,
102
- applicable_to: tagData.applicable_to || ['ticket'],
103
- category: tagData.category,
104
- usage_count: 1
105
- },
106
- status: 'active'
107
- })
108
- .select('*')
109
- .single()
110
-
111
- if (error || !tag) throw new Error(`Failed to create tag: ${error?.message}`)
112
-
113
- return {
114
- id: tag.id,
115
- slug: tag.data?.slug || '',
116
- name: tag.title,
117
- purpose: tag.description,
118
- category: tag.data?.category || '',
119
- applicable_to: tag.data?.applicable_to || ['ticket'],
120
- usage_count: tag.data?.usage_count || 0,
121
- created_at: tag.created_at,
122
- updated_at: tag.updated_at
123
- }
124
- }
125
-
126
- async function incrementTagUsage(tagId: string): Promise<void> {
127
- await adminDb
128
- .from('items')
129
- .update({
130
- data: adminDb.sql`jsonb_set(data, '{usage_count}', COALESCE((data->>'usage_count')::int, 0) + 1)`,
131
- updated_at: new Date().toISOString()
132
- })
133
- .eq('id', tagId)
134
- }
135
-
136
- // ─── ACTIONS ───────────────────────────────────────────────────────────────────
137
-
138
- async function handleCreateOrGetTag(
139
- ctx: any,
140
- body: TagRequest
141
- ): Promise<TagResponse> {
142
- const account_id = ctx.accountId as string
143
-
144
- if (!account_id) throw new Error('Account context required')
145
- if (!body.slug || !body.name || !body.category) {
146
- throw new Error('slug, name, and category are required')
147
- }
148
-
149
- // Validate slug format
150
- if (!/^[a-z0-9_-]+$/.test(body.slug)) {
151
- throw new Error('Slug must contain only lowercase letters, numbers, hyphens, and underscores')
152
- }
153
-
154
- // Try to find existing tag
155
- const existingTag = await findExistingTag(body.slug)
156
-
157
- if (existingTag) {
158
- // Increment usage count
159
- await incrementTagUsage(existingTag.id)
160
-
161
- // Return updated tag with incremented count
162
- const updatedTag = await findExistingTag(body.slug)
163
- if (!updatedTag) throw new Error('Failed to retrieve updated tag')
164
-
165
- return updatedTag
166
- }
167
-
168
- // Create new tag
169
- return await createTag(body, account_id)
170
- }
171
-
172
- async function handleListTags(
173
- ctx: any,
174
- body: ListTagsRequest
175
- ): Promise<{ tags: TagResponse[]; total: number }> {
176
- const { category, applicable_to, limit = 50, offset = 0 } = body
177
- const tagTypeId = await getTagTypeId()
178
-
179
- let query = adminDb
180
- .from('items')
181
- .select('*', { count: 'exact' })
182
- .eq('type_id', tagTypeId)
183
- .eq('status', 'active')
184
- .order('data->>usage_count', { ascending: false })
185
- .range(offset, offset + limit - 1)
186
-
187
- // Add filters
188
- if (category) {
189
- query = query.eq('data->>category', category)
190
- }
191
-
192
- if (applicable_to) {
193
- query = query.contains('data->>applicable_to', [applicable_to])
194
- }
195
-
196
- const { data: tags, error, count } = await query
197
-
198
- if (error) throw new Error(`Failed to list tags: ${error?.message}`)
199
-
200
- const formattedTags: TagResponse[] = (tags || []).map(tag => ({
201
- id: tag.id,
202
- slug: tag.data?.slug || '',
203
- name: tag.title,
204
- purpose: tag.description,
205
- category: tag.data?.category || '',
206
- applicable_to: tag.data?.applicable_to || ['ticket'],
207
- usage_count: tag.data?.usage_count || 0,
208
- created_at: tag.created_at,
209
- updated_at: tag.updated_at
210
- }))
211
-
212
- return {
213
- tags: formattedTags,
214
- total: count || 0
215
- }
216
- }
217
-
218
- async function handleUpdateTagUsage(
219
- ctx: any,
220
- body: { tag_id: string }
221
- ): Promise<void> {
222
- if (!body.tag_id) throw new Error('tag_id is required')
223
-
224
- await incrementTagUsage(body.tag_id)
225
- }
226
-
227
- async function handleMergeTags(
228
- ctx: any,
229
- body: { source_tag_id: string; target_tag_id: string }
230
- ): Promise<{ merged_count: number }> {
231
- const { source_tag_id, target_tag_id } = body
232
-
233
- if (!source_tag_id || !target_tag_id) {
234
- throw new Error('source_tag_id and target_tag_id are required')
235
- }
236
-
237
- if (source_tag_id === target_tag_id) {
238
- throw new Error('Source and target tags cannot be the same')
239
- }
240
-
241
- // Get source tag info
242
- const { data: sourceTag } = await adminDb
243
- .from('items')
244
- .select('data')
245
- .eq('id', source_tag_id)
246
- .single()
247
-
248
- if (!sourceTag) throw new Error('Source tag not found')
249
-
250
- // Update all references to source tag to point to target tag
251
- const sourceTagSlug = sourceTag.data?.slug
252
-
253
- // Update ticket analysis_tags arrays
254
- const { data: ticketsToUpdate } = await adminDb
255
- .from('items')
256
- .select('id, data')
257
- .eq('status', 'active')
258
- .contains('data->>case_analysis->>analysis_tags', [source_tag_id])
259
-
260
- let mergedCount = 0
261
-
262
- for (const ticket of ticketsToUpdate || []) {
263
- const caseAnalysis = ticket.data?.case_analysis || {}
264
- const analysisTags = caseAnalysis.analysis_tags || []
265
-
266
- // Replace source tag with target tag
267
- const updatedTags = analysisTags.map((tagId: string) =>
268
- tagId === source_tag_id ? target_tag_id : tagId
269
- )
270
-
271
- await adminDb
272
- .from('items')
273
- .update({
274
- data: adminDb.sql`jsonb_set(data, '{case_analysis,analysis_tags}', ${updatedTags}::jsonb)`,
275
- updated_at: new Date().toISOString()
276
- })
277
- .eq('id', ticket.id)
278
-
279
- mergedCount++
280
- }
281
-
282
- // Increment target tag usage by source tag's usage count
283
- const sourceUsage = sourceTag.data?.usage_count || 0
284
- for (let i = 0; i < sourceUsage; i++) {
285
- await incrementTagUsage(target_tag_id)
286
- }
287
-
288
- // Delete source tag
289
- await adminDb
290
- .from('items')
291
- .update({ status: 'deleted', updated_at: new Date().toISOString() })
292
- .eq('id', source_tag_id)
293
-
294
- return { merged_count }
295
- }
296
-
297
- // ─── HANDLER ──────────────────────────────────────────────────────────────────
298
-
299
- export const handler = createHandler(async (ctx, body) => {
300
- const action = ctx.query?.action
301
-
302
- switch (action) {
303
- case 'create_or_get_tag':
304
- return await handleCreateOrGetTag(ctx, body)
305
- case 'list_tags':
306
- return await handleListTags(ctx, body)
307
- case 'update_tag_usage':
308
- return await handleUpdateTagUsage(ctx, body)
309
- case 'merge_tags':
310
- return await handleMergeTags(ctx, body)
311
- default:
312
- throw new Error(`Unknown action: ${action}. Use create_or_get_tag, list_tags, update_tag_usage, or merge_tags.`)
313
- }
314
- })
@@ -1,29 +0,0 @@
1
- /**
2
- * Webhook Handler Self-Registration
3
- *
4
- * Registers custom webhook handlers in the dynamic `webhook_handlers` table.
5
- * Core resolves handlers at runtime via `webhook-registry.ts` — no static imports needed.
6
- *
7
- * To add a new handler:
8
- * 1. Create custom/functions/custom_{name}.ts with a default export function
9
- * 2. Add a registerWebhookHandler() call below
10
- * 3. Set integration config handler.path = the handler name
11
- */
12
-
13
- import { registerWebhookHandler } from './_shared/webhook-registration'
14
-
15
- // Self-register on module load (idempotent)
16
- registerWebhookHandler({
17
- name: 'cortex-handler',
18
- functionName: 'custom_cortex-handler',
19
- description: 'Cortex integration webhook handler',
20
- events: ['integration.webhook'],
21
- }).catch(console.error)
22
-
23
- registerWebhookHandler({
24
- name: 'funnel-signal',
25
- functionName: 'custom_funnel-signal',
26
- description: 'Funnel signal processing webhook handler',
27
- events: ['integration.webhook'],
28
- }).catch(console.error)
29
-
@@ -1,16 +0,0 @@
1
- // Frontend type ID resolution — never hardcode UUIDs in custom app code.
2
- // IDs differ on every fresh install; always resolve by slug at call time.
3
-
4
- import { apiFetch } from '@core/lib/api'
5
-
6
- const cache: Record<string, string> = {}
7
-
8
- export async function resolveTypeId(slug: string): Promise<string> {
9
- if (cache[slug]) return cache[slug]
10
-
11
- const data = await apiFetch(`/.netlify/functions/types?action=getBySlug&slug=${encodeURIComponent(slug)}`)
12
- if (!data?.id) throw new Error(`Type not found: ${slug}`)
13
-
14
- cache[slug] = data.id
15
- return data.id
16
- }
@@ -1,184 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useParams, useNavigate } from 'react-router-dom'
3
- import { apiFetch } from '@core/lib/api'
4
- import { Badge } from '@core/components/ui/badge'
5
- import { Skeleton } from '@core/components/ui/skeleton'
6
- import { Button } from '@core/components/ui/button'
7
- import { Separator } from '@core/components/ui/separator'
8
- import { ArrowLeft, User, Mail, Phone, Building2, Calendar } from 'lucide-react'
9
-
10
- interface Person {
11
- id: string
12
- email?: string
13
- full_name?: string
14
- phone?: string
15
- status?: string
16
- avatar_url?: string
17
- account_id?: string
18
- data?: Record<string, any>
19
- is_active?: boolean
20
- created_at: string
21
- updated_at?: string
22
- }
23
-
24
- interface Account {
25
- id: string
26
- slug?: string
27
- display_name?: string
28
- }
29
-
30
- export default function ContactDetailPage() {
31
- const { id } = useParams<{ id: string }>()
32
- const navigate = useNavigate()
33
- const [person, setPerson] = useState<Person | null>(null)
34
- const [account, setAccount] = useState<Account | null>(null)
35
- const [loading, setLoading] = useState(true)
36
- const [error, setError] = useState<string | null>(null)
37
-
38
- useEffect(() => {
39
- if (!id) return
40
- setLoading(true)
41
- apiFetch(`/api/admin-data?action=get&entity=people&id=${id}`)
42
- .then(r => r.json())
43
- .then(json => {
44
- const p = json?.data || json
45
- if (!p?.id) throw new Error('Contact not found')
46
- setPerson(p)
47
- if (p.account_id) {
48
- return apiFetch(`/api/admin-data?action=get&entity=accounts&id=${p.account_id}`)
49
- .then(r2 => r2.json())
50
- .then(aj => setAccount(aj?.data || aj || null))
51
- }
52
- })
53
- .catch(err => setError(err.message || 'Failed to load contact'))
54
- .finally(() => setLoading(false))
55
- }, [id])
56
-
57
- if (loading) {
58
- return (
59
- <div className="p-6 space-y-4">
60
- <Skeleton className="h-8 w-48" />
61
- <Skeleton className="h-64 w-full" />
62
- </div>
63
- )
64
- }
65
-
66
- if (error || !person) {
67
- return (
68
- <div className="p-6">
69
- <Button variant="ghost" size="sm" onClick={() => navigate('/cortex/crm/contacts')}>
70
- <ArrowLeft className="h-4 w-4 mr-2" /> Back to Contacts
71
- </Button>
72
- <div className="mt-8 text-center text-slate-400">{error || 'Contact not found'}</div>
73
- </div>
74
- )
75
- }
76
-
77
- const initials = (person.full_name || '?')
78
- .split(' ')
79
- .map(w => w[0])
80
- .join('')
81
- .toUpperCase()
82
- .slice(0, 2)
83
-
84
- return (
85
- <div className="p-6 space-y-6 max-w-4xl">
86
- {/* Header */}
87
- <div className="flex items-center gap-3">
88
- <Button variant="ghost" size="icon" onClick={() => navigate('/cortex/crm/contacts')}>
89
- <ArrowLeft className="h-4 w-4" />
90
- </Button>
91
- <div className="h-12 w-12 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-lg font-semibold shrink-0">
92
- {initials}
93
- </div>
94
- <div className="flex-1 min-w-0">
95
- <h1 className="text-2xl font-bold text-slate-900 truncate">{person.full_name || 'Unnamed Contact'}</h1>
96
- <p className="text-sm text-slate-500">{person.email || '—'}</p>
97
- </div>
98
- <div className="flex items-center gap-2">
99
- {person.status && (
100
- <Badge variant={person.status === 'active' ? 'default' : 'secondary'}>{person.status}</Badge>
101
- )}
102
- {person.is_active === false && (
103
- <Badge variant="destructive">Inactive</Badge>
104
- )}
105
- </div>
106
- </div>
107
-
108
- <Separator />
109
-
110
- {/* Details grid */}
111
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
112
- {/* Profile card */}
113
- <div className="bg-white rounded-lg border border-slate-200 p-5 space-y-4">
114
- <h2 className="text-sm font-semibold text-slate-700 uppercase tracking-wider">Profile</h2>
115
-
116
- <div className="space-y-3">
117
- <div className="flex items-center gap-3 text-sm">
118
- <User className="h-4 w-4 text-slate-400 shrink-0" />
119
- <span className="text-slate-500 w-20">Name</span>
120
- <span className="text-slate-900 font-medium">{person.full_name || '—'}</span>
121
- </div>
122
-
123
- <div className="flex items-center gap-3 text-sm">
124
- <Mail className="h-4 w-4 text-slate-400 shrink-0" />
125
- <span className="text-slate-500 w-20">Email</span>
126
- <span className="text-slate-900">{person.email || '—'}</span>
127
- </div>
128
-
129
- <div className="flex items-center gap-3 text-sm">
130
- <Phone className="h-4 w-4 text-slate-400 shrink-0" />
131
- <span className="text-slate-500 w-20">Phone</span>
132
- <span className="text-slate-900">{person.phone || '—'}</span>
133
- </div>
134
-
135
- <div className="flex items-center gap-3 text-sm">
136
- <Calendar className="h-4 w-4 text-slate-400 shrink-0" />
137
- <span className="text-slate-500 w-20">Joined</span>
138
- <span className="text-slate-900">{new Date(person.created_at).toLocaleDateString()}</span>
139
- </div>
140
- </div>
141
- </div>
142
-
143
- {/* Account card */}
144
- <div className="bg-white rounded-lg border border-slate-200 p-5 space-y-4">
145
- <h2 className="text-sm font-semibold text-slate-700 uppercase tracking-wider">Account</h2>
146
-
147
- {account ? (
148
- <div
149
- className="flex items-center gap-3 p-3 rounded-md hover:bg-slate-50 cursor-pointer transition-colors"
150
- onClick={() => navigate(`/cortex/crm/accounts/${account.id}`)}
151
- >
152
- <div className="h-9 w-9 rounded-lg bg-slate-100 flex items-center justify-center shrink-0">
153
- <Building2 className="h-4 w-4 text-slate-500" />
154
- </div>
155
- <div>
156
- <p className="text-sm font-medium text-slate-900">{account.display_name || account.slug || '—'}</p>
157
- {account.slug && <p className="text-xs text-slate-400">{account.slug}</p>}
158
- </div>
159
- </div>
160
- ) : (
161
- <p className="text-sm text-slate-400">No account linked</p>
162
- )}
163
- </div>
164
- </div>
165
-
166
- {/* Data section */}
167
- {person.data && Object.keys(person.data).length > 0 && (
168
- <div className="bg-white rounded-lg border border-slate-200 p-5 space-y-3">
169
- <h2 className="text-sm font-semibold text-slate-700 uppercase tracking-wider">Additional Data</h2>
170
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
171
- {Object.entries(person.data).map(([key, value]) => (
172
- <div key={key} className="text-sm">
173
- <span className="text-slate-500">{key.replace(/_/g, ' ')}: </span>
174
- <span className="text-slate-900 font-medium">
175
- {typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
176
- </span>
177
- </div>
178
- ))}
179
- </div>
180
- </div>
181
- )}
182
- </div>
183
- )
184
- }