@antelopejs/dms-mailing 0.3.3 → 0.4.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 (705) hide show
  1. package/README.md +232 -22
  2. package/dist/automation/email-event-trigger.js +2 -2
  3. package/dist/automation/email-event-trigger.js.map +1 -1
  4. package/dist/automation/send-template-action.js +2 -2
  5. package/dist/automation/send-template-action.js.map +1 -1
  6. package/dist/config.js +3 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.d.ts +10 -1
  9. package/dist/constants.js +15 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/crons/attention.d.ts +3 -0
  12. package/dist/crons/attention.js +19 -0
  13. package/dist/crons/attention.js.map +1 -0
  14. package/dist/crons/index.d.ts +2 -0
  15. package/dist/crons/index.js +2 -0
  16. package/dist/crons/index.js.map +1 -1
  17. package/dist/crons/jobs.d.ts +12 -0
  18. package/dist/crons/jobs.js +27 -0
  19. package/dist/crons/jobs.js.map +1 -0
  20. package/dist/crons/retention.d.ts +2 -4
  21. package/dist/crons/retention.js +7 -22
  22. package/dist/crons/retention.js.map +1 -1
  23. package/dist/data/displays.d.ts +5 -0
  24. package/dist/data/displays.js +23 -0
  25. package/dist/data/displays.js.map +1 -0
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.js +1 -0
  28. package/dist/data/index.js.map +1 -1
  29. package/dist/data/locale-fallback.d.ts +3 -0
  30. package/dist/data/locale-fallback.js +20 -0
  31. package/dist/data/locale-fallback.js.map +1 -0
  32. package/dist/data/read-only-routes.d.ts +7 -0
  33. package/dist/data/read-only-routes.js +13 -0
  34. package/dist/data/read-only-routes.js.map +1 -0
  35. package/dist/data/sends-table.d.ts +19 -5
  36. package/dist/data/sends-table.js +185 -17
  37. package/dist/data/sends-table.js.map +1 -1
  38. package/dist/data/suppressions-table.d.ts +24 -0
  39. package/dist/data/suppressions-table.js +120 -0
  40. package/dist/data/suppressions-table.js.map +1 -0
  41. package/dist/data/templates-table.d.ts +20 -2
  42. package/dist/data/templates-table.js +188 -31
  43. package/dist/data/templates-table.js.map +1 -1
  44. package/dist/data-types/categories-type.d.ts +11 -13
  45. package/dist/data-types/categories-type.js +3 -3
  46. package/dist/data-types/categories-type.js.map +1 -1
  47. package/dist/data-types/category-type.js +1 -1
  48. package/dist/data-types/category-type.js.map +1 -1
  49. package/dist/db/models/index.d.ts +3 -0
  50. package/dist/db/models/index.js +3 -0
  51. package/dist/db/models/index.js.map +1 -1
  52. package/dist/db/models/sends.model.d.ts +12 -1
  53. package/dist/db/models/sends.model.js +64 -0
  54. package/dist/db/models/sends.model.js.map +1 -1
  55. package/dist/db/models/settings.model.d.ts +0 -1
  56. package/dist/db/models/settings.model.js +0 -7
  57. package/dist/db/models/settings.model.js.map +1 -1
  58. package/dist/db/models/suppressions.model.d.ts +28 -0
  59. package/dist/db/models/suppressions.model.js +46 -0
  60. package/dist/db/models/suppressions.model.js.map +1 -0
  61. package/dist/db/models/template-versions.model.d.ts +24 -0
  62. package/dist/db/models/template-versions.model.js +17 -0
  63. package/dist/db/models/template-versions.model.js.map +1 -0
  64. package/dist/db/models/templates.model.d.ts +2 -0
  65. package/dist/db/models/templates.model.js +3 -0
  66. package/dist/db/models/templates.model.js.map +1 -1
  67. package/dist/db/models/webhook-status.model.d.ts +24 -0
  68. package/dist/db/models/webhook-status.model.js +21 -0
  69. package/dist/db/models/webhook-status.model.js.map +1 -0
  70. package/dist/db/tables/index.d.ts +3 -0
  71. package/dist/db/tables/index.js +3 -0
  72. package/dist/db/tables/index.js.map +1 -1
  73. package/dist/db/tables/send-events.table.d.ts +2 -2
  74. package/dist/db/tables/send-events.table.js.map +1 -1
  75. package/dist/db/tables/sends.table.d.ts +10 -2
  76. package/dist/db/tables/sends.table.js +34 -1
  77. package/dist/db/tables/sends.table.js.map +1 -1
  78. package/dist/db/tables/suppressions.table.d.ts +12 -0
  79. package/dist/db/tables/suppressions.table.js +53 -0
  80. package/dist/db/tables/suppressions.table.js.map +1 -0
  81. package/dist/db/tables/template-versions.table.d.ts +13 -0
  82. package/dist/db/tables/template-versions.table.js +59 -0
  83. package/dist/db/tables/template-versions.table.js.map +1 -0
  84. package/dist/db/tables/templates.table.d.ts +11 -0
  85. package/dist/db/tables/templates.table.js +45 -0
  86. package/dist/db/tables/templates.table.js.map +1 -1
  87. package/dist/db/tables/webhook-status.table.d.ts +14 -0
  88. package/dist/db/tables/webhook-status.table.js +58 -0
  89. package/dist/db/tables/webhook-status.table.js.map +1 -0
  90. package/dist/engine/changes.d.ts +12 -0
  91. package/dist/engine/changes.js +107 -0
  92. package/dist/engine/changes.js.map +1 -0
  93. package/dist/engine/conditions.js +0 -2
  94. package/dist/engine/conditions.js.map +1 -1
  95. package/dist/engine/coverage.d.ts +1 -0
  96. package/dist/engine/coverage.js +6 -2
  97. package/dist/engine/coverage.js.map +1 -1
  98. package/dist/engine/fallback-html.d.ts +1 -1
  99. package/dist/engine/fallback-html.js +51 -34
  100. package/dist/engine/fallback-html.js.map +1 -1
  101. package/dist/engine/index.d.ts +3 -0
  102. package/dist/engine/index.js +3 -0
  103. package/dist/engine/index.js.map +1 -1
  104. package/dist/engine/plain-text.d.ts +3 -0
  105. package/dist/engine/plain-text.js +60 -0
  106. package/dist/engine/plain-text.js.map +1 -0
  107. package/dist/engine/resolve.d.ts +2 -1
  108. package/dist/engine/resolve.js +17 -9
  109. package/dist/engine/resolve.js.map +1 -1
  110. package/dist/engine/system-variables.d.ts +10 -0
  111. package/dist/engine/system-variables.js +70 -0
  112. package/dist/engine/system-variables.js.map +1 -0
  113. package/dist/engine/tokens.d.ts +2 -1
  114. package/dist/engine/tokens.js +6 -2
  115. package/dist/engine/tokens.js.map +1 -1
  116. package/dist/hooks/tenant-export.js +13 -1
  117. package/dist/hooks/tenant-export.js.map +1 -1
  118. package/dist/implementations/dms-mailing/index.d.ts +1 -1
  119. package/dist/implementations/dms-mailing/index.js +2 -2
  120. package/dist/implementations/dms-mailing/index.js.map +1 -1
  121. package/dist/index.js +13 -11
  122. package/dist/index.js.map +1 -1
  123. package/dist/pages/editor.js +9 -2
  124. package/dist/pages/editor.js.map +1 -1
  125. package/dist/pages/index.d.ts +1 -0
  126. package/dist/pages/index.js +1 -0
  127. package/dist/pages/index.js.map +1 -1
  128. package/dist/pages/module.d.ts +2 -0
  129. package/dist/pages/module.js +26 -2
  130. package/dist/pages/module.js.map +1 -1
  131. package/dist/pages/overview.d.ts +2 -6
  132. package/dist/pages/overview.js +109 -52
  133. package/dist/pages/overview.js.map +1 -1
  134. package/dist/pages/sends.d.ts +3 -4
  135. package/dist/pages/sends.js +197 -60
  136. package/dist/pages/sends.js.map +1 -1
  137. package/dist/pages/settings/form.js +114 -53
  138. package/dist/pages/settings/form.js.map +1 -1
  139. package/dist/pages/settings/page.d.ts +0 -1
  140. package/dist/pages/settings/page.js +8 -11
  141. package/dist/pages/settings/page.js.map +1 -1
  142. package/dist/pages/suppressions.d.ts +9 -0
  143. package/dist/pages/suppressions.js +151 -0
  144. package/dist/pages/suppressions.js.map +1 -0
  145. package/dist/pages/templates.d.ts +0 -1
  146. package/dist/pages/templates.js +114 -40
  147. package/dist/pages/templates.js.map +1 -1
  148. package/dist/permissions.js +2 -2
  149. package/dist/permissions.js.map +1 -1
  150. package/dist/quick-actions.js +7 -4
  151. package/dist/quick-actions.js.map +1 -1
  152. package/dist/routes/events.d.ts +8 -5
  153. package/dist/routes/events.js +55 -24
  154. package/dist/routes/events.js.map +1 -1
  155. package/dist/routes/index.d.ts +2 -0
  156. package/dist/routes/index.js +2 -0
  157. package/dist/routes/index.js.map +1 -1
  158. package/dist/routes/metrics.d.ts +60 -23
  159. package/dist/routes/metrics.js +231 -83
  160. package/dist/routes/metrics.js.map +1 -1
  161. package/dist/routes/period.d.ts +2 -1
  162. package/dist/routes/period.js +9 -3
  163. package/dist/routes/period.js.map +1 -1
  164. package/dist/routes/sends.d.ts +49 -8
  165. package/dist/routes/sends.js +239 -24
  166. package/dist/routes/sends.js.map +1 -1
  167. package/dist/routes/settings.d.ts +44 -5
  168. package/dist/routes/settings.js +190 -15
  169. package/dist/routes/settings.js.map +1 -1
  170. package/dist/routes/suppressions.d.ts +16 -0
  171. package/dist/routes/suppressions.js +92 -0
  172. package/dist/routes/suppressions.js.map +1 -0
  173. package/dist/routes/template-versions.d.ts +28 -0
  174. package/dist/routes/template-versions.js +87 -0
  175. package/dist/routes/template-versions.js.map +1 -0
  176. package/dist/routes/templates.d.ts +60 -6
  177. package/dist/routes/templates.js +204 -35
  178. package/dist/routes/templates.js.map +1 -1
  179. package/dist/services/attention.d.ts +19 -6
  180. package/dist/services/attention.js +177 -12
  181. package/dist/services/attention.js.map +1 -1
  182. package/dist/services/catalog.d.ts +3 -0
  183. package/dist/services/catalog.js +25 -0
  184. package/dist/services/catalog.js.map +1 -0
  185. package/dist/services/categories.d.ts +8 -32
  186. package/dist/services/categories.js +14 -25
  187. package/dist/services/categories.js.map +1 -1
  188. package/dist/services/checked-send.d.ts +9 -0
  189. package/dist/services/checked-send.js +54 -0
  190. package/dist/services/checked-send.js.map +1 -0
  191. package/dist/services/events.d.ts +5 -0
  192. package/dist/services/events.js +102 -9
  193. package/dist/services/events.js.map +1 -1
  194. package/dist/services/metrics.d.ts +17 -6
  195. package/dist/services/metrics.js +57 -18
  196. package/dist/services/metrics.js.map +1 -1
  197. package/dist/services/notify.d.ts +18 -10
  198. package/dist/services/notify.js +82 -37
  199. package/dist/services/notify.js.map +1 -1
  200. package/dist/services/provider-banner.d.ts +14 -0
  201. package/dist/services/provider-banner.js +115 -0
  202. package/dist/services/provider-banner.js.map +1 -0
  203. package/dist/services/render.js.map +1 -1
  204. package/dist/services/send-stat-items.d.ts +7 -0
  205. package/dist/services/send-stat-items.js +127 -0
  206. package/dist/services/send-stat-items.js.map +1 -0
  207. package/dist/services/send-stats.d.ts +23 -0
  208. package/dist/services/send-stats.js +63 -0
  209. package/dist/services/send-stats.js.map +1 -0
  210. package/dist/services/send.d.ts +18 -3
  211. package/dist/services/send.js +81 -17
  212. package/dist/services/send.js.map +1 -1
  213. package/dist/services/settings.d.ts +5 -0
  214. package/dist/services/settings.js +28 -7
  215. package/dist/services/settings.js.map +1 -1
  216. package/dist/services/starters.d.ts +15 -0
  217. package/dist/services/starters.js +264 -0
  218. package/dist/services/starters.js.map +1 -0
  219. package/dist/services/suppressions.d.ts +20 -0
  220. package/dist/services/suppressions.js +53 -0
  221. package/dist/services/suppressions.js.map +1 -0
  222. package/dist/services/template-attention.d.ts +4 -0
  223. package/dist/services/template-attention.js +43 -0
  224. package/dist/services/template-attention.js.map +1 -0
  225. package/dist/services/template-stats.d.ts +27 -0
  226. package/dist/services/template-stats.js +59 -0
  227. package/dist/services/template-stats.js.map +1 -0
  228. package/dist/services/templates.d.ts +31 -4
  229. package/dist/services/templates.js +240 -21
  230. package/dist/services/templates.js.map +1 -1
  231. package/dist/services/webhook-status.d.ts +27 -0
  232. package/dist/services/webhook-status.js +134 -0
  233. package/dist/services/webhook-status.js.map +1 -0
  234. package/dist/types/blocks.d.ts +1 -0
  235. package/dist/types/index.d.ts +2 -0
  236. package/dist/types/index.js +2 -0
  237. package/dist/types/index.js.map +1 -1
  238. package/dist/types/sends.d.ts +10 -1
  239. package/dist/types/sends.js +35 -2
  240. package/dist/types/sends.js.map +1 -1
  241. package/dist/types/suppressions.d.ts +9 -0
  242. package/dist/types/suppressions.js +10 -0
  243. package/dist/types/suppressions.js.map +1 -0
  244. package/dist/types/webhook.d.ts +17 -0
  245. package/dist/types/webhook.js +3 -0
  246. package/dist/types/webhook.js.map +1 -0
  247. package/dist/validation/settings.schema.d.ts +144 -9
  248. package/dist/validation/settings.schema.js +113 -16
  249. package/dist/validation/settings.schema.js.map +1 -1
  250. package/dist/validation/templates.schema.d.ts +137 -10
  251. package/dist/validation/templates.schema.js +28 -2
  252. package/dist/validation/templates.schema.js.map +1 -1
  253. package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
  254. package/frontend-vue/app/build/components/FormRows.vue +26 -0
  255. package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
  256. package/frontend-vue/app/components/CategoriesInput.vue +323 -77
  257. package/frontend-vue/app/components/CategoryInput.vue +13 -14
  258. package/frontend-vue/app/components/CategoryLabel.vue +34 -0
  259. package/frontend-vue/app/components/DuplicateModal.vue +201 -0
  260. package/frontend-vue/app/components/Editor.vue +55 -20
  261. package/frontend-vue/app/components/FixAddressModal.vue +133 -0
  262. package/frontend-vue/app/components/FunnelCard.vue +146 -43
  263. package/frontend-vue/app/components/LocaleSelect.vue +98 -0
  264. package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
  265. package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
  266. package/frontend-vue/app/components/OverviewGate.vue +220 -0
  267. package/frontend-vue/app/components/PageHeader.vue +98 -0
  268. package/frontend-vue/app/components/RealSendModal.vue +555 -0
  269. package/frontend-vue/app/components/RecipientsField.vue +41 -0
  270. package/frontend-vue/app/components/RenderingModal.vue +189 -0
  271. package/frontend-vue/app/components/RetentionInput.vue +212 -0
  272. package/frontend-vue/app/components/SendDrawer.vue +542 -209
  273. package/frontend-vue/app/components/StarterGallery.vue +153 -0
  274. package/frontend-vue/app/components/SystemVariables.vue +63 -0
  275. package/frontend-vue/app/components/TemplateCard.vue +218 -57
  276. package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
  277. package/frontend-vue/app/components/TemplateGallery.vue +312 -56
  278. package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
  279. package/frontend-vue/app/components/TestSendModal.vue +266 -95
  280. package/frontend-vue/app/components/TokenText.vue +61 -10
  281. package/frontend-vue/app/components/VariablePath.vue +20 -0
  282. package/frontend-vue/app/components/VersionHistory.vue +196 -0
  283. package/frontend-vue/app/components/VersionPreview.vue +164 -0
  284. package/frontend-vue/app/components/WebhookStatus.vue +130 -0
  285. package/frontend-vue/app/components/blocks/Button.vue +1 -1
  286. package/frontend-vue/app/components/blocks/Code.vue +1 -1
  287. package/frontend-vue/app/components/blocks/Footer.vue +56 -7
  288. package/frontend-vue/app/components/blocks/Heading.vue +1 -1
  289. package/frontend-vue/app/components/blocks/Hero.vue +13 -7
  290. package/frontend-vue/app/components/blocks/List.vue +50 -6
  291. package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
  292. package/frontend-vue/app/components/blocks/Total.vue +3 -3
  293. package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
  294. package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
  295. package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
  296. package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
  297. package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
  298. package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
  299. package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
  300. package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
  301. package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
  302. package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
  303. package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
  304. package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
  305. package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
  306. package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
  307. package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
  308. package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
  309. package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
  310. package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
  311. package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
  312. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
  313. package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
  314. package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
  315. package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
  316. package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
  317. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
  318. package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
  319. package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
  320. package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
  321. package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
  322. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
  323. package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
  324. package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
  325. package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
  326. package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
  327. package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
  328. package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
  329. package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
  330. package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
  331. package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
  332. package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
  333. package/frontend-vue/app/composables/useBlockContext.ts +16 -0
  334. package/frontend-vue/app/composables/useEditorActions.ts +381 -0
  335. package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
  336. package/frontend-vue/app/composables/useEditorContext.ts +30 -23
  337. package/frontend-vue/app/composables/useEditorSession.ts +299 -0
  338. package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
  339. package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
  340. package/frontend-vue/app/composables/useMailingApi.ts +142 -27
  341. package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
  342. package/frontend-vue/app/composables/useReplayToast.ts +72 -0
  343. package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
  344. package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
  345. package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
  346. package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
  347. package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
  348. package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
  349. package/frontend-vue/app/plugins/column-displays.ts +65 -0
  350. package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
  351. package/frontend-vue/app/types/mailing.ts +193 -18
  352. package/frontend-vue/app/utils/api-error.ts +33 -0
  353. package/frontend-vue/app/utils/autocomplete.ts +120 -0
  354. package/frontend-vue/app/utils/blocks/components.ts +10 -10
  355. package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
  356. package/frontend-vue/app/utils/blocks/factory.ts +199 -38
  357. package/frontend-vue/app/utils/blocks/fields.ts +39 -19
  358. package/frontend-vue/app/utils/blocks/index.ts +1 -0
  359. package/frontend-vue/app/utils/blocks/outline.ts +91 -0
  360. package/frontend-vue/app/utils/blocks/tree.ts +114 -2
  361. package/frontend-vue/app/utils/categories.ts +201 -15
  362. package/frontend-vue/app/utils/changes.ts +152 -0
  363. package/frontend-vue/app/utils/conditions.ts +25 -19
  364. package/frontend-vue/app/utils/editor-checks.ts +266 -0
  365. package/frontend-vue/app/utils/editor-route.ts +43 -4
  366. package/frontend-vue/app/utils/funnel.ts +74 -0
  367. package/frontend-vue/app/utils/gallery.ts +179 -1
  368. package/frontend-vue/app/utils/history.ts +66 -0
  369. package/frontend-vue/app/utils/lifecycle.ts +185 -0
  370. package/frontend-vue/app/utils/locales.ts +107 -2
  371. package/frontend-vue/app/utils/overview-setup.ts +83 -0
  372. package/frontend-vue/app/utils/pages.ts +5 -0
  373. package/frontend-vue/app/utils/paths.ts +51 -15
  374. package/frontend-vue/app/utils/period.ts +62 -22
  375. package/frontend-vue/app/utils/problems.ts +181 -0
  376. package/frontend-vue/app/utils/recipients.ts +49 -0
  377. package/frontend-vue/app/utils/rendering-title.ts +23 -0
  378. package/frontend-vue/app/utils/send-actions.ts +207 -0
  379. package/frontend-vue/app/utils/send-link.ts +28 -0
  380. package/frontend-vue/app/utils/send-snippet.ts +97 -0
  381. package/frontend-vue/app/utils/send-stats.ts +24 -0
  382. package/frontend-vue/app/utils/send-values.ts +156 -0
  383. package/frontend-vue/app/utils/shortcuts.ts +113 -0
  384. package/frontend-vue/app/utils/slug.ts +82 -0
  385. package/frontend-vue/app/utils/starters.ts +23 -0
  386. package/frontend-vue/app/utils/suppression.ts +112 -0
  387. package/frontend-vue/app/utils/system-variables.ts +165 -0
  388. package/frontend-vue/app/utils/template-attention.ts +104 -0
  389. package/frontend-vue/app/utils/timeline.ts +221 -19
  390. package/frontend-vue/app/utils/tokens.ts +115 -13
  391. package/frontend-vue/app/utils/variable-types.ts +23 -0
  392. package/frontend-vue/app/utils/variables.ts +208 -1
  393. package/frontend-vue/app/utils/version-history.ts +53 -0
  394. package/frontend-vue/app/utils/webhook-status.ts +170 -0
  395. package/frontend-vue/dms.frontend.ts +19 -11
  396. package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
  397. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
  398. package/frontend-vue/package.json +4 -1
  399. package/frontend-vue/pnpm-lock.yaml +46 -161
  400. package/frontend-vue/tests/api-error.test.ts +27 -0
  401. package/frontend-vue/tests/autocomplete.test.ts +84 -0
  402. package/frontend-vue/tests/block-components.test.ts +1 -1
  403. package/frontend-vue/tests/block-fields.test.ts +12 -2
  404. package/frontend-vue/tests/blocks-factory.test.ts +46 -0
  405. package/frontend-vue/tests/blocks-outline.test.ts +102 -0
  406. package/frontend-vue/tests/blocks-tree.test.ts +28 -1
  407. package/frontend-vue/tests/categories-input.test.ts +139 -0
  408. package/frontend-vue/tests/categories.test.ts +148 -15
  409. package/frontend-vue/tests/changes.test.ts +136 -0
  410. package/frontend-vue/tests/column-displays.test.ts +73 -0
  411. package/frontend-vue/tests/conditions.test.ts +49 -19
  412. package/frontend-vue/tests/dnd.test.ts +10 -0
  413. package/frontend-vue/tests/editor-block-list.test.ts +24 -2
  414. package/frontend-vue/tests/editor-checks.test.ts +178 -0
  415. package/frontend-vue/tests/editor-route.test.ts +37 -9
  416. package/frontend-vue/tests/editor-session.test.ts +206 -0
  417. package/frontend-vue/tests/footer-block.test.ts +88 -0
  418. package/frontend-vue/tests/funnel-card.test.ts +116 -0
  419. package/frontend-vue/tests/funnel.test.ts +81 -0
  420. package/frontend-vue/tests/gallery.test.ts +124 -2
  421. package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
  422. package/frontend-vue/tests/history.test.ts +48 -0
  423. package/frontend-vue/tests/i18n-editor.test.ts +38 -0
  424. package/frontend-vue/tests/i18n.test.ts +55 -0
  425. package/frontend-vue/tests/lifecycle.test.ts +145 -0
  426. package/frontend-vue/tests/locales.test.ts +60 -0
  427. package/frontend-vue/tests/localized-defaults.test.ts +54 -0
  428. package/frontend-vue/tests/new-template-modal.test.ts +210 -0
  429. package/frontend-vue/tests/overview-gate.test.ts +146 -0
  430. package/frontend-vue/tests/overview-setup.test.ts +78 -0
  431. package/frontend-vue/tests/paths.test.ts +23 -0
  432. package/frontend-vue/tests/period.test.ts +59 -10
  433. package/frontend-vue/tests/problems.test.ts +153 -0
  434. package/frontend-vue/tests/recipients.test.ts +47 -0
  435. package/frontend-vue/tests/rendering-title.test.ts +24 -0
  436. package/frontend-vue/tests/replay-toast.test.ts +67 -0
  437. package/frontend-vue/tests/retention-input.test.ts +129 -0
  438. package/frontend-vue/tests/send-actions.test.ts +128 -0
  439. package/frontend-vue/tests/send-modals.test.ts +415 -0
  440. package/frontend-vue/tests/send-snippet.test.ts +51 -0
  441. package/frontend-vue/tests/send-stats.test.ts +12 -0
  442. package/frontend-vue/tests/send-values.test.ts +119 -0
  443. package/frontend-vue/tests/shortcuts.test.ts +75 -0
  444. package/frontend-vue/tests/slug.test.ts +88 -0
  445. package/frontend-vue/tests/suppression.test.ts +159 -0
  446. package/frontend-vue/tests/system-variables.test.ts +133 -0
  447. package/frontend-vue/tests/template-attention.test.ts +123 -0
  448. package/frontend-vue/tests/template-drawer.test.ts +449 -0
  449. package/frontend-vue/tests/template-editor.test.ts +218 -9
  450. package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
  451. package/frontend-vue/tests/template-gallery.test.ts +370 -0
  452. package/frontend-vue/tests/template-overlays.test.ts +125 -0
  453. package/frontend-vue/tests/timeline.test.ts +175 -20
  454. package/frontend-vue/tests/tokens.test.ts +79 -1
  455. package/frontend-vue/tests/variable-path.test.ts +22 -0
  456. package/frontend-vue/tests/variable-types.test.ts +20 -0
  457. package/frontend-vue/tests/variables.test.ts +90 -2
  458. package/frontend-vue/tests/version-history.test.ts +41 -0
  459. package/frontend-vue/tests/webhook-status.test.ts +108 -0
  460. package/package.json +11 -9
  461. package/src/automation/email-event-trigger.ts +3 -3
  462. package/src/automation/send-template-action.ts +7 -5
  463. package/src/config.ts +3 -1
  464. package/src/constants.ts +18 -2
  465. package/src/crons/attention.ts +21 -0
  466. package/src/crons/index.ts +2 -0
  467. package/src/crons/jobs.ts +35 -0
  468. package/src/crons/retention.ts +7 -22
  469. package/src/data/displays.ts +18 -0
  470. package/src/data/index.ts +1 -0
  471. package/src/data/locale-fallback.ts +26 -0
  472. package/src/data/read-only-routes.ts +14 -0
  473. package/src/data/sends-table.ts +204 -19
  474. package/src/data/suppressions-table.ts +127 -0
  475. package/src/data/templates-table.ts +295 -63
  476. package/src/data-types/categories-type.ts +4 -5
  477. package/src/data-types/category-type.ts +1 -1
  478. package/src/db/models/index.ts +3 -0
  479. package/src/db/models/sends.model.ts +102 -1
  480. package/src/db/models/settings.model.ts +2 -12
  481. package/src/db/models/suppressions.model.ts +53 -0
  482. package/src/db/models/template-versions.model.ts +23 -0
  483. package/src/db/models/templates.model.ts +17 -0
  484. package/src/db/models/webhook-status.model.ts +25 -0
  485. package/src/db/tables/index.ts +3 -0
  486. package/src/db/tables/send-events.table.ts +6 -2
  487. package/src/db/tables/sends.table.ts +37 -2
  488. package/src/db/tables/suppressions.table.ts +46 -0
  489. package/src/db/tables/template-versions.table.ts +44 -0
  490. package/src/db/tables/templates.table.ts +67 -5
  491. package/src/db/tables/webhook-status.table.ts +44 -0
  492. package/src/engine/changes.ts +198 -0
  493. package/src/engine/conditions.ts +1 -7
  494. package/src/engine/coverage.ts +7 -2
  495. package/src/engine/fallback-html.ts +80 -42
  496. package/src/engine/index.ts +3 -0
  497. package/src/engine/plain-text.ts +86 -0
  498. package/src/engine/resolve.ts +49 -15
  499. package/src/engine/system-variables.ts +124 -0
  500. package/src/engine/tokens.ts +13 -1
  501. package/src/hooks/tenant-export.ts +21 -2
  502. package/src/implementations/dms-mailing/index.ts +1 -1
  503. package/src/index.ts +29 -13
  504. package/src/pages/editor.ts +13 -2
  505. package/src/pages/index.ts +6 -3
  506. package/src/pages/module.ts +31 -2
  507. package/src/pages/overview.ts +187 -128
  508. package/src/pages/sends.ts +271 -112
  509. package/src/pages/settings/form.ts +144 -54
  510. package/src/pages/settings/page.ts +9 -17
  511. package/src/pages/suppressions.ts +157 -0
  512. package/src/pages/templates.ts +139 -42
  513. package/src/permissions.ts +6 -4
  514. package/src/quick-actions.ts +12 -5
  515. package/src/routes/events.ts +110 -33
  516. package/src/routes/index.ts +2 -0
  517. package/src/routes/metrics.ts +348 -98
  518. package/src/routes/period.ts +23 -6
  519. package/src/routes/sends.ts +413 -41
  520. package/src/routes/settings.ts +291 -26
  521. package/src/routes/suppressions.ts +87 -0
  522. package/src/routes/template-versions.ts +116 -0
  523. package/src/routes/templates.ts +334 -48
  524. package/src/services/attention.ts +283 -30
  525. package/src/services/catalog.ts +28 -0
  526. package/src/services/categories.ts +28 -27
  527. package/src/services/checked-send.ts +123 -0
  528. package/src/services/events.ts +174 -20
  529. package/src/services/metrics.ts +137 -28
  530. package/src/services/notify.ts +126 -44
  531. package/src/services/provider-banner.ts +165 -0
  532. package/src/services/render.ts +1 -0
  533. package/src/services/send-stat-items.ts +160 -0
  534. package/src/services/send-stats.ts +122 -0
  535. package/src/services/send.ts +193 -31
  536. package/src/services/settings.ts +52 -8
  537. package/src/services/starters.ts +383 -0
  538. package/src/services/suppressions.ts +116 -0
  539. package/src/services/template-attention.ts +82 -0
  540. package/src/services/template-stats.ts +97 -0
  541. package/src/services/templates.ts +400 -37
  542. package/src/services/webhook-status.ts +202 -0
  543. package/src/types/blocks.ts +6 -0
  544. package/src/types/index.ts +2 -0
  545. package/src/types/sends.ts +64 -2
  546. package/src/types/suppressions.ts +27 -0
  547. package/src/types/webhook.ts +29 -0
  548. package/src/validation/settings.schema.ts +173 -17
  549. package/src/validation/templates.schema.ts +53 -1
  550. package/dist/antelope-modules.d.ts +0 -32
  551. package/dist/antelope-modules.js +0 -36
  552. package/dist/antelope-modules.js.map +0 -1
  553. package/dist/test/antelope.test.d.ts +0 -2
  554. package/dist/test/antelope.test.js +0 -180
  555. package/dist/test/antelope.test.js.map +0 -1
  556. package/dist/test/helpers/http.d.ts +0 -4
  557. package/dist/test/helpers/http.js +0 -28
  558. package/dist/test/helpers/http.js.map +0 -1
  559. package/dist/test/helpers/owner.d.ts +0 -5
  560. package/dist/test/helpers/owner.js +0 -30
  561. package/dist/test/helpers/owner.js.map +0 -1
  562. package/dist/test/integration/events.test.d.ts +0 -1
  563. package/dist/test/integration/events.test.js +0 -143
  564. package/dist/test/integration/events.test.js.map +0 -1
  565. package/dist/test/integration/retention.test.d.ts +0 -1
  566. package/dist/test/integration/retention.test.js +0 -291
  567. package/dist/test/integration/retention.test.js.map +0 -1
  568. package/dist/test/integration/sends.test.d.ts +0 -1
  569. package/dist/test/integration/sends.test.js +0 -210
  570. package/dist/test/integration/sends.test.js.map +0 -1
  571. package/dist/test/integration/settings.test.d.ts +0 -1
  572. package/dist/test/integration/settings.test.js +0 -137
  573. package/dist/test/integration/settings.test.js.map +0 -1
  574. package/dist/test/integration/setup.test.d.ts +0 -1
  575. package/dist/test/integration/setup.test.js +0 -32
  576. package/dist/test/integration/setup.test.js.map +0 -1
  577. package/dist/test/integration/templates.test.d.ts +0 -1
  578. package/dist/test/integration/templates.test.js +0 -233
  579. package/dist/test/integration/templates.test.js.map +0 -1
  580. package/dist/test/integration/tenant-export.test.d.ts +0 -1
  581. package/dist/test/integration/tenant-export.test.js +0 -85
  582. package/dist/test/integration/tenant-export.test.js.map +0 -1
  583. package/dist/test/unit/attention.test.d.ts +0 -1
  584. package/dist/test/unit/attention.test.js +0 -109
  585. package/dist/test/unit/attention.test.js.map +0 -1
  586. package/dist/test/unit/automation.test.d.ts +0 -1
  587. package/dist/test/unit/automation.test.js +0 -104
  588. package/dist/test/unit/automation.test.js.map +0 -1
  589. package/dist/test/unit/categories.test.d.ts +0 -1
  590. package/dist/test/unit/categories.test.js +0 -38
  591. package/dist/test/unit/categories.test.js.map +0 -1
  592. package/dist/test/unit/engine/conditions.test.d.ts +0 -1
  593. package/dist/test/unit/engine/conditions.test.js +0 -23
  594. package/dist/test/unit/engine/conditions.test.js.map +0 -1
  595. package/dist/test/unit/engine/coverage.test.d.ts +0 -1
  596. package/dist/test/unit/engine/coverage.test.js +0 -106
  597. package/dist/test/unit/engine/coverage.test.js.map +0 -1
  598. package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
  599. package/dist/test/unit/engine/fallback-html.test.js +0 -78
  600. package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
  601. package/dist/test/unit/engine/paths.test.d.ts +0 -1
  602. package/dist/test/unit/engine/paths.test.js +0 -33
  603. package/dist/test/unit/engine/paths.test.js.map +0 -1
  604. package/dist/test/unit/engine/resolve.test.d.ts +0 -1
  605. package/dist/test/unit/engine/resolve.test.js +0 -157
  606. package/dist/test/unit/engine/resolve.test.js.map +0 -1
  607. package/dist/test/unit/engine/tokens.test.d.ts +0 -1
  608. package/dist/test/unit/engine/tokens.test.js +0 -26
  609. package/dist/test/unit/engine/tokens.test.js.map +0 -1
  610. package/dist/test/unit/events.test.d.ts +0 -1
  611. package/dist/test/unit/events.test.js +0 -13
  612. package/dist/test/unit/events.test.js.map +0 -1
  613. package/dist/test/unit/metrics.test.d.ts +0 -1
  614. package/dist/test/unit/metrics.test.js +0 -108
  615. package/dist/test/unit/metrics.test.js.map +0 -1
  616. package/dist/test/unit/notify.test.d.ts +0 -1
  617. package/dist/test/unit/notify.test.js +0 -49
  618. package/dist/test/unit/notify.test.js.map +0 -1
  619. package/dist/test/unit/period.test.d.ts +0 -1
  620. package/dist/test/unit/period.test.js +0 -61
  621. package/dist/test/unit/period.test.js.map +0 -1
  622. package/dist/test/unit/permissions.test.d.ts +0 -1
  623. package/dist/test/unit/permissions.test.js +0 -27
  624. package/dist/test/unit/permissions.test.js.map +0 -1
  625. package/dist/test/unit/provider.test.d.ts +0 -1
  626. package/dist/test/unit/provider.test.js +0 -47
  627. package/dist/test/unit/provider.test.js.map +0 -1
  628. package/dist/test/unit/realtime.test.d.ts +0 -1
  629. package/dist/test/unit/realtime.test.js +0 -13
  630. package/dist/test/unit/realtime.test.js.map +0 -1
  631. package/dist/test/unit/render.test.d.ts +0 -1
  632. package/dist/test/unit/render.test.js +0 -23
  633. package/dist/test/unit/render.test.js.map +0 -1
  634. package/dist/test/unit/retention.test.d.ts +0 -1
  635. package/dist/test/unit/retention.test.js +0 -66
  636. package/dist/test/unit/retention.test.js.map +0 -1
  637. package/dist/test/unit/send-batch.test.d.ts +0 -1
  638. package/dist/test/unit/send-batch.test.js +0 -46
  639. package/dist/test/unit/send-batch.test.js.map +0 -1
  640. package/dist/test/unit/send.test.d.ts +0 -1
  641. package/dist/test/unit/send.test.js +0 -120
  642. package/dist/test/unit/send.test.js.map +0 -1
  643. package/dist/test/unit/smoke.test.d.ts +0 -1
  644. package/dist/test/unit/smoke.test.js +0 -10
  645. package/dist/test/unit/smoke.test.js.map +0 -1
  646. package/dist/test/unit/status.test.d.ts +0 -1
  647. package/dist/test/unit/status.test.js +0 -16
  648. package/dist/test/unit/status.test.js.map +0 -1
  649. package/dist/test/unit/templates.test.d.ts +0 -1
  650. package/dist/test/unit/templates.test.js +0 -48
  651. package/dist/test/unit/templates.test.js.map +0 -1
  652. package/dist/test/unit/tenant-export.test.d.ts +0 -1
  653. package/dist/test/unit/tenant-export.test.js +0 -59
  654. package/dist/test/unit/tenant-export.test.js.map +0 -1
  655. package/dist/test/unit/webhook-secret.test.d.ts +0 -1
  656. package/dist/test/unit/webhook-secret.test.js +0 -43
  657. package/dist/test/unit/webhook-secret.test.js.map +0 -1
  658. package/frontend-vue/app/components/AttentionCard.vue +0 -78
  659. package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
  660. package/frontend-vue/app/components/ProviderChip.vue +0 -39
  661. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
  662. package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
  663. package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
  664. package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
  665. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
  666. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
  667. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
  668. package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
  669. package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
  670. package/src/antelope-modules.ts +0 -48
  671. package/src/test/antelope.test.ts +0 -190
  672. package/src/test/helpers/http.ts +0 -23
  673. package/src/test/helpers/owner.ts +0 -37
  674. package/src/test/integration/events.test.ts +0 -159
  675. package/src/test/integration/retention.test.ts +0 -363
  676. package/src/test/integration/sends.test.ts +0 -233
  677. package/src/test/integration/settings.test.ts +0 -128
  678. package/src/test/integration/setup.test.ts +0 -35
  679. package/src/test/integration/templates.test.ts +0 -272
  680. package/src/test/integration/tenant-export.test.ts +0 -125
  681. package/src/test/unit/attention.test.ts +0 -129
  682. package/src/test/unit/automation.test.ts +0 -130
  683. package/src/test/unit/categories.test.ts +0 -51
  684. package/src/test/unit/engine/conditions.test.ts +0 -35
  685. package/src/test/unit/engine/coverage.test.ts +0 -118
  686. package/src/test/unit/engine/fallback-html.test.ts +0 -86
  687. package/src/test/unit/engine/paths.test.ts +0 -39
  688. package/src/test/unit/engine/resolve.test.ts +0 -176
  689. package/src/test/unit/engine/tokens.test.ts +0 -26
  690. package/src/test/unit/events.test.ts +0 -19
  691. package/src/test/unit/metrics.test.ts +0 -170
  692. package/src/test/unit/notify.test.ts +0 -71
  693. package/src/test/unit/period.test.ts +0 -91
  694. package/src/test/unit/permissions.test.ts +0 -30
  695. package/src/test/unit/provider.test.ts +0 -57
  696. package/src/test/unit/realtime.test.ts +0 -11
  697. package/src/test/unit/render.test.ts +0 -23
  698. package/src/test/unit/retention.test.ts +0 -91
  699. package/src/test/unit/send-batch.test.ts +0 -72
  700. package/src/test/unit/send.test.ts +0 -161
  701. package/src/test/unit/smoke.test.ts +0 -8
  702. package/src/test/unit/status.test.ts +0 -14
  703. package/src/test/unit/templates.test.ts +0 -52
  704. package/src/test/unit/tenant-export.test.ts +0 -88
  705. package/src/test/unit/webhook-secret.test.ts +0 -51
