@oneuptime/common 14.0.8 → 14.0.9

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 (639) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  4. package/Models/DatabaseModels/Project.ts +55 -5
  5. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  6. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  7. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  8. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  9. package/Server/API/AIChatAPI.ts +6 -3
  10. package/Server/API/AIInvestigationAPI.ts +17 -10
  11. package/Server/API/AutoRemediationAPI.ts +207 -25
  12. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  13. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  14. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  15. package/Server/Infrastructure/GlobalCache.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/TelemetryIngest.ts +41 -18
  21. package/Server/Services/AIService.ts +70 -1
  22. package/Server/Services/AlertService.ts +352 -11
  23. package/Server/Services/AlertSeverityService.ts +65 -15
  24. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  25. package/Server/Services/IncidentService.ts +280 -104
  26. package/Server/Services/IncidentSeverityService.ts +66 -15
  27. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  28. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  29. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  30. package/Server/Services/KubernetesClusterService.ts +129 -21
  31. package/Server/Services/ProjectService.ts +32 -0
  32. package/Server/Services/ProxmoxResourceService.ts +322 -5
  33. package/Server/Services/RunnerJobService.ts +241 -104
  34. package/Server/Services/RunnerService.ts +1 -1
  35. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  36. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  37. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  38. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  39. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  40. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  41. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  42. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  43. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  44. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  45. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  46. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  47. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  48. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  49. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  50. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  51. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  52. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  53. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  54. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  55. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  56. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  57. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  58. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  59. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  60. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  61. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  62. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  63. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  64. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  65. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  66. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  67. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  68. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  69. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  70. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  71. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  72. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  73. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  74. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  75. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  76. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  77. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  78. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  79. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  80. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  81. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  82. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  83. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  84. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  85. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  86. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  87. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  88. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  89. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  90. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  91. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  92. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  93. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  94. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  95. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  96. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  97. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  98. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  99. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  100. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  101. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  102. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  103. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  104. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  105. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  106. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  107. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  108. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  109. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  110. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  111. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  112. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  113. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  114. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  115. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  116. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  117. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  118. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  119. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  120. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  121. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  122. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  123. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  124. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  125. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  126. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  127. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  128. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  129. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  130. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  131. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  132. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  133. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  134. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  135. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  136. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  137. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  138. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  139. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  140. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  141. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  142. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  143. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  144. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  145. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  146. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  147. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  148. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  149. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  150. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  151. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  152. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  153. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  154. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  155. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  156. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  157. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  158. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  159. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  160. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  161. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  162. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  163. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  164. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  165. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  166. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  167. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  168. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  169. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  170. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  171. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  172. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  173. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  174. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  175. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  176. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  177. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  178. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  179. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  180. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  181. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  182. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  183. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  184. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  185. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  186. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  187. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  188. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  189. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  190. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  191. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  192. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  193. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  194. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  195. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  196. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  197. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  198. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  199. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  200. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  201. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  202. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  203. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  204. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  205. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  206. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  207. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  208. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  209. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  210. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  211. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  212. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  213. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  214. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  215. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  216. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  217. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  218. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  219. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  220. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  221. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  222. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  223. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  224. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  225. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  226. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  227. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  228. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  229. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  230. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  231. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  232. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  233. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  234. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  235. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  236. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  237. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  238. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  239. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  240. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  241. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  242. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  243. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  244. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  245. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  246. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  247. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  248. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  249. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  250. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  251. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  252. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  253. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  254. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  255. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  256. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  257. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  258. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  259. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  260. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  261. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  262. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  263. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  264. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  265. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  266. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  267. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  268. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  269. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  270. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  271. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  272. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  273. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  274. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  275. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  276. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  277. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  278. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  279. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  280. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  281. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  282. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  283. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  284. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  285. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  286. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  287. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  288. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  289. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  290. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  291. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  292. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  293. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  294. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  295. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  296. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  297. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  298. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  299. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  300. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  301. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  302. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  303. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  304. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  305. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  306. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  307. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  308. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  309. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  310. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  311. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  312. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  313. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  314. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  315. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  316. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  317. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  318. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  319. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  320. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  321. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  322. package/Tests/UI/Components/Input.test.tsx +80 -1
  323. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  324. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  325. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  326. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  327. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  328. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  329. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  330. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  331. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  332. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  333. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  334. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  335. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  336. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  337. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  338. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  339. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  340. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  341. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  342. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  343. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  344. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  345. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  346. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  347. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  348. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  349. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  350. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  351. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  352. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  353. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  354. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  355. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  356. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  357. package/Types/AI/AIRunType.ts +9 -4
  358. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  359. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  360. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  361. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  362. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  363. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  364. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  365. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  366. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  367. package/Types/Team/ComplianceRule.ts +434 -0
  368. package/Types/Team/ComplianceRuleType.ts +21 -0
  369. package/Types/Team/TeamComplianceStatus.ts +81 -0
  370. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  371. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  372. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  373. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  374. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  375. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  376. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  377. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  378. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  379. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  380. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  381. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  382. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  383. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  384. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  385. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  386. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  387. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  388. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  389. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  390. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  391. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  392. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  393. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  394. package/UI/Components/Input/Input.tsx +90 -57
  395. package/UI/Components/List/ListRow.tsx +8 -74
  396. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  397. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  398. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  399. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  400. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  401. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  402. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  403. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  404. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  405. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  406. package/UI/Components/Table/TableBody.tsx +8 -2
  407. package/UI/Components/Table/TableRow.tsx +20 -140
  408. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  409. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  410. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  411. package/UI/Components/TelemetryViewer/types.ts +2 -0
  412. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  413. package/UI/Components/Toggle/Toggle.tsx +26 -0
  414. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  415. package/UI/Styles/Theme.css +14 -2
  416. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  417. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  418. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  419. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  420. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  421. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  422. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  423. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  424. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  425. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  426. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  427. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  428. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  429. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  430. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  431. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  432. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  433. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  434. package/build/dist/Server/API/AIChatAPI.js +6 -1
  435. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  436. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  437. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  438. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  439. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  440. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  441. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  442. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  443. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  444. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  445. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  446. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  447. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  456. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  457. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  458. package/build/dist/Server/Services/AIService.js +59 -1
  459. package/build/dist/Server/Services/AIService.js.map +1 -1
  460. package/build/dist/Server/Services/AlertService.js +248 -13
  461. package/build/dist/Server/Services/AlertService.js.map +1 -1
  462. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  463. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  464. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  465. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  466. package/build/dist/Server/Services/IncidentService.js +199 -72
  467. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  468. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  469. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  470. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  471. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  472. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  473. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  474. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  475. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  476. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  477. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  478. package/build/dist/Server/Services/ProjectService.js +22 -0
  479. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  480. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  481. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  482. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  483. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  484. package/build/dist/Server/Services/RunnerService.js +1 -1
  485. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  486. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  487. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  489. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  490. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  491. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  492. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  493. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  494. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  495. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  496. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  497. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  498. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  499. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  500. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  501. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  502. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  503. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  504. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  505. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  506. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  507. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  508. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  509. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  510. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  512. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  513. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  514. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  515. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  516. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  517. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  518. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  519. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  520. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  521. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  522. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  523. package/build/dist/Types/AI/AIRunType.js +9 -4
  524. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  525. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  526. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  527. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  528. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  529. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  530. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  531. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  532. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  533. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  534. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  535. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  536. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  537. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  538. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  539. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  540. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  541. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  542. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  543. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  544. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  545. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  546. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  547. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  548. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  549. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  550. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  551. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  552. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  553. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  554. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  556. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  558. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  560. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  562. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  564. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  570. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  571. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  572. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  574. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  575. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  576. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  578. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  579. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  580. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  582. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  583. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  584. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  585. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  586. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  587. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  588. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  589. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  590. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  591. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  592. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  593. package/build/dist/UI/Components/Input/Input.js +69 -54
  594. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  595. package/build/dist/UI/Components/List/ListRow.js +4 -39
  596. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  597. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  598. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  600. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  601. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  602. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  603. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  604. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  605. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  606. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  607. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  608. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  609. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  610. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  611. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  612. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  613. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  614. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  615. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  616. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  617. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  618. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  619. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  620. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  621. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  622. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  626. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  627. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  628. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  629. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  630. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  631. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  632. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  633. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  634. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  635. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  636. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  637. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  638. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  639. package/package.json +1 -1
@@ -18,12 +18,14 @@ import Version from "../../Types/Version";
18
18
  import { getDeletedAgentRunnerRebindNote } from "../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
19
19
  import RunbookCredentialType from "../../Types/Runbook/RunbookCredentialType";
20
20
  import { RUNNER_ALIVE_WINDOW_IN_MINUTES, RunnerLiveStatus, getRunnerLiveStatus, } from "../../Types/Runner/RunnerLiveStatus";
21
- import { KUBERNETES_AGENT_RUNNER_NAME_PREFIX, KubernetesAiRemediationMode, isInClusterPostureForCluster, isSameKubernetesClusterIdentifier, normalizeKubernetesClusterIdentifier, parseKubernetesRunnerPosture, } from "../../Types/Kubernetes/KubernetesClusterAiAccess";
21
+ import { KUBECTL_ALLOW_WRITES_ENV, KUBECTL_WRITE_NAMESPACES_ENV, KUBERNETES_AGENT_RUNNER_NAME_PREFIX, KUBERNETES_AI_AGENT_COMPONENT, KUBERNETES_AI_AGENT_DISPLAY_NAME, KubernetesAiRemediationMode, isInClusterPostureForCluster, isSameKubernetesClusterIdentifier, normalizeKubernetesClusterIdentifier, parseKubernetesAgentPosture, parseKubernetesRunnerPosture, } from "../../Types/Kubernetes/KubernetesClusterAiAccess";
22
22
  import { KubernetesClusterFeedEventType } from "../../Models/DatabaseModels/KubernetesClusterFeed";
23
23
  import { Blue500, Green500, Yellow500 } from "../../Types/BrandColors";
24
24
  import QueryHelper from "../Types/Database/QueryHelper";
25
+ import AIService from "./AIService";
25
26
  import AlertService from "./AlertService";
26
27
  import IncidentService from "./IncidentService";
28
+ import KubernetesAiAgentService from "./KubernetesAiAgentService";
27
29
  import KubernetesClusterFeedService from "./KubernetesClusterFeedService";
28
30
  import KubernetesClusterService from "./KubernetesClusterService";
29
31
  import LlmProviderService from "./LlmProviderService";
@@ -73,15 +75,139 @@ export const CLUSTER_AI_ACCESS_SELECT = {
73
75
  };
74
76
  const MAX_CLUSTERS_PER_SUBJECT = 10;
75
77
  /*
76
- * What to do about an in-cluster Runner that reports read-only RBAC while
77
- * remediation is on. It points at the AI page's write-access section, which
78
- * carries the complete helm commands (a bare --set line would miss the
79
- * chart index refresh and --reuse-values), and names the two values that
80
- * bound what the write role reaches: the namespaces it is bound in and the
81
- * node switch. Shown verbatim on incident pages too, so it names the page
82
- * rather than saying "this page".
78
+ * Where every next step sends an operator: the cluster's AI agent page
79
+ * (AI → Agent in the cluster's side menu). Shown verbatim on incident pages
80
+ * and in the investigation panel too, so it always names the page rather
81
+ * than saying "this page".
83
82
  */
