querysub 0.328.0 → 0.330.0

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.
@@ -0,0 +1,409 @@
1
+ module.noserverhotreload = false;
2
+ module.hotreload = true;
3
+
4
+
5
+ import { SocketFunction } from "socket-function/SocketFunction";
6
+ import { qreact } from "../../../4-dom/qreact";
7
+ import { getSyncedController } from "../../../library-components/SyncedController";
8
+ import { ErrorDigestController, errorDigestHistory, ErrorDigestInfo } from "./errorDigests";
9
+ import { isManagementUser } from "../../../-0-hooks/hooks";
10
+ import { assertIsManagementUser } from "../../managementPages";
11
+ import { URLParam } from "../../../library-components/URLParam";
12
+ import { tabURL } from "../../../library-components/urlResetGroups";
13
+ import { Anchor, ATag } from "../../../library-components/ATag";
14
+ import { css } from "../../../4-dom/css";
15
+ import { list, sort } from "socket-function/src/misc";
16
+ import { formatDateTime, formatVeryNiceDateTime, formatNumber } from "socket-function/src/formatting/format";
17
+ import { Table } from "../../../5-diagnostics/Table";
18
+ import { Histogram } from "../../../library-components/Histogram";
19
+ import { TabbedUI } from "../../../library-components/TabbedUI";
20
+ import { LogDatum } from "../diskLogger";
21
+ import { t } from "../../../2-proxy/schema2";
22
+ import { getErrorLogsLink } from "./ErrorWarning";
23
+
24
+ export const digestKeyURL = new URLParam("digestKey", "");
25
+ export const fileDetailTabURL = new URLParam("fileDetailTab", "errors");
26
+
27
+ export class ErrorDigestPage extends qreact.Component {
28
+ state = t.state({
29
+ selectedFile: t.string("")
30
+ });
31
+ render() {
32
+ // Get the current tab mode
33
+ let currentTab = tabURL.value;
34
+ let selectedDigestKey = digestKeyURL.value;
35
+
36
+ if (currentTab === "detail" && selectedDigestKey) {
37
+ return this.renderDigestDetail(selectedDigestKey);
38
+ }
39
+
40
+ return this.renderDigestList();
41
+ }
42
+
43
+ renderDigestList() {
44
+ let controller = ErrorDigestController(SocketFunction.browserNodeId());
45
+
46
+ let keys = controller.getDigestKeys();
47
+
48
+ if (!keys || keys.length === 0) {
49
+ return (
50
+ <div className={css.vbox(16).pad2(24)}>
51
+ <div className={css.fontSize(24).fontWeight(600)}>
52
+ Error Digest History
53
+ </div>
54
+ <div>No digest data available</div>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ // Sort lexicographically (highest = newest) and take latest 100
60
+ let sortedKeys = sort(keys as string[], key => key).reverse().slice(0, 100);
61
+
62
+ return (
63
+ <div className={css.vbox(16).paddingTop(24).paddingLeft(24).fillHeight.fillWidth}>
64
+ <div className={css.fontSize(24).fontWeight(600)}>
65
+ Error Digest History
66
+ </div>
67
+ <div className={css.fontSize(14).color("gray")}>
68
+ Showing latest {Math.min(sortedKeys.length, 100)} digests (sorted by creation time, newest first)
69
+ </div>
70
+
71
+ <div className={css.vbox(8).overflowAuto.fillWidth}>
72
+ {sortedKeys.map(key => (
73
+ <ATag
74
+ key={key}
75
+ values={[
76
+ tabURL.getOverride("detail"),
77
+ digestKeyURL.getOverride(key)
78
+ ]}
79
+ className={css.pad2(12).hsla(0, 0, 0, 0.1).pointer.hslahover(0, 0, 0, 0.15)}
80
+ >
81
+ {formatVeryNiceDateTime(+key.split(".")[0])}
82
+ </ATag>
83
+ ))}
84
+ </div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ renderDigestDetail(digestKey: string) {
90
+ let controller = ErrorDigestController(SocketFunction.browserNodeId());
91
+
92
+ let digestData = controller.getDigest(digestKey);
93
+
94
+ if (!digestData) {
95
+ return (
96
+ <div className={css.vbox(16).pad2(24)}>
97
+ <div className={css.hbox(16)}>
98
+ <ATag
99
+ values={[
100
+ tabURL.getOverride(""),
101
+ digestKeyURL.getOverride("")
102
+ ]}
103
+ >
104
+ ← Back to Digest List
105
+ </ATag>
106
+ </div>
107
+ <div>Loading digest data...</div>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ // If a file is selected, show file details
113
+ if (this.state.selectedFile) {
114
+ return this.renderFileDetail(digestData, digestKey);
115
+ }
116
+
117
+ return (
118
+ <div className={css.vbox(24).pad2(24).fillHeight.overflowAuto}>
119
+ {/* Header */}
120
+ <div className={css.hbox(16).alignItems("center")}>
121
+ <ATag
122
+ values={[
123
+ tabURL.getOverride(""),
124
+ digestKeyURL.getOverride("")
125
+ ]}
126
+ >
127
+ ← Back to Digest List
128
+ </ATag>
129
+ <div className={css.fontSize(24).fontWeight(600)}>
130
+ Digest: {formatVeryNiceDateTime(+digestKey.split(".")[0])}
131
+ </div>
132
+ </div>
133
+
134
+ <Anchor
135
+ className={css.fontSize(24)}
136
+ values={getErrorLogsLink({
137
+ startTime: digestData.startTime,
138
+ endTime: digestData.endTime,
139
+ })}
140
+ >
141
+ View live logs
142
+ </Anchor>
143
+
144
+ {/* Metadata Section */}
145
+ {this.renderMetadata(digestData)}
146
+
147
+ {/* Charts Section */}
148
+ {this.renderCharts(digestData)}
149
+
150
+ {/* Files Table */}
151
+ {this.renderFilesTable(digestData)}
152
+ </div>
153
+ );
154
+ }
155
+
156
+ renderMetadata(digestData: ErrorDigestInfo) {
157
+ return (
158
+ <div className={css.vbox(16)}>
159
+ <div className={css.fontSize(20).fontWeight(600)}>Metadata</div>
160
+ <div className={css.hbox(32).wrap}>
161
+ <div className={css.vbox(4)}>
162
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Compressed Bytes</div>
163
+ <div className={css.fontSize(16).fontWeight(500)}>
164
+ {formatNumber(digestData.totalCompressedBytes)}
165
+ </div>
166
+ </div>
167
+ <div className={css.vbox(4)}>
168
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Uncompressed Bytes</div>
169
+ <div className={css.fontSize(16).fontWeight(500)}>
170
+ {formatNumber(digestData.totalUncompressedBytes)}
171
+ </div>
172
+ </div>
173
+ <div className={css.vbox(4)}>
174
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Total Files</div>
175
+ <div className={css.fontSize(16).fontWeight(500)}>
176
+ {formatNumber(digestData.totalFiles)}
177
+ </div>
178
+ </div>
179
+ <div className={css.vbox(4)}>
180
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Scan Duration</div>
181
+ <div className={css.fontSize(16).fontWeight(500)}>
182
+ {formatNumber(digestData.scanDuration)}ms
183
+ </div>
184
+ </div>
185
+ <div className={css.vbox(4)}>
186
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Start Time</div>
187
+ <div className={css.fontSize(16).fontWeight(500)}>
188
+ {formatVeryNiceDateTime(digestData.startTime)}
189
+ </div>
190
+ </div>
191
+ <div className={css.vbox(4)}>
192
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>End Time</div>
193
+ <div className={css.fontSize(16).fontWeight(500)}>
194
+ {formatVeryNiceDateTime(digestData.endTime)}
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ renderCharts(digestData: ErrorDigestInfo) {
203
+ // Convert histogram data to chart data
204
+ let unsuppressedErrorsData: { x: number; y: number }[] = [];
205
+ let unsuppressedWarningsData: { x: number; y: number }[] = [];
206
+ let corruptData: { x: number; y: number }[] = [];
207
+ let suppressedData: { x: number; y: number }[] = [];
208
+
209
+ for (let [time, data] of digestData.histogram.entries()) {
210
+ unsuppressedErrorsData.push({ x: time, y: data.unsuppressedErrors });
211
+ unsuppressedWarningsData.push({ x: time, y: data.unsuppressedWarnings });
212
+ corruptData.push({ x: time, y: data.corruptErrors + data.corruptWarnings });
213
+ suppressedData.push({ x: time, y: data.suppressedErrors + data.suppressedWarnings });
214
+ }
215
+
216
+ return (
217
+ <div className={css.vbox(16)}>
218
+ <div className={css.fontSize(20).fontWeight(600)}>Error Distribution Over Time</div>
219
+ <div className={css.vbox(16)}>
220
+ <div className={css.hbox(16).wrap}>
221
+ <div className={css.fillWidth.minWidth(400).height(300)}>
222
+ <Histogram
223
+ title="Unsuppressed Errors"
224
+ values={unsuppressedErrorsData}
225
+ xColumn={{
226
+ title: "Time",
227
+ format: (value) => formatDateTime(value)
228
+ }}
229
+ yColumn={{ title: "Count" }}
230
+ />
231
+ </div>
232
+ <div className={css.fillWidth.minWidth(400).height(300)}>
233
+ <Histogram
234
+ title="Unsuppressed Warnings"
235
+ values={unsuppressedWarningsData}
236
+ xColumn={{
237
+ title: "Time",
238
+ format: (value) => formatDateTime(value)
239
+ }}
240
+ yColumn={{ title: "Count" }}
241
+ />
242
+ </div>
243
+ </div>
244
+ <div className={css.hbox(16).wrap}>
245
+ <div className={css.fillWidth.minWidth(400).height(300)}>
246
+ <Histogram
247
+ title="Corrupt Errors + Warnings"
248
+ values={corruptData}
249
+ xColumn={{
250
+ title: "Time",
251
+ format: (value) => formatDateTime(value)
252
+ }}
253
+ yColumn={{ title: "Count" }}
254
+ />
255
+ </div>
256
+ <div className={css.fillWidth.minWidth(400).height(300)}>
257
+ <Histogram
258
+ title="Suppressed Errors + Warnings"
259
+ values={suppressedData}
260
+ xColumn={{
261
+ title: "Time",
262
+ format: (value) => formatDateTime(value)
263
+ }}
264
+ yColumn={{ title: "Count" }}
265
+ />
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ renderFilesTable(digestData: ErrorDigestInfo) {
274
+ // Convert byFile map to table rows
275
+ let fileRows = Array.from(digestData.byFile.entries()).map(([fileName, fileData]) => {
276
+ let latestError = fileData.latestErrors[fileData.latestErrors.length - 1];
277
+ let latestWarning = fileData.latestWarnings[fileData.latestWarnings.length - 1];
278
+ let latestLog = latestError ?? latestWarning;
279
+
280
+ return {
281
+ fileName,
282
+ errorCount: fileData.errors,
283
+ warningCount: fileData.warnings,
284
+ latestMessage: latestLog?.param0 ?? "No messages",
285
+ latestTime: latestLog?.time ?? 0
286
+ };
287
+ });
288
+
289
+ // Sort by error count descending
290
+ fileRows = sort(fileRows, row => row.errorCount).reverse();
291
+
292
+ return (
293
+ <div className={css.vbox(16)}>
294
+ <div className={css.fontSize(20).fontWeight(600)}>Files ({fileRows.length} files)</div>
295
+ <Table
296
+ columns={{
297
+ fileName: { title: "File Name" },
298
+ errorCount: { title: "Errors" },
299
+ warningCount: { title: "Warnings" },
300
+ latestMessage: {
301
+ title: "Latest Message",
302
+ formatter: (value: any) => String(value).slice(0, 100) + (String(value).length > 100 ? "..." : "")
303
+ }
304
+ }}
305
+ rows={fileRows}
306
+ getRowAttributes={(row: any) => ({
307
+ className: css.cursor("pointer").hslhover(210, 20, 95),
308
+ onClick: () => {
309
+ this.state.selectedFile = row.fileName;
310
+ }
311
+ })}
312
+ />
313
+ </div>
314
+ );
315
+ }
316
+
317
+ renderFileDetail(digestData: ErrorDigestInfo, digestKey: string) {
318
+ let selectedFileName = this.state.selectedFile;
319
+ let fileData = digestData.byFile.get(selectedFileName);
320
+
321
+ if (!fileData) {
322
+ return (
323
+ <div className={css.vbox(16).pad2(24)}>
324
+ <div>File not found</div>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ return (
330
+ <div className={css.vbox(24).pad2(24).fillHeight}>
331
+ {/* Header with back button */}
332
+ <div className={css.hbox(16).alignItems("center")}>
333
+ <ATag
334
+ clickOverride={() => {
335
+ this.state.selectedFile = "";
336
+ }}
337
+ className={css.cursor("pointer")}
338
+ >
339
+ ← Back to Files Table
340
+ </ATag>
341
+ <div className={css.fontSize(20).fontWeight(600)}>
342
+ {selectedFileName}
343
+ </div>
344
+ </div>
345
+
346
+ {/* File stats */}
347
+ <div className={css.hbox(32)}>
348
+ <div className={css.vbox(4)}>
349
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Errors</div>
350
+ <div className={css.fontSize(16).fontWeight(500)}>{fileData.errors}</div>
351
+ </div>
352
+ <div className={css.vbox(4)}>
353
+ <div className={css.fontSize(14).colorhsl(0, 0, 60)}>Warnings</div>
354
+ <div className={css.fontSize(16).fontWeight(500)}>{fileData.warnings}</div>
355
+ </div>
356
+ </div>
357
+
358
+ {/* Tabbed interface for errors/warnings */}
359
+ <div className={css.fillHeight.overflowHidden}>
360
+ <TabbedUI
361
+ tab={fileDetailTabURL}
362
+ tabs={[
363
+ {
364
+ value: "errors",
365
+ title: `Errors (${fileData.errors})`,
366
+ contents: this.renderLogEntries(fileData.latestErrors, "errors")
367
+ },
368
+ {
369
+ value: "warnings",
370
+ title: `Warnings (${fileData.warnings})`,
371
+ contents: this.renderLogEntries(fileData.latestWarnings, "warnings")
372
+ }
373
+ ]}
374
+ />
375
+ </div>
376
+ </div>
377
+ );
378
+ }
379
+
380
+ renderLogEntries(logs: LogDatum[], type: "errors" | "warnings") {
381
+ if (logs.length === 0) {
382
+ return <div className={css.colorhsl(0, 0, 60)}>No {type} found</div>;
383
+ }
384
+
385
+ let tableRows = logs.map((log, index) => ({
386
+ index: logs.length - index,
387
+ time: formatVeryNiceDateTime(log.time),
388
+ message: log.param0 ?? "No message",
389
+ threadId: log.__threadId ?? "Unknown",
390
+ name: log.__NAME__ ?? "Unknown"
391
+ }));
392
+
393
+ return (
394
+ <div className={css.fillHeight.overflowAuto}>
395
+ <Table
396
+ columns={{
397
+ index: { title: "#" },
398
+ time: { title: "Time" },
399
+ name: { title: "Source" },
400
+ threadId: { title: "Thread" },
401
+ message: { title: "Message" }
402
+ }}
403
+ rows={tableRows}
404
+ initialLimit={50}
405
+ />
406
+ </div>
407
+ );
408
+ }
409
+ }
@@ -62,7 +62,7 @@ type SuppressedChecker = {
62
62
  fnc: (buffer: Buffer, posStart: number, posEnd: number) => boolean;
63
63
  }
64
64
 
65
- function getAppendables() {
65
+ export function getErrorAppendables() {
66
66
  let loggers = getLoggers();
67
67
  if (!loggers) throw new Error("Loggers not available?");
68
68
  // error, warn
@@ -222,6 +222,14 @@ const suppressionListArchive = archiveJSONT<SuppressionListBase>(() =>
222
222
  );
223
223
  const suppressionUpdatedChannel = new SocketChannel<boolean>("suppression-updated");
224
224
 
225
+ export async function getSuppressionListRaw(): Promise<SuppressionListBase> {
226
+ let entries = await suppressionListArchive.get(suppressionListKey);
227
+ if (!entries) {
228
+ entries = { entries: {} };
229
+ }
230
+ return entries;
231
+ }
232
+
225
233
  class SuppressionList {
226
234
  private init = lazy(async () => {
227
235
  suppressionUpdatedChannel.watch(async () => {
@@ -233,8 +241,8 @@ class SuppressionList {
233
241
  });
234
242
  });
235
243
  private cacheEntries: SuppressionListBase | undefined = undefined;
236
- private updateEntriesNow = async () => {
237
- let entries = await suppressionListArchive.get(suppressionListKey);
244
+ public updateEntriesNow = async () => {
245
+ let entries = await getSuppressionListRaw();
238
246
  if (!entries) {
239
247
  entries = { entries: {} };
240
248
  }
@@ -616,7 +624,7 @@ export class RecentErrors {
616
624
  if (!this.lastSuppressionList || !config.noLocalFiles) {
617
625
  this.lastSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
618
626
  }
619
- for (let appendable of getAppendables()) {
627
+ for (let appendable of getErrorAppendables()) {
620
628
  let startTime = Date.now() - VIEW_WINDOW;
621
629
  let endTime = Date.now() + timeInHour * 2;
622
630
  let result = await new FastArchiveAppendableControllerBase().startSynchronizeInternal({
@@ -18,7 +18,12 @@ import { endTimeParam, startTimeParam } from "../TimeRangeSelector";
18
18
  import { formatDateJSX } from "../../../misc/formatJSX";
19
19
  import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
20
20
 
21
- export function getLogsLinkParts(): URLOverride[] {
21
+ export function getErrorLogsLink(config?: {
22
+ startTime?: number;
23
+ endTime?: number;
24
+ }): URLOverride[] {
25
+ let startTime = config?.startTime ?? Date.now() - timeInDay * 1;
26
+ let endTime = config?.endTime ?? Date.now() + timeInHour * 2;
22
27
  return [
23
28
  showingManagementURL.getOverride(true),
24
29
  managementPageURL.getOverride("LogViewer2"),
@@ -26,8 +31,8 @@ export function getLogsLinkParts(): URLOverride[] {
26
31
  filterParam.getOverride(""),
27
32
 
28
33
  // NOTE: While loading a weeks worth of logs clientside is a bit slow. Scanning serverside is not nearly as bad, as it can be done over hours, but... we want the page to be snappy, loading in seconds, so... just use a day, and we might reduce it even further if needed...
29
- startTimeParam.getOverride(Date.now() - timeInDay * 1),
30
- endTimeParam.getOverride(Date.now() + timeInHour * 2),
34
+ startTimeParam.getOverride(startTime),
35
+ endTimeParam.getOverride(endTime),
31
36
  ];
32
37
  }
33
38
 
@@ -99,7 +104,7 @@ export class ErrorWarning extends qreact.Component {
99
104
  );
100
105
  }
101
106
 
102
- const logLink = getLogsLinkParts();
107
+ const logLink = getErrorLogsLink();
103
108
 
104
109
  if (!recentErrors || recentErrors.length === 0) {
105
110
  return (
@@ -0,0 +1,174 @@
1
+ import { formatDate, formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
2
+ import { getNotifyEmails } from "../../../config";
3
+ import { sendEmail_postmark } from "../../../email_ims_notifications/postmark";
4
+ import { sendEmail_sendgrid } from "../../../email_ims_notifications/sendgrid";
5
+ import { sendEmail } from "../../../user-implementation/userData";
6
+ import { ErrorDigestInfo } from "./errorDigests";
7
+ import { qreact } from "../../../4-dom/qreact";
8
+ import { LogDatum } from "../diskLogger";
9
+ import { sort } from "socket-function/src/misc";
10
+ import { createLink } from "../../../library-components/ATag";
11
+ import { getErrorLogsLink } from "./ErrorWarning";
12
+ import { managementPageURL, showingManagementURL } from "../../managementPages";
13
+ import { digestKeyURL } from "./ErrorDigestPage";
14
+ import { tabURL } from "../../../library-components/urlResetGroups";
15
+
16
+ const MAX_COUNT_PER_FILE = 2;
17
+ const MAX_COUNT = 100;
18
+
19
+ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
20
+ let notifyEmails = getNotifyEmails();
21
+ if (notifyEmails.length === 0) {
22
+ throw new Error(`No notify emails set, so we can't send email. Set in querysub.json config file (beside your package.json) with the { "notifyemails": ["email1@example.com", "email2@example.com"] }, or as a command line argument with --notifyemails "email1@example.com"`);
23
+ }
24
+
25
+ let errors: {
26
+ errorsInFile: number;
27
+ warningsInFile: number;
28
+ message: string;
29
+ messageTime: string;
30
+ }[] = [];
31
+ let errorCount = 0;
32
+ let warningCount = 0;
33
+ let suppressedErrors = 0;
34
+ let suppressedWarnings = 0;
35
+ let corruptErrors = 0;
36
+ let corruptWarnings = 0;
37
+
38
+ let corruptWarning = "";
39
+
40
+ let failingFiles = 0;
41
+
42
+ for (let value of digestInfo.histogram.values()) {
43
+ errorCount += value.unsuppressedErrors;
44
+ warningCount += value.unsuppressedWarnings;
45
+ suppressedErrors += value.suppressedErrors;
46
+ suppressedWarnings += value.suppressedWarnings;
47
+ corruptErrors += value.corruptErrors;
48
+ corruptWarnings += value.corruptWarnings;
49
+ if (value.firstCorruptError && !corruptWarning) {
50
+ corruptWarning = value.firstCorruptError;
51
+ }
52
+ if (value.firstCorruptWarning && !corruptWarning) {
53
+ corruptWarning = value.firstCorruptWarning;
54
+ }
55
+ if (value.unsuppressedErrors > 0) {
56
+ failingFiles++;
57
+ }
58
+ }
59
+
60
+ for (let value of digestInfo.byFile.values()) {
61
+ for (let error of value.latestErrors.slice(-MAX_COUNT_PER_FILE)) {
62
+ errors.push({
63
+ errorsInFile: value.errors,
64
+ warningsInFile: value.warnings,
65
+ message: `${error.param0} (${error.__NAME__})`,
66
+ messageTime: formatDateTime(error.time),
67
+ });
68
+ }
69
+ }
70
+
71
+ sort(errors, x => -x.errorsInFile);
72
+ errors = errors.slice(0, MAX_COUNT);
73
+
74
+ let link = createLink([
75
+ showingManagementURL.getOverride(true),
76
+ managementPageURL.getOverride("ErrorDigestPage"),
77
+ digestKeyURL.getOverride(digestInfo.key),
78
+ tabURL.getOverride("detail"),
79
+ ]);
80
+
81
+ await sendEmail({
82
+ to: notifyEmails,
83
+ fromPrefix: "error-digest",
84
+ subject: `${errorCount} errors | ${formatNumber(failingFiles)} failing files | ${warningCount} warnings${corruptErrors + corruptWarnings > 0 ? ` | ${corruptErrors + corruptWarnings} corrupt` : ""} | ${formatTime(digestInfo.scanDuration)} | ${formatNumber(digestInfo.totalCompressedBytes)} / ${formatNumber(digestInfo.totalUncompressedBytes)} | ${formatNumber(digestInfo.totalFiles)} files`,
85
+ contents: <div>
86
+ <h2>Error Summary</h2>
87
+ <ul style="list-style-type: none; padding-left: 0;">
88
+ <li style="margin-bottom: 8px;">
89
+ <strong style="color: #dc3545;">Errors:</strong>
90
+ <span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{errorCount}</span>
91
+ <span style="color: #dc3545;"> unsuppressed</span>
92
+ {suppressedErrors > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedErrors)} suppressed</span></span>}
93
+ </li>
94
+ <li>
95
+ <strong style="color: #fd7e14;">Warnings:</strong>
96
+ <span style="background-color: #fd7e14; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{warningCount}</span>
97
+ <span style="color: #fd7e14;"> unsuppressed</span>
98
+ {suppressedWarnings > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedWarnings)} suppressed</span></span>}
99
+ </li>
100
+ </ul>
101
+
102
+ {corruptWarning && <div>
103
+ <h2 style="color: #dc3545;">
104
+ Corrupt Lines
105
+ <span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{formatNumber(corruptErrors + corruptWarnings)}</span>
106
+ , example:
107
+ </h2>
108
+ <p style="color: #dc3545; font-weight: bold; background-color: #f8d7da; padding: 10px; border-left: 4px solid #dc3545;">{corruptWarning}</p>
109
+ </div>}
110
+
111
+ <div style="background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
112
+ <table style="width: 100%; border-collapse: collapse;">
113
+ <tr>
114
+ <td style="text-align: center; padding: 10px;">
115
+ <div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Total Files</div>
116
+ <div style="background-color: #17a2b8; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalFiles)}</div>
117
+ </td>
118
+ <td style="text-align: center; padding: 10px;">
119
+ <div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Compressed Bytes</div>
120
+ <div style="background-color: #ffc107; color: #212529; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalCompressedBytes)}</div>
121
+ </td>
122
+ <td style="text-align: center; padding: 10px;">
123
+ <div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Uncompressed Bytes</div>
124
+ <div style="background-color: #6f42c1; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalUncompressedBytes)}</div>
125
+ </td>
126
+ </tr>
127
+ </table>
128
+ <div style="border-top: 1px solid #dee2e6; padding-top: 15px; margin-top: 15px;">
129
+ <table style="width: 100%; border-collapse: collapse;">
130
+ <tr>
131
+ <td style="text-align: center; padding: 10px; width: 200px;">
132
+ <div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Scan Duration</div>
133
+ <div style="background-color: #007bff; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px; font-family: monospace;">{formatTime(digestInfo.scanDuration)}</div>
134
+ </td>
135
+ <td style="padding: 10px; vertical-align: top;">
136
+ <div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Time Range</div>
137
+ <div style="font-size: 14px;">
138
+ <div style="margin-bottom: 8px;">From:&nbsp;&nbsp;<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{formatDateTime(digestInfo.startTime)}</span></div>
139
+ <div>To:&nbsp;&nbsp;<span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{formatDateTime(digestInfo.endTime)}</span></div>
140
+ </div>
141
+ </td>
142
+ </tr>
143
+ </table>
144
+ </div>
145
+ </div>
146
+
147
+ <a href={link} style="display: block; margin-bottom: 20px; padding: 10px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; text-align: center;">View live logs</a>
148
+
149
+ {errors.length > 0 && <div>
150
+ <h2 style="color: #495057;">Recent Errors (<span style="color: #dc3545; font-weight: bold;">{errors.length}</span> shown)</h2>
151
+ <table style="border-collapse: collapse; width: 100%; margin-top: 10px;">
152
+ <thead>
153
+ <tr style="background-color: #495057; color: white;">
154
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Time</th>
155
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Message</th>
156
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #dc3545;">Errors in File</th>
157
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #fd7e14;">Warnings in File</th>
158
+ </tr>
159
+ </thead>
160
+ <tbody>
161
+ {errors.map((error, index) => (
162
+ <tr key={index} style={`background-color: ${index % 2 === 0 ? "#f8f9fa" : "white"}`}>
163
+ <td style="border: 1px solid #ddd; padding: 8px;">{error.messageTime}</td>
164
+ <td style="border: 1px solid #ddd; padding: 8px;">{error.message}</td>
165
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #f8d7da; color: #721c24; font-weight: bold;">{error.errorsInFile}</td>
166
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #fff3cd; color: #856404; font-weight: bold;">{error.warningsInFile}</td>
167
+ </tr>
168
+ ))}
169
+ </tbody>
170
+ </table>
171
+ </div>}
172
+ </div>,
173
+ });
174
+ }
@@ -0,0 +1,7 @@
1
+ import { runDigestLoop } from "./errorDigests";
2
+
3
+ async function main() {
4
+ await runDigestLoop();
5
+ }
6
+ // The digest loop should never exit, and if it does, we probably want to terminate ourselves so that the service manager will restart us, hopefully putting us back in a good state.
7
+ main().catch(console.error).finally(() => process.exit());