pinokiod 8.0.48 → 8.0.50

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.
@@ -1712,6 +1712,7 @@ class Vault {
1712
1712
  )
1713
1713
  let bytesOnDisk = 0
1714
1714
  let wouldBe = 0
1715
+ let trackedLogicalBytes = 0
1715
1716
  for (let index = 0; index < blobEntries.length; index++) {
1716
1717
  const [hash, blob] = blobEntries[index]
1717
1718
  const names = namesByHash.get(hash) || []
@@ -1723,6 +1724,7 @@ class Vault {
1723
1724
  const linkedCopy = storeStat || names.some((name) => name.mode === "link") ? 1 : 0
1724
1725
  const physicalCopies = linkedCopy + names.filter((name) => name.mode === "copy").length
1725
1726
  const pendingBytes = pendingBytesByHash.get(hash) || 0
1727
+ trackedLogicalBytes += (size * names.length) + pendingBytes
1726
1728
  bytesOnDisk += (size * physicalCopies) + pendingBytes
1727
1729
  wouldBe += (size * Math.max(physicalCopies, names.length)) + pendingBytes
1728
1730
  const orphan = !!(storeStat && storeStat.nlink === 1)
@@ -1733,6 +1735,22 @@ class Vault {
1733
1735
  blobs.push(publicBlob)
1734
1736
  blobByHash.set(hash, publicBlob)
1735
1737
  }
1738
+ if (!scopeId) {
1739
+ // The scan total covers every regular file, including files below the
1740
+ // candidate threshold and files the user kept separate. The registry
1741
+ // figures above cover only content tracked by Save space. Add the
1742
+ // remainder to both sides so the global comparison represents the
1743
+ // complete scanned locations instead of silently omitting small files.
1744
+ const lastScan = registry.scanFor()
1745
+ const scannedBytes = lastScan && Number.isFinite(lastScan.bytes_total)
1746
+ ? lastScan.bytes_total
1747
+ : null
1748
+ const untrackedScannedBytes = scannedBytes === null
1749
+ ? 0
1750
+ : Math.max(0, scannedBytes - trackedLogicalBytes)
1751
+ bytesOnDisk += untrackedScannedBytes
1752
+ wouldBe += untrackedScannedBytes
1753
+ }
1736
1754
  const duplicates = []
1737
1755
  for (const [p, entry] of registry.duplicates) {
1738
1756
  if (scopeId && entry.source_id !== scopeId) continue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.48",
3
+ "version": "8.0.50",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -300,7 +300,8 @@ body[data-vault-mode="app"] .vault-overview {
300
300
  .vault-action-state,
301
301
  .vault-result,
302
302
  .vault-feedback,
303
- .vault-cloud-warning {
303
+ .vault-cloud-warning,
304
+ .vault-cleanup-notice {
304
305
  display: none;
305
306
  align-items: center;
306
307
  min-height: 38px;
@@ -319,7 +320,8 @@ body[data-vault-mode="app"] .vault-overview {
319
320
  .vault-action-state.show,
320
321
  .vault-result.show,
321
322
  .vault-feedback.show,
322
- .vault-cloud-warning.show { display: flex; }
323
+ .vault-cloud-warning.show,
324
+ .vault-cleanup-notice.show { display: flex; }
323
325
  .vault-scan-state i,
324
326
  .vault-action-state i,
325
327
  .vault-result > i { color: var(--task-accent); }
@@ -469,6 +471,28 @@ body[data-vault-mode="app"] .vault-overview {
469
471
  .vault-cloud-warning { min-height: 34px; background: var(--vault-warning-soft); }
470
472
  .vault-cloud-warning > i { color: var(--vault-warning); }
471
473
  .vault-cloud-warning .vault-text-button { margin-left: auto; }
474
+ .vault-cleanup-notice {
475
+ flex: 0 0 auto;
476
+ border-bottom-color: color-mix(in srgb, var(--vault-warning) 45%, var(--task-border));
477
+ background: color-mix(in srgb, var(--vault-warning) 16%, var(--task-panel));
478
+ }
479
+ .vault-cleanup-notice > i { color: var(--vault-warning); }
480
+ .vault-cleanup-notice strong { flex: 0 0 auto; font-weight: 650; }
481
+ .vault-cleanup-notice-detail {
482
+ min-width: 0;
483
+ flex: 1 1 auto;
484
+ color: color-mix(in srgb, var(--task-text) 78%, var(--vault-warning));
485
+ }
486
+ .vault-cleanup-notice .vault-button {
487
+ flex: 0 0 auto;
488
+ margin-left: auto;
489
+ border-color: color-mix(in srgb, var(--vault-warning) 70%, var(--task-border-strong));
490
+ background: var(--task-panel);
491
+ font-weight: 650;
492
+ }
493
+ .vault-cleanup-notice .vault-button:hover {
494
+ background: color-mix(in srgb, var(--vault-warning) 12%, var(--task-panel));
495
+ }
472
496
  .vault-explorer {
473
497
  display: grid;
474
498
  min-height: 0;
@@ -952,6 +976,15 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
952
976
  .vault-result-actions { grid-column: 2; }
953
977
  .vault-pane-footer { align-items: flex-start; flex-direction: column; }
954
978
  .vault-result.incomplete .vault-button { margin: 0; }
979
+ .vault-cleanup-notice.show {
980
+ display: grid;
981
+ grid-template-columns: 16px minmax(0, 1fr) auto;
982
+ align-items: center;
983
+ }
984
+ .vault-cleanup-notice > i { grid-area: 1 / 1; }
985
+ .vault-cleanup-notice strong { grid-area: 1 / 2; }
986
+ .vault-cleanup-notice-detail { grid-area: 2 / 2 / 3 / 4; }
987
+ .vault-cleanup-notice .vault-button { grid-area: 1 / 3; margin-left: 0; }
955
988
  }
956
989
  @media (pointer: coarse) {
957
990
  .vault-page {
@@ -10,8 +10,9 @@ const COPY = {
10
10
  all_description: "Every scanned file and its current deduplication status.",
11
11
  duplicates_description: "Identical files waiting to be deduplicated or kept separate.",
12
12
  shared_description: "Files that share disk storage across multiple locations.",
13
+ tracked_description: "Files with no duplicate action required.",
13
14
  independent_description: "Duplicate files that remain as separate copies.",
14
- reclaimable_description: "Stored copies no longer used by any configured location.",
15
+ reclaimable_description: "Private links no longer used by any linked file.",
15
16
  activity_description: "A history of scans and changes made by Save space.",
16
17
  add_external_folder: "Add external folder",
17
18
  files_region: "Files",
@@ -21,11 +22,12 @@ const COPY = {
21
22
  external_other_disk: "Added to Locations. It can be scanned, but files on this disk cannot be deduplicated with files in your Pinokio folder.",
22
23
  pinokio_folder: "Pinokio folder",
23
24
  save_space: "Save space",
24
- disk_space_saved: "of disk space saved",
25
- saved_for_app: "{size} saved for this app",
26
- before: "Before",
27
- before_help: "Estimated space if every app stored its own copy.",
28
- after: "After",
25
+ disk_space_freed: "of disk space freed",
26
+ sharing_saves_for_app: "Sharing saves {size} for this app",
27
+ without_sharing: "Without sharing",
28
+ without_sharing_help: "Estimated size of every scanned location if each app stored its own copy. File Explorer may count deduplicated files differently.",
29
+ on_disk: "On disk",
30
+ app_share: "This app’s share",
29
31
  effective_help: "For deduplicated files, disk usage is divided evenly among every location using them.",
30
32
  nothing_more_to_save: "Nothing else to save",
31
33
  more_can_be_saved: "{size} more can be saved",
@@ -35,7 +37,8 @@ const COPY = {
35
37
  find_savings: "Scan to find duplicate files and save disk space",
36
38
  find_app_savings: "Scan this app to find duplicate files and save disk space",
37
39
  storage_details: "Storage details",
38
- saved_by_actions: "Saved by your actions",
40
+ kept_deduplicated: "Kept deduplicated",
41
+ already_shared: "Already shared before Save space",
39
42
  last_scanned: "Last scanned",
40
43
  never: "Never",
41
44
  scan: "Scan now",
@@ -86,6 +89,7 @@ const COPY = {
86
89
  search_all: "Search files",
87
90
  search_duplicates: "Search duplicates",
88
91
  search_shared: "Search deduplicated files",
92
+ search_tracked: "Search files with no action needed",
89
93
  search_skipped: "Search files kept separate",
90
94
  search_activity: "Search activity",
91
95
  search_in: "Search in {location}",
@@ -152,8 +156,13 @@ const COPY = {
152
156
  keep_separate: "Keep separate",
153
157
  kept_separate: "Kept separate",
154
158
  make_separate: "Make separate",
155
- reclaim: "Delete",
156
- reclaim_all: "Delete all",
159
+ reclaim: "Clean up",
160
+ reclaim_all: "Clean up all",
161
+ cleanup_ready: "{size} ready to clean up",
162
+ cleanup_ready_detail: "{count} left after their linked files were deleted.",
163
+ review_cleanup: "Review cleanup",
164
+ private_link: "private link",
165
+ private_links: "private links",
157
166
  undo: "Undo",
158
167
  identical_contents_at: "Identical contents at",
159
168
  no_files: "No files found",
@@ -164,26 +173,29 @@ const COPY = {
164
173
  no_duplicates_hint: "There are no files waiting for your review.",
165
174
  no_shared: "Nothing deduplicated yet",
166
175
  no_shared_hint: "Deduplicated files will appear here after you review duplicates.",
176
+ no_tracked: "No files with no action needed",
177
+ no_tracked_hint: "Files without a duplicate action will appear here after a scan.",
167
178
  no_skipped: "Nothing kept separate",
168
179
  no_skipped_hint: "Files you keep separate will appear here.",
169
- no_reclaimable: "No unused files",
170
- no_reclaimable_hint: "Copies no longer used by any configured location will appear here.",
180
+ no_reclaimable: "No cleanup needed",
181
+ no_reclaimable_hint: "Private links with no remaining linked files will appear here.",
171
182
  no_activity: "No activity yet",
172
183
  no_activity_hint: "Scans and actions will be recorded here.",
173
184
  view_all: "View all files",
174
185
  show_all_locations: "Show all locations",
175
186
  tracked_note: "Only files {size} and larger appear here. Files keep their current locations.",
176
187
  duplicate_note: "Only files waiting for review are shown.",
177
- reclaimable_note: "These copies are no longer used by any configured location. Deleting them frees disk space.",
188
+ reclaimable_note: "These private links have no remaining linked files. Cleaning them up frees disk space.",
189
+ activity_note: "Recent scans and Save space actions.",
178
190
  converted: "Deduplicated",
179
191
  files_left_separate: "{count} remained separate",
180
192
  skipped_action: "Kept separate",
181
193
  separated: "Separated",
182
- reclaimed: "Deleted",
194
+ reclaimed: "Cleaned up",
183
195
  event_convert: "Deduplicated",
184
196
  event_found: "Duplicate found",
185
197
  event_adopt: "Added",
186
- event_reclaim: "Deleted unused file",
198
+ event_reclaim: "Cleaned up unused link",
187
199
  event_undo: "Undid deduplication",
188
200
  event_diverged: "Changed by an app — no longer deduplicated",
189
201
  event_detach: "Separated",
@@ -280,6 +292,16 @@ const sourcePath = (source) => {
280
292
  if (source.kind === "app") return IS_APP_MODE ? (source.display_path || source.root || "") : ""
281
293
  return source.display_path || source.root || ""
282
294
  }
295
+ const externalLocation = (item) => {
296
+ const source = sourceById(item.source_id)
297
+ if (!source || source.kind !== "external") return ""
298
+ const base = sourcePath(source).replace(/[\\/]+$/, "")
299
+ const relative = String(item.relative_path || "").replace(/^[\\/]+/, "")
300
+ if (!base) return relative
301
+ if (!relative) return base
302
+ const separator = base.includes("\\") && !base.includes("/") ? "\\" : "/"
303
+ return `${base}${separator}${separator === "\\" ? relative.replace(/\//g, "\\") : relative}`
304
+ }
283
305
  const isDescendantSource = (candidateId, parentId) => {
284
306
  if (!parentId) return true
285
307
  if (candidateId === parentId) return true
@@ -337,9 +359,10 @@ const getCounts = (items) => ({
337
359
  all: items.length,
338
360
  duplicates: items.filter((item) => item.status === "duplicate").length,
339
361
  shared: items.filter((item) => item.status === "shared").length,
362
+ tracked: items.filter((item) => item.status === "tracked").length,
340
363
  independent: items.filter((item) => item.status === "independent").length,
341
364
  reclaimable: state.data.blobs.filter((blob) => blob.orphan).length,
342
- activity: state.data.events.length
365
+ activity: activityItems("").length
343
366
  })
344
367
 
345
368
  const sourceCounts = (items) => {
@@ -362,6 +385,7 @@ const activeItems = (items) => {
362
385
  let result = items
363
386
  if (state.view === "duplicates") result = result.filter((item) => item.status === "duplicate")
364
387
  if (state.view === "shared") result = result.filter((item) => item.status === "shared")
388
+ if (state.view === "tracked") result = result.filter((item) => item.status === "tracked")
365
389
  if (state.view === "independent") result = result.filter((item) => item.status === "independent")
366
390
  if (state.sourceId) result = result.filter((item) => isDescendantSource(item.source_id, state.sourceId))
367
391
  if (state.view === "all" && state.statusFilter !== "all") result = result.filter((item) => item.status === state.statusFilter)
@@ -374,6 +398,7 @@ const viewIcon = {
374
398
  all: "fa-regular fa-file-lines",
375
399
  duplicates: "fa-regular fa-copy",
376
400
  shared: "fa-solid fa-link",
401
+ tracked: "fa-regular fa-circle-check",
377
402
  independent: "fa-solid fa-circle-minus",
378
403
  reclaimable: "fa-regular fa-trash-can",
379
404
  activity: "fa-solid fa-wave-square"
@@ -382,6 +407,7 @@ const viewLabel = {
382
407
  all: COPY.all,
383
408
  duplicates: COPY.duplicates,
384
409
  shared: COPY.shared,
410
+ tracked: COPY.tracked,
385
411
  independent: COPY.skipped,
386
412
  reclaimable: COPY.reclaimable,
387
413
  activity: COPY.activity
@@ -397,18 +423,30 @@ const eventLabels = {
397
423
  skip: COPY.event_skip,
398
424
  reshare: COPY.event_reshare
399
425
  }
400
- const activityItems = () => {
401
- const query = state.query.toLowerCase()
402
- return state.data.events.filter((event) => !query ||
426
+ const activityItems = (queryText = state.query) => {
427
+ const query = queryText.trim().toLowerCase()
428
+ const events = state.data.events.filter((event) => !query ||
403
429
  `${eventLabels[event.kind] || event.kind} ${event.path || ""}`.toLowerCase().includes(query))
430
+ const seenBatches = new Set()
431
+ const eventItems = events.map((event) => {
432
+ const showUndo = event.kind === "convert" && event.batch_id &&
433
+ event.undoable !== false && !seenBatches.has(event.batch_id)
434
+ if (showUndo) seenBatches.add(event.batch_id)
435
+ return Object.assign({}, event, { activity_type: "event", show_undo: showUndo })
436
+ })
437
+ const batchItems = (Array.isArray(state.data.undo_batches) ? state.data.undo_batches : [])
438
+ .filter((batch) => !seenBatches.has(batch.batch_id) && (!query ||
439
+ `${COPY.event_convert} ${batch.files || 0} ${COPY.files}`.toLowerCase().includes(query)))
440
+ .map((batch) => Object.assign({}, batch, { activity_type: "batch" }))
441
+ return batchItems.concat(eventItems)
404
442
  }
405
443
 
406
444
  const renderViews = (items) => {
407
445
  const counts = getCounts(items)
408
446
  el("views-label").textContent = COPY.views
409
447
  const views = IS_APP_MODE
410
- ? ["all", "duplicates", "shared", "independent", "activity"]
411
- : ["all", "duplicates", "shared", "independent", "reclaimable", "activity"]
448
+ ? ["all", "duplicates", "shared", "tracked", "independent", "activity"]
449
+ : ["all", "duplicates", "shared", "tracked", "independent", "reclaimable", "activity"]
412
450
  el("vault-views").innerHTML = views.map((view) => `
413
451
  <button class="vault-nav-row ${state.view === view ? "selected" : ""}" type="button" data-view="${view}" ${state.view === view ? 'aria-current="page"' : ""}>
414
452
  <i class="${viewIcon[view]}"></i>
@@ -516,12 +554,14 @@ const updateToolbarSummary = (visibleItems) => {
516
554
  const searchPlaceholder = () => {
517
555
  if (state.view === "duplicates") return COPY.search_duplicates
518
556
  if (state.view === "shared") return COPY.search_shared
557
+ if (state.view === "tracked") return COPY.search_tracked
519
558
  if (state.view === "independent") return COPY.search_skipped
520
559
  if (state.view === "activity") return COPY.search_activity
521
560
  const source = selectedSource()
522
561
  return source && source.kind === "app" ? COPY.search_in.replace("{location}", source.label) : COPY.search_all
523
562
  }
524
- const supportsDisplayMode = () => state.view === "all" || state.view === "shared" || state.view === "independent"
563
+ const supportsDisplayMode = () => state.view === "all" || state.view === "shared" ||
564
+ state.view === "tracked" || state.view === "independent"
525
565
  const displayModeControl = () => supportsDisplayMode() ? `<div class="vault-display-mode" role="group" aria-label="${attr(COPY.display_mode)}">
526
566
  <button type="button" data-display-mode="folders" aria-pressed="${state.displayMode === "folders"}" class="${state.displayMode === "folders" ? "selected" : ""}">${esc(COPY.folders)}</button>
527
567
  <button type="button" data-display-mode="files" aria-pressed="${state.displayMode === "files"}" class="${state.displayMode === "files" ? "selected" : ""}">${esc(COPY.files_mode)}</button>
@@ -589,7 +629,7 @@ const sharingControl = (item) => {
589
629
  const fileDetail = (item) => {
590
630
  if (!state.expandedFiles.has(item.path) || !item.locations || item.locations.length < 2) return ""
591
631
  return `<div class="vault-detail"><div class="vault-detail-label">${esc(COPY.identical_contents_at)} ${countLabel(item.locations.length, COPY.location, COPY.locations_lower)}</div>${item.locations.map((location) => `
592
- <div class="vault-location-detail"><i class="fa-regular fa-file"></i><span>${esc(location.source_label || "")}${location.relative_path ? ` / ${esc(location.relative_path)}` : ""}</span></div>`).join("")}</div>`
632
+ <div class="vault-location-detail"><i class="fa-regular fa-file"></i><span>${esc(externalLocation(location) || [location.source_label, location.relative_path].filter(Boolean).join(" / "))}</span></div>`).join("")}</div>`
593
633
  }
594
634
 
595
635
  const renderFileRow = (item, depth = 0, showMatch = false) => {
@@ -686,7 +726,8 @@ const sourceSort = (a, b) => {
686
726
  const rank = (source) => source && source.kind === "external" ? 1 : source ? 0 : 2
687
727
  return rank(first) - rank(second) || groupTitle(first).localeCompare(groupTitle(second))
688
728
  }
689
- const flatLocation = (item) => [groupTitle(sourceById(item.source_id)), item.relative_path].filter(Boolean).join(" / ")
729
+ const flatLocation = (item) => externalLocation(item) ||
730
+ [groupTitle(sourceById(item.source_id)), item.relative_path].filter(Boolean).join(" / ")
690
731
  const renderFlatFiles = (items) => [...items]
691
732
  .sort((a, b) => compareRows(a, b, flatLocation))
692
733
  .map((item) => {
@@ -750,6 +791,7 @@ const emptyState = (view) => {
750
791
  : [COPY.no_files, COPY.no_files_hint, "fa-regular fa-folder-open"],
751
792
  duplicates: [COPY.no_duplicates, COPY.no_duplicates_hint, "fa-regular fa-circle-check"],
752
793
  shared: [COPY.no_shared, COPY.no_shared_hint, "fa-solid fa-link"],
794
+ tracked: [COPY.no_tracked, COPY.no_tracked_hint, "fa-regular fa-circle-check"],
753
795
  independent: [COPY.no_skipped, COPY.no_skipped_hint, "fa-solid fa-circle-minus"],
754
796
  reclaimable: [COPY.no_reclaimable, COPY.no_reclaimable_hint, "fa-regular fa-circle-check"],
755
797
  activity: [COPY.no_activity, COPY.no_activity_hint, "fa-solid fa-wave-square"]
@@ -763,30 +805,21 @@ const renderReclaimable = (blobs) => {
763
805
  return blobs.map((blob) => `<div class="vault-file-row"><div class="vault-name-cell"><span class="vault-disclosure"></span><i class="fa-regular fa-file vault-name-icon"></i><span class="vault-name-copy"><span class="vault-file-name">${esc(blob.names[0] ? basename(blob.names[0].path) : `${blob.hash.slice(0, 12)}…`)}</span></span></div><span class="vault-size">${fmt(blob.size)}</span><span class="vault-space">${fmt(blob.size)}</span><span class="vault-row-action"><button class="vault-text-button" type="button" data-reclaim="${attr(blob.hash)}">${esc(COPY.reclaim)}</button></span></div>`).join("")
764
806
  }
765
807
 
766
- const renderActivity = () => {
767
- const events = activityItems()
768
- const query = state.query.trim().toLowerCase()
769
- const undoBatches = Array.isArray(state.data.undo_batches)
770
- ? state.data.undo_batches.filter((batch) => !query ||
771
- `${COPY.event_convert} ${batch.files || 0} ${COPY.files}`.toLowerCase().includes(query))
772
- : []
773
- if (!events.length && !undoBatches.length) return emptyState("activity")
774
- const seenBatches = new Set()
775
- const eventRows = events.map((event) => {
776
- let undo = ""
777
- if (event.kind === "convert" && event.batch_id && event.undoable !== false && !seenBatches.has(event.batch_id)) {
778
- seenBatches.add(event.batch_id)
779
- undo = `<button class="vault-text-button" type="button" data-undo="${attr(event.batch_id)}">${esc(COPY.undo)}</button>`
808
+ const renderActivity = (items) => {
809
+ if (!items.length) return emptyState("activity")
810
+ return items.map((item) => {
811
+ if (item.activity_type === "batch") {
812
+ return `<div class="vault-file-row"><div class="vault-name-cell"><span class="vault-disclosure"></span><i class="fa-solid fa-wave-square vault-name-icon"></i><span class="vault-name-copy"><span class="vault-file-name vault-event-kind">${esc(COPY.event_convert)}</span><span class="vault-file-path">${esc(countLabel(item.files || 0))}</span></span></div><span class="vault-size">${fmt(item.bytes || 0)}</span><span class="vault-event-time">${item.ts ? esc(new Date(item.ts).toLocaleString()) : "—"}</span><span class="vault-row-action"><button class="vault-text-button" type="button" data-undo="${attr(item.batch_id)}">${esc(COPY.undo)}</button></span></div>`
780
813
  }
814
+ const event = item
815
+ const undo = event.show_undo
816
+ ? `<button class="vault-text-button" type="button" data-undo="${attr(event.batch_id)}">${esc(COPY.undo)}</button>`
817
+ : ""
781
818
  const eventPath = event.path
782
819
  ? `${event.source_label ? `${event.source_label} / ` : ""}${event.relative_path || event.path}`
783
820
  : (event.hash || "").slice(0, 12)
784
821
  return `<div class="vault-file-row"><div class="vault-name-cell"><span class="vault-disclosure"></span><i class="fa-solid fa-wave-square vault-name-icon"></i><span class="vault-name-copy"><span class="vault-file-name vault-event-kind">${esc(eventLabels[event.kind] || event.kind)}</span><span class="vault-file-path">${esc(eventPath)}</span></span></div><span class="vault-size">${event.bytes_saved ? fmt(event.bytes_saved) : event.size ? fmt(event.size) : "—"}</span><span class="vault-event-time">${esc(new Date(event.ts).toLocaleString())}</span><span class="vault-row-action">${undo}</span></div>`
785
822
  }).join("")
786
- const batchRows = undoBatches.filter((batch) => !seenBatches.has(batch.batch_id)).map((batch) =>
787
- `<div class="vault-file-row"><div class="vault-name-cell"><span class="vault-disclosure"></span><i class="fa-solid fa-wave-square vault-name-icon"></i><span class="vault-name-copy"><span class="vault-file-name vault-event-kind">${esc(COPY.event_convert)}</span><span class="vault-file-path">${esc(countLabel(batch.files || 0))}</span></span></div><span class="vault-size">${fmt(batch.bytes || 0)}</span><span class="vault-event-time">${batch.ts ? esc(new Date(batch.ts).toLocaleString()) : "—"}</span><span class="vault-row-action"><button class="vault-text-button" type="button" data-undo="${attr(batch.batch_id)}">${esc(COPY.undo)}</button></span></div>`
788
- ).join("")
789
- return batchRows + eventRows
790
823
  }
791
824
 
792
825
  const renderTable = (items) => {
@@ -804,7 +837,7 @@ const renderTable = (items) => {
804
837
  } else if (state.view === "activity") {
805
838
  tableClass = "activity"
806
839
  headers = [COPY.name, COPY.size, COPY.last_scanned, ""]
807
- body = renderActivity()
840
+ body = renderActivity(items)
808
841
  } else if (state.displayMode === "files" && supportsDisplayMode()) {
809
842
  tableClass = "flat"
810
843
  headers = [COPY.name, COPY.location_column, COPY.size, COPY.status]
@@ -837,9 +870,6 @@ const orderedItems = (items) => {
837
870
  }
838
871
 
839
872
  const pagedItems = (items) => {
840
- if (state.view === "activity") {
841
- return { items, start: 0, end: items.length, total: items.length, pages: 1 }
842
- }
843
873
  const context = JSON.stringify([
844
874
  state.view, state.sourceId, state.query, state.statusFilter,
845
875
  state.displayMode, state.sizeSort
@@ -870,6 +900,7 @@ const paneFooterText = (items) => {
870
900
  }
871
901
  if (state.view === "duplicates") return COPY.duplicate_note
872
902
  if (state.view === "reclaimable") return COPY.reclaimable_note
903
+ if (state.view === "activity") return COPY.activity_note
873
904
  return COPY.tracked_note.replace("{size}", fmt(candidateSize()))
874
905
  }
875
906
 
@@ -910,23 +941,31 @@ const renderOverview = () => {
910
941
  (!!last || beforeBytes > 0 || afterBytes > 0 || Number(data.pending_bytes) > 0)
911
942
  const afterRatio = beforeBytes ? Math.min(100, (afterBytes / beforeBytes) * 100) : 0
912
943
  const pendingBytes = Math.max(0, Number(data.pending_bytes) || 0)
944
+ // The headline reports bytes actually freed by user actions, so it always
945
+ // survives a comparison against the OS file manager. Sharing that already
946
+ // existed when a scan adopted it (package-manager hardlinks, earlier runs)
947
+ // never changed free space and stays out of this number.
948
+ const freedBytes = Math.max(0, Number(data.lifetime_bytes_saved) || 0)
949
+ const sharedNow = Math.max(0, Number(data.saved_by_sharing) || 0)
913
950
  const headline = hasComparison
914
951
  ? (IS_APP_MODE
915
- ? COPY.saved_for_app.replace("{size}", fmt(Math.max(0, beforeBytes - afterBytes)))
916
- : `${fmt(data.saved_by_sharing)} ${COPY.disk_space_saved}`)
952
+ ? COPY.sharing_saves_for_app.replace("{size}", fmt(Math.max(0, beforeBytes - afterBytes)))
953
+ : `${fmt(freedBytes)} ${COPY.disk_space_freed}`)
917
954
  : (IS_APP_MODE ? COPY.find_app_savings : COPY.find_savings)
918
- const help = IS_APP_MODE ? COPY.effective_help : COPY.before_help
955
+ const beforeLabel = COPY.without_sharing
956
+ const afterLabel = IS_APP_MODE ? COPY.app_share : COPY.on_disk
957
+ const help = IS_APP_MODE ? COPY.effective_help : COPY.without_sharing_help
919
958
  const helpId = IS_APP_MODE ? "vault-after-help" : "vault-before-help"
920
959
  const helpMarkup = `<span class="vault-compare-info" tabindex="0" aria-describedby="${helpId}"><i class="fa-regular fa-circle-question" aria-hidden="true"></i><span class="vault-compare-tooltip" id="${helpId}" role="tooltip">${esc(help)}</span></span>`
921
960
  const comparison = hasComparison ? `
922
- <div class="vault-comparison" aria-label="${attr(`${COPY.before}: ${fmt(beforeBytes)}. ${COPY.after}: ${fmt(afterBytes)}.`)}">
961
+ <div class="vault-comparison" aria-label="${attr(`${beforeLabel}: ${fmt(beforeBytes)}. ${afterLabel}: ${fmt(afterBytes)}.`)}">
923
962
  <div class="vault-compare-row">
924
- <span class="vault-compare-label">${esc(COPY.before)}${IS_APP_MODE ? "" : helpMarkup}</span>
963
+ <span class="vault-compare-label">${esc(beforeLabel)}${IS_APP_MODE ? "" : helpMarkup}</span>
925
964
  <span class="vault-compare-track"><span class="vault-compare-fill before"></span></span>
926
965
  <span class="vault-compare-value">${fmt(beforeBytes)}</span>
927
966
  </div>
928
967
  <div class="vault-compare-row">
929
- <span class="vault-compare-label">${esc(COPY.after)}${IS_APP_MODE ? helpMarkup : ""}</span>
968
+ <span class="vault-compare-label">${esc(afterLabel)}${IS_APP_MODE ? helpMarkup : ""}</span>
930
969
  <span class="vault-compare-track"><span class="vault-compare-fill after" style="--vault-after-ratio:${afterRatio.toFixed(2)}%"></span></span>
931
970
  <span class="vault-compare-value">${fmt(afterBytes)}</span>
932
971
  </div>
@@ -947,10 +986,14 @@ const renderOverview = () => {
947
986
  const storageDetails = !IS_APP_MODE && el("vault-storage-details")
948
987
  if (storageDetails) {
949
988
  const homeBytes = last ? (last.home_bytes_total == null ? last.bytes_total : last.home_bytes_total) : null
989
+ // Clamped because deleting linked files later shrinks the live sharing
990
+ // total without touching the lifetime counter.
991
+ const alreadyShared = Math.max(0, sharedNow - freedBytes)
950
992
  storageDetails.innerHTML = `<div class="vault-advanced-title">${esc(COPY.storage_details)}</div>
951
993
  <dl class="vault-storage-list">
952
994
  <div><dt>${esc(COPY.pinokio_folder)}</dt><dd>${homeBytes == null ? "—" : fmt(homeBytes)}</dd></div>
953
- <div><dt>${esc(COPY.saved_by_actions)}</dt><dd>${fmt(data.lifetime_bytes_saved)}</dd></div>
995
+ <div><dt>${esc(COPY.kept_deduplicated)}</dt><dd>${fmt(sharedNow)}</dd></div>
996
+ <div><dt>${esc(COPY.already_shared)}</dt><dd>${fmt(alreadyShared)}</dd></div>
954
997
  </dl>`
955
998
  }
956
999
  }
@@ -1188,12 +1231,34 @@ const renderCloudWarning = () => {
1188
1231
  warning.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i><span>${esc(message)}</span><button class="vault-text-button" id="btn-dismiss-cloud" type="button" data-warning-key="${attr(key)}">${esc(COPY.dismiss)}</button>`
1189
1232
  }
1190
1233
 
1234
+ const renderCleanupNotice = () => {
1235
+ const notice = el("vault-cleanup-notice")
1236
+ const blobs = state.data && Array.isArray(state.data.blobs) ? state.data.blobs : []
1237
+ const unused = !IS_APP_MODE && state.data && state.data.enabled
1238
+ ? blobs.filter((blob) => blob.orphan)
1239
+ : []
1240
+ if (!unused.length || state.view === "reclaimable") {
1241
+ notice.className = "vault-cleanup-notice"
1242
+ notice.innerHTML = ""
1243
+ return
1244
+ }
1245
+ const bytes = unused.reduce((sum, blob) => sum + (Number(blob.size) || 0), 0)
1246
+ const title = COPY.cleanup_ready.replace("{size}", fmt(bytes))
1247
+ const detail = COPY.cleanup_ready_detail.replace(
1248
+ "{count}",
1249
+ countLabel(unused.length, COPY.private_link, COPY.private_links)
1250
+ )
1251
+ notice.className = "vault-cleanup-notice show"
1252
+ 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>`
1253
+ }
1254
+
1191
1255
  const render = () => {
1192
1256
  if (!state.data) return
1193
1257
  renderOverview()
1194
1258
  renderResult()
1195
1259
  renderFeedback()
1196
1260
  renderCloudWarning()
1261
+ renderCleanupNotice()
1197
1262
  if (!state.data.enabled) {
1198
1263
  el("vault-explorer").style.display = "none"
1199
1264
  return
@@ -1263,13 +1328,16 @@ const applyFullData = (data) => {
1263
1328
  render()
1264
1329
  return scanning || !!fileAction
1265
1330
  }
1331
+ let refreshSequence = 0
1266
1332
  const refresh = async (forceFull = false) => {
1333
+ const sequence = ++refreshSequence
1267
1334
  let delay = null
1268
1335
  try {
1269
1336
  const progressOnly = !forceFull &&
1270
1337
  !!(state.data && (scanActive(state.data.scan) || state.actionProgress))
1271
1338
  if (progressOnly) {
1272
1339
  const progress = await fetchJson(statusUrl(true))
1340
+ if (sequence !== refreshSequence) return
1273
1341
  state.data.scan = progress.scan
1274
1342
  state.data.last_scan = progress.last_scan
1275
1343
  const fileAction = serverFileAction(progress.file_action)
@@ -1281,16 +1349,22 @@ const refresh = async (forceFull = false) => {
1281
1349
  renderActionProgress()
1282
1350
  renderFeedback()
1283
1351
  } else {
1284
- if (applyFullData(await fetchJson(statusUrl())) || state.scanRequested) delay = 1500
1352
+ const data = await fetchJson(statusUrl())
1353
+ if (sequence !== refreshSequence) return
1354
+ if (applyFullData(data) || state.scanRequested) delay = 1500
1285
1355
  }
1286
1356
  } else {
1287
- if (applyFullData(await fetchJson(statusUrl())) || state.scanRequested) delay = 1500
1357
+ const data = await fetchJson(statusUrl())
1358
+ if (sequence !== refreshSequence) return
1359
+ if (applyFullData(data) || state.scanRequested) delay = 1500
1288
1360
  }
1289
1361
  } catch (error) {
1362
+ if (sequence !== refreshSequence) return
1290
1363
  state.feedback = { error: true, message: error && error.message ? error.message : String(error) }
1291
1364
  renderFeedback()
1292
1365
  delay = 5000
1293
1366
  } finally {
1367
+ if (sequence !== refreshSequence) return
1294
1368
  clearTimeout(window.__vaultRefresh)
1295
1369
  window.__vaultRefresh = delay == null ? null : setTimeout(refresh, delay)
1296
1370
  }
@@ -1476,8 +1550,8 @@ document.addEventListener("click", async (event) => {
1476
1550
  state.displayMode = target.dataset.displayMode === "files" ? "files" : "folders"
1477
1551
  state.sizeSort = state.displayMode === "files" ? "desc" : null
1478
1552
  render()
1479
- } else if (target.dataset.view) {
1480
- state.view = target.dataset.view
1553
+ } else if (target.dataset.view || target.id === "btn-review-cleanup") {
1554
+ state.view = target.dataset.view || "reclaimable"
1481
1555
  state.sourceId = SCOPE_ID
1482
1556
  state.query = ""
1483
1557
  state.statusFilter = "all"
@@ -25,6 +25,7 @@
25
25
  <div class='vault-result' id='vault-result' role='status' aria-live='polite'></div>
26
26
  <div class='vault-feedback' id='vault-feedback' role='status' aria-live='polite'></div>
27
27
  <div class='vault-cloud-warning' id='vault-cloud-warning' role='status'></div>
28
+ <div class='vault-cleanup-notice' id='vault-cleanup-notice' role='status'></div>
28
29
  <section class='vault-explorer' id='vault-explorer'>
29
30
  <aside class='vault-rail'>
30
31
  <div class='vault-rail-section'>
@@ -97,6 +97,27 @@ describe('vault engine (phase 1)', () => {
97
97
  assert.strictEqual(fs.existsSync(path.resolve(h, 'vault', 'registry.json')), true)
98
98
  })
99
99
 
100
+ test('startup verification reports unused private links without deleting them', async () => {
101
+ const h = await home()
102
+ const vault = await makeVault(h)
103
+ const content = crypto.randomBytes(4096)
104
+ const hash = sha256(content)
105
+ const file = await writeFile(path.resolve(h, 'api', 'appA', 'unused.bin'), content)
106
+ await vault.adopt(file, hash, { app: 'appA' })
107
+ await vault.registry.flush()
108
+ await fs.promises.unlink(file)
109
+
110
+ const fresh = new Vault(fakeKernel(h))
111
+ await fresh.init({ existingOnly: true })
112
+ fresh.verificationPending = true
113
+ await fresh.ensureInitialized()
114
+
115
+ assert.strictEqual(fs.existsSync(fresh.storePathFor(hash)), true)
116
+ assert.strictEqual(fresh.registry.links.has(file), false)
117
+ assert.strictEqual(fresh.registry.blobs.get(hash).orphan, true)
118
+ assert.strictEqual((await fresh.status()).reclaimable, content.length)
119
+ })
120
+
100
121
  test('adopt: metadata-only, store name shares the inode', async () => {
101
122
  const h = await home()
102
123
  const vault = await makeVault(h)
@@ -674,7 +674,8 @@ describe('vault dashboard backend (phase 4)', () => {
674
674
  await vault.sweeper.scan()
675
675
  const status = await vault.status()
676
676
  assert.strictEqual(status.enabled, true)
677
- assert.strictEqual(status.bytes_on_disk, content.length)
677
+ assert.strictEqual(status.bytes_on_disk, content.length * 2)
678
+ assert.strictEqual(status.bytes_without_sharing, content.length * 2)
678
679
  assert.ok(status.last_scan && status.last_scan.files > 0)
679
680
  assert.strictEqual(status.excluded.length, 1)
680
681
  assert.strictEqual(status.excluded[0].size, content.length)
@@ -705,6 +706,79 @@ describe('vault dashboard backend (phase 4)', () => {
705
706
  assert.strictEqual(status.saved_by_sharing, 0)
706
707
  })
707
708
 
709
+ test('global storage metrics include scanned files below the candidate threshold', async () => {
710
+ const { home, vault } = await makeEnv()
711
+ const { content } = await makeSharedPair(home, vault)
712
+ const small = crypto.randomBytes(512)
713
+ await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), small)
714
+ await vault.sweeper.scan()
715
+
716
+ const status = await vault.status()
717
+ assert.strictEqual(status.last_scan.bytes_total, (content.length * 2) + small.length)
718
+ assert.strictEqual(status.bytes_on_disk, content.length + small.length)
719
+ assert.strictEqual(status.bytes_without_sharing, (content.length * 2) + small.length)
720
+ assert.strictEqual(status.saved_by_sharing, content.length)
721
+ })
722
+
723
+ test('global storage metrics do not disappear when every scanned file is below the candidate threshold', async () => {
724
+ const { home, vault } = await makeEnv()
725
+ const first = crypto.randomBytes(511)
726
+ const second = crypto.randomBytes(257)
727
+ await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), first)
728
+ await writeFile(path.resolve(home, 'api', 'appB', 'notes.txt'), second)
729
+ await vault.sweeper.scan()
730
+
731
+ const status = await vault.status()
732
+ const total = first.length + second.length
733
+ assert.strictEqual(status.last_scan.bytes_total, total)
734
+ assert.strictEqual(status.bytes_on_disk, total)
735
+ assert.strictEqual(status.bytes_without_sharing, total)
736
+ assert.strictEqual(status.saved_by_sharing, 0)
737
+ })
738
+
739
+ test('global storage metrics include below-threshold files from external locations', async (t) => {
740
+ const { home, vault } = await makeEnv()
741
+ const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-size-external-'))
742
+ homes.push(external)
743
+ const local = crypto.randomBytes(401)
744
+ const imported = crypto.randomBytes(607)
745
+ await writeFile(path.resolve(home, 'api', 'appA', 'local.json'), local)
746
+ await writeFile(path.resolve(external, 'external.json'), imported)
747
+ try {
748
+ await vault.addExternalSource(external)
749
+ } catch (error) {
750
+ t.skip(`directory links unavailable: ${error.message}`)
751
+ return
752
+ }
753
+ await vault.sweeper.scan()
754
+
755
+ const status = await vault.status()
756
+ const total = local.length + imported.length
757
+ assert.strictEqual(status.last_scan.home_bytes_total, local.length)
758
+ assert.strictEqual(status.last_scan.bytes_total, total)
759
+ assert.strictEqual(status.bytes_on_disk, total)
760
+ assert.strictEqual(status.bytes_without_sharing, total)
761
+ assert.strictEqual(status.saved_by_sharing, 0)
762
+ })
763
+
764
+ test('global storage metrics add unused managed files without double-counting scanned files', async () => {
765
+ const { home, vault } = await makeEnv()
766
+ const managed = crypto.randomBytes(4096)
767
+ const small = crypto.randomBytes(379)
768
+ const managedPath = await writeFile(path.resolve(home, 'api', 'appA', 'unused.bin'), managed)
769
+ await vault.adopt(managedPath, sha256(managed), { app: 'appA' })
770
+ await fs.promises.unlink(managedPath)
771
+ await writeFile(path.resolve(home, 'api', 'appA', 'settings.json'), small)
772
+ await vault.sweeper.scan()
773
+
774
+ const status = await vault.status()
775
+ assert.strictEqual(status.last_scan.bytes_total, small.length)
776
+ assert.strictEqual(status.reclaimable, managed.length)
777
+ assert.strictEqual(status.bytes_on_disk, managed.length + small.length)
778
+ assert.strictEqual(status.bytes_without_sharing, managed.length + small.length)
779
+ assert.strictEqual(status.saved_by_sharing, 0)
780
+ })
781
+
708
782
  test('app status exposes only that app while retaining its matching locations', async () => {
709
783
  const { home, vault } = await makeEnv()
710
784
  const content = crypto.randomBytes(4096)
@@ -1061,6 +1135,81 @@ describe('vault dashboard backend (phase 4)', () => {
1061
1135
  }
1062
1136
  })
1063
1137
 
1138
+ test('an older status response cannot replace newer scan progress', async () => {
1139
+ const views = path.resolve(__dirname, '..', 'server', 'views')
1140
+ const publicRoot = path.resolve(__dirname, '..', 'server', 'public')
1141
+ const workspace = await ejs.renderFile(
1142
+ path.resolve(views, 'partials', 'vault_workspace.ejs'),
1143
+ { appMode: false }
1144
+ )
1145
+ const baseStatus = {
1146
+ enabled: true,
1147
+ mode: 'link',
1148
+ last_scan: null,
1149
+ bytes_on_disk: 0,
1150
+ bytes_without_sharing: 0,
1151
+ saved_by_sharing: 0,
1152
+ lifetime_bytes_saved: 0,
1153
+ pending_bytes: 0,
1154
+ reclaimable: 0,
1155
+ file_action: null,
1156
+ activity_error: null,
1157
+ cloud_sync_warning: null,
1158
+ sources: [{
1159
+ id: 'pinokio', kind: 'pinokio', label: 'Pinokio', root: '/pinokio',
1160
+ display_path: '/pinokio', parent_id: null, available: true, shareable: true
1161
+ }],
1162
+ blobs: [], duplicates: [], excluded: [], events: [], undo_batches: []
1163
+ }
1164
+ const scan = (queued, currentFile) => ({
1165
+ active: true, pending: false, phase: 'analyzing', scope_id: null,
1166
+ dirs: 4, files: 8, total_files: 8, bytes_total: 8000,
1167
+ hash_total: 937, queued, current_file: currentFile,
1168
+ current_file_bytes: 0, current_file_size: 1000,
1169
+ started: 123
1170
+ })
1171
+ const response = (data) => ({ ok: true, json: async () => data })
1172
+ let requestCount = 0
1173
+ let resolveOlder
1174
+ const olderResponse = new Promise((resolve) => { resolveOlder = resolve })
1175
+ const dom = new JSDOM(
1176
+ `<body data-platform="darwin" data-vault-mode="global">${workspace}</body>`,
1177
+ { url: 'http://localhost/vault', runScripts: 'dangerously' }
1178
+ )
1179
+ dom.window.fetch = async () => {
1180
+ requestCount += 1
1181
+ if (requestCount === 1) {
1182
+ return response(Object.assign({}, baseStatus, {
1183
+ scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null }
1184
+ }))
1185
+ }
1186
+ if (requestCount === 2) return olderResponse
1187
+ return response(Object.assign({}, baseStatus, { scan: scan(115, 'newer.bin') }))
1188
+ }
1189
+ try {
1190
+ const formatter = await fs.promises.readFile(path.resolve(publicRoot, 'storage-size.js'), 'utf8')
1191
+ const source = await fs.promises.readFile(path.resolve(publicRoot, 'vault.js'), 'utf8')
1192
+ dom.window.eval(formatter)
1193
+ dom.window.eval(source.replace(/\nrefresh\(\)\s*$/, '\nwindow.__testVaultRefresh = refresh\nrefresh()'))
1194
+ await new Promise((resolve) => setTimeout(resolve, 25))
1195
+
1196
+ const olderRefresh = dom.window.__testVaultRefresh()
1197
+ await new Promise((resolve) => setTimeout(resolve, 0))
1198
+ const newerRefresh = dom.window.__testVaultRefresh()
1199
+ await newerRefresh
1200
+ assert.strictEqual(dom.window.document.querySelector('.vault-scan-percent').textContent, '87.7%')
1201
+ assert.match(dom.window.document.querySelector('.vault-scan-detail').textContent, /822 of 937 large files checked/)
1202
+
1203
+ resolveOlder(response(Object.assign({}, baseStatus, { scan: scan(204, 'older.bin') })))
1204
+ await olderRefresh
1205
+ assert.strictEqual(dom.window.document.querySelector('.vault-scan-percent').textContent, '87.7%')
1206
+ assert.match(dom.window.document.querySelector('.vault-scan-detail').textContent, /822 of 937 large files checked/)
1207
+ assert.doesNotMatch(dom.window.document.querySelector('.vault-scan-detail').textContent, /older\.bin/)
1208
+ } finally {
1209
+ dom.window.close()
1210
+ }
1211
+ })
1212
+
1064
1213
  test('item 16: vocabulary lint — vault page copy avoids forbidden terms', async () => {
1065
1214
  const forbidden = /hard.?link|junction|symlink|inode|\bblob\b|\bstore\b|\bdedupe\b|\bvault\b/i
1066
1215
  const vaultPage = await vaultPageSource()
@@ -1086,27 +1235,36 @@ describe('vault dashboard backend (phase 4)', () => {
1086
1235
  test('copy-mode tracking never claims physical sharing savings', async () => {
1087
1236
  const { home, vault } = await makeEnv()
1088
1237
  const content = crypto.randomBytes(4096)
1238
+ const small = crypto.randomBytes(383)
1089
1239
  const hash = sha256(content)
1090
1240
  const first = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
1091
1241
  const second = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
1242
+ await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), small)
1092
1243
  const firstStat = await fs.promises.stat(first)
1093
1244
  const secondStat = await fs.promises.stat(second)
1094
1245
  vault.registry.addBlob(hash, { size: content.length })
1095
1246
  vault.registry.addLink(first, { hash, app: 'appA', dev: firstStat.dev, ino: firstStat.ino, mode: 'copy' })
1096
1247
  vault.registry.addLink(second, { hash, app: 'appB', dev: secondStat.dev, ino: secondStat.ino, mode: 'copy' })
1248
+ vault.registry.setLastScan({
1249
+ ts: Date.now(),
1250
+ files: 3,
1251
+ bytes_total: (content.length * 2) + small.length,
1252
+ home_bytes_total: (content.length * 2) + small.length
1253
+ })
1097
1254
 
1098
1255
  const status = await vault.status()
1099
- assert.strictEqual(status.bytes_on_disk, content.length * 2)
1256
+ assert.strictEqual(status.bytes_on_disk, (content.length * 2) + small.length)
1257
+ assert.strictEqual(status.bytes_without_sharing, (content.length * 2) + small.length)
1100
1258
  assert.strictEqual(status.saved_by_sharing, 0)
1101
1259
  })
1102
1260
 
1103
1261
  test('repair is advanced, metrics distinguish detected and explicit savings, and refresh retries', async () => {
1104
1262
  const vaultPage = await vaultPageSource()
1105
- assert.match(vaultPage, /disk_space_saved:\s*"of disk space saved"/)
1106
- assert.match(vaultPage, /before_help:\s*"Estimated space if every app stored its own copy\."/)
1263
+ assert.match(vaultPage, /disk_space_freed:\s*"of disk space freed"/)
1264
+ assert.match(vaultPage, /without_sharing_help:\s*"Estimated size of every scanned location if each app stored its own copy\. File Explorer may count deduplicated files differently\."/)
1107
1265
  assert.match(vaultPage, /nothing_more_to_save:\s*"Nothing else to save"/)
1108
1266
  assert.match(vaultPage, /more_can_be_saved:\s*"\{size\} more can be saved"/)
1109
- assert.match(vaultPage, /fmt\(data\.saved_by_sharing\)/)
1267
+ assert.match(vaultPage, /Number\(data\.saved_by_sharing\)/)
1110
1268
  assert.match(vaultPage, /Number\(data\.bytes_without_sharing\)/)
1111
1269
  assert.match(vaultPage, /Number\(data\.bytes_on_disk\)/)
1112
1270
  assert.match(vaultPage, /Number\(data\.pending_bytes\)/)
@@ -1118,7 +1276,8 @@ describe('vault dashboard backend (phase 4)', () => {
1118
1276
  assert.match(vaultPage, /candidate_size:\s*candidateSize\(\)/)
1119
1277
  assert.match(vaultPage, /localStorage\.setItem\(candidateSizeKey/)
1120
1278
  assert.match(vaultPage, /id='vault-storage-details'/)
1121
- assert.match(vaultPage, /fmt\(data\.lifetime_bytes_saved\)/)
1279
+ assert.match(vaultPage, /Number\(data\.lifetime_bytes_saved\)/)
1280
+ assert.match(vaultPage, /Math\.max\(0, sharedNow - freedBytes\)/)
1122
1281
  assert.match(vaultPage, /repair_index:\s*"Repair index"/)
1123
1282
  assert.match(vaultPage, /<details class='vault-advanced'/)
1124
1283
  assert.doesNotMatch(vaultPage, /id='btn-rebuild'/)
@@ -1186,7 +1345,8 @@ describe('vault dashboard backend (phase 4)', () => {
1186
1345
  assert.match(vaultPage, /pinokio:vault:reviewed-scan/)
1187
1346
  assert.match(vaultPage, /completed \|\| incomplete \|\| \(!state\.scanResult && unreviewed\)/)
1188
1347
  assert.match(vaultPage, /state\.data\.undo_batches/)
1189
- assert.match(vaultPage, /data-undo="\$\{attr\(batch\.batch_id\)\}"/)
1348
+ assert.match(vaultPage, /data-undo="\$\{attr\(item\.batch_id\)\}"/)
1349
+ assert.match(vaultPage, /data-undo="\$\{attr\(event\.batch_id\)\}"/)
1190
1350
  assert.match(vaultPage, /last_scan && data\.last_scan\.hash_failures/)
1191
1351
  assert.match(vaultPage, /scan_not_analyzed:\s*"could not be analyzed"/)
1192
1352
  assert.match(vaultPage, /\.vault-progress-bar\.determinate \{[\s\S]*?transform:\s*scaleX/)
@@ -1366,7 +1526,7 @@ describe('vault dashboard backend (phase 4)', () => {
1366
1526
  await runVaultScript(dom)
1367
1527
  await new Promise((resolve) => setTimeout(resolve, 25))
1368
1528
 
1369
- assert.strictEqual(dom.window.document.querySelector('.vault-summary-value').textContent, '4.51 GB saved for this app')
1529
+ assert.strictEqual(dom.window.document.querySelector('.vault-summary-value').textContent, 'Sharing saves 4.51 GB for this app')
1370
1530
  assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-value')].map((node) => node.textContent), ['7.09 GB', '2.58 GB'])
1371
1531
  assert.match(dom.window.document.querySelector('.vault-compare-fill.after').getAttribute('style'), /--vault-after-ratio:36\.36%/)
1372
1532
  assert.strictEqual(dom.window.document.getElementById('vault-after-help').textContent,
@@ -1387,11 +1547,14 @@ describe('vault dashboard backend (phase 4)', () => {
1387
1547
  const formatter = await fs.promises.readFile(
1388
1548
  path.resolve(__dirname, '..', 'server', 'public', 'storage-size.js'), 'utf8')
1389
1549
  dom.window.eval(formatter)
1550
+ const screenshotFolderBytes = 2_002_022_055_452
1390
1551
  assert.strictEqual(dom.window.PinokioFormatStorageSize(7.05e9), '7.05 GB')
1391
1552
  assert.strictEqual(dom.window.PinokioFormatStorageSize(5.3e9), '5.3 GB')
1392
1553
  assert.strictEqual(dom.window.PinokioFormatStorageSize(1024 ** 3), '1.07 GB')
1554
+ assert.strictEqual(dom.window.PinokioFormatStorageSize(screenshotFolderBytes), '2 TB')
1393
1555
  dom.window.document.body.dataset.platform = 'win32'
1394
1556
  assert.strictEqual(dom.window.PinokioFormatStorageSize(1024 ** 3), '1 GB')
1557
+ assert.strictEqual(dom.window.PinokioFormatStorageSize(screenshotFolderBytes), '1.82 TB')
1395
1558
 
1396
1559
  const appView = await fs.promises.readFile(
1397
1560
  path.resolve(__dirname, '..', 'server', 'views', 'app.ejs'), 'utf8')
@@ -1439,24 +1602,37 @@ describe('vault dashboard backend (phase 4)', () => {
1439
1602
  await runVaultScript(dom)
1440
1603
  await new Promise((resolve) => setTimeout(resolve, 25))
1441
1604
 
1442
- assert.strictEqual(dom.window.document.querySelector('.vault-summary-value').textContent, '256.95 GB of disk space saved')
1443
- assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-label')].map((node) => node.firstChild.textContent.trim()), ['Before', 'After'])
1605
+ assert.strictEqual(dom.window.document.querySelector('.vault-summary-value').textContent, '258.77 GB of disk space freed')
1606
+ assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-label')].map((node) => node.firstChild.textContent.trim()), ['Without sharing', 'On disk'])
1444
1607
  assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-value')].map((node) => node.textContent), ['661.75 GB', '404.8 GB'])
1445
1608
  assert.match(dom.window.document.querySelector('.vault-compare-fill.after').getAttribute('style'), /--vault-after-ratio:61\.17%/)
1446
- assert.strictEqual(dom.window.document.getElementById('vault-before-help').textContent, 'Estimated space if every app stored its own copy.')
1609
+ assert.strictEqual(dom.window.document.getElementById('vault-before-help').textContent,
1610
+ 'Estimated size of every scanned location if each app stored its own copy. File Explorer may count deduplicated files differently.')
1447
1611
  assert.strictEqual(dom.window.document.querySelector('.vault-compare-info').getAttribute('tabindex'), '0')
1448
1612
  assert.match(dom.window.document.querySelector('.vault-summary-side').textContent, /13\.31 GB more can be saved/)
1449
1613
  assert.strictEqual(dom.window.document.getElementById('btn-review-metric').textContent, 'Review files')
1450
1614
  assert.strictEqual(dom.window.document.getElementById('btn-review-metric').classList.contains('primary'), true)
1451
1615
  assert.strictEqual(dom.window.document.getElementById('btn-scan').classList.contains('primary'), false)
1452
1616
  assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Pinokio folder\s*166\.22 GB/)
1453
- assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Saved by your actions\s*258\.77 GB/)
1617
+ assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Kept deduplicated\s*256\.95 GB/)
1618
+ // lifetime (241) exceeds live sharing (239.3) here, so the clamp keeps
1619
+ // the pre-existing figure at zero instead of going negative.
1620
+ assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Already shared before Save space\s*0 B/)
1621
+ const cleanupNotice = dom.window.document.getElementById('vault-cleanup-notice')
1622
+ assert.strictEqual(cleanupNotice.classList.contains('show'), true)
1623
+ assert.match(cleanupNotice.textContent, /ready to clean up/)
1624
+ assert.match(cleanupNotice.textContent, /1 private link left/)
1625
+ dom.window.document.getElementById('btn-review-cleanup').click()
1626
+ assert.strictEqual(dom.window.document.querySelector('[data-view="reclaimable"]').classList.contains('selected'), true)
1627
+ assert.strictEqual(cleanupNotice.classList.contains('show'), false)
1628
+ dom.window.document.querySelector('[data-view="all"]').click()
1454
1629
  const viewDescriptions = {
1455
1630
  all: 'Every scanned file and its current deduplication status.',
1456
1631
  duplicates: 'Identical files waiting to be deduplicated or kept separate.',
1457
1632
  shared: 'Files that share disk storage across multiple locations.',
1633
+ tracked: 'Files with no duplicate action required.',
1458
1634
  independent: 'Duplicate files that remain as separate copies.',
1459
- reclaimable: 'Stored copies no longer used by any configured location.',
1635
+ reclaimable: 'Private links no longer used by any linked file.',
1460
1636
  activity: 'A history of scans and changes made by Save space.'
1461
1637
  }
1462
1638
  for (const [view, description] of Object.entries(viewDescriptions)) {
@@ -1472,9 +1648,10 @@ describe('vault dashboard backend (phase 4)', () => {
1472
1648
  const unusedView = dom.window.document.querySelector('[data-view="reclaimable"]')
1473
1649
  assert.strictEqual(unusedView.querySelector('.vault-nav-name').textContent, 'Unused files')
1474
1650
  unusedView.click()
1475
- assert.strictEqual(dom.window.document.getElementById('btn-reclaim-all').textContent, 'Delete all')
1476
- assert.strictEqual(dom.window.document.querySelector('[data-reclaim]').textContent, 'Delete')
1477
- assert.match(dom.window.document.getElementById('vault-pane-footer').textContent, /Deleting them frees disk space/)
1651
+ assert.strictEqual(dom.window.document.getElementById('btn-reclaim-all').textContent, 'Clean up all')
1652
+ assert.strictEqual(dom.window.document.querySelector('[data-reclaim]').textContent, 'Clean up')
1653
+ assert.match(dom.window.document.getElementById('vault-pane-footer').textContent,
1654
+ /Cleaning them up frees disk space/)
1478
1655
  dom.window.close()
1479
1656
  })
1480
1657
 
@@ -1511,12 +1688,13 @@ describe('vault dashboard backend (phase 4)', () => {
1511
1688
  await new Promise((resolve) => setTimeout(resolve, 25))
1512
1689
 
1513
1690
  assert.strictEqual(dom.window.document.querySelector('.vault-summary-value').textContent,
1514
- '3 GB of disk space saved')
1691
+ '3 GB of disk space freed')
1515
1692
  assert.deepStrictEqual(
1516
1693
  [...dom.window.document.querySelectorAll('.vault-compare-value')].map((node) => node.textContent),
1517
1694
  ['10 GB', '7 GB'])
1518
1695
  assert.match(dom.window.document.querySelector('.vault-summary-side').textContent,
1519
1696
  /1 GB more can be saved.*Not scanned yet/)
1697
+ assert.strictEqual(dom.window.document.getElementById('vault-cleanup-notice').classList.contains('show'), false)
1520
1698
  dom.window.close()
1521
1699
  })
1522
1700
 
@@ -1549,8 +1727,8 @@ describe('vault dashboard backend (phase 4)', () => {
1549
1727
  assert.match(source, /headers = \[COPY\.name, COPY\.size, COPY\.can_free, ""\]/)
1550
1728
  assert.match(source, /headers = \[COPY\.name, COPY\.size, COPY\.last_scanned, ""\]/)
1551
1729
  assert.match(source, /reclaimable:\s*"Unused files"/)
1552
- assert.match(source, /reclaim:\s*"Delete"/)
1553
- assert.match(source, /reclaim_all:\s*"Delete all"/)
1730
+ assert.match(source, /reclaim:\s*"Clean up"/)
1731
+ assert.match(source, /reclaim_all:\s*"Clean up all"/)
1554
1732
  assert.match(source, /data-sort-size/)
1555
1733
  assert.match(source, /data-display-mode="folders"/)
1556
1734
  assert.match(source, /data-display-mode="files"/)
@@ -1853,6 +2031,156 @@ describe('vault dashboard backend (phase 4)', () => {
1853
2031
  }
1854
2032
  })
1855
2033
 
2034
+ test('No action needed is a counted, filtered, paginated file view', async () => {
2035
+ const views = path.resolve(__dirname, '..', 'server', 'views')
2036
+ const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
2037
+ theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
2038
+ })
2039
+ const dom = new JSDOM(html, {
2040
+ url: 'http://localhost/vault/app/appB',
2041
+ runScripts: 'dangerously'
2042
+ })
2043
+ const blobs = Array.from({ length: 501 }, (_, index) => {
2044
+ const name = `tracked-${String(index).padStart(4, '0')}.bin`
2045
+ return {
2046
+ hash: index.toString(16).padStart(64, '0'),
2047
+ size: 1024 + index,
2048
+ orphan: false,
2049
+ nlink: 2,
2050
+ names: [{
2051
+ path: `/pinokio/api/appB/${name}`,
2052
+ relative_path: name,
2053
+ source_id: 'app:appB',
2054
+ source_label: 'appB',
2055
+ mode: 'link'
2056
+ }]
2057
+ }
2058
+ })
2059
+ const status = {
2060
+ enabled: true,
2061
+ mode: 'link',
2062
+ scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
2063
+ last_scan: { ts: Date.now(), bytes_total: 1024 * 502 },
2064
+ tracked_bytes: 1024 * 502,
2065
+ effective_bytes: 1024 * 502,
2066
+ shared_bytes: 0,
2067
+ pending_bytes: 1024,
2068
+ activity_error: null,
2069
+ cloud_sync_warning: null,
2070
+ sources: [{
2071
+ id: 'app:appB', kind: 'app', label: 'appB', root: '/pinokio/api/appB',
2072
+ display_path: '/pinokio/api/appB', parent_id: null, available: true, shareable: true
2073
+ }],
2074
+ blobs,
2075
+ duplicates: [{
2076
+ path: '/pinokio/api/appB/duplicate.bin',
2077
+ relative_path: 'duplicate.bin',
2078
+ source_id: 'app:appB',
2079
+ source_label: 'appB',
2080
+ size: 1024,
2081
+ shareable: true,
2082
+ match: { path: '/pinokio/api/appA/duplicate.bin' }
2083
+ }],
2084
+ excluded: [],
2085
+ events: [],
2086
+ undo_batches: []
2087
+ }
2088
+ dom.window.fetch = async () => ({ ok: true, json: async () => status })
2089
+ await runVaultScript(dom)
2090
+ await new Promise((resolve) => setTimeout(resolve, 25))
2091
+
2092
+ const trackedView = dom.window.document.querySelector('[data-view="tracked"]')
2093
+ assert.strictEqual(trackedView.querySelector('.vault-nav-name').textContent, 'No action needed')
2094
+ assert.strictEqual(trackedView.querySelector('.vault-nav-count').textContent, '501')
2095
+ trackedView.click()
2096
+
2097
+ const rows = () => [...dom.window.document.querySelectorAll('.vault-table .vault-file-row')]
2098
+ assert.strictEqual(rows().length, 500)
2099
+ assert.strictEqual(dom.window.document.getElementById('vault-search').placeholder,
2100
+ 'Search files with no action needed')
2101
+ assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '1–500 of 501')
2102
+ assert.doesNotMatch(dom.window.document.getElementById('vault-table-wrap').textContent, /duplicate\.bin/)
2103
+
2104
+ dom.window.document.querySelector('[data-page="next"]').click()
2105
+ assert.strictEqual(rows().length, 1)
2106
+ assert.strictEqual(rows()[0].querySelector('.vault-file-name').textContent, 'tracked-0500.bin')
2107
+ assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '501–501 of 501')
2108
+ dom.window.close()
2109
+ })
2110
+
2111
+ test('Activity is paginated without repeating a batch undo action', async () => {
2112
+ const views = path.resolve(__dirname, '..', 'server', 'views')
2113
+ const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
2114
+ theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
2115
+ })
2116
+ const dom = new JSDOM(html, {
2117
+ url: 'http://localhost/vault/app/appB',
2118
+ runScripts: 'dangerously'
2119
+ })
2120
+ const events = Array.from({ length: 501 }, (_, index) => ({
2121
+ kind: 'convert',
2122
+ path: `/pinokio/api/appB/event-${String(index).padStart(4, '0')}.bin`,
2123
+ relative_path: `event-${String(index).padStart(4, '0')}.bin`,
2124
+ source_id: 'app:appB',
2125
+ source_label: 'appB',
2126
+ batch_id: 'batch-large',
2127
+ undoable: index !== 0,
2128
+ bytes_saved: 1024 + index,
2129
+ ts: Date.now() - index
2130
+ }))
2131
+ const status = {
2132
+ enabled: true,
2133
+ mode: 'link',
2134
+ scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
2135
+ last_scan: { ts: Date.now(), bytes_total: 1024 * 501 },
2136
+ tracked_bytes: 0,
2137
+ effective_bytes: 0,
2138
+ shared_bytes: 0,
2139
+ pending_bytes: 0,
2140
+ activity_error: null,
2141
+ cloud_sync_warning: null,
2142
+ sources: [{
2143
+ id: 'app:appB', kind: 'app', label: 'appB', root: '/pinokio/api/appB',
2144
+ display_path: '/pinokio/api/appB', parent_id: null, available: true, shareable: true
2145
+ }],
2146
+ blobs: [],
2147
+ duplicates: [],
2148
+ excluded: [],
2149
+ events,
2150
+ undo_batches: [{
2151
+ batch_id: 'batch-large',
2152
+ files: events.length,
2153
+ bytes: events.reduce((sum, event) => sum + event.bytes_saved, 0),
2154
+ ts: events[0].ts
2155
+ }, {
2156
+ batch_id: 'batch-compacted',
2157
+ files: 2,
2158
+ bytes: 4096,
2159
+ ts: events[events.length - 1].ts - 1
2160
+ }]
2161
+ }
2162
+ dom.window.fetch = async () => ({ ok: true, json: async () => status })
2163
+ await runVaultScript(dom)
2164
+ await new Promise((resolve) => setTimeout(resolve, 25))
2165
+
2166
+ const activityView = dom.window.document.querySelector('[data-view="activity"]')
2167
+ assert.strictEqual(activityView.querySelector('.vault-nav-count').textContent, '502')
2168
+ activityView.click()
2169
+
2170
+ const rows = () => [...dom.window.document.querySelectorAll('.vault-table.activity .vault-file-row')]
2171
+ assert.strictEqual(rows().length, 500)
2172
+ assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-large"]').length, 1)
2173
+ assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-compacted"]').length, 1)
2174
+ assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '1–500 of 502')
2175
+
2176
+ dom.window.document.querySelector('[data-page="next"]').click()
2177
+ assert.strictEqual(rows().length, 2)
2178
+ assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-large"]').length, 0)
2179
+ assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-compacted"]').length, 0)
2180
+ assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '501–502 of 502')
2181
+ dom.window.close()
2182
+ })
2183
+
1856
2184
  test('large file lists render bounded pages without narrowing Deduplicate all', async () => {
1857
2185
  const views = path.resolve(__dirname, '..', 'server', 'views')
1858
2186
  const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
@@ -2148,6 +2476,64 @@ describe('vault dashboard backend (phase 4)', () => {
2148
2476
  dom.window.close()
2149
2477
  })
2150
2478
 
2479
+ test('external file rows and expanded matches show the full target path', async () => {
2480
+ const views = path.resolve(__dirname, '..', 'server', 'views')
2481
+ const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
2482
+ theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'external:api'
2483
+ })
2484
+ const dom = new JSDOM(html, { url: 'http://localhost/vault/app/external%3Aapi', runScripts: 'dangerously' })
2485
+ const targetRoot = '/Volumes/Models/api'
2486
+ const relativePath = 'ideogram/app/comfy_models/model.safetensors'
2487
+ const externalPath = `${targetRoot}/${relativePath}`
2488
+ const status = {
2489
+ enabled: true,
2490
+ mode: 'link',
2491
+ scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
2492
+ last_scan: { ts: Date.now(), bytes_total: 4096 },
2493
+ tracked_bytes: 4096,
2494
+ effective_bytes: 2048,
2495
+ shared_bytes: 4096,
2496
+ pending_bytes: 0,
2497
+ activity_error: null,
2498
+ cloud_sync_warning: null,
2499
+ sources: [{
2500
+ id: 'external:api', kind: 'external', label: 'api', root: targetRoot,
2501
+ display_path: '/pinokio/vault/sources/api', target_path: targetRoot,
2502
+ parent_id: null, available: true, shareable: true
2503
+ }],
2504
+ blobs: [{
2505
+ hash: 'a'.repeat(64), size: 4096, orphan: false, nlink: 3,
2506
+ names: [
2507
+ {
2508
+ path: externalPath, relative_path: relativePath,
2509
+ source_id: 'external:api', source_label: 'api', mode: 'link'
2510
+ },
2511
+ {
2512
+ path: `/pinokio/api/appA/${relativePath}`, relative_path: relativePath,
2513
+ source_id: 'app:appA', source_label: 'appA', mode: 'link'
2514
+ }
2515
+ ]
2516
+ }],
2517
+ duplicates: [], excluded: [], events: [], undo_batches: []
2518
+ }
2519
+ dom.window.fetch = async () => ({ ok: true, json: async () => status })
2520
+ await runVaultScript(dom)
2521
+ await new Promise((resolve) => setTimeout(resolve, 25))
2522
+
2523
+ dom.window.document.querySelector('[data-view="shared"]').click()
2524
+ assert.strictEqual(
2525
+ dom.window.document.querySelector('.vault-flat-location').textContent,
2526
+ externalPath
2527
+ )
2528
+ dom.window.document.querySelector('[data-expand-file]').click()
2529
+ assert.deepStrictEqual(
2530
+ [...dom.window.document.querySelectorAll('.vault-location-detail span')]
2531
+ .map((node) => node.textContent),
2532
+ [externalPath, `appA / ${relativePath}`]
2533
+ )
2534
+ dom.window.close()
2535
+ })
2536
+
2151
2537
  test('vault route provisions the dev requirements needed by its folder picker', async () => {
2152
2538
  const serverSource = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'index.js'), 'utf8')
2153
2539
  const routeStart = serverSource.indexOf('this.app.get("/vault"')