querysub 0.356.0 → 0.358.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 (76) hide show
  1. package/.cursorrules +9 -0
  2. package/bin/movelogs.js +4 -0
  3. package/package.json +13 -6
  4. package/scripts/postinstall.js +23 -0
  5. package/src/-a-archives/archiveCache.ts +10 -12
  6. package/src/-a-archives/archives.ts +29 -0
  7. package/src/-a-archives/archivesBackBlaze.ts +60 -12
  8. package/src/-a-archives/archivesDisk.ts +39 -13
  9. package/src/-a-archives/archivesLimitedCache.ts +21 -0
  10. package/src/-a-archives/archivesMemoryCache.ts +374 -0
  11. package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
  12. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  13. package/src/0-path-value-core/auditLogs.ts +5 -1
  14. package/src/0-path-value-core/pathValueCore.ts +7 -7
  15. package/src/4-dom/qreact.tsx +1 -0
  16. package/src/4-querysub/Querysub.ts +1 -5
  17. package/src/config.ts +5 -0
  18. package/src/deployManager/components/MachineDetailPage.tsx +43 -2
  19. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  20. package/src/deployManager/machineApplyMainCode.ts +3 -3
  21. package/src/deployManager/machineSchema.ts +39 -0
  22. package/src/diagnostics/MachineThreadInfo.tsx +235 -0
  23. package/src/diagnostics/NodeViewer.tsx +5 -3
  24. package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
  25. package/src/diagnostics/logs/FastArchiveController.ts +102 -63
  26. package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
  27. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +462 -0
  28. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
  29. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
  30. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
  31. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +222 -0
  32. package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
  33. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
  34. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
  35. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +208 -0
  36. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +716 -0
  37. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
  38. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +569 -0
  39. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
  40. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +685 -0
  41. package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
  42. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +901 -0
  43. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
  44. package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
  45. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +251 -0
  46. package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
  47. package/src/diagnostics/logs/LogViewer2.tsx +120 -55
  48. package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
  49. package/src/diagnostics/logs/diskLogger.ts +32 -48
  50. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
  51. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
  52. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  53. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
  54. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +150 -15
  55. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -0
  56. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
  57. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
  58. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
  59. package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
  60. package/src/diagnostics/managementPages.tsx +10 -0
  61. package/src/diagnostics/trackResources.ts +1 -1
  62. package/src/functional/limitProcessing.ts +39 -0
  63. package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
  64. package/src/misc/lz4_wasm_nodejs.js +178 -0
  65. package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
  66. package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
  67. package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
  68. package/src/storage/CompressedStream.ts +13 -0
  69. package/src/storage/LZ4.ts +32 -0
  70. package/src/storage/ZSTD.ts +10 -0
  71. package/src/wat/watCompiler.ts +1716 -0
  72. package/src/wat/watGrammar.pegjs +93 -0
  73. package/src/wat/watHandler.ts +179 -0
  74. package/src/wat/watInstructions.txt +707 -0
  75. package/src/zip.ts +3 -89
  76. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +0 -125
@@ -26,7 +26,7 @@ import { getPathIndex, getPathStr2 } from "../../path";
26
26
  import { onNextPaint } from "../../functional/onNextPaint";
27
27
  import { getArchivesBackblazePrivateImmutable, getArchivesBackblazePublicImmutable } from "../../-a-archives/archivesBackBlaze";
28
28
  import { httpsRequest } from "socket-function/src/https";
29
- import { getDomain } from "../../config";
29
+ import { getDomain, isPublic } from "../../config";
30
30
  import { getIPDomain } from "../../-e-certs/EdgeCertController";
31
31
  import { getArchivesPrivateFileSystem } from "../../-a-archives/archivesPrivateFileSystem";
32
32
  import { createArchivesLimitedCache } from "../../-a-archives/archivesLimitedCache";
@@ -40,6 +40,7 @@ import { Querysub } from "../../4-querysub/QuerysubController";
40
40
 
