spine-framework 0.1.61 → 0.2.0

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