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
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1498,6 +1498,28 @@ class Server {
|
|
|
1498
1498
|
community_url = ""
|
|
1499
1499
|
}
|
|
1500
1500
|
}
|
|
1501
|
+
const buildGitRemoteWebUrl = (value) => {
|
|
1502
|
+
const raw = typeof value === "string" ? value.trim() : ""
|
|
1503
|
+
if (!raw) {
|
|
1504
|
+
return ""
|
|
1505
|
+
}
|
|
1506
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
1507
|
+
return raw.replace(/\.git$/i, "")
|
|
1508
|
+
}
|
|
1509
|
+
const sshUrlMatch = raw.match(/^ssh:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/i)
|
|
1510
|
+
if (sshUrlMatch && sshUrlMatch[1] && sshUrlMatch[2]) {
|
|
1511
|
+
return `https://${sshUrlMatch[1]}/${sshUrlMatch[2]}`
|
|
1512
|
+
}
|
|
1513
|
+
const scpMatch = raw.match(/^(?:[^@]+@)?([^:/]+):(.+?)(?:\.git)?$/)
|
|
1514
|
+
if (scpMatch && scpMatch[1] && scpMatch[2]) {
|
|
1515
|
+
return `https://${scpMatch[1]}/${scpMatch[2]}`
|
|
1516
|
+
}
|
|
1517
|
+
return ""
|
|
1518
|
+
}
|
|
1519
|
+
const launcherRemote = this.kernel.api && typeof this.kernel.api.parentGitURI === "function"
|
|
1520
|
+
? (this.kernel.api.parentGitURI(this.kernel.path("api", name)) || "")
|
|
1521
|
+
: ""
|
|
1522
|
+
const launcherRemoteUrl = buildGitRemoteWebUrl(launcherRemote)
|
|
1501
1523
|
|
|
1502
1524
|
let editor_tab = `/pinokio/fileview/${encodeURIComponent(name)}`
|
|
1503
1525
|
const tabsStorageKey = `${name}:${type}`
|
|
@@ -1593,6 +1615,7 @@ class Server {
|
|
|
1593
1615
|
review_tab,
|
|
1594
1616
|
files_tab,
|
|
1595
1617
|
community_url,
|
|
1618
|
+
launcher_remote_url: launcherRemoteUrl,
|
|
1596
1619
|
// paths,
|
|
1597
1620
|
theme: this.theme,
|
|
1598
1621
|
agent: req.agent,
|
|
@@ -15968,12 +15991,21 @@ class Server {
|
|
|
15968
15991
|
}
|
|
15969
15992
|
return
|
|
15970
15993
|
}
|
|
15971
|
-
const
|
|
15994
|
+
const reviewedArchiveRequested = Boolean(req.body && (
|
|
15995
|
+
Object.prototype.hasOwnProperty.call(req.body, 'redacted_overrides') ||
|
|
15996
|
+
Object.prototype.hasOwnProperty.call(req.body, 'excluded_paths')
|
|
15997
|
+
))
|
|
15998
|
+
const allowPartialOverrides = Boolean(req.body && (
|
|
15999
|
+
req.body.allow_partial_overrides === true ||
|
|
16000
|
+
req.body.allow_partial_overrides === 'true'
|
|
16001
|
+
))
|
|
15972
16002
|
let redactionOverrides = []
|
|
16003
|
+
let redactionExclusions = []
|
|
15973
16004
|
try {
|
|
15974
16005
|
redactionOverrides = logRedaction.normalizeLogRedactionOverrides(req.body || {})
|
|
16006
|
+
redactionExclusions = logRedaction.normalizeLogRedactionExclusions(req.body || {})
|
|
15975
16007
|
} catch (error) {
|
|
15976
|
-
res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction
|
|
16008
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction request' })
|
|
15977
16009
|
return
|
|
15978
16010
|
}
|
|
15979
16011
|
|
|
@@ -15991,22 +16023,26 @@ class Server {
|
|
|
15991
16023
|
|
|
15992
16024
|
let folder = this.kernel.path("exported_logs")
|
|
15993
16025
|
let zipPath = this.kernel.path("logs.zip")
|
|
15994
|
-
if (
|
|
16026
|
+
if (reviewedArchiveRequested) {
|
|
15995
16027
|
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
15996
16028
|
}
|
|
15997
16029
|
let appliedRedactionOverrides = 0
|
|
16030
|
+
let appliedRedactionExclusions = 0
|
|
15998
16031
|
try {
|
|
15999
|
-
if (
|
|
16000
|
-
await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides
|
|
16032
|
+
if (reviewedArchiveRequested) {
|
|
16033
|
+
await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides, redactionExclusions, {
|
|
16034
|
+
requireComplete: !allowPartialOverrides
|
|
16035
|
+
})
|
|
16001
16036
|
}
|
|
16037
|
+
appliedRedactionExclusions = await logRedaction.applyLogRedactionExclusions(folder, redactionExclusions)
|
|
16002
16038
|
appliedRedactionOverrides = await logRedaction.applyLogRedactionOverrides(folder, redactionOverrides)
|
|
16003
16039
|
} catch (error) {
|
|
16004
|
-
res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction
|
|
16040
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction request' })
|
|
16005
16041
|
return
|
|
16006
16042
|
}
|
|
16007
16043
|
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
16008
16044
|
await compressing.zip.compressDir(folder, zipPath)
|
|
16009
|
-
res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides })
|
|
16045
|
+
res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides, excluded_paths: appliedRedactionExclusions })
|
|
16010
16046
|
}))
|
|
16011
16047
|
this.app.get("/pinokio/version", ex(async (req, res) => {
|
|
16012
16048
|
let version = this.version
|
|
@@ -5,6 +5,7 @@ const { isBinaryFile } = require('isbinaryfile')
|
|
|
5
5
|
const LOG_REDACTION_FILE_MAX_BYTES = 2 * 1024 * 1024
|
|
6
6
|
const LOG_REDACTION_OVERRIDE_MAX_BYTES = 8 * 1024 * 1024
|
|
7
7
|
const LOG_REDACTION_TEXT_EXTENSIONS = new Set(['.json', '.log', '.txt'])
|
|
8
|
+
const LOG_REDACTION_TAIL_LINE_COUNTS = new Set([500, 1000, 2000])
|
|
8
9
|
const CADDY_LOG_PATTERN = /^caddy(?:-.+)?\.log$/i
|
|
9
10
|
|
|
10
11
|
function isTopLevelRedactableLogPath(relativePath = '') {
|
|
@@ -21,8 +22,84 @@ function isTopLevelRedactableLogPath(relativePath = '') {
|
|
|
21
22
|
return LOG_REDACTION_TEXT_EXTENSIONS.has(path.extname(value).toLowerCase())
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function normalizeTailLineCount(value) {
|
|
26
|
+
const parsed = Number.parseInt(String(value || ''), 10)
|
|
27
|
+
return LOG_REDACTION_TAIL_LINE_COUNTS.has(parsed) ? parsed : 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readTailTextFile(filePath, stats, tailLines) {
|
|
31
|
+
const readBytes = Math.min(stats.size, LOG_REDACTION_FILE_MAX_BYTES - 2048)
|
|
32
|
+
const start = Math.max(0, stats.size - readBytes)
|
|
33
|
+
const buffer = Buffer.alloc(readBytes)
|
|
34
|
+
const handle = await fs.promises.open(filePath, 'r')
|
|
35
|
+
let bytesRead = 0
|
|
36
|
+
try {
|
|
37
|
+
const result = await handle.read(buffer, 0, readBytes, start)
|
|
38
|
+
bytesRead = result.bytesRead
|
|
39
|
+
} finally {
|
|
40
|
+
await handle.close()
|
|
41
|
+
}
|
|
42
|
+
let text = buffer.slice(0, bytesRead).toString('utf8')
|
|
43
|
+
if (start > 0) {
|
|
44
|
+
const firstNewline = text.indexOf('\n')
|
|
45
|
+
if (firstNewline >= 0) {
|
|
46
|
+
text = text.slice(firstNewline + 1)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const lines = text.split(/\r?\n/)
|
|
50
|
+
const selected = lines.length > tailLines ? lines.slice(-tailLines) : lines
|
|
51
|
+
const omittedLines = Math.max(0, lines.length - selected.length)
|
|
52
|
+
const prefix = start > 0 || omittedLines > 0
|
|
53
|
+
? `[Older log content omitted by user. Showing the last ${tailLines.toLocaleString()} lines.]\n`
|
|
54
|
+
: ''
|
|
55
|
+
return {
|
|
56
|
+
text: `${prefix}${selected.join('\n')}`,
|
|
57
|
+
truncated: start > 0 || omittedLines > 0,
|
|
58
|
+
included_lines: selected.length
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clonePlainObject(value) {
|
|
63
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
64
|
+
? { ...value }
|
|
65
|
+
: {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createRuntimeEnvSnapshot(kernel) {
|
|
69
|
+
const baseEnv = {
|
|
70
|
+
...clonePlainObject(process.env),
|
|
71
|
+
...clonePlainObject(kernel && kernel.envs)
|
|
72
|
+
}
|
|
73
|
+
if (kernel && kernel.bin && typeof kernel.bin.envs === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
return clonePlainObject(kernel.bin.envs(baseEnv))
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
}
|
|
78
|
+
return baseEnv
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createFallbackStateSnapshot(kernel) {
|
|
82
|
+
const env = createRuntimeEnvSnapshot(kernel)
|
|
83
|
+
if (Object.keys(env).length === 0) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
state: 'snapshot',
|
|
88
|
+
id: 'runtime-environment',
|
|
89
|
+
group: 'system',
|
|
90
|
+
env,
|
|
91
|
+
path: kernel && kernel.homedir,
|
|
92
|
+
cmd: 'pinokio environment snapshot',
|
|
93
|
+
done: true,
|
|
94
|
+
ready: true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
24
98
|
function createCurrentLogSnapshot(kernel, version) {
|
|
25
|
-
const
|
|
99
|
+
const liveShells = kernel && kernel.shell && Array.isArray(kernel.shell.shells)
|
|
100
|
+
? kernel.shell.shells
|
|
101
|
+
: []
|
|
102
|
+
const states = liveShells.map((s) => {
|
|
26
103
|
return {
|
|
27
104
|
state: s.state,
|
|
28
105
|
id: s.id,
|
|
@@ -34,6 +111,12 @@ function createCurrentLogSnapshot(kernel, version) {
|
|
|
34
111
|
ready: s.ready,
|
|
35
112
|
}
|
|
36
113
|
})
|
|
114
|
+
if (states.length === 0) {
|
|
115
|
+
const fallbackState = createFallbackStateSnapshot(kernel)
|
|
116
|
+
if (fallbackState) {
|
|
117
|
+
states.push(fallbackState)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
37
120
|
|
|
38
121
|
const info = {
|
|
39
122
|
platform: kernel.platform,
|
|
@@ -104,12 +187,45 @@ function normalizeLogRedactionOverrides(body = {}) {
|
|
|
104
187
|
return overrides
|
|
105
188
|
}
|
|
106
189
|
|
|
107
|
-
|
|
190
|
+
function normalizeLogRedactionExclusions(body = {}) {
|
|
191
|
+
if (!body || !Object.prototype.hasOwnProperty.call(body, 'excluded_paths')) {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
const source = body.excluded_paths
|
|
195
|
+
if (!Array.isArray(source)) {
|
|
196
|
+
throw new Error('excluded_paths must be an array')
|
|
197
|
+
}
|
|
198
|
+
const exclusions = []
|
|
199
|
+
const seen = new Set()
|
|
200
|
+
for (const candidate of source) {
|
|
201
|
+
const relativePath = typeof candidate === 'string' ? candidate.trim() : ''
|
|
202
|
+
if (!isTopLevelRedactableLogPath(relativePath)) {
|
|
203
|
+
throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
|
|
204
|
+
}
|
|
205
|
+
if (seen.has(relativePath)) {
|
|
206
|
+
throw new Error(`Duplicate excluded path: ${relativePath}`)
|
|
207
|
+
}
|
|
208
|
+
seen.add(relativePath)
|
|
209
|
+
exclusions.push(relativePath)
|
|
210
|
+
}
|
|
211
|
+
return exclusions
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function assertCompleteLogRedactionOverrides(exportRoot, overrides = [], exclusions = [], options = {}) {
|
|
108
215
|
const overridePaths = new Set((Array.isArray(overrides) ? overrides : []).map((override) => override && override.path).filter(Boolean))
|
|
216
|
+
const excludedPaths = new Set(Array.isArray(exclusions) ? exclusions.filter(Boolean) : [])
|
|
217
|
+
for (const excludedPath of excludedPaths) {
|
|
218
|
+
if (overridePaths.has(excludedPath)) {
|
|
219
|
+
throw new Error(`File cannot be both redacted and excluded: ${excludedPath}`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (options && options.requireComplete === false) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
109
225
|
const missing = []
|
|
110
226
|
const entries = await fs.promises.readdir(exportRoot, { withFileTypes: true })
|
|
111
227
|
for (const entry of entries) {
|
|
112
|
-
if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name)) {
|
|
228
|
+
if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name) && !excludedPaths.has(entry.name)) {
|
|
113
229
|
missing.push(entry.name)
|
|
114
230
|
}
|
|
115
231
|
}
|
|
@@ -119,6 +235,35 @@ async function assertCompleteLogRedactionOverrides(exportRoot, overrides = []) {
|
|
|
119
235
|
}
|
|
120
236
|
}
|
|
121
237
|
|
|
238
|
+
async function applyLogRedactionExclusions(exportRoot, exclusions = []) {
|
|
239
|
+
if (!Array.isArray(exclusions) || exclusions.length === 0) {
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
let removed = 0
|
|
243
|
+
for (const relativePath of exclusions) {
|
|
244
|
+
if (!isTopLevelRedactableLogPath(relativePath)) {
|
|
245
|
+
throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
|
|
246
|
+
}
|
|
247
|
+
const target = path.resolve(exportRoot, relativePath)
|
|
248
|
+
const relative = path.relative(exportRoot, target)
|
|
249
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
|
|
250
|
+
throw new Error(`Invalid excluded path: ${relativePath}`)
|
|
251
|
+
}
|
|
252
|
+
let stats
|
|
253
|
+
try {
|
|
254
|
+
stats = await fs.promises.stat(target)
|
|
255
|
+
} catch (_) {
|
|
256
|
+
throw new Error(`Excluded path target not found: ${relativePath}`)
|
|
257
|
+
}
|
|
258
|
+
if (!stats.isFile()) {
|
|
259
|
+
throw new Error(`Excluded path target is not a file: ${relativePath}`)
|
|
260
|
+
}
|
|
261
|
+
await fs.promises.rm(target, { force: true })
|
|
262
|
+
removed += 1
|
|
263
|
+
}
|
|
264
|
+
return removed
|
|
265
|
+
}
|
|
266
|
+
|
|
122
267
|
async function applyLogRedactionOverrides(exportRoot, overrides = []) {
|
|
123
268
|
if (!Array.isArray(overrides) || overrides.length === 0) {
|
|
124
269
|
return 0
|
|
@@ -215,6 +360,7 @@ function createTopLevelLogFileHandler(server) {
|
|
|
215
360
|
res.status(400).json({ error: 'Only top-level text log files can be read for redaction' })
|
|
216
361
|
return
|
|
217
362
|
}
|
|
363
|
+
const tailLines = normalizeTailLineCount(req.query.tail_lines || req.query.lines)
|
|
218
364
|
let stats
|
|
219
365
|
try {
|
|
220
366
|
stats = await fs.promises.stat(descriptor.absolutePath)
|
|
@@ -226,7 +372,7 @@ function createTopLevelLogFileHandler(server) {
|
|
|
226
372
|
res.status(400).json({ error: 'Path is not a file' })
|
|
227
373
|
return
|
|
228
374
|
}
|
|
229
|
-
if (stats.size > LOG_REDACTION_FILE_MAX_BYTES) {
|
|
375
|
+
if (stats.size > LOG_REDACTION_FILE_MAX_BYTES && !tailLines) {
|
|
230
376
|
res.status(413).json({
|
|
231
377
|
error: 'File is too large to redact in the browser',
|
|
232
378
|
size: stats.size,
|
|
@@ -242,8 +388,14 @@ function createTopLevelLogFileHandler(server) {
|
|
|
242
388
|
}
|
|
243
389
|
} catch (_) {}
|
|
244
390
|
let text
|
|
391
|
+
let tail = null
|
|
245
392
|
try {
|
|
246
|
-
|
|
393
|
+
if (tailLines) {
|
|
394
|
+
tail = await readTailTextFile(descriptor.absolutePath, stats, tailLines)
|
|
395
|
+
text = tail.text
|
|
396
|
+
} else {
|
|
397
|
+
text = await fs.promises.readFile(descriptor.absolutePath, 'utf8')
|
|
398
|
+
}
|
|
247
399
|
} catch (error) {
|
|
248
400
|
res.status(500).json({ error: 'Failed to read file', detail: error.message })
|
|
249
401
|
return
|
|
@@ -254,6 +406,9 @@ function createTopLevelLogFileHandler(server) {
|
|
|
254
406
|
name: path.basename(relativePath),
|
|
255
407
|
size: stats.size,
|
|
256
408
|
modified: stats.mtime,
|
|
409
|
+
tail_lines: tailLines || null,
|
|
410
|
+
truncated: tail ? tail.truncated : false,
|
|
411
|
+
included_lines: tail ? tail.included_lines : null,
|
|
257
412
|
text
|
|
258
413
|
})
|
|
259
414
|
}
|
|
@@ -262,10 +417,13 @@ function createTopLevelLogFileHandler(server) {
|
|
|
262
417
|
module.exports = {
|
|
263
418
|
LOG_REDACTION_FILE_MAX_BYTES,
|
|
264
419
|
isTopLevelRedactableLogPath,
|
|
420
|
+
normalizeTailLineCount,
|
|
265
421
|
createCurrentLogSnapshot,
|
|
266
422
|
writeCurrentLogSnapshot,
|
|
267
423
|
normalizeLogRedactionOverrides,
|
|
424
|
+
normalizeLogRedactionExclusions,
|
|
268
425
|
assertCompleteLogRedactionOverrides,
|
|
426
|
+
applyLogRedactionExclusions,
|
|
269
427
|
applyLogRedactionOverrides,
|
|
270
428
|
createLogRedactionBodyParser,
|
|
271
429
|
createLogRedactionBodyParsers,
|
|
@@ -19,39 +19,6 @@ body.dark .logs-redact-button:focus-visible {
|
|
|
19
19
|
border-color: rgba(96, 165, 250, 0.34);
|
|
20
20
|
background: rgba(96, 165, 250, 0.14);
|
|
21
21
|
}
|
|
22
|
-
.logs-redaction-chip {
|
|
23
|
-
border: 1px solid rgba(28, 96, 186, 0.18);
|
|
24
|
-
border-radius: 999px;
|
|
25
|
-
min-height: 26px;
|
|
26
|
-
padding: 0 9px;
|
|
27
|
-
display: inline-flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
justify-content: center;
|
|
30
|
-
gap: 6px;
|
|
31
|
-
background: rgba(28, 96, 186, 0.055);
|
|
32
|
-
color: #174a8b;
|
|
33
|
-
font-size: 10px;
|
|
34
|
-
line-height: 1;
|
|
35
|
-
font-weight: 760;
|
|
36
|
-
cursor: pointer;
|
|
37
|
-
white-space: nowrap;
|
|
38
|
-
}
|
|
39
|
-
.logs-redaction-chip:hover,
|
|
40
|
-
.logs-redaction-chip:focus-visible {
|
|
41
|
-
border-color: rgba(28, 96, 186, 0.32);
|
|
42
|
-
background: rgba(28, 96, 186, 0.1);
|
|
43
|
-
outline: none;
|
|
44
|
-
}
|
|
45
|
-
body.dark .logs-redaction-chip {
|
|
46
|
-
border-color: rgba(96, 165, 250, 0.2);
|
|
47
|
-
background: rgba(96, 165, 250, 0.08);
|
|
48
|
-
color: #bfdbfe;
|
|
49
|
-
}
|
|
50
|
-
body.dark .logs-redaction-chip:hover,
|
|
51
|
-
body.dark .logs-redaction-chip:focus-visible {
|
|
52
|
-
border-color: rgba(96, 165, 250, 0.32);
|
|
53
|
-
background: rgba(96, 165, 250, 0.13);
|
|
54
|
-
}
|
|
55
22
|
body.dark .logs-report-status.is-error {
|
|
56
23
|
color: #ff8373;
|
|
57
24
|
}
|
|
@@ -81,18 +48,29 @@ body.dark .logs-report-status.is-error {
|
|
|
81
48
|
justify-content: space-between;
|
|
82
49
|
gap: 10px;
|
|
83
50
|
}
|
|
51
|
+
.logs-top-redaction-header-actions {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: flex-end;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
flex: 0 0 auto;
|
|
57
|
+
}
|
|
58
|
+
.logs-top-redaction-header-actions .logs-icon-button.is-busy i {
|
|
59
|
+
animation: spin 1s linear infinite;
|
|
60
|
+
}
|
|
84
61
|
.logs-top-redaction-files {
|
|
85
62
|
display: flex;
|
|
86
63
|
flex-direction: column;
|
|
87
64
|
gap: 6px;
|
|
88
65
|
margin-top: 10px;
|
|
89
|
-
max-height:
|
|
66
|
+
max-height: 244px;
|
|
90
67
|
overflow: auto;
|
|
91
68
|
}
|
|
92
69
|
.logs-top-redaction-file {
|
|
93
70
|
display: grid;
|
|
94
|
-
grid-template-columns: minmax(0, 1fr);
|
|
95
|
-
align-items:
|
|
71
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
72
|
+
align-items: start;
|
|
73
|
+
gap: 8px;
|
|
96
74
|
border: 1px solid transparent;
|
|
97
75
|
border-radius: 7px;
|
|
98
76
|
padding: 7px 8px;
|
|
@@ -101,6 +79,10 @@ body.dark .logs-report-status.is-error {
|
|
|
101
79
|
cursor: pointer;
|
|
102
80
|
text-align: left;
|
|
103
81
|
}
|
|
82
|
+
.logs-top-redaction-file.is-warning {
|
|
83
|
+
border-color: rgba(186, 133, 28, 0.2);
|
|
84
|
+
background: rgba(186, 133, 28, 0.055);
|
|
85
|
+
}
|
|
104
86
|
.logs-top-redaction-file:hover,
|
|
105
87
|
.logs-top-redaction-file.is-active {
|
|
106
88
|
border-color: var(--logs-border-strong);
|
|
@@ -133,10 +115,76 @@ body.dark .logs-top-redaction-file {
|
|
|
133
115
|
font-size: 10px;
|
|
134
116
|
line-height: 1.2;
|
|
135
117
|
}
|
|
136
|
-
.logs-top-redaction-
|
|
118
|
+
.logs-top-redaction-file-badge {
|
|
119
|
+
display: inline-flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
border-radius: 999px;
|
|
122
|
+
padding: 2px 6px;
|
|
123
|
+
background: rgba(186, 133, 28, 0.11);
|
|
124
|
+
color: #7a5413;
|
|
125
|
+
font-size: 9px;
|
|
126
|
+
line-height: 1.2;
|
|
127
|
+
font-weight: 800;
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
}
|
|
130
|
+
body.dark .logs-top-redaction-file-badge {
|
|
131
|
+
background: rgba(245, 189, 75, 0.12);
|
|
132
|
+
color: #e1b261;
|
|
133
|
+
}
|
|
134
|
+
.logs-top-redaction-mode {
|
|
135
|
+
grid-column: 1 / -1;
|
|
136
|
+
width: 100%;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
min-height: 26px;
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
border: 1px solid var(--logs-border);
|
|
141
|
+
border-radius: 6px;
|
|
142
|
+
padding: 0 7px;
|
|
143
|
+
background: var(--logs-bg);
|
|
144
|
+
color: var(--logs-text);
|
|
145
|
+
font-size: 10px;
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
}
|
|
148
|
+
.logs-top-redaction-mode:focus-visible {
|
|
149
|
+
border-color: var(--logs-border-strong);
|
|
150
|
+
outline: none;
|
|
151
|
+
}
|
|
152
|
+
.logs-top-redaction-mode:disabled {
|
|
153
|
+
opacity: 0.7;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
156
|
+
.logs-top-redaction-footer {
|
|
137
157
|
flex: 0 0 auto;
|
|
158
|
+
min-width: 0;
|
|
138
159
|
border-top: 1px solid var(--logs-border);
|
|
139
160
|
padding: 10px 12px;
|
|
161
|
+
display: block;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
}
|
|
164
|
+
.logs-top-redaction-actions {
|
|
165
|
+
display: grid;
|
|
166
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
167
|
+
gap: 6px;
|
|
168
|
+
align-items: stretch;
|
|
169
|
+
min-width: 0;
|
|
170
|
+
}
|
|
171
|
+
.logs-top-redaction-actions .logs-primary-button,
|
|
172
|
+
.logs-top-redaction-actions .logs-secondary-button,
|
|
173
|
+
.logs-top-redaction-actions .logs-flat-button {
|
|
174
|
+
box-sizing: border-box;
|
|
175
|
+
width: 100%;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
max-width: 100%;
|
|
178
|
+
}
|
|
179
|
+
.logs-top-redaction-actions #logs-download-archive {
|
|
180
|
+
grid-column: 1 / -1;
|
|
181
|
+
}
|
|
182
|
+
.logs-top-redaction-footer .logs-zip-status {
|
|
183
|
+
display: block;
|
|
184
|
+
width: 100%;
|
|
185
|
+
box-sizing: border-box;
|
|
186
|
+
margin-top: 8px;
|
|
187
|
+
text-align: left;
|
|
140
188
|
color: var(--logs-muted);
|
|
141
189
|
font-size: 11px;
|
|
142
190
|
line-height: 1.35;
|