@oneuptime/common 13.0.8 → 14.0.1

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 (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -26,6 +26,8 @@ import ExternalStatusPageMonitorCriteria from "./Criteria/ExternalStatusPageMoni
26
26
  import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
27
27
  import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
28
28
  import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
29
+ import AffectedResourceList from "./AffectedResourceList";
30
+ import RootCauseList from "./RootCauseList";
29
31
  import MonitorCriteriaInstance from "../../../Types/Monitor/MonitorCriteriaInstance";
30
32
  import MonitorStep from "../../../Types/Monitor/MonitorStep";
31
33
  import FilterCondition from "../../../Types/Filter/FilterCondition";
@@ -1113,9 +1115,7 @@ ${contextBlock}
1113
1115
  samples: breachingSamples,
1114
1116
  totalSamples: ctx.totalSamplesInWindow,
1115
1117
  unit: ctx.unit,
1116
- metricName: ctx.metricName,
1117
1118
  unitHeuristicMetricName: unitHeuristicMetricName,
1118
- alias: ctx.alias,
1119
1119
  components: ctx.components || [],
1120
1120
  })}`);
1121
1121
  }
@@ -1151,24 +1151,41 @@ ${contextBlock}
1151
1151
  return sections.join("\n");
1152
1152
  }
1153
1153
  /**
1154
- * Build the **Breaching Samples** markdown section — a table of
1155
- * timestamps, values, and any group-by attributes. Caps the row count
1156
- * so a 30-minute window at 1-second granularity doesn't dump thousands
1157
- * of lines onto the root-cause page; the caller can always drill in
1158
- * via the metric explorer link.
1154
+ * Build the **Breaching Samples** markdown section — a numbered list of
1155
+ * the samples that breached, oldest first. Caps the list so a 30-minute
1156
+ * window at 1-second granularity doesn't dump thousands of lines onto
1157
+ * the root-cause page; the caller can always drill in via the metric
1158
+ * explorer link.
1159
+ *
1160
+ * 1. `2026-08-14T10:30:00.000Z` — **1.07 GB**
1161
+ * - `a`: 537 MB
1162
+ * - `b`: 1.5 sec
1163
+ * - `k8s.pod.name`: `web-1`
1164
+ *
1165
+ * It used to be a table — Timestamp | Metric | Alias | Value, a column
1166
+ * per formula component and one per attribute key — which grew wider
1167
+ * with every attribute and read badly in the email card, in Slack and on
1168
+ * the dashboard (see RootCauseList). Its Metric and Alias columns
1169
+ * repeated the same two values on every row, the ones the Metric
1170
+ * Details list directly above already states, so the list leaves them
1171
+ * out; and a component or attribute a sample has no value for is left
1172
+ * out rather than printed as "-".
1159
1173
  *
1160
1174
  * Timestamps are emitted as inline code wrapping ISO 8601 strings so
1161
1175
  * the client-side markdown viewer can localize them to the viewer's
1162
1176
  * timezone (without losing the canonical instant).
1163
1177
  */
1164
1178
  static formatBreachingSamplesSection(input) {
1165
- const MAX_ROWS = 20;
1179
+ const MAX_SAMPLES_SHOWN = 20;
1166
1180
  // Sort chronologically and de-duplicate any accidental repeats
1167
1181
  const sorted = [...input.samples].sort((a, b) => {
1168
1182
  return (new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1169
1183
  });
1170
- const displayedSamples = sorted.slice(0, MAX_ROWS);
1171
- // Collect attribute keys that appear on any displayed sample
1184
+ const displayedSamples = sorted.slice(0, MAX_SAMPLES_SHOWN);
1185
+ /*
1186
+ * Collect attribute keys that appear on any displayed sample, so every
1187
+ * item lists the attributes it has in the same order.
1188
+ */
1172
1189
  const attrKeySet = new Set();
1173
1190
  for (const s of displayedSamples) {
1174
1191
  for (const k of Object.keys(s.attributes || {})) {
@@ -1176,94 +1193,64 @@ ${contextBlock}
1176
1193
  }
1177
1194
  }
1178
1195
  const attrKeys = Array.from(attrKeySet);
1179
- /*
1180
- * Escape pipe characters that could appear in the metric display
1181
- * name (formulas like "a | b" are unlikely but possible) so they
1182
- * don't break GitHub-flavored-markdown tables.
1183
- */
1184
- const escapeCell = (value) => {
1185
- return value.replace(/\|/g, "\\|");
1186
- };
1187
- /*
1188
- * Column layout:
1189
- * Timestamp | Metric | Alias | Value | <component_1> | ... | <attr_1> | ...
1190
- *
1191
- * The component columns let the reader see what each variable of a
1192
- * formula resolved to at the breach time — e.g. "when c = a + b
1193
- * breached 100, a was 55, b was 46". They are omitted for plain
1194
- * metric criteria.
1195
- */
1196
- const headerCells = [
1197
- "Timestamp",
1198
- "Metric",
1199
- "Alias",
1200
- "Value",
1201
- ];
1202
- /*
1203
- * The component columns carry no unit in their header any more. Each
1204
- * cell now names the unit it actually landed on, and auto-scaling
1205
- * means neighbouring rows can land on different ones — a header that
1206
- * read "a (By)" over cells reading "900 KB" and "1.2 MB" states a
1207
- * unit that neither row uses. The configured unit is not lost: the
1208
- * Components bullet list above the table still records it.
1209
- */
1210
- for (const component of input.components) {
1211
- headerCells.push(component.alias);
1212
- }
1213
- headerCells.push(...attrKeys);
1214
- const headerRow = `| ${headerCells.join(" | ")} |`;
1215
- const dividerRow = `| ${headerCells
1216
- .map(() => {
1217
- return "---";
1218
- })
1219
- .join(" | ")} |`;
1220
- const metricCell = `\`${escapeCell(input.metricName)}\``;
1221
- const aliasCell = input.alias
1222
- ? `\`${escapeCell(input.alias)}\``
1223
- : "-";
1224
- const dataRows = displayedSamples.map((s) => {
1225
- const timestampIso = new Date(s.timestamp).toISOString();
1226
- const cells = [
1227
- `\`${timestampIso}\``,
1228
- metricCell,
1229
- aliasCell,
1230
- MetricValueFormatter.format({
1231
- value: s.value,
1232
- unit: input.unit,
1233
- metricName: input.unitHeuristicMetricName,
1234
- }),
1235
- ];
1196
+ const items = displayedSamples.map((s) => {
1197
+ const details = [];
1198
+ /*
1199
+ * What each variable of a formula resolved to at the breach time —
1200
+ * e.g. "when c = a + b breached 100, a was 55, b was 46". Absent
1201
+ * for plain metric criteria.
1202
+ */
1236
1203
  for (const component of input.components) {
1237
1204
  const match = (s.componentValues || []).find((cv) => {
1238
1205
  return cv.alias === component.alias;
1239
1206
  });
1240
- if (match && typeof match.value === "number") {
1241
- /*
1242
- * Each component keeps its OWN unit. Component values are
1243
- * indexed off the per-query series and never go through the
1244
- * sample→threshold-unit conversion the main Value column
1245
- * does, so they are in the component's legendUnit — mixing
1246
- * units across the row is deliberate, and is why every cell
1247
- * has to say which one it is in.
1248
- */
1249
- cells.push(MetricValueFormatter.format({
1207
+ if (!match || typeof match.value !== "number") {
1208
+ continue;
1209
+ }
1210
+ /*
1211
+ * Each component keeps its OWN unit. Component values are
1212
+ * indexed off the per-query series and never go through the
1213
+ * sample→threshold-unit conversion the sample's value does, so
1214
+ * they are in the component's legendUnit — which is why each
1215
+ * one names the unit it is in. Its configured unit is in the
1216
+ * Components list of the Metric Details above.
1217
+ */
1218
+ details.push({
1219
+ label: RootCauseList.code(component.alias),
1220
+ value: MetricValueFormatter.format({
1250
1221
  value: match.value,
1251
1222
  unit: component.unit,
1252
1223
  metricName: MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1253
1224
  metricName: component.name,
1254
1225
  isFormula: component.isFormula,
1255
1226
  }),
1256
- }));
1257
- }
1258
- else {
1259
- cells.push("-");
1260
- }
1227
+ }),
1228
+ });
1261
1229
  }
1230
+ /*
1231
+ * The attributes identify the series the sample came from. Keys
1232
+ * and values are telemetry, so both go in code spans:
1233
+ * RootCauseList.code keeps whatever they contain inside the span.
1234
+ */
1262
1235
  for (const k of attrKeys) {
1263
1236
  const v = s.attributes[k];
1264
- cells.push(v === undefined || v === null ? "-" : String(v));
1237
+ if (v === undefined || v === null) {
1238
+ continue;
1239
+ }
1240
+ details.push({
1241
+ label: RootCauseList.code(k),
1242
+ value: RootCauseList.code(String(v)),
1243
+ });
1265
1244
  }
1266
- return `| ${cells.join(" | ")} |`;
1245
+ return {
1246
+ title: RootCauseList.code(new Date(s.timestamp).toISOString()),
1247
+ value: `**${MetricValueFormatter.format({
1248
+ value: s.value,
1249
+ unit: input.unit,
1250
+ metricName: input.unitHeuristicMetricName,
1251
+ })}**`,
1252
+ details: details,
1253
+ };
1267
1254
  });
