pinokiod 8.0.45 → 8.0.47
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/kernel/gpu/amd_gfx_targets.json +3 -1
- package/kernel/gpu/amd_pci.js +184 -0
- package/kernel/gpu/amd_pci_targets.json +3243 -0
- package/kernel/sysinfo.js +6 -1
- package/kernel/vault/constants.js +9 -2
- package/kernel/vault/hash_worker.js +13 -1
- package/kernel/vault/index.js +83 -23
- package/kernel/vault/sweeper.js +15 -23
- package/package.json +2 -1
- package/script/update-amd-gfx-targets.js +289 -13
- package/server/lib/app_log_report.js +45 -3
- package/server/lib/log_redaction.js +26 -14
- package/server/public/nav.js +1 -1
- package/server/public/style.css +0 -1
- package/server/public/vault.css +6 -1
- package/server/public/vault.js +62 -32
- package/server/views/partials/vault_workspace.ejs +1 -0
- package/server/views/vault.ejs +2 -0
- package/test/amd-gfx-generator.test.js +75 -0
- package/test/amd-gpu-target.test.js +108 -10
- package/test/amd-pci-target.test.js +159 -0
- package/test/app-log-report.test.js +67 -0
- package/test/header-collapse.test.js +10 -5
- package/test/sysinfo-gpu-driver.test.js +4 -0
- package/test/vault-engine.test.js +60 -1
- package/test/vault-sweep.test.js +50 -31
- package/test/vault-ui.test.js +145 -14
|
@@ -2,10 +2,12 @@ const fs = require('fs')
|
|
|
2
2
|
const os = require('os')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const Environment = require('../../kernel/environment')
|
|
5
|
+
const { createCurrentSystemSnapshot } = require('./log_redaction')
|
|
5
6
|
|
|
6
7
|
const DEFAULT_TAIL_LINES = 800
|
|
7
8
|
const MAX_SECTION_CHARS = 120000
|
|
8
9
|
const SENSITIVE_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API[_-]?KEY|APIKEY|CREDENTIAL|COOKIE|SESSION|AUTH|PRIVATE[_-]?KEY)/i
|
|
10
|
+
const SENSITIVE_FIELD_KEY = /(?:^|[_-])(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API[_-]?KEY|APIKEY|CREDENTIAL|COOKIE|SESSION|AUTH|AUTHORIZATION|PRIVATE[_-]?KEY)(?:$|[_-])/i
|
|
9
11
|
|
|
10
12
|
const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
11
13
|
|
|
@@ -52,6 +54,13 @@ class AppLogReportService {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
let systemSpec = this.buildSystemSpec()
|
|
58
|
+
if (redact) {
|
|
59
|
+
const redacted = this.redactStructuredValue(systemSpec, { appRoot, workspaceRoot })
|
|
60
|
+
systemSpec = redacted.value
|
|
61
|
+
this.mergeCounts(totals, redacted.counts)
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
const metadata = {
|
|
56
65
|
app_id: appId,
|
|
57
66
|
title: status.title || appId,
|
|
@@ -62,7 +71,7 @@ class AppLogReportService {
|
|
|
62
71
|
arch: os.arch(),
|
|
63
72
|
node: process.version,
|
|
64
73
|
tail_count: tailLines,
|
|
65
|
-
system_spec:
|
|
74
|
+
system_spec: systemSpec,
|
|
66
75
|
redaction_mode: redact ? 'server_deterministic' : 'none',
|
|
67
76
|
latest_session: sessionIndex.latest_session,
|
|
68
77
|
session: selectedSession,
|
|
@@ -305,6 +314,27 @@ class AppLogReportService {
|
|
|
305
314
|
}
|
|
306
315
|
}
|
|
307
316
|
|
|
317
|
+
redactStructuredValue(input, context = {}) {
|
|
318
|
+
const counts = {}
|
|
319
|
+
const json = JSON.stringify(input, (key, value) => {
|
|
320
|
+
const normalizedKey = String(key).replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
321
|
+
if (key && SENSITIVE_FIELD_KEY.test(normalizedKey) && value !== undefined && value !== null && value !== '') {
|
|
322
|
+
counts.env_secrets = (counts.env_secrets || 0) + 1
|
|
323
|
+
return '[REDACTED_SECRET]'
|
|
324
|
+
}
|
|
325
|
+
if (typeof value === 'string') {
|
|
326
|
+
const redacted = this.redactText(value, context)
|
|
327
|
+
this.mergeCounts(counts, redacted.counts)
|
|
328
|
+
return redacted.text
|
|
329
|
+
}
|
|
330
|
+
return value
|
|
331
|
+
})
|
|
332
|
+
return {
|
|
333
|
+
value: JSON.parse(json),
|
|
334
|
+
counts
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
308
338
|
renderMarkdown(metadata, sections, redactions) {
|
|
309
339
|
const lines = [
|
|
310
340
|
'# Issue Report',
|
|
@@ -418,9 +448,11 @@ class AppLogReportService {
|
|
|
418
448
|
buildSystemSpec() {
|
|
419
449
|
const kernel = this.kernel || {}
|
|
420
450
|
const info = kernel.sysinfo && typeof kernel.sysinfo === 'object' ? kernel.sysinfo : {}
|
|
421
|
-
|
|
451
|
+
const version = this.readPinokioVersion()
|
|
452
|
+
const snapshot = createCurrentSystemSnapshot(kernel, version)
|
|
453
|
+
const spec = this.compactObject({
|
|
422
454
|
pinokio: {
|
|
423
|
-
version
|
|
455
|
+
version,
|
|
424
456
|
node: process.version,
|
|
425
457
|
platform: kernel.platform || process.platform,
|
|
426
458
|
arch: kernel.arch || process.arch
|
|
@@ -438,6 +470,16 @@ class AppLogReportService {
|
|
|
438
470
|
gpus: this.sanitizeGpus(info.gpus),
|
|
439
471
|
graphics: this.sanitizeGraphics(info.graphics)
|
|
440
472
|
})
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
...spec,
|
|
476
|
+
hardware: {
|
|
477
|
+
...spec.hardware,
|
|
478
|
+
gpu_driver: snapshot.gpu_driver ?? null,
|
|
479
|
+
gpu_target: snapshot.gpu_target ?? null
|
|
480
|
+
},
|
|
481
|
+
diagnostics: snapshot
|
|
482
|
+
}
|
|
441
483
|
}
|
|
442
484
|
|
|
443
485
|
sanitizeOsInfo(value) {
|
|
@@ -95,6 +95,28 @@ function createFallbackStateSnapshot(kernel) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function createCurrentSystemSnapshot(kernel, version) {
|
|
99
|
+
kernel = kernel || {}
|
|
100
|
+
return {
|
|
101
|
+
platform: kernel.platform,
|
|
102
|
+
arch: kernel.arch,
|
|
103
|
+
running: kernel.api && kernel.api.running,
|
|
104
|
+
home: kernel.homedir,
|
|
105
|
+
vars: kernel.vars,
|
|
106
|
+
memory: kernel.memory,
|
|
107
|
+
procs: kernel.procs,
|
|
108
|
+
gpu: kernel.gpu,
|
|
109
|
+
gpus: kernel.gpus,
|
|
110
|
+
gpu_model: kernel.gpu_model,
|
|
111
|
+
gpu_driver: kernel.gpu_driver,
|
|
112
|
+
gpu_target: kernel.gpu_target,
|
|
113
|
+
ram: kernel.ram,
|
|
114
|
+
vram: kernel.vram,
|
|
115
|
+
version,
|
|
116
|
+
...kernel.sysinfo
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
function createCurrentLogSnapshot(kernel, version) {
|
|
99
121
|
const liveShells = kernel && kernel.shell && Array.isArray(kernel.shell.shells)
|
|
100
122
|
? kernel.shell.shells
|
|
@@ -118,21 +140,10 @@ function createCurrentLogSnapshot(kernel, version) {
|
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
running: kernel.api.running,
|
|
125
|
-
home: kernel.homedir,
|
|
126
|
-
vars: kernel.vars,
|
|
127
|
-
memory: kernel.memory,
|
|
128
|
-
procs: kernel.procs,
|
|
129
|
-
gpu: kernel.gpu,
|
|
130
|
-
gpus: kernel.gpus,
|
|
131
|
-
version,
|
|
132
|
-
...kernel.sysinfo
|
|
143
|
+
return {
|
|
144
|
+
info: createCurrentSystemSnapshot(kernel, version),
|
|
145
|
+
states
|
|
133
146
|
}
|
|
134
|
-
|
|
135
|
-
return { info, states }
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
async function writeCurrentLogSnapshot(kernel, version) {
|
|
@@ -418,6 +429,7 @@ module.exports = {
|
|
|
418
429
|
LOG_REDACTION_FILE_MAX_BYTES,
|
|
419
430
|
isTopLevelRedactableLogPath,
|
|
420
431
|
normalizeTailLineCount,
|
|
432
|
+
createCurrentSystemSnapshot,
|
|
421
433
|
createCurrentLogSnapshot,
|
|
422
434
|
writeCurrentLogSnapshot,
|
|
423
435
|
normalizeLogRedactionOverrides,
|
package/server/public/nav.js
CHANGED
|
@@ -6,7 +6,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
6
6
|
|
|
7
7
|
const newWindowButton = document.querySelector("#new-window");
|
|
8
8
|
const agent = document.body.getAttribute("data-agent");
|
|
9
|
-
if (newWindowButton
|
|
9
|
+
if (newWindowButton) {
|
|
10
10
|
newWindowButton.addEventListener("click", (event) => {
|
|
11
11
|
if (agent === "electron") {
|
|
12
12
|
window.open("/", "_blank", "pinokio");
|
package/server/public/style.css
CHANGED
|
@@ -4536,7 +4536,6 @@ aside .qr {
|
|
|
4536
4536
|
body.main-sidebar-page > header.navheader #inspector,
|
|
4537
4537
|
body.main-sidebar-page > header.navheader #mobile-link-button,
|
|
4538
4538
|
body.main-sidebar-page > header.navheader .urlbar,
|
|
4539
|
-
body.main-sidebar-page > header.navheader #close-window,
|
|
4540
4539
|
body.main-sidebar-page > header.navheader .runner,
|
|
4541
4540
|
body.main-sidebar-page > header.navheader h1 > .path,
|
|
4542
4541
|
body.main-sidebar-page > header.navheader h1 > .nav-button,
|
package/server/public/vault.css
CHANGED
|
@@ -29,7 +29,10 @@ body.dark.vault-page {
|
|
|
29
29
|
min-height: 0;
|
|
30
30
|
overflow: hidden;
|
|
31
31
|
}
|
|
32
|
-
.vault-embed-main .vault-shell {
|
|
32
|
+
.vault-embed-main .vault-shell {
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
}
|
|
33
36
|
body.vault-page .task-container {
|
|
34
37
|
display: flex;
|
|
35
38
|
min-height: 0;
|
|
@@ -924,6 +927,8 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
|
|
|
924
927
|
}
|
|
925
928
|
@media (max-width: 820px) {
|
|
926
929
|
body.vault-page .task-container { display: block; overflow-y: auto; }
|
|
930
|
+
.vault-embed-main { overflow-y: auto; }
|
|
931
|
+
.vault-embed-main .vault-shell { height: auto; }
|
|
927
932
|
.vault-shell,
|
|
928
933
|
.vault-shell .task-shell-body,
|
|
929
934
|
.vault-body { height: auto; overflow: visible; }
|
package/server/public/vault.js
CHANGED
|
@@ -42,6 +42,7 @@ const COPY = {
|
|
|
42
42
|
scan_app: "Scan this app",
|
|
43
43
|
scan_again: "Scan again",
|
|
44
44
|
scanning: "Scanning…",
|
|
45
|
+
minimum_file_size: "Minimum file size to scan",
|
|
45
46
|
scanning_elsewhere: "Another location is being scanned",
|
|
46
47
|
scanning_elsewhere_hint: "This app can be scanned when the current scan finishes.",
|
|
47
48
|
vault_options: "Save space options",
|
|
@@ -56,15 +57,13 @@ const COPY = {
|
|
|
56
57
|
scan_queued: "Waiting to start scan",
|
|
57
58
|
scan_analyzing: "Analyzing large files",
|
|
58
59
|
scan_finishing: "Finishing scan",
|
|
59
|
-
scan_estimate_help: "Overall progress combines file scanning and large-file analysis",
|
|
60
60
|
scan_counting_help: "The first scan cannot know its total until this pass finishes",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
scan_hash_progress_help: "File discovery is complete; the remaining large-file count is exact",
|
|
61
|
+
scan_file_progress_help: "Based on the exact current file total",
|
|
62
|
+
scan_hash_progress_help: "Based on the exact large-file count and current-file bytes",
|
|
64
63
|
scan_finishing_help: "File analysis is complete; deduplication records are being verified",
|
|
65
|
-
about_percent: "About {percent} percent.",
|
|
66
64
|
exact_percent: "{percent} percent.",
|
|
67
65
|
scan_checked: "{done} of {total} large files checked",
|
|
66
|
+
scan_file_bytes: "{done} of {total}",
|
|
68
67
|
scan_files_checked: "{done} of {total} files checked",
|
|
69
68
|
scan_folders: "folders checked",
|
|
70
69
|
scan_files: "files checked",
|
|
@@ -170,7 +169,7 @@ const COPY = {
|
|
|
170
169
|
no_activity_hint: "Scans and actions will be recorded here.",
|
|
171
170
|
view_all: "View all files",
|
|
172
171
|
show_all_locations: "Show all locations",
|
|
173
|
-
tracked_note: "Only files
|
|
172
|
+
tracked_note: "Only files {size} and larger appear here. Files keep their current locations.",
|
|
174
173
|
duplicate_note: "Only files waiting for review are shown.",
|
|
175
174
|
reclaimable_note: "These copies are no longer used by any configured location. Deleting them frees disk space.",
|
|
176
175
|
converted: "Deduplicated",
|
|
@@ -199,6 +198,11 @@ const statusUrl = (progress = false) => {
|
|
|
199
198
|
return `/info/dedup${suffix ? `?${suffix}` : ""}`
|
|
200
199
|
}
|
|
201
200
|
const reviewedScanKey = `pinokio:vault:reviewed-scan:${SCOPE_ID || "global"}`
|
|
201
|
+
const candidateSizeKey = "pinokio:vault:candidate-size"
|
|
202
|
+
const candidateSizeBase = document.body.dataset.platform === "win32" ? 1024 : 1000
|
|
203
|
+
const candidateSizeOptions = [10, 50, 100, 500]
|
|
204
|
+
.map((value) => value * candidateSizeBase ** 2)
|
|
205
|
+
.concat(candidateSizeBase ** 3)
|
|
202
206
|
|
|
203
207
|
const state = {
|
|
204
208
|
data: null,
|
|
@@ -226,6 +230,10 @@ const esc = (value) => String(value == null ? "" : value).replace(/[&<>"']/g, (c
|
|
|
226
230
|
}[char]))
|
|
227
231
|
const attr = esc
|
|
228
232
|
const fmt = window.PinokioFormatStorageSize
|
|
233
|
+
const candidateSize = () => {
|
|
234
|
+
const value = Number(el("vault-candidate-size").value)
|
|
235
|
+
return candidateSizeOptions.includes(value) ? value : candidateSizeOptions[2]
|
|
236
|
+
}
|
|
229
237
|
const countLabel = (count, singular = COPY.file, plural = COPY.files) => `${count} ${count === 1 ? singular : plural}`
|
|
230
238
|
const basename = (value) => String(value || "").split(/[\\/]/).filter(Boolean).pop() || ""
|
|
231
239
|
const dirname = (value) => {
|
|
@@ -877,6 +885,7 @@ const renderOverview = () => {
|
|
|
877
885
|
? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}`
|
|
878
886
|
: `<i class="fa-solid fa-rotate"></i>${esc(last ? COPY.scan_again : (IS_APP_MODE ? COPY.scan_app : COPY.scan))}`
|
|
879
887
|
el("btn-scan").disabled = activeScan
|
|
888
|
+
el("vault-candidate-size").disabled = activeScan
|
|
880
889
|
const optionsButton = el("btn-vault-options")
|
|
881
890
|
const repairTitle = el("vault-repair-title")
|
|
882
891
|
const repairDescription = el("vault-repair-description")
|
|
@@ -903,50 +912,48 @@ const renderOverview = () => {
|
|
|
903
912
|
const verifying = scanPhase === "verifying"
|
|
904
913
|
const hashTotal = scan.hash_total || 0
|
|
905
914
|
const hashDone = Math.min(Math.max(0, hashTotal - (scan.queued || 0)), hashTotal)
|
|
906
|
-
const
|
|
915
|
+
const currentFileSize = Math.max(0, Number(scan.current_file_size) || 0)
|
|
916
|
+
const currentFileBytes = Math.min(currentFileSize, Math.max(0, Number(scan.current_file_bytes) || 0))
|
|
917
|
+
const currentFileRatio = currentFileSize ? currentFileBytes / currentFileSize : 0
|
|
918
|
+
const hashRatio = hashTotal ? Math.min(1, (hashDone + currentFileRatio) / hashTotal) : 1
|
|
907
919
|
const scanSource = scan.scope_id ? sourceById(scan.scope_id) : null
|
|
908
920
|
const scanProgressLabel = scanSource
|
|
909
921
|
? COPY.scan_location.replace("{location}", scanSource.label)
|
|
910
922
|
: COPY.scan_progress
|
|
911
923
|
const phase = queued ? COPY.scan_queued : counting ? COPY.scan_counting : walking ? scanProgressLabel : verifying ? COPY.scan_finishing : COPY.scan_analyzing
|
|
912
|
-
const rawCountWeight = Number.isFinite(scan.estimated_count_weight) ? scan.estimated_count_weight : 0.4
|
|
913
|
-
const countWeight = Math.max(0, Math.min(0.98, rawCountWeight))
|
|
914
|
-
const rawWalkWeight = Number.isFinite(scan.estimated_walk_weight) ? scan.estimated_walk_weight : 0.5
|
|
915
|
-
const walkWeight = Math.max(0, Math.min(0.98 - countWeight, rawWalkWeight))
|
|
916
|
-
const analysisWeight = Math.max(0, 1 - countWeight - walkWeight)
|
|
917
|
-
const countEstimate = Number.isFinite(scan.count_estimate_files) && scan.count_estimate_files > 0
|
|
918
|
-
? scan.count_estimate_files
|
|
919
|
-
: null
|
|
920
|
-
const countRatio = countEstimate === null ? null : Math.min(1, (scan.counted_files || 0) / countEstimate)
|
|
921
924
|
const totalFiles = Number.isFinite(scan.total_files) ? scan.total_files : null
|
|
922
925
|
const details = counting
|
|
923
926
|
? [`${scan.counted_dirs || 0} ${COPY.scan_folders_found}`, `${scan.counted_files || 0} ${COPY.scan_files_found}`]
|
|
924
927
|
: [`${scan.dirs || 0} ${COPY.scan_folders}`, totalFiles === null
|
|
925
928
|
? `${scan.files || 0} ${COPY.scan_files}`
|
|
926
929
|
: COPY.scan_files_checked.replace("{done}", scan.files || 0).replace("{total}", totalFiles), fmt(scan.bytes_total || 0)]
|
|
927
|
-
if (scan.current_file)
|
|
930
|
+
if (scan.current_file) {
|
|
931
|
+
const fileProgress = currentFileSize
|
|
932
|
+
? ` (${COPY.scan_file_bytes.replace("{done}", fmt(currentFileBytes)).replace("{total}", fmt(currentFileSize))})`
|
|
933
|
+
: ""
|
|
934
|
+
details.push(`${COPY.analyzing} ${scan.current_file}${fileProgress}`)
|
|
935
|
+
}
|
|
928
936
|
if (walking && scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
|
|
929
937
|
if (!walking && hashTotal) {
|
|
930
938
|
details.push(COPY.scan_checked.replace("{done}", hashDone).replace("{total}", hashTotal))
|
|
931
939
|
}
|
|
932
940
|
let percent = ""
|
|
933
941
|
let progress
|
|
934
|
-
if (queued ||
|
|
935
|
-
const
|
|
942
|
+
if (queued || counting || verifying || (walking && totalFiles === null)) {
|
|
943
|
+
const progressHelp = verifying
|
|
944
|
+
? COPY.scan_finishing_help
|
|
945
|
+
: counting ? COPY.scan_counting_help : phase
|
|
946
|
+
const ariaText = `${details.join(" · ")}. ${progressHelp}.`
|
|
936
947
|
progress = { determinate: false, ariaText }
|
|
937
948
|
} else {
|
|
938
|
-
const progressRatio =
|
|
939
|
-
?
|
|
940
|
-
:
|
|
941
|
-
? 0.99
|
|
942
|
-
: walking
|
|
943
|
-
? countWeight + ((totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1) * walkWeight)
|
|
944
|
-
: Math.min(0.98, countWeight + walkWeight + (analysisWeight * hashRatio))
|
|
949
|
+
const progressRatio = walking
|
|
950
|
+
? (totalFiles ? Math.min(1, (scan.files || 0) / totalFiles) : 1)
|
|
951
|
+
: hashRatio
|
|
945
952
|
const boundedProgress = Math.max(0, Math.min(1, progressRatio))
|
|
946
953
|
const progressValue = Math.round(boundedProgress * 1000) / 10
|
|
947
|
-
const progressHelp =
|
|
948
|
-
const progressText = COPY.
|
|
949
|
-
percent =
|
|
954
|
+
const progressHelp = walking ? COPY.scan_file_progress_help : COPY.scan_hash_progress_help
|
|
955
|
+
const progressText = COPY.exact_percent.replace("{percent}", progressValue)
|
|
956
|
+
percent = `${progressValue}%`
|
|
950
957
|
progress = { determinate: true, value: progressValue, ratio: boundedProgress, help: progressHelp, text: progressText }
|
|
951
958
|
}
|
|
952
959
|
if (!scanState.querySelector(".vault-progress-track")) {
|
|
@@ -1118,7 +1125,7 @@ const render = () => {
|
|
|
1118
1125
|
el("vault-explorer").style.display = "none"
|
|
1119
1126
|
return
|
|
1120
1127
|
}
|
|
1121
|
-
el("vault-explorer").style.display = "
|
|
1128
|
+
el("vault-explorer").style.display = ""
|
|
1122
1129
|
const items = buildItems()
|
|
1123
1130
|
renderViews(items)
|
|
1124
1131
|
renderLocations(items)
|
|
@@ -1137,7 +1144,9 @@ const render = () => {
|
|
|
1137
1144
|
} else {
|
|
1138
1145
|
el("vault-pane-footer").textContent = state.view === "duplicates"
|
|
1139
1146
|
? COPY.duplicate_note
|
|
1140
|
-
: state.view === "reclaimable"
|
|
1147
|
+
: state.view === "reclaimable"
|
|
1148
|
+
? COPY.reclaimable_note
|
|
1149
|
+
: COPY.tracked_note.replace("{size}", fmt(candidateSize()))
|
|
1141
1150
|
}
|
|
1142
1151
|
}
|
|
1143
1152
|
|
|
@@ -1472,7 +1481,11 @@ document.addEventListener("click", async (event) => {
|
|
|
1472
1481
|
state.scanProblemsOpen = false
|
|
1473
1482
|
state.feedback = null
|
|
1474
1483
|
try {
|
|
1475
|
-
const result = await post({
|
|
1484
|
+
const result = await post({
|
|
1485
|
+
action: "scan",
|
|
1486
|
+
scope_id: SCOPE_ID,
|
|
1487
|
+
candidate_size: candidateSize()
|
|
1488
|
+
})
|
|
1476
1489
|
if (result.error) throw new Error(result.error)
|
|
1477
1490
|
await refresh()
|
|
1478
1491
|
} catch (error) {
|
|
@@ -1588,12 +1601,29 @@ document.addEventListener("input", (event) => {
|
|
|
1588
1601
|
renderActionProgress()
|
|
1589
1602
|
})
|
|
1590
1603
|
document.addEventListener("change", (event) => {
|
|
1604
|
+
if (event.target.id === "vault-candidate-size") {
|
|
1605
|
+
try { localStorage.setItem(candidateSizeKey, String(candidateSize())) } catch (error) {}
|
|
1606
|
+
render()
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1591
1609
|
if (event.target.id !== "vault-status-filter") return
|
|
1592
1610
|
state.statusFilter = event.target.value
|
|
1593
1611
|
render()
|
|
1594
1612
|
})
|
|
1595
1613
|
|
|
1596
1614
|
el("btn-scan").textContent = COPY.scan
|
|
1615
|
+
const candidateSizeSelect = el("vault-candidate-size")
|
|
1616
|
+
candidateSizeSelect.setAttribute("aria-label", COPY.minimum_file_size)
|
|
1617
|
+
candidateSizeSelect.innerHTML = candidateSizeOptions
|
|
1618
|
+
.map((size) => `<option value="${size}">${fmt(size)}+</option>`)
|
|
1619
|
+
.join("")
|
|
1620
|
+
candidateSizeSelect.value = String(candidateSizeOptions[2])
|
|
1621
|
+
try {
|
|
1622
|
+
const storedCandidateSize = Number(localStorage.getItem(candidateSizeKey))
|
|
1623
|
+
if (candidateSizeOptions.includes(storedCandidateSize)) {
|
|
1624
|
+
candidateSizeSelect.value = String(storedCandidateSize)
|
|
1625
|
+
}
|
|
1626
|
+
} catch (error) {}
|
|
1597
1627
|
const addSourceButton = el("btn-add-source")
|
|
1598
1628
|
if (addSourceButton) {
|
|
1599
1629
|
addSourceButton.setAttribute("aria-label", COPY.add_external_folder)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<section class='vault-overview'>
|
|
5
5
|
<div class='vault-metrics' id='vault-metrics'></div>
|
|
6
6
|
<div class='vault-overview-actions'>
|
|
7
|
+
<select class='vault-select' id='vault-candidate-size'></select>
|
|
7
8
|
<button class='vault-button' id='btn-scan' type='button'></button>
|
|
8
9
|
<% if (!appMode) { %>
|
|
9
10
|
<details class='vault-advanced' id='vault-advanced'>
|
package/server/views/vault.ejs
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
<%- include('partials/main_sidebar', { selected: 'vault' }) %>
|
|
24
24
|
</main>
|
|
25
25
|
<script src="/Socket.js"></script>
|
|
26
|
+
<script src="/common.js"></script>
|
|
27
|
+
<script src="/nav.js" data-header-collapse-only></script>
|
|
26
28
|
<script src="/storage-size.js"></script>
|
|
27
29
|
<script src="/vault.js"></script>
|
|
28
30
|
</body>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const generator = require('../script/update-amd-gfx-targets')
|
|
5
|
+
|
|
6
|
+
test('AMD generator joins LLVM aliases, Mesa families, and PCI IDs', () => {
|
|
7
|
+
const llvm = `
|
|
8
|
+
def : ProcessorModel<"gfx803", Model, FeatureISAVersion8_0_3.Features>;
|
|
9
|
+
def : ProcessorModel<"polaris10", Model, FeatureISAVersion8_0_3.Features>;
|
|
10
|
+
`
|
|
11
|
+
const families = `
|
|
12
|
+
const char *ac_get_llvm_processor_name(enum radeon_family family) {
|
|
13
|
+
switch (family) {
|
|
14
|
+
case CHIP_POLARIS10:
|
|
15
|
+
return "polaris10";
|
|
16
|
+
case CHIP_NAVI23:
|
|
17
|
+
return "gfx1032";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const char *ac_get_ip_type_string(void) {}
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
const processors = generator.parseLlvmProcessorTargets(llvm)
|
|
24
|
+
const familyTargets = generator.parseMesaFamilyTargets(families, processors)
|
|
25
|
+
const entries = generator.parseMesaPciTargets(
|
|
26
|
+
'CHIPSET(0x67DF, POLARIS10)\n',
|
|
27
|
+
familyTargets
|
|
28
|
+
)
|
|
29
|
+
generator.parseLinuxPciTargets(
|
|
30
|
+
'{0x1002, 0x73FF, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CHIP_DIMGREY_CAVEFISH},',
|
|
31
|
+
familyTargets,
|
|
32
|
+
{ DIMGREY_CAVEFISH: 'NAVI23' },
|
|
33
|
+
entries
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert.equal(familyTargets.POLARIS10, 'gfx803')
|
|
37
|
+
assert.equal(entries['1002:67df'].target, 'gfx803')
|
|
38
|
+
assert.equal(entries['1002:73ff'].target, 'gfx1032')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('AMD generator derives former Mesa family aliases from pinned comments', () => {
|
|
42
|
+
assert.deepEqual(generator.parseMesaFamilyAliases(`
|
|
43
|
+
CHIP_NAVI21, /* formerly "Sienna Cichlid" */
|
|
44
|
+
CHIP_NAVI23, /* formerly "Dimgrey Cavefish" */
|
|
45
|
+
`), {
|
|
46
|
+
SIENNA_CICHLID: 'NAVI21',
|
|
47
|
+
DIMGREY_CAVEFISH: 'NAVI23'
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('AMD generator rejects conflicting PCI family targets', () => {
|
|
52
|
+
assert.throws(() => generator.parseLinuxPciTargets(
|
|
53
|
+
[
|
|
54
|
+
'{0x1002, 0x73FF, 0, 0, 0, 0, CHIP_NAVI21},',
|
|
55
|
+
'{0x1002, 0x73FF, 0, 0, 0, 0, CHIP_NAVI23},'
|
|
56
|
+
].join('\n'),
|
|
57
|
+
{ NAVI21: 'gfx1030', NAVI23: 'gfx1032' },
|
|
58
|
+
{}
|
|
59
|
+
), /Conflicting targets/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('AMD generator keeps exact libdrm revisions without promoting model names', () => {
|
|
63
|
+
const pciTargets = {}
|
|
64
|
+
const revisions = generator.parseLibdrmRevisionTargets(
|
|
65
|
+
'744c, c8, AMD Radeon RX 7900 XTX\n',
|
|
66
|
+
{ 'radeon rx 7900 xtx': 'gfx1100' },
|
|
67
|
+
pciTargets
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(revisions['1002:744c:c8'], {
|
|
71
|
+
target: 'gfx1100',
|
|
72
|
+
model: 'AMD Radeon RX 7900 XTX'
|
|
73
|
+
})
|
|
74
|
+
assert.equal(pciTargets['1002:744c'], undefined)
|
|
75
|
+
})
|
|
@@ -6,17 +6,19 @@ const test = require('node:test')
|
|
|
6
6
|
const system = require('systeminformation')
|
|
7
7
|
|
|
8
8
|
const amd = require('../kernel/gpu/amd')
|
|
9
|
+
const amdPci = require('../kernel/gpu/amd_pci')
|
|
9
10
|
const gfxTargets = require('../kernel/gpu/amd_gfx_targets.json')
|
|
11
|
+
const pciTargets = require('../kernel/gpu/amd_pci_targets.json')
|
|
10
12
|
const packageJson = require('../package.json')
|
|
11
13
|
const Sysinfo = require('../kernel/sysinfo')
|
|
12
14
|
|
|
13
15
|
const root = path.join(__dirname, '..')
|
|
14
16
|
|
|
15
17
|
test('AMD gfx generated data is checked in and self describing', () => {
|
|
16
|
-
assert.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
18
|
+
assert.match(gfxTargets.source, new RegExp(gfxTargets.source_revision))
|
|
19
|
+
assert.doesNotMatch(gfxTargets.source, /\/develop\//)
|
|
20
|
+
assert.match(gfxTargets.source_revision, /^[0-9a-f]{40}$/)
|
|
21
|
+
assert.match(gfxTargets.source_sha256, /^[0-9a-f]{64}$/)
|
|
20
22
|
assert.equal(gfxTargets.generated_by, 'script/update-amd-gfx-targets.js')
|
|
21
23
|
|
|
22
24
|
assert.equal(gfxTargets.entries['radeon rx 6800 xt'], 'gfx1030')
|
|
@@ -24,8 +26,32 @@ test('AMD gfx generated data is checked in and self describing', () => {
|
|
|
24
26
|
assert.equal(gfxTargets.entries.mi210, 'gfx90a')
|
|
25
27
|
})
|
|
26
28
|
|
|
29
|
+
test('AMD PCI generated data is pinned, auditable, and covers mobile and RDNA1 GPUs', () => {
|
|
30
|
+
assert.equal(pciTargets.generated_by, 'script/update-amd-gfx-targets.js')
|
|
31
|
+
assert.ok(pciTargets.sources.length >= 6)
|
|
32
|
+
for (const source of pciTargets.sources) {
|
|
33
|
+
assert.match(source.revision, /^[0-9a-f]{40}$/)
|
|
34
|
+
assert.match(source.sha256, /^[0-9a-f]{64}$/)
|
|
35
|
+
assert.match(source.url, new RegExp(source.revision))
|
|
36
|
+
assert.doesNotMatch(source.url, /\/(?:main|master|develop)\//)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assert.equal(pciTargets.entries['1002:731f'].target, 'gfx1010')
|
|
40
|
+
assert.equal(pciTargets.entries['1002:7340'].target, 'gfx1012')
|
|
41
|
+
assert.equal(pciTargets.entries['1002:73ff'].target, 'gfx1032')
|
|
42
|
+
assert.equal(pciTargets.entries['1002:73ff'].family, 'NAVI23')
|
|
43
|
+
assert.equal(pciTargets.revision_entries['1002:73ff:c3'].model, 'AMD Radeon RX 6600M')
|
|
44
|
+
assert.equal(pciTargets.revision_entries['1002:73ff:c3'].target, 'gfx1032')
|
|
45
|
+
assert.equal(pciTargets.revision_entries['1002:744c:c8'].target, 'gfx1100')
|
|
46
|
+
assert.equal(
|
|
47
|
+
Object.values(pciTargets.entries).some((entry) => entry.family === 'LIBDRM_MODEL_CONSENSUS'),
|
|
48
|
+
false
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
27
52
|
test('AMD gfx target refresh is an explicit maintainer command', () => {
|
|
28
53
|
assert.equal(packageJson.scripts['update:amd-gfx-targets'], 'node script/update-amd-gfx-targets.js')
|
|
54
|
+
assert.equal(packageJson.scripts['check:amd-gfx-targets'], 'node script/update-amd-gfx-targets.js --check')
|
|
29
55
|
assert.equal(Object.prototype.hasOwnProperty.call(packageJson.scripts, 'update:amd-rocm-targets'), false)
|
|
30
56
|
|
|
31
57
|
for (const script of ['preinstall', 'install', 'postinstall', 'prestart', 'start', 'poststart']) {
|
|
@@ -51,6 +77,13 @@ test('AMD gfx resolver maps common product names to exact gfx targets', () => {
|
|
|
51
77
|
}
|
|
52
78
|
})
|
|
53
79
|
|
|
80
|
+
test('AMD model resolver does not hand-maintain product gaps covered by PCI identity', () => {
|
|
81
|
+
assert.equal(gfxTargets.entries['radeon rx 6600m'], undefined)
|
|
82
|
+
assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon RX 6600'), 'gfx1032')
|
|
83
|
+
assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon RX 6600M'), null)
|
|
84
|
+
assert.equal(amd.resolve_rocm_gfx_target('AMD Radeon(TM) RX 6600M'), null)
|
|
85
|
+
})
|
|
86
|
+
|
|
54
87
|
test('AMD gfx resolver rejects unknown products and preserves raw gfx targets', () => {
|
|
55
88
|
const unknownModels = [
|
|
56
89
|
'AMD Radeon RX 480',
|
|
@@ -88,6 +121,9 @@ test('AMD gpu_target resolution uses CPU brand only for generic Radeon Graphics
|
|
|
88
121
|
})
|
|
89
122
|
|
|
90
123
|
test('Sysinfo exposes AMD gpu_target for resolved discrete GPUs', async (t) => {
|
|
124
|
+
t.mock.method(amdPci, 'resolve_gpu_target', async () => {
|
|
125
|
+
throw new Error('PCI fallback should not run for a resolved model')
|
|
126
|
+
})
|
|
91
127
|
t.mock.method(system, 'graphics', async () => ({
|
|
92
128
|
controllers: [
|
|
93
129
|
{
|
|
@@ -109,7 +145,63 @@ test('Sysinfo exposes AMD gpu_target for resolved discrete GPUs', async (t) => {
|
|
|
109
145
|
assert.equal(sys.info.gpu_target, 'gfx1030')
|
|
110
146
|
})
|
|
111
147
|
|
|
148
|
+
test('Sysinfo exposes AMD gpu_target for a PCI-resolved mobile GPU', async (t) => {
|
|
149
|
+
t.mock.method(amdPci, 'resolve_gpu_target', async (controller) => {
|
|
150
|
+
assert.equal(controller.model, 'AMD Radeon RX 6600M')
|
|
151
|
+
return 'gfx1032'
|
|
152
|
+
})
|
|
153
|
+
t.mock.method(system, 'graphics', async () => ({
|
|
154
|
+
controllers: [
|
|
155
|
+
{
|
|
156
|
+
vendor: 'Advanced Micro Devices, Inc.',
|
|
157
|
+
model: 'AMD Radeon(TM) Graphics',
|
|
158
|
+
vram: 512
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
vendor: 'Advanced Micro Devices, Inc.',
|
|
162
|
+
model: 'AMD Radeon RX 6600M',
|
|
163
|
+
vram: 8192
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
displays: []
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
const sys = new Sysinfo()
|
|
170
|
+
sys.info = {}
|
|
171
|
+
|
|
172
|
+
await sys.gpus()
|
|
173
|
+
|
|
174
|
+
assert.equal(sys.info.gpu, 'amd')
|
|
175
|
+
assert.equal(sys.info.gpu_model, 'amd radeon rx 6600m')
|
|
176
|
+
assert.equal(sys.info.gpu_target, 'gfx1032')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('Sysinfo uses exact AMD PCI identity after model lookup misses', async (t) => {
|
|
180
|
+
t.mock.method(amdPci, 'resolve_gpu_target', async (controller) => {
|
|
181
|
+
assert.equal(controller.model, 'AMD Radeon RX 5700 XT')
|
|
182
|
+
return 'gfx1010'
|
|
183
|
+
})
|
|
184
|
+
t.mock.method(system, 'graphics', async () => ({
|
|
185
|
+
controllers: [
|
|
186
|
+
{
|
|
187
|
+
vendor: 'AMD',
|
|
188
|
+
model: 'AMD Radeon RX 5700 XT',
|
|
189
|
+
vram: 8192
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
displays: []
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
const sys = new Sysinfo()
|
|
196
|
+
sys.info = {}
|
|
197
|
+
|
|
198
|
+
await sys.gpus()
|
|
199
|
+
|
|
200
|
+
assert.equal(sys.info.gpu_target, 'gfx1010')
|
|
201
|
+
})
|
|
202
|
+
|
|
112
203
|
test('Sysinfo leaves unresolved AMD gpu_target null', async (t) => {
|
|
204
|
+
t.mock.method(amdPci, 'resolve_gpu_target', async () => null)
|
|
113
205
|
t.mock.method(system, 'graphics', async () => ({
|
|
114
206
|
controllers: [
|
|
115
207
|
{
|
|
@@ -133,6 +225,9 @@ test('Sysinfo leaves unresolved AMD gpu_target null', async (t) => {
|
|
|
133
225
|
|
|
134
226
|
test('Sysinfo exposes AMD gpu_target through generic APU CPU fallback', async (t) => {
|
|
135
227
|
let cpuBrandCalls = 0
|
|
228
|
+
t.mock.method(amdPci, 'resolve_gpu_target', async () => {
|
|
229
|
+
throw new Error('PCI fallback should not run for a resolved APU')
|
|
230
|
+
})
|
|
136
231
|
t.mock.method(system, 'graphics', async () => ({
|
|
137
232
|
controllers: [
|
|
138
233
|
{
|
|
@@ -159,11 +254,14 @@ test('Sysinfo exposes AMD gpu_target through generic APU CPU fallback', async (t
|
|
|
159
254
|
assert.equal(cpuBrandCalls, 1)
|
|
160
255
|
})
|
|
161
256
|
|
|
162
|
-
test('AMD
|
|
163
|
-
const
|
|
257
|
+
test('AMD model fallback stays offline and data-only', () => {
|
|
258
|
+
const modelSource = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd.js'), 'utf8')
|
|
259
|
+
const pciSource = fs.readFileSync(path.join(root, 'kernel', 'gpu', 'amd_pci.js'), 'utf8')
|
|
164
260
|
|
|
165
|
-
assert.doesNotMatch(
|
|
166
|
-
assert.doesNotMatch(
|
|
167
|
-
assert.doesNotMatch(
|
|
168
|
-
assert.doesNotMatch(
|
|
261
|
+
assert.doesNotMatch(modelSource, /require\(["'](?:node:)?https?["']\)/)
|
|
262
|
+
assert.doesNotMatch(modelSource, /require\(["'](?:node:)?child_process["']\)/)
|
|
263
|
+
assert.doesNotMatch(modelSource, /\b(fetch|axios|rocminfo|hipInfo|ze_info)\b/)
|
|
264
|
+
assert.doesNotMatch(modelSource, /\b(kfd|topology)\b/i)
|
|
265
|
+
assert.doesNotMatch(pciSource, /require\(["'](?:node:)?https?["']\)/)
|
|
266
|
+
assert.doesNotMatch(pciSource, /\b(fetch|axios|OpenCL|rocminfo|hipInfo)\b/i)
|
|
169
267
|
})
|