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.
- package/package.json +1 -1
- package/server/index.js +43 -7
- package/server/lib/log_redaction.js +163 -5
- package/server/public/logs-top-redaction.css +85 -37
- package/server/public/logs-top-redaction.js +276 -102
- package/server/public/logs.js +20 -4
- package/server/public/style.css +82 -0
- package/server/views/app.ejs +323 -10
- package/server/views/logs.ejs +36 -25
- package/server/views/partials/logs_top_redaction_actions.ejs +1 -2
- package/server/views/partials/logs_top_redaction_pane.ejs +16 -6
- package/test/launch-settings-ui.test.js +122 -0
- package/test/log-redaction-overrides.test.js +85 -3
- package/test/logs-ask-ai.test.js +157 -17
|
@@ -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.
|
|
66
|
-
this.
|
|
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.
|
|
118
|
-
this.
|
|
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.
|
|
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.
|
|
330
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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.
|
|
342
|
-
this.
|
|
343
|
-
this.
|
|
344
|
-
this.
|
|
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.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 =
|
|
444
|
+
this.selectedPath = planned.path
|
|
358
445
|
}
|
|
359
|
-
if (!this.selectedItemId &&
|
|
360
|
-
this.selectedItemId =
|
|
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 =
|
|
370
|
-
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
653
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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
|
-
|
|
688
|
-
|
|
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'),
|
package/server/public/logs.js
CHANGED
|
@@ -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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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.')
|