@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -1,5 +1,5 @@
1
1
  import DatabaseProperty from "../Database/DatabaseProperty";
2
- import OneUptimeDate from "../Date";
2
+ import OneUptimeDate, { CalendarUnit } from "../Date";
3
3
  import BadDataException from "../Exception/BadDataException";
4
4
  import { JSONArray, JSONObject, ObjectType } from "../JSON";
5
5
  import JSONFunctions from "../JSONFunctions";
@@ -109,6 +109,104 @@ export default class Recurring extends DatabaseProperty {
109
109
  return nextDate;
110
110
  }
111
111
 
112
+ /*
113
+ * The calendar unit this interval is expressed in, for the date helpers that
114
+ * do calendar (rather than fixed-millisecond) arithmetic.
115
+ */
116
+ public static toCalendarUnit(intervalType: EventInterval): CalendarUnit {
117
+ switch (intervalType) {
118
+ case EventInterval.Hour:
119
+ return "hour";
120
+ case EventInterval.Day:
121
+ return "day";
122
+ case EventInterval.Week:
123
+ return "week";
124
+ case EventInterval.Month:
125
+ return "month";
126
+ case EventInterval.Year:
127
+ return "year";
128
+ default:
129
+ throw new BadDataException("Invalid Interval Type: " + intervalType);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * The first occurrence of `recurring` (anchored at `startDate`) strictly after
135
+ * `afterDate`, stepping in whole CALENDAR units in `timezone`.
136
+ *
137
+ * getNextDate() approximates a month as 30.4375 days and a year as 365.25
138
+ * days, which is fine for hours/days/weeks but makes a monthly recurrence
139
+ * walk backwards through the calendar: anchored on 1 Jul it fires on 31 Jul,
140
+ * then 30 Aug, and eventually skips a month entirely. This steps 1 Jul ->
141
+ * 1 Aug -> 1 Sep instead, and holds the local time of day across DST.
142
+ *
143
+ * A start date already after `afterDate` is returned unchanged - the first
144
+ * occurrence has not happened yet.
145
+ */
146
+ public static getNextDateAfter(data: {
147
+ startDate: Date;
148
+ recurring: Recurring;
149
+ afterDate: Date;
150
+ timezone?: string | undefined;
151
+ }): Date {
152
+ const startDate: Date = OneUptimeDate.fromString(data.startDate);
153
+ const recurring: Recurring = Recurring.fromJSON(data.recurring);
154
+ const unit: CalendarUnit = Recurring.toCalendarUnit(recurring.intervalType);
155
+
156
+ const intervalCount: number = Math.max(
157
+ 1,
158
+ Math.floor(recurring.intervalCount.toNumber()),
159
+ );
160
+
161
+ if (startDate.getTime() > data.afterDate.getTime()) {
162
+ return startDate;
163
+ }
164
+
165
+ /*
166
+ * Jump most of the way in one step so a status page left disabled for a year
167
+ * does not loop 365 times, then walk the remainder. The estimate can only be
168
+ * off by a single interval either way (calendar units are not uniform), so
169
+ * the two loops below run a handful of times at most.
170
+ */
171
+ const elapsedUnits: number = OneUptimeDate.getCalendarUnitsBetween(
172
+ startDate,
173
+ data.afterDate,
174
+ unit,
175
+ data.timezone,
176
+ );
177
+
178
+ let intervals: number = Math.max(
179
+ 0,
180
+ Math.floor(elapsedUnits / intervalCount),
181
+ );
182
+
183
+ type OccurrenceFunction = (intervalsToAdd: number) => Date;
184
+
185
+ const occurrence: OccurrenceFunction = (intervalsToAdd: number): Date => {
186
+ return OneUptimeDate.addRemoveCalendarUnits(
187
+ startDate,
188
+ unit,
189
+ intervalsToAdd * intervalCount,
190
+ data.timezone,
191
+ );
192
+ };
193
+
194
+ // Overshot - come back until we are at or before the cutoff.
195
+ while (
196
+ intervals > 0 &&
197
+ occurrence(intervals - 1).getTime() > data.afterDate.getTime()
198
+ ) {
199
+ intervals--;
200
+ }
201
+
202
+ // Undershot - advance until we are strictly after it.
203
+ while (occurrence(intervals).getTime() <= data.afterDate.getTime()) {
204
+ intervals++;
205
+ }
206
+
207
+ return occurrence(intervals);
208
+ }
209
+
112
210
  private data: RecurringData = Recurring.getDefaultRotationData();
113
211
 
114
212
  public get intervalType(): EventInterval {
@@ -72,6 +72,21 @@ export interface AIStepConfig {
72
72
  * Response token cap. Defaults to 4096; clamped server-side.
73
73
  */
74
74
  maxTokens?: number;
75
+ /*
76
+ * Pin this step to a specific LLM provider. Optional — when unset the step
77
+ * uses the project's default provider (and, failing that, the global
78
+ * OneUptime provider), which is what every AI step did before this field
79
+ * existed.
80
+ *
81
+ * The id must name a provider the project owns or a global one; the server
82
+ * re-checks that on every run rather than trusting the stored config, since
83
+ * Runbook.steps is an unvalidated JSON column. A pinned provider that no
84
+ * longer resolves FAILS the step instead of quietly falling back to the
85
+ * default: an AI step runs unattended, and silently answering on a
86
+ * different model than the one a responder pinned is a change nobody would
87
+ * see. (Interactive chat, which has a human watching, does fall back.)
88
+ */
89
+ llmProviderId?: string;
75
90
  }
76
91
 
77
92
  /*
@@ -0,0 +1,114 @@
1
+ import OneUptimeDate from "../Date";
2
+
3
+ /*
4
+ * How recently a Runner must have heartbeated to count as online. Matches the
5
+ * window AIAgentService.isAgentAlive uses, and comfortably clears the Runner's
6
+ * 60s heartbeat interval.
7
+ *
8
+ * This lives in Types, not in RunnerService, because the dashboard has to
9
+ * answer the same question the server does. RunnerService imports it from
10
+ * here so the two cannot drift: a server that hands out work to Runners it
11
+ * considers alive, next to a dashboard that calls those same Runners
12
+ * disconnected, is worse than either answer on its own.
13
+ */
14
+ export const RUNNER_ALIVE_WINDOW_IN_MINUTES: number = 5;
15
+
16
+ /*
17
+ * What a Runner's heartbeat actually says about it right now.
18
+ *
19
+ * Deliberately NOT the same thing as the persisted RunnerConnectionStatus
20
+ * column. That column is written exactly twice — Disconnected when the row is
21
+ * created, Connected on the first heartbeat — and nothing ever writes it back,
22
+ * so a Runner that ran once last March still reads "connected" today. Every
23
+ * server-side liveness check already ignores it and compares lastAlive against
24
+ * the window above; this enum lets the dashboard tell the user the same truth.
25
+ */
26
+ export enum RunnerLiveStatus {
27
+ /* Heartbeated within the window: it is polling for work right now. */
28
+ Connected = "connected",
29
+
30
+ /*
31
+ * Heartbeated at some point, but not recently enough. The container is
32
+ * stopped, wedged, or cannot reach OneUptime.
33
+ */
34
+ Disconnected = "disconnected",
35
+
36
+ /*
37
+ * Never heartbeated at all. The row exists because someone created it in the
38
+ * dashboard, but the Docker command has not been run yet. This is a setup
39
+ * step the user still owes us, not a failure, and the UI must not dress it up
40
+ * as one.
41
+ */
42
+ NeverConnected = "never-connected",
43
+ }
44
+
45
+ /*
46
+ * lastAlive arrives as a Date from a hydrated model and as an ISO string from
47
+ * a raw API payload, so both are accepted. Anything falsy — null from Postgres,
48
+ * undefined from a select that never asked for the column — means "no heartbeat
49
+ * has ever been recorded".
50
+ */
51
+ export type GetRunnerLiveStatusFunction = (
52
+ lastAlive: Date | string | null | undefined,
53
+ ) => RunnerLiveStatus;
54
+
55
+ export const getRunnerLiveStatus: GetRunnerLiveStatusFunction = (
56
+ lastAlive: Date | string | null | undefined,
57
+ ): RunnerLiveStatus => {
58
+ if (!lastAlive) {
59
+ return RunnerLiveStatus.NeverConnected;
60
+ }
61
+
62
+ const lastAliveDate: Date = OneUptimeDate.fromString(lastAlive);
63
+
64
+ /*
65
+ * An unparseable value is not evidence of life. Returning Disconnected keeps
66
+ * a malformed timestamp from being reported as a healthy Runner.
67
+ */
68
+ if (isNaN(lastAliveDate.getTime())) {
69
+ return RunnerLiveStatus.Disconnected;
70
+ }
71
+
72
+ const windowStart: Date = OneUptimeDate.getSomeMinutesAgo(
73
+ RUNNER_ALIVE_WINDOW_IN_MINUTES,
74
+ );
75
+
76
+ if (OneUptimeDate.isAfter(lastAliveDate, windowStart)) {
77
+ return RunnerLiveStatus.Connected;
78
+ }
79
+
80
+ return RunnerLiveStatus.Disconnected;
81
+ };
82
+
83
+ /*
84
+ * The English label for each state, which is also its i18next lookup key.
85
+ *
86
+ * "Connected" and "Disconnected" already had translations in all sixteen
87
+ * Dashboard locale files before any of this — the probe pages have used them
88
+ * for years — so routing every Runner status label through these three
89
+ * constants is what lets the new state reuse them instead of inventing a
90
+ * parallel set that would have to be translated again.
91
+ *
92
+ * It lives here rather than in the component because two very different
93
+ * surfaces need the same words: the status bubble, and the plain-string
94
+ * option label in the runbook step editor's Runner picker.
95
+ */
96
+ export type GetRunnerLiveStatusLabelFunction = (
97
+ status: RunnerLiveStatus,
98
+ ) => string;
99
+
100
+ export const getRunnerLiveStatusLabel: GetRunnerLiveStatusLabelFunction = (
101
+ status: RunnerLiveStatus,
102
+ ): string => {
103
+ if (status === RunnerLiveStatus.Connected) {
104
+ return "Connected";
105
+ }
106
+
107
+ if (status === RunnerLiveStatus.NeverConnected) {
108
+ return "Never connected";
109
+ }
110
+
111
+ return "Disconnected";
112
+ };
113
+
114
+ export default RunnerLiveStatus;
@@ -85,6 +85,17 @@ export interface StatusPageReportRow {
85
85
 
86
86
  export interface StatusPageReport {
87
87
  reportDates: string; // start date and end date in string. e.g. "01 July 2021 - 14 July 2021"
88
+ /*
89
+ * How a sentence refers to the period this report covers - "the last 30
90
+ * days", "July 2026", "the week of Jul 27, 2026". `reportDates` is the raw
91
+ * range; this is the phrase that goes after "your status summary for ...".
92
+ */
93
+ reportPeriodName: string;
94
+ // The two ends of the window on their own, for templates that lay them out.
95
+ reportStartDate: string;
96
+ reportEndDate: string;
97
+ // The IANA zone every date above was resolved in, e.g. "America/New_York".
98
+ reportTimezone: string;
88
99
  totalResources: number;
89
100
  totalIncidents: number;
90
101
  averageUptimePercent: string;
@@ -0,0 +1,21 @@
1
+ /*
2
+ * What stretch of time a scheduled status page report covers.
3
+ *
4
+ * The report used to only ever cover "the last N days counted back from the
5
+ * moment the email went out", which can never line up with a calendar month -
6
+ * a monthly report sent on 1 Aug covered 2 Jul - 1 Aug rather than the July
7
+ * everyone reading it expects, and consecutive reports silently overlapped or
8
+ * left gaps whenever the day count did not match the send frequency.
9
+ */
10
+ enum StatusPageReportPeriodType {
11
+ // The window ends when the email is sent and runs back `reportDataInDays`.
12
+ Rolling = "Rolling",
13
+ /*
14
+ * The last whole calendar period before the email is sent, sized by the send
15
+ * frequency: a report sent monthly covers 1 Jul 00:00 - 31 Jul 23:59:59.999
16
+ * in the status page's report timezone.
17
+ */
18
+ PreviousCalendarPeriod = "Previous Calendar Period",
19
+ }
20
+
21
+ export default StatusPageReportPeriodType;
@@ -178,6 +178,126 @@ const Detail: DetailFunction = <T extends GenericObject>(
178
178
  );
179
179
  };
180
180
 
181
+ /*
182
+ * Relation columns arrive as whole model instances. React throws (and takes
183
+ * the entire page down with it) when handed one as a child, so every object
184
+ * that reaches the renderer is reduced to a label with these keys, in order.
185
+ */
186
+ const entityLabelKeys: Array<string> = [
187
+ "name",
188
+ "title",
189
+ "label",
190
+ "email",
191
+ "slug",
192
+ "value",
193
+ ];
194
+
195
+ type GetEntityLabelFunction = (entity: unknown) => string;
196
+
197
+ const getEntityLabel: GetEntityLabelFunction = (entity: unknown): string => {
198
+ if (entity === null || entity === undefined) {
199
+ return "";
200
+ }
201
+
202
+ if (entity instanceof DatabaseProperty) {
203
+ return entity.toString();
204
+ }
205
+
206
+ if (typeof entity !== "object") {
207
+ return String(entity);
208
+ }
209
+
210
+ for (const key of entityLabelKeys) {
211
+ const value: unknown = (entity as Dictionary<unknown>)[key];
212
+
213
+ if (value === null || value === undefined || value === "") {
214
+ continue;
215
+ }
216
+
217
+ if (value instanceof DatabaseProperty) {
218
+ return value.toString();
219
+ }
220
+
221
+ if (typeof value !== "object") {
222
+ return String(value);
223
+ }
224
+ }
225
+
226
+ return "";
227
+ };
228
+
229
+ type GetEntityColorFunction = (entity: unknown) => Color | string | undefined;
230
+
231
+ const getEntityColor: GetEntityColorFunction = (
232
+ entity: unknown,
233
+ ): Color | string | undefined => {
234
+ if (!entity || typeof entity !== "object") {
235
+ return undefined;
236
+ }
237
+
238
+ const color: unknown = (entity as Dictionary<unknown>)["color"];
239
+
240
+ if (color instanceof Color || typeof color === "string") {
241
+ return color;
242
+ }
243
+
244
+ return undefined;
245
+ };
246
+
247
+ type GetEntityViewerFunction = (entity: unknown) => ReactElement | string;
248
+
249
+ const getEntityViewer: GetEntityViewerFunction = (
250
+ entity: unknown,
251
+ ): ReactElement | string => {
252
+ const label: string = getEntityLabel(entity);
253
+
254
+ if (!label) {
255
+ // Fall through to the placeholder the field already declares.
256
+ return "";
257
+ }
258
+
259
+ return <DropdownValueBadge label={label} color={getEntityColor(entity)} />;
260
+ };
261
+
262
+ type GetEntityArrayViewerFunction = (
263
+ entities: Array<unknown>,
264
+ ) => ReactElement | string;
265
+
266
+ const getEntityArrayViewer: GetEntityArrayViewerFunction = (
267
+ entities: Array<unknown>,
268
+ ): ReactElement | string => {
269
+ const labelled: Array<{ label: string; entity: unknown }> = entities
270
+ .map((entity: unknown) => {
271
+ return { label: getEntityLabel(entity), entity: entity };
272
+ })
273
+ .filter((item: { label: string; entity: unknown }) => {
274
+ return Boolean(item.label);
275
+ });
276
+
277
+ if (labelled.length === 0) {
278
+ return "";
279
+ }
280
+
281
+ return (
282
+ <div className="flex flex-wrap gap-1.5">
283
+ {labelled.map(
284
+ (
285
+ item: { label: string; entity: unknown },
286
+ index: number,
287
+ ): ReactElement => {
288
+ return (
289
+ <DropdownValueBadge
290
+ key={`${item.label}-${index}`}
291
+ label={item.label}
292
+ color={getEntityColor(item.entity)}
293
+ />
294
+ );
295
+ },
296
+ )}
297
+ </div>
298
+ );
299
+ };
300
+
181
301
  type GetDictionaryOfStringsViewerFunction = (
182
302
  data: Dictionary<string>,
183
303
  ) => ReactElement;
@@ -486,6 +606,16 @@ const Detail: DetailFunction = <T extends GenericObject>(
486
606
  );
487
607
  }
488
608
 
609
+ if (data && field.fieldType === FieldType.Entity) {
610
+ data = getEntityViewer(data);
611
+ }
612
+
613
+ if (data && field.fieldType === FieldType.EntityArray) {
614
+ data = getEntityArrayViewer(
615
+ Array.isArray(data) ? (data as Array<unknown>) : [data],
616
+ );
617
+ }
618
+
489
619
  if (data && field.fieldType === FieldType.HiddenText) {
490
620
  data = (
491
621
  <HiddenText
@@ -608,6 +738,31 @@ const Detail: DetailFunction = <T extends GenericObject>(
608
738
  data = data.toString();
609
739
  }
610
740
 
741
+ /*
742
+ * Last resort. Anything still an object here is something no field type
743
+ * claimed - usually a relation column declared without FieldType.Entity.
744
+ * Rendering it would throw "Objects are not valid as a React child" and
745
+ * blank the whole page, so label it instead and log the misconfiguration.
746
+ */
747
+ if (data && typeof data === "object" && !React.isValidElement(data)) {
748
+ Logger.error(
749
+ "Detail: no field type renders the value of '" +
750
+ fieldKeyStr +
751
+ "'. Set fieldType (FieldType.Entity for relations) or getElement on the field.",
752
+ );
753
+
754
+ data = Array.isArray(data)
755
+ ? (data as Array<unknown>)
756
+ .map((entity: unknown) => {
757
+ return getEntityLabel(entity);
758
+ })
759
+ .filter((label: string) => {
760
+ return Boolean(label);
761
+ })
762
+ .join(", ")
763
+ : getEntityLabel(data);
764
+ }
765
+
611
766
  // Determine style-based classes
612
767
  const styleType: DetailStyle = props.style || DetailStyle.Default;
613
768
  const isCardStyle: boolean = styleType === DetailStyle.Card;
@@ -5,6 +5,7 @@ import DropdownValueBadge from "../Dropdown/DropdownValueBadge";
5
5
  import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
6
6
  import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
7
7
  import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
8
+ import { CustomFieldDefinition } from "../../../Types/CustomField/CustomFieldDefinition";
8
9
  import {
9
10
  CustomFieldDropdownOption,
10
11
  parseCustomFieldDropdownOptions,
@@ -45,12 +46,7 @@ import React, { ReactElement } from "react";
45
46
  * fall-through text branch understands a dotted key.
46
47
  */
47
48
 
48
- export interface CustomFieldDefinition {
49
- name: string;
50
- description?: string | undefined;
51
- customFieldType?: CustomFieldType | undefined;
52
- dropdownOptions?: string | undefined;
53
- }
49
+ export type { CustomFieldDefinition } from "../../../Types/CustomField/CustomFieldDefinition";
54
50
 
55
51
  export const CustomFieldsColumnKey: string = "customFields";
56
52
 
@@ -55,18 +55,37 @@ const useCustomFieldColumns: UseCustomFieldColumnsFunction = <
55
55
  const [definitions, setDefinitions] = useState<Array<CustomFieldDefinition>>(
56
56
  [],
57
57
  );
58
- const [isLoading, setIsLoading] = useState<boolean>(false);
59
- const [error, setError] = useState<string>("");
60
58
 
61
59
  const projectId: ObjectID | null =
62
60
  data.projectId ?? ProjectUtil.getCurrentProjectId();
63
61
 
62
+ /*
63
+ * Starts true whenever a request is actually going to happen, rather than
64
+ * false-until-the-effect-runs.
65
+ *
66
+ * Columns did not care — they only ever appear — but a caller that has to
67
+ * distinguish "no custom fields" from "not yet" does. The facet bar is one:
68
+ * on the first render it must know that a chip restored from a shared link
69
+ * may still be on its way, or it drops the link's filter before the request
70
+ * has even been sent.
71
+ */
72
+ const [isLoading, setIsLoading] = useState<boolean>(
73
+ Boolean(data.customFieldsModelType && projectId),
74
+ );
75
+ const [error, setError] = useState<string>("");
76
+
64
77
  const projectIdString: string = projectId?.toString() || "";
65
78
  const modelName: string = customFieldsModelType?.name || "";
66
79
 
67
80
  useEffect(() => {
68
81
  if (!customFieldsModelType || !projectId) {
69
82
  setDefinitions([]);
83
+ /*
84
+ * Nothing to wait for. Said explicitly because the initial state is now
85
+ * "loading" whenever a request is expected, and a model type that goes
86
+ * away between renders would otherwise leave it stuck there.
87
+ */
88
+ setIsLoading(false);
70
89
  return;
71
90
  }
72
91