41
41
  export type FileMetadata = {
42
42
  nodeId?: string;
43
+ machineId?: string;
43
44
  path: string;
44
45
  url: string;
45
46
  size: number;
@@ -67,16 +68,8 @@ type SynchronizeInfo = {
67
68
 
68
69
  // Excluding authorize tokens, then hashed
69
70
  export function getFileMetadataHash(file: FileMetadata): string {
70
- let urlObj = new URL(file.url);
71
- if (!file.nodeId) {
72
- urlObj.search = "";
73
- } else {
74
- let args = JSON.parse(urlObj.searchParams.get("args") || "");
75
- // The syncId shouldn't be part of the hash though
76
- args[0] = "";
77
- urlObj.searchParams.set("args", JSON.stringify(args));
78
- }
79
- return sha256(urlObj.toString());
71
+ // NOTE: This size will be at least equal or older to the size when we read it. And so for the file to have new data, it means the size will have to be larger. And because this size is always equal older to when we read it and never newer, it means it won't include the newer writes. And so even for local files which are constantly changing, this will properly invalidate when we write to them again.
72
+ return sha256(JSON.stringify({ path: file.path, size: file.size, machineId: file.machineId }));
80
73
  }
81
74
 
82
75
  export class FastArchiveAppendableControllerBase {
@@ -131,13 +124,21 @@ export class FastArchiveAppendableControllerBase {
131
124
  /** Download a local file from another server (coordinator forwards request).
132
125
  * NOTE: This doesn't support streaming, which isn't great. However, eventually most logs will make their way to backblaze. So not supporting streaming here doesn't mean we don't support streaming in general. We also compress the file. So for the compressed file to be so big that you can't send it all at once would mean your uncompressed data would just be ridiculously large.
133
126
  */
134
- public async downloadLocalFile(syncId: string, targetNodeId: string, path: string): Promise<Buffer> {
127
+ public async downloadLocalFile(syncId: string, targetNodeId: string, path: string, fromLocalArchives?: boolean): Promise<Buffer> {
135
128
  const caller = SocketFunction.getCaller();
136
129
  let syncInfo = FastArchiveAppendableControllerBase.activeSynchronizes.get(syncId);
137
130
  if (!syncInfo) {
138
131
  throw new Error(`Invalid sync ID: ${syncId}`);
139
132
  }
140
133
 
134
+ if (fromLocalArchives) {
135
+ let archives = new FastArchiveAppendable(syncInfo.config.rootPath).getArchives(false);
136
+ let file = await archives.get(path);
137
+ if (!file) {
138
+ throw new Error(`File not found in local archives: ${path}`);
139
+ }
140
+ return file;
141
+ }
141
142
  try {
142
143
  // Forward the request to the actual server that has the file
143
144
  let targetController = FastArchiveAppendableController.nodes[targetNodeId];
@@ -205,6 +206,7 @@ export class FastArchiveAppendableControllerBase {
205
206
  };
206
207
  rootPath: string;
207
208
  noLocalFiles?: boolean;
209
+ forceGetPublic?: boolean;
208
210
  }): Promise<{
209
211
  files: FileMetadata[];
210
212
  }> {
@@ -234,6 +236,7 @@ export class FastArchiveAppendableControllerBase {
234
236
  range: config.range,
235
237
  rootPath: config.rootPath,
236
238
  noLocalFiles: config.noLocalFiles,
239
+ forceGetPublic: config.forceGetPublic,
237
240
  });
238
241
  }
239
242
 
@@ -244,6 +247,7 @@ export class FastArchiveAppendableControllerBase {
244
247
  };
245
248
  rootPath: string;
246
249
  noLocalFiles?: boolean;
250
+ forceGetPublic?: boolean;
247
251
  }): Promise<{
248
252
  files: FileMetadata[];
249
253
  }> {
@@ -262,6 +266,7 @@ export class FastArchiveAppendableControllerBase {
262
266
  range: config.range,
263
267
  rootPath: config.rootPath,
264
268
  noLocalFiles: config.noLocalFiles,
269
+ forceGetPublic: config.forceGetPublic,
265
270
  });
266
271
  }
267
272
 
@@ -273,6 +278,7 @@ export class FastArchiveAppendableControllerBase {
273
278
  };
274
279
  rootPath: string;
275
280
  noLocalFiles?: boolean;
281
+ forceGetPublic?: boolean;
276
282
  }): Promise<{
277
283
  files: FileMetadata[];
278
284
  }> {
@@ -281,15 +287,60 @@ export class FastArchiveAppendableControllerBase {
281
287
  }
282
288
  let syncId = config.syncId ?? "";
283
289
 
290
+ let isPublicValue = isPublic() || config.forceGetPublic;
291
+
292
+ const getLocalFileMetadata = async (config: {
293
+ nodeId: string;
294
+ path: string;
295
+ size: number;
296
+ fromLocalArchives?: boolean;
297
+ }): Promise<FileMetadata> => {
298
+ let { nodeId, path, size, fromLocalArchives } = config;
299
+ // Create download URL that points to the coordinator (this server)
300
+ // The coordinator will forward the request to the actual file server
301
+ let coordinatorNodeId = getOwnNodeId();
302
+ let coordinatorController = FastArchiveAppendableController.nodes[coordinatorNodeId];
303
+ let downloadCall = coordinatorController.downloadLocalFile[getCallObj](
304
+ syncId,
305
+ nodeId, // Target server that has the file
306
+ path,
307
+ fromLocalArchives,
308
+ );
309
+ let url = SocketFunction.getHTTPCallLink(downloadCall);
310
+ // Have to use the IP domain, as it's externally available. That, plus the port, should uniquely identify us.
311
+ let ipDomain = await getIPDomain();
312
+ let urlObj = new URL(url);
313
+ urlObj.hostname = ipDomain;
314
+ url = urlObj.toString();
315
+
316
+ let timeStamp = getFileTimeStamp(path);
317
+ let startTime = timeStamp.startTime;
318
+ let endTime = timeStamp.endTime;
319
+
320
+ return {
321
+ nodeId: nodeId,
322
+ machineId: getMachineId(nodeId),
323
+ path: path,
324
+ url: url,
325
+ size: size,
326
+ startTime: startTime,
327
+ endTime: endTime,
328
+ };
329
+ };
330
+
284
331
  // Define inline functions for parallel execution
285
332
  const searchBackblazeFiles = async (): Promise<FileMetadata[]> => {
286
- let archives = new FastArchiveAppendable(config.rootPath).getArchives();
333
+ let archives = new FastArchiveAppendable(config.rootPath).getArchives(config.forceGetPublic ?? false);
287
334
  let backblazeFiles: FileMetadata[] = [];
288
- if (!archives.getDownloadAuthorization) throw new Error(`archives.getDownloadAuthorization is missing?`);
289
- let authorization = await archives.getDownloadAuthorization({
335
+ if (!archives.getDownloadAuthorization && isPublicValue) {
336
+ throw new Error(`archives.getDownloadAuthorization is `);
337
+ }
338
+
339
+ if (!archives.getURL && isPublicValue) throw new Error(`archives.getURL is missing?`);
340
+ let authorization = await archives.getDownloadAuthorization?.({
290
341
  validDurationInSeconds: timeInDay * 6 / 1000,
291
342
  });
292
- let authToken = authorization.authorizationToken;
343
+ let authToken = authorization?.authorizationToken;
293
344
 
294
345
  const folderProgress = this.updateProgress(syncId, "Backblaze folder search", 0);
295
346
  let folderMax = 0;
@@ -346,21 +397,28 @@ export class FastArchiveAppendableControllerBase {
346
397
  let filePaths = await archives.findInfo(folder + "/", { shallow: true, type: "files" });
347
398
  for (let info of filePaths) {
348
399
  if (!info.path.endsWith(".log")) continue;
349
-
350
- if (!archives.getURL) throw new Error(`archives.getURL is missing?`);
351
- let url = await archives.getURL(info.path);
352
- let urlObj = new URL(url);
353
- // IMPORTANT! This is CASE SENSITIVE! Ugh...
354
- urlObj.searchParams.set("Authorization", authToken);
355
- url = urlObj.toString();
356
-
357
- backblazeFiles.push({
358
- path: info.path,
359
- url: url,
360
- size: info.size,
361
- startTime: hourStart,
362
- endTime: hourEnd,
363
- });
400
+ if (archives.getURL && authToken !== undefined) {
401
+ let url = await archives.getURL(info.path);
402
+ let urlObj = new URL(url);
403
+ // IMPORTANT! This is CASE SENSITIVE! Ugh...
404
+ urlObj.searchParams.set("Authorization", authToken);
405
+ url = urlObj.toString();
406
+
407
+ backblazeFiles.push({
408
+ path: info.path,
409
+ url: url,
410
+ size: info.size,
411
+ startTime: hourStart,
412
+ endTime: hourEnd,
413
+ });
414
+ } else {
415
+ backblazeFiles.push(await getLocalFileMetadata({
416
+ nodeId: getOwnNodeId(),
417
+ path: info.path,
418
+ size: info.size,
419
+ fromLocalArchives: true,
420
+ }));
421
+ }
364
422
  }
365
423
  }
366
424
  }));
@@ -371,12 +429,20 @@ export class FastArchiveAppendableControllerBase {
371
429
  return backblazeFiles;
372
430
  };
373
431
 
374
- const getRemoteFiles = async (): Promise<FileMetadata[]> => {
432
+ // NOTE: Disk files are on our machine, and on every other server (as we want to search live logs as well).
433
+ const getDiskFiles = async (): Promise<FileMetadata[]> => {
375
434
  const getControllerProgress = this.updateProgress(syncId, "Discovering remote machines", 0);
376
435
 
377
436
  let localFiles: FileMetadata[] = [];
378
437
 
379
438
  let nodeIds = await getControllerNodeIdList(FastArchiveAppendableController);
439
+ // This filtering is somewhat of a hack, as machines can have mixed public status, and change it easliy. However in practice... they don't... We COULD have two sets of pending logs, for public and non-public (like we have backblaze and local archives for non-pending logs). But... at the moment... I don't think it's needed.
440
+ let ownMachineId = getOwnMachineId();
441
+ if (!isPublicValue) {
442
+ nodeIds = nodeIds.filter(x => getMachineId(x.nodeId) === ownMachineId);
443
+ } else if (isPublicValue && !isPublic()) {
444
+ nodeIds = nodeIds.filter(x => getMachineId(x.nodeId) !== ownMachineId);
445
+ }
380
446
  let byMachineId = keyByArray(nodeIds, x => getMachineId(x.nodeId));
381
447
  getControllerProgress(byMachineId.size, byMachineId.size);
382
448
 
@@ -402,7 +468,8 @@ export class FastArchiveAppendableControllerBase {
402
468
  let controller = FastArchiveAppendableController.nodes[aliveNodeId];
403
469
 
404
470
  let pendingFiles = await errorToUndefined(
405
- controller.getPendingFiles(config.rootPath, config.range));
471
+ controller.getPendingFiles(config.rootPath, config.range)
472
+ );
406
473
  console.log(blue(`Found ${pendingFiles?.length} pending files on node ${aliveNodeId}`));
407
474
 
408
475
  remoteValue++;
@@ -410,44 +477,16 @@ export class FastArchiveAppendableControllerBase {
410
477
 
411
478
  if (!pendingFiles) return;
412
479
  for (let file of pendingFiles) {
413
- // Create download URL that points to the coordinator (this server)
414
- // The coordinator will forward the request to the actual file server
415
- let coordinatorNodeId = getOwnNodeId();
416
- let coordinatorController = FastArchiveAppendableController.nodes[coordinatorNodeId];
417
- let downloadCall = coordinatorController.downloadLocalFile[getCallObj](
418
- syncId,
419
- aliveNodeId, // Target server that has the file
420
- file.path
421
- );
422
- let url = SocketFunction.getHTTPCallLink(downloadCall);
423
- // Have to use the IP domain, as it's externally available. That, plus the port, should uniquely identify us.
424
- let ipDomain = await getIPDomain();
425
- let urlObj = new URL(url);
426
- urlObj.hostname = ipDomain;
427
- url = urlObj.toString();
428
-
429
- let timeStamp = getFileTimeStamp(file.path);
430
- let startTime = timeStamp.startTime;
431
- let endTime = timeStamp.endTime;
432
-
433
- localFiles.push({
434
- nodeId: aliveNodeId,
435
- path: file.path,
436
- url: url,
437
- size: file.size,
438
- startTime: startTime,
439
- endTime: endTime,
440
- });
480
+ localFiles.push(await getLocalFileMetadata({ nodeId: aliveNodeId, path: file.path, size: file.size }));
441
481
  }
442
482
  }));
443
483
 
444
484
  return localFiles;
445
485
  };