1268
1255
  const lines = [
1269
1256
  `**Breaching Samples**`,
@@ -1272,12 +1259,14 @@ ${contextBlock}
1272
1259
  totalSamples: input.totalSamples,
1273
1260
  }),
1274
1261
  "",
1275
- headerRow,
1276
- dividerRow,
1277
- ...dataRows,
1262
+ RootCauseList.render(items),
1278
1263
  ];
1279
1264
  if (sorted.length > displayedSamples.length) {
1280
- lines.push(`\n_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`);
1265
+ /*
1266
+ * The blank line matters: without it the note is a lazy
1267
+ * continuation of the last item, not a paragraph of its own.
1268
+ */
1269
+ lines.push("", `_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`);
1281
1270
  }
1282
1271
  return lines.join("\n");
1283
1272
  }
@@ -1436,7 +1425,7 @@ ${contextBlock}
1436
1425
  return ` _(scope partially carried — not applied: ${dropped.join(", ")})_`;
1437
1426
  }
1438
1427
  /**
1439
- * The unit for a value in a per-platform "Affected Resources" table.
1428
+ * The unit for a value in a per-platform "Affected Resources" list.
1440
1429
  *
1441
1430
  * These rows are NOT the same numbers as the criteria's breaching
1442
1431
  * samples: the worker collects them with a raw `MetricService.findBy`
@@ -1475,10 +1464,11 @@ ${contextBlock}
1475
1464
  }
1476
1465
  }
1477
1466
  /**
1478
- * Render one cell of a platform breakdown table's Value column.
1467
+ * Render the value of one resource in a platform "Affected Resources"
1468
+ * list.
1479
1469
  *
1480
- * Bold, because that is how these tables have always drawn the value —
1481
- * it is the one number in the row a reader scans for.
1470
+ * Bold, because that is how the breakdown has always drawn the value —
1471
+ * it is the one number on the line a reader scans for.
1482
1472
  */
1483
1473
  static formatPlatformResourceValue(input) {
1484
1474
  return `**${MetricValueFormatter.format({
@@ -1490,6 +1480,78 @@ ${contextBlock}
1490
1480
  metricName: input.metricName,
1491
1481
  })}**`;
1492
1482
  }