84
- export const REMEDIATION_WRITE_ACCESS_NEXT_STEP = 'Grant the in-cluster Runner write access with a helm upgrade that adds --set aiAccess.remediation.enabled=true; the complete commands are under "Let AI apply fixes (write access)" on the cluster\'s AI page. List the namespaces AI may fix in aiAccess.remediation.namespaces (without it the write role is bound cluster-wide), and add aiAccess.remediation.nodeOperations=false to keep fixes off nodes.';
83
+ export const CLUSTER_AI_AGENT_PAGE = "the cluster's AI agent page (AI → Agent)";
84
+ /*
85
+ * The one command that installs the Kubernetes AI agent (or turns it back
86
+ * on), exactly as the dashboard, the docs and the chart's notes print it.
87
+ * --reuse-values keeps everything else the release already has.
88
+ */
89
+ export const AI_AGENT_INSTALL_COMMAND = [
90
+ "helm repo update",
91
+ "helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\",
92
+ " --namespace oneuptime-agent --reuse-values \\",
93
+ " --set aiAgent.enabled=true",
94
+ ].join("\n");
95
+ /*
96
+ * How to read the agent pod's logs. The namespace is the one the agent
97
+ * reported (posture.podNamespace) when it is known, a placeholder otherwise.
98
+ */
99
+ export function getAiAgentLogsCommand(namespace) {
100
+ return `kubectl logs -n ${(namespace === null || namespace === void 0 ? void 0 : namespace.trim()) || "<namespace>"} -l component=${KUBERNETES_AI_AGENT_COMPONENT} --tail=100`;
101
+ }
102
+ /*
103
+ * The next step for a cluster no access target can reach: install the
104
+ * agent, with the command verbatim, or check its pod if that was done.
105
+ */
106
+ export const AI_AGENT_NOT_CONNECTED_NEXT_STEP = `Install the Kubernetes AI agent (use your own release name and namespace if they differ):\n\n${AI_AGENT_INSTALL_COMMAND}\n\nAlready installed? Check its logs: ${getAiAgentLogsCommand()}`;
107
+ /*
108
+ * What to do about a read-only executor while remediation is on. For the
109
+ * agent it names the value that grants writes and the two that bound them
110
+ * (the namespaces the write role is bound in and the node switch); the
111
+ * complete command is on the AI agent page (a bare --set line would miss
112
+ * the chart index refresh and --reuse-values).
113
+ */
114
+ export const REMEDIATION_WRITE_ACCESS_NEXT_STEP = `Upgrade the Kubernetes agent chart with --set aiAgent.remediation.enabled=true; the complete command is on ${CLUSTER_AI_AGENT_PAGE}. List the namespaces AI may fix in aiAgent.remediation.namespaces (without it the write role is cluster-wide), and add aiAgent.remediation.nodeOperations=false to keep fixes off nodes.`;
115
+ // The same for the previous in-cluster Runner: the AI agent replaces it.
116
+ export const LEGACY_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP = `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over. Add --set aiAgent.remediation.enabled=true to the upgrade to let it apply fixes; the complete command is on ${CLUSTER_AI_AGENT_PAGE}.`;
117
+ // The same for a Runner no chart configures: its own environment.
118
+ export const CREDENTIAL_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP = `Set ${KUBECTL_ALLOW_WRITES_ENV}=true on the Runner's host and restart it (${KUBECTL_WRITE_NAMESPACES_ENV} limits the namespaces it may change).`;
119
+ export function getRemediationWriteAccessNextStep(kind) {
120
+ switch (kind) {
121
+ case "ai_agent":
122
+ return REMEDIATION_WRITE_ACCESS_NEXT_STEP;
123
+ case "legacy_runner":
124
+ return LEGACY_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP;
125
+ default:
126
+ return CREDENTIAL_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP;
127
+ }
128
+ }
129
+ // The id jobs and plans name the target by (runnerId), or null for none.
130
+ export function getKubernetesAiAccessTargetId(target) {
131
+ var _a, _b;
132
+ switch (target.type) {
133
+ case "ai_agent":
134
+ return ((_a = target.agent.id) === null || _a === void 0 ? void 0 : _a.toString()) || null;
135
+ case "advanced_runner":
136
+ case "legacy_runner":
137
+ return ((_b = target.runner.id) === null || _b === void 0 ? void 0 : _b.toString()) || null;
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+ /*
143
+ * THE rule for which access target AI uses for a cluster. Liveness based,
144
+ * so a helm upgrade to the AI agent switches over the moment the agent is
145
+ * online, and a rollback that stops the agent falls back on its own to the
146
+ * previous in-cluster Runner the cluster is still bound to — no dashboard
147
+ * step either way:
148
+ *
149
+ * 1. a bound Runner that is not a kubernetes-agent row: the advanced Runner
150
+ * an operator chose (with its Kubernetes credential). An explicit
151
+ * operator choice wins over everything the chart installs.
152
+ * 2. the cluster's AI agent, when online.
153
+ * 3. the bound Runner, when it is THIS cluster's previous in-cluster Runner
154
+ * and online (the agent is not, or was never installed).
155
+ * 4. the cluster's AI agent, offline (its offline gap says what to check).
156
+ * 5. the bound previous in-cluster Runner, offline.
157
+ * 6. nothing.
158
+ *
159
+ * A bound Runner that is another cluster's kubernetes-agent row is none of
160
+ * these: its ServiceAccount can never reach this cluster. Pure: liveness is
161
+ * decided by the caller (isRunnerOnline, KubernetesAiAgentService.isOnline).
162
+ */
163
+ export function resolveKubernetesAiAccessTarget(data) {
164
+ /*
165
+ * Only the row the binding names: a row with another id (a stale read)
166
+ * is not the bound Runner.
167
+ */
168
+ const boundRunner = data.aiAccessRunnerId &&
169
+ data.boundRunner &&
170
+ (!data.boundRunner.id ||
171
+ data.boundRunner.id.toString() === data.aiAccessRunnerId.toString())
172
+ ? data.boundRunner
173
+ : null;
174
+ if (boundRunner &&
175
+ !RunnerServiceClass.isKubernetesAgentRunnerRow(boundRunner)) {
176
+ return {
177
+ type: "advanced_runner",
178
+ runner: boundRunner,
179
+ isOnline: data.isBoundRunnerOnline,
180
+ };
181
+ }
182
+ const legacyRunner = boundRunner &&
183
+ RunnerServiceClass.isKubernetesAgentRunnerOfCluster(boundRunner, data.clusterIdentifier)
184
+ ? boundRunner
185
+ : null;
186
+ if (data.agent && data.isAgentOnline) {
187
+ return { type: "ai_agent", agent: data.agent, isOnline: true };
188
+ }
189
+ if (legacyRunner && data.isBoundRunnerOnline) {
190
+ return { type: "legacy_runner", runner: legacyRunner, isOnline: true };
191
+ }
192
+ if (data.agent) {
193
+ return { type: "ai_agent", agent: data.agent, isOnline: false };
194
+ }
195
+ if (legacyRunner) {
196
+ return { type: "legacy_runner", runner: legacyRunner, isOnline: false };
197
+ }
198
+ return { type: "none" };
199
+ }
200
+ // What target resolution and the status read off a bound Runner row.
201
+ const BOUND_RUNNER_SELECT = {
202
+ _id: true,
203
+ name: true,
204
+ lastAlive: true,
205
+ connectionStatus: true,
206
+ canRunAiCommands: true,
207
+ canRunRunbooks: true,
208
+ canRunCodeFixTasks: true,
209
+ hostInfo: true,
210
+ };
85
211
  /*
86
212
  * Why deleting an in-cluster Runner is a two-step remedy (the helper lives
87
213
  * with the permissions it names, so RunnerService can use it too).
@@ -108,6 +234,17 @@ export class KubernetesAgentRegistrationRefusedException extends ForbiddenExcept
108
234
  this.retryAfterSeconds = data.retryAfterSeconds;
109
235
  }
110
236
  }
237
+ /*
238
+ * How long a previous in-cluster Runner refused because the cluster's
239
+ * Kubernetes AI agent is online waits before it asks again. The refusal
240
+ * lasts only while the agent is online, so the Runner keeps asking — slowly
241
+ * — and is admitted within a minute of the agent stopping (a helm rollback).
242
+ */
243
+ export const SUPERSEDED_BY_AI_AGENT_RETRY_AFTER_SECONDS = 60;
244
+ // Why the previous in-cluster Runner was refused: the AI agent replaces it.
245
+ export function getSupersededByAiAgentMessage(clusterIdentifier) {
246
+ return `The Kubernetes AI agent of cluster "${clusterIdentifier}" is online and replaces this in-cluster Runner, so the Runner was not registered. Nothing needs to be done: upgrading the Kubernetes agent chart removes the Runner, and it is admitted again only if the AI agent stops (for example after a helm rollback).`;
247
+ }
111
248
  /*
112
249
  * When a registration refused because the previous instance is online may
113
250
  * try again: once that instance's last heartbeat is older than the alive
@@ -121,6 +258,8 @@ export function getPreviousInstanceRetryAfterSeconds(data) {
121
258
  const now = (data.now || OneUptimeDate.getCurrentDate()).getTime();
122
259
  return Math.max(1, Math.ceil((admittedAt - now) / 1000));
123
260
  }
261
+ // What to do about an empty AI balance (ai_balance_insufficient).
262
+ export const AI_BALANCE_INSUFFICIENT_NEXT_STEP = "Add AI credits under Project Settings → AI Credits (or enable auto-recharge).";
124
263
  class KubernetesClusterAiAccessServiceClass {
125
264
  /*
126
265
  * ------------------------------------------------------------------
@@ -134,24 +273,51 @@ class KubernetesClusterAiAccessServiceClass {
134
273
  enableAi: true,
135
274
  enableAutoRemediation: true,
136
275
  enableAiCommandExecution: true,
276
+ enableAutomaticIncidentInvestigation: true,
277
+ enableAutomaticAlertInvestigation: true,
137
278
  },
138
279
  props: { isRoot: true },
139
280
  });
140
- let hasLlmProvider = false;
281
+ // undefined: the lookup failed, so neither it nor the balance is known.
282
+ let llmProvider = undefined;
141
283
  try {
142
- hasLlmProvider =
143
- (await LlmProviderService.getLLMProviderForProject(projectId)) !== null;
284
+ llmProvider =
285
+ await LlmProviderService.getLLMProviderForProject(projectId);
144
286
  }
145
287
  catch (error) {
146
288
  logger.error(`KubernetesClusterAiAccess: could not resolve the LLM provider for project ${projectId.toString()}: ${error}`);
147
289
  }
290
+ /*
291
+ * An empty balance is a precondition like the provider: when it cannot
292
+ * be checked, the status says nothing about it rather than claiming a
293
+ * blocker that may not exist (the AI call itself still refuses). The
294
+ * provider resolved above is handed over, so it is looked up once.
295
+ */
296
+ let aiBalanceBlocker = null;
297
+ if (llmProvider !== undefined) {
298
+ try {
299
+ aiBalanceBlocker = await AIService.getAiBalanceBlocker({
300
+ projectId,
301
+ llmProvider,
302
+ });
303
+ }
304
+ catch (error) {
305
+ logger.error(`KubernetesClusterAiAccess: could not check the AI balance of project ${projectId.toString()}: ${error}`);
306
+ }
307
+ }
148
308
  return {
149
309
  // Kill switches: a missing column counts as enabled (=== false idiom).
150
310
  isAiEnabled: (project === null || project === void 0 ? void 0 : project.enableAi) !== false,
151
311
  isAutoRemediationEnabled: (project === null || project === void 0 ? void 0 : project.enableAutoRemediation) !== false,
152
312
  // Explicit opt-in (=== true idiom), same as the Bash/SSH lane.
153
313
  isAiCommandExecutionEnabled: (project === null || project === void 0 ? void 0 : project.enableAiCommandExecution) === true,
154
- hasLlmProvider,
314
+ // A failed lookup reads as none, like no provider at all.
315
+ hasLlmProvider: Boolean(llmProvider),
316
+ aiBalanceBlocker,
317
+ automaticInvestigation: {
318
+ incidents: (project === null || project === void 0 ? void 0 : project.enableAutomaticIncidentInvestigation) === true,
319
+ alerts: (project === null || project === void 0 ? void 0 : project.enableAutomaticAlertInvestigation) === true,
320
+ },
155
321
  };
156
322
  }
157
323
  async getStatusForCluster(data) {
@@ -172,226 +338,178 @@ class KubernetesClusterAiAccessServiceClass {
172
338
  });
173
339
  }
