@oneuptime/common 12.0.18 → 12.0.20

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 (262) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  11. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  12. package/Server/API/UserSlackAPI.ts +125 -0
  13. package/Server/EnvironmentConfig.ts +29 -107
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  19. package/Server/Infrastructure/Queue.ts +7 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +171 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Response.ts +2 -2
  40. package/Server/Utils/StartServer.ts +2 -2
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  42. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  43. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  44. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  45. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  46. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  47. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  48. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  49. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  50. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  51. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  52. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  53. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  54. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  55. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  56. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  57. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  58. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  59. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  60. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  61. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  62. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  63. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  64. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  65. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  66. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  67. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  68. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  69. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  70. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  71. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  72. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  73. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  74. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  75. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  76. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  77. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  78. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  79. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  80. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  81. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  82. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  83. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  84. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  85. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  86. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  87. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  88. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  89. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  90. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  91. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  92. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  93. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  94. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  95. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  97. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  98. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  99. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  100. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  101. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  102. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  103. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  104. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  105. package/Types/Analytics/RevenueEvent.ts +1 -0
  106. package/Types/Marketing/MarketingEvent.ts +140 -0
  107. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  108. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  109. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  110. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  111. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  112. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  113. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  114. package/UI/Components/Detail/Detail.tsx +18 -1
  115. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  116. package/UI/Utils/PermissionGate.ts +56 -0
  117. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  118. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  119. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  121. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  123. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  125. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  126. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  127. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  129. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  131. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  133. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  134. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  135. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  136. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  137. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  138. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  139. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  140. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  141. package/build/dist/Server/EnvironmentConfig.js +25 -70
  142. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  152. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  153. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  154. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  155. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  156. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  157. package/build/dist/Server/Services/Index.js +6 -2
  158. package/build/dist/Server/Services/Index.js.map +1 -1
  159. package/build/dist/Server/Services/LlmLogService.js +25 -9
  160. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  161. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  162. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  163. package/build/dist/Server/Services/ProjectService.js +144 -0
  164. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  165. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  166. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  167. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  168. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  169. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  170. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  171. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  172. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  173. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  174. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  175. package/build/dist/Server/Services/UserService.js +26 -1
  176. package/build/dist/Server/Services/UserService.js.map +1 -1
  177. package/build/dist/Server/Services/UserSlackService.js +195 -0
  178. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  179. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  180. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  181. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  182. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  183. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  184. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  185. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  186. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  187. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  188. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  190. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  192. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  193. package/build/dist/Server/Utils/Response.js +2 -2
  194. package/build/dist/Server/Utils/Response.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +2 -2
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  198. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  199. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  200. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  201. package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
  202. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  203. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  204. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  205. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  206. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  207. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  208. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  209. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  210. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  211. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  212. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  213. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  214. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  215. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  216. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  217. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  218. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  219. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  220. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  221. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  222. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  223. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  224. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  225. package/package.json +1 -1
  226. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  227. package/Server/Services/MarketingConversionService.ts +0 -10
  228. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  229. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  230. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  231. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  232. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  233. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  234. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  235. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  236. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  237. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  238. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  239. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  240. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  241. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  242. package/Types/Marketing/MarketingConversion.ts +0 -53
  243. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  244. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  245. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  246. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  247. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  248. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  249. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  250. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  251. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  252. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  253. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  254. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  255. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  256. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  257. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  258. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  259. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  260. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  261. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  262. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,533 @@
