pinokiod 7.5.40 → 7.5.42

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.
@@ -2,6 +2,13 @@
2
2
  const TOP_LEVEL_REDACTION_MAX_FILE_BYTES = 2 * 1024 * 1024
3
3
  const TOP_LEVEL_REDACTION_EXTENSIONS = new Set(['.json', '.log', '.txt'])
4
4
  const CADDY_LOG_PATTERN = /^caddy(?:-.+)?\.log$/i
5
+ const TOP_LEVEL_REDACTION_MODES = [
6
+ { value: 'full', label: 'Redact full file' },
7
+ { value: 'tail-2000', label: 'Redact last 2000 lines', lines: 2000 },
8
+ { value: 'tail-1000', label: 'Redact last 1000 lines', lines: 1000 },
9
+ { value: 'tail-500', label: 'Redact last 500 lines', lines: 500 },
10
+ { value: 'exclude', label: 'Exclude from report' }
11
+ ]
5
12
 
6
13
  const humanBytes = (value) => {
7
14
  const units = ['B', 'KB', 'MB', 'GB']
@@ -32,6 +39,19 @@
32
39
  )
33
40
  }
34
41
 
42
+ const modeConfig = (mode) => {
43
+ return TOP_LEVEL_REDACTION_MODES.find((candidate) => candidate.value === mode) || TOP_LEVEL_REDACTION_MODES[0]
44
+ }
45
+
46
+ const tailLinesForMode = (mode) => {
47
+ const config = modeConfig(mode)
48
+ return Number(config && config.lines) || 0
49
+ }
50
+
51
+ const defaultModeForFile = (file) => {
52
+ return file && file.oversized ? 'tail-2000' : 'full'
53
+ }
54
+
35
55
  const TOP_LEVEL_REDACTION_PRIORITY = new Map([
36
56
  ['system.json', 0],
37
57
  ['state.json', 1],
@@ -62,8 +82,8 @@
62
82
 
63
83
  class LogsTopLevelRedactor {
64
84
  constructor(options) {
65
- this.button = options.button
66
- this.chip = options.chip
85
+ this.openButton = options.openButton || null
86
+ this.button = options.redactButton || options.button
67
87
  this.pane = options.pane
68
88
  this.collapseButton = options.collapseButton
69
89
  this.statusEl = options.statusEl
@@ -87,35 +107,44 @@
87
107
  this.hasRun = false
88
108
  this.isOpen = false
89
109
  this.isRunning = false
110
+ this.isPreparing = false
111
+ this.hasCustomFileChoices = false
112
+ if (this.openButton) {
113
+ this.openButton.addEventListener('click', () => this.prepare())
114
+ }
90
115
  if (this.button) {
91
116
  this.button.addEventListener('click', () => this.handleRedactClick())
92
117
  }
93
- if (this.chip) {
94
- this.chip.addEventListener('click', () => this.setOpen(true))
95
- }
96
118
  if (this.collapseButton) {
97
119
  this.collapseButton.addEventListener('click', () => this.setOpen(false))
98
120
  }
99
121
  this.renderEmptyReview()
100
- this.updateChip()
101
122
  }
102
123
  handleRedactClick() {
103
- if (this.isRunning) {
104
- return
105
- }
106
- if (this.hasRun && !this.isOpen) {
107
- this.setOpen(true)
124
+ if (this.isRunning || this.isPreparing) {
108
125
  return
109
126
  }
110
127
  this.run()
111
128
  }
129
+ clearPlan(message = 'File list refreshed. Generate log report to review files.') {
130
+ this.files = []
131
+ this.items = []
132
+ this.hasRun = false
133
+ this.selectedPath = ''
134
+ this.selectedItemId = null
135
+ this.activeFilter = 'all'
136
+ this.hasCustomFileChoices = false
137
+ this.renderEmptyReview('Run Redact report to mask sensitive items.')
138
+ this.updateCount()
139
+ this.setStatus(message)
140
+ }
112
141
  setOpen(open) {
113
142
  this.isOpen = Boolean(open)
114
143
  if (this.pane) {
115
144
  this.pane.classList.toggle('hidden', !this.isOpen)
116
145
  }
117
- if (this.chip) {
118
- this.chip.setAttribute('aria-expanded', this.isOpen ? 'true' : 'false')
146
+ if (this.openButton) {
147
+ this.openButton.setAttribute('aria-expanded', this.isOpen ? 'true' : 'false')
119
148
  }
120
149
  if (this.isOpen && this.hasRun) {
121
150
  this.renderPreview()
@@ -132,7 +161,7 @@
132
161
  if (this.isRunning) {
133
162
  this.button.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i><span>Redacting…</span>'
134
163
  } else {
135
- this.button.innerHTML = '<i class="fa-solid fa-shield-halved"></i><span>Redact</span>'
164
+ this.button.innerHTML = '<i class="fa-solid fa-shield-halved"></i><span>Redact report</span>'
136
165
  }
137
166
  }
138
167
  setStatus(message, isError) {
@@ -146,7 +175,8 @@
146
175
  return
147
176
  }
148
177
  if (!this.hasRun && !this.isRunning) {
149
- this.countEl.textContent = 'Not run'
178
+ const selected = this.files.filter((file) => file && file.mode !== 'exclude').length
179
+ this.countEl.textContent = this.files.length ? `${selected} selected · Not run` : 'Not run'
150
180
  return
151
181
  }
152
182
  const files = this.files.filter((file) => file.reviewed).length
@@ -156,24 +186,6 @@
156
186
  ? `${files} / ${total} files · ${enabled} masked`
157
187
  : `${files} file${files === 1 ? '' : 's'} · ${enabled} masked`
158
188
  }
159
- updateChip() {
160
- if (!this.chip) {
161
- return
162
- }
163
- const labelEl = this.chip.querySelector('span') || this.chip
164
- if (!this.hasRun && !this.isRunning) {
165
- this.chip.classList.add('hidden')
166
- labelEl.textContent = 'Not run'
167
- return
168
- }
169
- const files = this.files.filter((file) => file.reviewed).length
170
- const total = this.files.length
171
- const enabled = this.enabledRedactionCount()
172
- labelEl.textContent = this.isRunning
173
- ? `Redacting ${files}/${total} · ${enabled} item${enabled === 1 ? '' : 's'}`
174
- : `Redacted ${files} file${files === 1 ? '' : 's'} · ${enabled} item${enabled === 1 ? '' : 's'}`
175
- this.chip.classList.remove('hidden')
176
- }
177
189
  enabledRedactionCount() {
178
190
  return this.items.reduce((total, item) => total + (item && item.enabled ? 1 : 0), 0)
179
191
  }
@@ -268,17 +280,87 @@
268
280
  const entries = (Array.isArray(payload && payload.entries) ? payload.entries : [])
269
281
  .filter((entry) => entry && entry.type !== 'directory' && isTopLevelRedactableLogPath(entry.path || entry.name))
270
282
  .sort(compareTopLevelRedactionEntries)
271
- const oversized = entries.find((entry) => Number.isFinite(Number(entry.size)) && Number(entry.size) > TOP_LEVEL_REDACTION_MAX_FILE_BYTES)
272
- if (oversized) {
273
- const name = oversized.path || oversized.name || 'file'
274
- throw new Error(`${name} is too large to redact in the browser (${humanBytes(Number(oversized.size))}).`)
275
- }
276
283
  return entries
277
284
  }
285
+ buildPlannedFile(entry) {
286
+ const pathValue = String(entry && (entry.path || entry.name) || '')
287
+ const name = String(entry && entry.name || pathValue)
288
+ const size = Number(entry && entry.size) || 0
289
+ const oversized = size > TOP_LEVEL_REDACTION_MAX_FILE_BYTES
290
+ return {
291
+ path: pathValue,
292
+ name,
293
+ size,
294
+ mode: oversized ? 'tail-2000' : 'full',
295
+ oversized,
296
+ reviewed: false,
297
+ items: [],
298
+ renderedItems: [],
299
+ text: ''
300
+ }
301
+ }
302
+ async prepare() {
303
+ if (this.isPreparing || this.isRunning) {
304
+ return
305
+ }
306
+ this.setOpen(true)
307
+ if (this.files.length) {
308
+ this.renderFileList()
309
+ this.renderReview()
310
+ this.updateCount()
311
+ this.setStatus(this.hasRun
312
+ ? 'Report redacted. Zip will use reviewed redactions.'
313
+ : (this.hasCustomFileChoices ? 'File choices changed. Generate zip will use these choices without redaction.' : 'Zip will include original logs unless you redact the report.'))
314
+ return
315
+ }
316
+ this.isPreparing = true
317
+ this.renderEmptyReview('Run Redact report to mask sensitive items.')
318
+ try {
319
+ this.setStatus('Finding top-level files…')
320
+ const entries = await this.fetchTopLevelEntries()
321
+ if (!entries.length) {
322
+ throw new Error('No top-level text log files are available for a report.')
323
+ }
324
+ this.files = entries.map((entry) => this.buildPlannedFile(entry))
325
+ this.items = []
326
+ this.hasRun = false
327
+ this.hasCustomFileChoices = false
328
+ this.selectedItemId = null
329
+ this.selectedPath = (this.files[0] || {}).path || ''
330
+ this.renderFileList()
331
+ this.renderReview()
332
+ this.updateCount()
333
+ this.setStatus('Zip will include original logs unless you redact the report.')
334
+ } catch (error) {
335
+ this.files = []
336
+ this.items = []
337
+ this.hasRun = false
338
+ this.selectedPath = ''
339
+ this.selectedItemId = null
340
+ this.renderEmptyReview('No report file plan is available.')
341
+ this.updateCount()
342
+ this.setStatus(error && error.message ? error.message : 'Failed to prepare log report.', true)
343
+ } finally {
344
+ this.isPreparing = false
345
+ }
346
+ }
278
347
  async fetchFile(entry) {
279
348
  const pathValue = String(entry && (entry.path || entry.name) || '')
349
+ if (entry && entry.mode === 'exclude') {
350
+ return {
351
+ ...entry,
352
+ text: '',
353
+ reviewed: false,
354
+ items: [],
355
+ renderedItems: []
356
+ }
357
+ }
280
358
  const url = new URL('/pinokio/logs/file', window.location.origin)
281
359
  url.searchParams.set('path', pathValue)
360
+ const tailLines = tailLinesForMode(entry && entry.mode)
361
+ if (tailLines) {
362
+ url.searchParams.set('tail_lines', String(tailLines))
363
+ }
282
364
  const response = await fetch(url, { headers: { 'Accept': 'application/json' } })
283
365
  if (!response.ok) {
284
366
  const message = await response.text()
@@ -289,6 +371,9 @@
289
371
  path: payload.path || pathValue,
290
372
  name: payload.name || pathValue,
291
373
  size: Number(payload.size) || Number(entry && entry.size) || 0,
374
+ mode: entry && entry.mode ? entry.mode : 'full',
375
+ oversized: Boolean(entry && entry.oversized),
376
+ truncated: Boolean(payload.truncated),
292
377
  text: String(payload.text || ''),
293
378
  reviewed: false,
294
379
  items: [],
@@ -316,80 +401,78 @@
316
401
  }
317
402
  async run() {
318
403
  this.setOpen(true)
319
- this.setBusy(true)
320
- this.hasRun = false
321
- this.files = []
322
- this.items = []
323
- this.selectedItemId = null
324
- this.activeFilter = 'all'
325
- this.updateCount()
326
- this.updateChip()
327
- this.renderEmptyReview('Preparing top-level files…')
328
404
  try {
329
- this.setStatus('Finding top-level files…')
330
- const entries = await this.fetchTopLevelEntries()
331
- if (!entries.length) {
332
- throw new Error('No top-level text log files are available for redaction.')
405
+ if (!this.files.length) {
406
+ await this.prepare()
333
407
  }
334
- const files = []
335
- for (let index = 0; index < entries.length; index += 1) {
336
- const entry = entries[index]
337
- this.setStatus(`Loading ${entry.path || entry.name}… ${index + 1} / ${entries.length}`)
338
- const file = await this.fetchFile(entry)
339
- files.push(file)
408
+ if (!this.files.length) {
409
+ return
340
410
  }
341
- this.files = files
342
- this.selectedPath = files[0] ? files[0].path : ''
343
- this.renderFileList()
344
- this.renderReview()
411
+ this.setBusy(true)
412
+ this.hasRun = false
413
+ this.items = []
414
+ this.selectedItemId = null
415
+ this.activeFilter = 'all'
416
+ this.files.forEach((file) => {
417
+ if (file) {
418
+ file.reviewed = false
419
+ file.text = ''
420
+ file.items = []
421
+ file.renderedItems = []
422
+ }
423
+ })
345
424
  this.updateCount()
346
- this.updateChip()
347
- for (let index = 0; index < files.length; index += 1) {
348
- const file = files[index]
349
- this.setStatus(`Filtering ${file.name}… ${index + 1} / ${files.length}`)
350
- const result = await this.privacyFilter.filter(file.text, (progress) => this.renderFilterProgress(file, progress))
351
- file.items = this.normalizeItems(file, result)
352
- file.reviewed = true
353
- this.buildFileText(file)
354
- this.items = files.flatMap((candidate) => candidate.items)
355
- this.hasRun = true
425
+ this.renderEmptyReview('Redacting selected files…')
426
+ const filesToRedact = this.files.filter(Boolean)
427
+ for (let index = 0; index < filesToRedact.length; index += 1) {
428
+ const planned = filesToRedact[index]
429
+ this.setStatus(`Loading ${planned.name}… ${index + 1} / ${filesToRedact.length}`)
430
+ const loaded = await this.fetchFile(planned)
431
+ Object.assign(planned, loaded)
432
+ if (planned.mode === 'exclude') {
433
+ planned.items = []
434
+ } else {
435
+ this.setStatus(`Filtering ${planned.name}… ${index + 1} / ${filesToRedact.length}`)
436
+ const result = await this.privacyFilter.filter(planned.text, (progress) => this.renderFilterProgress(planned, progress))
437
+ planned.items = this.normalizeItems(planned, result)
438
+ }
439
+ planned.reviewed = true
440
+ this.buildFileText(planned)
441
+ this.items = this.files.flatMap((candidate) => candidate ? candidate.items : [])
442
+ this.hasRun = this.files.some((candidate) => candidate && candidate.reviewed)
356
443
  if (!this.selectedPath || !this.files.find((candidate) => candidate.path === this.selectedPath && candidate.reviewed)) {
357
- this.selectedPath = file.path
444
+ this.selectedPath = planned.path
358
445
  }
359
- if (!this.selectedItemId && file.items.length) {
360
- this.selectedItemId = file.items[0].id
446
+ if (!this.selectedItemId && planned.items.length) {
447
+ this.selectedItemId = planned.items[0].id
361
448
  }
362
449
  this.renderFileList()
363
450
  this.renderReview()
364
451
  this.renderPreview()
365
452
  this.updateCount()
366
- this.updateChip()
367
453
  }
368
- this.items = files.flatMap((file) => file.items)
369
- this.hasRun = true
370
- this.selectedPath = this.selectedPath || (files[0] ? files[0].path : '')
454
+ this.items = this.files.flatMap((file) => file ? file.items : [])
455
+ this.hasRun = this.files.some((file) => file && file.reviewed)
456
+ const firstReviewed = this.files.find((file) => file && file.reviewed)
457
+ this.selectedPath = this.selectedPath || (firstReviewed ? firstReviewed.path : '')
371
458
  this.selectedItemId = this.items.length ? this.items[0].id : null
372
459
  this.renderFileList()
373
460
  this.renderReview()
374
461
  this.renderPreview()
375
462
  this.updateCount()
376
- this.updateChip()
377
463
  const enabled = this.enabledRedactionCount()
378
- this.setStatus(`Privacy filter finished. ${enabled} item${enabled === 1 ? '' : 's'} masked across ${files.length} top-level file${files.length === 1 ? '' : 's'}.`)
464
+ const reviewed = this.files.filter((file) => file && file.reviewed).length
465
+ this.setStatus(`Report redacted. ${enabled} item${enabled === 1 ? '' : 's'} masked across ${reviewed} file${reviewed === 1 ? '' : 's'}.`)
379
466
  } catch (error) {
380
- this.files = []
381
467
  this.items = []
382
468
  this.hasRun = false
383
- this.selectedPath = ''
384
469
  this.selectedItemId = null
385
470
  this.renderEmptyReview('No redaction review is available.')
386
471
  this.updateCount()
387
- this.updateChip()
388
472
  this.setStatus(error && error.message ? error.message : 'Privacy filtering failed.', true)
389
473
  } finally {
390
474
  this.setBusy(false)
391
475
  this.updateCount()
392
- this.updateChip()
393
476
  }
394
477
  }
395
478
  renderEmptyReview(message = 'Run Redact to review detected items.') {
@@ -413,11 +496,11 @@
413
496
  }
414
497
  this.filesEl.textContent = ''
415
498
  this.files.forEach((file) => {
416
- const row = document.createElement('button')
417
- row.type = 'button'
499
+ const row = document.createElement('div')
418
500
  row.className = 'logs-top-redaction-file'
419
501
  row.classList.toggle('is-active', file.path === this.selectedPath)
420
- row.setAttribute('aria-label', `Preview redacted ${file.name}`)
502
+ row.classList.toggle('is-warning', Boolean(file.oversized))
503
+ row.setAttribute('aria-label', `Configure ${file.name}`)
421
504
  const text = document.createElement('span')
422
505
  text.className = 'logs-top-redaction-file-text'
423
506
  const name = document.createElement('span')
@@ -426,13 +509,34 @@
426
509
  const meta = document.createElement('span')
427
510
  meta.className = 'logs-top-redaction-file-meta'
428
511
  const count = file.items.reduce((total, item) => total + (item.enabled ? 1 : 0), 0)
429
- meta.textContent = file.reviewed
430
- ? `${humanBytes(file.size)} · ${count} masked`
431
- : `${humanBytes(file.size)} · pending`
512
+ const config = modeConfig(file.mode)
513
+ meta.textContent = file.reviewed ? `${humanBytes(file.size)} · ${count} masked` : `${humanBytes(file.size)} · ${config.label}`
432
514
  text.appendChild(name)
433
515
  text.appendChild(meta)
434
516
  row.appendChild(text)
435
- row.addEventListener('click', () => {
517
+ if (file.oversized) {
518
+ const badge = document.createElement('span')
519
+ badge.className = 'logs-top-redaction-file-badge'
520
+ badge.textContent = 'Too large'
521
+ row.appendChild(badge)
522
+ }
523
+ const select = document.createElement('select')
524
+ select.className = 'logs-top-redaction-mode'
525
+ select.setAttribute('aria-label', `Report handling for ${file.name}`)
526
+ const options = TOP_LEVEL_REDACTION_MODES.filter((option) => option.value !== 'full' || !file.oversized)
527
+ options.forEach((optionConfig) => {
528
+ const option = document.createElement('option')
529
+ option.value = optionConfig.value
530
+ option.textContent = optionConfig.label
531
+ select.appendChild(option)
532
+ })
533
+ select.value = file.mode
534
+ select.disabled = this.isRunning
535
+ select.addEventListener('change', (event) => {
536
+ this.setFileMode(file.path, event.target.value)
537
+ })
538
+ row.appendChild(select)
539
+ text.addEventListener('click', () => {
436
540
  this.selectedPath = file.path
437
541
  this.renderFileList()
438
542
  this.renderPreview()
@@ -440,6 +544,32 @@
440
544
  this.filesEl.appendChild(row)
441
545
  })
442
546
  }
547
+ setFileMode(pathValue, mode) {
548
+ const file = this.files.find((candidate) => candidate && candidate.path === pathValue)
549
+ if (!file) {
550
+ return
551
+ }
552
+ const allowedModes = TOP_LEVEL_REDACTION_MODES
553
+ .filter((option) => option.value !== 'full' || !file.oversized)
554
+ .map((option) => option.value)
555
+ file.mode = allowedModes.includes(mode) ? mode : (file.oversized ? 'tail-2000' : 'full')
556
+ this.hasCustomFileChoices = this.files.some((candidate) => {
557
+ return candidate && candidate.mode !== defaultModeForFile(candidate)
558
+ })
559
+ file.reviewed = false
560
+ file.text = ''
561
+ file.items = []
562
+ file.renderedItems = []
563
+ this.items = []
564
+ this.hasRun = false
565
+ this.selectedItemId = null
566
+ this.renderFileList()
567
+ this.renderReview()
568
+ this.updateCount()
569
+ this.setStatus(this.hasCustomFileChoices
570
+ ? 'File choices changed. Generate zip will use these choices without redaction.'
571
+ : 'Zip will include original logs unless you redact the report.')
572
+ }
443
573
  renderReview() {
444
574
  const includedItems = this.items
445
575
  const labels = new Map()
@@ -470,7 +600,7 @@
470
600
  if (!visibleItems.length) {
471
601
  const empty = document.createElement('div')
472
602
  empty.className = 'logs-redaction-empty'
473
- empty.textContent = this.hasRun ? 'No redactions match this filter.' : 'Run Redact to review detected items.'
603
+ empty.textContent = this.hasRun ? 'No redactions match this filter.' : 'Run Redact report to mask sensitive items.'
474
604
  this.listEl.appendChild(empty)
475
605
  return
476
606
  }
@@ -528,7 +658,6 @@
528
658
  this.renderReview()
529
659
  this.renderPreview()
530
660
  this.updateCount()
531
- this.updateChip()
532
661
  })
533
662
  const toggleTrack = document.createElement('span')
534
663
  toggleTrack.className = 'logs-redaction-toggle-track'
@@ -649,19 +778,21 @@
649
778
  return true
650
779
  }
651
780
  getArchiveBlockMessage() {
652
- return this.isRunning
653
- ? 'Privacy filter is still reviewing top-level files. Generate zip after redaction finishes.'
654
- : ''
781
+ if (this.isRunning) {
782
+ return 'Redaction is still running. Generate zip after redaction finishes.'
783
+ }
784
+ return ''
655
785
  }
656
786
  buildArchivePayload() {
657
787
  if (this.isRunning) {
658
788
  return null
659
789
  }
660
790
  if (!this.hasRun) {
661
- return null
791
+ return this.buildConfiguredArchivePayload()
662
792
  }
663
793
  const overrides = this.files
664
794
  .filter((file) => file.reviewed && isTopLevelRedactableLogPath(file.path))
795
+ .filter((file) => file.mode !== 'exclude')
665
796
  .map((file) => {
666
797
  const text = this.buildFileText(file)
667
798
  return {
@@ -669,12 +800,55 @@
669
800
  text
670
801
  }
671
802
  })
672
- if (!overrides.length) {
803
+ const exclusions = this.files
804
+ .filter((file) => file.reviewed && file.mode === 'exclude' && isTopLevelRedactableLogPath(file.path))
805
+ .map((file) => file.path)
806
+ if (!overrides.length && !exclusions.length) {
673
807
  return null
674
808
  }
675
- return {
676
- redacted_overrides: overrides
809
+ const payload = {}
810
+ if (overrides.length) {
811
+ payload.redacted_overrides = overrides
812
+ }
813
+ if (exclusions.length) {
814
+ payload.excluded_paths = exclusions
815
+ }
816
+ payload.require_complete_overrides = true
817
+ return payload
818
+ }
819
+ async buildConfiguredArchivePayload() {
820
+ if (!this.hasCustomFileChoices) {
821
+ return null
822
+ }
823
+ const overrides = []
824
+ const exclusions = []
825
+ const files = this.files.filter((file) => file && isTopLevelRedactableLogPath(file.path))
826
+ for (const file of files) {
827
+ if (file.mode === 'exclude') {
828
+ exclusions.push(file.path)
829
+ continue
830
+ }
831
+ if (file.mode === defaultModeForFile(file)) {
832
+ continue
833
+ }
834
+ const loaded = await this.fetchFile(file)
835
+ overrides.push({
836
+ path: loaded.path,
837
+ text: loaded.text
838
+ })
839
+ }
840
+ if (!overrides.length && !exclusions.length) {
841
+ return null
842
+ }
843
+ const payload = {}
844
+ if (overrides.length) {
845
+ payload.redacted_overrides = overrides
846
+ }
847
+ if (exclusions.length) {
848
+ payload.excluded_paths = exclusions
677
849
  }
850
+ payload.allow_partial_overrides = true
851
+ return payload
678
852
  }
679
853
  }
680
854
 
@@ -684,8 +858,8 @@
684
858
  return null
685
859
  }
686
860
  return new LogsTopLevelRedactor({
687
- button,
688
- chip: document.getElementById('logs-redaction-chip'),
861
+ openButton: document.getElementById('logs-open-log-report'),
862
+ redactButton: button,
689
863
  pane: document.getElementById('logs-top-redaction-pane'),
690
864
  collapseButton: document.getElementById('logs-redaction-collapse'),
691
865
  statusEl: document.getElementById('logs-top-redaction-status'),
@@ -333,7 +333,15 @@
333
333
  this.setStatus('Generating archive…')
334
334
  try {
335
335
  const request = { method: 'POST' }
336
- const requestPayload = this.payloadProvider ? this.payloadProvider() : null
336
+ const requestPayload = this.payloadProvider ? await this.payloadProvider() : null
337
+ const requestedReviewedArchive = Boolean(
338
+ requestPayload &&
339
+ typeof requestPayload === 'object' &&
340
+ (
341
+ (Array.isArray(requestPayload.redacted_overrides) && requestPayload.redacted_overrides.length > 0) ||
342
+ (Array.isArray(requestPayload.excluded_paths) && requestPayload.excluded_paths.length > 0)
343
+ )
344
+ )
337
345
  if (requestPayload && typeof requestPayload === 'object' && Object.keys(requestPayload).length > 0) {
338
346
  request.headers = { 'Content-Type': 'application/json' }
339
347
  request.body = JSON.stringify(requestPayload)
@@ -347,9 +355,11 @@
347
355
  const downloadHref = responsePayload && responsePayload.download ? responsePayload.download : this.defaultDownloadHref
348
356
  this.updateDownloadLink(downloadHref)
349
357
  const overrideCount = Number(responsePayload && responsePayload.redacted_overrides) || 0
350
- this.setStatus(overrideCount > 0
351
- ? `Archive ready with ${overrideCount} reviewed file${overrideCount === 1 ? '' : 's'}.`
352
- : 'Archive ready. Click download.')
358
+ const excludedCount = Number(responsePayload && responsePayload.excluded_paths) || 0
359
+ const handledCount = overrideCount + excludedCount
360
+ this.setStatus(requestedReviewedArchive || handledCount > 0
361
+ ? `Archive ready with ${handledCount} selected file${handledCount === 1 ? '' : 's'}${excludedCount ? `, ${excludedCount} excluded` : ''}.`
362
+ : 'Archive ready with original logs.')
353
363
  } catch (error) {
354
364
  this.setStatus(error.message || 'Failed to generate archive.', true)
355
365
  } finally {
@@ -2777,6 +2787,12 @@
2777
2787
  refreshBtn.classList.add('is-busy')
2778
2788
  try {
2779
2789
  await this.tree.refresh()
2790
+ if (this.topRedactor) {
2791
+ this.topRedactor.clearPlan('Files refreshed. Zip will include original logs unless you redact the report.')
2792
+ if (this.topRedactor.isOpen) {
2793
+ await this.topRedactor.prepare()
2794
+ }
2795
+ }
2780
2796
  this.viewer.setStatus('Tree refreshed.')
2781
2797
  } catch (error) {
2782
2798
  this.viewer.setStatus(error.message || 'Failed to refresh tree.')