@@ -7,10 +7,13 @@ import {
7
7
  } from "@antelopejs/interface-api";
8
8
  import { assert } from "@antelopejs/interface-api-util";
9
9
  import { AuthUserWithPermission } from "@antelopejs/interface-dms/guards";
10
+ import { getRequestTenantId } from "@antelopejs/interface-dms/request-tenant";
10
11
  import { TenantScopedModel } from "@antelopejs/interface-dms/tenant-scoped-model";
11
- import type { TopListItem } from "@antelopejs/interface-dms/base";
12
+ import type { KpiCardData, TopListItem } from "@antelopejs/interface-dms/base";
13
+ import type { ComposedText } from "@antelopejs/interface-dms/base/types";
12
14
  import type { User } from "@antelopejs/interface-dms/auth/db";
13
15
  import { API_BASE_PATH, HTTP_NOT_FOUND } from "../constants";
16
+ import { isMarketingCategory } from "../engine";
14
17
  import {
15
18
  type SendSummary,
16
19
  type MailingTemplate,
@@ -19,32 +22,42 @@ import {
19
22
  } from "../db";
20
23
  import { OverviewPageController } from "../pages/overview";
21
24
  import { readCapabilities } from "../services/provider";
22
- import {
23
- buildAttentionItems,
24
- type ProviderTracking,
25
- } from "../services/attention";
25
+ import { trackingOf } from "../services/send-stats";
26
+ import { findSettings } from "../services/settings";
27
+ import { attentionFeedItem, buildAttentionItems } from "../services/attention";
26
28
  import {
27
29
  aggregate,
30
+ alignSeries,
28
31
  dailySeries,
32
+ dailyValues,
29
33
  deltaPercent,
30
34
  domainOf,
31
35
  forAudience,
32
36
  groupBy,
37
+ isDeliveryUntracked,
38
+ leftForProvider,
33
39
  ratio,
34
40
  readAudience,
35
41
  type DateRange,
42
+ type ProviderTracking,
36
43
  type Totals,
44
+ type TotalsExtractor,
37
45
  } from "../services/metrics";
38
46
  import { SEND_AUDIENCE_QUERY_KEY, type SendAudience } from "../types";
39
- import { comparisonRange, currentRange } from "./period";
47
+ import { comparisonRange, currentRange, isComparisonWanted } from "./period";
40
48
 
41
49
  const UNKNOWN_METRIC = "$dms_mailing.errors.unknown_metric";
42
- const EDITOR_PATH = "/modules/mailing/editor";
50
+ const TEMPLATES_PATH = "/modules/mailing/templates";
43
51
  const MAX_TOP_TEMPLATES = 10;
44
52
  const MAX_TOP_DOMAINS = 6;
45
- const FULL_RATE = 100;
53
+ /** Below this many deliveries a rate swings on a single e-mail: not ranked. */
54
+ export const MIN_RANKED_DELIVERIES = 5;
55
+ const DELIVERED_COUNT_KEY = "$dms_mailing.metrics.delivered_count";
56
+ const TEMPLATE_VOLUME_KEY = "$dms_mailing.metrics.template_volume";
57
+ const FIRST_ROW_OFFSET = 0;
58
+ const SINGLE_ROW = 1;
46
59
 
47
- const KPI_EXTRACTORS: Record<string, (totals: Totals) => number> = {
60
+ const KPI_EXTRACTORS: Record<string, TotalsExtractor> = {
48
61
  sends: (totals) => totals.sends,
49
62
  deliverability: (totals) => totals.deliverability,
50
63
  "open-rate": (totals) => totals.openRate,
@@ -55,20 +68,94 @@ const KPI_EXTRACTORS: Record<string, (totals: Totals) => number> = {
55
68
  latency: (totals) => totals.latencyMedian,
56
69
  };
57
70
 
58
- const TOP_METRICS: Record<string, (totals: Totals) => number> = {
59
- "open-rate": (totals) => totals.openRate,
60
- "click-rate": (totals) => totals.clickRate,
61
- sends: (totals) => totals.sends,
71
+ /** The KPIs read from delivery events: unmeasured when nothing reports them. */
72
+ const DELIVERY_METRICS = new Set(["deliverability", "open-rate", "click-rate"]);
73
+
74
+ export interface TopMetric {
75
+ extract: TotalsExtractor;
76
+ /** Deliveries a row needs before its value is ranked. */
77
+ minDeliveries: number;
78
+ }
79
+
80
+ const TOP_METRICS: Record<string, TopMetric> = {
81
+ "open-rate": {
82
+ extract: (totals) => totals.openRate,
83
+ minDeliveries: MIN_RANKED_DELIVERIES,
84
+ },
85
+ "click-rate": {
86
+ extract: (totals) => totals.clickRate,
87
+ minDeliveries: MIN_RANKED_DELIVERIES,
88
+ },
89
+ sends: { extract: (totals) => totals.sends, minDeliveries: 0 },
62
90
  };
91
+ const DEFAULT_TOP_METRIC = TOP_METRICS["open-rate"] as TopMetric;
92
+
93
+ interface FunnelStepDefinition {
94
+ id: string;
95
+ count: (totals: Totals) => number;
96
+ /** What the step's rate is a share of, the base its KPI uses. */
97
+ base: (totals: Totals) => number;
98
+ /** Counted from events the provider reports back. */
99
+ isDeliveryEvent: boolean;
100
+ }
63
101
 
64
- const FUNNEL_STEPS: Array<[string, (totals: Totals) => number]> = [
65
- ["sent", (totals) => totals.sends],
66
- ["delivered", (totals) => totals.delivered],
67
- ["opened", (totals) => totals.opened],
68
- ["clicked", (totals) => totals.clicked],
69
- ["unsubscribed", (totals) => totals.unsubscribed],
102
+ const FUNNEL_STEPS: FunnelStepDefinition[] = [
103
+ {
104
+ id: "sent",
105
+ count: (totals) => totals.sends,
106
+ base: (totals) => totals.sends,
107
+ isDeliveryEvent: false,
108
+ },
109
+ {
110
+ id: "delivered",
111
+ count: (totals) => totals.delivered,
112
+ base: (totals) => totals.sends,
113
+ isDeliveryEvent: true,
114
+ },
115
+ {
116
+ id: "opened",
117
+ count: (totals) => totals.opened,
118
+ base: (totals) => totals.delivered,
119
+ isDeliveryEvent: true,
120
+ },
121
+ {
122
+ id: "clicked",
123
+ count: (totals) => totals.clicked,
124
+ base: (totals) => totals.delivered,
125
+ isDeliveryEvent: true,
126
+ },
127
+ {
128
+ id: "unsubscribed",
129
+ count: (totals) => totals.unsubscribed,
130
+ base: (totals) => totals.delivered,
131
+ isDeliveryEvent: false,
132
+ },
70
133
  ];
71
134
 
135
+ export interface FunnelStepResponse {
136
+ id: string;
137
+ label: string;
138
+ count: number;
139
+ /** Share of the step's base, as its KPI counts it; `null` on an empty base. */
140
+ rate: number | null;
141
+ /** `false` when nothing reports the events the step counts. */
142
+ isMeasured: boolean;
143
+ }
144
+
145
+ /** The sends of the window the figures are compared with. */
146
+ export interface ComparedWindow {
147
+ range: DateRange;
148
+ sends: SendSummary[];
149
+ }
150
+
151
+ /** What the Overview's first-run checklist reads. */
152
+ export interface SetupFactsResponse {
153
+ isProviderConnected: boolean;
154
+ isSenderSet: boolean;
155
+ liveTemplateCount: number;
156
+ hasTestSend: boolean;
157
+ }
158
+
72
159
  export class MetricsController extends Controller(`${API_BASE_PATH}/metrics`) {
73
160
  @Context()
74
161
  declare ctx: RequestContext;
@@ -82,6 +169,10 @@ export class MetricsController extends Controller(`${API_BASE_PATH}/metrics`) {
82
169
  @TenantScopedModel(TemplateModel)
83
170
  declare templates: TemplateModel;
84
171
 
172
+ private get tenantId(): string {
173
+ return getRequestTenantId(this.ctx);
174
+ }
175
+
85
176
  /**
86
177
  * Which sends this request describes. Declared by the calling page, never
87
178
  * inferred: the Overview asks for the business view, the Sends page for the
@@ -96,73 +187,59 @@ export class MetricsController extends Controller(`${API_BASE_PATH}/metrics`) {
96
187
  return forAudience(rows, this.audience);
97
188
  }
98
189
 
190
+ /** The comparison window and its sends; `null` when the user turned it off. */
191
+ private async loadComparison(
192
+ range: DateRange,
193
+ ): Promise<ComparedWindow | null> {
194
+ if (!isComparisonWanted(this.ctx)) return null;
195
+ const previousRange = comparisonRange(this.ctx, range);
196
+ return { range: previousRange, sends: await this.load(previousRange) };
197
+ }
198
+
99
199
  @Get("/kpi/:metric")
100
- async kpi(@Parameter("metric", "param") metric: string) {
200
+ async kpi(
201
+ @Parameter("metric", "param") metric: string,
202
+ ): Promise<KpiCardData> {
101
203
  const extract = KPI_EXTRACTORS[metric];
102
204
  assert(extract, HTTP_NOT_FOUND, UNKNOWN_METRIC);
103
205
  const range = currentRange(this.ctx);
104
- const previousRange = comparisonRange(this.ctx, range);
105
- const [current, previous] = await Promise.all([
206
+ const [current, previous, tracking] = await Promise.all([
106
207
  this.load(range),
107
- this.load(previousRange),
208
+ this.loadComparison(range),
209
+ readTracking(),
108
210
  ]);
109
- const value = extract(aggregate(current));
110
- const previousValue = extract(aggregate(previous));
111
- return {
112
- value,
113
- previousValue,
114
- delta: deltaPercent(value, previousValue),
115
- sparkline: dailySeries(current, range.from, range.to).map(
116
- (point) => point.y,
117
- ),
118
- };
211
+ if (isUnmeasured(metric, current, tracking)) return {};
212
+ const previousSends =
213
+ previous && !isUnmeasured(metric, previous.sends, tracking)
214
+ ? previous.sends
215
+ : null;
216
+ return kpiFigure(extract, current, previousSends, range);
119
217
  }
120
218
 
121
219
  @Get("/volume")
122
220
  async volume() {
123
221
  const range = currentRange(this.ctx);
124
- const previousRange = comparisonRange(this.ctx, range);
125
222
  const [current, previous] = await Promise.all([
126
223
  this.load(range),
127
- this.load(previousRange),
224
+ this.loadComparison(range),
128
225
  ]);
129
- const totals = aggregate(current);
130
- const previousTotals = aggregate(previous);
131
- return {
132
- value: totals.sends,
133
- previousValue: previousTotals.sends,
134
- delta: deltaPercent(totals.sends, previousTotals.sends),
135
- series: [
136
- {
137
- name: "$dms_mailing.metrics.sends",
138
- data: dailySeries(current, range.from, range.to),
139
- },
140
- ],
141
- comparisonSeries: [
142
- {
143
- name: "$dms_mailing.metrics.previous",
144
- data: dailySeries(previous, previousRange.from, previousRange.to),
145
- },
146
- ],
147
- };
226
+ return volumeChart(current, range, previous);
148
227
  }
149
228
 
150
229
  @Get("/top-templates")
151
230
  async topTemplates() {
152
- const metric = this.ctx.url.searchParams.get("metric") ?? "open-rate";
153
- const extract =
154
- TOP_METRICS[metric] ??
155
- (TOP_METRICS["open-rate"] as (totals: Totals) => number);
156
- const sends = await this.load(currentRange(this.ctx));
157
- const templates = await this.templates.getAll();
231
+ const metric =
232
+ TOP_METRICS[this.ctx.url.searchParams.get("metric") ?? ""] ??
233
+ DEFAULT_TOP_METRIC;
234
+ const [sends, templates] = await Promise.all([
235
+ this.load(currentRange(this.ctx)),
236
+ this.templates.getAll(),
237
+ ]);
158
238
  const items = [...groupBy(sends, (send) => send.templateSlug)].map(
159
- ([slug, rows]) => toTemplateItem(slug, rows, templates, extract),
239
+ ([slug, rows]) =>
240
+ toTemplateItem(slug, aggregate(rows), templates, metric),
160
241
  );
161
- return {
162
- items: items
163
- .sort((left, right) => right.value - left.value)
164
- .slice(0, MAX_TOP_TEMPLATES),
165
- };
242
+ return { items: ranked(items, MAX_TOP_TEMPLATES) };
166
243
  }
167
244
 
168
245
  @Get("/domains")
@@ -170,74 +247,247 @@ export class MetricsController extends Controller(`${API_BASE_PATH}/metrics`) {
170
247
  const sends = await this.load(currentRange(this.ctx));
171
248
  const items = [
172
249
  ...groupBy(sends, (send) => domainOf(send.recipientEmail)),
173
- ].map(([domain, rows]) => ({
174
- id: domain,
175
- title: domain,
176
- value: aggregate(rows).openRate,
177
- }));
178
- return {
179
- items: items
180
- .sort((left, right) => right.value - left.value)
181
- .slice(0, MAX_TOP_DOMAINS),
182
- };
250
+ ].map(([domain, rows]) => toDomainItem(domain, aggregate(rows)));
251
+ return { items: ranked(items, MAX_TOP_DOMAINS) };
183
252
  }
184
253
 
185
254
  @Get("/funnel")
186
255
  async funnel() {
187
- const totals = aggregate(await this.load(currentRange(this.ctx)));
256
+ const kind = readFunnelKind(this.ctx.url.searchParams.get("kind"));
257
+ const [sends, templates, tracking] = await Promise.all([
258
+ this.load(currentRange(this.ctx)),
259
+ this.templates.getAll(),
260
+ readTracking(),
261
+ ]);
262
+ const kept = filterByKind(sends, templates, kind);
188
263
  return {
189
- steps: FUNNEL_STEPS.map(([id, extract]) => ({
190
- id,
191
- label: `$dms_mailing.funnel.${id}`,
192
- count: extract(totals),
193
- rate: id === "sent" ? FULL_RATE : ratio(extract(totals), totals.sends),
194
- })),
264
+ steps: funnelSteps(aggregate(kept), isDeliveryUntracked(kept, tracking)),
195
265
  };
196
266
  }
197
267
 
198
268
  @Get("/attention")
199
269
  async attention() {
200
270
  const range = currentRange(this.ctx);
201
- const [templates, rows] = await Promise.all([
271
+ const [templates, rows, tracking, settings] = await Promise.all([
202
272
  this.templates.getAll(),
203
273
  this.sends.listBetween(range.from, range.to),
274
+ readTracking(),
275
+ findSettings(this.tenantId),
204
276
  ]);
205
277
  const sends = forAudience(rows, this.audience);
206
278
  const excluded = rows.filter((row) => !sends.includes(row));
207
- const tracking = await readTracking();
279
+ const items = buildAttentionItems({
280
+ templates,
281
+ sends,
282
+ excluded,
283
+ windowEnd: range.to,
284
+ tracking,
285
+ isSenderSet: Boolean(settings?.senderEmail),
286
+ });
287
+ return { items: items.map(attentionFeedItem) };
288
+ }
289
+
290
+ /** The facts behind the Overview's first-run checklist, in one read. */
291
+ @Get("/setup")
292
+ async setup(): Promise<SetupFactsResponse> {
293
+ const [capabilities, templates, settings, testSends] = await Promise.all([
294
+ readCapabilities(),
295
+ this.templates.getAll(),
296
+ findSettings(this.tenantId),
297
+ this.sends.table
298
+ .filter((row) => row.key("isTest").eq(true))
299
+ .slice(FIRST_ROW_OFFSET, SINGLE_ROW)
300
+ .without("json_variables")
301
+ .run(),
302
+ ]);
208
303
  return {
209
- items: buildAttentionItems({ templates, sends, excluded, tracking }),
304
+ isProviderConnected: capabilities !== null,
305
+ isSenderSet: Boolean(settings?.senderEmail),
306
+ liveTemplateCount: templates.filter(
307
+ (template) => template.status === "live",
308
+ ).length,
309
+ hasTestSend: testSends.length > 0,
210
310
  };
211
311
  }
212
312
  }
213
313
 
314
+ /** A figure and its change, each left out when it cannot be stated. */
315
+ export function comparedFigure(
316
+ value: number | null,
317
+ previousValue: number | null,
318
+ ): KpiCardData {
319
+ const figure: KpiCardData = {};
320
+ const delta = deltaPercent(value, previousValue);
321
+ if (value !== null) figure.value = value;
322
+ if (previousValue !== null) figure.previousValue = previousValue;
323
+ if (delta !== null) figure.delta = delta;
324
+ return figure;
325
+ }
326
+
327
+ /**
328
+ * A KPI card's answer: the figure, its change when a comparison window is
329
+ * read, and the figure's own daily trend as the sparkline.
330
+ */
331
+ export function kpiFigure(
332
+ extract: TotalsExtractor,
333
+ current: SendSummary[],
334
+ previous: SendSummary[] | null,
335
+ range: DateRange,
336
+ ): KpiCardData {
337
+ const value = extract(aggregate(current));
338
+ const previousValue = previous ? extract(aggregate(previous)) : null;
339
+ return {
340
+ ...comparedFigure(value, previousValue),
341
+ sparkline: dailyValues(current, range, extract),
342
+ };
343
+ }
344
+
345
+ /**
346
+ * Whether a window cannot state a delivery KPI: nothing reported deliveries
347
+ * for its sends. A previous window in that case has nothing to compare.
348
+ */
349
+ export function isUnmeasured(
350
+ metric: string,
351
+ sends: SendSummary[],
352
+ tracking: ProviderTracking | null,
353
+ ): boolean {
354
+ return DELIVERY_METRICS.has(metric) && isDeliveryUntracked(sends, tracking);
355
+ }
356
+
357
+ /**
358
+ * The send volume chart; the comparison series overlaps the current one. A
359
+ * suppressed send never left, so it is not counted, as in the Sends KPI.
360
+ */
361
+ export function volumeChart(
362
+ current: SendSummary[],
363
+ range: DateRange,
364
+ previous: ComparedWindow | null,
365
+ ) {
366
+ const sent = current.filter(leftForProvider);
367
+ const previousSent = previous?.sends.filter(leftForProvider) ?? null;
368
+ const series = dailySeries(sent, range.from, range.to);
369
+ const comparison =
370
+ previous && previousSent
371
+ ? dailySeries(previousSent, previous.range.from, previous.range.to)
372
+ : null;
373
+ return {
374
+ ...comparedFigure(sent.length, previousSent ? previousSent.length : null),
375
+ series: [{ name: "$dms_mailing.metrics.sends", data: series }],
376
+ comparisonSeries: comparison
377
+ ? [
378
+ {
379
+ name: "$dms_mailing.metrics.previous",
380
+ data: alignSeries(comparison, series),
381
+ },
382
+ ]
383
+ : [],
384
+ };
385
+ }
386
+
387
+ export function funnelSteps(
388
+ totals: Totals,
389
+ isUntracked: boolean,
390
+ ): FunnelStepResponse[] {
391
+ return FUNNEL_STEPS.map((step) => ({
392
+ id: step.id,
393
+ label: `$dms_mailing.funnel.${step.id}`,
394
+ count: step.count(totals),
395
+ rate: ratio(step.count(totals), step.base(totals)),
396
+ isMeasured: !(isUntracked && step.isDeliveryEvent),
397
+ }));
398
+ }
399
+
400
+ /** Which sends the funnel counts: all, or one side of the marketing split. */
401
+ export type FunnelKind = "all" | "marketing" | "transactional";
402
+
403
+ const FUNNEL_KINDS: Record<string, FunnelKind> = {
404
+ all: "all",
405
+ marketing: "marketing",
406
+ transactional: "transactional",
407
+ };
408
+
409
+ export function readFunnelKind(raw: string | null): FunnelKind {
410
+ return FUNNEL_KINDS[raw ?? ""] ?? "all";
411
+ }
412
+
413
+ const KIND_KEEPS: Record<FunnelKind, (category: string) => boolean> = {
414
+ all: () => true,
415
+ marketing: (category) => isMarketingCategory(category),
416
+ transactional: (category) => !isMarketingCategory(category),
417
+ };
418
+
419
+ /**
420
+ * Sends of the templates on one side of the split. A template's category
421
+ * decides: "marketing" is the campaign side, everything else transactional.
422
+ */
423
+ export function filterByKind(
424
+ sends: SendSummary[],
425
+ templates: MailingTemplate[],
426
+ kind: FunnelKind,
427
+ ): SendSummary[] {
428
+ const categories = new Map(
429
+ templates.map((template) => [template.slug, template.category ?? ""]),
430
+ );
431
+ const keeps = KIND_KEEPS[kind];
432
+ return sends.filter((send) => keeps(categories.get(send.templateSlug) ?? ""));
433
+ }
434
+
214
435
  /**
215
436
  * What the provider says it can report. An unreachable provider yields `null`
216
437
  * rather than an error: the Overview keeps its other notices, and the provider
217
438
  * card is the one that reports the outage.
218
439
  */
219
440
  async function readTracking(): Promise<ProviderTracking | null> {
220
- const capabilities = await readCapabilities();
221
- if (!capabilities) return null;
441
+ return trackingOf(await readCapabilities());
442
+ }
443
+
444
+ /** The rows that can be ranked, best first, at most `limit` of them. */
445
+ export function ranked(
446
+ items: Array<TopListItem | null>,
447
+ limit: number,
448
+ ): TopListItem[] {
449
+ return items
450
+ .filter((item): item is TopListItem => item !== null)
451
+ .sort((left, right) => right.value - left.value)
452
+ .slice(0, limit);
453
+ }
454
+
455
+ const deliveredCount = (delivered: number): ComposedText => ({
456
+ key: DELIVERED_COUNT_KEY,
457
+ params: { count: { type: "count", value: delivered } },
458
+ });
459
+
460
+ export function toDomainItem(
461
+ domain: string,
462
+ totals: Totals,
463
+ ): TopListItem | null {
464
+ if (totals.openRate === null || totals.delivered < MIN_RANKED_DELIVERIES)
465
+ return null;
222
466
  return {
223
- name: capabilities.name,
224
- openTracking: capabilities.features.openTracking,
225
- clickTracking: capabilities.features.clickTracking,
467
+ id: domain,
468
+ title: domain,
469
+ description: deliveredCount(totals.delivered),
470
+ value: totals.openRate,
226
471
  };
227
472
  }
228
473
 
229
- function toTemplateItem(
474
+ export function toTemplateItem(
230
475
  slug: string,
231
- rows: SendSummary[],
476
+ totals: Totals,
232
477
  templates: MailingTemplate[],
233
- extract: (totals: Totals) => number,
234
- ): TopListItem {
478
+ metric: TopMetric,
479
+ ): TopListItem | null {
480
+ const value = metric.extract(totals);
481
+ if (value === null || totals.delivered < metric.minDeliveries) return null;
235
482
  const template = templates.find((candidate) => candidate.slug === slug);
236
483
  return {
237
484
  id: slug,
238
485
  title: template?.name ?? slug,
239
- description: slug,
240
- value: extract(aggregate(rows)),
241
- to: template ? `${EDITOR_PATH}?id=${template._id}` : undefined,
486
+ description: {
487
+ key: TEMPLATE_VOLUME_KEY,
488
+ params: { slug, count: { type: "count", value: totals.delivered } },
489
+ },
490
+ value,
491
+ to: template ? `${TEMPLATES_PATH}/${template._id}` : undefined,
242
492
  };
243
493
  }
@@ -1,15 +1,17 @@
1
1
  import type { RequestContext } from "@antelopejs/interface-api";
2
+ import { MAX_WINDOW_DAYS } from "../constants";
3
+ import type { DateRange } from "../services/metrics";
2
4
 
3
5
  /**
4
6
  * Both readers only ever touch the query string, so they ask for that slice
5
7
  * rather than the whole request: a full `RequestContext` still satisfies it.
6
8
  */
7
9
  export type RangeContext = Pick<RequestContext, "url">;
8
- import { MAX_WINDOW_DAYS } from "../constants";
9
- import type { DateRange } from "../services/metrics";
10
10
 
11
11
  const DAY_MS = 86_400_000;
12
12
  const DEFAULT_WINDOW_DAYS = 30;
13
+ /** The DMS period selector's `comparison` value when the user turned it off. */
14
+ const NO_COMPARISON = "none";
13
15
 
14
16
  function readQuery(ctx: RangeContext, key: string): string | undefined {
15
17
  return ctx.url.searchParams.get(key) ?? undefined;
@@ -21,8 +23,14 @@ function parseDate(raw: string | undefined): Date | undefined {
21
23
  return Number.isNaN(parsed.getTime()) ? undefined : parsed;
22
24
  }
23
25
 
24
- /** Every window is read from the send log in full, so none may exceed the cap. */
25
- function clampToMaxWindow(requested: Date, to: Date): DateRange {
26
+ /**
27
+ * Every window is read from the send log in full, so none may exceed the cap.
28
+ * Bounds given the wrong way round are swapped: a reversed window is a typo,
29
+ * not a request for a comparison in the future.
30
+ */
31
+ function boundedWindow(first: Date, second: Date): DateRange {
32
+ const [requested, to] =
33
+ first.getTime() <= second.getTime() ? [first, second] : [second, first];
26
34
  const earliest = to.getTime() - MAX_WINDOW_DAYS * DAY_MS;
27
35
  const from = requested.getTime() < earliest ? new Date(earliest) : requested;
28
36
  return { from, to };
@@ -33,7 +41,16 @@ export function currentRange(ctx: RangeContext): DateRange {
33
41
  const requested =
34
42
  parseDate(readQuery(ctx, "from")) ??
35
43
  new Date(to.getTime() - DEFAULT_WINDOW_DAYS * DAY_MS);
36
- return clampToMaxWindow(requested, to);
44
+ return boundedWindow(requested, to);
45
+ }
46
+
47
+ /**
48
+ * Whether the caller compares the window with another one. The DMS period
49
+ * selector says `comparison=none` when the user turned it off; a caller that
50
+ * says nothing keeps the previous period.
51
+ */
52
+ export function isComparisonWanted(ctx: RangeContext): boolean {
53
+ return readQuery(ctx, "comparison") !== NO_COMPARISON;
37
54
  }
38
55
 
39
56
  export function comparisonRange(
@@ -42,7 +59,7 @@ export function comparisonRange(
42
59
  ): DateRange {
43
60
  const from = parseDate(readQuery(ctx, "compareFrom"));
44
61
  const to = parseDate(readQuery(ctx, "compareTo"));
45
- if (from && to) return clampToMaxWindow(from, to);
62
+ if (from && to) return boundedWindow(from, to);
46
63
  const span = current.to.getTime() - current.from.getTime();
47
64
  return {
48
65
  from: new Date(current.from.getTime() - span),