1
+ <%#
2
+ Consent Mode v2 and attribution capture, shared by every OneUptime-owned
3
+ origin that loads the Google Tag Manager container.
4
+
5
+ This lived only in Home/Views/head-basic.ejs, which meant the marketing site
6
+ denied storage by default and asked the visitor, while /accounts and
7
+ /dashboard loaded the same container with no consent signal at all and
8
+ captured nothing. Two consequences, both bad: somebody who pressed "Reject
9
+ all" on the marketing site was tracked anyway the moment they reached the
10
+ signup page — which is exactly where the conversion fires — and an ad
11
+ pointed straight at /accounts/register?gclid=... stored no click id, so
12
+ that signup was permanently unattributable.
13
+
14
+ It is one file rather than a copy per origin because the storage contract
15
+ has to stay byte-identical: the same 'cookiesAccepted' values, the same
16
+ 'clickIds' and 'firstTouch' JSON shapes. Three copies is how those stop
17
+ agreeing.
18
+
19
+ Include this BEFORE the Google Tag Manager snippet. Consent Mode defaults
20
+ are only meaningful if they are on the dataLayer before the container reads
21
+ it.
22
+
23
+ Home resolves this as ../../Common/Server/Views/Partials/AnalyticsConsent
24
+ and the App FeatureSets as ../../../../Common/Server/Views/Partials/...;
25
+ /usr/src/Common is present in both images.
26
+
27
+ Deliberately NOT here: the PostHog loader (its project key is Home's) and
28
+ the home/-prefixed page view and CTA listeners. Those are Home's alone and
29
+ stay in head-basic.ejs. Reporting on a captured touch is handed back to
30
+ the page through window.oneUptimeOnAttributionCaptured for the same
31
+ reason. The Cal booking helper does live here: it is a pure reader over
32
+ the shared attribution store, and splitting it from the store it reads
33
+ would be worse than sharing an unused function definition.
34
+ -%>
35
+ <!--
36
+ Consent.
37
+
38
+ This block runs BEFORE Google Tag Manager and before PostHog, because both
39
+ of the things it does are only meaningful ahead of them: Consent Mode's
40
+ defaults have to be on the dataLayer before the container reads it, and
41
+ PostHog must not be loaded at all until there is consent to load it.
42
+
43
+ Three states, not two. `granted` and `denied` are the visitor's explicit
44
+ choices; `unset` is the state before they have answered, and it is treated
45
+ as denied for storage purposes rather than as permission-by-silence.
46
+
47
+ What that costs, stated plainly: a visitor who never touches the banner is
48
+ not measured, and their attribution is not stored. What stops that from
49
+ losing an ad click outright is the pending-attribution buffer further
50
+ down — attribution seen on this page is held in memory and written the
51
+ moment consent is granted, so accepting the banner on the landing page
52
+ keeps everything the click carried.
53
+ -->
54
+ <% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
55
+ <!--
56
+ The dataLayer and the gtag shim, defined before both the consent signals
57
+ below and the container itself.
58
+
59
+ Deliberately inside the enableGoogleTagManager gate. A self-hosted install
60
+ loads no Google tag at all, and every OneUptime event guards on
61
+ `typeof dataLayer !== 'undefined'` — so creating the array there would turn
62
+ those no-ops into an array that grows for the life of the page and that
63
+ nothing will ever read.
64
+ -->
65
+ <script>
66
+ window.dataLayer = window.dataLayer || [];
67
+ function gtag() { dataLayer.push(arguments); }
68
+ </script>
69
+ <% } %>
70
+
71
+ <script>
72
+ window.oneUptimeConsent = (function () {
73
+ var STORAGE_KEY = 'cookiesAccepted';
74
+ var listeners = [];
75
+
76
+ function readRaw() {
77
+ try {
78
+ return window.localStorage.getItem(STORAGE_KEY);
79
+ } catch (e) {
80
+ /*
81
+ * Private mode, disabled storage, or a cross-origin sandbox.
82
+ * Unreadable storage is not consent.
83
+ */
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function state() {
89
+ var raw = readRaw();
90
+ if (raw === 'true') { return 'granted'; }
91
+ if (raw === 'false') { return 'denied'; }
92
+ return 'unset';
93
+ }
94
+
95
+ function isGranted() {
96
+ return state() === 'granted';
97
+ }
98
+
99
+ /*
100
+ * Consent Mode v2. `security_storage` is granted because it covers
101
+ * things like fraud prevention that are not analytics; everything that
102
+ * feeds advertising or measurement starts denied.
103
+ *
104
+ * wait_for_update gives the container half a second to see an update
105
+ * before it decides how to behave, which is what makes a visitor who
106
+ * accepts immediately still count.
107
+ */
108
+ function pushConsentSignal(mode, granted) {
109
+ /*
110
+ * No tag, nothing to tell. On a self-hosted install gtag is never
111
+ * defined, and consent is still tracked — it gates the attribution
112
+ * capture and PostHog either way.
113
+ */
114
+ if (typeof window.gtag !== 'function') {
115
+ return;
116
+ }
117
+
118
+ window.gtag('consent', mode, {
119
+ ad_storage: granted ? 'granted' : 'denied',
120
+ ad_user_data: granted ? 'granted' : 'denied',
121
+ ad_personalization: granted ? 'granted' : 'denied',
122
+ analytics_storage: granted ? 'granted' : 'denied',
123
+ functionality_storage: granted ? 'granted' : 'denied',
124
+ personalization_storage: granted ? 'granted' : 'denied',
125
+ security_storage: 'granted',
126
+ wait_for_update: 500
127
+ });
128
+ }
129
+
130
+ pushConsentSignal('default', false);
131
+
132
+ // An earlier visit already answered: tell Google before the tag loads.
133
+ if (isGranted()) {
134
+ pushConsentSignal('update', true);
135
+ }
136
+
137
+ function set(granted) {
138
+ try {
139
+ window.localStorage.setItem(STORAGE_KEY, granted ? 'true' : 'false');
140
+ } catch (e) {
141
+ // Nothing to persist to. The in-page decision still applies.
142
+ }
143
+
144
+ pushConsentSignal('update', granted);
145
+
146
+ for (var i = 0; i < listeners.length; i++) {
147
+ try {
148
+ listeners[i](granted);
149
+ } catch (e) {
150
+ // One bad listener must not stop the others.
151
+ }
152
+ }
153
+ }
154
+
155
+ return {
156
+ state: state,
157
+ isGranted: isGranted,
158
+ grant: function () { set(true); },
159
+ deny: function () { set(false); },
160
+ onChange: function (listener) {
161
+ if (typeof listener === 'function') {
162
+ listeners.push(listener);
163
+ }
164
+ }
165
+ };
166
+ })();
167
+ </script>
168
+
169
+ <!--
170
+ Attribution capture (UTM parameters + ad platform click IDs), and the
171
+ readers that hand it on to the two places it has to reach: the signup form
172
+ (through localStorage, same origin) and a Cal.com booking (through the
173
+ embed's metadata).
174
+
175
+ STORAGE MODEL
176
+
177
+ - Last-touch UTM values live in the individual utm* localStorage keys, read
178
+ by the signup page and stored on the User record.
179
+ - Last-touch click IDs live as a JSON object under 'clickIds'.
180
+ - The visitor's first attributed visit is preserved as JSON under
181
+ 'firstTouch' and never overwritten.
182
+
183
+ A visit counts as a new "touch" if it carries ANY utm parameter or ad click
184
+ ID — Google Ads auto-tagging sends only gclid, no UTMs.
185
+
186
+ CONSENT
187
+
188
+ Nothing is written without consent. Attribution seen before the visitor has
189
+ answered the banner is held in memory and flushed if they accept, so the
190
+ ordinary case — land on an ad, accept, sign up — keeps everything. An
191
+ explicit refusal clears anything a previous visit stored.
192
+ -->
193
+ <script>
194
+ (function () {
195
+ var UTM_KEY_MAP = {
196
+ 'utm_source': 'utmSource',
197
+ 'utm_medium': 'utmMedium',
198
+ 'utm_campaign': 'utmCampaign',
199
+ 'utm_term': 'utmTerm',
200
+ 'utm_content': 'utmContent'
201
+ };
202
+
203
+ /*
204
+ * Must stay in step with AdClickIdKeys in
205
+ * Common/Types/Marketing/Attribution.ts — the server whitelists on that
206
+ * list, so a key sent from here that is missing there is silently
207
+ * dropped on arrival.
208
+ */
209
+ var CLICK_ID_PARAMS = ['gclid', 'wbraid', 'gbraid', 'fbclid', 'msclkid', 'li_fat_id', 'twclid', 'rdt_cid'];
210
+
211
+ // Mirrors MaxAttributionValueLength on the server.
212
+ var MAX_VALUE_LENGTH = 500;
213
+
214
+ // Mirrors MAX_FIRST_TOUCH_LENGTH in App/API/CalWebhook.ts.
215
+ var MAX_FIRST_TOUCH_LENGTH = 4000;
216
+
217
+ // Attribution seen on this page but not yet allowed to be stored.
218
+ var pending = null;
219
+
220
+ function canStore() {
221
+ return Boolean(window.oneUptimeConsent && window.oneUptimeConsent.isGranted());
222
+ }
223
+
224
+ function setItem(key, value) {
225
+ try {
226
+ window.localStorage.setItem(key, value);
227
+ } catch (e) {
228
+ // Storage unavailable; attribution is best-effort.
229
+ }
230
+ }
231
+
232
+ function removeItem(key) {
233
+ try {
234
+ window.localStorage.removeItem(key);
235
+ } catch (e) {
236
+ // Nothing to remove from.
237
+ }
238
+ }
239
+
240
+ function getItem(key) {
241
+ try {
242
+ return window.localStorage.getItem(key);
243
+ } catch (e) {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ function bound(value) {
249
+ return String(value).slice(0, MAX_VALUE_LENGTH);
250
+ }
251
+
252
+ function readTouchFromUrl() {
253
+ var urlParams = new URLSearchParams(window.location.search);
254
+
255
+ var utmValues = {};
256
+ var hasUtm = false;
257
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
258
+ var value = urlParams.get(param);
259
+ if (value) {
260
+ utmValues[UTM_KEY_MAP[param]] = bound(value);
261
+ hasUtm = true;
262
+ }
263
+ });
264
+
265
+ var clickIds = {};
266
+ var hasClickId = false;
267
+ CLICK_ID_PARAMS.forEach(function (param) {
268
+ var value = urlParams.get(param);
269
+ if (value) {
270
+ clickIds[param] = bound(value);
271
+ hasClickId = true;
272
+ }
273
+ });
274
+
275
+ if (!hasUtm && !hasClickId) {
276
+ return null;
277
+ }
278
+
279
+ return {
280
+ utmValues: utmValues,
281
+ clickIds: clickIds,
282
+ hasClickId: hasClickId,
283
+ url: window.location.href,
284
+ referrer: document.referrer || '',
285
+ timestamp: new Date().toISOString()
286
+ };
287
+ }
288
+
289
+ function storeTouch(touch) {
290
+ // New touch: clear previous last-touch values, then store this visit's.
291
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
292
+ removeItem(UTM_KEY_MAP[param]);
293
+ });
294
+ Object.keys(touch.utmValues).forEach(function (key) {
295
+ setItem(key, touch.utmValues[key]);
296
+ });
297
+ setItem('utmUrl', touch.url);
298
+
299
+ if (touch.hasClickId) {
300
+ setItem('clickIds', JSON.stringify(touch.clickIds));
301
+ } else {
302
+ removeItem('clickIds');
303
+ }
304
+
305
+ // Preserve the first attributed touch forever.
306
+ if (!getItem('firstTouch')) {
307
+ var firstTouch = {};
308
+ Object.keys(touch.utmValues).forEach(function (key) {
309
+ firstTouch[key] = touch.utmValues[key];
310
+ });
311
+ if (touch.hasClickId) {
312
+ firstTouch.clickIds = touch.clickIds;
313
+ }
314
+ firstTouch.landingUrl = touch.url;
315
+ firstTouch.referrer = touch.referrer;
316
+ firstTouch.timestamp = touch.timestamp;
317
+ setItem('firstTouch', JSON.stringify(firstTouch));
318
+ }
319
+ }
320
+
321
+ function clearStoredAttribution() {
322
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
323
+ removeItem(UTM_KEY_MAP[param]);
324
+ });
325
+ removeItem('utmUrl');
326
+ removeItem('clickIds');
327
+ removeItem('firstTouch');
328
+ }
329
+
330
+ function parseStoredJson(key) {
331
+ var raw = getItem(key);
332
+ if (!raw) { return null; }
333
+ try {
334
+ var parsed = JSON.parse(raw);
335
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
336
+ } catch (e) {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ /*
342
+ * Everything known about where this visitor came from, whether it has
343
+ * been written to storage yet or is still only in memory.
344
+ *
345
+ * The in-memory touch wins: it is this page load, and the stored copy
346
+ * is at best the same thing and at worst an older visit.
347
+ */
348
+ window.oneUptimeGetAttribution = function () {
349
+ var utm = {};
350
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
351
+ var key = UTM_KEY_MAP[param];
352
+ var value = (pending && pending.utmValues[key]) || getItem(key);
353
+ if (value && value !== 'null' && value !== 'undefined') {
354
+ utm[key] = value;
355
+ }
356
+ });
357
+
358
+ var utmUrl = (pending && pending.url) || getItem('utmUrl');
359
+ if (utmUrl) {
360
+ utm.utmUrl = utmUrl;
361
+ }
362
+
363
+ var clickIds = (pending && pending.hasClickId ? pending.clickIds : null) || parseStoredJson('clickIds') || {};
364
+
365
+ var firstTouch = parseStoredJson('firstTouch');
366
+
367
+ if (!firstTouch && pending) {
368
+ firstTouch = {};
369
+ Object.keys(pending.utmValues).forEach(function (key) {
370
+ firstTouch[key] = pending.utmValues[key];
371
+ });
372
+ if (pending.hasClickId) {
373
+ firstTouch.clickIds = pending.clickIds;
374
+ }
375
+ firstTouch.landingUrl = pending.url;
376
+ firstTouch.referrer = pending.referrer;
377
+ firstTouch.timestamp = pending.timestamp;
378
+ }
379
+
380
+ return {
381
+ utm: utm,
382
+ clickIds: clickIds,
383
+ firstTouch: firstTouch
384
+ };
385
+ };
386
+
387
+ /*
388
+ * The same attribution, flattened into the scalar string map Cal.com
389
+ * accepts as booking metadata.
390
+ *
391
+ * Keys are the snake_case wire spelling the server parses in
392
+ * App/API/CalWebhook.ts. The first touch is a nested object, and Cal
393
+ * metadata values are scalars, so it travels as one JSON string under
394
+ * ou_first_touch — which is exactly what the webhook expects to parse.
395
+ *
396
+ * Returns {} when there is nothing to attribute, so a caller can spread
397
+ * it unconditionally without sending an empty metadata bag.
398
+ */
399
+ /*
400
+ * `bookingKind` says which conversation this embed books. All three
401
+ * embeds use the same Cal event type, so without it the webhook cannot
402
+ * tell a support call from an enterprise demo. It travels in the same
403
+ * bracketed metadata bag as the attribution and is read back by
404
+ * App/API/CalWebhook.ts, which allowlists the value.
405
+ */
406
+ window.oneUptimeCalAttributionMetadata = function (bookingKind) {
407
+ var attribution = window.oneUptimeGetAttribution();
408
+ var config = {};
409
+
410
+ /*
411
+ * Cal takes booking metadata as FLAT, BRACKETED keys on the embed
412
+ * config — `metadata[utm_source]` — and hands them back on the
413
+ * webhook as payload.metadata.utm_source.
414
+ *
415
+ * Not as a nested `metadata: { utm_source: ... }` object. Cal
416
+ * serialises each config value into a query parameter, so a nested
417
+ * object becomes the string "[object Object]" and every key inside
418
+ * it is lost. Nothing errors; bookings keep being recorded, they
419
+ * just arrive attributable to nothing — which is precisely the bug
420
+ * this whole path exists to fix, so getting the shape wrong here
421
+ * would have been indistinguishable from not having done the work.
422
+ *
423
+ * https://cal.com/help/embedding/prefill-booking-form-embed
424
+ */
425
+ function put(key, value, maxLength) {
426
+ if (value) {
427
+ config['metadata[' + key + ']'] =
428
+ String(value).slice(0, maxLength || MAX_VALUE_LENGTH);
429
+ }
430
+ }
431
+
432
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
433
+ put(param, attribution.utm[UTM_KEY_MAP[param]]);
434
+ });
435
+
436
+ put('utm_url', attribution.utm.utmUrl);
437
+
438
+ Object.keys(attribution.clickIds).forEach(function (param) {
439
+ put(param, attribution.clickIds[param]);
440
+ });
441
+
442
+ if (attribution.firstTouch) {
443
+ var serialized = JSON.stringify(attribution.firstTouch);
444
+ /*
445
+ * The first touch is JSON, so it gets the JSON bound, NOT the
446
+ * 500-char per-value one. A realistic first touch — five UTM
447
+ * values, a click id, and a landing URL that repeats them all
448
+ * as query parameters — runs past 500 characters easily, and
449
+ * truncating JSON produces a string the webhook's JSON.parse
450
+ * rejects. It would drop the whole first touch, silently.
451
+ *
452
+ * Mirrors MAX_FIRST_TOUCH_LENGTH in App/API/CalWebhook.ts.
453
+ */
454
+ if (serialized.length <= MAX_FIRST_TOUCH_LENGTH) {
455
+ put('ou_first_touch', serialized, MAX_FIRST_TOUCH_LENGTH);
456
+ }
457
+ }
458
+
459
+ /*
460
+ * Unconditional, unlike everything above it. The attribution keys
461
+ * describe the visitor and are absent when there is nothing to say
462
+ * about them; the booking kind describes the embed, which always
463
+ * knows what it is. A booking from a visitor who refused
464
+ * attribution storage still arrives knowing it was a demo.
465
+ */
466
+ if (bookingKind) {
467
+ config['metadata[ou_booking_kind]'] = String(bookingKind).slice(0, MAX_VALUE_LENGTH);
468
+ }
469
+
470
+ return config;
471
+ };
472
+
473
+ var touch = readTouchFromUrl();
474
+
475
+ if (touch) {
476
+ if (canStore()) {
477
+ storeTouch(touch);
478
+ } else {
479
+ /*
480
+ * Held, not dropped. A visitor who accepts the banner on this
481
+ * very page — the ordinary path for someone arriving from an
482
+ * ad — keeps the click id that brought them here.
483
+ */
484
+ pending = touch;
485
+ }
486
+ }
487
+
488
+ if (window.oneUptimeConsent) {
489
+ window.oneUptimeConsent.onChange(function (granted) {
490
+ if (granted) {
491
+ if (pending) {
492
+ storeTouch(pending);
493
+ }
494
+ return;
495
+ }
496
+
497
+ // Refused: forget what earlier visits stored, and this page's touch.
498
+ pending = null;
499
+ clearStoredAttribution();
500
+ });
501
+ }
502
+
503
+ /*
504
+ * Reporting is the including page's business, not this partial's.
505
+ *
506
+ * The marketing site emits a `home/`-prefixed PostHog event from here;
507
+ * the product origins emit nothing, and must not start emitting under
508
+ * a `home/` name just because they now share the capture code. So the
509
+ * touch is handed out and the page decides what, if anything, to do
510
+ * with it.
511
+ *
512
+ * Wire keys (utm_source), not property keys (utmSource): this is what
513
+ * arrived on the URL.
514
+ */
515
+ document.addEventListener('DOMContentLoaded', function () {
516
+ if (!touch || typeof window.oneUptimeOnAttributionCaptured !== 'function') {
517
+ return;
518
+ }
519
+
520
+ var eventProps = { utm_url: touch.url };
521
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
522
+ if (touch.utmValues[UTM_KEY_MAP[param]]) {
523
+ eventProps[param] = touch.utmValues[UTM_KEY_MAP[param]];
524
+ }
525
+ });
526
+ Object.keys(touch.clickIds).forEach(function (param) {
527
+ eventProps[param] = touch.clickIds[param];
528
+ });
529
+
530
+ window.oneUptimeOnAttributionCaptured(eventProps);
531
+ });
532
+ })();
533
+ </script>
@@ -59,7 +59,7 @@ import getJestMockFunction, { MockFunction } from "../../MockType";
59
59
  * to be tempted will add a button, not a sentence.
