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.
Files changed (50) hide show
  1. package/bin/error-email.js +8 -0
  2. package/bin/error-im.js +8 -0
  3. package/package.json +4 -3
  4. package/src/-a-archives/archivesBackBlaze.ts +20 -0
  5. package/src/-a-archives/archivesCborT.ts +52 -0
  6. package/src/-a-archives/archivesDisk.ts +5 -5
  7. package/src/-a-archives/archivesJSONT.ts +19 -5
  8. package/src/-a-archives/archivesLimitedCache.ts +118 -7
  9. package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
  10. package/src/-g-core-values/NodeCapabilities.ts +26 -11
  11. package/src/0-path-value-core/auditLogs.ts +4 -2
  12. package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
  13. package/src/3-path-functions/PathFunctionRunner.ts +2 -2
  14. package/src/4-querysub/Querysub.ts +1 -1
  15. package/src/5-diagnostics/GenericFormat.tsx +2 -2
  16. package/src/config.ts +15 -3
  17. package/src/deployManager/machineApplyMainCode.ts +10 -8
  18. package/src/deployManager/machineSchema.ts +4 -3
  19. package/src/deployManager/setupMachineMain.ts +3 -2
  20. package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
  21. package/src/diagnostics/logs/FastArchiveController.ts +11 -2
  22. package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
  23. package/src/diagnostics/logs/LogViewer2.tsx +78 -34
  24. package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
  25. package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
  26. package/src/diagnostics/logs/diskLogger.ts +70 -23
  27. package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
  28. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
  29. package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
  30. package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
  31. package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
  32. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
  33. package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
  34. package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
  35. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
  36. package/src/diagnostics/managementPages.tsx +33 -15
  37. package/src/email_ims_notifications/discord.tsx +203 -0
  38. package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
  39. package/src/fs.ts +9 -0
  40. package/src/functional/SocketChannel.ts +9 -0
  41. package/src/functional/throttleRender.ts +134 -0
  42. package/src/library-components/ATag.tsx +2 -2
  43. package/src/library-components/SyncedController.ts +3 -3
  44. package/src/misc.ts +18 -0
  45. package/src/misc2.ts +106 -0
  46. package/src/user-implementation/SecurityPage.tsx +11 -5
  47. package/src/user-implementation/userData.ts +57 -23
  48. package/testEntry2.ts +14 -5
  49. package/src/user-implementation/setEmailKey.ts +0 -25
  50. /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 getAppendables() {
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
- const getLogTime = () => {
158
- try {
159
- let logEntry = JSON.parse(data.slice(posStart, posEnd).toString()) as LogDatum;
160
- return typeof logEntry.time === "number" ? logEntry.time : 0;
161
- } catch {
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
- if (!mostRecentOutdatedSuppressionKey) {
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 (!mostRecentOutdatedSuppressionKey) {
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 && !suppressed) {
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
- void this.updateEntriesNow();
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
- private updateEntriesNow = async () => {
233
- let entries = await suppressionListArchive.get(suppressionListKey);
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
- void suppressionListArchive.set(suppressionListKey, entries);
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
- void suppressionListArchive.set(suppressionListKey, entries);
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 (!fs.existsSync(this.root)) {
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 addErrors(objs: LogDatum[]) {
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
- public async onSuppressionChanged() {
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
- for (let appendable of getAppendables()) {
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
- let sizeT = size;
623
- let fd = await fs.promises.open(path, "r");
624
- try {
625
- await new Promise<void>(async (resolve, reject) => {
626
- const gunzip = zlib.createGunzip();
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.vbox(8).fillWidth.overflowAuto.maxHeight("30vh")}>
141
- {entries.map((entry) => {
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
- + ((count === 0 && expiredCount === 0) && css.opacity(0.6))
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
- const logLink = [
71
- showingManagementURL.getOverride(true),
72
- managementPageURL.getOverride("LogViewer2"),
73
- errorNotifyToggleURL.getOverride(true),
74
- filterParam.getOverride(""),
75
- startTimeParam.getOverride(Date.now() - timeInDay * 7),
76
- endTimeParam.getOverride(Date.now() + timeInHour * 2),
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 <span className={css.hbox(8)}>
81
- <ATag target="_blank" values={logLink}>
82
- No Errors
83
- </ATag>
84
- {refreshButton}
85
- </span>;
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 &&