174
340
  /*
175
- * The readiness computation proper. Reads the bound Runner and credential
176
- * as root (the cluster row is already tenant-scoped by the caller) and
177
- * folds project gates in. Never throws: a lookup failure becomes a gap,
178
- * because "we could not check" must not read as "ready".
341
+ * ------------------------------------------------------------------
342
+ * The access target
343
+ * ------------------------------------------------------------------
179
344
  */
180
- async getStatusForClusterModel(data) {
181
- var _a, _b, _c;
182
- const { cluster, gates } = data;
183
- const gaps = [];
184
- const remediationMode = this.normalizeRemediationMode(cluster.aiRemediationMode);
185
- const isInvestigationEnabled = cluster.isAiInvestigationEnabled === true;
186
- let runnerSummary = null;
187
- let accessMethod = "none";
188
- let credentialId = undefined;
189
- let credentialName = undefined;
190
- if (!cluster.aiAccessRunnerId) {
191
- gaps.push(await this.getNoRunnerBoundGap(cluster));
345
+ /*
346
+ * Is this Runner online right now? One rule for readiness, target
347
+ * resolution and registration: it heartbeated within the alive window
348
+ * (RunnerLiveStatus) AND it has not signed off since. A Runner signs off
349
+ * on a clean shutdown (/runner-ingest/disconnect), which is what a helm
350
+ * upgrade or uninstall does to the chart's in-cluster Runner pod — it
351
+ * must read offline at once, not stay "Connected" until its last
352
+ * heartbeat ages out while investigations queue kubectl it will never
353
+ * claim.
354
+ */
355
+ isRunnerOnline(runner) {
356
+ return this.getRunnerPresence(runner).isOnline;
357
+ }
358
+ /*
359
+ * resolveKubernetesAiAccessTarget with each row's liveness read the one
360
+ * way this module reads it.
361
+ */
362
+ resolveAccessTarget(cluster, data) {
363
+ return resolveKubernetesAiAccessTarget({
364
+ clusterIdentifier: cluster.clusterIdentifier,
365
+ aiAccessRunnerId: cluster.aiAccessRunnerId,
366
+ boundRunner: data.boundRunner,
367
+ isBoundRunnerOnline: data.boundRunner
368
+ ? this.isRunnerOnline(data.boundRunner)
369
+ : false,
370
+ agent: data.agentRow,
371
+ isAgentOnline: data.agentRow
372
+ ? KubernetesAiAgentService.isOnline(data.agentRow)
373
+ : false,
374
+ });
375
+ }
376
+ /*
377
+ * Read what the resolution needs — the bound Runner row and the cluster's
378
+ * agent row (unless the caller already has it) — and resolve. Lookup
379
+ * failures propagate: the enqueue chokepoint must fail closed on a target
380
+ * it could not check.
381
+ */
382
+ async loadAccessTarget(data) {
383
+ const { cluster } = data;
384
+ if (!cluster.id || !cluster.projectId) {
385
+ throw new BadDataException("The cluster's id and project are needed to resolve its AI access target.");
192
386
  }
193
- else {
194
- const runner = await RunnerService.findOneBy({
387
+ const agent = data.agentRow !== undefined
388
+ ? data.agentRow
389
+ : await KubernetesAiAgentService.findForCluster({
390
+ projectId: cluster.projectId,
391
+ kubernetesClusterId: cluster.id,
392
+ });
393
+ const boundRunner = cluster.aiAccessRunnerId
394
+ ? await RunnerService.findOneBy({
195
395
  query: {
196
396
  _id: cluster.aiAccessRunnerId.toString(),
197
397
  projectId: cluster.projectId,
198
398
  },
199
- select: {
200
- _id: true,
201
- name: true,
202
- lastAlive: true,
203
- connectionStatus: true,
204
- canRunAiCommands: true,
205
- // What would stop an offline agent row from re-registering.
206
- canRunRunbooks: true,
207
- canRunCodeFixTasks: true,
208
- hostInfo: true,
209
- },
399
+ select: BOUND_RUNNER_SELECT,
210
400
  props: { isRoot: true },
211
- });
212
- if (!runner) {
213
- /*
214
- * Only a race reaches this: the binding's foreign key is ON DELETE
215
- * SET NULL, so deleting the bound Runner clears aiAccessRunnerId in
216
- * the same statement, and the next read takes the no_runner_bound
217
- * branch above. This is the window between reading the cluster row
218
- * and reading its Runner while a delete lands.
219
- */
220
- gaps.push({
221
- code: "runner_missing",
222
- title: "The bound Runner was just deleted",
223
- description: "The Runner this cluster was bound to was deleted while its status was being read.",
224
- nextStep: "Reload the cluster's AI page, then bind another Runner there, or reinstall the in-cluster Runner with --set aiAccess.enabled=true.",
225
- blocks: "both",
226
- });
227
- }
228
- else {
229
- const posture = parseKubernetesRunnerPosture(runner.hostInfo);
230
- const presence = this.getRunnerPresence(runner);
231
- const isOnline = presence.isOnline;
232
- // The one "is an agent row" rule: the name marker or an agent posture.
233
- const isAgentRunnerRow = RunnerServiceClass.isKubernetesAgentRunnerRow(runner);
234
- const isAgentRunner = isAgentRunnerRow || (posture === null || posture === void 0 ? void 0 : posture.inCluster) === true;
235
- runnerSummary = {
236
- id: runner.id.toString(),
237
- name: runner.name || "Runner",
238
- isOnline,
239
- lastAliveAt: runner.lastAlive
240
- ? OneUptimeDate.toString(runner.lastAlive)
241
- : undefined,
242
- canRunAiCommands: runner.canRunAiCommands === true,
243
- posture,
244
- };
245
- if (!isOnline) {
246
- /*
247
- * An offline agent Runner of THIS cluster that holds more than the
248
- * agent defaults will be refused on every re-registration its
249
- * restarted pod attempts (it cannot present the key it held), so
250
- * "it reconnects within a minute" would be false: say what blocks
251
- * it instead.
252
- */
253
- const holdingsGap = RunnerServiceClass.isKubernetesAgentRunnerOfCluster(runner, cluster.clusterIdentifier)
254
- ? await this.getAgentRunnerHoldingsGap({ runner, cluster })
255
- : null;
256
- gaps.push(holdingsGap ||
257
- this.getRunnerOfflineGap({ runner, presence, isAgentRunner }));
258
- }
259
- if (runner.canRunAiCommands !== true) {
260
- gaps.push({
261
- code: "runner_ai_commands_disabled",
262
- title: "The Runner does not accept AI commands",
263
- description: `"Runs AI Remediation Commands" is turned off on Runner "${runner.name}", so it will not be served kubectl work.`,
264
- nextStep: 'Turn on "Runs AI Remediation Commands" on the Runner (Project Settings → Runners).',
265
- blocks: "both",
266
- });
267
- }
268
- /*
269
- * In-cluster access means "kubectl with the pod's own
270
- * ServiceAccount", which reaches whatever cluster the pod lives
271
- * in. So it is usable only when the Runner's posture names THIS
272
- * cluster — a Runner that is in-cluster somewhere else (the
273
- * dashboard lets an operator pick any Runner) would run every
274
- * command for this cluster against the wrong one.
275
- */
276
- const isInClusterForThisCluster = isInClusterPostureForCluster(posture, cluster.clusterIdentifier);
277
- if (isInClusterForThisCluster) {
278
- accessMethod = "in_cluster";
279
- }
280
- else if (cluster.aiAccessCredentialId && isAgentRunnerRow) {
281
- /*
282
- * A kubernetes-agent Runner is never handed credential material:
283
- * its row is minted and re-keyed with the project's telemetry
284
- * ingestion key, so a credential it carried would be one leaked
285
- * ingestion key away from anyone. The claim path refuses to
286
- * resolve one for it (by the same name-or-posture rule);
287
- * readiness says so instead of promising access that would fail
288
- * on every command.
289
- */
290
- gaps.push({
291
- code: "credential_on_agent_runner",
292
- title: "An in-cluster Runner cannot carry a credential",
293
- description: `Runner "${runner.name}" is ${((_a = posture === null || posture === void 0 ? void 0 : posture.clusterIdentifier) === null || _a === void 0 ? void 0 : _a.trim())
294
- ? `the in-cluster Runner of cluster "${posture.clusterIdentifier.trim()}"`
295
- : "a Kubernetes agent's in-cluster Runner"}. It runs kubectl with its own ServiceAccount only and is never given a credential, so the Kubernetes credential selected for this cluster cannot be used through it.`,
296
- nextStep: "Create a Runner under Project Settings → Runners, assign the Kubernetes credential to it and select both on the cluster's AI page — or install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true, which needs no credential.",
297
- blocks: "both",
298
- });
299
- }
300
- else if (cluster.aiAccessCredentialId) {
301
- const credential = await RunbookCredentialService.findOneBy({
302
- query: {
303
- _id: cluster.aiAccessCredentialId.toString(),
304
- projectId: cluster.projectId,
305
- },
306
- select: {
307
- _id: true,
308
- name: true,
309
- credentialType: true,
310
- runners: { _id: true },
311
- },
312
- props: { isRoot: true },
313
- });
314
- const isAssignedToRunner = Boolean((_b = credential === null || credential === void 0 ? void 0 : credential.runners) === null || _b === void 0 ? void 0 : _b.some((assigned) => {
315
- var _a, _b;
316
- return ((_a = assigned.id) === null || _a === void 0 ? void 0 : _a.toString()) === ((_b = runner.id) === null || _b === void 0 ? void 0 : _b.toString());
317
- }));
318
- if (!credential ||
319
- credential.credentialType !== RunbookCredentialType.Kubernetes ||
320
- !isAssignedToRunner) {
321
- gaps.push({
322
- code: "credential_missing",
323
- title: "The Kubernetes credential is not usable by the Runner",
324
- description: !credential
325
- ? "The credential bound to this cluster no longer exists."
326
- : credential.credentialType !== RunbookCredentialType.Kubernetes
327
- ? `"${credential.name}" is not a Kubernetes credential.`
328
- : `"${credential.name}" is not assigned to Runner "${runner.name}".`,
329
- nextStep: "Create a Kubernetes credential (API server URL + ServiceAccount token) under Project Settings → Runner Credentials, assign it to the Runner, and select it on the cluster's AI page.",
330
- blocks: "both",
331
- });
332
- }
333
- else {
334
- accessMethod = "credential";
335
- credentialId = credential.id.toString();
336
- credentialName = credential.name;
337
- }
338
- }
339
- else if (posture === null || posture === void 0 ? void 0 : posture.inCluster) {
340
- /*
341
- * In-cluster, but for another cluster (or for no named cluster
342
- * at all). Say so explicitly: the operator picked this Runner
343
- * believing "in-cluster" meant "in this cluster".
344
- */
345
- const reportedCluster = ((_c = posture.clusterIdentifier) === null || _c === void 0 ? void 0 : _c.trim()) || undefined;
346
- gaps.push({
347
- code: "runner_cluster_mismatch",
348
- title: reportedCluster
349
- ? "The bound Runner is the in-cluster Runner of a different cluster"
350
- : "The bound Runner did not report which cluster it runs in",
351
- description: reportedCluster
352
- ? `Runner "${runner.name}" runs inside cluster "${reportedCluster}", not "${cluster.clusterIdentifier || cluster.name || "this cluster"}". Its ServiceAccount would run every kubectl command against "${reportedCluster}", so OneUptime AI will not use it for this cluster.`
353
- : `Runner "${runner.name}" reports that it runs inside a Kubernetes cluster but not which one, so OneUptime AI cannot tell whether that is this cluster.`,
354
- nextStep: "Install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true and select it on the cluster's AI page as its Runner (a registering Runner never replaces the Runner a cluster is bound to), or bind a Runner that holds a Kubernetes credential for this cluster and select that credential on the cluster's AI page.",
355
- blocks: "both",
356
- });
357
- }
358
- else {
359
- gaps.push({
360
- code: "credential_missing",
361
- title: "The Runner has no credential for this cluster",
362
- description: `Runner "${runner.name}" runs outside the cluster, so it needs a Kubernetes credential to reach the API server.`,
363
- nextStep: "Select a Kubernetes credential assigned to this Runner on the cluster's AI page — or install the in-cluster Runner with --set aiAccess.enabled=true, which needs no credential.",
364
- blocks: "both",
365
- });
366
- }
367
- if (remediationMode !== KubernetesAiRemediationMode.Disabled &&
368
- (posture === null || posture === void 0 ? void 0 : posture.inCluster) &&
369
- posture.allowWrites !== true) {
370
- gaps.push({
371
- code: "remediation_write_access_missing",
372
- title: "The in-cluster Runner is read-only",
373
- description: "The Kubernetes agent was installed without write access, so kubectl changes would be refused by the cluster.",
374
- nextStep: REMEDIATION_WRITE_ACCESS_NEXT_STEP,
375
- blocks: "remediation",
376
- });
377
- }
378
- }
401
+ })
402
+ : null;
403
+ return {
404
+ target: this.resolveAccessTarget(cluster, {
405
+ boundRunner,
406
+ agentRow: agent,
407
+ }),
408
+ boundRunner,
409
+ agent,
410
+ };
411
+ }
412
+ /*
413
+ * This cluster's previous in-cluster Runner — the kubernetes-agent Runner
414
+ * row an older chart registered — or null. Found by its (bounded) name,
415
+ * matched case-insensitively like registration matches it; a row whose
416
+ * posture names a DIFFERENT cluster is not this cluster's (a shortened
417
+ * name's hash collision). Failing that, the cluster's bound Runner when it
418
+ * is this cluster's agent by posture.
419
+ *
420
+ * A failed lookup propagates rather than reading as none: the AI agent's
421
+ * registration asks this to keep a live Runner's cluster, and "could not
422
+ * tell" must refuse (the agent retries) — never admit an agent that then
423
+ * outranks a Runner that is still working. A caller that may treat "could
424
+ * not tell" as "nothing to do" catches it itself.
425
+ */
426
+ async getLegacyAgentRunnerForCluster(data) {
427
+ const clusterIdentifier = (data.clusterIdentifier || "").trim();
428
+ if (!clusterIdentifier) {
429
+ return null;
430
+ }
431
+ const byName = await RunnerService.findOneBy({
432
+ query: {
433
+ projectId: data.projectId,
434
+ name: QueryHelper.findWithSameText(getKubernetesAgentRunnerNameForCluster(clusterIdentifier)),
435
+ },
436
+ select: BOUND_RUNNER_SELECT,
437
+ props: { isRoot: true },
438
+ });
439
+ if (byName) {
440
+ return this.postureNamesAnotherCluster({
441
+ runner: byName,
442
+ clusterIdentifier,
443
+ })
444
+ ? null
445
+ : byName;
446
+ }
447
+ const cluster = await KubernetesClusterService.findOneBy({
448
+ query: {
449
+ _id: data.kubernetesClusterId.toString(),
450
+ projectId: data.projectId,
451
+ },
452
+ select: { _id: true, aiAccessRunnerId: true },
453
+ props: { isRoot: true },
454
+ });
455
+ if (!(cluster === null || cluster === void 0 ? void 0 : cluster.aiAccessRunnerId)) {
456
+ return null;
379
457
  }
458
+ const bound = await RunnerService.findOneBy({
459
+ query: {
460
+ _id: cluster.aiAccessRunnerId.toString(),
461
+ projectId: data.projectId,
462
+ },
463
+ select: BOUND_RUNNER_SELECT,
464
+ props: { isRoot: true },
465
+ });
466
+ return bound &&
467
+ RunnerServiceClass.isKubernetesAgentRunnerOfCluster(bound, clusterIdentifier)
468
+ ? bound
469
+ : null;
470
+ }
471
+ /*
472
+ * The readiness computation proper. Resolves the cluster's access target
473
+ * (resolveKubernetesAiAccessTarget), reads the bound Runner, the agent row
474
+ * and the credential as root (the cluster row is already tenant-scoped by
475
+ * the caller) and folds project gates in. Always sets aiAgent (the
476
+ * cluster's agent whether or not it is the target) and
477
+ * automaticInvestigation.
478
+ */
479
+ async getStatusForClusterModel(data) {
480
+ var _a, _b;
481
+ const { cluster, gates } = data;
482
+ const gaps = [];
483
+ const remediationMode = this.normalizeRemediationMode(cluster.aiRemediationMode);
484
+ const isInvestigationEnabled = cluster.isAiInvestigationEnabled === true;
485
+ const agentRow = data.aiAgentRow !== undefined
486
+ ? data.aiAgentRow
487
+ : await this.findAiAgentRowForStatus(cluster);
488
+ const loaded = await this.loadAccessTarget({
489
+ cluster,
490
+ agentRow,
491
+ });
492
+ const access = await this.getTargetAccess({
493
+ cluster,
494
+ loaded,
495
+ remediationMode,
496
+ });
497
+ gaps.push(...access.gaps);
380
498
  if (!isInvestigationEnabled) {
381
499
  gaps.push({
382
500
  code: "investigation_disabled",
383
501
  title: "AI investigation is turned off for this cluster",
384
502
  description: "OneUptime AI will investigate incidents and alerts on this cluster with OneUptime data only — it will not run kubectl.",
385
- nextStep: 'Turn on "Let AI investigate with kubectl" on the cluster\'s AI page.',
503
+ nextStep: `Turn on "Investigate with kubectl" on ${CLUSTER_AI_AGENT_PAGE}.`,
386
504
  blocks: "investigation",
387
505
  });
388
506
  }
389
507
  if (remediationMode === KubernetesAiRemediationMode.Disabled) {
390
508
  gaps.push({
391
509
  code: "remediation_disabled",
392
- title: "AI remediation is turned off for this cluster",
510
+ title: "AI fixes are turned off for this cluster",
393
511
  description: "OneUptime AI will diagnose but never propose or apply a fix on this cluster.",
394
- nextStep: 'Set "AI remediation" to "Ask for approval", "Automatic" or "Bypass approval" on the cluster\'s AI page.',
512
+ nextStep: `Set "Fixes" to "Ask for approval", "Automatic" or "Bypass approval" on ${CLUSTER_AI_AGENT_PAGE}.`,
395
513
  blocks: "remediation",
396
514
  });
397
515
  }
@@ -400,7 +518,7 @@ class KubernetesClusterAiAccessServiceClass {
400
518
  code: "project_ai_disabled",
401
519
  title: "AI is disabled for this project",
402
520
  description: "OneUptime AI is switched off at the project level.",
403
- nextStep: "Enable AI under Project Settings → AI.",
521
+ nextStep: "Enable AI under Project Settings → AI Features.",
404
522
  blocks: "both",
405
523
  });
406
524
  }
@@ -413,21 +531,38 @@ class KubernetesClusterAiAccessServiceClass {
413
531
  blocks: "both",
414
532
  });
415
533
  }