1483
+ /*
1484
+ * One Kubernetes resource as an "Affected Resources" list item.
1485
+ *
1486
+ * The item is titled by the most specific object the series names —
1487
+ * container, then pod, then workload, then node, then namespace — because
1488
+ * that is the thing that is actually breaching; everything above it in
1489
+ * the hierarchy is listed underneath as context. A series with no
1490
+ * Kubernetes identity at all is a cluster-level series, so it is titled
1491
+ * by the cluster.
1492
+ *
1493
+ * A container is titled together with its pod. Every replica of a
1494
+ * workload runs a container of the same name, so "Container `checkout`"
1495
+ * alone would title three crash-looping replicas identically and leave
1496
+ * the pod that tells them apart buried in the bullets.
1497
+ */
1498
+ static getKubernetesAffectedResourceEntry(input) {
1499
+ const resource = input.resource;
1500
+ // "Deployment", "StatefulSet", ... — the worker only ever sets one of those.
1501
+ const workloadLabel = resource.workloadType || "Workload";
1502
+ let kind = "Cluster";
1503
+ let name = AffectedResourceList.code(input.clusterName);
1504
+ if (resource.containerName) {
1505
+ kind = "Container";
1506
+ name = resource.podName
1507
+ ? `${AffectedResourceList.code(resource.containerName)} in pod ${AffectedResourceList.code(resource.podName)}`
1508
+ : AffectedResourceList.code(resource.containerName);
1509
+ }
1510
+ else if (resource.podName) {
1511
+ kind = "Pod";
1512
+ name = AffectedResourceList.code(resource.podName);
1513
+ }
1514
+ else if (resource.workloadName) {
1515
+ kind = workloadLabel;
1516
+ name = AffectedResourceList.code(resource.workloadName);
1517
+ }
1518
+ else if (resource.nodeName) {
1519
+ kind = "Node";
1520
+ name = AffectedResourceList.code(resource.nodeName);
1521
+ }
1522
+ else if (resource.namespace) {
1523
+ kind = "Namespace";
1524
+ name = AffectedResourceList.code(resource.namespace);
1525
+ }
1526
+ const details = [];
1527
+ if (resource.namespace && kind !== "Namespace") {
1528
+ details.push({
1529
+ label: "Namespace",
1530
+ value: AffectedResourceList.code(resource.namespace),
1531
+ });
1532
+ }
1533
+ const titledByWorkload = !resource.containerName &&
1534
+ !resource.podName &&
1535
+ Boolean(resource.workloadName);
1536
+ if (resource.workloadName && !titledByWorkload) {
1537
+ details.push({
1538
+ label: workloadLabel,
1539
+ value: AffectedResourceList.code(resource.workloadName),
1540
+ });
1541
+ }
1542
+ if (resource.nodeName && kind !== "Node") {
1543
+ details.push({
1544
+ label: "Node",
1545
+ value: AffectedResourceList.code(resource.nodeName),
1546
+ });
1547
+ }
1548
+ return {
1549
+ kind: kind,
1550
+ name: name,
1551
+ value: input.value,
1552
+ details: details,
1553
+ };
1554
+ }
1493
1555
  static async buildKubernetesRootCauseContext(input) {
1494
1556
  var _a;
1495
1557
  const metricResponse = input.dataToProcess;
@@ -1508,94 +1570,56 @@ ${contextBlock}
1508
1570
  sections.push(`**Kubernetes Cluster Details**\n${clusterDetails.join("\n")}`);
1509
1571
  // Affected resources
1510
1572
  if (breakdown.affectedResources && breakdown.affectedResources.length > 0) {
1511
- const resourceLines = [];
1512
- // Sort by metric value descending (worst first) and filter out zero-value resources
1513
- const sortedResources = [
1514
- ...breakdown.affectedResources,
1515
- ]
1573
+ /*
1574
+ * Keep the rows that satisfy the criteria that just matched, worst
1575
+ * first — NOT simply the non-zero rows. k8s-node-not-ready fires on
1576
+ * `k8s.node.condition_ready = 0`, so the zero rows are the NotReady
1577
+ * nodes; the old hardcoded `> 0` dropped exactly those and named a
1578
+ * healthy node in the analysis below.
1579
+ */
1580
+ const breach = MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
1581
+ criteriaInstance: input.criteriaInstance,
1582
+ floorEqualityFiresOnFall: true,
1583
+ });
1584
+ const sortedResources = breakdown.affectedResources
1585
+ .map((r) => {
1586
+ var _a;
1587
+ /*
1588
+ * A fall criteria breached on the resource's lowest sample
1589
+ * in the window, not its highest.
1590
+ */
1591
+ return breach.worstIsLowest
1592
+ ? Object.assign(Object.assign({}, r), { metricValue: (_a = r.lowestMetricValue) !== null && _a !== void 0 ? _a : r.metricValue }) : r;
1593
+ })
1516
1594
  .filter((r) => {
1517
- return r.metricValue > 0;
1595
+ return breach.matches(r.metricValue);
1518
1596
  })
1519
1597
  .sort((a, b) => {
1520
- return b.metricValue - a.metricValue;
1598
+ return breach.worstIsLowest
1599
+ ? a.metricValue - b.metricValue
1600
+ : b.metricValue - a.metricValue;
1521
1601
  });
1522
1602
  if (sortedResources.length === 0) {
1523
1603
  return sections.join("\n");
1524
1604
  }
1525
1605
  // Show top 10 affected resources
1526
- const resourcesToShow = sortedResources.slice(0, 10);
1527
- // Determine which columns are present across all resources
1528
- const hasNamespace = resourcesToShow.some((r) => {
1529
- return r.namespace;
1530
- });
1531
- const hasWorkload = resourcesToShow.some((r) => {
1532
- return r.workloadType && r.workloadName;
1533
- });
1534
- const hasPod = resourcesToShow.some((r) => {
1535
- return r.podName;
1536
- });
1537
- const hasContainer = resourcesToShow.some((r) => {
1538
- return r.containerName;
1539
- });
1540
- const hasNode = resourcesToShow.some((r) => {
1541
- return r.nodeName;
1542
- });
1543
- // Build table header
1544
- const headerCells = [];
1545
- if (hasNamespace) {
1546
- headerCells.push("Namespace");
1547
- }
1548
- if (hasWorkload) {
1549
- headerCells.push("Workload Type");
1550
- headerCells.push("Workload");
1551
- }
1552
- if (hasPod) {
1553
- headerCells.push("Pod");
1554
- }
1555
- if (hasContainer) {
1556
- headerCells.push("Container");
1557
- }
1558
- if (hasNode) {
1559
- headerCells.push("Node");
1560
- }
1561
- headerCells.push("Value");
1562
- const headerRow = `| ${headerCells.join(" | ")} |`;
1563
- const separatorRow = `| ${headerCells
1564
- .map(() => {
1565
- return "---";
1566
- })
1567
- .join(" | ")} |`;
1568
- resourceLines.push(headerRow);
1569
- resourceLines.push(separatorRow);
1570
- for (const resource of resourcesToShow) {
1571
- const cells = [];
1572
- if (hasNamespace) {
1573
- cells.push(resource.namespace ? `\`${resource.namespace}\`` : "-");
1574
- }
1575
- if (hasWorkload) {
1576
- cells.push(resource.workloadType ? `${resource.workloadType}` : "-");
1577
- cells.push(resource.workloadName ? `\`${resource.workloadName}\`` : "-");
1578
- }
1579
- if (hasPod) {
1580
- cells.push(resource.podName ? `\`${resource.podName}\`` : "-");
1581
- }
1582
- if (hasContainer) {
1583
- cells.push(resource.containerName ? `\`${resource.containerName}\`` : "-");
1584
- }
1585
- if (hasNode) {
1586
- cells.push(resource.nodeName ? `\`${resource.nodeName}\`` : "-");
1587
- }
1588
- cells.push(MonitorCriteriaEvaluator.formatPlatformResourceValue({
1589
- platform: "kubernetes",
1590
- metricName: breakdown.metricName,
1591
- value: resource.metricValue,
1592
- }));
1593
- resourceLines.push(`| ${cells.join(" | ")} |`);
1594
- }
1595
- if (sortedResources.length > 10) {
1596
- resourceLines.push(`\n*... and ${sortedResources.length - 10} more affected resources*`);
1597
- }
1598
- sections.push(`\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`);
1606
+ const resourcesToShow = sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
1607
+ sections.push(AffectedResourceList.render({
1608
+ heading: "Affected Resources",
1609
+ overflowNoun: "affected resources",
1610
+ totalCount: sortedResources.length,
1611
+ entries: resourcesToShow.map((resource) => {
1612
+ return MonitorCriteriaEvaluator.getKubernetesAffectedResourceEntry({
1613
+ resource: resource,
1614
+ clusterName: breakdown.clusterName,
1615
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
1616
+ platform: "kubernetes",
1617
+ metricName: breakdown.metricName,
1618
+ value: resource.metricValue,
1619
+ }),
1620
+ });
1621
+ }),
1622
+ }));
1599
1623
  // Add root cause analysis based on metric type
1600
1624
  const analysis = MonitorCriteriaEvaluator.buildKubernetesRootCauseAnalysis({
1601
1625
  breakdown: breakdown,
@@ -1771,6 +1795,75 @@ ${contextBlock}
1771
1795
  }
1772
1796
  return sections.length > 0 ? sections.join("\n") : null;
1773
1797
  }