60
60
  *
61
61
  * - AN ADMIN READING SOMEBODY ELSE'S PHONE NUMBER, which is the same defect
62
- * one step earlier and is what this page originally did. The seven method
62
+ * one step earlier and is what this page originally did. The nine method
63
63
  * models are scoped to the person who owns the device - the columns behind
64
64
  * them are the raw number, the webhook bearer url, the push device token,
65
65
  * the telegram chat id and the verification code - and that scope was
@@ -389,8 +389,10 @@ import TeamMember from "../../../Models/DatabaseModels/TeamMember";
389
389
  import User from "../../../Models/DatabaseModels/User";
390
390
  import UserCall from "../../../Models/DatabaseModels/UserCall";
391
391
  import UserEmail from "../../../Models/DatabaseModels/UserEmail";
392
+ import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
392
393
  import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
393
394
  import UserPush from "../../../Models/DatabaseModels/UserPush";
395
+ import UserSlack from "../../../Models/DatabaseModels/UserSlack";
394
396
  import UserSMS from "../../../Models/DatabaseModels/UserSMS";
395
397
  import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
396
398
  import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
@@ -413,7 +415,7 @@ const PROJECT_ID: ObjectID = new ObjectID(PROJECT_ID_STRING);
413
415
  const SIGNED_IN_USER_ID: ObjectID = new ObjectID(SIGNED_IN_USER_ID_STRING);