446
-
447
486
  // Execute both operations in parallel
448
487
  let filePromises: Promise<FileMetadata[]>[] = [];
449
488
  filePromises.push(searchBackblazeFiles());
450
- if (!config.noLocalFiles) filePromises.push(getRemoteFiles());
489
+ if (!config.noLocalFiles) filePromises.push(getDiskFiles());
451
490
 
452
491
  let allFilesList = await Promise.all(filePromises);
453
492
  let allFiles = allFilesList.flat();
@@ -32,11 +32,11 @@ export const filterParam2 = new URLParam("filter2", "");
32
32
  export const cacheBustParam = new URLParam("cacheBust", 0);
33
33
  const caseInsensitiveParam = new URLParam("caseInsensitive", false);
34
34
  const hideAllDataParam = new URLParam("hideAllData", false);
35
-
36
35
  export type ScanFnc = (posStart: number, posEnd: number, data: Buffer) => boolean;
37
36
 
38
37
  export class FastArchiveViewer<T> extends qreact.Component<{
39
- fastArchives: FastArchiveAppendable<T>[];
38
+ fastArchives: () => FastArchiveAppendable<T>[];
39
+ forceGetPublic?: boolean;
40
40
  runOnLoad?: boolean;
41
41
  onStart: () => MaybePromise<void>;
42
42
  getScanFnc?: () => ScanFnc;
@@ -83,6 +83,8 @@ export class FastArchiveViewer<T> extends qreact.Component<{
83
83
  private limitedScanCount = 0;
84
84
  private limitedMatchCount = 0;
85
85
 
86
+ private fileErrors: Array<{ file: FileMetadata; error: Error }> = [];
87
+
86
88
  private progressBars: Record<string, {
87
89
  section: string,
88
90
  value: number,
@@ -105,7 +107,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
105
107
  }
106
108
 
107
109
  cancelAllSynchronizes() {
108
- for (let fastArchive of this.props.fastArchives) {
110
+ for (let fastArchive of this.props.fastArchives()) {
109
111
  fastArchive.cancelAllSynchronizes();
110
112
  }
111
113
  }
@@ -161,7 +163,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
161
163
  filterString,
162
164
  startTime: timeRange.startTime,
163
165
  endTime: timeRange.endTime,
164
- fastArchivePaths: fastArchives.map(archive => archive.rootPath),
166
+ fastArchivePaths: fastArchives().map(archive => archive.rootPath),
165
167
  };
166
168
  });
