@antelopejs/dms-mailing 0.3.4 → 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 +2 -2
  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
@@ -6,57 +6,107 @@ import {
6
6
  Notification,
7
7
  } from "@antelopejs/interface-dms/notifications";
8
8
  import type { NotificationData } from "@antelopejs/interface-dms/notifications/types";
9
+ import { type SendSummary, SendModel } from "../db";
9
10
  import { PROBLEM_STATUSES, type SendEventType } from "../types";
10
11
 
11
- export interface BounceNotificationInput {
12
+ /** The problem event that may tip a template over the alert threshold. */
13
+ export interface ProblemNotificationInput {
12
14
  sendId: string;
13
15
  templateSlug: string;
14
- recipientEmail: string;
15
16
  type: SendEventType;
17
+ isTest: boolean;
16
18
  }
17
19
 
18
- export interface IdempotentNotificationTarget {
19
- toUsersIdempotently(userIds: string[], idempotencyKey: string): Promise<void>;
20
+ /** A template's real sends of the last hour, and how many had a problem. */
21
+ export interface ProblemWindow {
22
+ sends: number;
23
+ problems: number;
20
24
  }
21
25
 
22
- export interface BounceNotificationData extends NotificationData {
26
+ export interface ProblemNotificationData extends NotificationData {
23
27
  linkTo: string;
24
28
  params: Record<string, string | number>;
25
29
  }
26
30
 
27
- const SENDS_PROBLEMS_LINK = "/modules/mailing/sends?tab=problems";
31
+ /** Below this many problems in the hour a template is not worth a ping. */
32
+ export const ALERT_MIN_PROBLEMS = 3;
33
+ /** Share of the hour's sends, in percent, that must have had a problem. */
34
+ export const ALERT_MIN_RATE = 5;
35
+ export const ALERT_WINDOW_MS = 3_600_000;
36
+
37
+ const PERCENT = 100;
38
+ const SENDS_PAGE = "/modules/mailing/sends";
39
+ const PROBLEMS_VIEW = "problems";
28
40
  const IDEMPOTENCY_KEY_SEPARATOR = ":";
29
- const BOUNCE_ICON = "i-ph-warning";
30
- const BOUNCE_TITLE = "$dms_mailing.notifications.bounce_title";
31
- const BOUNCE_DESCRIPTION = "$dms_mailing.notifications.bounce_description";
41
+ // The DMS keys notifications across every module: ours carry the module's name.
42
+ const IDEMPOTENCY_KEY_PREFIX = "mailing:problems";
43
+ const PROBLEMS_ICON = "i-ph-warning";
44
+ const PROBLEMS_TITLE = "$dms_mailing.notifications.problems_title";
45
+ const PROBLEMS_DESCRIPTION = "$dms_mailing.notifications.problems_description";
46
+ /** Only a real send's problem counts: a test bouncing is the tester's business. */
47
+ export function shouldNotify(input: ProblemNotificationInput): boolean {
48
+ return PROBLEM_STATUSES.includes(input.type) && !input.isTest;
49
+ }
32
50
 
33
- export function shouldNotify(type: SendEventType): boolean {
34
- return PROBLEM_STATUSES.includes(type);
51
+ export function problemRate(window: ProblemWindow): number {
52
+ return window.sends
53
+ ? Math.round((window.problems * PERCENT) / window.sends)
54
+ : 0;
35
55
  }
36
56
 
37
- export function bounceNotification(
38
- input: BounceNotificationInput,
39
- ): BounceNotificationData {
57
+ /** Whether a template's last hour went wrong often enough to tell its owners. */
58
+ export function crossesThreshold(window: ProblemWindow): boolean {
59
+ return (
60
+ window.problems >= ALERT_MIN_PROBLEMS &&
61
+ problemRate(window) >= ALERT_MIN_RATE
62
+ );
63
+ }
64
+
65
+ export function problemWindowOf(sends: SendSummary[]): ProblemWindow {
66
+ const real = sends.filter((send) => !send.isTest);
40
67
  return {
41
- icon: BOUNCE_ICON,
42
- title: BOUNCE_TITLE,
43
- description: BOUNCE_DESCRIPTION,
44
- subject: GeneralSubject,
45
- linkTo: SENDS_PROBLEMS_LINK,
46
- params: { slug: input.templateSlug, email: input.recipientEmail },
68
+ sends: real.length,
69
+ problems: real.filter((send) => PROBLEM_STATUSES.includes(send.status))
70
+ .length,
47
71
  };
48
72
  }
49
73
 
50
- export function bounceIdempotencyKey(input: BounceNotificationInput): string {
51
- return `${input.sendId}${IDEMPOTENCY_KEY_SEPARATOR}${input.type}`;
74
+ /** The send log on the Problems tab, the drawer open on the send that tipped it. */
75
+ export function problemsLink(sendId: string): string {
76
+ const query = new URLSearchParams({ view: PROBLEMS_VIEW, record: sendId });
77
+ return `${SENDS_PAGE}?${query.toString()}`;
52
78
  }
53
79
 
54
- export function deliverBounceNotification(
55
- target: IdempotentNotificationTarget,
56
- userIds: string[],
57
- input: BounceNotificationInput,
58
- ): Promise<void> {
59
- return target.toUsersIdempotently(userIds, bounceIdempotencyKey(input));
80
+ export function problemsNotification(
81
+ input: ProblemNotificationInput,
82
+ window: ProblemWindow,
83
+ ): ProblemNotificationData {
84
+ return {
85
+ icon: PROBLEMS_ICON,
86
+ title: PROBLEMS_TITLE,
87
+ description: PROBLEMS_DESCRIPTION,
88
+ subject: GeneralSubject,
89
+ linkTo: problemsLink(input.sendId),
90
+ params: {
91
+ count: window.problems,
92
+ slug: input.templateSlug,
93
+ rate: problemRate(window),
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * One notification per template and clock hour, however many problems follow:
100
+ * the DMS keeps the first delivery under a key and ignores the later ones.
101
+ */
102
+ export function problemsIdempotencyKey(
103
+ input: ProblemNotificationInput,
104
+ at: Date,
105
+ ): string {
106
+ const hour = Math.floor(at.getTime() / ALERT_WINDOW_MS);
107
+ return [IDEMPOTENCY_KEY_PREFIX, input.templateSlug, hour].join(
108
+ IDEMPOTENCY_KEY_SEPARATOR,
109
+ );
60
110
  }
61
111
 
62
112
  async function tenantOwnerIds(tenantId: string): Promise<string[]> {
@@ -64,27 +114,59 @@ async function tenantOwnerIds(tenantId: string): Promise<string[]> {
64
114
  return owners.map((owner) => owner.userId);
65
115
  }
66
116
 
67
- export async function notifyBounce(
117
+ async function lastHourOf(
118
+ tenantId: string,
119
+ templateSlug: string,
120
+ now: Date,
121
+ ): Promise<ProblemWindow> {
122
+ const since = new Date(now.getTime() - ALERT_WINDOW_MS);
123
+ const sends = await GetModel(SendModel, tenantId).listForTemplate(
124
+ templateSlug,
125
+ since,
126
+ );
127
+ return problemWindowOf(sends);
128
+ }
129
+
130
+ async function sendProblemsNotification(
131
+ tenantId: string,
132
+ input: ProblemNotificationInput,
133
+ now: Date,
134
+ ): Promise<void> {
135
+ const window = await lastHourOf(tenantId, input.templateSlug, now);
136
+ if (!crossesThreshold(window)) return;
137
+ const userIds = await tenantOwnerIds(tenantId);
138
+ if (userIds.length === 0) return;
139
+ const data = problemsNotification(input, window);
140
+ const notification = Notification()
141
+ .icon(data.icon)
142
+ .title(data.title)
143
+ .description(data.description)
144
+ .subject(data.subject)
145
+ .linkTo(data.linkTo)
146
+ .params(data.params)
147
+ .build();
148
+ await notification.toUsersIdempotently(
149
+ userIds,
150
+ problemsIdempotencyKey(input, now),
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Tells the tenant's owners once a template's real sends keep failing: one
156
+ * aggregated notification per template and hour, past a threshold, never one
157
+ * per bounce.
158
+ */
159
+ export async function notifyProblems(
68
160
  tenantId: string,
69
- input: BounceNotificationInput,
161
+ input: ProblemNotificationInput,
162
+ now: Date = new Date(),
70
163
  ): Promise<void> {
71
- if (!shouldNotify(input.type)) return;
164
+ if (!shouldNotify(input)) return;
72
165
  try {
73
- const userIds = await tenantOwnerIds(tenantId);
74
- if (userIds.length === 0) return;
75
- const data = bounceNotification(input);
76
- const notification = Notification()
77
- .icon(data.icon)
78
- .title(data.title)
79
- .description(data.description)
80
- .subject(data.subject)
81
- .linkTo(data.linkTo)
82
- .params(data.params)
83
- .build();
84
- await deliverBounceNotification(notification, userIds, input);
166
+ await sendProblemsNotification(tenantId, input, now);
85
167
  } catch (error) {
86
168
  Logging.Error(
87
- `[dms-mailing] Failed to notify the bounce of "${input.recipientEmail}": ${String(error)}`,
169
+ `[dms-mailing] Failed to notify the problems of "${input.templateSlug}": ${String(error)}`,
88
170
  );
89
171
  }
90
172
  }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ BannerAction,
3
+ BannerContent,
4
+ } from "@antelopejs/interface-dms/base";
5
+ import type { ComposedText } from "@antelopejs/interface-dms/base/types";
6
+ import { API_BASE_PATH } from "../constants";
7
+ import { MAX_BULK_REPLAY } from "./send-stats";
8
+
9
+ /** What the provider banner is computed from. */
10
+ export interface ProviderHealth {
11
+ /** Empty when no provider is connected. */
12
+ providerName: string;
13
+ providerReachable: boolean;
14
+ /** Provider failures since `since`, the ones a send again may fix. */
15
+ recentFailures: number;
16
+ /** Of those, the ones where the provider could not be reached at all. */
17
+ transportFailures: number;
18
+ /**
19
+ * Of those, the ones whose address is on the suppression list: a send
20
+ * again leaves them out, nothing would reach it.
21
+ */
22
+ suppressedFailures: number;
23
+ /** Real sends since `since`, the base of the failure rate. */
24
+ recentSends: number;
25
+ since: Date;
26
+ }
27
+
28
+ /** Fewer failures than this in the hour are a recipient's, not the provider's. */
29
+ export const FAILING_MIN_COUNT = 3;
30
+ /** Share of the hour's sends, in percent, that must have failed. */
31
+ export const FAILING_MIN_RATE = 10;
32
+
33
+ const PERCENT = 100;
34
+
35
+ type BannerState = "missing" | "unreachable" | "failing";
36
+
37
+ const KEY = "$dms_mailing.sends.banner.";
38
+ const SETTINGS_PROVIDER_PAGE = "/modules/mailing/settings#provider";
39
+
40
+ const TITLE_KEYS: Record<BannerState, string> = {
41
+ missing: "missing_title",
42
+ unreachable: "unreachable_title",
43
+ failing: "failures_title",
44
+ };
45
+
46
+ const key = (path: string): string => `${KEY}${path}`;
47
+
48
+ function bannerState(health: ProviderHealth): BannerState {
49
+ if (health.providerReachable) return "failing";
50
+ return health.providerName ? "unreachable" : "missing";
51
+ }
52
+
53
+ const providerParam = (health: ProviderHealth): ComposedText | string =>
54
+ health.providerName || { key: key("the_provider") };
55
+
56
+ function description(health: ProviderHealth, state: BannerState): ComposedText {
57
+ if (!health.recentFailures) return { key: key(`${state}_description`) };
58
+ return {
59
+ key: key("failures_description"),
60
+ params: {
61
+ count: { type: "count", value: health.recentFailures },
62
+ provider: providerParam(health),
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Whether the failures add up to a provider problem: any connection failure,
69
+ * or enough failures, and a large enough share of the hour's sends. A single
70
+ * recipient's rejection is told on its own row, not here.
71
+ */
72
+ export function isProviderFailing(health: ProviderHealth): boolean {
73
+ if (health.transportFailures) return true;
74
+ const rate = health.recentSends
75
+ ? (health.recentFailures * PERCENT) / health.recentSends
76
+ : 0;
77
+ return health.recentFailures >= FAILING_MIN_COUNT && rate >= FAILING_MIN_RATE;
78
+ }
79
+
80
+ /** The failures a send again would send: not those to a suppressed address. */
81
+ const sendableFailures = (health: ProviderHealth): number =>
82
+ Math.max(health.recentFailures - health.suppressedFailures, 0);
83
+
84
+ function confirmTexts(total: number) {
85
+ const count = Math.min(total, MAX_BULK_REPLAY);
86
+ const isCapped = total > count;
87
+ return {
88
+ description: key(
89
+ isCapped ? "confirm.description_capped" : "confirm.description",
90
+ ),
91
+ params: isCapped ? { count, total } : { count },
92
+ };
93
+ }
94
+
95
+ /** The failures left out, listed in the dialog so its count adds up. */
96
+ function confirmImpact(health: ProviderHealth) {
97
+ if (!health.suppressedFailures) return {};
98
+ return {
99
+ impact: [
100
+ {
101
+ icon: "i-ph-prohibit",
102
+ label: key("confirm.suppressed"),
103
+ count: health.suppressedFailures,
104
+ },
105
+ ],
106
+ };
107
+ }
108
+
109
+ function replayAction(health: ProviderHealth): BannerAction {
110
+ return {
111
+ label: key("send_again"),
112
+ icon: "i-ph-arrow-clockwise",
113
+ color: "error",
114
+ target: {
115
+ type: "api",
116
+ url: `${API_BASE_PATH}/sends/replay-failed`,
117
+ method: "POST",
118
+ body: { since: health.since.toISOString() },
119
+ successMessage: key("replayed"),
120
+ },
121
+ confirm: {
122
+ title: key("confirm.title"),
123
+ ...confirmTexts(sendableFailures(health)),
124
+ ...confirmImpact(health),
125
+ color: "warning",
126
+ icon: "i-ph-arrow-clockwise",
127
+ confirmLabel: key("confirm.confirm"),
128
+ cancelLabel: key("confirm.cancel"),
129
+ },
130
+ };
131
+ }
132
+
133
+ function actions(health: ProviderHealth): BannerAction[] {
134
+ const settings: BannerAction = {
135
+ label: key("settings"),
136
+ to: SETTINGS_PROVIDER_PAGE,
137
+ variant: "outline",
138
+ color: "neutral",
139
+ };
140
+ return sendableFailures(health)
141
+ ? [settings, replayAction(health)]
142
+ : [settings];
143
+ }
144
+
145
+ /**
146
+ * The send log's provider banner: `null` while the provider answers and its
147
+ * failures do not add up to a provider problem, otherwise what is wrong and
148
+ * the way out.
149
+ */
150
+ export function buildProviderBanner(
151
+ health: ProviderHealth,
152
+ ): BannerContent | null {
153
+ if (health.providerReachable && !isProviderFailing(health)) return null;
154
+ const state = bannerState(health);
155
+ return {
156
+ tone: "error",
157
+ icon: "i-ph-plugs",
158
+ title: {
159
+ key: key(TITLE_KEYS[state]),
160
+ params: { provider: providerParam(health) },
161
+ },
162
+ description: description(health, state),
163
+ actions: actions(health),
164
+ };
165
+ }
@@ -23,6 +23,7 @@ export async function renderEmailHtml(
23
23
  ): Promise<string> {
24
24
  if (!getConfig().useHtmlRender) return renderFallbackHtml(email, locale);
25
25
  try {
26
+ // The DMS renderer already drops Vue's SSR markers (dms-frontend 0.5.4).
26
27
  return await GenerateHtml(MailingEmailTemplate, { locale, email });
27
28
  } catch (error) {
28
29
  Logging.Warn(
@@ -0,0 +1,160 @@
1
+ import type { StatGroupItem } from "@antelopejs/interface-dms/base";
2
+ import type {
3
+ BlockText,
4
+ ComposedText,
5
+ } from "@antelopejs/interface-dms/base/types";
6
+ import type { SendStats } from "./send-stats";
7
+
8
+ /** Past this wait the oldest queued send is worth a warning. */
9
+ export const QUEUE_WARNING_MS = 10 * 60_000;
10
+ /** Past this latency the figure switches to seconds. */
11
+ export const SLOW_LATENCY_MS = 5000;
12
+
13
+ const KEY = "$dms_mailing.sends.stats.";
14
+ const PERCENT = 100;
15
+ const MS_PER_SECOND = 1000;
16
+ const EMPTY_FIGURE = "—";
17
+ const PROBLEMS_PAGE = "/modules/mailing/sends?view=problems";
18
+ const PROBLEM_PARTS = ["bounced", "failed", "spam"] as const;
19
+ const THE_PROVIDER: ComposedText = {
20
+ key: "$dms_mailing.sends.banner.the_provider",
21
+ };
22
+
23
+ const key = (path: string): string => `${KEY}${path}`;
24
+
25
+ const text = (path: string, params?: ComposedText["params"]): ComposedText =>
26
+ params ? { key: key(path), params } : { key: key(path) };
27
+
28
+ const count = (value: number) => ({ type: "count" as const, value });
29
+
30
+ const percent = (rate: number) => ({
31
+ type: "number" as const,
32
+ value: Math.round(rate) / PERCENT,
33
+ format: "percent" as const,
34
+ });
35
+
36
+ /** A list of texts joined as one line ("▲ 8 % · + 3 tests, not counted"). */
37
+ function joined(parts: ComposedText[], fallback: ComposedText): BlockText {
38
+ if (!parts.length) return fallback;
39
+ return parts.reduce((left, right) => text("joined", { left, right }));
40
+ }
41
+
42
+ function deltaText(delta: number | null): ComposedText[] {
43
+ if (!delta) return [];
44
+ return [
45
+ text(delta > 0 ? "delta_up" : "delta_down", {
46
+ delta: percent(Math.abs(delta)),
47
+ }),
48
+ ];
49
+ }
50
+
51
+ function sendsItem(stats: SendStats): StatGroupItem {
52
+ const tests = stats.tests
53
+ ? [text("tests_excluded", { count: count(stats.tests) })]
54
+ : [];
55
+ return {
56
+ id: "sends",
57
+ icon: "i-ph-paper-plane-tilt",
58
+ eyebrow: key("sends"),
59
+ value: stats.sends,
60
+ detail: joined(
61
+ [...deltaText(stats.sendsDelta), ...tests],
62
+ text("no_tests"),
63
+ ),
64
+ };
65
+ }
66
+
67
+ function deliveredItem(stats: SendStats, provider: string): StatGroupItem {
68
+ if (stats.isDeliveryUntracked) {
69
+ return {
70
+ id: "delivered",
71
+ icon: "i-ph-check-circle",
72
+ eyebrow: key("delivered"),
73
+ value: EMPTY_FIGURE,
74
+ detail: text("not_tracked", { provider: provider || THE_PROVIDER }),
75
+ };
76
+ }
77
+ return {
78
+ id: "delivered",
79
+ icon: "i-ph-check-circle",
80
+ eyebrow: key("delivered"),
81
+ value:
82
+ stats.sends && stats.deliverability !== null
83
+ ? text("rate", { rate: percent(stats.deliverability) })
84
+ : EMPTY_FIGURE,
85
+ detail: text("delivered_of", {
86
+ delivered: stats.delivered,
87
+ sends: stats.sends,
88
+ }),
89
+ };
90
+ }
91
+
92
+ function problemsItem(stats: SendStats): StatGroupItem {
93
+ const parts = PROBLEM_PARTS.filter((part) => stats[part]).map((part) =>
94
+ text(`breakdown.${part}`, { count: count(stats[part]) }),
95
+ );
96
+ return {
97
+ id: "problems",
98
+ icon: "i-ph-warning-octagon",
99
+ tone: stats.problems ? "error" : undefined,
100
+ eyebrow: key("problems"),
101
+ value: stats.problems,
102
+ detail: joined(parts, text("no_problems")),
103
+ to: stats.problems ? PROBLEMS_PAGE : undefined,
104
+ };
105
+ }
106
+
107
+ function queuedItem(stats: SendStats, now: number): StatGroupItem {
108
+ const oldest = stats.oldestQueuedAt;
109
+ const waited = oldest ? now - new Date(oldest).getTime() : 0;
110
+ return {
111
+ id: "queued",
112
+ icon: "i-ph-clock",
113
+ eyebrow: key("queued"),
114
+ value: stats.queued,
115
+ detail: oldest
116
+ ? text("oldest_waiting", { time: { type: "relative", value: oldest } })
117
+ : text("nothing_waiting"),
118
+ detailTone: waited > QUEUE_WARNING_MS ? "warning" : undefined,
119
+ };
120
+ }
121
+
122
+ /** The median latency in milliseconds, or in seconds past five seconds. */
123
+ export function latencyText(ms: number): ComposedText {
124
+ if (ms < SLOW_LATENCY_MS) return text("latency_ms", { value: ms });
125
+ return text("latency_s", {
126
+ value: { type: "number", value: ms / MS_PER_SECOND, format: "decimal" },
127
+ });
128
+ }
129
+
130
+ function latencyItem(stats: SendStats, provider: string): StatGroupItem {
131
+ return {
132
+ id: "latency",
133
+ icon: "i-ph-timer",
134
+ eyebrow: key("latency"),
135
+ value: stats.sends ? latencyText(stats.latencyMedian) : EMPTY_FIGURE,
136
+ detail: provider
137
+ ? text("latency_detail", { provider })
138
+ : text("latency_detail_generic"),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * The five cells of the send log's stat strip, as `StatGroup` items whose
144
+ * texts the DMS composes in the reader's language: sends with the tests left
145
+ * out, deliverability, problems with their breakdown, the queue with its
146
+ * oldest wait, and the median API latency.
147
+ */
148
+ export function buildSendStatItems(
149
+ stats: SendStats,
150
+ provider: string,
151
+ now: number = Date.now(),
152
+ ): StatGroupItem[] {
153
+ return [
154
+ sendsItem(stats),
155
+ deliveredItem(stats, provider),
156
+ problemsItem(stats),
157
+ queuedItem(stats, now),
158
+ latencyItem(stats, provider),
159
+ ];
160
+ }
@@ -0,0 +1,122 @@
1
+ import type { ProviderCapabilities } from "@antelopejs/interface-email";
2
+ import type { SendSummary } from "../db";
3
+ import { isResolvedStage, type SendStatus } from "../types";
4
+ import {
5
+ aggregate,
6
+ deltaPercent,
7
+ isDeliveryUntracked,
8
+ type ProviderTracking,
9
+ } from "./metrics";
10
+
11
+ /** The figures of the send log's stat strip, for one period. */
12
+ export interface SendStats {
13
+ sends: number;
14
+ /** `null` when the previous period had no send to compare with. */
15
+ sendsDelta: number | null;
16
+ /** Test sends of the period, shown next to the count but not in it. */
17
+ tests: number;
18
+ delivered: number;
19
+ /** `null` without a real send in the period. */
20
+ deliverability: number | null;
21
+ /**
22
+ * The provider reports no delivery (SMTP) and no webhook event came back:
23
+ * every send stays "sent", and a deliverability would mislead.
24
+ */
25
+ isDeliveryUntracked: boolean;
26
+ /**
27
+ * Problems nobody dealt with yet, like the Problems tab and the nav badge: a
28
+ * send again resolves its original. The breakdown counts the same sends.
29
+ */
30
+ problems: number;
31
+ bounced: number;
32
+ failed: number;
33
+ spam: number;
34
+ queued: number;
35
+ /** When the oldest send still queued was made; `null` with none queued. */
36
+ oldestQueuedAt: string | null;
37
+ latencyMedian: number;
38
+ }
39
+
40
+ const isQueued = (send: SendSummary): boolean => send.status === "queued";
41
+
42
+ function oldestQueuedAt(sends: SendSummary[]): string | null {
43
+ const queued = sends
44
+ .filter(isQueued)
45
+ .map((send) => new Date(send.createdAt).getTime());
46
+ if (!queued.length) return null;
47
+ return new Date(Math.min(...queued)).toISOString();
48
+ }
49
+
50
+ /** What the provider says it tracks; `null` while it cannot be reached. */
51
+ export function trackingOf(
52
+ capabilities: ProviderCapabilities | null,
53
+ ): ProviderTracking | null {
54
+ if (!capabilities) return null;
55
+ return {
56
+ name: capabilities.name,
57
+ openTracking: capabilities.features.openTracking,
58
+ clickTracking: capabilities.features.clickTracking,
59
+ };
60
+ }
61
+
62
+ type ProblemBreakdown = Pick<
63
+ SendStats,
64
+ "problems" | "bounced" | "failed" | "spam"
65
+ >;
66
+
67
+ function problemBreakdown(real: SendSummary[]): ProblemBreakdown {
68
+ const open = real.filter((send) => !isResolvedStage(send.stage));
69
+ const countOf = (status: SendStatus): number =>
70
+ open.filter((send) => send.status === status).length;
71
+ const bounced = countOf("bounced");
72
+ const failed = countOf("failed");
73
+ const spam = countOf("spam");
74
+ return { problems: bounced + failed + spam, bounced, failed, spam };
75
+ }
76
+
77
+ /** `previous` is `null` when the user turned the comparison off. */
78
+ export function buildSendStats(
79
+ current: SendSummary[],
80
+ previous: SendSummary[] | null,
81
+ tracking: ProviderTracking | null = null,
82
+ ): SendStats {
83
+ const real = current.filter((send) => !send.isTest);
84
+ const totals = aggregate(real);
85
+ const previousTotals = previous
86
+ ? aggregate(previous.filter((send) => !send.isTest))
87
+ : null;
88
+ return {
89
+ sends: totals.sends,
90
+ sendsDelta: deltaPercent(totals.sends, previousTotals?.sends ?? null),
91
+ tests: current.length - real.length,
92
+ delivered: totals.delivered,
93
+ deliverability: totals.deliverability,
94
+ isDeliveryUntracked: isDeliveryUntracked(real, tracking),
95
+ ...problemBreakdown(real),
96
+ queued: totals.queued,
97
+ oldestQueuedAt: oldestQueuedAt(real),
98
+ latencyMedian: totals.latencyMedian,
99
+ };
100
+ }
101
+
102
+ /** At most this many failures go again in one "send the failures again". */
103
+ export const MAX_BULK_REPLAY = 100;
104
+
105
+ /** An error of the connection to the provider rather than of one recipient. */
106
+ const TRANSPORT_ERROR =
107
+ /ECONN|ETIMEDOUT|EHOSTUNREACH|ENOTFOUND|EAI_AGAIN|ESOCKET|timed? ?out|connection/i;
108
+
109
+ /**
110
+ * Provider failures worth a send again: failed, not a bounce, not a test, and
111
+ * not already sent again by someone.
112
+ */
113
+ export function isProviderFailure(send: SendSummary): boolean {
114
+ return (
115
+ send.status === "failed" && !send.isTest && !isResolvedStage(send.stage)
116
+ );
117
+ }
118
+
119
+ /** A provider failure that says the provider could not be reached at all. */
120
+ export function isTransportFailure(send: SendSummary): boolean {
121
+ return isProviderFailure(send) && TRANSPORT_ERROR.test(send.error ?? "");
122
+ }