414
416
 
415
417
  /*
416
- * The seven models only their owner may read. They are asserted as a SET because
418
+ * The nine models only their owner may read. They are asserted as a SET because
417
419
  * which one an admin surface reaches for hardly matters - every one of them
418
420
  * carries a raw identifier and most carry a credential.
419
421
  */
@@ -424,11 +426,13 @@ const NOTIFICATION_METHOD_MODELS: Array<unknown> = [
424
426
  UserPush,
425
427
  UserWhatsApp,
426
428
  UserTelegram,
429
+ UserSlack,
430
+ UserMicrosoftTeams,
427
431
  UserWebhook,
428
432
  ];
429
433
 
430
434
  /*
431
- * The RELATION spelling of those same seven, as they appear in a nested select
435
+ * The RELATION spelling of those same nine, as they appear in a nested select
432
436
  * on UserNotificationRule. Selecting any of them pulls a column out of the
433
437
  * method model itself - the address, the number, the handle - through a table
434
438
  * an administrator may read and which is not row-scoped for them. It is the
@@ -442,6 +446,8 @@ const METHOD_RELATION_KEYS: Array<string> = [
442
446
  "userPush",
443
447
  "userWhatsApp",
444
448
  "userTelegram",
449
+ "userSlack",
450
+ "userMicrosoftTeams",
445
451
  "userWebhook",
446
452
  ];
447
453
 
@@ -453,6 +459,8 @@ const METHOD_FOREIGN_KEYS: Array<keyof UserNotificationRule> = [
453
459
  "userPushId",
454
460
  "userWhatsAppId",
455
461
  "userTelegramId",
462
+ "userSlackId",
463
+ "userMicrosoftTeamsId",
456
464
  "userWebhookId",
457
465
  ];
458
466
 
@@ -673,7 +681,7 @@ const setUpLists: SetUpListsFunction = (
673
681
  }
674
682
 
675
683
  /*
676
- * Anything else, INCLUDING the seven notification-method models - which this
684
+ * Anything else, INCLUDING the nine notification-method models - which this
677
685
  * page must never ask for. A non-empty answer here would make a leak look
678
686
  * like a feature working, so the fallback stays empty and the assertion that
679
687
  * matters is about the request rather than the response: see "never reads a
@@ -1172,7 +1180,7 @@ afterEach(async (): Promise<void> => {
1172
1180
 
1173
1181
  /*
1174
1182
  * Unmounting stops the RENDERING, not the fetching. A rule table that belongs
1175
- * to the viewer loads the seven notification-method models one after another,
1183
+ * to the viewer loads the nine notification-method models one after another,
1176
1184
  * so a test that reads its own page leaves a chain of awaits running after its
1177
1185
  * last assertion - and every one of those calls is recorded on the same
1178
1186
  * `getListMock` the next test resets and then asserts against.
@@ -1365,14 +1373,14 @@ describe("the page is read at rest, whichever order its fetches land in", () =>
1365
1373
  assertEveryTableIsSettled();
1366
1374
  });
1367
1375
 
1368
- test("waits out the seven method reads a self-serve page makes for its owner", async () => {
1376
+ test("waits out the nine method reads a self-serve page makes for its owner", async () => {
1369
1377
  // A self-serve page is always the viewer's own, so this is the owner path.
1370
1378
  await renderSelfServePage(IncidentOnCallRules);
1371
1379
 
1372
1380
  /*
1373
- * A rule table belonging to the viewer loads the seven notification-method
1381
+ * A rule table belonging to the viewer loads the nine notification-method
1374
1382
  * models one after another, and unmounting the page does not stop that
1375
- * chain - it stops the rendering. Any of those seven still running when the
1383
+ * chain - it stops the rendering. Any of those nine still running when the
1376
1384
  * next test resets `getListMock` is recorded as that test's request, which
1377
1385
  * is how "never reads a notification method model" comes to fail on a page
1378
1386
  * that never asked for one.
@@ -1484,7 +1492,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1484
1492
  /*
1485
1493
  * The redesign, in one assertion.
1486
1494
  *
1487
- * These seven models are readable only by the person who owns the row, and
1495
+ * These nine models are readable only by the person who owns the row, and
1488
1496
  * that is the whole protection: the table scope pins every read to the
1489
1497
  * caller. Widening it so this page could fill a dropdown exposed the raw
1490
1498
  * email and phone, the webhook url, the push device token, the telegram chat
@@ -1619,7 +1627,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1619
1627
  });
1620
1628
 
1621
1629
  /*
1622
- * The seven foreign keys are mutually exclusive, and writing an SMS id into
1630
+ * The nine foreign keys are mutually exclusive, and writing an SMS id into
1623
1631
  * `userEmailId` would page an address instead of a phone without erroring
1624
1632
  * anywhere, so the whole set is asserted rather than the one column.
1625
1633
  */
