spine-framework 0.3.17 → 0.3.19
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/.framework/cli/bin.mjs +16 -0
- package/.framework/cli/commands/create-app.ts +1 -1
- package/.framework/cli/commands/dev.ts +215 -33
- package/.framework/cli/index.ts +37 -75
- package/.framework/functions/_shared/app-manifest.ts +8 -15
- package/.framework/functions/_shared/db.ts +11 -15
- package/.framework/functions/_shared/index.ts +7 -16
- package/.framework/functions/_shared/principal.ts +4 -62
- package/.framework/functions/_shared/resolve-ids.ts +6 -6
- package/.framework/functions/_shared/webhook-registry.ts +43 -1
- package/.framework/functions/admin-funnel.ts +104 -0
- package/.framework/functions/admin-install-funnel.ts +225 -0
- package/.framework/functions/admin-signals.ts +241 -0
- package/.framework/functions/integration-routes.ts +4 -8
- package/.framework/migrations/001_schema.sql +406 -0
- package/.framework/migrations/002_rls_and_functions.sql +710 -0
- package/.framework/migrations/003_system_data.sql +520 -0
- package/.framework/migrations/004_enhancements.sql +43 -0
- package/.framework/public/_redirects +3 -0
- package/.framework/public/spine-logo.jpg +0 -0
- package/.framework/public/spine-logo.svg +3 -0
- package/{scripts → .framework/scripts}/assemble-frontend.sh +8 -5
- package/{scripts → .framework/scripts}/assemble-functions.sh +4 -47
- package/{scripts → .framework/scripts}/assemble.sh +2 -8
- package/{scripts → .framework/scripts}/boundary-check.sh +5 -9
- package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
- package/.framework/seeds/link_types/account_cli_instances.json +9 -0
- package/.framework/seeds/types/funnel_signal_use.json +98 -0
- package/.framework/seeds/types/spine_instance.json +109 -0
- package/.framework/src/App.tsx +11 -21
- package/.framework/src/components/CustomAppLoader.tsx +2 -19
- package/.framework/src/components/ui/accordion.tsx +1 -1
- package/.framework/src/components/ui/alert-dialog.tsx +1 -1
- package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
- package/.framework/src/components/ui/avatar.tsx +1 -1
- package/.framework/src/components/ui/badge.tsx +2 -2
- package/.framework/src/components/ui/breadcrumb.tsx +2 -2
- package/.framework/src/components/ui/button-group.tsx +2 -2
- package/.framework/src/components/ui/button.tsx +2 -2
- package/.framework/src/components/ui/chart.tsx +6 -1
- package/.framework/src/components/ui/checkbox.tsx +1 -1
- package/.framework/src/components/ui/collapsible.tsx +1 -1
- package/.framework/src/components/ui/context-menu.tsx +1 -1
- package/.framework/src/components/ui/dialog.tsx +1 -1
- package/.framework/src/components/ui/direction.tsx +17 -1
- package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
- package/.framework/src/components/ui/hover-card.tsx +1 -1
- package/.framework/src/components/ui/item.tsx +2 -2
- package/.framework/src/components/ui/label.tsx +1 -1
- package/.framework/src/components/ui/menubar.tsx +1 -1
- package/.framework/src/components/ui/navigation-menu.tsx +1 -1
- package/.framework/src/components/ui/popover.tsx +1 -1
- package/.framework/src/components/ui/progress.tsx +1 -1
- package/.framework/src/components/ui/radio-group.tsx +1 -1
- package/.framework/src/components/ui/scroll-area.tsx +1 -1
- package/.framework/src/components/ui/select.tsx +1 -1
- package/.framework/src/components/ui/separator.tsx +1 -1
- package/.framework/src/components/ui/sheet.tsx +1 -1
- package/.framework/src/components/ui/sidebar.tsx +6 -6
- package/.framework/src/components/ui/slider.tsx +1 -1
- package/.framework/src/components/ui/switch.tsx +1 -1
- package/.framework/src/components/ui/tabs.tsx +1 -1
- package/.framework/src/components/ui/toggle-group.tsx +1 -1
- package/.framework/src/components/ui/toggle.tsx +1 -1
- package/.framework/src/components/ui/tooltip.tsx +1 -1
- package/.framework/src/pages/auth/LoginPage.tsx +10 -9
- package/LICENSE.md +63 -93
- package/README.md +254 -129
- package/custom/.devin/skills/install-app/SKILL.md +39 -0
- package/custom/.devin/workflows/app-install.yaml +83 -0
- package/custom/README.md +79 -0
- package/docs/APPS.md +232 -0
- package/docs/ARCHITECTURE.md +246 -0
- package/docs/INSTALL.md +155 -0
- package/docs/README.md +170 -0
- package/docs/design-schema-spec.md +1837 -0
- package/docs/dev-guides/01-create-an-app.md +183 -0
- package/docs/dev-guides/02-create-a-function.md +178 -0
- package/docs/dev-guides/03-create-a-component.md +178 -0
- package/docs/dev-guides/04-webhook-handlers.md +129 -0
- package/docs/dev-guides/05-testing.md +168 -0
- package/docs/dev-guides/README.md +40 -0
- package/docs/npm-packages-plan.md +288 -0
- package/docs/spine-core--api-reference.md +822 -0
- package/package.json +79 -150
- package/.framework/cli/bin.cjs +0 -14
- package/.framework/cli/commands/claim.ts +0 -430
- package/.framework/cli/commands/init.ts +0 -219
- package/.framework/cli/commands/install-app.ts +0 -685
- package/.framework/cli/commands/migrate.ts +0 -178
- package/.framework/cli/commands/publish.ts +0 -191
- package/.framework/cli/commands/status.ts +0 -268
- package/.framework/cli/commands/uninstall-app.ts +0 -166
- package/.framework/cli/commands/update-app.ts +0 -397
- package/.framework/cli/welcome.cjs +0 -45
- package/.framework/config/signal-client.ts +0 -147
- package/.framework/config/signals.ts +0 -70
- package/.framework/docs/STABILITY.md +0 -52
- package/.framework/docs/examples/custom-app/README.md +0 -77
- package/.framework/docs/examples/custom-function/README.md +0 -27
- package/.framework/docs/examples/custom-function/handler.ts +0 -48
- package/.framework/docs/examples/custom-webhook/README.md +0 -68
- package/.framework/functions/handler-registry.ts +0 -22
- package/.framework/functions/signal.ts +0 -168
- package/.framework/migrations/000_foundation.sql +0 -1302
- package/.framework/migrations/001_seed.sql +0 -163
- package/.framework/migrations/002_seed_constraints.sql +0 -19
- package/.framework/migrations/003_auth_user_trigger.sql +0 -67
- package/.framework/migrations/004_app_version_tracking.sql +0 -8
- package/.framework/migrations/005_pipelines_slug_stages.sql +0 -21
- package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
- package/CHANGELOG.md +0 -71
- package/STRUCTURE.md +0 -150
- package/bin/spine-framework.cjs +0 -62
- package/bin/welcome.cjs +0 -58
- package/bin/ws-shim.ts +0 -10
- package/config/components.json +0 -25
- package/config/deno.lock +0 -108
- package/config/package-lock.json +0 -17183
- package/config/postcss.config.cjs +0 -10
- package/config/tailwind.config.cjs +0 -78
- package/config/tsconfig.build.json +0 -32
- package/config/tsconfig.cli.json +0 -18
- package/config/tsconfig.json +0 -41
- package/config/tsconfig.node.json +0 -17
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/typedoc.json +0 -16
- package/config/vite.config.d.ts +0 -2
- package/config/vite.config.ts +0 -71
- package/dist/cli/commands/agents.d.ts +0 -39
- package/dist/cli/commands/agents.d.ts.map +0 -1
- package/dist/cli/commands/auth.d.ts +0 -36
- package/dist/cli/commands/auth.d.ts.map +0 -1
- package/dist/cli/commands/claim.d.ts +0 -38
- package/dist/cli/commands/claim.d.ts.map +0 -1
- package/dist/cli/commands/create-app.d.ts +0 -23
- package/dist/cli/commands/create-app.d.ts.map +0 -1
- package/dist/cli/commands/dev.d.ts +0 -24
- package/dist/cli/commands/dev.d.ts.map +0 -1
- package/dist/cli/commands/doctor.d.ts +0 -42
- package/dist/cli/commands/doctor.d.ts.map +0 -1
- package/dist/cli/commands/generate.d.ts +0 -36
- package/dist/cli/commands/generate.d.ts.map +0 -1
- package/dist/cli/commands/init.d.ts +0 -20
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/install-app.d.ts +0 -30
- package/dist/cli/commands/install-app.d.ts.map +0 -1
- package/dist/cli/commands/items.d.ts +0 -45
- package/dist/cli/commands/items.d.ts.map +0 -1
- package/dist/cli/commands/migrate.d.ts +0 -21
- package/dist/cli/commands/migrate.d.ts.map +0 -1
- package/dist/cli/commands/migrations.d.ts +0 -41
- package/dist/cli/commands/migrations.d.ts.map +0 -1
- package/dist/cli/commands/pipelines.d.ts +0 -40
- package/dist/cli/commands/pipelines.d.ts.map +0 -1
- package/dist/cli/commands/publish.d.ts +0 -23
- package/dist/cli/commands/publish.d.ts.map +0 -1
- package/dist/cli/commands/status.d.ts +0 -23
- package/dist/cli/commands/status.d.ts.map +0 -1
- package/dist/cli/commands/system.d.ts +0 -29
- package/dist/cli/commands/system.d.ts.map +0 -1
- package/dist/cli/commands/test.d.ts +0 -46
- package/dist/cli/commands/test.d.ts.map +0 -1
- package/dist/cli/commands/uninstall-app.d.ts +0 -23
- package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
- package/dist/cli/commands/update-app.d.ts +0 -24
- package/dist/cli/commands/update-app.d.ts.map +0 -1
- package/dist/cli/context.d.ts +0 -88
- package/dist/cli/context.d.ts.map +0 -1
- package/dist/cli/env-loader.d.ts +0 -14
- package/dist/cli/env-loader.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -44
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/config/signals.d.ts +0 -82
- package/dist/config/signals.d.ts.map +0 -1
- package/dist/functions/_shared/agent-runner.d.ts +0 -156
- package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
- package/dist/functions/_shared/app-manifest.d.ts +0 -68
- package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
- package/dist/functions/_shared/audit.d.ts +0 -91
- package/dist/functions/_shared/audit.d.ts.map +0 -1
- package/dist/functions/_shared/db.d.ts +0 -125
- package/dist/functions/_shared/db.d.ts.map +0 -1
- package/dist/functions/_shared/index.d.ts +0 -299
- package/dist/functions/_shared/index.d.ts.map +0 -1
- package/dist/functions/_shared/middleware.d.ts +0 -315
- package/dist/functions/_shared/middleware.d.ts.map +0 -1
- package/dist/functions/_shared/permissions.d.ts +0 -626
- package/dist/functions/_shared/permissions.d.ts.map +0 -1
- package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
- package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
- package/dist/functions/_shared/principal.d.ts +0 -284
- package/dist/functions/_shared/principal.d.ts.map +0 -1
- package/dist/functions/_shared/resolve-ids.d.ts +0 -10
- package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
- package/dist/functions/_shared/schema-utils.d.ts +0 -181
- package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
- package/dist/functions/_shared/testing.d.ts +0 -172
- package/dist/functions/_shared/testing.d.ts.map +0 -1
- package/dist/functions/_shared/trigger-engine.d.ts +0 -140
- package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registration.d.ts +0 -81
- package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registry.d.ts +0 -41
- package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
- package/dist/functions/account-nodes.d.ts +0 -48
- package/dist/functions/account-nodes.d.ts.map +0 -1
- package/dist/functions/admin-data.d.ts +0 -178
- package/dist/functions/admin-data.d.ts.map +0 -1
- package/dist/functions/ai-agents.d.ts +0 -125
- package/dist/functions/ai-agents.d.ts.map +0 -1
- package/dist/functions/api-keys.d.ts +0 -140
- package/dist/functions/api-keys.d.ts.map +0 -1
- package/dist/functions/apps.d.ts +0 -163
- package/dist/functions/apps.d.ts.map +0 -1
- package/dist/functions/auth.d.ts +0 -74
- package/dist/functions/auth.d.ts.map +0 -1
- package/dist/functions/debug-auth.d.ts +0 -33
- package/dist/functions/debug-auth.d.ts.map +0 -1
- package/dist/functions/embeddings.d.ts +0 -205
- package/dist/functions/embeddings.d.ts.map +0 -1
- package/dist/functions/handler-registry.d.ts +0 -22
- package/dist/functions/handler-registry.d.ts.map +0 -1
- package/dist/functions/integration-routes.d.ts +0 -45
- package/dist/functions/integration-routes.d.ts.map +0 -1
- package/dist/functions/integrations.d.ts +0 -124
- package/dist/functions/integrations.d.ts.map +0 -1
- package/dist/functions/item-progress.d.ts +0 -41
- package/dist/functions/item-progress.d.ts.map +0 -1
- package/dist/functions/logs.d.ts +0 -162
- package/dist/functions/logs.d.ts.map +0 -1
- package/dist/functions/observability.d.ts +0 -123
- package/dist/functions/observability.d.ts.map +0 -1
- package/dist/functions/pipeline-executions.d.ts +0 -190
- package/dist/functions/pipeline-executions.d.ts.map +0 -1
- package/dist/functions/pipelines.d.ts +0 -171
- package/dist/functions/pipelines.d.ts.map +0 -1
- package/dist/functions/prompt-configs.d.ts +0 -125
- package/dist/functions/prompt-configs.d.ts.map +0 -1
- package/dist/functions/roles.d.ts +0 -118
- package/dist/functions/roles.d.ts.map +0 -1
- package/dist/functions/signal.d.ts +0 -73
- package/dist/functions/signal.d.ts.map +0 -1
- package/dist/functions/system-cron.d.ts +0 -65
- package/dist/functions/system-cron.d.ts.map +0 -1
- package/dist/functions/system.d.ts +0 -29
- package/dist/functions/system.d.ts.map +0 -1
- package/dist/functions/tests.d.ts +0 -28
- package/dist/functions/tests.d.ts.map +0 -1
- package/dist/functions/timers.d.ts +0 -139
- package/dist/functions/timers.d.ts.map +0 -1
- package/dist/functions/triggers.d.ts +0 -203
- package/dist/functions/triggers.d.ts.map +0 -1
- package/dist/functions/types.d.ts +0 -151
- package/dist/functions/types.d.ts.map +0 -1
- package/dist/src/types/types.d.ts +0 -364
- package/dist/src/types/types.d.ts.map +0 -1
- package/index.html +0 -13
- package/netlify.toml +0 -36
- package/package-project.json +0 -71
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/netlify-dev-wrapper.sh +0 -22
- package/vitest.config.ts +0 -45
- /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
- /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
- /package/{scripts → .framework/scripts}/verify-integrity.sh +0 -0
|
@@ -32,11 +32,6 @@
|
|
|
32
32
|
|
|
33
33
|
import { getUserDb, adminDb } from './db'
|
|
34
34
|
import { createClient } from '@supabase/supabase-js'
|
|
35
|
-
import WebSocket from 'ws'
|
|
36
|
-
|
|
37
|
-
// WebSocket constructor for Supabase Realtime in Node.js environment
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
const RealtimeWebSocket = WebSocket as any
|
|
40
35
|
|
|
41
36
|
// ─── AUTH CLIENT ─────────────────────────────────────────────────────────────
|
|
42
37
|
|
|
@@ -51,8 +46,7 @@ const supabase = createClient(
|
|
|
51
46
|
auth: {
|
|
52
47
|
autoRefreshToken: false,
|
|
53
48
|
persistSession: false
|
|
54
|
-
}
|
|
55
|
-
realtime: { transport: RealtimeWebSocket } // WebSocket for Node.js 20+
|
|
49
|
+
}
|
|
56
50
|
}
|
|
57
51
|
)
|
|
58
52
|
|
|
@@ -501,7 +495,7 @@ async function resolveHumanPrincipal(token: string, event: any): Promise<Princip
|
|
|
501
495
|
type: 'human',
|
|
502
496
|
accountId: person.account_id || null,
|
|
503
497
|
roles: roleSlugs,
|
|
504
|
-
displayName: person.
|
|
498
|
+
displayName: person.display_name || person.email,
|
|
505
499
|
email: person.email,
|
|
506
500
|
provenance: {
|
|
507
501
|
sourceType: 'jwt',
|
|
@@ -567,60 +561,8 @@ async function resolveInternalPersonId(authUserId: string, email?: string): Prom
|
|
|
567
561
|
|
|
568
562
|
if (byAuthId) return byAuthId.id
|
|
569
563
|
|
|
570
|
-
// Not found
|
|
571
|
-
|
|
572
|
-
const { data: account } = await adminDb
|
|
573
|
-
.from('accounts')
|
|
574
|
-
.select('id')
|
|
575
|
-
.eq('slug', 'spine-system')
|
|
576
|
-
.single()
|
|
577
|
-
|
|
578
|
-
const { data: type } = await adminDb
|
|
579
|
-
.from('types')
|
|
580
|
-
.select('id')
|
|
581
|
-
.eq('kind', 'person')
|
|
582
|
-
.eq('slug', 'person')
|
|
583
|
-
.single()
|
|
584
|
-
|
|
585
|
-
if (!account || !type) {
|
|
586
|
-
throw new Error('Cannot auto-create person: spine-system account or person type not found in DB')
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// First person ever gets system_admin role
|
|
590
|
-
const { count } = await adminDb
|
|
591
|
-
.from('people')
|
|
592
|
-
.select('id', { count: 'exact', head: true })
|
|
593
|
-
|
|
594
|
-
let roleId: string | null = null
|
|
595
|
-
if ((count ?? 0) === 0) {
|
|
596
|
-
const { data: adminRole } = await adminDb
|
|
597
|
-
.from('roles')
|
|
598
|
-
.select('id')
|
|
599
|
-
.eq('slug', 'system_admin')
|
|
600
|
-
.single()
|
|
601
|
-
roleId = adminRole?.id ?? null
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const { data: newPerson, error: createError } = await adminDb
|
|
605
|
-
.from('people')
|
|
606
|
-
.upsert({
|
|
607
|
-
id: authUserId,
|
|
608
|
-
auth_uid: authUserId,
|
|
609
|
-
email: email ?? '',
|
|
610
|
-
full_name: email ? email.split('@')[0] : 'Unknown',
|
|
611
|
-
account_id: account.id,
|
|
612
|
-
type_id: type.id,
|
|
613
|
-
role_id: roleId,
|
|
614
|
-
is_active: true
|
|
615
|
-
}, { onConflict: 'id' })
|
|
616
|
-
.select('id')
|
|
617
|
-
.single()
|
|
618
|
-
|
|
619
|
-
if (createError || !newPerson) {
|
|
620
|
-
throw new Error('Failed to auto-create person for auth user ' + authUserId + ': ' + createError?.message)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return newPerson.id
|
|
564
|
+
// Not found - return the auth ID as fallback
|
|
565
|
+
return authUserId
|
|
624
566
|
}
|
|
625
567
|
|
|
626
568
|
// ─── HELPER FUNCTIONS ────────────────────────────────────────────────────────
|
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { adminDb } from './db'
|
|
6
6
|
|
|
7
|
-
interface ResolvedIds {
|
|
8
|
-
types: Record<string, string>
|
|
9
|
-
linkTypes: Record<string, string>
|
|
10
|
-
accounts: Record<string, string>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
// Resolve type IDs by (kind, slug) pairs
|
|
14
8
|
export async function resolveTypeIds(
|
|
15
9
|
pairs: Array<{ kind: string; slug: string }>
|
|
@@ -67,6 +61,12 @@ export async function resolveLinkTypeIds(slugs: string[]): Promise<Record<string
|
|
|
67
61
|
return result
|
|
68
62
|
}
|
|
69
63
|
|
|
64
|
+
// Resolve a single link type ID — throws if not found
|
|
65
|
+
export async function resolveLinkTypeId(slug: string): Promise<string> {
|
|
66
|
+
const ids = await resolveLinkTypeIds([slug])
|
|
67
|
+
return ids[slug]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
70
|
// Resolve account ID by slug
|
|
71
71
|
export async function resolveAccountId(slug: string): Promise<string> {
|
|
72
72
|
const { data, error } = await adminDb
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - Multi-tenancy: different tenants can have different handlers
|
|
19
19
|
*
|
|
20
20
|
* @seeAlso integration-routes.ts (uses this registry)
|
|
21
|
-
* @seeAlso
|
|
21
|
+
* @seeAlso custom_webhook-handlers.ts (legacy static registry - being phased out)
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { adminDb } from './db'
|
|
@@ -62,6 +62,48 @@ export async function lookupHandler(handlerName: string): Promise<{ functionName
|
|
|
62
62
|
return { functionName: data.function_name }
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Loads a webhook handler function dynamically.
|
|
67
|
+
* Uses dynamic import to avoid static dependencies.
|
|
68
|
+
*
|
|
69
|
+
* @param functionName - The Netlify function name (e.g., "custom_cortex-handler")
|
|
70
|
+
* @returns The handler function or null if not found
|
|
71
|
+
*/
|
|
72
|
+
export async function loadHandler(functionName: string): Promise<Function | null> {
|
|
73
|
+
try {
|
|
74
|
+
// Dynamic import of the handler function
|
|
75
|
+
// Functions are in the same directory after assembly
|
|
76
|
+
const module = await import(`../${functionName}`)
|
|
77
|
+
|
|
78
|
+
// Handler should be exported as 'handler' or default
|
|
79
|
+
const handler = module.handler || module.default
|
|
80
|
+
|
|
81
|
+
if (typeof handler !== 'function') {
|
|
82
|
+
console.error(`[webhook-registry] Export 'handler' not found in ${functionName}`)
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return handler
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`[webhook-registry] Failed to load ${functionName}:`, err)
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves a webhook handler by name.
|
|
95
|
+
* Combines lookup + load for convenience.
|
|
96
|
+
*
|
|
97
|
+
* @param handlerName - The handler identifier
|
|
98
|
+
* @returns The handler function or null
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveHandler(handlerName: string): Promise<Function | null> {
|
|
101
|
+
const metadata = await lookupHandler(handlerName)
|
|
102
|
+
if (!metadata) return null
|
|
103
|
+
|
|
104
|
+
return loadHandler(metadata.functionName)
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
/**
|
|
66
108
|
* Clears the handler cache. Useful for testing or after deployments.
|
|
67
109
|
*/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module admin-funnel
|
|
3
|
+
* @audience system-admin
|
|
4
|
+
* @layer api-handler
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Admin API for funnel metrics and analytics
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHandler } from './_shared/middleware'
|
|
11
|
+
import { adminDb } from './_shared/db'
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// FUNNEL METRICS
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
export const metrics = createHandler(async (ctx, body) => {
|
|
18
|
+
// Get type IDs
|
|
19
|
+
const { data: signalType } = await adminDb
|
|
20
|
+
.from('item_type_registry')
|
|
21
|
+
.select('id')
|
|
22
|
+
.eq('slug', 'funnel_signal')
|
|
23
|
+
.single()
|
|
24
|
+
|
|
25
|
+
const { data: sessionType } = await adminDb
|
|
26
|
+
.from('item_type_registry')
|
|
27
|
+
.select('id')
|
|
28
|
+
.eq('slug', 'anonymous_session')
|
|
29
|
+
.single()
|
|
30
|
+
|
|
31
|
+
const { data: instanceType } = await adminDb
|
|
32
|
+
.from('item_type_registry')
|
|
33
|
+
.select('id')
|
|
34
|
+
.eq('slug', 'spine_instance')
|
|
35
|
+
.single()
|
|
36
|
+
|
|
37
|
+
if (!signalType || !sessionType || !instanceType) {
|
|
38
|
+
throw new Error('Required types not found')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Count by stage
|
|
42
|
+
const { data: stageData } = await adminDb
|
|
43
|
+
.from('items')
|
|
44
|
+
.select('data->>stage, account_id')
|
|
45
|
+
.eq('type_id', signalType.id)
|
|
46
|
+
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())
|
|
47
|
+
|
|
48
|
+
const stageCounts = stageData?.reduce((acc, item) => {
|
|
49
|
+
const stage = item.data?.stage || 'unknown'
|
|
50
|
+
if (item.account_id) {
|
|
51
|
+
acc[stage] = (acc[stage] || 0) + 1
|
|
52
|
+
}
|
|
53
|
+
return acc
|
|
54
|
+
}, {} as Record<string, number>)
|
|
55
|
+
|
|
56
|
+
const totalStageSignals = Object.values(stageCounts).reduce((sum, count) => sum + count, 0)
|
|
57
|
+
|
|
58
|
+
// Anonymous visitors (sessions without account)
|
|
59
|
+
const { count: anonymousSessions } = await adminDb
|
|
60
|
+
.from('items')
|
|
61
|
+
.select('*', { count: 'exact', head: true })
|
|
62
|
+
.eq('type_id', sessionType.id)
|
|
63
|
+
.is('data->lifecycle->>stitched_to_account_id', null)
|
|
64
|
+
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())
|
|
65
|
+
|
|
66
|
+
// Identified users (unique accounts with signals)
|
|
67
|
+
const { data: accountData } = await adminDb
|
|
68
|
+
.from('items')
|
|
69
|
+
.select('account_id')
|
|
70
|
+
.eq('type_id', signalType.id)
|
|
71
|
+
.gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())
|
|
72
|
+
.not('account_id', 'is', null)
|
|
73
|
+
|
|
74
|
+
const uniqueAccounts = new Set(accountData?.map(item => item.account_id)).size
|
|
75
|
+
|
|
76
|
+
// Installed instances
|
|
77
|
+
const { count: installedInstances } = await adminDb
|
|
78
|
+
.from('items')
|
|
79
|
+
.select('*', { count: 'exact', head: true })
|
|
80
|
+
.eq('type_id', instanceType.id)
|
|
81
|
+
.not('data->>claimed_by_person_id', 'is', null)
|
|
82
|
+
|
|
83
|
+
// Stage distribution with percentages
|
|
84
|
+
const stageDistribution = Object.entries(stageCounts).map(([stage, count]) => ({
|
|
85
|
+
stage,
|
|
86
|
+
count,
|
|
87
|
+
percentage: totalStageSignals > 0 ? (count / totalStageSignals) * 100 : 0
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
// Conversion rates (simplified)
|
|
91
|
+
const anonymousToIdentified = anonymousSessions > 0 ? (uniqueAccounts / anonymousSessions) * 100 : 0
|
|
92
|
+
const identifiedToInstalled = uniqueAccounts > 0 ? (installedInstances / uniqueAccounts) * 100 : 0
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
anonymous_visitors: anonymousSessions || 0,
|
|
96
|
+
identified_users: uniqueAccounts,
|
|
97
|
+
installed_instances: installedInstances || 0,
|
|
98
|
+
conversion_rates: {
|
|
99
|
+
anonymous_to_identified: anonymousToIdentified,
|
|
100
|
+
identified_to_installed: identifiedToInstalled
|
|
101
|
+
},
|
|
102
|
+
stage_distribution: stageDistribution
|
|
103
|
+
}
|
|
104
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module admin-install-funnel
|
|
3
|
+
* @audience system-admin
|
|
4
|
+
* @layer api-handler
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Admin API for install funnel metrics and analytics
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHandler } from './_shared/middleware'
|
|
11
|
+
import { adminDb } from './_shared/db'
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// INSTALL FUNNEL METRICS
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
export const metrics = createHandler(async (ctx, body) => {
|
|
18
|
+
// Get type IDs
|
|
19
|
+
const { data: signalType } = await adminDb
|
|
20
|
+
.from('item_type_registry')
|
|
21
|
+
.select('id')
|
|
22
|
+
.eq('slug', 'funnel_signal')
|
|
23
|
+
.single()
|
|
24
|
+
|
|
25
|
+
if (!signalType) {
|
|
26
|
+
throw new Error('funnel_signal type not found')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Total downloads (signals with source='inst' and action_type='download')
|
|
30
|
+
const { count: totalDownloads } = await adminDb
|
|
31
|
+
.from('items')
|
|
32
|
+
.select('*', { count: 'exact', head: true })
|
|
33
|
+
.eq('type_id', signalType.id)
|
|
34
|
+
.eq('data->>source', 'inst')
|
|
35
|
+
.eq('data->>action_type', 'download')
|
|
36
|
+
|
|
37
|
+
// Unique installers (unique serial numbers from install signals)
|
|
38
|
+
const { data: installerData } = await adminDb
|
|
39
|
+
.from('items')
|
|
40
|
+
.select('data->>serial')
|
|
41
|
+
.eq('type_id', signalType.id)
|
|
42
|
+
.eq('data->>source', 'inst')
|
|
43
|
+
.not('data->>serial', 'is', null)
|
|
44
|
+
|
|
45
|
+
const uniqueInstallers = new Set(installerData?.map(item => item.data?.serial)).size
|
|
46
|
+
|
|
47
|
+
// Active instances (install signals with serial in last 7 days)
|
|
48
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
49
|
+
const { count: activeInstances } = await adminDb
|
|
50
|
+
.from('items')
|
|
51
|
+
.select('*', { count: 'exact', head: true })
|
|
52
|
+
.eq('type_id', signalType.id)
|
|
53
|
+
.eq('data->>source', 'inst')
|
|
54
|
+
.not('data->>serial', 'is', null)
|
|
55
|
+
.gte('created_at', sevenDaysAgo)
|
|
56
|
+
|
|
57
|
+
// Conversion funnel counts
|
|
58
|
+
const { count: installations } = await adminDb
|
|
59
|
+
.from('items')
|
|
60
|
+
.select('*', { count: 'exact', head: true })
|
|
61
|
+
.eq('type_id', signalType.id)
|
|
62
|
+
.eq('data->>source', 'inst')
|
|
63
|
+
.eq('data->>action_type', 'install')
|
|
64
|
+
|
|
65
|
+
const { count: firstUse } = await adminDb
|
|
66
|
+
.from('items')
|
|
67
|
+
.select('*', { count: 'exact', head: true })
|
|
68
|
+
.eq('type_id', signalType.id)
|
|
69
|
+
.eq('data->>source', 'inst')
|
|
70
|
+
.eq('data->>action_type', 'first_command')
|
|
71
|
+
|
|
72
|
+
const { count: claimed } = await adminDb
|
|
73
|
+
.from('items')
|
|
74
|
+
.select('*', { count: 'exact', head: true })
|
|
75
|
+
.eq('type_id', signalType.id)
|
|
76
|
+
.eq('data->>source', 'inst')
|
|
77
|
+
.not('data->>claimed_by_person_id', 'is', null)
|
|
78
|
+
|
|
79
|
+
// Recent installs with account info
|
|
80
|
+
const { data: recentInstalls } = await adminDb
|
|
81
|
+
.from('items')
|
|
82
|
+
.select(`
|
|
83
|
+
id,
|
|
84
|
+
data->>serial,
|
|
85
|
+
data->>version,
|
|
86
|
+
data->>environment,
|
|
87
|
+
data->>first_seen_at,
|
|
88
|
+
data->>last_signal_at,
|
|
89
|
+
data->>claimed_by_person_id,
|
|
90
|
+
data->>claimed_at,
|
|
91
|
+
account_id,
|
|
92
|
+
people(email),
|
|
93
|
+
accounts(name)
|
|
94
|
+
`)
|
|
95
|
+
.eq('type_id', signalType.id)
|
|
96
|
+
.eq('data->>source', 'inst')
|
|
97
|
+
.not('data->>serial', 'is', null)
|
|
98
|
+
.order('created_at', { ascending: false })
|
|
99
|
+
.limit(20)
|
|
100
|
+
|
|
101
|
+
const formattedInstalls = recentInstalls?.map(instance => ({
|
|
102
|
+
id: instance.id,
|
|
103
|
+
serial: instance.data?.serial || 'unknown',
|
|
104
|
+
app_version: instance.data?.version || 'unknown',
|
|
105
|
+
environment: instance.data?.environment || 'unknown',
|
|
106
|
+
first_seen_at: instance.data?.first_seen_at || instance.created_at,
|
|
107
|
+
last_signal_at: instance.data?.last_signal_at || instance.created_at,
|
|
108
|
+
claimed_by: instance.data?.claimed_by_person_id,
|
|
109
|
+
claimed_at: instance.data?.claimed_at,
|
|
110
|
+
account_name: instance.accounts?.name,
|
|
111
|
+
person_email: instance.people?.email
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
// Version distribution
|
|
115
|
+
const { data: versionData } = await adminDb
|
|
116
|
+
.from('items')
|
|
117
|
+
.select('data->>version')
|
|
118
|
+
.eq('type_id', instanceType.id)
|
|
119
|
+
|
|
120
|
+
const versionCounts = versionData?.reduce((acc, item) => {
|
|
121
|
+
const version = item.data?.version || 'unknown'
|
|
122
|
+
acc[version] = (acc[version] || 0) + 1
|
|
123
|
+
return acc
|
|
124
|
+
}, {} as Record<string, number>)
|
|
125
|
+
|
|
126
|
+
const totalVersions = Object.values(versionCounts).reduce((sum, count) => sum + count, 0)
|
|
127
|
+
const topVersions = Object.entries(versionCounts)
|
|
128
|
+
.sort(([, a], [, b]) => b - a)
|
|
129
|
+
.slice(0, 10)
|
|
130
|
+
.map(([version, count]) => ({
|
|
131
|
+
version,
|
|
132
|
+
count,
|
|
133
|
+
percentage: totalVersions > 0 ? (count / totalVersions) * 100 : 0
|
|
134
|
+
}))
|
|
135
|
+
|
|
136
|
+
// Install trends (last 30 days)
|
|
137
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
|
138
|
+
const installTrends = []
|
|
139
|
+
|
|
140
|
+
for (let i = 29; i >= 0; i--) {
|
|
141
|
+
const date = new Date(thirtyDaysAgo)
|
|
142
|
+
date.setDate(date.getDate() + i)
|
|
143
|
+
const dateStr = date.toISOString().split('T')[0]
|
|
144
|
+
const nextDateStr = new Date(date.getTime() + 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
|
145
|
+
|
|
146
|
+
const { count: dailyDownloads } = await adminDb
|
|
147
|
+
.from('items')
|
|
148
|
+
.select('*', { count: 'exact', head: true })
|
|
149
|
+
.eq('type_id', signalType.id)
|
|
150
|
+
.eq('data->>source', 'inst')
|
|
151
|
+
.eq('data->>action_type', 'download')
|
|
152
|
+
.gte('created_at', dateStr)
|
|
153
|
+
.lt('created_at', nextDateStr)
|
|
154
|
+
|
|
155
|
+
const { count: dailyInstalls } = await adminDb
|
|
156
|
+
.from('items')
|
|
157
|
+
.select('*', { count: 'exact', head: true })
|
|
158
|
+
.eq('type_id', signalType.id)
|
|
159
|
+
.eq('data->>source', 'inst')
|
|
160
|
+
.eq('data->>action_type', 'install')
|
|
161
|
+
.gte('created_at', dateStr)
|
|
162
|
+
.lt('created_at', nextDateStr)
|
|
163
|
+
|
|
164
|
+
installTrends.push({
|
|
165
|
+
date: dateStr,
|
|
166
|
+
downloads: dailyDownloads || 0,
|
|
167
|
+
installations: dailyInstalls || 0
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
total_downloads: totalDownloads || 0,
|
|
173
|
+
unique_installers: uniqueInstallers,
|
|
174
|
+
active_instances: activeInstances || 0,
|
|
175
|
+
conversion_funnel: {
|
|
176
|
+
downloads: totalDownloads || 0,
|
|
177
|
+
installations: installations || 0,
|
|
178
|
+
first_use: firstUse || 0,
|
|
179
|
+
claimed: claimed || 0
|
|
180
|
+
},
|
|
181
|
+
install_trends: installTrends,
|
|
182
|
+
recent_installs: formattedInstalls || [],
|
|
183
|
+
top_versions: topVersions
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// ============================================
|
|
188
|
+
// INSTALL FUNNEL EXPORT
|
|
189
|
+
// ============================================
|
|
190
|
+
|
|
191
|
+
export const exportData = createHandler(async (ctx, body) => {
|
|
192
|
+
// Get type IDs
|
|
193
|
+
const { data: signalType } = await adminDb
|
|
194
|
+
.from('item_type_registry')
|
|
195
|
+
.select('id')
|
|
196
|
+
.eq('slug', 'funnel_signal')
|
|
197
|
+
.single()
|
|
198
|
+
|
|
199
|
+
if (!signalType) {
|
|
200
|
+
throw new Error('funnel_signal type not found')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Export all instances with full details
|
|
204
|
+
const { data: instances } = await adminDb
|
|
205
|
+
.from('items')
|
|
206
|
+
.select(`
|
|
207
|
+
id,
|
|
208
|
+
created_at,
|
|
209
|
+
updated_at,
|
|
210
|
+
data,
|
|
211
|
+
account_id,
|
|
212
|
+
people(email),
|
|
213
|
+
accounts(name)
|
|
214
|
+
`)
|
|
215
|
+
.eq('type_id', signalType.id)
|
|
216
|
+
.eq('data->>source', 'inst')
|
|
217
|
+
.not('data->>serial', 'is', null)
|
|
218
|
+
.order('created_at', { ascending: false })
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
instances: instances || [],
|
|
222
|
+
exported_at: new Date().toISOString(),
|
|
223
|
+
total_count: instances?.length || 0
|
|
224
|
+
}
|
|
225
|
+
})
|