167
169
 
@@ -174,6 +176,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
174
176
  delete this.progressBars[key];
175
177
  }
176
178
  this.state.fileMetadata = [];
179
+ this.fileErrors = [];
177
180
  this.limitedScanCount = 0;
178
181
  this.limitedMatchCount = 0;
179
182
  this.allSize = 0;
@@ -331,7 +334,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
331
334
  const limitedBuffer = Buffer.from(LOG_LIMIT_FLAG);
332
335
 
333
336
 
334
- await Promise.all(fastArchives.map(async (fastArchive, index) => {
337
+ await Promise.all(fastArchives().map(async (fastArchive, index) => {
335
338
  const result = await fastArchive.synchronizeData({
336
339
  range: timeRange,
337
340
  cacheBust: Querysub.fastRead(() => cacheBustParam.value),
@@ -407,6 +410,10 @@ export class FastArchiveViewer<T> extends qreact.Component<{
407
410
  }
408
411
  }
409
412
  }),
413
+ onError: (error, file) => {
414
+ if (!isLatestSync()) return;
415
+ this.fileErrors.push({ file, error });
416
+ },
410
417
  onProgress,
411
418
  onFinish: () => {
412
419
  ifLatest(() => {
@@ -615,7 +622,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
615
622
  private getOutdatedInfo(): string[] {
616
623
  const currentTimeRange = getTimeRange();
617
624
  const currentFilterString = filterParam.value;
618
- const currentArchivePaths = this.props.fastArchives.map(archive => archive.rootPath);
625
+ const currentArchivePaths = this.props.fastArchives().map(archive => archive.rootPath);
619
626
 
620
627
  // Get stored sync parameters
621
628
  const storedParams = this.state.currentSyncParams;
@@ -735,6 +742,19 @@ export class FastArchiveViewer<T> extends qreact.Component<{
735
742
  <div className={css.vbox(10)}>
736
743
  {this.state.runCount > 0 && (() => {
737
744
  const outdatedWarnings = this.getOutdatedInfo();
745
+ const errorCount = this.fileErrors.length;
746
+
747
+ // Group errors by file path
748
+ const errorsByFile = new Map<string, number>();
749
+ for (let errorInfo of this.fileErrors) {
750
+ const path = errorInfo.file.path;
751
+ errorsByFile.set(path, (errorsByFile.get(path) || 0) + 1);
752
+ }
753
+
754
+ const errorTooltip = Array.from(errorsByFile.entries())
755
+ .map(([path, count]) => `${path}: ${count} errors`)
756
+ .join("\n");
757
+
738
758
  return (
739
759
  <div className={css.hbox(10).wrap}>
740
760
  <div
@@ -743,8 +763,16 @@ export class FastArchiveViewer<T> extends qreact.Component<{
743
763
  `${x.path} (${formatNumber(x.size)})`
744
764
  ).join("\n")}
745
765
  >
746
- File count: {formatNumber(totalFileCount)}, Backblaze size: {formatNumber(totalBackblazeByteCount)}B (compressed), Disk size: {formatNumber(totalLocalByteCount)}B (uncompressed)
766
+ File count: {formatNumber(totalFileCount)}{errorCount > 0 && ` (${errorCount} erred)`}, Backblaze size: {formatNumber(totalBackblazeByteCount)}B (compressed), Disk size: {formatNumber(totalLocalByteCount)}B (uncompressed)
747
767
  </div>
768
+ {errorCount > 0 && (
769
+ <div
770
+ className={infoDisplay(45)}
771
+ title={errorTooltip}
772
+ >
773
+ {errorCount} file{errorCount > 1 ? "s" : ""} failed to load
774
+ </div>
775
+ )}
748
776
  {outdatedWarnings.length > 0 && (
749
777
  <div
750
778
  className={infoDisplay(30).button}
@@ -772,9 +800,9 @@ export class FastArchiveViewer<T> extends qreact.Component<{
772
800
  </div>
773
801
  )}
774
802
  {this.state.finished && (() => {
775
- if (readLocalTimes.length === 0) return null;
776
803
 
777
804
  const timeRange = getTimeRange();
805
+ if (timeRange.endTime && readLocalTimes.length === 0) return null;
778
806
  const earliestTime = Math.min(...readLocalTimes);
779
807
 
780
808
  if (earliestTime >= timeRange.endTime) return null;