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.
Files changed (270) hide show
  1. package/.framework/cli/bin.mjs +16 -0
  2. package/.framework/cli/commands/create-app.ts +1 -1
  3. package/.framework/cli/commands/dev.ts +215 -33
  4. package/.framework/cli/index.ts +37 -75
  5. package/.framework/functions/_shared/app-manifest.ts +8 -15
  6. package/.framework/functions/_shared/db.ts +11 -15
  7. package/.framework/functions/_shared/index.ts +7 -16
  8. package/.framework/functions/_shared/principal.ts +4 -62
  9. package/.framework/functions/_shared/resolve-ids.ts +6 -6
  10. package/.framework/functions/_shared/webhook-registry.ts +43 -1
  11. package/.framework/functions/admin-funnel.ts +104 -0
  12. package/.framework/functions/admin-install-funnel.ts +225 -0
  13. package/.framework/functions/admin-signals.ts +241 -0
  14. package/.framework/functions/integration-routes.ts +4 -8
  15. package/.framework/migrations/001_schema.sql +406 -0
  16. package/.framework/migrations/002_rls_and_functions.sql +710 -0
  17. package/.framework/migrations/003_system_data.sql +520 -0
  18. package/.framework/migrations/004_enhancements.sql +43 -0
  19. package/.framework/public/_redirects +3 -0
  20. package/.framework/public/spine-logo.jpg +0 -0
  21. package/.framework/public/spine-logo.svg +3 -0
  22. package/{scripts → .framework/scripts}/assemble-frontend.sh +8 -5
  23. package/{scripts → .framework/scripts}/assemble-functions.sh +4 -47
  24. package/{scripts → .framework/scripts}/assemble.sh +2 -8
  25. package/{scripts → .framework/scripts}/boundary-check.sh +5 -9
  26. package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
  27. package/.framework/seeds/link_types/account_cli_instances.json +9 -0
  28. package/.framework/seeds/types/funnel_signal_use.json +98 -0
  29. package/.framework/seeds/types/spine_instance.json +109 -0
  30. package/.framework/src/App.tsx +11 -21
  31. package/.framework/src/components/CustomAppLoader.tsx +2 -19
  32. package/.framework/src/components/ui/accordion.tsx +1 -1
  33. package/.framework/src/components/ui/alert-dialog.tsx +1 -1
  34. package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
  35. package/.framework/src/components/ui/avatar.tsx +1 -1
  36. package/.framework/src/components/ui/badge.tsx +2 -2
  37. package/.framework/src/components/ui/breadcrumb.tsx +2 -2
  38. package/.framework/src/components/ui/button-group.tsx +2 -2
  39. package/.framework/src/components/ui/button.tsx +2 -2
  40. package/.framework/src/components/ui/chart.tsx +6 -1
  41. package/.framework/src/components/ui/checkbox.tsx +1 -1
  42. package/.framework/src/components/ui/collapsible.tsx +1 -1
  43. package/.framework/src/components/ui/context-menu.tsx +1 -1
  44. package/.framework/src/components/ui/dialog.tsx +1 -1
  45. package/.framework/src/components/ui/direction.tsx +17 -1
  46. package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
  47. package/.framework/src/components/ui/hover-card.tsx +1 -1
  48. package/.framework/src/components/ui/item.tsx +2 -2
  49. package/.framework/src/components/ui/label.tsx +1 -1
  50. package/.framework/src/components/ui/menubar.tsx +1 -1
  51. package/.framework/src/components/ui/navigation-menu.tsx +1 -1
  52. package/.framework/src/components/ui/popover.tsx +1 -1
  53. package/.framework/src/components/ui/progress.tsx +1 -1
  54. package/.framework/src/components/ui/radio-group.tsx +1 -1
  55. package/.framework/src/components/ui/scroll-area.tsx +1 -1
  56. package/.framework/src/components/ui/select.tsx +1 -1
  57. package/.framework/src/components/ui/separator.tsx +1 -1
  58. package/.framework/src/components/ui/sheet.tsx +1 -1
  59. package/.framework/src/components/ui/sidebar.tsx +6 -6
  60. package/.framework/src/components/ui/slider.tsx +1 -1
  61. package/.framework/src/components/ui/switch.tsx +1 -1
  62. package/.framework/src/components/ui/tabs.tsx +1 -1
  63. package/.framework/src/components/ui/toggle-group.tsx +1 -1
  64. package/.framework/src/components/ui/toggle.tsx +1 -1
  65. package/.framework/src/components/ui/tooltip.tsx +1 -1
  66. package/.framework/src/pages/auth/LoginPage.tsx +10 -9
  67. package/LICENSE.md +63 -93
  68. package/README.md +254 -129
  69. package/custom/.devin/skills/install-app/SKILL.md +39 -0
  70. package/custom/.devin/workflows/app-install.yaml +83 -0
  71. package/custom/README.md +79 -0
  72. package/docs/APPS.md +232 -0
  73. package/docs/ARCHITECTURE.md +246 -0
  74. package/docs/INSTALL.md +155 -0
  75. package/docs/README.md +170 -0
  76. package/docs/design-schema-spec.md +1837 -0
  77. package/docs/dev-guides/01-create-an-app.md +183 -0
  78. package/docs/dev-guides/02-create-a-function.md +178 -0
  79. package/docs/dev-guides/03-create-a-component.md +178 -0
  80. package/docs/dev-guides/04-webhook-handlers.md +129 -0
  81. package/docs/dev-guides/05-testing.md +168 -0
  82. package/docs/dev-guides/README.md +40 -0
  83. package/docs/npm-packages-plan.md +288 -0
  84. package/docs/spine-core--api-reference.md +822 -0
  85. package/package.json +79 -150
  86. package/.framework/cli/bin.cjs +0 -14
  87. package/.framework/cli/commands/claim.ts +0 -430
  88. package/.framework/cli/commands/init.ts +0 -219
  89. package/.framework/cli/commands/install-app.ts +0 -685
  90. package/.framework/cli/commands/migrate.ts +0 -178
  91. package/.framework/cli/commands/publish.ts +0 -191
  92. package/.framework/cli/commands/status.ts +0 -268
  93. package/.framework/cli/commands/uninstall-app.ts +0 -166
  94. package/.framework/cli/commands/update-app.ts +0 -397
  95. package/.framework/cli/welcome.cjs +0 -45
  96. package/.framework/config/signal-client.ts +0 -147
  97. package/.framework/config/signals.ts +0 -70
  98. package/.framework/docs/STABILITY.md +0 -52
  99. package/.framework/docs/examples/custom-app/README.md +0 -77
  100. package/.framework/docs/examples/custom-function/README.md +0 -27
  101. package/.framework/docs/examples/custom-function/handler.ts +0 -48
  102. package/.framework/docs/examples/custom-webhook/README.md +0 -68
  103. package/.framework/functions/handler-registry.ts +0 -22
  104. package/.framework/functions/signal.ts +0 -168
  105. package/.framework/migrations/000_foundation.sql +0 -1302
  106. package/.framework/migrations/001_seed.sql +0 -163
  107. package/.framework/migrations/002_seed_constraints.sql +0 -19
  108. package/.framework/migrations/003_auth_user_trigger.sql +0 -67
  109. package/.framework/migrations/004_app_version_tracking.sql +0 -8
  110. package/.framework/migrations/005_pipelines_slug_stages.sql +0 -21
  111. package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
  112. package/CHANGELOG.md +0 -71
  113. package/STRUCTURE.md +0 -150
  114. package/bin/spine-framework.cjs +0 -62
  115. package/bin/welcome.cjs +0 -58
  116. package/bin/ws-shim.ts +0 -10
  117. package/config/components.json +0 -25
  118. package/config/deno.lock +0 -108
  119. package/config/package-lock.json +0 -17183
  120. package/config/postcss.config.cjs +0 -10
  121. package/config/tailwind.config.cjs +0 -78
  122. package/config/tsconfig.build.json +0 -32
  123. package/config/tsconfig.cli.json +0 -18
  124. package/config/tsconfig.json +0 -41
  125. package/config/tsconfig.node.json +0 -17
  126. package/config/tsconfig.node.tsbuildinfo +0 -1
  127. package/config/tsconfig.tsbuildinfo +0 -1
  128. package/config/typedoc.json +0 -16
  129. package/config/vite.config.d.ts +0 -2
  130. package/config/vite.config.ts +0 -71
  131. package/dist/cli/commands/agents.d.ts +0 -39
  132. package/dist/cli/commands/agents.d.ts.map +0 -1
  133. package/dist/cli/commands/auth.d.ts +0 -36
  134. package/dist/cli/commands/auth.d.ts.map +0 -1
  135. package/dist/cli/commands/claim.d.ts +0 -38
  136. package/dist/cli/commands/claim.d.ts.map +0 -1
  137. package/dist/cli/commands/create-app.d.ts +0 -23
  138. package/dist/cli/commands/create-app.d.ts.map +0 -1
  139. package/dist/cli/commands/dev.d.ts +0 -24
  140. package/dist/cli/commands/dev.d.ts.map +0 -1
  141. package/dist/cli/commands/doctor.d.ts +0 -42
  142. package/dist/cli/commands/doctor.d.ts.map +0 -1
  143. package/dist/cli/commands/generate.d.ts +0 -36
  144. package/dist/cli/commands/generate.d.ts.map +0 -1
  145. package/dist/cli/commands/init.d.ts +0 -20
  146. package/dist/cli/commands/init.d.ts.map +0 -1
  147. package/dist/cli/commands/install-app.d.ts +0 -30
  148. package/dist/cli/commands/install-app.d.ts.map +0 -1
  149. package/dist/cli/commands/items.d.ts +0 -45
  150. package/dist/cli/commands/items.d.ts.map +0 -1
  151. package/dist/cli/commands/migrate.d.ts +0 -21
  152. package/dist/cli/commands/migrate.d.ts.map +0 -1
  153. package/dist/cli/commands/migrations.d.ts +0 -41
  154. package/dist/cli/commands/migrations.d.ts.map +0 -1
  155. package/dist/cli/commands/pipelines.d.ts +0 -40
  156. package/dist/cli/commands/pipelines.d.ts.map +0 -1
  157. package/dist/cli/commands/publish.d.ts +0 -23
  158. package/dist/cli/commands/publish.d.ts.map +0 -1
  159. package/dist/cli/commands/status.d.ts +0 -23
  160. package/dist/cli/commands/status.d.ts.map +0 -1
  161. package/dist/cli/commands/system.d.ts +0 -29
  162. package/dist/cli/commands/system.d.ts.map +0 -1
  163. package/dist/cli/commands/test.d.ts +0 -46
  164. package/dist/cli/commands/test.d.ts.map +0 -1
  165. package/dist/cli/commands/uninstall-app.d.ts +0 -23
  166. package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
  167. package/dist/cli/commands/update-app.d.ts +0 -24
  168. package/dist/cli/commands/update-app.d.ts.map +0 -1
  169. package/dist/cli/context.d.ts +0 -88
  170. package/dist/cli/context.d.ts.map +0 -1
  171. package/dist/cli/env-loader.d.ts +0 -14
  172. package/dist/cli/env-loader.d.ts.map +0 -1
  173. package/dist/cli/index.d.ts +0 -44
  174. package/dist/cli/index.d.ts.map +0 -1
  175. package/dist/config/signals.d.ts +0 -82
  176. package/dist/config/signals.d.ts.map +0 -1
  177. package/dist/functions/_shared/agent-runner.d.ts +0 -156
  178. package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
  179. package/dist/functions/_shared/app-manifest.d.ts +0 -68
  180. package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
  181. package/dist/functions/_shared/audit.d.ts +0 -91
  182. package/dist/functions/_shared/audit.d.ts.map +0 -1
  183. package/dist/functions/_shared/db.d.ts +0 -125
  184. package/dist/functions/_shared/db.d.ts.map +0 -1
  185. package/dist/functions/_shared/index.d.ts +0 -299
  186. package/dist/functions/_shared/index.d.ts.map +0 -1
  187. package/dist/functions/_shared/middleware.d.ts +0 -315
  188. package/dist/functions/_shared/middleware.d.ts.map +0 -1
  189. package/dist/functions/_shared/permissions.d.ts +0 -626
  190. package/dist/functions/_shared/permissions.d.ts.map +0 -1
  191. package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
  192. package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
  193. package/dist/functions/_shared/principal.d.ts +0 -284
  194. package/dist/functions/_shared/principal.d.ts.map +0 -1
  195. package/dist/functions/_shared/resolve-ids.d.ts +0 -10
  196. package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
  197. package/dist/functions/_shared/schema-utils.d.ts +0 -181
  198. package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
  199. package/dist/functions/_shared/testing.d.ts +0 -172
  200. package/dist/functions/_shared/testing.d.ts.map +0 -1
  201. package/dist/functions/_shared/trigger-engine.d.ts +0 -140
  202. package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
  203. package/dist/functions/_shared/webhook-registration.d.ts +0 -81
  204. package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
  205. package/dist/functions/_shared/webhook-registry.d.ts +0 -41
  206. package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
  207. package/dist/functions/account-nodes.d.ts +0 -48
  208. package/dist/functions/account-nodes.d.ts.map +0 -1
  209. package/dist/functions/admin-data.d.ts +0 -178
  210. package/dist/functions/admin-data.d.ts.map +0 -1
  211. package/dist/functions/ai-agents.d.ts +0 -125
  212. package/dist/functions/ai-agents.d.ts.map +0 -1
  213. package/dist/functions/api-keys.d.ts +0 -140
  214. package/dist/functions/api-keys.d.ts.map +0 -1
  215. package/dist/functions/apps.d.ts +0 -163
  216. package/dist/functions/apps.d.ts.map +0 -1
  217. package/dist/functions/auth.d.ts +0 -74
  218. package/dist/functions/auth.d.ts.map +0 -1
  219. package/dist/functions/debug-auth.d.ts +0 -33
  220. package/dist/functions/debug-auth.d.ts.map +0 -1
  221. package/dist/functions/embeddings.d.ts +0 -205
  222. package/dist/functions/embeddings.d.ts.map +0 -1
  223. package/dist/functions/handler-registry.d.ts +0 -22
  224. package/dist/functions/handler-registry.d.ts.map +0 -1
  225. package/dist/functions/integration-routes.d.ts +0 -45
  226. package/dist/functions/integration-routes.d.ts.map +0 -1
  227. package/dist/functions/integrations.d.ts +0 -124
  228. package/dist/functions/integrations.d.ts.map +0 -1
  229. package/dist/functions/item-progress.d.ts +0 -41
  230. package/dist/functions/item-progress.d.ts.map +0 -1
  231. package/dist/functions/logs.d.ts +0 -162
  232. package/dist/functions/logs.d.ts.map +0 -1
  233. package/dist/functions/observability.d.ts +0 -123
  234. package/dist/functions/observability.d.ts.map +0 -1
  235. package/dist/functions/pipeline-executions.d.ts +0 -190
  236. package/dist/functions/pipeline-executions.d.ts.map +0 -1
  237. package/dist/functions/pipelines.d.ts +0 -171
  238. package/dist/functions/pipelines.d.ts.map +0 -1
  239. package/dist/functions/prompt-configs.d.ts +0 -125
  240. package/dist/functions/prompt-configs.d.ts.map +0 -1
  241. package/dist/functions/roles.d.ts +0 -118
  242. package/dist/functions/roles.d.ts.map +0 -1
  243. package/dist/functions/signal.d.ts +0 -73
  244. package/dist/functions/signal.d.ts.map +0 -1
  245. package/dist/functions/system-cron.d.ts +0 -65
  246. package/dist/functions/system-cron.d.ts.map +0 -1
  247. package/dist/functions/system.d.ts +0 -29
  248. package/dist/functions/system.d.ts.map +0 -1
  249. package/dist/functions/tests.d.ts +0 -28
  250. package/dist/functions/tests.d.ts.map +0 -1
  251. package/dist/functions/timers.d.ts +0 -139
  252. package/dist/functions/timers.d.ts.map +0 -1
  253. package/dist/functions/triggers.d.ts +0 -203
  254. package/dist/functions/triggers.d.ts.map +0 -1
  255. package/dist/functions/types.d.ts +0 -151
  256. package/dist/functions/types.d.ts.map +0 -1
  257. package/dist/src/types/types.d.ts +0 -364
  258. package/dist/src/types/types.d.ts.map +0 -1
  259. package/index.html +0 -13
  260. package/netlify.toml +0 -36
  261. package/package-project.json +0 -71
  262. package/scripts/ingest-chunks.cjs +0 -202
  263. package/scripts/kb-chunk-parser.cjs +0 -312
  264. package/scripts/kb-chunk-parser.ts +0 -330
  265. package/scripts/load-test-app-install.ts +0 -484
  266. package/scripts/netlify-dev-wrapper.sh +0 -22
  267. package/vitest.config.ts +0 -45
  268. /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
  269. /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
  270. /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.full_name || person.email,
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 — auto-create the person (handles cases where auth user pre-existed DB reset
571
- // or trigger fired before seed data was in place)
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 webhook-registration.ts (self-registration helper for custom handlers)
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
+ })