spine-framework 0.3.16 → 0.3.18
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/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 +3 -7
- 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 +78 -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 -68
- 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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module admin-signals
|
|
3
|
+
* @audience system-admin
|
|
4
|
+
* @layer api-handler
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Admin API for signal analytics and audit functionality
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHandler } from './_shared/middleware'
|
|
11
|
+
import { adminDb } from './_shared/db'
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// SIGNAL METRICS
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
export const metrics = createHandler(async (ctx, body) => {
|
|
18
|
+
const now = new Date()
|
|
19
|
+
const today = now.toISOString().split('T')[0]
|
|
20
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
21
|
+
|
|
22
|
+
// Get funnel_signal type ID
|
|
23
|
+
const { data: signalType } = await adminDb
|
|
24
|
+
.from('item_type_registry')
|
|
25
|
+
.select('id')
|
|
26
|
+
.eq('slug', 'funnel_signal')
|
|
27
|
+
.single()
|
|
28
|
+
|
|
29
|
+
if (!signalType) {
|
|
30
|
+
throw new Error('funnel_signal type not found')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Total signals
|
|
34
|
+
const { count: totalSignals } = await adminDb
|
|
35
|
+
.from('items')
|
|
36
|
+
.select('*', { count: 'exact', head: true })
|
|
37
|
+
.eq('type_id', signalType.id)
|
|
38
|
+
|
|
39
|
+
// Signals today
|
|
40
|
+
const { count: signalsToday } = await adminDb
|
|
41
|
+
.from('items')
|
|
42
|
+
.select('*', { count: 'exact', head: true })
|
|
43
|
+
.eq('type_id', signalType.id)
|
|
44
|
+
.gte('created_at', today)
|
|
45
|
+
|
|
46
|
+
// Signals this week
|
|
47
|
+
const { count: signalsThisWeek } = await adminDb
|
|
48
|
+
.from('items')
|
|
49
|
+
.select('*', { count: 'exact', head: true })
|
|
50
|
+
.eq('type_id', signalType.id)
|
|
51
|
+
.gte('created_at', weekAgo)
|
|
52
|
+
|
|
53
|
+
// Top sources
|
|
54
|
+
const { data: sourceData } = await adminDb
|
|
55
|
+
.from('items')
|
|
56
|
+
.select('data->>source')
|
|
57
|
+
.eq('type_id', signalType.id)
|
|
58
|
+
.gte('created_at', weekAgo)
|
|
59
|
+
|
|
60
|
+
const sourceCounts = sourceData?.reduce((acc, item) => {
|
|
61
|
+
const source = item.data?.source || 'unknown'
|
|
62
|
+
acc[source] = (acc[source] || 0) + 1
|
|
63
|
+
return acc
|
|
64
|
+
}, {} as Record<string, number>)
|
|
65
|
+
|
|
66
|
+
const topSources = Object.entries(sourceCounts)
|
|
67
|
+
.sort(([, a], [, b]) => b - a)
|
|
68
|
+
.slice(0, 10)
|
|
69
|
+
.map(([source, count]) => ({ source, count }))
|
|
70
|
+
|
|
71
|
+
// Recent signals with account info
|
|
72
|
+
const { data: recentSignals } = await adminDb
|
|
73
|
+
.from('items')
|
|
74
|
+
.select(`
|
|
75
|
+
id,
|
|
76
|
+
data->>action_type,
|
|
77
|
+
data->>source,
|
|
78
|
+
data->>stage,
|
|
79
|
+
created_at,
|
|
80
|
+
data->>rating,
|
|
81
|
+
account_id,
|
|
82
|
+
accounts(name)
|
|
83
|
+
`)
|
|
84
|
+
.eq('type_id', signalType.id)
|
|
85
|
+
.order('created_at', { ascending: false })
|
|
86
|
+
.limit(20)
|
|
87
|
+
|
|
88
|
+
const formattedSignals = recentSignals?.map(signal => ({
|
|
89
|
+
id: signal.id,
|
|
90
|
+
action_type: signal.data?.action_type || 'unknown',
|
|
91
|
+
source: signal.data?.source || 'unknown',
|
|
92
|
+
stage: signal.data?.stage || 'unknown',
|
|
93
|
+
created_at: signal.created_at,
|
|
94
|
+
account_name: signal.accounts?.name,
|
|
95
|
+
rating: parseInt(signal.data?.rating || '1')
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
// Calculate conversion rate (simplified - signals with rating >= 4)
|
|
99
|
+
const { count: highValueSignals } = await adminDb
|
|
100
|
+
.from('items')
|
|
101
|
+
.select('*', { count: 'exact', head: true })
|
|
102
|
+
.eq('type_id', signalType.id)
|
|
103
|
+
.gte('data->>rating', '4')
|
|
104
|
+
.gte('created_at', weekAgo)
|
|
105
|
+
|
|
106
|
+
const conversionRate = signalsThisWeek > 0 ? (highValueSignals / signalsThisWeek) * 100 : 0
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
total_signals: totalSignals || 0,
|
|
110
|
+
signals_today: signalsToday || 0,
|
|
111
|
+
signals_this_week: signalsThisWeek || 0,
|
|
112
|
+
conversion_rate: conversionRate,
|
|
113
|
+
top_sources: topSources,
|
|
114
|
+
recent_signals: formattedSignals || []
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// SIGNAL AUDIT
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
export const audit = createHandler(async (ctx, body) => {
|
|
123
|
+
const {
|
|
124
|
+
page = '1',
|
|
125
|
+
limit = '50',
|
|
126
|
+
date_from,
|
|
127
|
+
date_to,
|
|
128
|
+
source,
|
|
129
|
+
stage,
|
|
130
|
+
action_type,
|
|
131
|
+
min_rating,
|
|
132
|
+
account_id,
|
|
133
|
+
search,
|
|
134
|
+
export: exportData
|
|
135
|
+
} = ctx.query || {}
|
|
136
|
+
|
|
137
|
+
// Get funnel_signal type ID
|
|
138
|
+
const { data: signalType } = await adminDb
|
|
139
|
+
.from('item_type_registry')
|
|
140
|
+
.select('id')
|
|
141
|
+
.eq('slug', 'funnel_signal')
|
|
142
|
+
.single()
|
|
143
|
+
|
|
144
|
+
if (!signalType) {
|
|
145
|
+
throw new Error('funnel_signal type not found')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build query
|
|
149
|
+
let query = adminDb
|
|
150
|
+
.from('items')
|
|
151
|
+
.select(`
|
|
152
|
+
id,
|
|
153
|
+
data->>action_type,
|
|
154
|
+
data->>action_value,
|
|
155
|
+
data->>stage,
|
|
156
|
+
data->>source,
|
|
157
|
+
created_at,
|
|
158
|
+
data->>rating,
|
|
159
|
+
data->>raw_score,
|
|
160
|
+
data->identity->>anonymous_id,
|
|
161
|
+
data->identity->>session_id,
|
|
162
|
+
data->attribution->>first_touch_referrer_domain,
|
|
163
|
+
data->attribution->>utm_source,
|
|
164
|
+
data->attribution->>utm_medium,
|
|
165
|
+
data->attribution->>utm_campaign,
|
|
166
|
+
account_id,
|
|
167
|
+
people(email),
|
|
168
|
+
accounts(name)
|
|
169
|
+
`, { count: 'exact' })
|
|
170
|
+
.eq('type_id', signalType.id)
|
|
171
|
+
|
|
172
|
+
// Apply filters
|
|
173
|
+
if (date_from) {
|
|
174
|
+
query = query.gte('created_at', date_from)
|
|
175
|
+
}
|
|
176
|
+
if (date_to) {
|
|
177
|
+
query = query.lte('created_at', date_to)
|
|
178
|
+
}
|
|
179
|
+
if (source) {
|
|
180
|
+
query = query.eq('data->>source', source)
|
|
181
|
+
}
|
|
182
|
+
if (stage) {
|
|
183
|
+
query = query.eq('data->>stage', stage)
|
|
184
|
+
}
|
|
185
|
+
if (action_type) {
|
|
186
|
+
query = query.eq('data->>action_type', action_type)
|
|
187
|
+
}
|
|
188
|
+
if (min_rating) {
|
|
189
|
+
query = query.gte('data->>rating', min_rating)
|
|
190
|
+
}
|
|
191
|
+
if (account_id) {
|
|
192
|
+
query = query.eq('account_id', account_id)
|
|
193
|
+
}
|
|
194
|
+
if (search) {
|
|
195
|
+
query = query.or(`accounts.name.ilike.%${search}%,people.email.ilike.%${search}%,data->attribution->>first_touch_referrer_domain.ilike.%${search}%`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Order and paginate
|
|
199
|
+
const offset = (parseInt(page) - 1) * parseInt(limit)
|
|
200
|
+
query = query.order('created_at', { ascending: false })
|
|
201
|
+
|
|
202
|
+
if (!exportData) {
|
|
203
|
+
query = query.range(offset, offset + parseInt(limit) - 1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { data, error, count } = await query
|
|
207
|
+
|
|
208
|
+
if (error) {
|
|
209
|
+
throw new Error(`Failed to fetch signals: ${error.message}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const formattedSignals = data?.map(signal => ({
|
|
213
|
+
id: signal.id,
|
|
214
|
+
action_type: signal.data?.action_type || 'unknown',
|
|
215
|
+
action_value: parseInt(signal.data?.action_value || '1'),
|
|
216
|
+
stage: signal.data?.stage || 'unknown',
|
|
217
|
+
source: signal.data?.source || 'unknown',
|
|
218
|
+
created_at: signal.created_at,
|
|
219
|
+
account_name: signal.accounts?.name,
|
|
220
|
+
person_email: signal.people?.email,
|
|
221
|
+
anonymous_id: signal.data?.identity?.anonymous_id,
|
|
222
|
+
session_id: signal.data?.identity?.session_id,
|
|
223
|
+
rating: parseInt(signal.data?.rating || '1'),
|
|
224
|
+
raw_score: parseFloat(signal.data?.raw_score || '0'),
|
|
225
|
+
referrer_domain: signal.data?.attribution?.first_touch_referrer_domain,
|
|
226
|
+
utm_source: signal.data?.attribution?.utm_source,
|
|
227
|
+
utm_medium: signal.data?.attribution?.utm_medium,
|
|
228
|
+
utm_campaign: signal.data?.attribution?.utm_campaign
|
|
229
|
+
}))
|
|
230
|
+
|
|
231
|
+
if (exportData) {
|
|
232
|
+
return formattedSignals
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
signals: formattedSignals,
|
|
237
|
+
total: count || 0,
|
|
238
|
+
page: parseInt(page),
|
|
239
|
+
limit: parseInt(limit)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
import { adminDb } from './_shared/db'
|
|
39
39
|
import { resolvePrincipal } from './_shared/principal'
|
|
40
|
-
import { listRegisteredHandlers } from './_shared/webhook-registry'
|
|
41
|
-
import { HANDLER_REGISTRY } from './handler-registry'
|
|
40
|
+
import { resolveHandler, listRegisteredHandlers } from './_shared/webhook-registry'
|
|
42
41
|
|
|
43
42
|
// ─── TYPES ────────────────────────────────────────────────────────────────
|
|
44
43
|
|
|
@@ -129,7 +128,7 @@ function validateSchema(data: any, schema: any, requestId: string): ValidationRe
|
|
|
129
128
|
// Validate data type
|
|
130
129
|
if (config.type) {
|
|
131
130
|
const typeValid = validateType(value, config.type, fieldName)
|
|
132
|
-
if (!typeValid.valid
|
|
131
|
+
if (!typeValid.valid) {
|
|
133
132
|
errors.push(typeValid.error)
|
|
134
133
|
}
|
|
135
134
|
}
|
|
@@ -256,10 +255,7 @@ function sanitizeData(data: any, rules: string = 'strict'): any {
|
|
|
256
255
|
* @param handlerName - The handler key from integration config (e.g. "webhook-handler")
|
|
257
256
|
*/
|
|
258
257
|
async function loadCustomScript(handlerName: string): Promise<Function | null> {
|
|
259
|
-
|
|
260
|
-
if (!mod) return null
|
|
261
|
-
const fn = mod.handler || mod.default
|
|
262
|
-
return typeof fn === 'function' ? fn : null
|
|
258
|
+
return resolveHandler(handlerName)
|
|
263
259
|
}
|
|
264
260
|
|
|
265
261
|
// ─── DIAGNOSTIC HANDLER (temporary — full trace in response) ──────────────
|