querysub 0.327.0 → 0.329.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.
- package/bin/error-email.js +8 -0
- package/bin/error-im.js +8 -0
- package/package.json +4 -3
- package/src/-a-archives/archivesBackBlaze.ts +20 -0
- package/src/-a-archives/archivesCborT.ts +52 -0
- package/src/-a-archives/archivesDisk.ts +5 -5
- package/src/-a-archives/archivesJSONT.ts +19 -5
- package/src/-a-archives/archivesLimitedCache.ts +118 -7
- package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
- package/src/-g-core-values/NodeCapabilities.ts +26 -11
- package/src/0-path-value-core/auditLogs.ts +4 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-querysub/Querysub.ts +1 -1
- package/src/5-diagnostics/GenericFormat.tsx +2 -2
- package/src/config.ts +15 -3
- package/src/deployManager/machineApplyMainCode.ts +10 -8
- package/src/deployManager/machineSchema.ts +4 -3
- package/src/deployManager/setupMachineMain.ts +3 -2
- package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
- package/src/diagnostics/logs/FastArchiveController.ts +11 -2
- package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
- package/src/diagnostics/logs/LogViewer2.tsx +78 -34
- package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
- package/src/diagnostics/logs/diskLogger.ts +70 -23
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
- package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
- package/src/diagnostics/managementPages.tsx +33 -15
- package/src/email_ims_notifications/discord.tsx +203 -0
- package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
- package/src/fs.ts +9 -0
- package/src/functional/SocketChannel.ts +9 -0
- package/src/functional/throttleRender.ts +134 -0
- package/src/library-components/ATag.tsx +2 -2
- package/src/library-components/SyncedController.ts +3 -3
- package/src/misc.ts +18 -0
- package/src/misc2.ts +106 -0
- package/src/user-implementation/SecurityPage.tsx +11 -5
- package/src/user-implementation/userData.ts +57 -23
- package/testEntry2.ts +14 -5
- package/src/user-implementation/setEmailKey.ts +0 -25
- /package/src/{email → email_ims_notifications}/sendgrid.tsx +0 -0
|
@@ -2,7 +2,7 @@ import { isNode } from "typesafecss";
|
|
|
2
2
|
import { getArchives } from "../../../-a-archives/archives";
|
|
3
3
|
import { SizeLimiter } from "../../SizeLimiter";
|
|
4
4
|
import { FastArchiveAppendable, createLogScanner, objectDelimitterBuffer } from "../FastArchiveAppendable";
|
|
5
|
-
import { LogDatum, getLoggers } from "../diskLogger";
|
|
5
|
+
import { LogDatum, getLogHash, getLoggers } from "../diskLogger";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
8
8
|
import { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
@@ -21,6 +21,8 @@ import { qreact } from "../../../4-dom/qreact";
|
|
|
21
21
|
import { requiresNetworkTrustHook } from "../../../-d-trust/NetworkTrust2";
|
|
22
22
|
import { assertIsManagementUser } from "../../managementPages";
|
|
23
23
|
import { streamToIteratable } from "../../../misc";
|
|
24
|
+
import { fsExistsAsync } from "../../../fs";
|
|
25
|
+
import { getPathStr2 } from "../../../path";
|
|
24
26
|
|
|
25
27
|
export const MAX_RECENT_ERRORS = 20;
|
|
26
28
|
const MAX_RECENT_ERRORS_PER_FILE = 3;
|
|
@@ -60,7 +62,7 @@ type SuppressedChecker = {
|
|
|
60
62
|
fnc: (buffer: Buffer, posStart: number, posEnd: number) => boolean;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function
|
|
65
|
+
export function getErrorAppendables() {
|
|
64
66
|
let loggers = getLoggers();
|
|
65
67
|
if (!loggers) throw new Error("Loggers not available?");
|
|
66
68
|
// error, warn
|
|
@@ -151,18 +153,17 @@ export const getSuppressionFull = measureWrap(function getSuppressionFull(config
|
|
|
151
153
|
|
|
152
154
|
// Handle definitelyExpired - these are outdated suppressions
|
|
153
155
|
let mostRecentOutdatedSuppressionKey: string | undefined = undefined;
|
|
156
|
+
let mostRecentOutdatedSuppressionTime = 0;
|
|
154
157
|
|
|
155
158
|
// Handle maybeExpired - need to parse timestamp to check if suppression was active
|
|
156
159
|
if (maybeExpired.length > 0 && (suppressionCounts || expiredSuppressionCounts || obj)) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return 0;
|
|
160
|
+
let logTime = 0;
|
|
161
|
+
try {
|
|
162
|
+
let logEntry = JSON.parse(data.slice(posStart, posEnd).toString()) as LogDatum;
|
|
163
|
+
if (typeof logEntry.time === "number") {
|
|
164
|
+
logTime = logEntry.time;
|
|
163
165
|
}
|
|
164
|
-
}
|
|
165
|
-
let logTime = getLogTime();
|
|
166
|
+
} catch { }
|
|
166
167
|
|
|
167
168
|
for (let checker of maybeExpired) {
|
|
168
169
|
if (checker.fnc(data, posStart, posEnd)) {
|
|
@@ -174,8 +175,10 @@ export const getSuppressionFull = measureWrap(function getSuppressionFull(config
|
|
|
174
175
|
suppressionCounts.set(checker.entry.key, count);
|
|
175
176
|
}
|
|
176
177
|
} else {
|
|
177
|
-
|
|
178
|
+
|
|
179
|
+
if (checker.entry.expiresAt > mostRecentOutdatedSuppressionTime) {
|
|
178
180
|
mostRecentOutdatedSuppressionKey = checker.entry.key;
|
|
181
|
+
mostRecentOutdatedSuppressionTime = checker.entry.expiresAt;
|
|
179
182
|
}
|
|
180
183
|
// Even if we don't want the expired suppression counts, we might want the normal suppression counts, so we have to keep going.
|
|
181
184
|
if (expiredSuppressionCounts) {
|
|
@@ -192,7 +195,7 @@ export const getSuppressionFull = measureWrap(function getSuppressionFull(config
|
|
|
192
195
|
for (let checker of definitelyExpired) {
|
|
193
196
|
if (checker.fnc(data, posStart, posEnd)) {
|
|
194
197
|
// First match is the most recent (entries are sorted by lastUpdateTime desc)
|
|
195
|
-
if (
|
|
198
|
+
if (checker.entry.expiresAt > mostRecentOutdatedSuppressionTime) {
|
|
196
199
|
mostRecentOutdatedSuppressionKey = checker.entry.key;
|
|
197
200
|
}
|
|
198
201
|
if (!expiredSuppressionCounts) break;
|
|
@@ -204,7 +207,7 @@ export const getSuppressionFull = measureWrap(function getSuppressionFull(config
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
// Set the most recent outdated suppression key if we found any and weren't suppressed
|
|
207
|
-
if (obj && mostRecentOutdatedSuppressionKey
|
|
210
|
+
if (obj && mostRecentOutdatedSuppressionKey) {
|
|
208
211
|
obj.outdatedSuppressionKey = mostRecentOutdatedSuppressionKey;
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -219,18 +222,27 @@ const suppressionListArchive = archiveJSONT<SuppressionListBase>(() =>
|
|
|
219
222
|
);
|
|
220
223
|
const suppressionUpdatedChannel = new SocketChannel<boolean>("suppression-updated");
|
|
221
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
|
+
|
|
222
233
|
class SuppressionList {
|
|
223
234
|
private init = lazy(async () => {
|
|
224
|
-
suppressionUpdatedChannel.watch(() => {
|
|
225
|
-
|
|
235
|
+
suppressionUpdatedChannel.watch(async () => {
|
|
236
|
+
await this.updateEntriesNow();
|
|
237
|
+
await recentErrors.onSuppressionChanged();
|
|
226
238
|
});
|
|
227
239
|
await runInfinitePollCallAtStart(SUPPRESSION_POLL_INTERVAL, async () => {
|
|
228
240
|
await this.updateEntriesNow();
|
|
229
241
|
});
|
|
230
242
|
});
|
|
231
243
|
private cacheEntries: SuppressionListBase | undefined = undefined;
|
|
232
|
-
|
|
233
|
-
let entries = await
|
|
244
|
+
public updateEntriesNow = async () => {
|
|
245
|
+
let entries = await getSuppressionListRaw();
|
|
234
246
|
if (!entries) {
|
|
235
247
|
entries = { entries: {} };
|
|
236
248
|
}
|
|
@@ -336,14 +348,14 @@ class SuppressionList {
|
|
|
336
348
|
let entries = await this.getEntries();
|
|
337
349
|
entry.lastUpdateTime = Date.now();
|
|
338
350
|
entries.entries[entry.key] = entry;
|
|
339
|
-
|
|
351
|
+
await suppressionListArchive.set(suppressionListKey, entries);
|
|
340
352
|
suppressionUpdatedChannel.broadcast(true);
|
|
341
353
|
await recentErrors.onSuppressionChanged();
|
|
342
354
|
}
|
|
343
355
|
public async removeSuppressionEntry(key: string) {
|
|
344
356
|
let entries = await this.getEntries();
|
|
345
357
|
delete entries.entries[key];
|
|
346
|
-
|
|
358
|
+
await suppressionListArchive.set(suppressionListKey, entries);
|
|
347
359
|
suppressionUpdatedChannel.broadcast(true);
|
|
348
360
|
await recentErrors.onSuppressionChanged();
|
|
349
361
|
}
|
|
@@ -353,7 +365,7 @@ class SuppressionList {
|
|
|
353
365
|
return entries;
|
|
354
366
|
}
|
|
355
367
|
}
|
|
356
|
-
const suppressionList = new SuppressionList();
|
|
368
|
+
export const suppressionList = new SuppressionList();
|
|
357
369
|
export const SuppressionListController = getSyncedController(SocketFunction.register(
|
|
358
370
|
"SuppressionListController-08f985d8-8d06-4041-ac4b-44566c54615d",
|
|
359
371
|
suppressionList,
|
|
@@ -397,7 +409,7 @@ class URLCache {
|
|
|
397
409
|
if (!isNode()) return undefined;
|
|
398
410
|
|
|
399
411
|
// Create cache directory if it doesn't exist
|
|
400
|
-
if (!
|
|
412
|
+
if (!await fsExistsAsync(this.root)) {
|
|
401
413
|
await fs.promises.mkdir(this.root, { recursive: true });
|
|
402
414
|
}
|
|
403
415
|
|
|
@@ -498,13 +510,18 @@ const urlCache = new URLCache();
|
|
|
498
510
|
const limitRecentErrors = measureWrap(function limitRecentErrors(objs: LogDatum[]) {
|
|
499
511
|
sort(objs, x => x.time);
|
|
500
512
|
let recent: LogDatum[] = [];
|
|
513
|
+
let foundHashes = new Set<string>();
|
|
501
514
|
let countByFile = new Map<string, number>();
|
|
502
515
|
// NOTE: We iterate backwards, because... usually new logs come in at the end, and are pushed, so we want to sort by time (that way we often don't have to resort by much). And if we sort by time, the newest at at the end!
|
|
503
516
|
for (let i = objs.length - 1; i >= 0; i--) {
|
|
504
517
|
let obj = objs[i];
|
|
505
518
|
let file = String(obj.__FILE__) || "";
|
|
506
519
|
let count = countByFile.get(file) || 0;
|
|
520
|
+
if (count > MAX_RECENT_ERRORS_PER_FILE) continue;
|
|
507
521
|
count++;
|
|
522
|
+
let hash = getLogHash(obj);
|
|
523
|
+
if (foundHashes.has(hash)) continue;
|
|
524
|
+
foundHashes.add(hash);
|
|
508
525
|
if (count > MAX_RECENT_ERRORS_PER_FILE) continue;
|
|
509
526
|
countByFile.set(file, count);
|
|
510
527
|
recent.push(obj);
|
|
@@ -513,8 +530,13 @@ const limitRecentErrors = measureWrap(function limitRecentErrors(objs: LogDatum[
|
|
|
513
530
|
return recent;
|
|
514
531
|
});
|
|
515
532
|
|
|
516
|
-
class RecentErrors {
|
|
533
|
+
export class RecentErrors {
|
|
534
|
+
|
|
535
|
+
constructor(private addErrorsCallback?: (objs: LogDatum[]) => void | Promise<void>) {
|
|
536
|
+
this.addErrorsCallback = addErrorsCallback;
|
|
537
|
+
}
|
|
517
538
|
|
|
539
|
+
// TODO: Uninitialize (stopping the infinite polling), if all of our recent errors watchers go away.
|
|
518
540
|
private initialize = lazy(async () => {
|
|
519
541
|
errorWatcherBase.watch(x => {
|
|
520
542
|
void this.addErrors(x);
|
|
@@ -526,7 +548,7 @@ class RecentErrors {
|
|
|
526
548
|
});
|
|
527
549
|
|
|
528
550
|
private _recentErrors: LogDatum[] = [];
|
|
529
|
-
private updateRecentErrors = async (objs: LogDatum[]) => {
|
|
551
|
+
private updateRecentErrors = runInSerial(async (objs: LogDatum[]) => {
|
|
530
552
|
objs = await suppressionList.filterObjsToNonSuppressed(objs);
|
|
531
553
|
let newRecentErrors = limitRecentErrors(objs);
|
|
532
554
|
// If any changed
|
|
@@ -549,28 +571,60 @@ class RecentErrors {
|
|
|
549
571
|
this._recentErrors = newRecentErrors;
|
|
550
572
|
void this.broadcastUpdate(undefined);
|
|
551
573
|
}
|
|
552
|
-
};
|
|
574
|
+
});
|
|
553
575
|
private broadcastUpdate = batchFunction({ delay: NOTIFICATION_BROADCAST_BATCH }, () => {
|
|
554
576
|
recentErrorsChannel.broadcast(true);
|
|
555
577
|
});
|
|
556
578
|
|
|
557
|
-
private async
|
|
579
|
+
private addErrors = runInSerial(async (objs: LogDatum[]) => {
|
|
558
580
|
if (objs.length === 0) return;
|
|
581
|
+
|
|
582
|
+
if (this.addErrorsCallback) {
|
|
583
|
+
await this.addErrorsCallback(objs);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
559
586
|
for (let obj of objs) {
|
|
560
587
|
this._recentErrors.push(obj);
|
|
561
588
|
}
|
|
562
589
|
await this.updateRecentErrors(this._recentErrors);
|
|
563
|
-
}
|
|
590
|
+
});
|
|
564
591
|
|
|
565
|
-
|
|
592
|
+
private lastSuppressionList = new Map<string, SuppressionEntry>();
|
|
593
|
+
public onSuppressionChanged = runInSerial(async () => {
|
|
594
|
+
let newSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
|
|
595
|
+
let prev = this.lastSuppressionList;
|
|
596
|
+
function anyReduced() {
|
|
597
|
+
for (let newEntry of newSuppressionList.values()) {
|
|
598
|
+
let oldEntry = prev.get(newEntry.key);
|
|
599
|
+
if (oldEntry && newEntry.expiresAt < oldEntry.expiresAt) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (let oldEntry of prev.values()) {
|
|
604
|
+
if (!newSuppressionList.has(oldEntry.key)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
if (anyReduced()) {
|
|
611
|
+
console.info("Suppression has been reduced (entries removed or expiry times decreased), performing full rescan to find any revealed values.");
|
|
612
|
+
this.scannedHashes.clear();
|
|
613
|
+
void this.scanNow({});
|
|
614
|
+
}
|
|
615
|
+
this.lastSuppressionList = newSuppressionList;
|
|
566
616
|
await this.updateRecentErrors(this._recentErrors);
|
|
567
|
-
}
|
|
617
|
+
});
|
|
568
618
|
|
|
569
619
|
private scannedHashes = new Set<string>();
|
|
570
620
|
private scanNow = runInSerial(async (config: {
|
|
571
621
|
noLocalFiles?: boolean;
|
|
572
622
|
}) => {
|
|
573
|
-
|
|
623
|
+
// If we're scanning everything, we should update the suppression list, because it might have been changed remotely, and we might be scanning everything because the user clicked refresh.
|
|
624
|
+
if (!this.lastSuppressionList || !config.noLocalFiles) {
|
|
625
|
+
this.lastSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
|
|
626
|
+
}
|
|
627
|
+
for (let appendable of getErrorAppendables()) {
|
|
574
628
|
let startTime = Date.now() - VIEW_WINDOW;
|
|
575
629
|
let endTime = Date.now() + timeInHour * 2;
|
|
576
630
|
let result = await new FastArchiveAppendableControllerBase().startSynchronizeInternal({
|
|
@@ -619,43 +673,11 @@ class RecentErrors {
|
|
|
619
673
|
await fs.promises.unlink(path);
|
|
620
674
|
continue;
|
|
621
675
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
gunzip.on("data", (chunk: Buffer) => {
|
|
629
|
-
void scanner.onData(chunk);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
gunzip.on("end", async () => {
|
|
633
|
-
try {
|
|
634
|
-
resolve();
|
|
635
|
-
} catch (error) {
|
|
636
|
-
reject(error);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
gunzip.on("error", reject);
|
|
641
|
-
|
|
642
|
-
try {
|
|
643
|
-
for (let i = 0; i < sizeT; i += READ_CHUNK_SIZE) {
|
|
644
|
-
let chunkSize = Math.min(READ_CHUNK_SIZE, sizeT - i);
|
|
645
|
-
let buffer = Buffer.alloc(chunkSize);
|
|
646
|
-
await fd.read(buffer, 0, chunkSize, i);
|
|
647
|
-
let result = gunzip.write(buffer);
|
|
648
|
-
if (!result) {
|
|
649
|
-
await new Promise(resolve => gunzip.once("drain", resolve));
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
gunzip.end();
|
|
653
|
-
} catch (error) {
|
|
654
|
-
reject(error);
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
} finally {
|
|
658
|
-
await fd.close();
|
|
676
|
+
const fileStream = fs.createReadStream(path);
|
|
677
|
+
const gunzip = zlib.createGunzip();
|
|
678
|
+
const decompressedStream = fileStream.pipe(gunzip);
|
|
679
|
+
for await (const chunk of decompressedStream) {
|
|
680
|
+
scanner.onData(chunk);
|
|
659
681
|
}
|
|
660
682
|
let newErrors = await scanner.finish();
|
|
661
683
|
await this.addErrors(newErrors);
|
|
@@ -678,6 +700,10 @@ class RecentErrors {
|
|
|
678
700
|
await this.scanNow({});
|
|
679
701
|
return this._recentErrors;
|
|
680
702
|
}
|
|
703
|
+
|
|
704
|
+
public async raiseTestError(...params: unknown[]) {
|
|
705
|
+
console.error(...params);
|
|
706
|
+
}
|
|
681
707
|
}
|
|
682
708
|
const recentErrors = new RecentErrors();
|
|
683
709
|
export const RecentErrorsController = getSyncedController(SocketFunction.register(
|
|
@@ -686,6 +712,7 @@ export const RecentErrorsController = getSyncedController(SocketFunction.registe
|
|
|
686
712
|
() => ({
|
|
687
713
|
getRecentErrors: {},
|
|
688
714
|
rescanAllErrorsNow: {},
|
|
715
|
+
raiseTestError: {},
|
|
689
716
|
}),
|
|
690
717
|
() => ({
|
|
691
718
|
hooks: [assertIsManagementUser],
|
|
@@ -722,4 +749,4 @@ export const notifyWatchersOfError = batchFunction({
|
|
|
722
749
|
}
|
|
723
750
|
);
|
|
724
751
|
|
|
725
|
-
const errorWatcherBase = new SocketChannel<LogDatum[]>("error-watcher-38de08cd-3247-4f75-9ac0-7919b240607d");
|
|
752
|
+
export const errorWatcherBase = new SocketChannel<LogDatum[]>("error-watcher-38de08cd-3247-4f75-9ac0-7919b240607d");
|
|
@@ -10,6 +10,8 @@ import { nextId, sort, timeInDay } from "socket-function/src/misc";
|
|
|
10
10
|
import { formatNumber, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
11
11
|
import { formatDateJSX } from "../../../misc/formatJSX";
|
|
12
12
|
import { LogDatum } from "../diskLogger";
|
|
13
|
+
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
14
|
+
import { throttleRender } from "../../../functional/throttleRender";
|
|
13
15
|
|
|
14
16
|
export class ErrorSuppressionUI extends qreact.Component<{
|
|
15
17
|
dataSeqNum: number;
|
|
@@ -20,8 +22,10 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
20
22
|
}> {
|
|
21
23
|
state = t.state({
|
|
22
24
|
matchedInput: t.string(""),
|
|
25
|
+
renderLimit: t.number(10)
|
|
23
26
|
});
|
|
24
27
|
|
|
28
|
+
@measureFnc
|
|
25
29
|
private calculatePreviewMatchCount(pattern: string): number {
|
|
26
30
|
if (!pattern.trim()) return 0;
|
|
27
31
|
|
|
@@ -50,6 +54,8 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
public render() {
|
|
57
|
+
if (throttleRender({ key: "ErrorSuppressionUI", frameDelay: 30 })) return undefined;
|
|
58
|
+
|
|
53
59
|
this.props.dataSeqNum;
|
|
54
60
|
const controller = SuppressionListController(SocketFunction.browserNodeId());
|
|
55
61
|
const entries = (controller.getSuppressionList() || []);
|
|
@@ -117,6 +123,27 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
117
123
|
>
|
|
118
124
|
Fixed
|
|
119
125
|
</Button>
|
|
126
|
+
<Button
|
|
127
|
+
onClick={() => {
|
|
128
|
+
let value = this.state.matchedInput;
|
|
129
|
+
this.state.matchedInput = "";
|
|
130
|
+
void Querysub.onCommitFinished(async () => {
|
|
131
|
+
await controller.setSuppressionEntry.promise({
|
|
132
|
+
key: nextId(),
|
|
133
|
+
match: value,
|
|
134
|
+
comment: "",
|
|
135
|
+
lastUpdateTime: Date.now(),
|
|
136
|
+
expiresAt: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
Querysub.commit(() => {
|
|
139
|
+
this.props.rerunFilters();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}}
|
|
143
|
+
title="Fixed immediately, any future errors even that happen right now will trigger again. "
|
|
144
|
+
>
|
|
145
|
+
Fixed Now
|
|
146
|
+
</Button>
|
|
120
147
|
<Button onClick={() => {
|
|
121
148
|
let value = this.state.matchedInput;
|
|
122
149
|
this.state.matchedInput = "";
|
|
@@ -137,8 +164,12 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
137
164
|
</Button>
|
|
138
165
|
</div>
|
|
139
166
|
|
|
140
|
-
<div className={css.
|
|
141
|
-
|
|
167
|
+
<div className={css.pad2(12).bord2(200, 40, 85).hsl(200, 40, 95).fillWidth}>
|
|
168
|
+
<strong>Note:</strong> Suppression time updates don't automatically rerun the search. Click Run to rerun the search.
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className={css.vbox(8).fillWidth.overflowAuto.maxHeight("20vh")}>
|
|
172
|
+
{entries.slice(0, this.state.renderLimit).map((entry) => {
|
|
142
173
|
const updateEntry = (changes: Partial<SuppressionEntry>) => {
|
|
143
174
|
let newEntry = { ...entry, ...changes };
|
|
144
175
|
void Querysub.onCommitFinished(async () => {
|
|
@@ -152,7 +183,7 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
152
183
|
className={
|
|
153
184
|
css.hbox(8).pad2(12).bord2(0, 0, 10).fillWidth
|
|
154
185
|
//+ (entry.expiresAt < Date.now() && expiredCount > 0 && css.opacity(0.5))
|
|
155
|
-
+ ((
|
|
186
|
+
+ ((expiredCount === 0) && css.opacity(0.6))
|
|
156
187
|
+ (
|
|
157
188
|
count > 0 && entry.expiresAt !== NOT_AN_ERROR_EXPIRE_TIME && css.hsla(0, 50, 50, 0.5)
|
|
158
189
|
|| css.hsla(0, 0, 0, 0.1)
|
|
@@ -226,6 +257,9 @@ export class ErrorSuppressionUI extends qreact.Component<{
|
|
|
226
257
|
</Button>
|
|
227
258
|
</div>;
|
|
228
259
|
})}
|
|
260
|
+
{entries.length > this.state.renderLimit && <Button onClick={() => this.state.renderLimit *= 2}>
|
|
261
|
+
Load More
|
|
262
|
+
</Button>}
|
|
229
263
|
</div>
|
|
230
264
|
</div>;
|
|
231
265
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../../4-dom/qreact";
|
|
3
3
|
import { css } from "../../../4-dom/css";
|
|
4
|
-
import { isCurrentUserSuperUser } from "../../../user-implementation/userData";
|
|
4
|
+
import { isCurrentUserSuperUser, user_data } from "../../../user-implementation/userData";
|
|
5
5
|
import { RecentErrorsController, SuppressionListController, watchRecentErrors, MAX_RECENT_ERRORS, NOT_AN_ERROR_EXPIRE_TIME, SuppressionEntry } from "./ErrorNotificationController";
|
|
6
6
|
import { t } from "../../../2-proxy/schema2";
|
|
7
7
|
import { InputLabel } from "../../../library-components/InputLabel";
|
|
8
8
|
import { Button } from "../../../library-components/Button";
|
|
9
|
-
import { ATag } from "../../../library-components/ATag";
|
|
9
|
+
import { ATag, Anchor, URLOverride } from "../../../library-components/ATag";
|
|
10
10
|
import { managementPageURL, showingManagementURL } from "../../managementPages";
|
|
11
11
|
import { errorNotifyToggleURL } from "../LogViewer2";
|
|
12
12
|
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
@@ -16,6 +16,25 @@ import { Icon } from "../../../library-components/icons";
|
|
|
16
16
|
import { filterParam } from "../FastArchiveViewer";
|
|
17
17
|
import { endTimeParam, startTimeParam } from "../TimeRangeSelector";
|
|
18
18
|
import { formatDateJSX } from "../../../misc/formatJSX";
|
|
19
|
+
import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
|
|
20
|
+
|
|
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;
|
|
27
|
+
return [
|
|
28
|
+
showingManagementURL.getOverride(true),
|
|
29
|
+
managementPageURL.getOverride("LogViewer2"),
|
|
30
|
+
errorNotifyToggleURL.getOverride(true),
|
|
31
|
+
filterParam.getOverride(""),
|
|
32
|
+
|
|
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...
|
|
34
|
+
startTimeParam.getOverride(startTime),
|
|
35
|
+
endTimeParam.getOverride(endTime),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
19
38
|
|
|
20
39
|
export class ErrorWarning extends qreact.Component {
|
|
21
40
|
state = t.state({
|
|
@@ -67,22 +86,36 @@ export class ErrorWarning extends qreact.Component {
|
|
|
67
86
|
</style>
|
|
68
87
|
</Button>;
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
let discordURLWarning: qreact.ComponentChildren = undefined;
|
|
90
|
+
if (!atomic(user_data().secure.notifyDiscordWebhookURL)) {
|
|
91
|
+
discordURLWarning = (
|
|
92
|
+
<Anchor
|
|
93
|
+
target="_blank"
|
|
94
|
+
title="Can't send application notifications to developers due to missing Discord hook URL. Click here and set it."
|
|
95
|
+
values={[
|
|
96
|
+
showingManagementURL.getOverride(true),
|
|
97
|
+
managementPageURL.getOverride("SecurityPage"),
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
<Button hue={0}>
|
|
101
|
+
⚠️ Missing Discord Hook URL <span className={css.filter("invert(1)")}>📞</span>
|
|
102
|
+
</Button>
|
|
103
|
+
</Anchor>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const logLink = getErrorLogsLink();
|
|
78
108
|
|
|
79
109
|
if (!recentErrors || recentErrors.length === 0) {
|
|
80
|
-
return
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
return (
|
|
111
|
+
<span className={css.hbox(8)}>
|
|
112
|
+
<ATag target="_blank" values={logLink}>
|
|
113
|
+
No Errors
|
|
114
|
+
</ATag>
|
|
115
|
+
{refreshButton}
|
|
116
|
+
{discordURLWarning}
|
|
117
|
+
</span>
|
|
118
|
+
);
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
// Count unique files
|
|
@@ -128,6 +161,7 @@ export class ErrorWarning extends qreact.Component {
|
|
|
128
161
|
View Logs
|
|
129
162
|
</ATag>
|
|
130
163
|
{refreshButton}
|
|
164
|
+
{discordURLWarning}
|
|
131
165
|
</div>
|
|
132
166
|
|
|
133
167
|
{topExpired &&
|