@oneuptime/common 11.0.10 → 11.0.13

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 (148) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +134 -0
  2. package/Models/DatabaseModels/CloudResource.ts +134 -0
  3. package/Models/DatabaseModels/DockerHost.ts +134 -0
  4. package/Models/DatabaseModels/DockerSwarmCluster.ts +134 -0
  5. package/Models/DatabaseModels/Host.ts +134 -0
  6. package/Models/DatabaseModels/KubernetesCluster.ts +134 -0
  7. package/Models/DatabaseModels/PodmanHost.ts +134 -0
  8. package/Models/DatabaseModels/ProxmoxCluster.ts +134 -0
  9. package/Models/DatabaseModels/RumApplication.ts +134 -0
  10. package/Models/DatabaseModels/ServerlessFunction.ts +134 -0
  11. package/Models/DatabaseModels/Service.ts +134 -0
  12. package/Models/DatabaseModels/StatusPage.ts +0 -42
  13. package/README.md +1 -1
  14. package/Scripts/generate-service-worker.js +46 -36
  15. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +4 -2
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.ts +331 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.ts +35 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Readme.md +3 -3
  20. package/Server/Infrastructure/Queue.ts +122 -0
  21. package/Server/Services/AIAgentService.ts +9 -4
  22. package/Server/Services/AnalyticsDatabaseService.ts +89 -56
  23. package/Server/Services/CephClusterService.ts +6 -4
  24. package/Server/Services/CloudResourceService.ts +6 -4
  25. package/Server/Services/DatabaseService.ts +126 -0
  26. package/Server/Services/DockerHostService.ts +6 -4
  27. package/Server/Services/DockerSwarmClusterService.ts +6 -4
  28. package/Server/Services/HostService.ts +6 -4
  29. package/Server/Services/KubernetesClusterService.ts +6 -4
  30. package/Server/Services/PodmanHostService.ts +6 -4
  31. package/Server/Services/ProbeService.ts +10 -4
  32. package/Server/Services/ProxmoxClusterService.ts +6 -4
  33. package/Server/Services/RumApplicationService.ts +6 -4
  34. package/Server/Services/ServerlessFunctionService.ts +6 -4
  35. package/Server/Services/ServiceService.ts +17 -4
  36. package/Server/Types/Markdown.ts +103 -0
  37. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +5 -2
  38. package/Server/Utils/CodeRepository/CodeRepository.ts +78 -0
  39. package/Server/Utils/Execute.ts +6 -0
  40. package/Server/Utils/Logger.ts +66 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +24 -10
  42. package/Server/Utils/Telemetry/EntityRegistry.ts +10 -5
  43. package/Server/Utils/Workspace/Slack/app-manifest.json +86 -92
  44. package/Tests/Server/Services/UpdateColumnsByIdWithoutHooks.test.ts +154 -0
  45. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +87 -9
  46. package/Tests/Types/DiskSize.test.ts +49 -0
  47. package/Types/Dashboard/DashboardComponents/DashboardTableComponent.ts +11 -0
  48. package/Types/Icon/IconProp.ts +1 -0
  49. package/Types/Monitor/MonitorType.ts +15 -31
  50. package/Types/Monitor/schema.json +32 -35
  51. package/UI/Components/BulkUpdate/BulkArchiveActions.tsx +150 -0
  52. package/UI/Components/Icon/Icon.tsx +42 -126
  53. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -5
  54. package/UI/esbuild-config.js +136 -110
  55. package/Utils/ClientAPI/README.md +1 -1
  56. package/build/dist/Models/DatabaseModels/CephCluster.js +136 -0
  57. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  58. package/build/dist/Models/DatabaseModels/CloudResource.js +136 -0
  59. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/DockerHost.js +136 -0
  61. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +136 -0
  63. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Host.js +136 -0
  65. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +136 -0
  67. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/PodmanHost.js +136 -0
  69. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +136 -0
  71. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/RumApplication.js +136 -0
  73. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  74. package/build/dist/Models/DatabaseModels/ServerlessFunction.js +136 -0
  75. package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/Service.js +136 -0
  77. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/StatusPage.js +0 -44
  79. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  80. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +4 -2
  81. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js +120 -0
  83. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js.map +1 -0
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.js +28 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  88. package/build/dist/Server/Infrastructure/Queue.js +63 -0
  89. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  90. package/build/dist/Server/Services/AIAgentService.js +9 -4
  91. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  92. package/build/dist/Server/Services/AnalyticsDatabaseService.js +52 -17
  93. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  94. package/build/dist/Server/Services/CephClusterService.js +6 -4
  95. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  96. package/build/dist/Server/Services/CloudResourceService.js +6 -4
  97. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  98. package/build/dist/Server/Services/DatabaseService.js +101 -0
  99. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  100. package/build/dist/Server/Services/DockerHostService.js +6 -4
  101. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  102. package/build/dist/Server/Services/DockerSwarmClusterService.js +6 -4
  103. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  104. package/build/dist/Server/Services/HostService.js +6 -4
  105. package/build/dist/Server/Services/HostService.js.map +1 -1
  106. package/build/dist/Server/Services/KubernetesClusterService.js +6 -4
  107. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  108. package/build/dist/Server/Services/PodmanHostService.js +6 -4
  109. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  110. package/build/dist/Server/Services/ProbeService.js +10 -4
  111. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  112. package/build/dist/Server/Services/ProxmoxClusterService.js +6 -4
  113. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  114. package/build/dist/Server/Services/RumApplicationService.js +6 -4
  115. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  116. package/build/dist/Server/Services/ServerlessFunctionService.js +6 -4
  117. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  118. package/build/dist/Server/Services/ServiceService.js +17 -4
  119. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  120. package/build/dist/Server/Types/Markdown.js +87 -0
  121. package/build/dist/Server/Types/Markdown.js.map +1 -1
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +3 -0
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/CodeRepository/CodeRepository.js +60 -0
  125. package/build/dist/Server/Utils/CodeRepository/CodeRepository.js.map +1 -1
  126. package/build/dist/Server/Utils/Execute.js +1 -3
  127. package/build/dist/Server/Utils/Execute.js.map +1 -1
  128. package/build/dist/Server/Utils/Logger.js +49 -1
  129. package/build/dist/Server/Utils/Logger.js.map +1 -1
  130. package/build/dist/Server/Utils/Monitor/MonitorResource.js +24 -10
  131. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  132. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +10 -5
  133. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  134. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +3 -9
  135. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTableComponent.js.map +1 -1
  136. package/build/dist/Types/Icon/IconProp.js +1 -0
  137. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorType.js +15 -31
  139. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  140. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js +96 -0
  141. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js.map +1 -0
  142. package/build/dist/UI/Components/Icon/Icon.js +17 -41
  143. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  144. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -3
  145. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  146. package/jest.config.json +47 -66
  147. package/package.json +1 -1
  148. package/tsconfig.json +109 -113
