pinokiod 8.0.58 → 8.0.60
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/vault/automatic_scans.js +44 -4
- package/kernel/vault/index.js +33 -1
- package/package.json +1 -1
- package/server/index.js +3 -0
- package/server/public/app-vault-mode.js +14 -5
- package/server/public/layout.js +7 -0
- package/server/public/vault.css +25 -0
- package/server/public/vault.js +135 -24
- package/server/views/app.ejs +17 -3
- package/test/vault-automatic-scan-ui.test.js +46 -3
- package/test/vault-automatic-scans.test.js +24 -2
- package/test/vault-engine.test.js +73 -0
- package/test/vault-ui.test.js +152 -3
|
@@ -41,6 +41,7 @@ class AutomaticScans {
|
|
|
41
41
|
this.waitingFor = null
|
|
42
42
|
this.listeners = new Set()
|
|
43
43
|
this.appTransitions = new Map()
|
|
44
|
+
this.globalScanReady = false
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
log(event, details = {}) {
|
|
@@ -134,7 +135,8 @@ class AutomaticScans {
|
|
|
134
135
|
snapshot() {
|
|
135
136
|
return {
|
|
136
137
|
enabled: !!this.vault.enabled,
|
|
137
|
-
|
|
138
|
+
global_scan_ready: this.globalScanReady,
|
|
139
|
+
rows: (this.globalScanReady ? [...this.entries.values()] : [])
|
|
138
140
|
.filter((entry) => !entry.hidden)
|
|
139
141
|
.sort((left, right) =>
|
|
140
142
|
(Number(left.updated_at) || 0) -
|
|
@@ -147,6 +149,14 @@ class AutomaticScans {
|
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
async refreshGlobalScanReady() {
|
|
153
|
+
const ready = typeof this.vault.globalScanReady === "function" &&
|
|
154
|
+
!!await this.vault.globalScanReady()
|
|
155
|
+
const changed = ready !== this.globalScanReady
|
|
156
|
+
this.globalScanReady = ready
|
|
157
|
+
return changed
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
modeFor(app) {
|
|
151
161
|
const setting = this.settings.get(app)
|
|
152
162
|
return setting && setting.mode === "manual" ? "manual" : "automatic"
|
|
@@ -236,6 +246,10 @@ class AutomaticScans {
|
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
queueApp(app) {
|
|
249
|
+
if (!this.globalScanReady) {
|
|
250
|
+
this.log("queue-skipped", { app, reason: "global-scan-required" })
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
239
253
|
if (this.modeFor(app) === "manual") {
|
|
240
254
|
this.log("queue-skipped", { app, reason: "manual" })
|
|
241
255
|
return false
|
|
@@ -324,6 +338,12 @@ class AutomaticScans {
|
|
|
324
338
|
this.log("preparing", { app })
|
|
325
339
|
await this.vault.ensureRegistryInitialized()
|
|
326
340
|
if (this.pendingStops.get(app) !== pending) return
|
|
341
|
+
await this.refreshGlobalScanReady()
|
|
342
|
+
if (!this.globalScanReady) {
|
|
343
|
+
this.pendingStops.delete(app)
|
|
344
|
+
this.log("check-skipped", { app, reason: "global-scan-required" })
|
|
345
|
+
return
|
|
346
|
+
}
|
|
327
347
|
await this.hydrate()
|
|
328
348
|
await this.withAppTransition(app, async () => {
|
|
329
349
|
if (this.pendingStops.get(app) !== pending) return
|
|
@@ -395,6 +415,11 @@ class AutomaticScans {
|
|
|
395
415
|
if (!this.hydrated && typeof this.vault.automaticScanStatus === "function") {
|
|
396
416
|
await this.vault.automaticScanStatus()
|
|
397
417
|
}
|
|
418
|
+
await this.refreshGlobalScanReady()
|
|
419
|
+
if (!this.globalScanReady) {
|
|
420
|
+
this.log("check-skipped", { app, reason: "global-scan-required" })
|
|
421
|
+
return
|
|
422
|
+
}
|
|
398
423
|
if (this.modeFor(app) === "manual") {
|
|
399
424
|
this.log("check-skipped", { app, reason: "manual" })
|
|
400
425
|
return
|
|
@@ -547,14 +572,16 @@ class AutomaticScans {
|
|
|
547
572
|
|
|
548
573
|
async drain() {
|
|
549
574
|
if (!this.vault.enabled || !this.vault.registry || this.active) return
|
|
575
|
+
const entry = [...this.entries.values()].find((item) =>
|
|
576
|
+
item.state === "checking")
|
|
577
|
+
if (!entry) return
|
|
578
|
+
await this.refreshGlobalScanReady()
|
|
579
|
+
if (!this.globalScanReady) return
|
|
550
580
|
if (this.manualDepth > 0 || this.currentBusyPromise() ||
|
|
551
581
|
this.vault.fileActionProgress) {
|
|
552
582
|
this.waitForBusyWork()
|
|
553
583
|
return
|
|
554
584
|
}
|
|
555
|
-
const entry = [...this.entries.values()].find((item) =>
|
|
556
|
-
item.state === "checking")
|
|
557
|
-
if (!entry) return
|
|
558
585
|
let appAvailable
|
|
559
586
|
try {
|
|
560
587
|
appAvailable = await this.appRootIsAvailable(entry.app)
|
|
@@ -842,6 +869,7 @@ class AutomaticScans {
|
|
|
842
869
|
|
|
843
870
|
async scanFinished({ scopeId, result, error }) {
|
|
844
871
|
const complete = !error && result && COMPLETE_PHASES.has(result.outcome)
|
|
872
|
+
let readinessChanged = false
|
|
845
873
|
try {
|
|
846
874
|
if (complete) {
|
|
847
875
|
await this.hydrate()
|
|
@@ -852,7 +880,9 @@ class AutomaticScans {
|
|
|
852
880
|
{ cancelPending: true, states: ["checking", "result"] }
|
|
853
881
|
)
|
|
854
882
|
}
|
|
883
|
+
if (!scopeId) readinessChanged = await this.refreshGlobalScanReady()
|
|
855
884
|
} finally {
|
|
885
|
+
if (readinessChanged) this.broadcast()
|
|
856
886
|
this.schedule()
|
|
857
887
|
}
|
|
858
888
|
}
|
|
@@ -897,6 +927,16 @@ class AutomaticScans {
|
|
|
897
927
|
await this.persistMode(app, mode)
|
|
898
928
|
const entry = this.entries.get(app)
|
|
899
929
|
if (entry && entry.state === "paused") this.entries.delete(app)
|
|
930
|
+
await this.refreshGlobalScanReady()
|
|
931
|
+
if (!this.globalScanReady) {
|
|
932
|
+
this.log("mode-changed", {
|
|
933
|
+
app,
|
|
934
|
+
mode,
|
|
935
|
+
check: "global-scan-required"
|
|
936
|
+
})
|
|
937
|
+
this.broadcast()
|
|
938
|
+
return { app, mode }
|
|
939
|
+
}
|
|
900
940
|
if (!this.appIsRunning(app)) {
|
|
901
941
|
this.log("mode-changed", { app, mode })
|
|
902
942
|
this.queueApp(app)
|
package/kernel/vault/index.js
CHANGED
|
@@ -73,6 +73,9 @@ const STATUS_FILTERS = new Set([
|
|
|
73
73
|
const FOLDER_DISCOVERY_COMPLETE_PHASES = new Set([
|
|
74
74
|
"complete", "completed_with_exclusions"
|
|
75
75
|
])
|
|
76
|
+
const GLOBAL_SCAN_READY_OUTCOMES = new Set([
|
|
77
|
+
"complete", "completed_with_exclusions"
|
|
78
|
+
])
|
|
76
79
|
|
|
77
80
|
const isMissingError = (error) => !!(error &&
|
|
78
81
|
(error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
@@ -672,9 +675,23 @@ class Vault {
|
|
|
672
675
|
await this.ensureRegistryInitialized()
|
|
673
676
|
}
|
|
674
677
|
await this.automaticScans.hydrate()
|
|
678
|
+
await this.automaticScans.refreshGlobalScanReady()
|
|
675
679
|
return this.automaticScans.snapshot()
|
|
676
680
|
}
|
|
677
681
|
|
|
682
|
+
async globalScanReady() {
|
|
683
|
+
if (!this.registry) return false
|
|
684
|
+
const scan = await this.registry.scanFor()
|
|
685
|
+
if (scan) this.lastScanCache.set("", scan)
|
|
686
|
+
else this.lastScanCache.delete("")
|
|
687
|
+
return this.globalScanIsReady(scan)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
globalScanIsReady(scan = this.lastScanCache.get("")) {
|
|
691
|
+
return !!(scan && scan.ts &&
|
|
692
|
+
GLOBAL_SCAN_READY_OUTCOMES.has(scan.outcome))
|
|
693
|
+
}
|
|
694
|
+
|
|
678
695
|
async refreshAnchorStores() {
|
|
679
696
|
let config = this.readConfig()
|
|
680
697
|
const candidates = [
|
|
@@ -1790,9 +1807,19 @@ class Vault {
|
|
|
1790
1807
|
return this.runMutation(() =>
|
|
1791
1808
|
this.removeExternalSource(payload.source_id))
|
|
1792
1809
|
case "scan": {
|
|
1793
|
-
|
|
1810
|
+
const scanSource = payload.scope_id
|
|
1811
|
+
? this.scanSource(payload.scope_id)
|
|
1812
|
+
: null
|
|
1813
|
+
if (payload.scope_id && !scanSource) {
|
|
1794
1814
|
return { error: "That scan location is no longer available." }
|
|
1795
1815
|
}
|
|
1816
|
+
if (scanSource && scanSource.kind === "app" &&
|
|
1817
|
+
!await this.globalScanReady()) {
|
|
1818
|
+
return {
|
|
1819
|
+
error: "Run an initial scan before scanning individual apps.",
|
|
1820
|
+
code: "global_scan_required"
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1796
1823
|
let threshold = this.sizeThreshold
|
|
1797
1824
|
if (payload.candidate_size != null) {
|
|
1798
1825
|
if (!CANDIDATE_SIZE_OPTIONS.includes(payload.candidate_size)) {
|
|
@@ -3266,6 +3293,9 @@ class Vault {
|
|
|
3266
3293
|
}
|
|
3267
3294
|
|
|
3268
3295
|
const lastScan = await this.scanForScope(scopeId)
|
|
3296
|
+
const globalScanReady = scopeId
|
|
3297
|
+
? await this.globalScanReady()
|
|
3298
|
+
: this.globalScanIsReady(lastScan)
|
|
3269
3299
|
const before = lastScan && Number.isFinite(lastScan.bytes_total)
|
|
3270
3300
|
? lastScan.bytes_total
|
|
3271
3301
|
: 0
|
|
@@ -3302,6 +3332,7 @@ class Vault {
|
|
|
3302
3332
|
const sourceCounts = this.sourceCountMaps(snapshot.scopeRows)
|
|
3303
3333
|
const result = {
|
|
3304
3334
|
enabled: true,
|
|
3335
|
+
global_scan_ready: globalScanReady,
|
|
3305
3336
|
mode: this.mode,
|
|
3306
3337
|
scan: this.scanStatus(),
|
|
3307
3338
|
last_scan: lastScan,
|
|
@@ -3402,6 +3433,7 @@ class Vault {
|
|
|
3402
3433
|
async progressStatus(scopeId = null) {
|
|
3403
3434
|
const result = {
|
|
3404
3435
|
enabled: !!this.enabled,
|
|
3436
|
+
global_scan_ready: this.globalScanIsReady(),
|
|
3405
3437
|
scan: this.scanStatus(),
|
|
3406
3438
|
file_action: this.fileActionStatus(scopeId),
|
|
3407
3439
|
last_scan: this.lastScanCache.get(scopeId || "") || null
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1994,9 +1994,12 @@ class Server {
|
|
|
1994
1994
|
if (vault && vault.ready) await vault.ready
|
|
1995
1995
|
result.vault_enabled = !!(vault && vault.enabled)
|
|
1996
1996
|
result.vault_automatic_mode = "automatic"
|
|
1997
|
+
result.vault_global_scan_ready = false
|
|
1997
1998
|
if (result.vault_enabled) {
|
|
1998
1999
|
try {
|
|
1999
2000
|
const snapshot = await vault.automaticScanStatus()
|
|
2001
|
+
result.vault_global_scan_ready =
|
|
2002
|
+
snapshot && snapshot.global_scan_ready === true
|
|
2000
2003
|
const setting = Array.isArray(snapshot && snapshot.settings)
|
|
2001
2004
|
? snapshot.settings.find((item) =>
|
|
2002
2005
|
item && item.app === name)
|
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
const app = status.dataset.app || ""
|
|
6
6
|
const tab = status.closest("#save-space-tab")
|
|
7
7
|
const label = status.querySelector("[data-app-vault-mode-label]")
|
|
8
|
-
const
|
|
8
|
+
const setState = (value, ready) => {
|
|
9
9
|
const mode = value === "manual" ? "manual" : "automatic"
|
|
10
|
+
const globalScanReady = ready === true
|
|
10
11
|
status.dataset.mode = mode
|
|
12
|
+
status.dataset.ready = String(globalScanReady)
|
|
11
13
|
status.hidden = false
|
|
12
|
-
if (label)
|
|
14
|
+
if (label) {
|
|
15
|
+
label.textContent = globalScanReady
|
|
16
|
+
? (mode === "automatic" ? "Auto" : "Manual")
|
|
17
|
+
: "Set up"
|
|
18
|
+
}
|
|
13
19
|
if (tab) {
|
|
14
20
|
tab.setAttribute("aria-label",
|
|
15
|
-
|
|
21
|
+
globalScanReady
|
|
22
|
+
? `Disk Saver — ${mode === "automatic" ? "Automatic" : "Manual"} checking`
|
|
23
|
+
: "Disk Saver — Set up required")
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
const applySnapshot = (snapshot) => {
|
|
@@ -20,10 +28,11 @@
|
|
|
20
28
|
? snapshot.settings
|
|
21
29
|
: []
|
|
22
30
|
const setting = settings.find((item) => item && item.app === app)
|
|
23
|
-
|
|
31
|
+
setState(setting && setting.mode,
|
|
32
|
+
snapshot && snapshot.global_scan_ready === true)
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
setState(status.dataset.mode, status.dataset.ready === "true")
|
|
27
36
|
if (!app || typeof window.EventSource !== "function") return
|
|
28
37
|
|
|
29
38
|
const source = new window.EventSource(
|
package/server/public/layout.js
CHANGED
|
@@ -1068,6 +1068,13 @@
|
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
1070
|
function render(snapshot, options = {}) {
|
|
1071
|
+
if (!snapshot || snapshot.global_scan_ready !== true) {
|
|
1072
|
+
[...completions.keys()].forEach(removeCompletion);
|
|
1073
|
+
visibleCheckingApps = new Set();
|
|
1074
|
+
tray.replaceChildren();
|
|
1075
|
+
tray.hidden = true;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1071
1078
|
const rows = snapshot && Array.isArray(snapshot.rows)
|
|
1072
1079
|
? snapshot.rows
|
|
1073
1080
|
: [];
|
package/server/public/vault.css
CHANGED
|
@@ -218,6 +218,31 @@ body[data-vault-mode="app"] .vault-overview {
|
|
|
218
218
|
letter-spacing: -.025em;
|
|
219
219
|
line-height: 1.15;
|
|
220
220
|
}
|
|
221
|
+
.vault-setup-copy {
|
|
222
|
+
max-width: 560px;
|
|
223
|
+
margin: 8px 0 0;
|
|
224
|
+
color: var(--task-muted);
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
line-height: 1.5;
|
|
227
|
+
}
|
|
228
|
+
.vault-body.setup-required .vault-overview {
|
|
229
|
+
min-height: 260px;
|
|
230
|
+
flex: 1 1 auto;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
gap: 32px;
|
|
233
|
+
padding: 48px;
|
|
234
|
+
border-bottom: 0;
|
|
235
|
+
}
|
|
236
|
+
.vault-body.setup-required .vault-metrics.summary {
|
|
237
|
+
max-width: 620px;
|
|
238
|
+
flex: 0 1 620px;
|
|
239
|
+
grid-template-columns: minmax(0, 1fr);
|
|
240
|
+
}
|
|
241
|
+
.vault-body.setup-required .vault-summary-value {
|
|
242
|
+
margin-top: 8px;
|
|
243
|
+
font-size: 24px;
|
|
244
|
+
}
|
|
245
|
+
.vault-body.setup-required .vault-summary-side { display: none; }
|
|
221
246
|
.vault-storage-chart {
|
|
222
247
|
width: min(100%, 540px);
|
|
223
248
|
min-width: 0;
|
package/server/public/vault.js
CHANGED
|
@@ -105,6 +105,9 @@ const COPY = {
|
|
|
105
105
|
never: "Never",
|
|
106
106
|
scan: "Scan",
|
|
107
107
|
scan_app: "Scan this app",
|
|
108
|
+
scan_all_locations: "Scan all locations",
|
|
109
|
+
setup_title: "Run an initial scan to enable app scans",
|
|
110
|
+
setup_description: "This creates the file index used to compare apps and enables automatic checks.",
|
|
108
111
|
scan_again: "Scan again",
|
|
109
112
|
cancel_scan: "Cancel scan",
|
|
110
113
|
scanning: "Scanning…",
|
|
@@ -533,6 +536,14 @@ const post = async (payload) => {
|
|
|
533
536
|
if (!result) throw new Error(COPY.action_request_failed.replace("{status}", response.status))
|
|
534
537
|
return result
|
|
535
538
|
}
|
|
539
|
+
const openGlobalWorkspace = () => {
|
|
540
|
+
if (window.parent === window) {
|
|
541
|
+
window.location.assign("/vault")
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
window.parent.postMessage(
|
|
545
|
+
{ e: "vault-open-global" }, window.location.origin)
|
|
546
|
+
}
|
|
536
547
|
let automaticModeEventSource = null
|
|
537
548
|
const applyAutomaticScanSnapshot = (snapshot) => {
|
|
538
549
|
if (!IS_APP_MODE) return
|
|
@@ -543,9 +554,17 @@ const applyAutomaticScanSnapshot = (snapshot) => {
|
|
|
543
554
|
const mode = setting && setting.mode === "manual"
|
|
544
555
|
? "manual"
|
|
545
556
|
: "automatic"
|
|
546
|
-
|
|
557
|
+
const globalScanReady = !!(snapshot &&
|
|
558
|
+
snapshot.global_scan_ready === true)
|
|
559
|
+
const readinessChanged = state.data &&
|
|
560
|
+
state.data.global_scan_ready !== globalScanReady
|
|
561
|
+
if (state.data) state.data.global_scan_ready = globalScanReady
|
|
562
|
+
if (state.automaticMode === mode && !readinessChanged) return
|
|
547
563
|
state.automaticMode = mode
|
|
548
|
-
if (state.data)
|
|
564
|
+
if (state.data) {
|
|
565
|
+
if (readinessChanged) refresh(true)
|
|
566
|
+
else renderOverview()
|
|
567
|
+
}
|
|
549
568
|
}
|
|
550
569
|
const loadAutomaticMode = async () => {
|
|
551
570
|
if (!IS_APP_MODE) return
|
|
@@ -2176,7 +2195,51 @@ const automaticModeMarkup = () => {
|
|
|
2176
2195
|
</details>`
|
|
2177
2196
|
}
|
|
2178
2197
|
|
|
2179
|
-
const
|
|
2198
|
+
const appSetupRequired = () => IS_APP_MODE && state.data &&
|
|
2199
|
+
state.data.global_scan_ready !== true
|
|
2200
|
+
|
|
2201
|
+
const restoreAutomaticModeMenu = (open) => {
|
|
2202
|
+
const modeMenu = el("vault-auto-mode")
|
|
2203
|
+
if (modeMenu && open) modeMenu.open = true
|
|
2204
|
+
if (!modeMenu || !state.automaticModeMenuRequested) return
|
|
2205
|
+
state.automaticModeMenuRequested = false
|
|
2206
|
+
modeMenu.classList.add("targeted")
|
|
2207
|
+
const trigger = modeMenu.querySelector("summary")
|
|
2208
|
+
requestAnimationFrame(() => {
|
|
2209
|
+
if (trigger && trigger.isConnected) trigger.focus()
|
|
2210
|
+
})
|
|
2211
|
+
setTimeout(() => {
|
|
2212
|
+
if (modeMenu.isConnected) modeMenu.classList.remove("targeted")
|
|
2213
|
+
}, 1600)
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
const renderSetupOverview = () => {
|
|
2217
|
+
const metrics = el("vault-metrics")
|
|
2218
|
+
const existingModeMenu = el("vault-auto-mode")
|
|
2219
|
+
const modeMenuOpen = !!(existingModeMenu && existingModeMenu.open) ||
|
|
2220
|
+
state.automaticModeMenuRequested
|
|
2221
|
+
metrics.classList.add("summary")
|
|
2222
|
+
metrics.innerHTML = `
|
|
2223
|
+
<div class="vault-summary-main">
|
|
2224
|
+
<div class="vault-summary-label"><i class="fa-solid fa-hard-drive"></i><span>${esc(COPY.save_space)}</span>${automaticModeMarkup()}</div>
|
|
2225
|
+
<div class="vault-summary-value">${esc(COPY.setup_title)}</div>
|
|
2226
|
+
<p class="vault-setup-copy">${esc(COPY.setup_description)}</p>
|
|
2227
|
+
</div>
|
|
2228
|
+
<div class="vault-summary-side"></div>`
|
|
2229
|
+
restoreAutomaticModeMenu(modeMenuOpen)
|
|
2230
|
+
|
|
2231
|
+
const scanButton = el("btn-scan")
|
|
2232
|
+
scanButton.innerHTML = `<i class="fa-solid fa-rotate"></i>${esc(COPY.scan_all_locations)}`
|
|
2233
|
+
scanButton.classList.add("primary")
|
|
2234
|
+
scanButton.disabled = state.scanCancelRequested
|
|
2235
|
+
const candidateSizeSelect = el("vault-candidate-size")
|
|
2236
|
+
if (candidateSizeSelect) candidateSizeSelect.hidden = true
|
|
2237
|
+
const scanState = el("vault-scan-state")
|
|
2238
|
+
scanState.classList.remove("show")
|
|
2239
|
+
scanState.innerHTML = ""
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
const renderNormalOverview = () => {
|
|
2180
2243
|
const data = state.data
|
|
2181
2244
|
const last = data.last_scan
|
|
2182
2245
|
const activeScan = scanActive(data.scan)
|
|
@@ -2251,19 +2314,7 @@ const renderOverview = () => {
|
|
|
2251
2314
|
${summary}
|
|
2252
2315
|
</div>
|
|
2253
2316
|
<div class="vault-summary-side">${summarySide}</div>`
|
|
2254
|
-
|
|
2255
|
-
if (modeMenu && modeMenuOpen) modeMenu.open = true
|
|
2256
|
-
if (modeMenu && state.automaticModeMenuRequested) {
|
|
2257
|
-
state.automaticModeMenuRequested = false
|
|
2258
|
-
modeMenu.classList.add("targeted")
|
|
2259
|
-
const trigger = modeMenu.querySelector("summary")
|
|
2260
|
-
requestAnimationFrame(() => {
|
|
2261
|
-
if (trigger && trigger.isConnected) trigger.focus()
|
|
2262
|
-
})
|
|
2263
|
-
setTimeout(() => {
|
|
2264
|
-
if (modeMenu.isConnected) modeMenu.classList.remove("targeted")
|
|
2265
|
-
}, 1600)
|
|
2266
|
-
}
|
|
2317
|
+
restoreAutomaticModeMenu(modeMenuOpen)
|
|
2267
2318
|
}
|
|
2268
2319
|
const idleScanLabel = IS_APP_MODE
|
|
2269
2320
|
? (last ? COPY.scan_again : COPY.scan_app)
|
|
@@ -2289,7 +2340,10 @@ const renderOverview = () => {
|
|
|
2289
2340
|
})
|
|
2290
2341
|
}
|
|
2291
2342
|
const candidateSizeSelect = el("vault-candidate-size")
|
|
2292
|
-
if (candidateSizeSelect)
|
|
2343
|
+
if (candidateSizeSelect) {
|
|
2344
|
+
candidateSizeSelect.hidden = false
|
|
2345
|
+
candidateSizeSelect.disabled = activeScan
|
|
2346
|
+
}
|
|
2293
2347
|
const scanControl = el("vault-scan-control")
|
|
2294
2348
|
if (scanControl) scanControl.classList.toggle("single", activeScan)
|
|
2295
2349
|
const scanSizeMenu = el("vault-scan-size-menu")
|
|
@@ -2369,6 +2423,14 @@ const renderOverview = () => {
|
|
|
2369
2423
|
}
|
|
2370
2424
|
}
|
|
2371
2425
|
|
|
2426
|
+
const renderOverview = () => {
|
|
2427
|
+
if (appSetupRequired()) {
|
|
2428
|
+
renderSetupOverview()
|
|
2429
|
+
return
|
|
2430
|
+
}
|
|
2431
|
+
renderNormalOverview()
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2372
2434
|
const renderResult = () => {
|
|
2373
2435
|
const result = el("vault-result")
|
|
2374
2436
|
if (!state.scanResult) {
|
|
@@ -2555,11 +2617,34 @@ const renderCleanupNotice = () => {
|
|
|
2555
2617
|
notice.innerHTML = `<i class="fa-solid fa-broom" aria-hidden="true"></i><strong>${esc(title)}</strong><span class="vault-cleanup-notice-detail">${esc(detail)}</span><button class="vault-button" id="btn-review-cleanup" type="button">${esc(COPY.review_cleanup)}<i class="fa-solid fa-chevron-right" aria-hidden="true"></i></button>`
|
|
2556
2618
|
}
|
|
2557
2619
|
|
|
2620
|
+
const clearPanel = (id, className) => {
|
|
2621
|
+
const panel = el(id)
|
|
2622
|
+
panel.className = className
|
|
2623
|
+
panel.innerHTML = ""
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
const renderSetupWorkspace = () => {
|
|
2627
|
+
renderSetupOverview()
|
|
2628
|
+
renderFeedback()
|
|
2629
|
+
el("vault-explorer").style.display = "none"
|
|
2630
|
+
clearPanel("vault-action-state", "vault-action-state")
|
|
2631
|
+
clearPanel("vault-result", "vault-result")
|
|
2632
|
+
clearPanel("vault-cleanup-notice", "vault-cleanup-notice")
|
|
2633
|
+
renderedActionProgress = null
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2558
2636
|
const render = () => {
|
|
2559
2637
|
if (!state.data) return
|
|
2638
|
+
const setupRequired = appSetupRequired()
|
|
2639
|
+
const body = document.querySelector(".vault-body")
|
|
2640
|
+
if (body) body.classList.toggle("setup-required", setupRequired)
|
|
2641
|
+
if (setupRequired) {
|
|
2642
|
+
renderSetupWorkspace()
|
|
2643
|
+
return
|
|
2644
|
+
}
|
|
2560
2645
|
renderOverview()
|
|
2561
|
-
renderResult()
|
|
2562
2646
|
renderFeedback()
|
|
2647
|
+
renderResult()
|
|
2563
2648
|
renderCleanupNotice()
|
|
2564
2649
|
if (!state.data.enabled) {
|
|
2565
2650
|
el("vault-explorer").style.display = "none"
|
|
@@ -2738,12 +2823,16 @@ const refresh = async (forceFull = false) => {
|
|
|
2738
2823
|
folderDiscoveryActive(progress.folder_discovery) ||
|
|
2739
2824
|
fileAction) {
|
|
2740
2825
|
delay = 1500
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2826
|
+
if (appSetupRequired()) {
|
|
2827
|
+
renderSetupWorkspace()
|
|
2828
|
+
} else {
|
|
2829
|
+
renderOverview()
|
|
2830
|
+
renderResult()
|
|
2831
|
+
renderActionProgress()
|
|
2832
|
+
renderFeedback()
|
|
2833
|
+
renderExternalPrompt()
|
|
2834
|
+
renderFolderDiscovery()
|
|
2835
|
+
}
|
|
2747
2836
|
} else {
|
|
2748
2837
|
const data = await fetchJson(statusUrl())
|
|
2749
2838
|
if (sequence !== refreshSequence) return
|
|
@@ -3553,6 +3642,28 @@ document.addEventListener("click", async (event) => {
|
|
|
3553
3642
|
}
|
|
3554
3643
|
return
|
|
3555
3644
|
}
|
|
3645
|
+
if (appSetupRequired()) {
|
|
3646
|
+
target.disabled = true
|
|
3647
|
+
state.feedback = null
|
|
3648
|
+
renderFeedback()
|
|
3649
|
+
try {
|
|
3650
|
+
const result = await post({
|
|
3651
|
+
action: "scan",
|
|
3652
|
+
scope_id: null,
|
|
3653
|
+
candidate_size: candidateSize()
|
|
3654
|
+
})
|
|
3655
|
+
if (result.error) throw new Error(result.error)
|
|
3656
|
+
openGlobalWorkspace()
|
|
3657
|
+
} catch (error) {
|
|
3658
|
+
target.disabled = false
|
|
3659
|
+
state.feedback = {
|
|
3660
|
+
error: true,
|
|
3661
|
+
message: error && error.message ? error.message : String(error)
|
|
3662
|
+
}
|
|
3663
|
+
renderFeedback()
|
|
3664
|
+
}
|
|
3665
|
+
return
|
|
3666
|
+
}
|
|
3556
3667
|
state.scanRequested = true
|
|
3557
3668
|
state.scanBaseline = state.data.last_scan ? state.data.last_scan.ts : null
|
|
3558
3669
|
state.scanResult = null
|
package/server/views/app.ejs
CHANGED
|
@@ -4121,12 +4121,18 @@ body.dark .disk-usage {
|
|
|
4121
4121
|
.app-vault-mode[data-mode="automatic"] {
|
|
4122
4122
|
color: #b45309;
|
|
4123
4123
|
}
|
|
4124
|
+
.app-vault-mode[data-ready="false"] {
|
|
4125
|
+
color: #b45309;
|
|
4126
|
+
}
|
|
4124
4127
|
body.dark .app-vault-mode {
|
|
4125
4128
|
color: rgba(203, 213, 225, 0.74);
|
|
4126
4129
|
}
|
|
4127
4130
|
body.dark .app-vault-mode[data-mode="automatic"] {
|
|
4128
4131
|
color: #e0a54b;
|
|
4129
4132
|
}
|
|
4133
|
+
body.dark .app-vault-mode[data-ready="false"] {
|
|
4134
|
+
color: #e0a54b;
|
|
4135
|
+
}
|
|
4130
4136
|
.app-vault-mode-chevron,
|
|
4131
4137
|
.app-autolaunch-chevron {
|
|
4132
4138
|
display: inline-flex;
|
|
@@ -8501,13 +8507,14 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8501
8507
|
</button>
|
|
8502
8508
|
<% if (typeof vault_enabled !== 'undefined' && vault_enabled) { %>
|
|
8503
8509
|
<% const vaultAutomaticMode = typeof vault_automatic_mode === 'string' && vault_automatic_mode === 'manual' ? 'manual' : 'automatic' %>
|
|
8504
|
-
|
|
8510
|
+
<% const vaultGlobalScanReady = typeof vault_global_scan_ready !== 'undefined' && vault_global_scan_ready === true %>
|
|
8511
|
+
<a id='save-space-tab' data-mode="refresh" target="app-vault" href="/vault/app/<%=encodeURIComponent(name)%>" class="btn header-item frame-link" data-index="vault" data-static="retain" data-tab-link-popover="false" aria-label="<%=vaultGlobalScanReady ? `Disk Saver — ${vaultAutomaticMode === 'automatic' ? 'Automatic' : 'Manual'} checking` : 'Disk Saver — Set up required'%>">
|
|
8505
8512
|
<div class='tab'>
|
|
8506
8513
|
<i class="fa-solid fa-hard-drive menu-action-leading-icon"></i>
|
|
8507
8514
|
<div class='display'>Disk Saver</div>
|
|
8508
8515
|
<div class='flexible'></div>
|
|
8509
|
-
<span class="app-vault-mode" data-app-vault-mode data-app="<%=name%>" data-mode="<%=vaultAutomaticMode%>" aria-hidden="true">
|
|
8510
|
-
<span data-app-vault-mode-label><%=vaultAutomaticMode === 'automatic' ? 'Auto' : 'Manual'%></span>
|
|
8516
|
+
<span class="app-vault-mode" data-app-vault-mode data-app="<%=name%>" data-mode="<%=vaultAutomaticMode%>" data-ready="<%=vaultGlobalScanReady ? 'true' : 'false'%>" aria-hidden="true">
|
|
8517
|
+
<span data-app-vault-mode-label><%=vaultGlobalScanReady ? (vaultAutomaticMode === 'automatic' ? 'Auto' : 'Manual') : 'Set up'%></span>
|
|
8511
8518
|
</span>
|
|
8512
8519
|
<i class="fa-solid fa-angle-down app-vault-mode-chevron" aria-hidden="true"></i>
|
|
8513
8520
|
</div>
|
|
@@ -12900,6 +12907,13 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12900
12907
|
await renderSelection({ target: diskSaverTab, force: true })
|
|
12901
12908
|
openVaultAutomaticSettings()
|
|
12902
12909
|
})
|
|
12910
|
+
window.addEventListener("message", (event) => {
|
|
12911
|
+
const frame = document.querySelector("iframe[name='app-vault']")
|
|
12912
|
+
if (!frame || event.source !== frame.contentWindow ||
|
|
12913
|
+
event.origin !== window.location.origin ||
|
|
12914
|
+
!event.data || event.data.e !== "vault-open-global") return
|
|
12915
|
+
window.location.assign("/vault")
|
|
12916
|
+
})
|
|
12903
12917
|
if (window.parent !== window) {
|
|
12904
12918
|
window.parent.postMessage(
|
|
12905
12919
|
{ e: "vault-auto-settings-ready" }, window.location.origin)
|
|
@@ -50,6 +50,7 @@ test("the shared layout renders and reviews automatic possible-match notices", a
|
|
|
50
50
|
status: 200,
|
|
51
51
|
json: async () => ({
|
|
52
52
|
enabled: true,
|
|
53
|
+
global_scan_ready: true,
|
|
53
54
|
rows: []
|
|
54
55
|
})
|
|
55
56
|
}
|
|
@@ -72,6 +73,22 @@ test("the shared layout renders and reviews automatic possible-match notices", a
|
|
|
72
73
|
eventSources[0].onmessage({
|
|
73
74
|
data: JSON.stringify({
|
|
74
75
|
enabled: true,
|
|
76
|
+
global_scan_ready: false,
|
|
77
|
+
rows: [{
|
|
78
|
+
app: "ComfyUI",
|
|
79
|
+
state: "checking",
|
|
80
|
+
notice_id: "checking:0:"
|
|
81
|
+
}]
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
assert.equal(dom.window.document.getElementById(
|
|
85
|
+
"vault-auto-scan-tray").hidden, true)
|
|
86
|
+
assert.equal(dom.window.document.querySelector(
|
|
87
|
+
".vault-auto-scan-row"), null)
|
|
88
|
+
eventSources[0].onmessage({
|
|
89
|
+
data: JSON.stringify({
|
|
90
|
+
enabled: true,
|
|
91
|
+
global_scan_ready: true,
|
|
75
92
|
rows: [{
|
|
76
93
|
app: "ComfyUI",
|
|
77
94
|
state: "result",
|
|
@@ -171,6 +188,7 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
|
|
|
171
188
|
eventSources[0].onmessage({
|
|
172
189
|
data: JSON.stringify({
|
|
173
190
|
enabled: true,
|
|
191
|
+
global_scan_ready: true,
|
|
174
192
|
rows: [{
|
|
175
193
|
app: "ComfyUI",
|
|
176
194
|
state: "checking",
|
|
@@ -286,6 +304,7 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
286
304
|
status: 200,
|
|
287
305
|
json: async () => ({
|
|
288
306
|
enabled: true,
|
|
307
|
+
global_scan_ready: true,
|
|
289
308
|
rows: [],
|
|
290
309
|
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
291
310
|
})
|
|
@@ -304,7 +323,7 @@ test("an empty automatic check briefly confirms completion", async () => {
|
|
|
304
323
|
dom.window.eval(script)
|
|
305
324
|
await waitFor(() => eventSources.length === 1)
|
|
306
325
|
const send = (payload) => eventSources[0].onmessage({
|
|
307
|
-
data: JSON.stringify(payload)
|
|
326
|
+
data: JSON.stringify(Object.assign({ global_scan_ready: true }, payload))
|
|
308
327
|
})
|
|
309
328
|
|
|
310
329
|
send({
|
|
@@ -527,7 +546,7 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
|
|
|
527
546
|
const script = await fs.promises.readFile(
|
|
528
547
|
path.join(root, "server", "public", "app-vault-mode.js"), "utf8")
|
|
529
548
|
const dom = new JSDOM(`<a id="save-space-tab">
|
|
530
|
-
<span data-app-vault-mode data-app="ComfyUI" data-mode="automatic">
|
|
549
|
+
<span data-app-vault-mode data-app="ComfyUI" data-mode="automatic" data-ready="true">
|
|
531
550
|
<span data-app-vault-mode-label>Auto</span>
|
|
532
551
|
</span>
|
|
533
552
|
</a>`, {
|
|
@@ -557,6 +576,7 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
|
|
|
557
576
|
|
|
558
577
|
eventSources[0].onmessage({
|
|
559
578
|
data: JSON.stringify({
|
|
579
|
+
global_scan_ready: true,
|
|
560
580
|
settings: [{ app: "ComfyUI", mode: "manual" }]
|
|
561
581
|
})
|
|
562
582
|
})
|
|
@@ -566,10 +586,32 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
|
|
|
566
586
|
assert.equal(dom.window.document.getElementById("save-space-tab")
|
|
567
587
|
.getAttribute("aria-label"), "Disk Saver — Manual checking")
|
|
568
588
|
|
|
569
|
-
eventSources[0].onmessage({
|
|
589
|
+
eventSources[0].onmessage({
|
|
590
|
+
data: JSON.stringify({ global_scan_ready: true, settings: [] })
|
|
591
|
+
})
|
|
570
592
|
assert.equal(status.dataset.mode, "automatic")
|
|
571
593
|
assert.equal(status.hidden, false)
|
|
572
594
|
assert.equal(label.textContent, "Auto")
|
|
595
|
+
|
|
596
|
+
eventSources[0].onmessage({
|
|
597
|
+
data: JSON.stringify({
|
|
598
|
+
global_scan_ready: false,
|
|
599
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
assert.equal(status.dataset.ready, "false")
|
|
603
|
+
assert.equal(label.textContent, "Set up")
|
|
604
|
+
assert.equal(dom.window.document.getElementById("save-space-tab")
|
|
605
|
+
.getAttribute("aria-label"), "Disk Saver — Set up required")
|
|
606
|
+
|
|
607
|
+
eventSources[0].onmessage({
|
|
608
|
+
data: JSON.stringify({
|
|
609
|
+
global_scan_ready: true,
|
|
610
|
+
settings: [{ app: "ComfyUI", mode: "automatic" }]
|
|
611
|
+
})
|
|
612
|
+
})
|
|
613
|
+
assert.equal(status.dataset.ready, "true")
|
|
614
|
+
assert.equal(label.textContent, "Auto")
|
|
573
615
|
assert.match(template, /data-app-vault-mode/)
|
|
574
616
|
assert.match(template, /app-vault-mode\.js/)
|
|
575
617
|
assert.match(template,
|
|
@@ -592,6 +634,7 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
|
|
|
592
634
|
assert.doesNotMatch(template, /app-vault-mode-dot/)
|
|
593
635
|
assert.match(server,
|
|
594
636
|
/result\.vault_automatic_mode = setting && setting\.mode === "manual"/)
|
|
637
|
+
assert.match(server, /result\.vault_global_scan_ready/)
|
|
595
638
|
|
|
596
639
|
dom.window.close()
|
|
597
640
|
})
|
|
@@ -39,6 +39,11 @@ const makeVault = async (home, options = {}) => {
|
|
|
39
39
|
} else {
|
|
40
40
|
await vault.init()
|
|
41
41
|
}
|
|
42
|
+
const globalScanReady = options.globalScanReady !== undefined
|
|
43
|
+
? !!options.globalScanReady
|
|
44
|
+
: true
|
|
45
|
+
vault.globalScanReady = async () => globalScanReady
|
|
46
|
+
vault.automaticScans.globalScanReady = globalScanReady
|
|
42
47
|
return vault
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -75,14 +80,28 @@ describe("automatic app checks", () => {
|
|
|
75
80
|
|
|
76
81
|
test("reading notice state does not create Vault storage", async () => {
|
|
77
82
|
const home = await makeHome()
|
|
78
|
-
const vault = await makeVault(home, {
|
|
83
|
+
const vault = await makeVault(home, {
|
|
84
|
+
deferStorage: true,
|
|
85
|
+
globalScanReady: false
|
|
86
|
+
})
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
const status = await vault.automaticScanStatus()
|
|
89
|
+
assert.equal(status.global_scan_ready, false)
|
|
90
|
+
assert.deepEqual(status.rows, [])
|
|
81
91
|
assert.equal(vault.initialized, false)
|
|
82
92
|
assert.equal(fs.existsSync(path.join(home, "vault")), false)
|
|
83
93
|
await close(vault)
|
|
84
94
|
})
|
|
85
95
|
|
|
96
|
+
test("missing readiness state keeps automatic checks locked", async () => {
|
|
97
|
+
const automatic = new AutomaticScans({ enabled: true })
|
|
98
|
+
|
|
99
|
+
await automatic.refreshGlobalScanReady()
|
|
100
|
+
|
|
101
|
+
assert.equal(automatic.globalScanReady, false)
|
|
102
|
+
assert.deepEqual(automatic.snapshot().rows, [])
|
|
103
|
+
})
|
|
104
|
+
|
|
86
105
|
test("disabled Vault ignores app-stop events", async () => {
|
|
87
106
|
const home = await makeHome()
|
|
88
107
|
const appRoot = path.join(home, "api", "disabled-app")
|
|
@@ -111,6 +130,7 @@ describe("automatic app checks", () => {
|
|
|
111
130
|
api: { running_paths: {} }
|
|
112
131
|
},
|
|
113
132
|
sources: () => [],
|
|
133
|
+
globalScanReady: async () => true,
|
|
114
134
|
automaticScanStatus: async () => ({ rows: [] })
|
|
115
135
|
}
|
|
116
136
|
const automatic = new AutomaticScans(vault)
|
|
@@ -840,6 +860,7 @@ describe("automatic app checks", () => {
|
|
|
840
860
|
}
|
|
841
861
|
}
|
|
842
862
|
})
|
|
863
|
+
automatic.globalScanReady = true
|
|
843
864
|
automatic.hydrated = true
|
|
844
865
|
automatic.log = () => {}
|
|
845
866
|
automatic.active = {
|
|
@@ -904,6 +925,7 @@ describe("automatic app checks", () => {
|
|
|
904
925
|
}
|
|
905
926
|
}
|
|
906
927
|
})
|
|
928
|
+
automatic.globalScanReady = true
|
|
907
929
|
automatic.hydrated = true
|
|
908
930
|
automatic.log = () => {}
|
|
909
931
|
automatic.entries.set("demo", {
|
|
@@ -134,6 +134,77 @@ describe("Save Space engine", () => {
|
|
|
134
134
|
await close(vault)
|
|
135
135
|
})
|
|
136
136
|
|
|
137
|
+
test("global readiness accepts only successful publication outcomes", async () => {
|
|
138
|
+
const { vault } = await makeVault()
|
|
139
|
+
const scan = (outcome) => ({ ts: 1, outcome })
|
|
140
|
+
|
|
141
|
+
assert.equal(vault.globalScanIsReady(scan("complete")), true)
|
|
142
|
+
assert.equal(vault.globalScanIsReady(
|
|
143
|
+
scan("completed_with_exclusions")), true)
|
|
144
|
+
assert.equal(vault.globalScanIsReady(scan("cancelled")), false)
|
|
145
|
+
assert.equal(vault.globalScanIsReady(scan("failed")), false)
|
|
146
|
+
assert.equal(vault.globalScanIsReady(null), false)
|
|
147
|
+
|
|
148
|
+
await close(vault)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("app scans stay locked until the first global scan is published", async () => {
|
|
152
|
+
const { home, vault } = await makeVault()
|
|
153
|
+
await write(path.join(home, "api", "demo", "model.bin"), "demo")
|
|
154
|
+
await vault.openWorkspace()
|
|
155
|
+
const source = vault.sources().find((item) =>
|
|
156
|
+
item.kind === "app" && item.app === "demo")
|
|
157
|
+
|
|
158
|
+
assert.equal(await vault.globalScanReady(), false)
|
|
159
|
+
assert.equal((await vault.status(source.id)).global_scan_ready, false)
|
|
160
|
+
assert.deepEqual(await vault.perform("scan", {
|
|
161
|
+
scope_id: source.id,
|
|
162
|
+
candidate_size: 0
|
|
163
|
+
}), {
|
|
164
|
+
error: "Run an initial scan before scanning individual apps.",
|
|
165
|
+
code: "global_scan_required"
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
assert.deepEqual(await vault.perform("automatic_set_mode", {
|
|
169
|
+
app: "demo",
|
|
170
|
+
mode: "automatic"
|
|
171
|
+
}), { app: "demo", mode: "automatic" })
|
|
172
|
+
const launchPath = path.join(home, "api", "demo", "start.js")
|
|
173
|
+
vault.automaticScans.handleStarted(launchPath)
|
|
174
|
+
await vault.automaticScans.handleStopped(launchPath)
|
|
175
|
+
assert.equal(vault.automaticScans.pendingStops.size, 0)
|
|
176
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
177
|
+
|
|
178
|
+
assert.equal((await vault.perform("scan", {
|
|
179
|
+
candidate_size: 0
|
|
180
|
+
})).started, true)
|
|
181
|
+
await waitForEngine(() =>
|
|
182
|
+
!vault.scanPromise && !vault.scanCompletionPromise)
|
|
183
|
+
|
|
184
|
+
assert.equal(await vault.globalScanReady(), true)
|
|
185
|
+
await vault.automaticScans.scanFinished({
|
|
186
|
+
scopeId: null,
|
|
187
|
+
result: { outcome: "cancelled" },
|
|
188
|
+
error: null
|
|
189
|
+
})
|
|
190
|
+
assert.equal(await vault.globalScanReady(), true)
|
|
191
|
+
await vault.automaticScans.scanFinished({
|
|
192
|
+
scopeId: null,
|
|
193
|
+
result: null,
|
|
194
|
+
error: new Error("scan failed")
|
|
195
|
+
})
|
|
196
|
+
assert.equal(await vault.globalScanReady(), true)
|
|
197
|
+
assert.equal(vault.automaticScans.active, null)
|
|
198
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
199
|
+
assert.equal((await vault.perform("scan", {
|
|
200
|
+
scope_id: source.id,
|
|
201
|
+
candidate_size: 0
|
|
202
|
+
})).started, true)
|
|
203
|
+
await waitForEngine(() =>
|
|
204
|
+
!vault.scanPromise && !vault.scanCompletionPromise)
|
|
205
|
+
await close(vault)
|
|
206
|
+
})
|
|
207
|
+
|
|
137
208
|
test("locations and anchor stores persist in the Disk Saver config", async () => {
|
|
138
209
|
const base = await makeHome()
|
|
139
210
|
const home = path.join(base, "pinokio")
|
|
@@ -903,6 +974,7 @@ describe("Save Space engine", () => {
|
|
|
903
974
|
const outside = await makeOutside()
|
|
904
975
|
const pair = await duplicatePair(home)
|
|
905
976
|
await vault.sweeper.scan()
|
|
977
|
+
assert.equal(await vault.globalScanReady(), true)
|
|
906
978
|
const duplicate = [...await vault.registry.files({
|
|
907
979
|
statuses: ["duplicate"]
|
|
908
980
|
})][0]
|
|
@@ -922,6 +994,7 @@ describe("Save Space engine", () => {
|
|
|
922
994
|
replacement.sizeThreshold = pair.contents.length * 2
|
|
923
995
|
|
|
924
996
|
assert.deepEqual(replacement.configuredLocations(), [])
|
|
997
|
+
assert.equal(await replacement.globalScanReady(), false)
|
|
925
998
|
assert.equal(await replacement.registry.countFiles(), 0)
|
|
926
999
|
assert.equal(fs.existsSync(anchorPath), true)
|
|
927
1000
|
await replacement.sweeper.scan()
|
package/test/vault-ui.test.js
CHANGED
|
@@ -3,7 +3,7 @@ const assert = require("node:assert/strict")
|
|
|
3
3
|
const fs = require("node:fs")
|
|
4
4
|
const path = require("node:path")
|
|
5
5
|
const ejs = require("ejs")
|
|
6
|
-
const { JSDOM } = require("jsdom")
|
|
6
|
+
const { JSDOM, VirtualConsole } = require("jsdom")
|
|
7
7
|
|
|
8
8
|
const root = path.resolve(__dirname, "..")
|
|
9
9
|
const publicRoot = path.join(root, "server", "public")
|
|
@@ -49,6 +49,7 @@ const fixture = (items = [], overrides = {}) => {
|
|
|
49
49
|
const shared = items.filter((entry) => entry.status === "shared")
|
|
50
50
|
return Object.assign({
|
|
51
51
|
enabled: true,
|
|
52
|
+
global_scan_ready: true,
|
|
52
53
|
mode: "link",
|
|
53
54
|
scan: {
|
|
54
55
|
active: false,
|
|
@@ -152,16 +153,24 @@ const makePage = async (status, options = {}) => {
|
|
|
152
153
|
const workspace = ejs.render(await source(workspacePath), {
|
|
153
154
|
appMode
|
|
154
155
|
})
|
|
156
|
+
const navigationErrors = []
|
|
157
|
+
const virtualConsole = options.captureNavigation
|
|
158
|
+
? new VirtualConsole().on("jsdomError", (error) => {
|
|
159
|
+
navigationErrors.push(error)
|
|
160
|
+
})
|
|
161
|
+
: undefined
|
|
155
162
|
const dom = new JSDOM(
|
|
156
163
|
`<body data-platform="${process.platform}" data-agent="electron" data-vault-mode="${appMode ? "app" : "global"}" data-vault-scope="${scopeId}" data-vault-app="${appMode ? "app" : ""}" data-vault-home="${homePath}">${workspace}</body>`,
|
|
157
164
|
{
|
|
158
165
|
runScripts: "outside-only",
|
|
166
|
+
virtualConsole,
|
|
159
167
|
url: appMode
|
|
160
168
|
? "http://localhost/vault/app/app"
|
|
161
169
|
: "http://localhost/vault"
|
|
162
170
|
}
|
|
163
171
|
)
|
|
164
172
|
const requests = []
|
|
173
|
+
const parentMessages = []
|
|
165
174
|
const selectedSources = new Set(options.folderDiscoverySelected || [])
|
|
166
175
|
const recommendedSources = new Set()
|
|
167
176
|
const confirmations = []
|
|
@@ -249,6 +258,16 @@ const makePage = async (status, options = {}) => {
|
|
|
249
258
|
}
|
|
250
259
|
dom.window.requestAnimationFrame = (callback) =>
|
|
251
260
|
dom.window.setTimeout(callback, 0)
|
|
261
|
+
if (options.embeddedApp) {
|
|
262
|
+
Object.defineProperty(dom.window, "parent", {
|
|
263
|
+
configurable: true,
|
|
264
|
+
value: {
|
|
265
|
+
postMessage(message, targetOrigin) {
|
|
266
|
+
parentMessages.push({ message, targetOrigin })
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
252
271
|
if (options.fastStatusRetry) {
|
|
253
272
|
const setTimeout = dom.window.setTimeout.bind(dom.window)
|
|
254
273
|
dom.window.setTimeout = (callback, delay, ...args) =>
|
|
@@ -370,7 +389,8 @@ const makePage = async (status, options = {}) => {
|
|
|
370
389
|
}
|
|
371
390
|
dom.window.eval(await source(path.join(publicRoot, "storage-size.js")))
|
|
372
391
|
dom.window.eval(await source(path.join(publicRoot, "vault.js")))
|
|
373
|
-
await waitFor(() => dom.window.document.querySelector(
|
|
392
|
+
await waitFor(() => dom.window.document.querySelector(
|
|
393
|
+
".vault-summary-value"))
|
|
374
394
|
const choosePickedPath = (folderPath) => {
|
|
375
395
|
const pending = pendingPickers.shift()
|
|
376
396
|
if (!pending) throw new Error("No folder picker is waiting for a selection.")
|
|
@@ -398,6 +418,8 @@ const makePage = async (status, options = {}) => {
|
|
|
398
418
|
return {
|
|
399
419
|
dom,
|
|
400
420
|
requests,
|
|
421
|
+
parentMessages,
|
|
422
|
+
navigationErrors,
|
|
401
423
|
confirmations,
|
|
402
424
|
pickerRequests,
|
|
403
425
|
choosePickedPath,
|
|
@@ -444,7 +466,7 @@ describe("Save Space interface", () => {
|
|
|
444
466
|
assert.doesNotMatch(combined, /Previous completed results were kept\./)
|
|
445
467
|
assert.match(combined, /Scan completed with exclusions/)
|
|
446
468
|
assert.match(combined, /Provisional until the scan completes/)
|
|
447
|
-
assert.
|
|
469
|
+
assert.match(combined, /Scan all locations/)
|
|
448
470
|
assert.match(combined, /data-find-home-folder/)
|
|
449
471
|
assert.match(combined, /data-find-other-folder/)
|
|
450
472
|
assert.doesNotMatch(combined,
|
|
@@ -2204,6 +2226,133 @@ describe("Save Space interface", () => {
|
|
|
2204
2226
|
dom.window.close()
|
|
2205
2227
|
})
|
|
2206
2228
|
|
|
2229
|
+
test("an app without a global baseline shows setup and starts a global scan", async () => {
|
|
2230
|
+
const status = fixture([], {
|
|
2231
|
+
global_scan_ready: false,
|
|
2232
|
+
last_scan: null,
|
|
2233
|
+
logical_bytes: 0,
|
|
2234
|
+
saved_by_sharing: 0,
|
|
2235
|
+
pending_bytes: 0
|
|
2236
|
+
})
|
|
2237
|
+
const { dom, requests, parentMessages } = await makePage(status, {
|
|
2238
|
+
appMode: true,
|
|
2239
|
+
embeddedApp: true,
|
|
2240
|
+
scopeId: "app:app",
|
|
2241
|
+
actionResults: {
|
|
2242
|
+
scan: { started: true },
|
|
2243
|
+
automatic_set_mode: { app: "app", mode: "manual" }
|
|
2244
|
+
}
|
|
2245
|
+
})
|
|
2246
|
+
const document = dom.window.document
|
|
2247
|
+
const scanButton = document.getElementById("btn-scan")
|
|
2248
|
+
|
|
2249
|
+
assert.match(document.querySelector(".vault-summary-value").textContent,
|
|
2250
|
+
/Run an initial scan to enable app scans/)
|
|
2251
|
+
assert.match(document.querySelector(".vault-setup-copy").textContent,
|
|
2252
|
+
/creates the file index used to compare apps/)
|
|
2253
|
+
assert.match(scanButton.textContent, /Scan all locations/)
|
|
2254
|
+
assert.equal(scanButton.classList.contains("primary"), true)
|
|
2255
|
+
assert.equal(document.getElementById("vault-candidate-size").hidden, true)
|
|
2256
|
+
assert.equal(document.getElementById("vault-explorer").style.display,
|
|
2257
|
+
"none")
|
|
2258
|
+
assert.doesNotMatch(document.getElementById("vault-metrics").textContent,
|
|
2259
|
+
/Nothing else to save/)
|
|
2260
|
+
|
|
2261
|
+
await waitFor(() => document.querySelector(
|
|
2262
|
+
'[data-automatic-mode="manual"]'))
|
|
2263
|
+
document.querySelector('[data-automatic-mode="manual"]').click()
|
|
2264
|
+
await waitFor(() => requests.some((request) =>
|
|
2265
|
+
request.action === "automatic_set_mode"))
|
|
2266
|
+
await waitFor(() => document.getElementById(
|
|
2267
|
+
"vault-auto-mode").classList.contains("manual"))
|
|
2268
|
+
assert.match(document.querySelector(".vault-summary-value").textContent,
|
|
2269
|
+
/Run an initial scan to enable app scans/)
|
|
2270
|
+
assert.equal(document.getElementById("vault-explorer").style.display,
|
|
2271
|
+
"none")
|
|
2272
|
+
|
|
2273
|
+
scanButton.click()
|
|
2274
|
+
await waitFor(() => requests.some((request) =>
|
|
2275
|
+
request.action === "scan"))
|
|
2276
|
+
assert.deepEqual(requests.find((request) => request.action === "scan"), {
|
|
2277
|
+
action: "scan",
|
|
2278
|
+
scope_id: null,
|
|
2279
|
+
candidate_size: 100 * (process.platform === "win32" ? 1024 : 1000) ** 2
|
|
2280
|
+
})
|
|
2281
|
+
await waitFor(() => parentMessages.length === 1)
|
|
2282
|
+
assert.equal(parentMessages[0].message.e, "vault-open-global")
|
|
2283
|
+
assert.equal(parentMessages[0].targetOrigin, "http://localhost")
|
|
2284
|
+
|
|
2285
|
+
dom.window.close()
|
|
2286
|
+
})
|
|
2287
|
+
|
|
2288
|
+
test("app setup navigates to global Disk Saver when standalone", async () => {
|
|
2289
|
+
const status = fixture([], {
|
|
2290
|
+
global_scan_ready: false,
|
|
2291
|
+
last_scan: null
|
|
2292
|
+
})
|
|
2293
|
+
const { dom, requests, navigationErrors } = await makePage(status, {
|
|
2294
|
+
appMode: true,
|
|
2295
|
+
captureNavigation: true,
|
|
2296
|
+
scopeId: "app:app",
|
|
2297
|
+
actionResults: { scan: { started: true } }
|
|
2298
|
+
})
|
|
2299
|
+
|
|
2300
|
+
dom.window.document.getElementById("btn-scan").click()
|
|
2301
|
+
await waitFor(() => requests.some((request) =>
|
|
2302
|
+
request.action === "scan"))
|
|
2303
|
+
await waitFor(() => navigationErrors.length > 0)
|
|
2304
|
+
|
|
2305
|
+
assert.deepEqual(requests.find((request) => request.action === "scan"), {
|
|
2306
|
+
action: "scan",
|
|
2307
|
+
scope_id: null,
|
|
2308
|
+
candidate_size: 100 *
|
|
2309
|
+
(process.platform === "win32" ? 1024 : 1000) ** 2
|
|
2310
|
+
})
|
|
2311
|
+
assert.match(navigationErrors[0].message,
|
|
2312
|
+
/Not implemented: navigation/)
|
|
2313
|
+
|
|
2314
|
+
dom.window.close()
|
|
2315
|
+
})
|
|
2316
|
+
|
|
2317
|
+
test("app setup remains visible during global scan progress", async () => {
|
|
2318
|
+
let progressRequests = 0
|
|
2319
|
+
const status = fixture([], {
|
|
2320
|
+
global_scan_ready: false,
|
|
2321
|
+
scan: {
|
|
2322
|
+
active: true,
|
|
2323
|
+
pending: false,
|
|
2324
|
+
phase: "walking",
|
|
2325
|
+
queued: 0,
|
|
2326
|
+
scope_id: null
|
|
2327
|
+
},
|
|
2328
|
+
last_scan: null,
|
|
2329
|
+
logical_bytes: 0,
|
|
2330
|
+
saved_by_sharing: 0,
|
|
2331
|
+
pending_bytes: 0
|
|
2332
|
+
})
|
|
2333
|
+
const { dom } = await makePage((url) => {
|
|
2334
|
+
if (url.includes("progress=1")) progressRequests += 1
|
|
2335
|
+
return status
|
|
2336
|
+
}, {
|
|
2337
|
+
appMode: true,
|
|
2338
|
+
scopeId: "app:app"
|
|
2339
|
+
})
|
|
2340
|
+
const document = dom.window.document
|
|
2341
|
+
|
|
2342
|
+
for (let attempt = 0; attempt < 400 && !progressRequests; attempt++) {
|
|
2343
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
2344
|
+
}
|
|
2345
|
+
assert.ok(progressRequests)
|
|
2346
|
+
assert.match(document.querySelector(".vault-summary-value").textContent,
|
|
2347
|
+
/Run an initial scan to enable app scans/)
|
|
2348
|
+
assert.equal(document.getElementById("vault-explorer").style.display,
|
|
2349
|
+
"none")
|
|
2350
|
+
assert.equal(document.getElementById("vault-result").textContent, "")
|
|
2351
|
+
assert.equal(document.getElementById("vault-action-state").textContent, "")
|
|
2352
|
+
|
|
2353
|
+
dom.window.close()
|
|
2354
|
+
})
|
|
2355
|
+
|
|
2207
2356
|
test("app mode uses full-width results without redundant locations", async () => {
|
|
2208
2357
|
const status = fixture([item()])
|
|
2209
2358
|
const { dom, requests } = await makePage(status, {
|