1798
+ /*
1799
+ * What a Proxmox resource is, from the `pve.type` / `pve.scope` the
1800
+ * agent's transform/pve-identity processor derives from its `id`
1801
+ * (node/…, qemu/…, lxc/…, storage/…, cluster/…). A value that processor
1802
+ * does not produce is shown as it arrived rather than guessed at.
1803
+ */
1804
+ static getProxmoxAffectedResourceKind(resource) {
1805
+ switch (resource.resourceType) {
1806
+ case "qemu":
1807
+ return "Virtual Machine";
1808
+ case "lxc":
1809
+ return "Container";
1810
+ case "node":
1811
+ return "Node";
1812
+ case "storage":
1813
+ return "Storage";
1814
+ default:
1815
+ break;
1816
+ }
1817
+ switch (resource.scope) {
1818
+ case "guest":
1819
+ return "Guest";
1820
+ case "node":
1821
+ return "Node";
1822
+ case "storage":
1823
+ return "Storage";
1824
+ case "cluster":
1825
+ return "Cluster";
1826
+ default:
1827
+ break;
1828
+ }
1829
+ return resource.resourceType || resource.scope || "Resource";
1830
+ }
1831
+ /*
1832
+ * One Proxmox resource as an "Affected Resources" list item: its kind,
1833
+ * its name next to its `qemu/100`-style id, and the node it runs on.
1834
+ */
1835
+ static getProxmoxAffectedResourceEntry(input) {
1836
+ const resource = input.resource;
1837
+ let kind = MonitorCriteriaEvaluator.getProxmoxAffectedResourceKind(resource);
1838
+ let name = AffectedResourceList.codeWithId({
1839
+ name: resource.resourceName,
1840
+ id: resource.resourceId,
1841
+ });
1842
+ if (!name && resource.nodeName) {
1843
+ // A per-node series that carries no id of its own.
1844
+ kind = "Node";
1845
+ name = AffectedResourceList.code(resource.nodeName);
1846
+ }
1847
+ if (!name) {
1848
+ // Nothing narrower than the cluster — a cluster-wide series.
1849
+ kind = "Cluster";
1850
+ name = AffectedResourceList.code(input.clusterName);
1851
+ }
1852
+ const details = [];
1853
+ // A node's own row would only repeat its name as "Node: …".
1854
+ if (resource.nodeName && kind !== "Node") {
1855
+ details.push({
1856
+ label: "Node",
1857
+ value: AffectedResourceList.code(resource.nodeName),
1858
+ });
1859
+ }
1860
+ return {
1861
+ kind: kind,
1862
+ name: name,
1863
+ value: input.value,
1864
+ details: details,
1865
+ };
1866
+ }
1774
1867
  static buildProxmoxRootCauseContext(input) {
1775
1868
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1776
1869
  const metricResponse = input.dataToProcess;
@@ -1807,15 +1900,15 @@ ${contextBlock}
1807
1900
  }
1808
1901
  sections.push(`**Proxmox Cluster Details**\n${clusterDetails.join("\n")}`);
1809
1902
  }
1810
- // Affected resources (Resource / Type / Node / Value)
1811
- let renderedBreakdownTable = false;
1903
+ // Affected resources: a ranked list of kind, name and node
1904
+ let renderedBreakdownList = false;
1812
1905
  if (breakdown && breakdown.affectedResources.length > 0) {
1813
1906
  /*
1814
1907
  * K8s-parity render: drop zero-value rows, worst (highest)
1815
1908
  * first, top 10. Note that for availability metrics (pve_up)
1816
1909
  * zero-valued rows ARE the down resources — those still drive
1817
1910
  * alerting through the per-series criteria evaluation; this
1818
- * table is supplementary context only.
1911
+ * list is supplementary context only.
1819
1912
  */
1820
1913
  const sortedResources = [
1821
1914
  ...breakdown.affectedResources,
@@ -1827,47 +1920,35 @@ ${contextBlock}
1827
1920
  return b.metricValue - a.metricValue;
1828
1921
  });
1829
1922
  /*
1830
- * Skip the table when no row carries any identity label
1923
+ * Skip the list when no row carries any identity label
1831
1924
  * (cluster-wide series) — it would add nothing.
1832
1925
  */
1833
1926
  const hasIdentity = sortedResources.some((r) => {
1834
1927
  return r.resourceId || r.resourceName || r.nodeName;
1835
1928
  });
1836
1929
  if (sortedResources.length > 0 && hasIdentity) {
1837
- const resourcesToShow = sortedResources.slice(0, 10);
1838
- const resourceLines = [];
1839
- resourceLines.push(`| Resource | Type | Node | Value |`);
1840
- resourceLines.push(`| --- | --- | --- | --- |`);
1841
- for (const resource of resourcesToShow) {
1842
- let resourceCell = "-";
1843
- if (resource.resourceName && resource.resourceId) {
1844
- resourceCell = `\`${resource.resourceName}\` (\`${resource.resourceId}\`)`;
1845
- }
1846
- else if (resource.resourceId) {
1847
- resourceCell = `\`${resource.resourceId}\``;
1848
- }
1849
- else if (resource.resourceName) {
1850
- resourceCell = `\`${resource.resourceName}\``;
1851
- }
1852
- const typeCell = resource.resourceType || resource.scope || "-";
1853
- const nodeCell = resource.nodeName
1854
- ? `\`${resource.nodeName}\``
1855
- : "-";
1856
- resourceLines.push(`| ${resourceCell} | ${typeCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue({
1857
- platform: "proxmox",
1858
- metricName: breakdown.metricName,
1859
- value: resource.metricValue,
1860
- })} |`);
1861
- }
1862
- if (sortedResources.length > 10) {
1863
- resourceLines.push(`\n*... and ${sortedResources.length - 10} more affected resources*`);
1864
- }
1865
- sections.push(`\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`);
1866
- renderedBreakdownTable = true;
1930
+ const resourcesToShow = sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
1931
+ sections.push(AffectedResourceList.render({
1932
+ heading: "Affected Resources",
1933
+ overflowNoun: "affected resources",
1934
+ totalCount: sortedResources.length,
1935
+ entries: resourcesToShow.map((resource) => {
1936
+ return MonitorCriteriaEvaluator.getProxmoxAffectedResourceEntry({
1937
+ resource: resource,
1938
+ clusterName: breakdown.clusterName,
1939
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
1940
+ platform: "proxmox",
1941
+ metricName: breakdown.metricName,
1942
+ value: resource.metricValue,
1943
+ }),
1944
+ });
1945
+ }),
1946
+ }));
1947
+ renderedBreakdownList = true;
1867
1948
  }
1868
1949
  }