534
+ if (gates.aiBalanceBlocker) {
535
+ gaps.push({
536
+ code: "ai_balance_insufficient",
537
+ title: "The project is out of AI credits",
538
+ description: gates.aiBalanceBlocker,
539
+ nextStep: AI_BALANCE_INSUFFICIENT_NEXT_STEP,
540
+ blocks: "both",
541
+ });
542
+ }
416
543
  if (!gates.isAutoRemediationEnabled) {
417
544
  gaps.push({
418
545
  code: "project_auto_remediation_disabled",
419
546
  title: "Auto-remediation is disabled for this project",
420
547
  description: "The project-level auto-remediation kill switch is off, so no AI fix can be proposed or run.",
421
- nextStep: "Enable auto-remediation under Project Settings → AI.",
548
+ nextStep: "Enable auto-remediation under Project Settings → AI Features.",
422
549
  blocks: "remediation",
423
550
  });
424
551
  }
425
- if (!gates.isAiCommandExecutionEnabled) {
552
+ /*
553
+ * The project's command-execution opt-in governs AI-composed commands
554
+ * on Runners. A cluster reached through its Kubernetes AI agent (or the
555
+ * chart's previous in-cluster Runner) is governed by the cluster's own
556
+ * Fixes setting and the chart's write RBAC instead, so the opt-in only
557
+ * gates a cluster reached through an advanced Runner and credential.
558
+ */
559
+ if (loaded.target.type === "advanced_runner" &&
560
+ !gates.isAiCommandExecutionEnabled) {
426
561
  gaps.push({
427
562
  code: "project_ai_command_execution_disabled",
428
563
  title: "AI command execution is not enabled for this project",
429
- description: "AI-composed commands (including kubectl fixes) never run in a project that has not opted in.",
430
- nextStep: 'Turn on "Enable AI Command Execution" under Project Settings → AI.',
564
+ description: "This cluster is reached through a Runner, and AI-composed commands (kubectl fixes included) never run on a Runner in a project that has not opted in.",
565
+ nextStep: 'Turn on "Enable AI Command Execution" under Project Settings → AI Features.',
431
566
  blocks: "remediation",
432
567
  });
433
568
  }
@@ -441,10 +576,17 @@ class KubernetesClusterAiAccessServiceClass {
441
576
  clusterId: cluster.id.toString(),
442
577
  clusterName: cluster.name || cluster.clusterIdentifier || "cluster",
443
578
  clusterIdentifier: cluster.clusterIdentifier,
444
- runner: runnerSummary,
445
- accessMethod,
446
- credentialId,
447
- credentialName,
579
+ runner: access.runner,
580
+ accessMethod: access.accessMethod,
581
+ aiAgent: loaded.agent
582
+ ? KubernetesAiAgentService.toSummary(loaded.agent)
583
+ : null,
584
+ automaticInvestigation: {
585
+ incidents: ((_a = gates.automaticInvestigation) === null || _a === void 0 ? void 0 : _a.incidents) === true,
586
+ alerts: ((_b = gates.automaticInvestigation) === null || _b === void 0 ? void 0 : _b.alerts) === true,
587
+ },
588
+ credentialId: access.credentialId,
589
+ credentialName: access.credentialName,
448
590
  kubectlAllowlist: this.normalizeAllowlist(cluster.aiKubectlCommandAllowlist),
449
591
  isInvestigationEnabled,
450
592
  isInvestigationReady: isInvestigationEnabled && !blocksInvestigation,
@@ -460,18 +602,316 @@ class KubernetesClusterAiAccessServiceClass {
460
602
  };
461
603
  }
462
604
  /*
463
- * Is this Runner online right now? One rule for readiness and for
464
- * registration: it heartbeated within the alive window (RunnerLiveStatus)
465
- * AND it has not signed off since. A Runner signs off on a clean shutdown
466
- * (/runner-ingest/disconnect), which is what a helm uninstall or turning
467
- * aiAccess off does to the agent's Runner pod — it must read offline at
468
- * once, not stay "Connected" until its last heartbeat ages out while
469
- * investigations queue kubectl it will never claim. A freshly created
470
- * row is Disconnected with no heartbeat yet: that is "never connected",
471
- * not a sign-off.
605
+ * The cluster's agent row for a status. Never throws: a failed read is
606
+ * logged and reads as "no agent", which can only make the status say
607
+ * less is reachable (or fall back to a bound Runner), never more.
608
+ */
609
+ async findAiAgentRowForStatus(cluster) {
610
+ if (!cluster.id || !cluster.projectId) {
611
+ return null;
612
+ }
613
+ try {
614
+ return await KubernetesAiAgentService.findForCluster({
615
+ projectId: cluster.projectId,
616
+ kubernetesClusterId: cluster.id,
617
+ });
618
+ }
619
+ catch (error) {
620
+ logger.error(`KubernetesClusterAiAccess: could not read the Kubernetes AI agent of cluster ${cluster.id.toString()}: ${error}`);
621
+ return null;
622
+ }
623
+ }
624
+ // How the resolved target reaches the cluster, and what stops it.
625
+ async getTargetAccess(data) {
626
+ const { cluster, loaded, remediationMode } = data;
627
+ const target = loaded.target;
628
+ switch (target.type) {
629
+ case "ai_agent":
630
+ return this.getAgentAccess({
631
+ cluster,
632
+ agent: target.agent,
633
+ isOnline: target.isOnline,
634
+ remediationMode,
635
+ });
636
+ case "legacy_runner":
637
+ case "advanced_runner":
638
+ return this.getRunnerAccess({
639
+ cluster,
640
+ runner: target.runner,
641
+ isLegacy: target.type === "legacy_runner",
642
+ remediationMode,
643
+ });
644
+ default:
645
+ return {
646
+ runner: null,
647
+ accessMethod: "none",
648
+ gaps: [
649
+ /*
650
+ * A bound id with no Runner row is only the race with a Runner
651
+ * delete: the binding's foreign key is ON DELETE SET NULL, so
652
+ * the next read has no binding at all and reads as not
653
+ * connected.
654
+ */
655
+ cluster.aiAccessRunnerId && !loaded.boundRunner
656
+ ? {
657
+ code: "runner_missing",
658
+ title: "The bound Runner was just deleted",
659
+ description: "The Runner this cluster was bound to was deleted while its status was being read.",
660
+ nextStep: `Reload ${CLUSTER_AI_AGENT_PAGE}.`,
661
+ blocks: "both",
662
+ }
663
+ : {
664
+ code: "ai_agent_not_connected",
665
+ title: "The Kubernetes AI agent is not connected",
666
+ description: `OneUptime AI runs kubectl on this cluster through the Kubernetes AI agent, and no agent has connected for this cluster yet.${this.getIgnoredBoundRunnerNote({ cluster, boundRunner: loaded.boundRunner })}`,
667
+ nextStep: AI_AGENT_NOT_CONNECTED_NEXT_STEP,
668
+ blocks: "both",
669
+ },
670
+ ],
671
+ };
672
+ }
673
+ }
674
+ /*
675
+ * A Runner still bound to a cluster that has no access target is another
676
+ * cluster's in-cluster Runner (a kubernetes-agent row that is not this
677
+ * cluster's): its ServiceAccount can never reach this cluster, so it is
678
+ * ignored. Said, so the status does not read as if nothing were
679
+ * selected; installing the agent (the gap's step) is still the fix.
680
+ * Empty when nothing is bound.
681
+ */
682
+ getIgnoredBoundRunnerNote(data) {
683
+ var _a, _b;
684
+ const { cluster, boundRunner } = data;
685
+ if (!cluster.aiAccessRunnerId || !boundRunner) {
686
+ return "";
687
+ }
688
+ const reportedCluster = ((_b = (_a = parseKubernetesRunnerPosture(boundRunner.hostInfo)) === null || _a === void 0 ? void 0 : _a.clusterIdentifier) === null || _b === void 0 ? void 0 : _b.trim()) || undefined;
689
+ return ` Runner "${boundRunner.name || "Runner"}" is still selected for this cluster, but it is not this cluster's in-cluster Runner${reportedCluster ? ` (it reports cluster "${reportedCluster}")` : ""}, so OneUptime AI does not use it.`;
690
+ }
691
+ /*
692
+ * The cluster's Kubernetes AI agent as the target. It runs kubectl with
693
+ * its own ServiceAccount (accessMethod "in_cluster"), always accepts AI
694
+ * commands and never carries a credential.
695
+ */
696
+ getAgentAccess(data) {
697
+ const { cluster, agent, isOnline, remediationMode } = data;
698
+ const gaps = [];
699
+ const posture = parseKubernetesAgentPosture(agent.posture);
700
+ const runner = {
701
+ id: agent.id.toString(),
702
+ name: KUBERNETES_AI_AGENT_DISPLAY_NAME,
703
+ kind: "ai_agent",
704
+ isOnline,
705
+ lastAliveAt: agent.lastAliveAt
706
+ ? OneUptimeDate.toString(agent.lastAliveAt)
707
+ : undefined,
708
+ canRunAiCommands: true,
709
+ posture,
710
+ };
711
+ if (!isOnline) {
712
+ const lastAliveText = agent.lastAliveAt
713
+ ? OneUptimeDate.getDateAsFormattedString(agent.lastAliveAt)
714
+ : "an unknown time";
715
+ gaps.push({
716
+ code: "ai_agent_offline",
717
+ title: "The Kubernetes AI agent is offline",
718
+ description: agent.connectionStatus === "disconnected"
719
+ ? `The Kubernetes AI agent signed off after its last report at ${lastAliveText}: it was stopped, uninstalled, turned off with aiAgent.enabled=false, or reset, or its pod is being replaced.`
720
+ : `The Kubernetes AI agent last reported in at ${lastAliveText}.`,
721
+ nextStep: `Check the agent's pod: ${getAiAgentLogsCommand(posture === null || posture === void 0 ? void 0 : posture.podNamespace)}. It must be able to reach your OneUptime URL.`,
722
+ blocks: "both",
723
+ });
724
+ }
725
+ /*
726
+ * The server stamps the cluster's identifier on the agent's posture
727
+ * every time it reports, so this only fails for a cluster row without
728
+ * an identifier. The enqueue chokepoint refuses the same case, so the
729
+ * status must not call it ready.
730
+ */
731
+ const isInClusterForThisCluster = isInClusterPostureForCluster(posture, cluster.clusterIdentifier);
732
+ if (isOnline && !isInClusterForThisCluster) {
733
+ gaps.push({
734
+ code: "runner_cluster_mismatch",
735
+ title: "The Kubernetes AI agent has not reported this cluster",
736
+ description: "The agent's last report does not name this cluster, so OneUptime AI does not use it yet.",
737
+ nextStep: `Reset the agent on ${CLUSTER_AI_AGENT_PAGE}; it reconnects on its own within a few minutes.`,
738
+ blocks: "both",
739
+ });
740
+ }
741
+ if (remediationMode !== KubernetesAiRemediationMode.Disabled &&
742
+ (posture === null || posture === void 0 ? void 0 : posture.allowWrites) !== true) {
743
+ gaps.push({
744
+ code: "remediation_write_access_missing",
745
+ title: "The Kubernetes AI agent is read-only",
746
+ description: "The Kubernetes AI agent was installed without write access, so it cannot apply fixes.",
747
+ nextStep: getRemediationWriteAccessNextStep("ai_agent"),
748
+ blocks: "remediation",
749
+ });
750
+ }
751
+ return {
752
+ runner,
753
+ accessMethod: isInClusterForThisCluster ? "in_cluster" : "none",
754
+ gaps,
755
+ };
756
+ }
757
+ /*
758
+ * A Runner as the target: the chart's previous in-cluster Runner of this
759
+ * cluster (isLegacy), or a Runner an operator bound, which reaches the
760
+ * cluster with the cluster's Kubernetes credential.
472
761
  */
762
+ async getRunnerAccess(data) {
763
+ var _a;
764
+ const { cluster, runner, isLegacy, remediationMode } = data;
765
+ const gaps = [];
766
+ const posture = parseKubernetesRunnerPosture(runner.hostInfo);
767
+ const presence = this.getRunnerPresence(runner);
768
+ let accessMethod = "none";
769
+ let credentialId = undefined;
770
+ let credentialName = undefined;
771
+ const summary = {
772
+ id: runner.id.toString(),
773
+ name: runner.name || "Runner",
774
+ kind: "runner",
775
+ isOnline: presence.isOnline,
776
+ lastAliveAt: runner.lastAlive
777
+ ? OneUptimeDate.toString(runner.lastAlive)
778
+ : undefined,
779
+ canRunAiCommands: runner.canRunAiCommands === true,
780
+ posture,
781
+ };
782
+ if (!presence.isOnline) {
783
+ gaps.push(this.getRunnerOfflineGap({ runner, presence, isLegacy }));
784
+ }
785
+ if (runner.canRunAiCommands !== true) {
786
+ gaps.push({
787
+ code: "runner_ai_commands_disabled",
788
+ title: "The Runner does not accept AI commands",
789
+ description: `"Runs AI Remediation Commands" is turned off on Runner "${runner.name}", so it will not be served kubectl work.`,
790
+ nextStep: 'Turn on "Runs AI Remediation Commands" on the Runner (Project Settings → Runners).',
791
+ blocks: "both",
792
+ });
793
+ }
794
+ /*
795
+ * In-cluster access means "kubectl with the pod's own ServiceAccount",
796
+ * which reaches whatever cluster the pod lives in. So it is usable only
797
+ * when the Runner's posture names THIS cluster — a Runner that is
798
+ * in-cluster somewhere else would run every command for this cluster
799
+ * against the wrong one. The previous in-cluster Runner of this cluster
800
+ * ignores a credential still selected: in-cluster access wins.
801
+ */
802
+ const isInClusterForThisCluster = isInClusterPostureForCluster(posture, cluster.clusterIdentifier);
803
+ if (isInClusterForThisCluster) {
804
+ accessMethod = "in_cluster";
805
+ }
806
+ else if (isLegacy) {
807
+ /*
808
+ * This cluster's previous in-cluster Runner by its name, but its last
809
+ * report does not say it runs here (a heartbeat that dropped its
810
+ * posture). It is never given a credential, so it cannot be used —
811
+ * and the AI agent, which needs neither, is the way forward.
812
+ */
813
+ gaps.push({
814
+ code: "runner_cluster_mismatch",
815
+ title: "The previous in-cluster Runner has not reported this cluster",
816
+ description: `Runner "${runner.name}" has not reported that it runs in this cluster, so OneUptime AI does not use it.`,
817
+ nextStep: `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over.\n\n${AI_AGENT_INSTALL_COMMAND}`,
818
+ blocks: "both",
819
+ });
820
+ }
821
+ else if (cluster.aiAccessCredentialId) {
822
+ const credential = await RunbookCredentialService.findOneBy({
823
+ query: {
824
+ _id: cluster.aiAccessCredentialId.toString(),
825
+ projectId: cluster.projectId,
826
+ },
827
+ select: {
828
+ _id: true,
829
+ name: true,
830
+ credentialType: true,
831
+ runners: { _id: true },
832
+ },
833
+ props: { isRoot: true },
834
+ });
835
+ const isAssignedToRunner = Boolean((_a = credential === null || credential === void 0 ? void 0 : credential.runners) === null || _a === void 0 ? void 0 : _a.some((assigned) => {
836
+ var _a, _b;
837
+ return ((_a = assigned.id) === null || _a === void 0 ? void 0 : _a.toString()) === ((_b = runner.id) === null || _b === void 0 ? void 0 : _b.toString());
838
+ }));
839
+ if (!credential ||
840
+ credential.credentialType !== RunbookCredentialType.Kubernetes ||
841
+ !isAssignedToRunner) {
842
+ gaps.push({
843
+ code: "credential_missing",
844
+ title: "The Kubernetes credential is not usable by the Runner",
845
+ description: !credential
846
+ ? "The credential bound to this cluster no longer exists."
847
+ : credential.credentialType !== RunbookCredentialType.Kubernetes
848
+ ? `"${credential.name}" is not a Kubernetes credential.`
849
+ : `"${credential.name}" is not assigned to Runner "${runner.name}".`,
850
+ nextStep: `Create a Kubernetes credential (API server URL + ServiceAccount token) under Project Settings → Runner Credentials, assign it to the Runner, and select it on ${CLUSTER_AI_AGENT_PAGE}.`,
851
+ blocks: "both",
852
+ });
853
+ }
854
+ else {
855
+ accessMethod = "credential";
856
+ credentialId = credential.id.toString();
857
+ credentialName = credential.name;
858
+ }
859
+ }
860
+ else if (posture === null || posture === void 0 ? void 0 : posture.inCluster) {
861
+ /*
862
+ * In-cluster, but for no named cluster. Say so explicitly: the
863
+ * operator picked this Runner believing "in-cluster" meant "in this
864
+ * cluster". (A Runner that names a cluster reports an agent posture,
865
+ * so it is a kubernetes-agent row and never reaches this branch:
866
+ * another cluster's is no target at all — see
867
+ * getIgnoredBoundRunnerNote.)
868
+ */
869
+ gaps.push({
870
+ code: "runner_cluster_mismatch",
871
+ title: "The bound Runner did not report which cluster it runs in",
872
+ description: `Runner "${runner.name}" reports that it runs inside a Kubernetes cluster but not which one, so OneUptime AI cannot tell whether that is this cluster.`,
873
+ nextStep: `Clear the Runner on ${CLUSTER_AI_AGENT_PAGE} to use this cluster's Kubernetes AI agent, or select a Kubernetes credential for this cluster that is assigned to the Runner.`,
874
+ blocks: "both",
875
+ });
876
+ }
877
+ else {
878
+ gaps.push({
879
+ code: "credential_missing",
880
+ title: "The Runner has no credential for this cluster",
881
+ description: `Runner "${runner.name}" runs outside the cluster, so it needs a Kubernetes credential to reach the API server.`,
882
+ nextStep: `Select a Kubernetes credential assigned to this Runner on ${CLUSTER_AI_AGENT_PAGE} — or clear the Runner there to use the Kubernetes AI agent, which needs no credential.`,
883
+ blocks: "both",
884
+ });
885
+ }
886
+ if (remediationMode !== KubernetesAiRemediationMode.Disabled &&
887
+ (posture === null || posture === void 0 ? void 0 : posture.inCluster) &&
888
+ posture.allowWrites !== true) {
889
+ gaps.push({
890
+ code: "remediation_write_access_missing",
891
+ title: isLegacy
892
+ ? "The previous in-cluster Runner is read-only"
893
+ : "The Runner is read-only",
894
+ description: isLegacy
895
+ ? "The Kubernetes agent was installed without write access, so kubectl changes would be refused by the cluster."
896
+ : `Runner "${runner.name}" reports that it may not change the cluster, so kubectl changes would be refused.`,
897
+ nextStep: getRemediationWriteAccessNextStep(isLegacy ? "legacy_runner" : "credential_runner"),
898
+ blocks: "remediation",
899
+ });
900
+ }
901
+ return {
902
+ runner: summary,
903
+ accessMethod,
904
+ credentialId,
905
+ credentialName,
906
+ gaps,
907
+ };
908
+ }
473
909
  getRunnerPresence(runner) {
474
910
  const liveStatus = getRunnerLiveStatus(runner.lastAlive);
911
+ /*
912
+ * A freshly created row is Disconnected with no heartbeat yet: that is
913
+ * "never connected", not a sign-off.
914
+ */
475
915
  const isSignedOff = runner.connectionStatus === RunnerConnectionStatus.Disconnected &&
476
916
  liveStatus !== RunnerLiveStatus.NeverConnected;
477
917
  return {
@@ -481,22 +921,34 @@ class KubernetesClusterAiAccessServiceClass {
481
921
  };
482
922
  }
483
923
  getRunnerOfflineGap(data) {
484
- const { runner, presence, isAgentRunner } = data;
924
+ const { runner, presence, isLegacy } = data;
485
925
  const lastAliveText = runner.lastAlive
486
926
  ? OneUptimeDate.getDateAsFormattedString(runner.lastAlive)
487
927
  : "an unknown time";
928
+ /*
929
+ * The previous in-cluster Runner is not coming back by itself after a
930
+ * chart upgrade (the new chart does not install it), and there is
931
+ * nothing to fix in it: the step is the upgrade to the AI agent.
932
+ */
933
+ if (isLegacy) {
934
+ return {
935
+ code: "runner_offline",
936
+ title: presence.isSignedOff
937
+ ? "The previous in-cluster Runner signed off"
938
+ : "The previous in-cluster Runner is offline",
939
+ description: presence.isSignedOff
940
+ ? `Runner "${runner.name}" shut down after its last report at ${lastAliveText}.`
941
+ : `Runner "${runner.name}" last reported in at ${lastAliveText}.`,
942
+ nextStep: `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over.\n\n${AI_AGENT_INSTALL_COMMAND}`,
943
+ blocks: "both",
944
+ };
945
+ }
488
946
  if (presence.isSignedOff) {
489
947
  return {
490
948
  code: "runner_offline",
491
- title: isAgentRunner
492
- ? "The in-cluster Runner signed off"
493
- : "The Runner signed off",
494
- description: isAgentRunner
495
- ? `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: the Kubernetes agent was uninstalled or upgraded with aiAccess turned off, or its Runner pod is being replaced.`
496
- : `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: its container was stopped.`,
497
- nextStep: isAgentRunner
498
- ? "If the agent was uninstalled or aiAccess was turned off on purpose, clear the Runner on the cluster's AI page. Otherwise upgrade the agent with --set aiAccess.enabled=true and the Runner reconnects within a minute."
499
- : "Start the Runner container again, or bind another Runner on the cluster's AI page.",
949
+ title: "The Runner signed off",
950
+ description: `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: its container was stopped.`,
951
+ nextStep: `Start the Runner container again, or clear the Runner on ${CLUSTER_AI_AGENT_PAGE} to use the Kubernetes AI agent.`,
500
952
  blocks: "both",
501
953
  };
502
954
  }
@@ -509,48 +961,7 @@ class KubernetesClusterAiAccessServiceClass {
509
961
  description: isNeverConnected
510
962
  ? `Runner "${runner.name}" exists but has not reported in yet.`
511
963
  : `Runner "${runner.name}" last reported in at ${lastAliveText}.`,
512
- nextStep: isAgentRunner
513
- ? "Check the in-cluster Runner pod: kubectl get pods -n <agent-namespace> -l component=ai-runner, then its logs. It must be able to reach your OneUptime URL."
514
- : "Start the Runner container and make sure it can reach your OneUptime URL.",
515
- blocks: "both",
516
- };
517
- }
518
- /*
519
- * The gap for an offline agent Runner of this cluster that holds more than
520
- * the agent defaults, or null when it holds nothing extra. Such a row is
521
- * refused on every registration that cannot present its current key — and
522
- * a restarted pod never can, it keeps the key in memory only — so the
523
- * Runner will not come back until an operator removes what it holds.
524
- * Never throws: a failed lookup falls back to the ordinary offline gap.
525
- */
526
- async getAgentRunnerHoldingsGap(data) {
527
- var _a;
528
- let holdings = [];
529
- try {
530
- holdings = await this.getRunnerHoldingsBeyondDefaults({
531
- runner: data.runner,
532
- clusterId: data.cluster.id,
533
- projectId: data.cluster.projectId,
534
- });
535
- }
536
- catch (error) {
537
- logger.error(`KubernetesClusterAiAccess: could not read what the agent Runner of cluster ${(_a = data.cluster.id) === null || _a === void 0 ? void 0 : _a.toString()} holds: ${error}`);
538
- return null;
539
- }
540
- if (holdings.length === 0) {
541
- return null;
542
- }
543
- return {
544
- code: "runner_offline",
545
- title: "The in-cluster Runner cannot re-register until it holds less",
546
- description: `Runner "${data.runner.name}" is offline, and it holds more than an in-cluster Runner's defaults (${this.describeHoldings(holdings)}). A restarted in-cluster Runner cannot prove it is the same Runner, so every registration it attempts is refused and it will not reconnect on its own.`,
547
- /*
548
- * Removing what it holds comes first: the Runner row survives, so the
549
- * cluster stays bound to it and it reconnects on its next retry.
550
- * Deleting it works too, but only with the second step — the fresh
551
- * Runner is not bound to this cluster until someone selects it.
552
- */
553
- nextStep: `Under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(holdings)}. The in-cluster Runner then reconnects on its next retry, still bound to this cluster. Or delete the Runner and, once the in-cluster Runner registers a fresh one (within a minute), select it on the cluster's AI page as its Runner. ${getDeletedAgentRunnerRebindNote()}`,
964
+ nextStep: "Start the Runner container and make sure it can reach your OneUptime URL.",
554
965
  blocks: "both",
555
966
  };
556
967
  }
@@ -568,73 +979,6 @@ class KubernetesClusterAiAccessServiceClass {
568
979
  })
569
980
  .join("; ");
570
981
  }
