pinokiod 8.0.57 → 8.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/vault/automatic_scans.js +58 -8
- package/kernel/vault/index.js +33 -1
- package/package.json +1 -1
- package/server/index.js +3 -0
- package/server/public/app-vault-mode.js +14 -5
- package/server/public/layout.js +144 -6
- package/server/public/vault.css +25 -0
- package/server/public/vault.js +135 -24
- package/server/views/app.ejs +17 -3
- package/server/views/layout.ejs +15 -3
- package/test/vault-automatic-scan-ui.test.js +273 -2
- package/test/vault-automatic-scans.test.js +41 -2
- package/test/vault-engine.test.js +73 -0
- package/test/vault-ui.test.js +152 -3
|
@@ -41,6 +41,7 @@ class AutomaticScans {
|
|
|
41
41
|
this.waitingFor = null
|
|
42
42
|
this.listeners = new Set()
|
|
43
43
|
this.appTransitions = new Map()
|
|
44
|
+
this.globalScanReady = false
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
log(event, details = {}) {
|
|
@@ -134,7 +135,8 @@ class AutomaticScans {
|
|
|
134
135
|
snapshot() {
|
|
135
136
|
return {
|
|
136
137
|
enabled: !!this.vault.enabled,
|
|
137
|
-
|
|
138
|
+
global_scan_ready: this.globalScanReady,
|
|
139
|
+
rows: (this.globalScanReady ? [...this.entries.values()] : [])
|
|
138
140
|
.filter((entry) => !entry.hidden)
|
|
139
141
|
.sort((left, right) =>
|
|
140
142
|
(Number(left.updated_at) || 0) -
|
|
@@ -147,13 +149,22 @@ 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"
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
broadcast() {
|
|
165
|
+
broadcast(completion = null) {
|
|
156
166
|
const snapshot = this.snapshot()
|
|
167
|
+
if (completion) snapshot.completion = completion
|
|
157
168
|
for (const listener of this.listeners) {
|
|
158
169
|
try {
|
|
159
170
|
listener(snapshot)
|
|
@@ -235,6 +246,10 @@ class AutomaticScans {
|
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
queueApp(app) {
|
|
249
|
+
if (!this.globalScanReady) {
|
|
250
|
+
this.log("queue-skipped", { app, reason: "global-scan-required" })
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
238
253
|
if (this.modeFor(app) === "manual") {
|
|
239
254
|
this.log("queue-skipped", { app, reason: "manual" })
|
|
240
255
|
return false
|
|
@@ -323,6 +338,12 @@ class AutomaticScans {
|
|
|
323
338
|
this.log("preparing", { app })
|
|
324
339
|
await this.vault.ensureRegistryInitialized()
|
|
325
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
|
+
}
|
|
326
347
|
await this.hydrate()
|
|
327
348
|
await this.withAppTransition(app, async () => {
|
|
328
349
|
if (this.pendingStops.get(app) !== pending) return
|
|
@@ -394,6 +415,11 @@ class AutomaticScans {
|
|
|
394
415
|
if (!this.hydrated && typeof this.vault.automaticScanStatus === "function") {
|
|
395
416
|
await this.vault.automaticScanStatus()
|
|
396
417
|
}
|
|
418
|
+
await this.refreshGlobalScanReady()
|
|
419
|
+
if (!this.globalScanReady) {
|
|
420
|
+
this.log("check-skipped", { app, reason: "global-scan-required" })
|
|
421
|
+
return
|
|
422
|
+
}
|
|
397
423
|
if (this.modeFor(app) === "manual") {
|
|
398
424
|
this.log("check-skipped", { app, reason: "manual" })
|
|
399
425
|
return
|
|
@@ -546,14 +572,16 @@ class AutomaticScans {
|
|
|
546
572
|
|
|
547
573
|
async drain() {
|
|
548
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
|
|
549
580
|
if (this.manualDepth > 0 || this.currentBusyPromise() ||
|
|
550
581
|
this.vault.fileActionProgress) {
|
|
551
582
|
this.waitForBusyWork()
|
|
552
583
|
return
|
|
553
584
|
}
|
|
554
|
-
const entry = [...this.entries.values()].find((item) =>
|
|
555
|
-
item.state === "checking")
|
|
556
|
-
if (!entry) return
|
|
557
585
|
let appAvailable
|
|
558
586
|
try {
|
|
559
587
|
appAvailable = await this.appRootIsAvailable(entry.app)
|
|
@@ -617,6 +645,7 @@ class AutomaticScans {
|
|
|
617
645
|
|
|
618
646
|
async precheckFinished(active, result, error) {
|
|
619
647
|
const app = active.app
|
|
648
|
+
let completion = null
|
|
620
649
|
try {
|
|
621
650
|
await this.withAppTransition(app, async () => {
|
|
622
651
|
let reason = active.reason
|
|
@@ -661,13 +690,20 @@ class AutomaticScans {
|
|
|
661
690
|
? Object.assign({}, this.settings.get(app))
|
|
662
691
|
: null
|
|
663
692
|
try {
|
|
664
|
-
await this.publishResultNow(app, result)
|
|
693
|
+
const outcome = await this.publishResultNow(app, result)
|
|
694
|
+
if (outcome === "empty") {
|
|
695
|
+
completion = {
|
|
696
|
+
app,
|
|
697
|
+
outcome: "no_possible_duplicates"
|
|
698
|
+
}
|
|
699
|
+
}
|
|
665
700
|
} catch (publicationError) {
|
|
666
701
|
this.restorePrevious(app)
|
|
667
702
|
throw publicationError
|
|
668
703
|
}
|
|
669
704
|
reason = active.reason
|
|
670
705
|
if (reason) {
|
|
706
|
+
completion = null
|
|
671
707
|
await this.restorePublishedState(app, entry, previousSetting)
|
|
672
708
|
this.entries.set(app, entry)
|
|
673
709
|
this.log("publication-reverted", { app, reason })
|
|
@@ -682,7 +718,7 @@ class AutomaticScans {
|
|
|
682
718
|
})
|
|
683
719
|
} finally {
|
|
684
720
|
if (this.active === active) this.active = null
|
|
685
|
-
this.broadcast()
|
|
721
|
+
this.broadcast(completion)
|
|
686
722
|
this.schedule()
|
|
687
723
|
}
|
|
688
724
|
}
|
|
@@ -764,7 +800,7 @@ class AutomaticScans {
|
|
|
764
800
|
possible_files: possibleFiles,
|
|
765
801
|
acknowledged: acknowledged === result.signature
|
|
766
802
|
})
|
|
767
|
-
return
|
|
803
|
+
return "result"
|
|
768
804
|
}
|
|
769
805
|
const acknowledged = (this.settings.get(app) || {})
|
|
770
806
|
.acknowledged_signature
|
|
@@ -777,6 +813,7 @@ class AutomaticScans {
|
|
|
777
813
|
}
|
|
778
814
|
this.entries.delete(app)
|
|
779
815
|
this.log("no-possible-matches", { app })
|
|
816
|
+
return "empty"
|
|
780
817
|
}
|
|
781
818
|
|
|
782
819
|
async clearAutomaticState(apps, reason, options = {}) {
|
|
@@ -832,6 +869,7 @@ class AutomaticScans {
|
|
|
832
869
|
|
|
833
870
|
async scanFinished({ scopeId, result, error }) {
|
|
834
871
|
const complete = !error && result && COMPLETE_PHASES.has(result.outcome)
|
|
872
|
+
let readinessChanged = false
|
|
835
873
|
try {
|
|
836
874
|
if (complete) {
|
|
837
875
|
await this.hydrate()
|
|
@@ -842,7 +880,9 @@ class AutomaticScans {
|
|
|
842
880
|
{ cancelPending: true, states: ["checking", "result"] }
|
|
843
881
|
)
|
|
844
882
|
}
|
|
883
|
+
if (!scopeId) readinessChanged = await this.refreshGlobalScanReady()
|
|
845
884
|
} finally {
|
|
885
|
+
if (readinessChanged) this.broadcast()
|
|
846
886
|
this.schedule()
|
|
847
887
|
}
|
|
848
888
|
}
|
|
@@ -887,6 +927,16 @@ class AutomaticScans {
|
|
|
887
927
|
await this.persistMode(app, mode)
|
|
888
928
|
const entry = this.entries.get(app)
|
|
889
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
|
+
}
|
|
890
940
|
if (!this.appIsRunning(app)) {
|
|
891
941
|
this.log("mode-changed", { app, mode })
|
|
892
942
|
this.queueApp(app)
|
package/kernel/vault/index.js
CHANGED
|
@@ -73,6 +73,9 @@ const STATUS_FILTERS = new Set([
|
|
|
73
73
|
const FOLDER_DISCOVERY_COMPLETE_PHASES = new Set([
|
|
74
74
|
"complete", "completed_with_exclusions"
|
|
75
75
|
])
|
|
76
|
+
const GLOBAL_SCAN_READY_OUTCOMES = new Set([
|
|
77
|
+
"complete", "completed_with_exclusions"
|
|
78
|
+
])
|
|
76
79
|
|
|
77
80
|
const isMissingError = (error) => !!(error &&
|
|
78
81
|
(error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
@@ -672,9 +675,23 @@ class Vault {
|
|
|
672
675
|
await this.ensureRegistryInitialized()
|
|
673
676
|
}
|
|
674
677
|
await this.automaticScans.hydrate()
|
|
678
|
+
await this.automaticScans.refreshGlobalScanReady()
|
|
675
679
|
return this.automaticScans.snapshot()
|
|
676
680
|
}
|
|
677
681
|
|
|
682
|
+
async globalScanReady() {
|
|
683
|
+
if (!this.registry) return false
|
|
684
|
+
const scan = await this.registry.scanFor()
|
|
685
|
+
if (scan) this.lastScanCache.set("", scan)
|
|
686
|
+
else this.lastScanCache.delete("")
|
|
687
|
+
return this.globalScanIsReady(scan)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
globalScanIsReady(scan = this.lastScanCache.get("")) {
|
|
691
|
+
return !!(scan && scan.ts &&
|
|
692
|
+
GLOBAL_SCAN_READY_OUTCOMES.has(scan.outcome))
|
|
693
|
+
}
|
|
694
|
+
|
|
678
695
|
async refreshAnchorStores() {
|
|
679
696
|
let config = this.readConfig()
|
|
680
697
|
const candidates = [
|
|
@@ -1790,9 +1807,19 @@ class Vault {
|
|
|
1790
1807
|
return this.runMutation(() =>
|
|
1791
1808
|
this.removeExternalSource(payload.source_id))
|
|
1792
1809
|
case "scan": {
|
|
1793
|
-
|
|
1810
|
+
const scanSource = payload.scope_id
|
|
1811
|
+
? this.scanSource(payload.scope_id)
|
|
1812
|
+
: null
|
|
1813
|
+
if (payload.scope_id && !scanSource) {
|
|
1794
1814
|
return { error: "That scan location is no longer available." }
|
|
1795
1815
|
}
|
|
1816
|
+
if (scanSource && scanSource.kind === "app" &&
|
|
1817
|
+
!await this.globalScanReady()) {
|
|
1818
|
+
return {
|
|
1819
|
+
error: "Run an initial scan before scanning individual apps.",
|
|
1820
|
+
code: "global_scan_required"
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1796
1823
|
let threshold = this.sizeThreshold
|
|
1797
1824
|
if (payload.candidate_size != null) {
|
|
1798
1825
|
if (!CANDIDATE_SIZE_OPTIONS.includes(payload.candidate_size)) {
|
|
@@ -3266,6 +3293,9 @@ class Vault {
|
|
|
3266
3293
|
}
|
|
3267
3294
|
|
|
3268
3295
|
const lastScan = await this.scanForScope(scopeId)
|
|
3296
|
+
const globalScanReady = scopeId
|
|
3297
|
+
? await this.globalScanReady()
|
|
3298
|
+
: this.globalScanIsReady(lastScan)
|
|
3269
3299
|
const before = lastScan && Number.isFinite(lastScan.bytes_total)
|
|
3270
3300
|
? lastScan.bytes_total
|
|
3271
3301
|
: 0
|
|
@@ -3302,6 +3332,7 @@ class Vault {
|
|
|
3302
3332
|
const sourceCounts = this.sourceCountMaps(snapshot.scopeRows)
|
|
3303
3333
|
const result = {
|
|
3304
3334
|
enabled: true,
|
|
3335
|
+
global_scan_ready: globalScanReady,
|
|
3305
3336
|
mode: this.mode,
|
|
3306
3337
|
scan: this.scanStatus(),
|
|
3307
3338
|
last_scan: lastScan,
|
|
@@ -3402,6 +3433,7 @@ class Vault {
|
|
|
3402
3433
|
async progressStatus(scopeId = null) {
|
|
3403
3434
|
const result = {
|
|
3404
3435
|
enabled: !!this.enabled,
|
|
3436
|
+
global_scan_ready: this.globalScanIsReady(),
|
|
3405
3437
|
scan: this.scanStatus(),
|
|
3406
3438
|
file_action: this.fileActionStatus(scopeId),
|
|
3407
3439
|
last_scan: this.lastScanCache.get(scopeId || "") || null
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1994,9 +1994,12 @@ class Server {
|
|
|
1994
1994
|
if (vault && vault.ready) await vault.ready
|
|
1995
1995
|
result.vault_enabled = !!(vault && vault.enabled)
|
|
1996
1996
|
result.vault_automatic_mode = "automatic"
|
|
1997
|
+
result.vault_global_scan_ready = false
|
|
1997
1998
|
if (result.vault_enabled) {
|
|
1998
1999
|
try {
|
|
1999
2000
|
const snapshot = await vault.automaticScanStatus()
|
|
2001
|
+
result.vault_global_scan_ready =
|
|
2002
|
+
snapshot && snapshot.global_scan_ready === true
|
|
2000
2003
|
const setting = Array.isArray(snapshot && snapshot.settings)
|
|
2001
2004
|
? snapshot.settings.find((item) =>
|
|
2002
2005
|
item && item.app === name)
|
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
const app = status.dataset.app || ""
|
|
6
6
|
const tab = status.closest("#save-space-tab")
|
|
7
7
|
const label = status.querySelector("[data-app-vault-mode-label]")
|
|
8
|
-
const
|
|
8
|
+
const setState = (value, ready) => {
|
|
9
9
|
const mode = value === "manual" ? "manual" : "automatic"
|
|
10
|
+
const globalScanReady = ready === true
|
|
10
11
|
status.dataset.mode = mode
|
|
12
|
+
status.dataset.ready = String(globalScanReady)
|
|
11
13
|
status.hidden = false
|
|
12
|
-
if (label)
|
|
14
|
+
if (label) {
|
|
15
|
+
label.textContent = globalScanReady
|
|
16
|
+
? (mode === "automatic" ? "Auto" : "Manual")
|
|
17
|
+
: "Set up"
|
|
18
|
+
}
|
|
13
19
|
if (tab) {
|
|
14
20
|
tab.setAttribute("aria-label",
|
|
15
|
-
|
|
21
|
+
globalScanReady
|
|
22
|
+
? `Disk Saver — ${mode === "automatic" ? "Automatic" : "Manual"} checking`
|
|
23
|
+
: "Disk Saver — Set up required")
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
const applySnapshot = (snapshot) => {
|
|
@@ -20,10 +28,11 @@
|
|
|
20
28
|
? snapshot.settings
|
|
21
29
|
: []
|
|
22
30
|
const setting = settings.find((item) => item && item.app === app)
|
|
23
|
-
|
|
31
|
+
setState(setting && setting.mode,
|
|
32
|
+
snapshot && snapshot.global_scan_ready === true)
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
setState(status.dataset.mode, status.dataset.ready === "true")
|
|
27
36
|
if (!app || typeof window.EventSource !== "function") return
|
|
28
37
|
|
|
29
38
|
const source = new window.EventSource(
|
package/server/public/layout.js
CHANGED
|
@@ -866,8 +866,16 @@
|
|
|
866
866
|
<circle cx="10" cy="10" r="7.5"></circle>
|
|
867
867
|
<path d="M8 7.25v5.5M12 7.25v5.5"></path>
|
|
868
868
|
</svg>`;
|
|
869
|
+
const COMPLETE_ICON = `
|
|
870
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
871
|
+
<circle cx="10" cy="10" r="7.5"></circle>
|
|
872
|
+
<path d="m6.75 10.1 2.1 2.1 4.6-4.65"></path>
|
|
873
|
+
</svg>`;
|
|
874
|
+
const COMPLETION_VISIBLE_MS = 4000;
|
|
869
875
|
|
|
870
876
|
let eventSource = null;
|
|
877
|
+
let visibleCheckingApps = new Set();
|
|
878
|
+
const completions = new Map();
|
|
871
879
|
|
|
872
880
|
function statusText(row) {
|
|
873
881
|
if (row.state === 'paused') {
|
|
@@ -912,6 +920,97 @@
|
|
|
912
920
|
tray.hidden = tray.childElementCount === 0;
|
|
913
921
|
}
|
|
914
922
|
|
|
923
|
+
function removeCompletion(app) {
|
|
924
|
+
const completion = completions.get(app);
|
|
925
|
+
if (!completion) return;
|
|
926
|
+
if (completion.timer) window.clearTimeout(completion.timer);
|
|
927
|
+
completions.delete(app);
|
|
928
|
+
removeRow(completion.item);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function clearCompletions() {
|
|
932
|
+
[...completions.keys()].forEach(removeCompletion);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function scheduleCompletion(completion) {
|
|
936
|
+
if (completion.paused.size || completion.timer) return;
|
|
937
|
+
completion.startedAt = Date.now();
|
|
938
|
+
completion.timer = window.setTimeout(() => {
|
|
939
|
+
completion.timer = null;
|
|
940
|
+
removeCompletion(completion.app);
|
|
941
|
+
}, completion.remaining);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function setCompletionPaused(completion, reason, paused) {
|
|
945
|
+
if (paused) {
|
|
946
|
+
if (completion.paused.has(reason)) return;
|
|
947
|
+
if (!completion.paused.size && completion.timer) {
|
|
948
|
+
completion.remaining = Math.max(0,
|
|
949
|
+
completion.remaining - (Date.now() - completion.startedAt));
|
|
950
|
+
window.clearTimeout(completion.timer);
|
|
951
|
+
completion.timer = null;
|
|
952
|
+
}
|
|
953
|
+
completion.paused.add(reason);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
completion.paused.delete(reason);
|
|
957
|
+
if (!completion.paused.size) scheduleCompletion(completion);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function startCompletion(app) {
|
|
961
|
+
if (completions.has(app)) return;
|
|
962
|
+
const item = document.createElement('div');
|
|
963
|
+
item.className = 'vault-auto-scan-row';
|
|
964
|
+
item.dataset.state = 'complete';
|
|
965
|
+
|
|
966
|
+
const close = document.createElement('button');
|
|
967
|
+
close.type = 'button';
|
|
968
|
+
close.className = 'vault-auto-scan-close';
|
|
969
|
+
close.setAttribute('aria-label',
|
|
970
|
+
`Dismiss Disk Saver completion for ${app}`);
|
|
971
|
+
close.title = 'Dismiss';
|
|
972
|
+
close.textContent = '×';
|
|
973
|
+
|
|
974
|
+
const icon = document.createElement('span');
|
|
975
|
+
icon.className = 'vault-auto-scan-icon';
|
|
976
|
+
icon.innerHTML = COMPLETE_ICON;
|
|
977
|
+
|
|
978
|
+
const copy = document.createElement('span');
|
|
979
|
+
copy.className = 'vault-auto-scan-copy';
|
|
980
|
+
const appName = document.createElement('span');
|
|
981
|
+
appName.className = 'vault-auto-scan-app';
|
|
982
|
+
appName.textContent = app;
|
|
983
|
+
appName.title = app;
|
|
984
|
+
const status = document.createElement('span');
|
|
985
|
+
status.className = 'vault-auto-scan-status';
|
|
986
|
+
status.textContent = 'No possible duplicate files found';
|
|
987
|
+
copy.append(appName, status);
|
|
988
|
+
item.append(close, icon, copy);
|
|
989
|
+
|
|
990
|
+
const completion = {
|
|
991
|
+
app,
|
|
992
|
+
item,
|
|
993
|
+
timer: null,
|
|
994
|
+
remaining: COMPLETION_VISIBLE_MS,
|
|
995
|
+
startedAt: 0,
|
|
996
|
+
paused: new Set()
|
|
997
|
+
};
|
|
998
|
+
close.addEventListener('click', () => removeCompletion(app));
|
|
999
|
+
item.addEventListener('mouseenter', () =>
|
|
1000
|
+
setCompletionPaused(completion, 'pointer', true));
|
|
1001
|
+
item.addEventListener('mouseleave', () =>
|
|
1002
|
+
setCompletionPaused(completion, 'pointer', false));
|
|
1003
|
+
item.addEventListener('focusin', () =>
|
|
1004
|
+
setCompletionPaused(completion, 'focus', true));
|
|
1005
|
+
item.addEventListener('focusout', (event) => {
|
|
1006
|
+
if (!item.contains(event.relatedTarget)) {
|
|
1007
|
+
setCompletionPaused(completion, 'focus', false);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
completions.set(app, completion);
|
|
1011
|
+
scheduleCompletion(completion);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
915
1014
|
function automaticSettingsKey(app) {
|
|
916
1015
|
return `pinokio:vault:auto-settings:${encodeURIComponent(app)}`;
|
|
917
1016
|
}
|
|
@@ -968,15 +1067,41 @@
|
|
|
968
1067
|
return opened;
|
|
969
1068
|
}
|
|
970
1069
|
|
|
971
|
-
function render(snapshot) {
|
|
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
|
+
}
|
|
972
1078
|
const rows = snapshot && Array.isArray(snapshot.rows)
|
|
973
1079
|
? snapshot.rows
|
|
974
1080
|
: [];
|
|
1081
|
+
const validRows = rows.filter((row) =>
|
|
1082
|
+
row && typeof row.app === 'string' && row.app);
|
|
1083
|
+
const liveApps = new Set(validRows.map((row) => row.app));
|
|
1084
|
+
[...completions.keys()].forEach((app) => {
|
|
1085
|
+
if (liveApps.has(app)) removeCompletion(app);
|
|
1086
|
+
});
|
|
1087
|
+
const manualApps = new Set(snapshot && Array.isArray(snapshot.settings)
|
|
1088
|
+
? snapshot.settings.filter((setting) =>
|
|
1089
|
+
setting && setting.mode === 'manual').map((setting) => setting.app)
|
|
1090
|
+
: []);
|
|
1091
|
+
manualApps.forEach(removeCompletion);
|
|
1092
|
+
|
|
1093
|
+
const completion = snapshot && snapshot.completion;
|
|
1094
|
+
if (options.acceptCompletion && completion &&
|
|
1095
|
+
completion.outcome === 'no_possible_duplicates' &&
|
|
1096
|
+
typeof completion.app === 'string' && completion.app &&
|
|
1097
|
+
visibleCheckingApps.has(completion.app) &&
|
|
1098
|
+
!liveApps.has(completion.app) &&
|
|
1099
|
+
!manualApps.has(completion.app)) {
|
|
1100
|
+
startCompletion(completion.app);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
975
1103
|
const fragment = document.createDocumentFragment();
|
|
976
|
-
|
|
977
|
-
if (!row || typeof row.app !== 'string' || !row.app) {
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
1104
|
+
validRows.forEach((row) => {
|
|
980
1105
|
const item = document.createElement('div');
|
|
981
1106
|
item.className = 'vault-auto-scan-row';
|
|
982
1107
|
item.dataset.state = row.state || 'checking';
|
|
@@ -988,6 +1113,7 @@
|
|
|
988
1113
|
close.title = 'Dismiss';
|
|
989
1114
|
close.textContent = '×';
|
|
990
1115
|
close.addEventListener('click', async () => {
|
|
1116
|
+
if (row.state === 'checking') visibleCheckingApps.delete(row.app);
|
|
991
1117
|
close.disabled = true;
|
|
992
1118
|
try {
|
|
993
1119
|
const result = await requestAction(
|
|
@@ -1000,6 +1126,9 @@
|
|
|
1000
1126
|
removeRow(item);
|
|
1001
1127
|
} catch (error) {
|
|
1002
1128
|
console.warn('[Disk Saver] Automatic check dismissal failed', error);
|
|
1129
|
+
if (row.state === 'checking' && item.isConnected) {
|
|
1130
|
+
visibleCheckingApps.add(row.app);
|
|
1131
|
+
}
|
|
1003
1132
|
close.disabled = false;
|
|
1004
1133
|
}
|
|
1005
1134
|
});
|
|
@@ -1088,8 +1217,16 @@
|
|
|
1088
1217
|
item.append(close, icon, copy, controls);
|
|
1089
1218
|
fragment.appendChild(item);
|
|
1090
1219
|
});
|
|
1220
|
+
completions.forEach((completionRow) => {
|
|
1221
|
+
if (!liveApps.has(completionRow.app)) {
|
|
1222
|
+
fragment.appendChild(completionRow.item);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1091
1225
|
tray.replaceChildren(fragment);
|
|
1092
1226
|
tray.hidden = tray.childElementCount === 0;
|
|
1227
|
+
visibleCheckingApps = new Set(validRows
|
|
1228
|
+
.filter((row) => row.state === 'checking')
|
|
1229
|
+
.map((row) => row.app));
|
|
1093
1230
|
}
|
|
1094
1231
|
|
|
1095
1232
|
async function loadState() {
|
|
@@ -1119,12 +1256,13 @@
|
|
|
1119
1256
|
'/info/vault/automatic-scans/events');
|
|
1120
1257
|
eventSource.onmessage = (event) => {
|
|
1121
1258
|
try {
|
|
1122
|
-
render(JSON.parse(event.data));
|
|
1259
|
+
render(JSON.parse(event.data), { acceptCompletion: true });
|
|
1123
1260
|
} catch (error) {
|
|
1124
1261
|
console.debug('[Disk Saver] Invalid automatic check state', error);
|
|
1125
1262
|
}
|
|
1126
1263
|
};
|
|
1127
1264
|
eventSource.onerror = () => {
|
|
1265
|
+
clearCompletions();
|
|
1128
1266
|
loadState();
|
|
1129
1267
|
};
|
|
1130
1268
|
}
|
package/server/public/vault.css
CHANGED
|
@@ -218,6 +218,31 @@ body[data-vault-mode="app"] .vault-overview {
|
|
|
218
218
|
letter-spacing: -.025em;
|
|
219
219
|
line-height: 1.15;
|
|
220
220
|
}
|
|
221
|
+
.vault-setup-copy {
|
|
222
|
+
max-width: 560px;
|
|
223
|
+
margin: 8px 0 0;
|
|
224
|
+
color: var(--task-muted);
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
line-height: 1.5;
|
|
227
|
+
}
|
|
228
|
+
.vault-body.setup-required .vault-overview {
|
|
229
|
+
min-height: 260px;
|
|
230
|
+
flex: 1 1 auto;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
gap: 32px;
|
|
233
|
+
padding: 48px;
|
|
234
|
+
border-bottom: 0;
|
|
235
|
+
}
|
|
236
|
+
.vault-body.setup-required .vault-metrics.summary {
|
|
237
|
+
max-width: 620px;
|
|
238
|
+
flex: 0 1 620px;
|
|
239
|
+
grid-template-columns: minmax(0, 1fr);
|
|
240
|
+
}
|
|
241
|
+
.vault-body.setup-required .vault-summary-value {
|
|
242
|
+
margin-top: 8px;
|
|
243
|
+
font-size: 24px;
|
|
244
|
+
}
|
|
245
|
+
.vault-body.setup-required .vault-summary-side { display: none; }
|
|
221
246
|
.vault-storage-chart {
|
|
222
247
|
width: min(100%, 540px);
|
|
223
248
|
min-width: 0;
|