1869
- // Metric results summary (fallback context when no table rendered)
1870
- if (!renderedBreakdownTable &&
1950
+ // Metric results summary (fallback context when no list rendered)
1951
+ if (!renderedBreakdownList &&
1871
1952
  metricResponse.metricResult &&
1872
1953
  metricResponse.metricResult.length > 0) {
1873
1954
  const resultDetails = [];
@@ -1886,8 +1967,8 @@ ${contextBlock}
1886
1967
  * Which vSphere object one VMware breakdown row describes, resolved
1887
1968
  * from the identity attributes the vcenter receiver stamped on it —
1888
1969
  * most specific first, in the same order the ingest inventory scan
1889
- * uses, so the Kind column here and the inventory's `kind` never
1890
- * disagree about the same series. A VM row carries its parent host
1970
+ * uses, so the kind an Affected Resources item is titled with and the
1971
+ * inventory's `kind` never disagree about the same series. A VM row carries its parent host
1891
1972
  * (and often a resource pool), so the VM check must run before the
1892
1973
  * host and pool checks.
1893
1974
  */
@@ -1912,6 +1993,84 @@ ${contextBlock}
1912
1993
  }
1913
1994
  return undefined;
1914
1995
  }
1996
+ /*
1997
+ * One vSphere object as an "Affected Resources" list item, titled by
1998
+ * the object itself, with the ESXi host and cluster it sits under
1999
+ * listed beneath it.
2000
+ */
2001
+ static getVMwareAffectedResourceEntry(input) {
2002
+ const resource = input.resource;
2003
+ const kind = MonitorCriteriaEvaluator.getVMwareAffectedResourceKind(resource);
2004
+ let name = "";
2005
+ let showHost = false;
2006
+ switch (kind) {
2007
+ case "Virtual Machine":
2008
+ /*
2009
+ * The VM's display name, falling back to its instance UUID
2010
+ * (`vcenter.vm.id`) when the receiver stamped no name. The Host
2011
+ * detail is the ESXi host the VM is running on.
2012
+ */
2013
+ name = AffectedResourceList.code(resource.vmName || resource.vmId);
2014
+ showHost = true;
2015
+ break;
2016
+ case "Resource Pool":
2017
+ /*
2018
+ * Pool names are only unique within a parent, so the inventory
2019
+ * path is shown next to the name. The Host detail is the owner
2020
+ * host for a pool on a standalone ESXi host (pools under a
2021
+ * cluster show the cluster).
2022
+ */
2023
+ name = AffectedResourceList.codeWithId({
2024
+ name: resource.resourcePoolName,
2025
+ id: resource.resourcePoolPath,
2026
+ });
2027
+ showHost = true;
2028
+ break;
2029
+ case "Host":
2030
+ // The host IS the resource; a "Host:" detail would repeat it.
2031
+ name = AffectedResourceList.code(resource.hostName);
2032
+ break;
2033
+ case "Datastore":
2034
+ name = AffectedResourceList.code(resource.datastoreName);
2035
+ break;
2036
+ case "Cluster":
2037
+ name = AffectedResourceList.code(resource.clusterName);
2038
+ break;
2039
+ case "Datacenter":
2040
+ name = AffectedResourceList.code(resource.datacenterName);
2041
+ break;
2042
+ default:
2043
+ break;
2044
+ }
2045
+ const details = [];
2046
+ if (showHost && resource.hostName) {
2047
+ details.push({
2048
+ label: "Host",
2049
+ value: AffectedResourceList.code(resource.hostName),
2050
+ });
2051
+ }
2052
+ if (kind !== "Cluster" && resource.clusterName) {
2053
+ details.push({
2054
+ label: "Cluster",
2055
+ value: AffectedResourceList.code(resource.clusterName),
2056
+ });
2057
+ }
2058
+ if (!kind || !name) {
2059
+ // No vSphere identity on the series — it describes the vCenter as a whole.
2060
+ return {
2061
+ kind: "vCenter",
2062
+ name: AffectedResourceList.code(input.vcenterName),
2063
+ value: input.value,
2064
+ details: details,
2065
+ };
2066
+ }
2067
+ return {
2068
+ kind: kind,
2069
+ name: name,
2070
+ value: input.value,
2071
+ details: details,
2072
+ };
2073
+ }
1915
2074
  static buildVMwareRootCauseContext(input) {
1916
2075
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1917
2076
  const metricResponse = input.dataToProcess;
@@ -1959,17 +2118,17 @@ ${contextBlock}
1959
2118
  }
1960
2119
  sections.push(`**vCenter Details**\n${vcenterDetails.join("\n")}`);
1961
2120
  }
1962
- // Affected resources (Resource / Kind / Host / Cluster / Value)
1963
- let renderedBreakdownTable = false;
2121
+ // Affected resources: a ranked list of object, host and cluster
2122
+ let renderedBreakdownList = false;
1964
2123
  if (breakdown && breakdown.affectedResources.length > 0) {
1965
2124
  /*
1966
2125
  * K8s/Proxmox-parity render: drop zero-value rows, worst (highest)
1967
2126
  * first, top 10. For count metrics filtered to an unhealthy state
1968
2127
  * (`vcenter.datacenter.host.count{status=red}`) the non-zero rows
1969
- * ARE the objects with a problem, so the table reads correctly;
2128
+ * ARE the objects with a problem, so the list reads correctly;
1970
2129
  * a zero row means "nothing in that state" and adds nothing. The
1971
2130
  * per-series criteria evaluation is what actually alerts; this
1972
- * table is supplementary context only.
2131
+ * list is supplementary context only.
1973
2132
  */
1974
2133
  const sortedResources = [
1975
2134
  ...breakdown.affectedResources,
@@ -1981,7 +2140,7 @@ ${contextBlock}
1981
2140
  return b.metricValue - a.metricValue;
1982
2141
  });