571
- /*
572
- * The gap for a cluster with no Runner bound. What to do depends on
573
- * whether this cluster's in-cluster Runner is already registered: a
574
- * registering Runner never binds a cluster whose AI access was configured
575
- * before (an operator cleared the binding, or the Runner it was bound to
576
- * was deleted), so re-running the helm upgrade would change nothing —
577
- * selecting the Runner on the AI page is the step that works. Never
578
- * throws: a failed lookup falls back to the install instruction.
579
- */
580
- async getNoRunnerBoundGap(cluster) {
581
- const agentRunner = await this.findAgentRunnerForCluster(cluster);
582
- if (agentRunner) {
583
- const isOnline = this.getRunnerPresence(agentRunner).isOnline;
584
- return {
585
- code: "no_runner_bound",
586
- title: "The in-cluster Runner is installed but not selected",
587
- description: `Runner "${agentRunner.name}", this cluster's in-cluster Runner, is registered${isOnline ? " and online" : " but not online right now"}, but no Runner is bound to this cluster, so OneUptime AI does not use it. A registering Runner never re-binds a cluster that had a Runner bound before (or already ran kubectl) — its binding was cleared by an operator, or the Runner it was bound to was deleted.`,
588
- nextStep: `Select the kubernetes-agent Runner "${agentRunner.name}" on the cluster's AI page as its Runner (leave the credential empty). No helm change is needed.`,
589
- blocks: "both",
590
- };
591
- }
592
- return {
593
- code: "no_runner_bound",
594
- title: "No Runner can reach this cluster",
595
- description: "OneUptime AI runs kubectl through a Runner. None is bound to this cluster yet.",
596
- nextStep: "Upgrade the Kubernetes agent with --set aiAccess.enabled=true to install an in-cluster Runner (one command), or bind an existing Runner and a Kubernetes credential on the cluster's AI page.",
597
- blocks: "both",
598
- };
599
- }
600
- /*
601
- * This cluster's agent Runner row, by its (bounded) name, matched
602
- * case-insensitively like registration matches it. A row whose posture
603
- * names a DIFFERENT cluster is not this cluster's agent (a shortened
604
- * name's hash collision, or a posture rewritten on heartbeat).
605
- */
606
- async findAgentRunnerForCluster(cluster) {
607
- var _a;
608
- const clusterIdentifier = (cluster.clusterIdentifier || "").trim();
609
- if (!clusterIdentifier || !cluster.projectId) {
610
- return null;
611
- }
612
- try {
613
- const runner = await RunnerService.findOneBy({
614
- query: {
615
- projectId: cluster.projectId,
616
- name: QueryHelper.findWithSameText(getKubernetesAgentRunnerNameForCluster(clusterIdentifier)),
617
- },
618
- select: {
619
- _id: true,
620
- name: true,
621
- lastAlive: true,
622
- connectionStatus: true,
623
- hostInfo: true,
624
- },
625
- props: { isRoot: true },
626
- });
627
- if (!runner ||
628
- this.postureNamesAnotherCluster({ runner, clusterIdentifier })) {
629
- return null;
630
- }
631
- return runner;
632
- }
633
- catch (error) {
634
- logger.error(`KubernetesClusterAiAccess: could not look up the agent Runner of cluster ${(_a = cluster.id) === null || _a === void 0 ? void 0 : _a.toString()}: ${error}`);
635
- return null;
636
- }
637
- }
638
982
  // The Runner's reported posture names a cluster, and it is not this one.