@@ -193,12 +193,14 @@ export class Service extends DatabaseService<Model> {
193
193
  data.agentVersion = extra.agentVersion;
194
194
  }
195
195
 
196
- await this.updateOneById({
196
+ /*
197
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
198
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
199
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
200
+ */
201
+ await this.updateColumnsByIdWithoutHooks({
197
202
  id: clusterId,
198
203
  data: data,
199
- props: {
200
- isRoot: true,
201
- },
202
204
  });
203
205
  }
204
206
 
@@ -236,12 +236,14 @@ export class Service extends DatabaseService<Model> {
236
236
  data.agentVersion = extra.agentVersion;
237
237
  }
238
238
 
239
- await this.updateOneById({
239
+ /*
240
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
241
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
242
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
243
+ */
244
+ await this.updateColumnsByIdWithoutHooks({
240
245
  id: hostId,
241
246
  data: data,
242
- props: {
243
- isRoot: true,
244
- },
245
247
  });
246
248
  }
247
249
 
@@ -120,14 +120,20 @@ export class Service extends DatabaseService<Model> {
120
120
 
121
121
  const now: Date = OneUptimeDate.getCurrentDate();
122
122
 
123
- await this.updateOneById({
123
+ /*
124
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
125
+ * `version` bump. This fires from the probe-auth middleware on every
126
+ * authenticated probe request, so it must not head the hot-row Postgres
127
+ * lock convoy the full updateOneById pipeline causes. A lastAlive-only
128
+ * write never sets connectionStatus, so the connection-status-change
129
+ * hooks (onBeforeUpdate/onUpdateSuccess) are inert here. See
130
+ * ServiceService.updateLastSeen.
131
+ */
132
+ await this.updateColumnsByIdWithoutHooks({
124
133
  id: probeId,
125
134
  data: {
126
135
  lastAlive: now,
127
136
  },
128
- props: {
129
- isRoot: true,
130
- },
131
137
  });
132
138
  }
133
139
 
@@ -250,12 +250,14 @@ export class Service extends DatabaseService<Model> {
250
250
  data.guestsWithoutBackupCount = extra.guestsWithoutBackupCount;
251
251
  }
252
252
 
253
- await this.updateOneById({
253
+ /*
254
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
255
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
256
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
257
+ */
258
+ await this.updateColumnsByIdWithoutHooks({
254
259
  id: clusterId,
255
260
  data: data,
256
- props: {
257
- isRoot: true,
258
- },
259
261
  });
260
262
  }
261
263
 
@@ -185,12 +185,14 @@ export class Service extends DatabaseService<Model> {
185
185
  data.sdkLanguage = extra.sdkLanguage;
186
186
  }
187
187
 
188
- await this.updateOneById({
188
+ /*
189
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
190
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
191
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
192
+ */
193
+ await this.updateColumnsByIdWithoutHooks({
189
194
  id: rumApplicationId,
190
195
  data: data,
191
- props: {
192
- isRoot: true,
193
- },
194
196
  });
195
197
  }
196
198
 
@@ -229,12 +229,14 @@ export class Service extends DatabaseService<Model> {
229
229
  data.runtimeVersion = extra.runtimeVersion;
230
230
  }
231
231
 
232
- await this.updateOneById({
232
+ /*
233
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
234
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
235
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
236
+ */
237
+ await this.updateColumnsByIdWithoutHooks({
233
238
  id: serverlessFunctionId,
234
239
  data: data,
235
- props: {
236
- isRoot: true,
237
- },
238
240
  });
239
241
  }
240
242
 
@@ -198,12 +198,25 @@ export class Service extends DatabaseService<Model> {
198
198
  data.cloudAccountId = extra.cloudAccountId;
199
199
  }
200
200
 
201
- await this.updateOneById({
201
+ /*
202
+ * Heartbeat write. This is the single hottest UPDATE in the system —
203
+ * every telemetry batch for a service re-resolves the same serviceId —
204
+ * so it must NOT go through the full updateOneById pipeline (permission
205
+ * SELECT -> _findBy SELECT -> save() with `version = version + 1` ->
206
+ * workflow/realtime/audit hooks). That pipeline holds the Service row's
207
+ * write lock across several round trips and, when an event loop stalls
208
+ * mid-`save()` transaction, leaves it open as the head of a lock convoy
209
+ * that starves the Postgres connection pool. lastSeenAt is an internal
210
+ * liveness timestamp with no onBeforeUpdate/onUpdateSuccess hooks on this
211
+ * model (connected / disconnected status is derived from it at read
212
+ * time), so a bare single-statement UPDATE is both correct and far
213
+ * cheaper. This also intentionally drops the per-update workflow trigger
214
+ * Service's @EnableWorkflow({ update: true }) fired on every heartbeat —
215
+ * a liveness ping should not trigger user workflows.
216
+ */
217
+ await this.updateColumnsByIdWithoutHooks({
202
218
  id: serviceId,
203
219
  data: data,
204
- props: {
205
- isRoot: true,
206
- },
207
220
  });
208
221
  }
209
222
 
@@ -7,6 +7,8 @@ export enum MarkdownContentType {
7
7
  Docs,
8
8
  Blog,
9
9
  Email,
10
+ // Compact rendering tuned for small surfaces (e.g. the blog validation modal).
11
+ BlogValidation,
10
12
  }
11
13
 
12
14
  export default class Markdown {
@@ -73,6 +75,7 @@ export default class Markdown {
73
75
  private static blogRenderer: Renderer | null = null;
74
76
  private static docsRenderer: Renderer | null = null;
75
77
  private static emailRenderer: Renderer | null = null;
78
+ private static blogValidationRenderer: Renderer | null = null;
76
79
 
77
80
  @CaptureSpan()
78
81
  public static async convertToHTML(
@@ -93,6 +96,10 @@ export default class Markdown {
93
96
  renderer = this.getEmailRenderer();
94
97
  }
95
98
 
99
+ if (contentType === MarkdownContentType.BlogValidation) {
100
+ renderer = this.getBlogValidationRenderer();
101
+ }
102
+
96
103
  /*
97
104
  * Escape raw HTML tokens in the markdown source to prevent XSS.
98
105
  * This only affects raw HTML that users type directly (e.g. <img onerror=...>),
@@ -132,6 +139,102 @@ export default class Markdown {
132
139
  return renderer;
133
140
  }
134
141
 
142
+ /*
143
+ * Compact renderer for small surfaces like the blog validation modal. Smaller,
144
+ * quieter headings (no oversized doc titles, no permalink anchors), tight
145
+ * paragraph/list rhythm, and subtle code styling so a long report stays
146
+ * readable in a constrained, scrollable panel.
147
+ */
148
+ private static getBlogValidationRenderer(): Renderer {
149
+ if (this.blogValidationRenderer !== null) {
150
+ return this.blogValidationRenderer;
151
+ }
152
+
153
+ const renderer: Renderer = new Renderer();
154
+
155
+ renderer.heading = function (text, level) {
156
+ if (level <= 2) {
157
+ return `<h3 class="mt-7 mb-3 pt-6 border-t border-gray-100 first:mt-0 first:pt-0 first:border-t-0 text-[0.7rem] font-semibold uppercase tracking-wider text-emerald-700">${text}</h3>`;
158
+ }
159
+ if (level === 3) {
160
+ return `<h4 class="mt-5 mb-1.5 text-sm font-semibold text-gray-900">${text}</h4>`;
161
+ }
162
+ return `<h5 class="mt-4 mb-1 text-sm font-semibold text-gray-700">${text}</h5>`;
163
+ };
164
+
165
+ renderer.paragraph = function (text) {
166
+ return `<p class="my-2.5 text-sm leading-relaxed text-gray-600">${text}</p>`;
167
+ };
168
+
169
+ renderer.list = function (body, ordered, start) {
170
+ const tag: string = ordered ? "ol" : "ul";
171
+ const cls: string = ordered
172
+ ? "list-decimal pl-5 my-2 space-y-1.5 text-sm text-gray-600 marker:text-gray-400"
173
+ : "list-disc pl-5 my-2 space-y-1.5 text-sm text-gray-600 marker:text-gray-300";
174
+ const startAttr: string =
175
+ ordered && start !== 1 ? ` start="${start}"` : "";
176
+ return `<${tag}${startAttr} class="${cls}">${body}</${tag}>`;
177
+ };
178
+ renderer.listitem = function (text) {
179
+ return `<li class="leading-relaxed pl-1">${text}</li>`;
180
+ };
181
+
182
+ renderer.strong = function (text) {
183
+ return `<strong class="font-semibold text-gray-800">${text}</strong>`;
184
+ };
185
+ renderer.em = function (text) {
186
+ return `<em class="italic">${text}</em>`;
187
+ };
188
+
189
+ renderer.blockquote = function (quote) {
190
+ return `<blockquote class="my-3 border-s-2 border-emerald-200 ps-3 text-sm text-gray-600">${quote}</blockquote>`;
191
+ };
192
+
193
+ renderer.hr = function () {
194
+ return '<hr class="my-5 border-t border-gray-100" />';
195
+ };
196
+
197
+ renderer.codespan = function (code) {
198
+ const escaped: string = Markdown.escapeHtml(code);
199
+ return `<code class="rounded bg-gray-100 px-1.5 py-0.5 text-[0.8125rem] text-gray-700 font-mono break-words">${escaped}</code>`;
200
+ };
201
+
202
+ renderer.code = function (code, language) {
203
+ const escaped: string = Markdown.escapeHtml(code);
204
+ return `<pre class="my-3 overflow-x-auto rounded-lg bg-gray-50 border border-gray-100 p-3 text-xs leading-relaxed"><code class="language-${language} font-mono text-gray-700">${escaped}</code></pre>`;
205
+ };
206
+
207
+ renderer.table = function (header, body) {
208
+ return `<div class="my-4 overflow-x-auto rounded-lg border border-gray-100"><table class="min-w-full text-sm text-left">${header}${body}</table></div>`;
209
+ };
210
+ renderer.tablerow = function (content) {
211
+ return `<tr class="border-b border-gray-100 last:border-b-0">${content}</tr>`;
212
+ };
213
+ renderer.tablecell = function (content, flags) {
214
+ const tag: string = flags.header ? "th" : "td";
215
+ const align: string = flags.align ? ` text-${flags.align}` : "";
216
+ const headerClass: string = flags.header
217
+ ? " font-semibold text-gray-900 bg-gray-50"
218
+ : " text-gray-600";
219
+ return `<${tag} class="px-3 py-2${align}${headerClass}">${content}</${tag}>`;
220
+ };
221
+
222
+ renderer.link = function (href, _title, text) {
223
+ if (!href) {
224
+ return text as string;
225
+ }
226
+ const isExternal: boolean = href.startsWith("http");
227
+ const rel: string = isExternal
228
+ ? ' target="_blank" rel="noopener noreferrer"'
229
+ : "";
230
+ return `<a href="${href}"${rel} class="text-emerald-700 font-medium underline decoration-emerald-200 underline-offset-2 hover:decoration-emerald-400 break-words">${text}</a>`;
231
+ };
232
+
233
+ this.blogValidationRenderer = renderer;
234
+
235
+ return renderer;
236
+ }
237
+
135
238
  private static slugify(text: string): string {
136
239
  return text
137
240
  .toLowerCase()
@@ -87,7 +87,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
87
87
  .append(setStatement)
88
88
  .append(
89
89
  SQL`
90
- WHERE TRUE `
90
+ WHERE TRUE `,
91
91
  )
92
92
  .append(whereStatement);
93
93
  /* eslint-enable prettier/prettier */
@@ -193,8 +193,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
193
193
  return record;
194
194
  }
195
195
 
196
- private escapeStringLiteral(raw: string): string {
196
+ private escapeStringLiteral(raw: string | undefined | null): string {
197
197
  // escape String literal based on https://clickhouse.com/docs/en/sql-reference/syntax#string
198
+ if (raw === undefined || raw === null) {
199
+ return "''";
200
+ }
198
201
  return `'${raw.replace(/'|\\/g, "\\$&")}'`;
199
202
  }
200
203
 
@@ -385,6 +385,84 @@ export default class CodeRepositoryUtil {
385
385
  return hash.trim();
386
386
  }
387
387
 
388
+ /**
389
+ * Returns the author (name + email) and the list of files changed for every
390
+ * non-merge commit in the repository, newest first. Optionally limited to a
391
+ * pathspec (e.g. "posts"). Used to derive per-directory contributors.
392
+ *
393
+ * Note: a shallow clone (`--depth 1`) only has the latest commit, so this
394
+ * returns a single contributor unless the repo was cloned with history.
395
+ */
396
+ @CaptureSpan()
397
+ public static async getCommitAuthorsWithFiles(data: {
398
+ repoPath: string;
399
+ path?: string | undefined;
400
+ }): Promise<
401
+ Array<{
402
+ authorName: string;
403
+ authorEmail: string;
404
+ files: Array<string>;
405
+ }>
406
+ > {
407
+ /*
408
+ * ASCII record (0x1e) / unit (0x1f) separators never occur in author names,
409
+ * emails or file paths, so they delimit commits and fields unambiguously.
410
+ */
411
+ const recordSeparator: string = "\x1e";
412
+ const unitSeparator: string = "\x1f";
413
+
414
+ const args: Array<string> = [
415
+ "log",
416
+ "--no-merges",
417
+ `--pretty=format:${recordSeparator}%an${unitSeparator}%ae`,
418
+ "--name-only",
419
+ ];
420
+
421
+ if (data.path) {
422
+ args.push("--", data.path);
423
+ }
424
+
425
+ const stdout: string = await Execute.executeCommandFile({
426
+ command: "git",
427
+ args,
428
+ cwd: path.resolve(data.repoPath),
429
+ // git log over a full repo can easily exceed the 1 MB default.
430
+ maxBuffer: 256 * 1024 * 1024,
431
+ });
432
+
433
+ const commits: Array<{
434
+ authorName: string;
435
+ authorEmail: string;
436
+ files: Array<string>;
437
+ }> = [];
438
+
439
+ for (const record of stdout.split(recordSeparator)) {
440
+ if (!record.trim()) {
441
+ continue;
442
+ }
443
+
444
+ const lines: Array<string> = record.split("\n");
445
+ const header: string = lines.shift() || "";
446
+ const [authorName, authorEmail] = header.split(unitSeparator);
447
+
448
+ const files: Array<string> = lines
449
+ .map((line: string) => {
450
+ return line.trim();
451
+ })
452
+ .filter((line: string) => {
453
+ return line.length > 0;
454
+ });
455
+
456
+ commits.push({
457
+ authorName: (authorName || "").trim(),
458
+ authorEmail: (authorEmail || "").trim(),
459
+ files,
460
+ });
461
+ }
462
+
463
+ return commits;
464
+ }
465
+
388
466
  @CaptureSpan()
389
467
  public static async listFilesInDirectory(data: {
390
468
  directoryPath: string;
@@ -54,6 +54,11 @@ export default class Execute {
54
54
  command: string;
55
55
  args: Array<string>;
56
56
  cwd: string;
57
+ /*
58
+ * Override the child_process default stdout/stderr cap (1 MB). Commands that
59
+ * can emit large output (e.g. `git log` over a whole repo) should raise this.
60
+ */
61
+ maxBuffer?: number | undefined;
57
62
  }): Promise<string> {
58
63
  return new Promise(
59
64
  (
@@ -65,6 +70,7 @@ export default class Execute {
65
70
  data.args,
66
71
  {
67
72
  cwd: data.cwd,
73
+ ...(data.maxBuffer ? { maxBuffer: data.maxBuffer } : {}),
68
74
  },
69
75
  (err: ExecException | null, stdout: string, stderr: string) => {
70
76
  if (err) {
@@ -8,6 +8,12 @@ import ConfigLogLevel from "../Types/ConfigLogLevel";
8
8
 
9
9
  export type LogBody = string | JSONObject | Exception | Error | unknown;
10
10
 
11
+ export interface RecentLogEntry {
12
+ time: string;
13
+ level: string;
14
+ message: string;
15
+ }
16
+
11
17
  export interface LogAttributes {
12
18
  userId?: string | undefined;
13
19
  projectId?: string | undefined;
@@ -50,6 +56,56 @@ export function getLogAttributesFromRequest(
50
56
  }
51
57
 
52
58
  export default class logger {
59
+ /*
60
+ * In-memory ring buffer of the most recent log lines this process emitted.
61
+ * The app writes logs to stdout only (no on-disk file), so this is the one
62
+ * way the master-admin support bundle / health dashboard can surface this
63
+ * instance's own recent logs for debugging — there is no path to the
64
+ * container's stdout or to other containers' logs from inside the process.
65
+ */
66
+ private static recentLogs: Array<RecentLogEntry> = [];
67
+ private static readonly maxRecentLogs: number = 1000;
68
+ private static readonly recentLogTrimSlack: number = 256;
69
+ private static readonly maxRecentLogMessageLength: number = 4000;
70
+
71
+ private static record(level: string, body: LogBody): void {
72
+ try {
73
+ const message: string = this.serializeLogBody(body);
74
+
75
+ this.recentLogs.push({
76
+ time: new Date().toISOString(),
77
+ level: level,
78
+ message:
79
+ message.length > this.maxRecentLogMessageLength
80
+ ? `${message.substring(0, this.maxRecentLogMessageLength)}… (truncated)`
81
+ : message,
82
+ });
83
+
84
+ /*
85
+ * Trim in batches rather than on every push so this stays O(1) amortised
86
+ * on the logging hot path: only re-slice once we have grown past a soft
87
+ * ceiling above the target size.
88
+ */
89
+ if (
90
+ this.recentLogs.length >
91
+ this.maxRecentLogs + this.recentLogTrimSlack
92
+ ) {
93
+ this.recentLogs = this.recentLogs.slice(-this.maxRecentLogs);
94
+ }
95
+ } catch {
96
+ // Never let log capture break the app.
97
+ }
98
+ }
99
+
100
+ // Newest-last snapshot of the recent-log ring buffer (optionally the last N).
101
+ public static getRecentLogs(limit?: number): Array<RecentLogEntry> {
102
+ if (!limit || limit >= this.recentLogs.length) {
103
+ return [...this.recentLogs];
104
+ }
105
+
106
+ return this.recentLogs.slice(this.recentLogs.length - limit);
107
+ }
108
+
53
109
  public static getLogLevel(): ConfigLogLevel {
54
110
  if (!LogLevel) {
55
111
  return ConfigLogLevel.INFO;
@@ -98,6 +154,8 @@ export default class logger {
98
154
  // eslint-disable-next-line no-console
99
155
  console.info(message);
100
156
 
157
+ this.record("INFO", message);
158
+
101
159
  this.emit({
102
160
  body: message,
103
161
  severityNumber: SeverityNumber.INFO,
@@ -118,6 +176,8 @@ export default class logger {
118
176
  // eslint-disable-next-line no-console
119
177
  console.error(message);
120
178
 
179
+ this.record("ERROR", message);
180
+
121
181
  this.emit({
122
182
  body: message,
123
183
  severityNumber: SeverityNumber.ERROR,
@@ -137,6 +197,8 @@ export default class logger {
137
197
  // eslint-disable-next-line no-console
138
198
  console.warn(message);
139
199
 
200
+ this.record("WARN", message);
201
+
140
202
  this.emit({
141
203
  body: message,
142
204
  severityNumber: SeverityNumber.WARN,
@@ -152,6 +214,8 @@ export default class logger {
152
214
  // eslint-disable-next-line no-console
153
215
  console.debug(message);
154
216
 
217
+ this.record("DEBUG", message);
218
+
155
219
  this.emit({
156
220
  body: message,
157
221
  severityNumber: SeverityNumber.DEBUG,
@@ -203,6 +267,8 @@ export default class logger {
203
267
  // eslint-disable-next-line no-console
204
268
  console.trace(message);
205
269
 
270
+ this.record("TRACE", message);
271
+
206
272
  this.emit({
207
273
  body: message,
208
274
  severityNumber: SeverityNumber.DEBUG,
@@ -316,17 +316,27 @@ export default class MonitorResourceUtil {
316
316
  * the monitor to flap between Online and Offline every minute.
317
317
  */
318
318
  if (!serverMonitorResponse.onlyCheckRequestReceivedAt) {
319
- await MonitorService.updateOneById({
319
+ /*
320
+ * Heartbeat write: single-statement UPDATE, no hooks and no
321
+ * `version` bump. This DELIBERATELY drops the per-update workflow
322
+ * trigger and audit-log entry that Monitor's @EnableWorkflow /
323
+ * @EnableAuditLog fire on every changed update — the old
324
+ * `ignoreHooks: true` did NOT suppress those (they are gated on
325
+ * the model flag, not on ignoreHooks), so a heartbeat used to
326
+ * spam an on-update workflow + an audit row every ingest. A
327
+ * liveness ping should do neither. onUpdateSuccess is inert here
328
+ * regardless (gated on status/interval/steps/name/etc., none of
329
+ * which are written). Also skips the pre-SELECT that would reload
330
+ * the large serverMonitorResponse jsonb row. See
331
+ * ServiceService.updateLastSeen.
332
+ */
333
+ await MonitorService.updateColumnsByIdWithoutHooks({
320
334
  id: monitor.id!,
321
335
  data: {
322
336
  serverMonitorRequestReceivedAt:
323
337
  serverMonitorResponse.requestReceivedAt!,
324
338
  serverMonitorResponse,
325
339
  },
326
- props: {
327
- isRoot: true,
328
- ignoreHooks: true,
329
- },
330
340
  });
331
341
 
332
342
  logger.debug(
@@ -344,7 +354,15 @@ export default class MonitorResourceUtil {
344
354
  `${dataToProcess.monitorId.toString()} - Incoming request received at ${incomingMonitorRequest.incomingRequestReceivedAt}`,
345
355
  );
346
356
 
347
- await MonitorService.updateOneById({
357
+ /*
358
+ * Heartbeat write: single-statement UPDATE, no hooks and no
359
+ * `version` bump. As with the server-monitor branch above, this
360
+ * deliberately drops the per-update workflow trigger + audit-log
361
+ * entry Monitor would otherwise fire on every heartbeat, and skips
362
+ * the pre-SELECT of the large incomingMonitorRequest jsonb row. See
363
+ * ServiceService.updateLastSeen.
364
+ */
365
+ await MonitorService.updateColumnsByIdWithoutHooks({
348
366
  id: monitor.id!,
349
367
  data: {
350
368
  incomingRequestMonitorHeartbeatCheckedAt:
@@ -353,10 +371,6 @@ export default class MonitorResourceUtil {
353
371
  JSON.stringify(incomingMonitorRequest),
354
372
  ) as IncomingMonitorRequest,
355
373
  } as any,
356
- props: {
357
- isRoot: true,
358
- ignoreHooks: true,
359
- },
360
374
  });
361
375
 
362
376
  logger.debug(
@@ -271,11 +271,17 @@ export async function reconcileByNaturalKey<
271
271
  });
272
272
 
273
273
  if (existing) {
274
- // Throttled bump of lastSeenAt (+ any caller-supplied merge fields).
275
- await data.service.updateOneById({
274
+ /*
275
+ * Throttled bump of lastSeenAt (+ any caller-supplied merge fields).
276
+ * Heartbeat write: single-statement UPDATE, no hooks and no `version`
277
+ * bump (TelemetryEntity/Relationship enable no update workflow/realtime/
278
+ * audit). buildBump returns only plain values — lastSeenAt plus, at most,
279
+ * the descriptiveAttributes / labels JSON columns — which the primitive
280
+ * persists via the driver transformer path. See ServiceService.updateLastSeen.
281
+ */
282
+ await data.service.updateColumnsByIdWithoutHooks({
276
283
  id: existing.id!,
277
284
  data: buildBump(existing),
278
- props: { isRoot: true },
279
285
  });
280
286
  return;
281
287
  }
@@ -306,10 +312,9 @@ export async function reconcileByNaturalKey<
306
312
  logger.debug(
307
313
  `EntityRegistry: create raced for ${data.describe} (concurrent insert); bumping lastSeenAt on the winning row.`,
308
314
  );
309
- await data.service.updateOneById({
315
+ await data.service.updateColumnsByIdWithoutHooks({
310
316
  id: winner.id!,
311
317
  data: buildBump(winner),
312
- props: { isRoot: true },
313
318
  });
314
319
  return;
315
320
  }