1983
2142
  /*
1984
- * Skip the table when no row carries any identity attribute — it
2143
+ * Skip the list when no row carries any identity attribute — it
1985
2144
  * would add nothing. Every vcenter-receiver series carries at
1986
2145
  * least `vcenter.datacenter.name`, so this only trips on a
1987
2146
  * breakdown the worker could not attribute at all.
@@ -1990,77 +2149,28 @@ ${contextBlock}
1990
2149
  return Boolean(MonitorCriteriaEvaluator.getVMwareAffectedResourceKind(r));
1991
2150
  });
1992
2151
  if (sortedResources.length > 0 && hasIdentity) {
1993
- const resourcesToShow = sortedResources.slice(0, 10);
1994
- const resourceLines = [];
1995
- resourceLines.push(`| Resource | Kind | Host | Cluster | Value |`);
1996
- resourceLines.push(`| --- | --- | --- | --- | --- |`);
1997
- for (const resource of resourcesToShow) {
1998
- const kind = MonitorCriteriaEvaluator.getVMwareAffectedResourceKind(resource);
1999
- let resourceCell = "-";
2000
- let hostCell = "-";
2001
- switch (kind) {
2002
- case "Virtual Machine":
2003
- /*
2004
- * The VM's display name, falling back to its instance
2005
- * UUID (`vcenter.vm.id`) when the receiver stamped no
2006
- * name. The Host column carries the ESXi host the VM is
2007
- * running on.
2008
- */
2009
- resourceCell = resource.vmName
2010
- ? `\`${resource.vmName}\``
2011
- : `\`${resource.vmId}\``;
2012
- hostCell = resource.hostName ? `\`${resource.hostName}\`` : "-";
2013
- break;
2014
- case "Resource Pool":
2015
- /*
2016
- * Pool names are only unique within a parent, so the
2017
- * inventory path is shown next to the name. The Host
2018
- * column is the owner host for a pool on a standalone
2019
- * ESXi host (pools under a cluster show the cluster).
2020
- */
2021
- if (resource.resourcePoolName && resource.resourcePoolPath) {
2022
- resourceCell = `\`${resource.resourcePoolName}\` (\`${resource.resourcePoolPath}\`)`;
2023
- }
2024
- else {
2025
- resourceCell = `\`${resource.resourcePoolPath || resource.resourcePoolName}\``;
2026
- }
2027
- hostCell = resource.hostName ? `\`${resource.hostName}\`` : "-";
2028
- break;
2029
- case "Host":
2030
- // The host IS the resource; repeating it in the Host column adds nothing.
2031
- resourceCell = `\`${resource.hostName}\``;
2032
- break;
2033
- case "Datastore":
2034
- resourceCell = `\`${resource.datastoreName}\``;
2035
- break;
2036
- case "Cluster":
2037
- resourceCell = `\`${resource.clusterName}\``;
2038
- break;
2039
- case "Datacenter":
2040
- resourceCell = `\`${resource.datacenterName}\``;
2041
- break;
2042
- default:
2043
- break;
2044
- }
2045
- const kindCell = kind || "-";
2046
- const clusterCell = kind !== "Cluster" && resource.clusterName
2047
- ? `\`${resource.clusterName}\``
2048
- : "-";
2049
- resourceLines.push(`| ${resourceCell} | ${kindCell} | ${hostCell} | ${clusterCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue({
2050
- platform: "vmware",
2051
- metricName: breakdown.metricName,
2052
- value: resource.metricValue,
2053
- })} |`);
2054
- }
2055
- if (sortedResources.length > 10) {
2056
- resourceLines.push(`\n*... and ${sortedResources.length - 10} more affected resources*`);
2057
- }
2058
- sections.push(`\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`);
2059
- renderedBreakdownTable = true;
2152
+ const resourcesToShow = sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2153
+ sections.push(AffectedResourceList.render({
2154
+ heading: "Affected Resources",
2155
+ overflowNoun: "affected resources",
2156
+ totalCount: sortedResources.length,
2157
+ entries: resourcesToShow.map((resource) => {
2158
+ return MonitorCriteriaEvaluator.getVMwareAffectedResourceEntry({
2159
+ resource: resource,
2160
+ vcenterName: breakdown.vcenterName,
2161
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
2162
+ platform: "vmware",
2163
+ metricName: breakdown.metricName,
2164
+ value: resource.metricValue,
2165
+ }),
2166
+ });
2167
+ }),
2168
+ }));
2169
+ renderedBreakdownList = true;
2060
2170
  }
2061
2171
  }
2062
- // Metric results summary (fallback context when no table rendered)
2063
- if (!renderedBreakdownTable &&
2172
+ // Metric results summary (fallback context when no list rendered)
2173
+ if (!renderedBreakdownList &&
2064
2174
  metricResponse.metricResult &&
2065
2175
  metricResponse.metricResult.length > 0) {
2066
2176
  const resultDetails = [];
@@ -2075,6 +2185,46 @@ ${contextBlock}
2075
2185
  }
2076
2186
  return sections.length > 0 ? sections.join("\n") : null;
2077
2187
  }
2188
+ /*
2189
+ * One Docker Swarm task as an "Affected Tasks" list item, with the
2190
+ * service it belongs to and the node it is scheduled on beneath it.
2191
+ */
2192
+ static getDockerSwarmAffectedResourceEntry(input) {
2193
+ const resource = input.resource;
2194
+ let kind = "Cluster";
2195
+ let name = AffectedResourceList.code(input.clusterName);
2196
+ if (resource.containerName) {
2197
+ kind = "Task";
2198
+ name = AffectedResourceList.code(resource.containerName);
2199
+ }
2200
+ else if (resource.serviceName) {
2201
+ kind = "Service";
2202
+ name = AffectedResourceList.code(resource.serviceName);
2203
+ }
2204
+ else if (resource.nodeName) {
2205
+ kind = "Node";
2206
+ name = AffectedResourceList.code(resource.nodeName);
2207
+ }
2208
+ const details = [];
2209
+ if (resource.serviceName && kind !== "Service") {
2210
+ details.push({
2211
+ label: "Service",
2212
+ value: AffectedResourceList.code(resource.serviceName),
2213
+ });
2214
+ }
2215
+ if (resource.nodeName && kind !== "Node") {
2216
+ details.push({
2217
+ label: "Node",
2218
+ value: AffectedResourceList.code(resource.nodeName),
2219
+ });
2220
+ }
2221
+ return {
2222
+ kind: kind,
2223
+ name: name,
2224
+ value: input.value,
2225
+ details: details,
2226
+ };
2227
+ }
2078
2228
  static buildDockerSwarmRootCauseContext(input) {
2079
2229
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
2080
2230
  const metricResponse = input.dataToProcess;
@@ -2111,14 +2261,14 @@ ${contextBlock}
2111
2261
  }
2112
2262
  sections.push(`**Docker Swarm Cluster Details**\n${clusterDetails.join("\n")}`);
2113
2263
  }
2114
- // Affected resources (Task / Service / Node / Value)
2115
- let renderedBreakdownTable = false;
2264
+ // Affected tasks: a ranked list of task, service and node
2265
+ let renderedBreakdownList = false;
2116
2266
  if (breakdown && breakdown.affectedResources.length > 0) {
2117
2267
  /*
2118
2268
  * K8s/Proxmox-parity render: drop zero-value rows, worst (highest)
2119
2269
  * first, top 10. For the task-down template the breaching rows ARE
2120
2270
  * the zero-uptime ones — those still drive alerting through the
2121
- * per-series criteria evaluation; this table is supplementary
2271
+ * per-series criteria evaluation; this list is supplementary
2122
2272
  * context only.
2123
2273
  */
2124
2274
  const sortedResources = [
@@ -2131,42 +2281,35 @@ ${contextBlock}
2131
2281
  return b.metricValue - a.metricValue;
2132
2282
  });
2133
2283
  /*
2134
- * Skip the table when no row carries any identity label
2284
+ * Skip the list when no row carries any identity label
2135
2285
  * (cluster-wide series) — it would add nothing.
2136
2286
  */
2137
2287
  const hasIdentity = sortedResources.some((r) => {
2138
2288
  return r.containerName || r.serviceName || r.nodeName;
2139
2289
  });