639
983
  postureNamesAnotherCluster(data) {
640
984
  var _a, _b;
@@ -755,19 +1099,57 @@ class KubernetesClusterAiAccessServiceClass {
755
1099
  return [];
756
1100
  }
757
1101
  const gates = await this.getProjectGates(data.projectId);
1102
+ return this.getStatusesForClusters({
1103
+ projectId: data.projectId,
1104
+ clusters,
1105
+ gates,
1106
+ });
1107
+ }
1108
+ /*
1109
+ * The status of several clusters of one project, with every cluster's
1110
+ * agent row read in ONE query instead of one per cluster. When that read
1111
+ * fails, each status reads its own row (and treats a failure there as no
1112
+ * agent), exactly as a single status would.
1113
+ */
1114
+ async getStatusesForClusters(data) {
1115
+ var _a;
1116
+ let agentRows = null;
1117
+ try {
1118
+ agentRows = await KubernetesAiAgentService.findForClusters({
1119
+ projectId: data.projectId,
1120
+ kubernetesClusterIds: data.clusters
1121
+ .map((cluster) => {
1122
+ return cluster.id || undefined;
1123
+ })
1124
+ .filter((id) => {
1125
+ return Boolean(id);
1126
+ }),
1127
+ });
1128
+ }
1129
+ catch (error) {
1130
+ logger.error(`KubernetesClusterAiAccess: could not read the Kubernetes AI agents of project ${data.projectId.toString()}; reading them one cluster at a time: ${error}`);
1131
+ }
758
1132
  const statuses = [];
759
- for (const cluster of clusters) {
760
- statuses.push(await this.getStatusForClusterModel({ cluster, gates }));
1133
+ for (const cluster of data.clusters) {
1134
+ statuses.push(await this.getStatusForClusterModel({
1135
+ cluster,
1136
+ gates: data.gates,
1137
+ aiAgentRow: agentRows
1138
+ ? agentRows.get(((_a = cluster.id) === null || _a === void 0 ? void 0 : _a.toString()) || "") || null
1139
+ : undefined,
1140
+ }));
761
1141
  }
762
1142
  return statuses;
763
1143
  }
764
1144
  /*
765
1145
  * ------------------------------------------------------------------
766
- * In-cluster Runner registration
1146
+ * Previous in-cluster Runner registration
767
1147
  * ------------------------------------------------------------------
768
1148
  */
769
1149
  /*
770
- * Called by the Runner the kubernetes-agent chart deploys. Authenticated
1150
+ * Called by the in-cluster Runner older kubernetes-agent charts deploy
1151
+ * (the Kubernetes AI agent replaces it and registers through
1152
+ * KubernetesAiAgentService instead). Authenticated
771
1153
  * by the project's telemetry ingestion key (the same key the agent ships
772
1154
  * telemetry with), so installing the chart with one extra flag is the
773
1155
  * whole setup. Idempotent per (project, cluster): a restarted pod gets a
@@ -798,15 +1180,30 @@ class KubernetesClusterAiAccessServiceClass {
798
1180
  * secret assigned to it, runbooks or code fixes turned on, or another
799
1181
  * cluster bound to it — is refused: re-keying it would hand all of that
800
1182
  * to whoever holds the ingestion key. The operator removes the extras
801
- * (the Runner then comes back still bound), or deletes the Runner, lets
802
- * the agent register a fresh one and selects it on the cluster's AI
803
- * page — a deleted Runner's cluster is left unbound, see below.
1183
+ * (the Runner then comes back still bound), or upgrades the chart to
1184
+ * the Kubernetes AI agent, which needs no Runner at all.
804
1185
  * - It finds the row it may re-key by this cluster's agent Runner NAME
805
1186
  * only — never by the posture a Runner reports about itself — and only
806
1187
  * binds a cluster that never had a Runner bound: a cluster whose
807
1188
  * binding was cleared (or whose Runner was deleted) stays unbound, and
808
1189
  * an operator's AI settings are never overwritten, only the chart's
809
1190
  * defaults fill in a cluster nobody configured.
1191
+ * - While the cluster's Kubernetes AI agent is online the Runner is
1192
+ * refused (superseded_by_ai_agent) before anything is written: the
1193
+ * agent replaces it. The refusal clears on its own once the agent
1194
+ * stops, so a helm rollback to a chart with the Runner works again for
1195
+ * a cluster still bound to that Runner (already_bound — every cluster
1196
+ * the Runner served before the upgrade).
1197
+ * - A cluster that has a Kubernetes AI agent row is never given the
1198
+ * chart's defaults by a Runner's first bind (the agent's own first
1199
+ * connection decides those). The binding rules below are unchanged by
1200
+ * the agent row: a cluster that never had a Runner bound and never ran
1201
+ * kubectl is bound with no switch moved, but one where AI already ran
1202
+ * kubectl — through the agent too: its commands and "Test connection"
1203
+ * record the same history — stays unbound (left_unbound_by_operator).
1204
+ * That history cannot tell the agent's commands from a revoked
1205
+ * Runner's, so it fails closed: nothing reaches the cluster until its
1206
+ * agent is back online.
810
1207
  * - Every refusal is a KubernetesAgentRegistrationRefusedException whose
811
1208
  * reason tells the Runner whether waiting helps.
812
1209
  *
@@ -815,7 +1212,7 @@ class KubernetesClusterAiAccessServiceClass {
815
1212
  * — so an operator can see when the identity behind AI access changed.
816
1213
  */
817
1214
  async registerKubernetesAgentRunner(data) {
818
- var _a;
1215
+ var _a, _b;
819
1216
  const clusterIdentifier = (data.clusterIdentifier || "").trim();
820
1217
  if (!clusterIdentifier) {
821
1218
  throw new BadDataException("clusterName is required.");
@@ -841,6 +1238,24 @@ class KubernetesClusterAiAccessServiceClass {
841
1238
  if (!cluster) {
842
1239
  throw new BadDataException("Cluster could not be resolved.");
843
1240
  }
1241
+ /*
1242
+ * The Kubernetes AI agent replaces this Runner: while the cluster's
1243
+ * agent is online nothing is created, re-keyed or bound. Checked
1244
+ * first, so a rolled-back pod that keeps retrying during an upgrade
1245
+ * never takes the cluster back from a working agent.
1246
+ */
1247
+ const aiAgent = await KubernetesAiAgentService.findForCluster({
1248
+ projectId: data.projectId,
1249
+ kubernetesClusterId: cluster.id,
1250
+ });
1251
+ if (aiAgent && KubernetesAiAgentService.isOnline(aiAgent)) {
1252
+ logger.warn(`KubernetesClusterAiAccess: refused to register the in-cluster Runner of cluster "${clusterIdentifier}" in project ${data.projectId.toString()}: the cluster's Kubernetes AI agent is online and replaces it.`);
1253
+ throw new KubernetesAgentRegistrationRefusedException({
1254
+ reason: "superseded_by_ai_agent",
1255
+ retryAfterSeconds: SUPERSEDED_BY_AI_AGENT_RETRY_AFTER_SECONDS,
1256
+ message: getSupersededByAiAgentMessage(clusterIdentifier),
1257
+ });
1258
+ }
844
1259
  /*
845
1260
  * What the row stores: the body's posture, with the cluster and
846
1261
  * in-cluster set by the service. A re-key may fill in the write scope
@@ -891,7 +1306,7 @@ class KubernetesClusterAiAccessServiceClass {
891
1306
  logger.warn(`KubernetesClusterAiAccess: refused to register an agent Runner for cluster "${clusterIdentifier}" in project ${data.projectId.toString()}: Runner "${runner.name}" (${(_a = runner.id) === null || _a === void 0 ? void 0 : _a.toString()}) reports that it belongs to a different cluster.`);
892
1307
  throw new KubernetesAgentRegistrationRefusedException({
893
1308
  reason: "runner_belongs_to_another_cluster",
894
- message: `Delete Runner "${runner.name}" under Project Settings → Runners (an in-cluster Runner cannot be renamed) and, once the agent registers a fresh one on its next retry, select it on the AI page of cluster "${clusterIdentifier}" as its Runner. That Runner already exists in this project but reports that it is the in-cluster Runner of a different cluster, so it was not reused for cluster "${clusterIdentifier}". ${getDeletedAgentRunnerRebindNote()}`,
1309
+ message: `Upgrade the Kubernetes agent chart of cluster "${clusterIdentifier}": its Kubernetes AI agent replaces the in-cluster Runner and does not need Runner "${runner.name}". That Runner already exists in this project but reports that it is the in-cluster Runner of a different cluster, so it was not reused for cluster "${clusterIdentifier}".`,
895
1310
  });
896
1311
  }
897
1312
  const agentRunnerExistedBefore = Boolean(runner);
@@ -959,13 +1374,21 @@ class KubernetesClusterAiAccessServiceClass {
959
1374
  * binding's foreign key is ON DELETE SET NULL, so Postgres clears
960
1375
  * aiAccessRunnerId in the same statement. Deleting the Runner is
961
1376
  * how an operator revokes or resets it, so it must not come back
962
- * bound a few heartbeats later. The AI page tells the operator to
963
- * select the Runner if they want it back.
1377
+ * bound a few heartbeats later. Such a cluster uses its
1378
+ * Kubernetes AI agent instead.
1379
+ * History counts whoever made it: with a Kubernetes AI agent row
1380
+ * it is usually the agent's (it records every command and "Test
1381
+ * connection" on the cluster), but it cannot be told apart from a
1382
+ * revoked Runner's, so such a cluster stays unbound too and is
1383
+ * reached again once its agent is back online. A rollback keeps
1384
+ * working through the Runner a cluster is still bound to
1385
+ * (already_bound above), not through a new binding here.
964
1386
  * - that never had a Runner bound but was configured on the AI page
965
1387
  * (aiAccessConfiguredAt: an operator chose a mode, the switches
966
- * or an allowlist BEFORE installing the chart) is bound to this
967
- * agent Runner with every setting left exactly as the operator
968
- * chose it — the one-flag install still just works.
1388
+ * or an allowlist BEFORE installing the chart), or that already
1389
+ * has a Kubernetes AI agent row that never ran kubectl here, is
1390
+ * bound to this agent Runner with every setting left exactly as
1391
+ * it is — the one-flag install still just works.
969
1392
  * - that nobody ever configured takes the chart's intent:
970
1393
  * investigation on, remediation "ask for approval" when the chart
971
1394
  * also granted write RBAC (a mode already on the row is kept).
@@ -1002,7 +1425,14 @@ class KubernetesClusterAiAccessServiceClass {
1002
1425
  else if (hadRunnerBound || hasCommandHistory) {
1003
1426
  bindingState = "left_unbound_by_operator";
1004
1427
  }
1005
- else if (cluster.aiAccessConfiguredAt) {
1428
+ else if (cluster.aiAccessConfiguredAt || aiAgent) {
1429
+ /*
1430
+ * Configured by an operator before the chart was installed, or the
1431
+ * cluster already has a Kubernetes AI agent (offline, or this
1432
+ * registration would have been refused) that has not run kubectl
1433
+ * here: bind, so AI works through this Runner, but leave every
1434
+ * switch — and the configured marker — exactly as they are.
1435
+ */
1006
1436
  bindingState = "bound_keeping_operator_settings";
1007
1437
  // Only the binding: not one of the operator's switches moves.
1008
1438
  await KubernetesClusterService.updateOneById({
@@ -1034,7 +1464,7 @@ class KubernetesClusterAiAccessServiceClass {
1034
1464
  });
1035
1465
  }
1036
1466
  if (bindingState === "left_unbound_by_operator") {
1037
- logger.info(`KubernetesClusterAiAccess: cluster "${clusterIdentifier}" in project ${data.projectId.toString()} had a Runner bound before (or has run kubectl) but has none bound now — the binding was cleared, or its Runner was deleted; the in-cluster Runner registered without re-binding it.`);
1467
+ logger.info(`KubernetesClusterAiAccess: cluster "${clusterIdentifier}" in project ${data.projectId.toString()} had a Runner bound before (or has run kubectl${aiAgent ? ", possibly through its Kubernetes AI agent" : ""}) but has none bound now; the in-cluster Runner registered without binding it.`);
1038
1468
  }
1039
1469
  const isBoundToCluster = bindingState === "first_bind" ||
1040
1470
  bindingState === "bound_keeping_operator_settings" ||
@@ -1047,6 +1477,11 @@ class KubernetesClusterAiAccessServiceClass {
1047
1477
  agentRunnerExistedBefore,
1048
1478
  appliedRemediationMode,
1049
1479
  reKeyAdmission,
1480
+ hasAiAgent: Boolean(aiAgent),
1481
+ hadRunnerBound,
1482
+ aiAgentPodNamespace: aiAgent
1483
+ ? (_b = parseKubernetesAgentPosture(aiAgent.posture)) === null || _b === void 0 ? void 0 : _b.podNamespace
1484
+ : undefined,
1050
1485
  });
1051
1486
  return {
1052
1487
  runnerId: runner.id,
@@ -1163,7 +1598,7 @@ class KubernetesClusterAiAccessServiceClass {
1163
1598
  */
1164
1599
  throw new KubernetesAgentRegistrationRefusedException({
1165
1600
  reason: "runner_holds_more_than_defaults",
1166
- message: `Under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(holdings)} — or delete the Runner and, once the agent registers a fresh one on its next retry, select it on the cluster's AI page as its Runner. It is offline, but it holds more than an in-cluster Runner's defaults (${this.describeHoldings(holdings)}), so a registration for cluster "${data.clusterIdentifier}" that does not present its current key may not take it over, and its key was not changed. ${getDeletedAgentRunnerRebindNote()}`,
1601
+ message: `Upgrade the Kubernetes agent chart: its Kubernetes AI agent replaces this in-cluster Runner. Or, under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(holdings)}. It is offline, but it holds more than an in-cluster Runner's defaults (${this.describeHoldings(holdings)}), so a registration for cluster "${data.clusterIdentifier}" that does not present its current key may not take it over, and its key was not changed.`,
1167
1602
  });
1168
1603
  }
1169
1604
  if (presence.isSignedOff) {
@@ -1179,7 +1614,8 @@ class KubernetesClusterAiAccessServiceClass {
1179
1614
  * kubectl access only (no runbooks, no code fixes, nothing assigned) bound
1180
1615
  * to its own cluster; anything else was entrusted to the row by an
1181
1616
  * operator. "Runs Runbooks" is read the way the claim path reads it: on
1182
- * unless explicitly false.
1617
+ * unless explicitly false. Also what decides whether an unused previous
1618
+ * in-cluster Runner may be retired (KubernetesAiAgentService).
1183
1619
  */
1184
1620
  async getRunnerHoldingsBeyondDefaults(data) {
1185
1621
  const holdings = [];
@@ -1232,7 +1668,7 @@ class KubernetesClusterAiAccessServiceClass {
1232
1668
  if (otherBoundClusters > 0) {
1233
1669
  holdings.push({
1234
1670
  description: `the AI access Runner of ${otherBoundClusters} other cluster(s)`,
1235
- remedy: `select another Runner on the AI page of the ${otherBoundClusters} other cluster(s) bound to it`,
1671
+ remedy: `clear the Runner on the AI agent page of the ${otherBoundClusters} other cluster(s) bound to it`,
1236
1672
  });
1237
1673
  }
1238
1674
  return holdings;
@@ -1282,12 +1718,29 @@ class KubernetesClusterAiAccessServiceClass {
1282
1718
  displayColor = Green500;
1283
1719
  feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** registered from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI investigation is on; AI remediation is ${this.describeRemediationMode(data.appliedRemediationMode)}.`;
1284
1720
  }
1721
+ else if (data.bindingState === "bound_keeping_operator_settings" &&
1722
+ data.hasAiAgent &&
1723
+ !data.cluster.aiAccessConfiguredAt) {
1724
+ displayColor = Green500;
1725
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) while this cluster's Kubernetes AI agent is offline, and was bound so AI works through it: no Runner was bound here before and AI had not run kubectl here yet. No AI setting was changed. The Kubernetes AI agent takes over again as soon as it is back online.`;
1726
+ }
1285
1727
  else if (data.bindingState === "bound_keeping_operator_settings") {
1286
1728
  displayColor = Green500;
1287
- feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI access had already been configured on the cluster's AI page, so no AI setting was changed: AI investigation with kubectl is ${data.cluster.isAiInvestigationEnabled === true ? "on" : "off"} and AI remediation is ${this.describeRemediationMode(data.cluster.aiRemediationMode)}, as an operator chose.`;
1729
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI access had already been configured on the cluster's AI agent page, so no AI setting was changed: AI investigation with kubectl is ${data.cluster.isAiInvestigationEnabled === true ? "on" : "off"} and AI remediation is ${this.describeRemediationMode(data.cluster.aiRemediationMode)}, as an operator chose.`;
1730
+ }
1731
+ else if (data.bindingState === "left_unbound_by_operator" &&
1732
+ data.hasAiAgent &&
1733
+ !data.hadRunnerBound &&
1734
+ !data.cluster.aiAccessRunnerId) {
1735
+ /*
1736
+ * No Runner was ever bound, so the history that kept this one
1737
+ * unbound is most likely the agent's own — not an operator's
1738
+ * doing. Say that, and where AI access comes back from.
1739
+ */
1740
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}) while this cluster's Kubernetes AI agent is offline, and was left unbound: AI has already run kubectl on this cluster (through its Kubernetes AI agent, or a Runner that is no longer bound), and a registering Runner is never bound over that history. No AI setting was changed. AI reaches this cluster again when its Kubernetes AI agent is back online: check the agent's pod with \`${getAiAgentLogsCommand(data.aiAgentPodNamespace)}\`, or upgrade the Kubernetes agent chart again.`;
1288
1741
  }
1289
1742
  else if (data.bindingState === "left_unbound_by_operator") {
1290
- feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but no Runner is bound to this cluster although one was before (or AI already ran kubectl here) — the binding was cleared by an operator, or the Runner it was bound to was deleted — so it was left unbound and no AI switch was changed. Select **${runnerName}** on the cluster's AI page to use it.`;
1743
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but no Runner is bound to this cluster although one was before (or AI already ran kubectl here) — the binding was cleared by an operator, or the Runner it was bound to was deleted — so it was left unbound and no AI switch was changed. Upgrade the Kubernetes agent chart to use the Kubernetes AI agent instead.`;
1291
1744
  }
1292
1745
  else if (data.bindingState === "bound_to_other_runner") {
1293
1746
  feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but this cluster is bound to a different Runner in the dashboard, so it was not used.`;
@@ -1435,6 +1888,18 @@ __decorate([
1435
1888
  __metadata("design:paramtypes", [Object]),
1436
1889
  __metadata("design:returntype", Promise)
1437
1890
  ], KubernetesClusterAiAccessServiceClass.prototype, "getStatusForCluster", null);
1891
+ __decorate([
1892
+ CaptureSpan(),
1893
+ __metadata("design:type", Function),
1894
+ __metadata("design:paramtypes", [Object]),
1895
+ __metadata("design:returntype", Promise)
1896
+ ], KubernetesClusterAiAccessServiceClass.prototype, "loadAccessTarget", null);
1897
+ __decorate([
1898
+ CaptureSpan(),
1899
+ __metadata("design:type", Function),
1900
+ __metadata("design:paramtypes", [Object]),
1901
+ __metadata("design:returntype", Promise)
1902
+ ], KubernetesClusterAiAccessServiceClass.prototype, "getLegacyAgentRunnerForCluster", null);
1438
1903
  __decorate([
1439
1904
  CaptureSpan(),
1440
1905
  __metadata("design:type", Function),
@@ -1453,6 +1918,12 @@ __decorate([
1453
1918
  __metadata("design:paramtypes", [Object]),
1454
1919
  __metadata("design:returntype", Promise)
1455
1920
  ], KubernetesClusterAiAccessServiceClass.prototype, "getStatusesForSubject", null);
1921
+ __decorate([
1922
+ CaptureSpan(),
1923
+ __metadata("design:type", Function),
1924
+ __metadata("design:paramtypes", [Object]),
1925
+ __metadata("design:returntype", Promise)
1926
+ ], KubernetesClusterAiAccessServiceClass.prototype, "getStatusesForClusters", null);
1456
1927
  __decorate([
1457
1928
  CaptureSpan(),
1458
1929
  __metadata("design:type", Function),