@@ -1639,7 +1647,7 @@ describe("the shared rules table, as the admin page wires it", () => {
1639
1647
  /*
1640
1648
  * The failure mode this seam had to be designed around. Readiness is the
1641
1649
  * only source of methods here, so when it is down there are none to offer -
1642
- * and "then read the models directly" is not a fallback, it is seven refused
1650
+ * and "then read the models directly" is not a fallback, it is nine refused
1643
1651
  * requests that would replace the rule tables with an error page. The rules
1644
1652
  * stay editable; only the method choice is missing.
1645
1653
  */
@@ -2002,7 +2010,7 @@ describe("methods are a page of their own now, not a card on this one", () => {
2002
2010
  * It asserted that an administrator was offered NO control for adding a
2003
2011
  * notification method to somebody else's account: a masked, read-only list
2004
2012
  * and a prefilled "please add one yourself" email. That was correct for what
2005
- * the server could do at the time - the seven method models are scoped to
2013
+ * the server could do at the time - the nine method models are scoped to
2006
2014
  * their owner and the attempt to widen them leaked every raw column behind
2007
2015
  * them - but it left the commonest broken responder, a new joiner with no
2008
2016
  * method at all, fixable by nobody but themselves.
@@ -2167,7 +2175,7 @@ describe("the coverage grid, now shared with the readiness page", () => {
2167
2175
  * The edit path.
2168
2176
  *
2169
2177
  * `isEditable` was hardcoded false on the ModelTable, so this page issued no
2170
- * PATCH at all: the phase opened `notifyAfterMinutes` (and the seven method
2178
+ * PATCH at all: the phase opened `notifyAfterMinutes` (and the nine method
2171
2179
  * foreign keys) to update behind the largest guard in the codebase, and bought
2172
2180
  * zero capability anybody could reach. These assertions are about the two halves
2173
2181
  * of making that real - the editor being on for exactly the viewers allowed to
@@ -2225,7 +2233,7 @@ describe("the rules are editable, not merely addable and removable", () => {
2225
2233
 
2226
2234
  /*
2227
2235
  * The dropdown is an override field: its value travels in `miscDataProps`
2228
- * and is mapped onto one of the seven foreign keys by `onBeforeCreate`.
2236
+ * and is mapped onto one of the nine foreign keys by `onBeforeCreate`.
2229
2237
  * ModelForm calls that hook only for FormType.Create and BaseAPI.updateItem
2230
2238
  * never reads `miscDataProps`, so on the edit path the choice is discarded
2231
2239
  * in transit. Rendering it anyway would give an administrator a control