2140
2290
  if (sortedResources.length > 0 && hasIdentity) {
2141
- const resourcesToShow = sortedResources.slice(0, 10);
2142
- const resourceLines = [];
2143
- resourceLines.push(`| Task / Container | Service | Node | Value |`);
2144
- resourceLines.push(`| --- | --- | --- | --- |`);
2145
- for (const resource of resourcesToShow) {
2146
- const taskCell = resource.containerName
2147
- ? `\`${resource.containerName}\``
2148
- : "-";
2149
- const serviceCell = resource.serviceName
2150
- ? `\`${resource.serviceName}\``
2151
- : "-";
2152
- const nodeCell = resource.nodeName
2153
- ? `\`${resource.nodeName}\``
2154
- : "-";
2155
- resourceLines.push(`| ${taskCell} | ${serviceCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue({
2156
- platform: "dockerSwarm",
2157
- metricName: breakdown.metricName,
2158
- value: resource.metricValue,
2159
- })} |`);
2160
- }
2161
- if (sortedResources.length > 10) {
2162
- resourceLines.push(`\n*... and ${sortedResources.length - 10} more affected tasks*`);
2163
- }
2164
- sections.push(`\n\n**Affected Tasks** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`);
2165
- renderedBreakdownTable = true;
2291
+ const resourcesToShow = sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2292
+ sections.push(AffectedResourceList.render({
2293
+ heading: "Affected Tasks",
2294
+ overflowNoun: "affected tasks",
2295
+ totalCount: sortedResources.length,
2296
+ entries: resourcesToShow.map((resource) => {
2297
+ return MonitorCriteriaEvaluator.getDockerSwarmAffectedResourceEntry({
2298
+ resource: resource,
2299
+ clusterName: breakdown.clusterName,
2300
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
2301
+ platform: "dockerSwarm",
2302
+ metricName: breakdown.metricName,
2303
+ value: resource.metricValue,
2304
+ }),
2305
+ });
2306
+ }),
2307
+ }));
2308
+ renderedBreakdownList = true;
2166
2309
  }
2167
2310
  }
