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.
@@ -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
- rows: [...this.entries.values()]
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)
@@ -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
- if (payload.scope_id && !this.scanSource(payload.scope_id)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.58",
3
+ "version": "8.0.60",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
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 setMode = (value) => {
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) label.textContent = mode === "automatic" ? "Auto" : "Manual"
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
- `Disk Saver — ${mode === "automatic" ? "Automatic" : "Manual"} checking`)
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
- setMode(setting && setting.mode)
31
+ setState(setting && setting.mode,
32
+ snapshot && snapshot.global_scan_ready === true)
24
33
  }
25
34
 
26
- setMode(status.dataset.mode)
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(
@@ -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
  : [];
@@ -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;
@@ -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
- if (state.automaticMode === mode) return
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) renderOverview()
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 renderOverview = () => {
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
- const modeMenu = el("vault-auto-mode")
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) candidateSizeSelect.disabled = activeScan
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
- renderOverview()
2742
- renderResult()
2743
- renderActionProgress()
2744
- renderFeedback()
2745
- renderExternalPrompt()
2746
- renderFolderDiscovery()
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
@@ -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
- <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="Disk Saver — <%=vaultAutomaticMode === 'automatic' ? 'Automatic' : 'Manual'%> checking">
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({ data: JSON.stringify({ settings: [] }) })
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, { deferStorage: true })
83
+ const vault = await makeVault(home, {
84
+ deferStorage: true,
85
+ globalScanReady: false
86
+ })
79
87
 
80
- assert.deepEqual((await vault.automaticScanStatus()).rows, [])
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()
@@ -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(".vault-table"))
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.doesNotMatch(combined, /Scan all locations/)
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, {