2168
- // Metric results summary (fallback context when no table rendered)
2169
- if (!renderedBreakdownTable &&
2311
+ // Metric results summary (fallback context when no list rendered)
2312
+ if (!renderedBreakdownList &&
2170
2313
  metricResponse.metricResult &&
2171
2314
  metricResponse.metricResult.length > 0) {
2172
2315
  const resultDetails = [];
@@ -2182,31 +2325,49 @@ ${contextBlock}
2182
2325
  return sections.length > 0 ? sections.join("\n") : null;
2183
2326
  }
2184
2327
  /*
2185
- * Which rows of the Ceph affected-resources table actually BREACHED.
2328
+ * Which rows of an affected-resources list actually BREACHED.
2186
2329
  *
2187
2330
  * The blanket `metricValue > 0` below is right for every count- or
2188
2331
  * level-style criteria ("> 0 active health checks", "> 85 % used"), but
2189
- * it is exactly backwards for the availability signals: the OSD Down /
2190
- * OSD Out / Quorum Degraded criteria fire on `< 1` over ceph_osd_up /
2191
- * ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows are the down
2192
- * daemons. Dropping them rendered a table of the HEALTHY daemons under
2193
- * an incident that tells the reader to "check the root cause for the
2194
- * affected ceph_daemon label".
2332
+ * it is exactly backwards for the availability signals: the Ceph OSD
2333
+ * Down / OSD Out / Quorum Degraded criteria fire on `< 1` over
2334
+ * ceph_osd_up / ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows
2335
+ * are the down daemons. Dropping them rendered a list of the HEALTHY
2336
+ * daemons under an incident that tells the reader to "check the root
2337
+ * cause for the affected ceph_daemon label".
2195
2338
  *
2196
2339
  * Invert only for a criteria that fires when the metric FALLS.
2197
2340
  * Everything else — every `> 0` health check, PG count, latency and
2198
- * capacity ratio, and every `= 0` recovery criteria — keeps the
2199
- * existing `> 0`, worst-highest-first table byte for byte.
2341
+ * capacity ratio, and (by default) every `= 0` recovery criteria —
2342
+ * keeps the existing `> 0`, worst-highest-first list byte for byte.
2343
+ *
2344
+ * `floorEqualityFiresOnFall` additionally counts a FIRING `= 0` (or
2345
+ * below) as a fall. Kubernetes needs it: k8s-node-not-ready and
2346
+ * k8s-etcd-no-leader fire on `= 0` over k8s.node.condition_ready /
2347
+ * etcd_server_has_leader, and zero is the floor of those metrics, so
2348
+ * the only way to reach it is down. It is gated on the criteria opening
2349
+ * an incident or alert because the same `= 0` is also how the restart,
2350
+ * failed-pod and unavailable-replica templates express RECOVERY, and
2351
+ * that all-clear must not become a list of healthy zero rows. Ceph
2352
+ * leaves it off: all of its `= 0` criteria are recoveries.
2200
2353
  */
2201
- static getCephBreachPredicate(criteriaInstance) {
2202
- var _a;
2203
- const metricFilters = (((_a = criteriaInstance === null || criteriaInstance === void 0 ? void 0 : criteriaInstance.data) === null || _a === void 0 ? void 0 : _a.filters) || []).filter((f) => {
2354
+ static getAffectedResourceBreachPredicate(input) {
2355
+ var _a, _b, _c, _d, _e, _f;
2356
+ const metricFilters = (((_b = (_a = input.criteriaInstance) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.filters) || []).filter((f) => {
2204
2357
  return f.checkOn === CheckOn.MetricValue && typeof f.value === "number";
2205
2358
  });
2359
+ const opensIncidentOrAlert = ((_d = (_c = input.criteriaInstance) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.createIncidents) === true ||
2360
+ ((_f = (_e = input.criteriaInstance) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f.createAlerts) === true;
2206
2361
  const firesWhenMetricFalls = metricFilters.length > 0 &&
2207
2362
  metricFilters.every((f) => {
2208
- return (f.filterType === FilterType.LessThan ||
2209
- f.filterType === FilterType.LessThanOrEqualTo);
2363
+ if (f.filterType === FilterType.LessThan ||
2364
+ f.filterType === FilterType.LessThanOrEqualTo) {
2365
+ return true;
2366
+ }
2367
+ return (input.floorEqualityFiresOnFall === true &&
2368
+ opensIncidentOrAlert &&
2369
+ f.filterType === FilterType.EqualTo &&
2370
+ f.value <= 0);
2210
2371
  });
2211
2372
  if (!firesWhenMetricFalls) {
2212
2373
  return {
@@ -2220,6 +2381,9 @@ ${contextBlock}
2220
2381
  matches: (value) => {
2221
2382
  return metricFilters.some((f) => {
2222
2383
  const threshold = f.value;
2384
+ if (f.filterType === FilterType.EqualTo) {
2385
+ return value === threshold;
2386
+ }
2223
2387
  return f.filterType === FilterType.LessThan
2224
2388
  ? value < threshold
2225
2389
  : value <= threshold;
@@ -2232,6 +2396,71 @@ ${contextBlock}
2232
2396
  worstIsLowest: true,
2233
2397
  };
2234
2398
  }
2399
+ /*
2400
+ * One Ceph daemon or pool as an "Affected Resources" list item, with
2401
+ * the pool and host it belongs to beneath it.
2402
+ */
2403
+ static getCephAffectedResourceEntry(input) {
2404
+ const resource = input.resource;
2405
+ /*
2406
+ * The worker stores every series' `name` label as poolName, but only
2407
+ * ceph_pool_metadata's `name` is a pool — and that series always
2408
+ * carries pool_id too. On ceph_health_detail, `name` is the health
2409
+ * check (RECENT_CRASH, OSD_NEARFULL, ...), and titling it "Pool" told
2410
+ * the reader a pool called OSD_NEARFULL was the problem. So a name is
2411
+ * treated as a pool only when it arrives with a pool id.
2412
+ */
2413
+ const pool = resource.poolId
2414
+ ? AffectedResourceList.codeWithId({
2415
+ name: resource.poolName,
2416
+ id: resource.poolId,
2417
+ })
2418
+ : "";
2419
+ const otherName = resource.poolId
2420
+ ? ""
2421
+ : AffectedResourceList.code(resource.poolName);
2422
+ const otherNameKind = input.metricName === "ceph_health_detail" ? "Health Check" : "Resource";
2423
+ let kind = "Cluster";
2424
+ let name = AffectedResourceList.code(input.clusterName);
2425
+ if (resource.daemon) {
2426
+ kind = "Daemon";
2427
+ name = AffectedResourceList.code(resource.daemon);
2428
+ }
2429
+ else if (pool) {
2430
+ kind = "Pool";
2431
+ name = pool;
2432
+ }
2433
+ else if (otherName) {
2434
+ kind = otherNameKind;
2435
+ name = otherName;
2436
+ }
2437
+ else if (resource.hostname) {
2438
+ kind = "Host";
2439
+ name = AffectedResourceList.code(resource.hostname);
2440
+ }
2441
+ const details = [];
2442
+ if (pool && name !== pool) {
2443
+ details.push({ label: "Pool", value: pool });
2444
+ }
2445
+ if (otherName && name !== otherName) {
2446
+ details.push({
2447
+ label: otherNameKind === "Resource" ? "Name" : otherNameKind,
2448
+ value: otherName,
2449
+ });
2450
+ }
2451
+ if (resource.hostname && kind !== "Host") {
2452
+ details.push({
2453
+ label: "Host",
2454
+ value: AffectedResourceList.code(resource.hostname),
2455
+ });
2456
+ }
2457
+ return {
2458
+ kind: kind,
2459
+ name: name,
2460
+ value: input.value,
2461
+ details: details,
2462
+ };
2463
+ }
2235
2464
  static buildCephRootCauseContext(input) {
2236
2465
  var _a, _b, _c, _d, _e, _f, _g;
2237
2466
  const metricResponse = input.dataToProcess;
@@ -2262,8 +2491,8 @@ ${contextBlock}
2262
2491
  }
2263
2492
  sections.push(`**Ceph Cluster Details**\n${clusterDetails.join("\n")}`);
2264
2493
  }
2265
- // Affected resources (Daemon / Pool / Host / Value)
2266
- let renderedBreakdownTable = false;
2494
+ // Affected resources: a ranked list of daemon, pool and host
2495
+ let renderedBreakdownList = false;
2267
2496
  if (breakdown && breakdown.affectedResources.length > 0) {
2268
2497
  /*
2269
2498
  * Keep the rows that satisfy the criteria that just matched, worst
@@ -2271,7 +2500,9 @@ ${contextBlock}
2271
2500
  * ceph_osd_in / ceph_mon_quorum_status the criteria is `< 1`, so
2272
2501
  * the zero rows are the whole point of the incident.
2273
2502
  */
2274
- const breach = MonitorCriteriaEvaluator.getCephBreachPredicate(input.criteriaInstance);
2503
+ const breach = MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
2504
+ criteriaInstance: input.criteriaInstance,
2505
+ });
2275
2506
  const sortedResources = [
2276
2507
  ...breakdown.affectedResources,
2277
2508
  ]
@@ -2284,7 +2515,7 @@ ${contextBlock}
2284
2515
  : b.metricValue - a.metricValue;
2285
2516
  });
2286
2517
  /*
2287
- * Skip the table when no row carries any identity label
2518
+ * Skip the list when no row carries any identity label
2288
2519
  * (cluster-wide series like ceph_health_status) — it would add
2289
2520
  * nothing.
2290
2521
  */
@@ -2292,42 +2523,29 @@ ${contextBlock}
2292
2523
  return r.daemon || r.poolId || r.poolName || r.hostname;
2293
2524
  });
2294
2525
  if (sortedResources.length > 0 && hasIdentity) {
2295
- const resourcesToShow = sortedResources.slice(0, 10);
2296
- const resourceLines = [];
2297
- resourceLines.push(`| Daemon | Pool | Host | Value |`);
2298
- resourceLines.push(`| --- | --- | --- | --- |`);
2299
- for (const resource of resourcesToShow) {
2300
- const daemonCell = resource.daemon
2301
- ? `\`${resource.daemon}\``
2302
- : "-";
2303
- let poolCell = "-";
2304
- if (resource.poolName && resource.poolId) {
2305
- poolCell = `\`${resource.poolName}\` (\`${resource.poolId}\`)`;
2306
- }
2307
- else if (resource.poolName) {
2308
- poolCell = `\`${resource.poolName}\``;
2309
- }
2310
- else if (resource.poolId) {
2311
- poolCell = `\`${resource.poolId}\``;
2312
- }
2313
- const hostCell = resource.hostname
2314
- ? `\`${resource.hostname}\``
2315
- : "-";
2316
- resourceLines.push(`| ${daemonCell} | ${poolCell} | ${hostCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue({
2317
- platform: "ceph",
2318
- metricName: breakdown.metricName,
2319
- value: resource.metricValue,
2320
- })} |`);
2321
- }
2322
- if (sortedResources.length > 10) {
2323
- resourceLines.push(`\n*... and ${sortedResources.length - 10} more affected resources*`);
2324
- }
2325
- sections.push(`\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`);
2326
- renderedBreakdownTable = true;
2526
+ const resourcesToShow = sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2527
+ sections.push(AffectedResourceList.render({
2528
+ heading: "Affected Resources",
2529
+ overflowNoun: "affected resources",
2530
+ totalCount: sortedResources.length,
2531
+ entries: resourcesToShow.map((resource) => {
2532
+ return MonitorCriteriaEvaluator.getCephAffectedResourceEntry({
2533
+ resource: resource,
2534
+ clusterName: breakdown.clusterName,
2535
+ metricName: breakdown.metricName,
2536
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
2537
+ platform: "ceph",
2538
+ metricName: breakdown.metricName,
2539
+ value: resource.metricValue,
2540
+ }),
2541
+ });
2542
+ }),
2543
+ }));
2544
+ renderedBreakdownList = true;
2327
2545
  }
2328
2546
  }
2329
- // Metric results summary (fallback context when no table rendered)
2330
- if (!renderedBreakdownTable &&
2547
+ // Metric results summary (fallback context when no list rendered)
2548
+ if (!renderedBreakdownList &&
2331
2549
  metricResponse.metricResult &&
2332
2550
  metricResponse.metricResult.length > 0) {
2333
2551
  const resultDetails = [];