pinokiod 8.0.74 → 8.0.76
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 +168 -58
- package/kernel/vault/registry_core.js +65 -23
- package/package.json +1 -1
- package/server/public/vault.css +34 -0
- package/server/public/vault.js +8 -4
- package/test/vault-automatic-scans.test.js +1325 -139
- package/test/vault-engine.test.js +7 -0
- package/test/vault-ui.test.js +62 -0
|
@@ -894,7 +894,7 @@ class AutomaticScans {
|
|
|
894
894
|
})
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
-
async
|
|
897
|
+
async currentAutomaticEntry(active, entry, counts) {
|
|
898
898
|
this.checkpoint(active)
|
|
899
899
|
let current
|
|
900
900
|
try {
|
|
@@ -915,7 +915,20 @@ class AutomaticScans {
|
|
|
915
915
|
active.app, entry, "snapshot-changed", counts)
|
|
916
916
|
return null
|
|
917
917
|
}
|
|
918
|
-
|
|
918
|
+
return {
|
|
919
|
+
stat: current,
|
|
920
|
+
entry: Object.assign({}, entry, fileSnapshot(current))
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async verifiedAutomaticHash(
|
|
925
|
+
active, entry, memory, verifiedHashes, counts, prepared = null
|
|
926
|
+
) {
|
|
927
|
+
const checked = prepared || await this.currentAutomaticEntry(
|
|
928
|
+
active, entry, counts)
|
|
929
|
+
if (!checked) return null
|
|
930
|
+
const current = checked.stat
|
|
931
|
+
const currentEntry = checked.entry
|
|
919
932
|
const key = this.automaticHashKey(currentEntry)
|
|
920
933
|
const cachedSnapshot = {
|
|
921
934
|
size: entry.cached_size,
|
|
@@ -976,16 +989,26 @@ class AutomaticScans {
|
|
|
976
989
|
async runPrecheck(active) {
|
|
977
990
|
const app = active.app
|
|
978
991
|
const root = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
992
|
+
const startedAt = Date.now()
|
|
993
|
+
active.startedAt = startedAt
|
|
994
|
+
const markStage = (stage, entry = null) => {
|
|
995
|
+
active.stage = stage
|
|
996
|
+
active.stagePath = entry && entry.path ? entry.path : null
|
|
997
|
+
active.stageKind = entry && entry.kind ? entry.kind : null
|
|
998
|
+
}
|
|
999
|
+
markStage("app-root-check")
|
|
979
1000
|
if (!await this.appRootIsAvailable(app)) {
|
|
980
1001
|
const error = new Error("That app is no longer available.")
|
|
981
1002
|
error.code = "ENOENT"
|
|
982
1003
|
throw error
|
|
983
1004
|
}
|
|
984
|
-
const startedAt = Date.now()
|
|
985
1005
|
const counts = {
|
|
986
1006
|
files: 0,
|
|
987
1007
|
bytes: 0,
|
|
988
1008
|
candidates: 0,
|
|
1009
|
+
registry_pages: 0,
|
|
1010
|
+
candidate_checks: 0,
|
|
1011
|
+
peer_checks: 0,
|
|
989
1012
|
path_errors: 0,
|
|
990
1013
|
hashed: 0,
|
|
991
1014
|
hash_bytes: 0,
|
|
@@ -993,6 +1016,7 @@ class AutomaticScans {
|
|
|
993
1016
|
hash_failures: 0,
|
|
994
1017
|
unstable_hashes: 0
|
|
995
1018
|
}
|
|
1019
|
+
markStage("threshold-read")
|
|
996
1020
|
const threshold = await this.candidateThreshold()
|
|
997
1021
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
|
998
1022
|
typeof filePath === "string" && inside(root, filePath))
|
|
@@ -1004,6 +1028,7 @@ class AutomaticScans {
|
|
|
1004
1028
|
changed_paths: paths.length,
|
|
1005
1029
|
policy: "metadata-prefilter-sha256"
|
|
1006
1030
|
})
|
|
1031
|
+
markStage("staging-reset")
|
|
1007
1032
|
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
1008
1033
|
try {
|
|
1009
1034
|
this.checkpoint(active)
|
|
@@ -1023,9 +1048,11 @@ class AutomaticScans {
|
|
|
1023
1048
|
return true
|
|
1024
1049
|
}
|
|
1025
1050
|
}
|
|
1051
|
+
markStage("changed-parent-metadata")
|
|
1026
1052
|
const safePaths = await this.changedPathsWithSafeParents(
|
|
1027
1053
|
paths, root, metadataOptions, () => this.checkpoint(active))
|
|
1028
1054
|
this.checkpoint(active)
|
|
1055
|
+
markStage("changed-path-metadata")
|
|
1029
1056
|
const safeStats = await this.statChangedPaths(
|
|
1030
1057
|
safePaths, metadataOptions, () => this.checkpoint(active))
|
|
1031
1058
|
const statsByPath = new Map(safePaths.map((filePath, index) =>
|
|
@@ -1052,95 +1079,164 @@ class AutomaticScans {
|
|
|
1052
1079
|
gid: stat.gid
|
|
1053
1080
|
})
|
|
1054
1081
|
}
|
|
1055
|
-
counts.candidates = candidates.length
|
|
1056
1082
|
if (candidates.length) {
|
|
1083
|
+
markStage("staging-write")
|
|
1057
1084
|
await this.vault.registry.stageAutomaticPrecheckFiles(app, candidates)
|
|
1058
1085
|
}
|
|
1059
1086
|
this.checkpoint(active)
|
|
1087
|
+
markStage("app-root-recheck")
|
|
1060
1088
|
if (!await this.appRootIsAvailable(app)) {
|
|
1061
1089
|
const error = new Error("That app is no longer available.")
|
|
1062
1090
|
error.code = "ENOENT"
|
|
1063
1091
|
throw error
|
|
1064
1092
|
}
|
|
1065
|
-
|
|
1093
|
+
this.log("verification-started", {
|
|
1094
|
+
app,
|
|
1095
|
+
files: counts.files,
|
|
1096
|
+
bytes: counts.bytes,
|
|
1097
|
+
threshold_candidates: candidates.length,
|
|
1098
|
+
duration_ms: Date.now() - startedAt
|
|
1099
|
+
})
|
|
1100
|
+
let proof = null
|
|
1066
1101
|
let group = null
|
|
1102
|
+
const acceptProof = (candidate, peer, hash) => {
|
|
1103
|
+
proof = { path: candidate.path, hash }
|
|
1104
|
+
this.log("first-proof-exit", {
|
|
1105
|
+
app,
|
|
1106
|
+
candidate_path: candidate.path,
|
|
1107
|
+
peer_path: peer.path,
|
|
1108
|
+
peer_kind: peer.kind,
|
|
1109
|
+
size: candidate.size,
|
|
1110
|
+
duration_ms: Date.now() - startedAt,
|
|
1111
|
+
registry_pages: counts.registry_pages,
|
|
1112
|
+
candidate_checks: counts.candidate_checks,
|
|
1113
|
+
peer_checks: counts.peer_checks,
|
|
1114
|
+
hashed: counts.hashed,
|
|
1115
|
+
hash_bytes: counts.hash_bytes,
|
|
1116
|
+
hash_reuses: counts.hash_reuses
|
|
1117
|
+
})
|
|
1118
|
+
}
|
|
1067
1119
|
const preparePeers = () => {
|
|
1068
1120
|
if (!group || group.unmatched) return
|
|
1069
1121
|
group.unmatched = new Map()
|
|
1070
1122
|
for (const [hash, value] of group.candidatesByHash) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
} else {
|
|
1076
|
-
const identity = value.identities.values().next().value
|
|
1077
|
-
group.unmatched.set(hash, {
|
|
1078
|
-
identity,
|
|
1079
|
-
candidates: value.candidates
|
|
1080
|
-
})
|
|
1081
|
-
}
|
|
1123
|
+
group.unmatched.set(hash, {
|
|
1124
|
+
identity: value.identities.values().next().value,
|
|
1125
|
+
candidate: value.candidate
|
|
1126
|
+
})
|
|
1082
1127
|
}
|
|
1083
1128
|
}
|
|
1084
|
-
const
|
|
1085
|
-
if (!group) return
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1129
|
+
const hashCandidates = async (peerIdentity = null) => {
|
|
1130
|
+
if (!group || group.candidatesHashed) return !!group
|
|
1131
|
+
const identities = new Set(group.candidates.map((candidate) =>
|
|
1132
|
+
this.automaticIdentityKey(candidate.entry)))
|
|
1133
|
+
if (identities.size < 2 &&
|
|
1134
|
+
(!peerIdentity || identities.has(peerIdentity))) {
|
|
1135
|
+
return false
|
|
1136
|
+
}
|
|
1137
|
+
group.candidatesHashed = true
|
|
1138
|
+
counts.candidates += group.candidates.length
|
|
1139
|
+
for (const candidate of group.candidates) {
|
|
1140
|
+
this.checkpoint(active)
|
|
1141
|
+
markStage("candidate-hash", candidate.entry)
|
|
1142
|
+
const verified = await this.verifiedAutomaticHash(
|
|
1143
|
+
active,
|
|
1144
|
+
candidate.entry,
|
|
1145
|
+
group.memory,
|
|
1146
|
+
verifiedHashes,
|
|
1147
|
+
counts,
|
|
1148
|
+
candidate
|
|
1149
|
+
)
|
|
1150
|
+
if (!verified) continue
|
|
1151
|
+
const hash = verified.hash
|
|
1152
|
+
let value = group.candidatesByHash.get(hash)
|
|
1153
|
+
if (!value) {
|
|
1154
|
+
value = { candidate: verified, identities: new Set() }
|
|
1155
|
+
group.candidatesByHash.set(hash, value)
|
|
1156
|
+
}
|
|
1157
|
+
value.identities.add(this.automaticIdentityKey(verified))
|
|
1158
|
+
if (value.identities.size > 1) {
|
|
1159
|
+
acceptProof(value.candidate, verified, hash)
|
|
1160
|
+
return true
|
|
1091
1161
|
}
|
|
1092
1162
|
}
|
|
1163
|
+
preparePeers()
|
|
1164
|
+
return true
|
|
1165
|
+
}
|
|
1166
|
+
const finishGroup = async () => {
|
|
1167
|
+
if (!group) return false
|
|
1168
|
+
await hashCandidates()
|
|
1169
|
+
if (proof) return true
|
|
1093
1170
|
group = null
|
|
1171
|
+
return false
|
|
1172
|
+
}
|
|
1173
|
+
const complete = async () => {
|
|
1174
|
+
this.checkpoint(active)
|
|
1175
|
+
markStage("result-signature")
|
|
1176
|
+
const result = await this.vault.registry.automaticPrecheckResult(
|
|
1177
|
+
app, proof ? [proof] : [])
|
|
1178
|
+
this.checkpoint(active)
|
|
1179
|
+
return Object.assign({}, counts, {
|
|
1180
|
+
signature: result.signature,
|
|
1181
|
+
verified_files: Math.max(0, Number(result.files) || 0),
|
|
1182
|
+
duration_ms: Date.now() - startedAt
|
|
1183
|
+
})
|
|
1094
1184
|
}
|
|
1095
1185
|
let cursor = null
|
|
1096
1186
|
do {
|
|
1187
|
+
markStage("candidate-query")
|
|
1188
|
+
counts.registry_pages += 1
|
|
1097
1189
|
const page = await this.vault.registry.automaticPrecheckEntries(
|
|
1098
1190
|
app, cursor, ENTRY_BATCH_SIZE)
|
|
1099
1191
|
this.checkpoint(active)
|
|
1100
1192
|
for (const entry of page.entries) {
|
|
1101
1193
|
const key = `${entry.dev}\0${entry.size}`
|
|
1102
1194
|
if (!group || group.key !== key) {
|
|
1103
|
-
finishGroup()
|
|
1195
|
+
if (await finishGroup()) return await complete()
|
|
1104
1196
|
group = {
|
|
1105
1197
|
key,
|
|
1198
|
+
candidates: [],
|
|
1199
|
+
candidatesHashed: false,
|
|
1106
1200
|
candidatesByHash: new Map(),
|
|
1107
|
-
matched: new Set(),
|
|
1108
1201
|
unmatched: null,
|
|
1109
1202
|
memory: new Map()
|
|
1110
1203
|
}
|
|
1111
1204
|
}
|
|
1112
1205
|
if (entry.kind === "changed") {
|
|
1113
|
-
const candidate = entry
|
|
1114
1206
|
this.checkpoint(active)
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (!value) {
|
|
1121
|
-
value = { candidates: [], identities: new Set() }
|
|
1122
|
-
group.candidatesByHash.set(hash, value)
|
|
1123
|
-
}
|
|
1124
|
-
value.candidates.push(verified)
|
|
1125
|
-
value.identities.add(this.automaticIdentityKey(verified))
|
|
1207
|
+
markStage("candidate-metadata", entry)
|
|
1208
|
+
counts.candidate_checks += 1
|
|
1209
|
+
const candidate = await this.currentAutomaticEntry(
|
|
1210
|
+
active, entry, counts)
|
|
1211
|
+
if (candidate) group.candidates.push(candidate)
|
|
1126
1212
|
continue
|
|
1127
1213
|
}
|
|
1128
|
-
|
|
1214
|
+
if (!group.candidatesHashed) await hashCandidates()
|
|
1215
|
+
if (proof) return await complete()
|
|
1216
|
+
if (group.candidatesHashed && !group.unmatched.size) continue
|
|
1217
|
+
markStage("peer-metadata", entry)
|
|
1218
|
+
counts.peer_checks += 1
|
|
1219
|
+
const peer = await this.currentAutomaticEntry(active, entry, counts)
|
|
1220
|
+
if (!peer) continue
|
|
1221
|
+
const identity = this.automaticIdentityKey(peer.entry)
|
|
1222
|
+
if (!await hashCandidates(identity)) continue
|
|
1223
|
+
if (proof) return await complete()
|
|
1129
1224
|
if (!group.unmatched.size) continue
|
|
1225
|
+
if (![...group.unmatched.values()].some((wanted) =>
|
|
1226
|
+
wanted.identity !== identity)) continue
|
|
1130
1227
|
this.checkpoint(active)
|
|
1228
|
+
markStage("peer-hash", entry)
|
|
1131
1229
|
const verified = await this.verifiedAutomaticHash(
|
|
1132
|
-
active, entry, group.memory, verifiedHashes, counts)
|
|
1230
|
+
active, entry, group.memory, verifiedHashes, counts, peer)
|
|
1133
1231
|
const wanted = verified && group.unmatched.get(verified.hash)
|
|
1134
|
-
const identity = verified && this.automaticIdentityKey(verified)
|
|
1135
1232
|
if (!wanted || wanted.identity === identity) {
|
|
1136
1233
|
continue
|
|
1137
1234
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
group.unmatched.delete(verified.hash)
|
|
1235
|
+
acceptProof(wanted.candidate, verified, verified.hash)
|
|
1236
|
+
return await complete()
|
|
1142
1237
|
}
|
|
1143
1238
|
if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
|
|
1239
|
+
markStage("hash-cache-write")
|
|
1144
1240
|
await this.vault.registry.rememberHashCache(
|
|
1145
1241
|
[...verifiedHashes.values()])
|
|
1146
1242
|
verifiedHashes.clear()
|
|
@@ -1148,24 +1244,26 @@ class AutomaticScans {
|
|
|
1148
1244
|
}
|
|
1149
1245
|
cursor = page.next_cursor
|
|
1150
1246
|
} while (cursor)
|
|
1151
|
-
finishGroup()
|
|
1152
|
-
|
|
1153
|
-
const result = await this.vault.registry.automaticPrecheckResult(
|
|
1154
|
-
app, verifiedMatches)
|
|
1155
|
-
this.checkpoint(active)
|
|
1156
|
-
return Object.assign({}, counts, {
|
|
1157
|
-
signature: result.signature,
|
|
1158
|
-
verified_files: Math.max(0, Number(result.files) || 0),
|
|
1159
|
-
duration_ms: Date.now() - startedAt
|
|
1160
|
-
})
|
|
1247
|
+
await finishGroup()
|
|
1248
|
+
return await complete()
|
|
1161
1249
|
} finally {
|
|
1162
1250
|
try {
|
|
1163
1251
|
if (verifiedHashes.size) {
|
|
1164
|
-
|
|
1165
|
-
|
|
1252
|
+
try {
|
|
1253
|
+
await this.vault.registry.rememberHashCache(
|
|
1254
|
+
[...verifiedHashes.values()])
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
markStage("hash-cache-write")
|
|
1257
|
+
throw error
|
|
1258
|
+
}
|
|
1166
1259
|
}
|
|
1167
1260
|
} finally {
|
|
1168
|
-
|
|
1261
|
+
try {
|
|
1262
|
+
await this.vault.registry.abortAutomaticPrecheck(app)
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
markStage("staging-cleanup")
|
|
1265
|
+
throw error
|
|
1266
|
+
}
|
|
1169
1267
|
}
|
|
1170
1268
|
}
|
|
1171
1269
|
}
|
|
@@ -1269,10 +1367,22 @@ class AutomaticScans {
|
|
|
1269
1367
|
? (error.code === "EVAULTCANCELLED" ? "cancelled" : "failed")
|
|
1270
1368
|
: "complete",
|
|
1271
1369
|
cancel_reason: reason,
|
|
1272
|
-
duration_ms: result
|
|
1370
|
+
duration_ms: result
|
|
1371
|
+
? result.duration_ms
|
|
1372
|
+
: error && active.startedAt
|
|
1373
|
+
? Date.now() - active.startedAt
|
|
1374
|
+
: undefined,
|
|
1375
|
+
failure_stage: error && active.stage,
|
|
1376
|
+
failure_path: error && (error.path || active.stagePath),
|
|
1377
|
+
failure_kind: error && active.stageKind,
|
|
1378
|
+
error_code: error && error.code,
|
|
1379
|
+
error_syscall: error && error.syscall,
|
|
1273
1380
|
files: result && result.files,
|
|
1274
1381
|
bytes: result && result.bytes,
|
|
1275
1382
|
candidates: result && result.candidates,
|
|
1383
|
+
registry_pages: result && result.registry_pages,
|
|
1384
|
+
candidate_checks: result && result.candidate_checks,
|
|
1385
|
+
peer_checks: result && result.peer_checks,
|
|
1276
1386
|
verified_files: result && result.verified_files,
|
|
1277
1387
|
hashed: result && result.hashed,
|
|
1278
1388
|
hash_bytes: result && result.hash_bytes,
|
|
@@ -2801,15 +2801,44 @@ class RegistryCore {
|
|
|
2801
2801
|
let dev = Number(after.dev) || 0
|
|
2802
2802
|
let size = Number(after.size) || 0
|
|
2803
2803
|
let afterPath = typeof after.path === "string" ? after.path : ""
|
|
2804
|
+
const possiblePeer = `(
|
|
2805
|
+
EXISTS (
|
|
2806
|
+
SELECT 1 FROM automatic_precheck_files peer
|
|
2807
|
+
WHERE peer.app = candidate.app
|
|
2808
|
+
AND peer.dev = candidate.dev
|
|
2809
|
+
AND peer.size = candidate.size
|
|
2810
|
+
AND pinokio_path_key(peer.path) !=
|
|
2811
|
+
pinokio_path_key(candidate.path)
|
|
2812
|
+
)
|
|
2813
|
+
OR EXISTS (
|
|
2814
|
+
SELECT 1 FROM files peer
|
|
2815
|
+
WHERE peer.dev = candidate.dev
|
|
2816
|
+
AND peer.size = candidate.size
|
|
2817
|
+
AND peer.unavailable_reason IS NOT 'stale'
|
|
2818
|
+
AND pinokio_path_key(peer.path) !=
|
|
2819
|
+
pinokio_path_key(candidate.path)
|
|
2820
|
+
)
|
|
2821
|
+
OR EXISTS (
|
|
2822
|
+
SELECT 1 FROM anchors peer
|
|
2823
|
+
WHERE peer.verified_at IS NOT NULL
|
|
2824
|
+
AND peer.dev = candidate.dev
|
|
2825
|
+
AND peer.size = candidate.size
|
|
2826
|
+
AND pinokio_path_key(peer.path) !=
|
|
2827
|
+
pinokio_path_key(candidate.path)
|
|
2828
|
+
)
|
|
2829
|
+
)`
|
|
2804
2830
|
if (!phase) {
|
|
2805
2831
|
const group = this.database.prepare(`
|
|
2806
|
-
SELECT dev, size
|
|
2807
|
-
FROM automatic_precheck_files
|
|
2808
|
-
WHERE app = @app
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2832
|
+
SELECT candidate.dev, candidate.size
|
|
2833
|
+
FROM automatic_precheck_files candidate
|
|
2834
|
+
WHERE candidate.app = @app
|
|
2835
|
+
AND ${possiblePeer}
|
|
2836
|
+
AND (
|
|
2837
|
+
@has_cursor = 0 OR candidate.dev > @dev OR
|
|
2838
|
+
(candidate.dev = @dev AND candidate.size > @size)
|
|
2839
|
+
)
|
|
2840
|
+
GROUP BY candidate.dev, candidate.size
|
|
2841
|
+
ORDER BY candidate.dev, candidate.size
|
|
2813
2842
|
LIMIT 1
|
|
2814
2843
|
`).get({
|
|
2815
2844
|
app,
|
|
@@ -3001,29 +3030,42 @@ class RegistryCore {
|
|
|
3001
3030
|
hashes.set(canonicalPathKey(entry.path), entry.hash)
|
|
3002
3031
|
}
|
|
3003
3032
|
const rows = this.database.prepare(`
|
|
3004
|
-
SELECT path, dev, size
|
|
3033
|
+
SELECT path, dev, ino, size, mtime, ctime
|
|
3005
3034
|
FROM automatic_precheck_files
|
|
3006
3035
|
WHERE app = ?
|
|
3007
|
-
ORDER BY pinokio_path_key(path), dev, size
|
|
3036
|
+
ORDER BY pinokio_path_key(path), path, dev, ino, size, mtime, ctime
|
|
3008
3037
|
`).iterate(app)
|
|
3009
3038
|
const digest = crypto.createHash("sha256")
|
|
3010
|
-
let
|
|
3039
|
+
let proof = null
|
|
3040
|
+
digest.update("automatic-precheck-snapshots\0")
|
|
3011
3041
|
for (const row of rows) {
|
|
3012
|
-
const
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3042
|
+
const canonicalPath = canonicalPathKey(row.path)
|
|
3043
|
+
const hash = hashes.get(canonicalPath)
|
|
3044
|
+
if (!proof && hash) proof = { canonicalPath, row, hash }
|
|
3045
|
+
for (const value of [
|
|
3046
|
+
canonicalPath,
|
|
3047
|
+
row.dev,
|
|
3048
|
+
row.ino,
|
|
3049
|
+
row.size,
|
|
3050
|
+
row.mtime,
|
|
3051
|
+
row.ctime
|
|
3052
|
+
]) {
|
|
3053
|
+
digest.update(String(value))
|
|
3054
|
+
digest.update("\0")
|
|
3055
|
+
}
|
|
3023
3056
|
}
|
|
3057
|
+
if (!proof) return { signature: null, files: 0 }
|
|
3058
|
+
digest.update("automatic-precheck-proof\0")
|
|
3059
|
+
digest.update(proof.canonicalPath)
|
|
3060
|
+
digest.update("\0")
|
|
3061
|
+
digest.update(String(proof.row.dev))
|
|
3062
|
+
digest.update("\0")
|
|
3063
|
+
digest.update(String(proof.row.size))
|
|
3064
|
+
digest.update("\0")
|
|
3065
|
+
digest.update(proof.hash)
|
|
3024
3066
|
return {
|
|
3025
|
-
signature:
|
|
3026
|
-
files
|
|
3067
|
+
signature: digest.digest("hex"),
|
|
3068
|
+
files: 1
|
|
3027
3069
|
}
|
|
3028
3070
|
}
|
|
3029
3071
|
|
package/package.json
CHANGED
package/server/public/vault.css
CHANGED
|
@@ -1678,6 +1678,40 @@ body.vault-page .vault-view-tabs {
|
|
|
1678
1678
|
.vault-view-tabs .vault-nav-count {
|
|
1679
1679
|
font-size: 10.5px;
|
|
1680
1680
|
}
|
|
1681
|
+
.vault-view-tabs .vault-nav-row.attention {
|
|
1682
|
+
background: color-mix(in srgb, var(--vault-warning) 9%, var(--task-panel));
|
|
1683
|
+
color: var(--vault-warning);
|
|
1684
|
+
}
|
|
1685
|
+
.vault-view-tabs .vault-nav-row.attention:hover {
|
|
1686
|
+
background: color-mix(in srgb, var(--vault-warning) 14%, var(--task-panel));
|
|
1687
|
+
}
|
|
1688
|
+
.vault-view-tabs .vault-nav-row.attention.selected {
|
|
1689
|
+
border-bottom-color: var(--task-accent);
|
|
1690
|
+
}
|
|
1691
|
+
.vault-view-tabs .vault-nav-row.attention .vault-nav-name {
|
|
1692
|
+
color: var(--vault-warning);
|
|
1693
|
+
font-weight: 680;
|
|
1694
|
+
}
|
|
1695
|
+
.vault-view-tabs .vault-nav-row.attention .vault-nav-name::before {
|
|
1696
|
+
width: 6px;
|
|
1697
|
+
height: 6px;
|
|
1698
|
+
display: inline-block;
|
|
1699
|
+
margin-right: 6px;
|
|
1700
|
+
border-radius: 50%;
|
|
1701
|
+
background: var(--vault-warning);
|
|
1702
|
+
content: "";
|
|
1703
|
+
vertical-align: 1px;
|
|
1704
|
+
}
|
|
1705
|
+
.vault-view-tabs .vault-nav-row.attention .vault-nav-count {
|
|
1706
|
+
min-width: 20px;
|
|
1707
|
+
padding: 1px 5px;
|
|
1708
|
+
border-radius: 5px;
|
|
1709
|
+
background: color-mix(in srgb, var(--vault-warning) 72%, var(--task-panel));
|
|
1710
|
+
color: #241300;
|
|
1711
|
+
font-weight: 720;
|
|
1712
|
+
line-height: 1.35;
|
|
1713
|
+
text-align: center;
|
|
1714
|
+
}
|
|
1681
1715
|
.vault-pane-action-note { color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
|
|
1682
1716
|
.vault-toolbar {
|
|
1683
1717
|
display: flex;
|
package/server/public/vault.js
CHANGED
|
@@ -707,16 +707,20 @@ const eventLabels = {
|
|
|
707
707
|
}
|
|
708
708
|
const renderViews = () => {
|
|
709
709
|
const counts = state.data.inventory.counts
|
|
710
|
+
const attentionViews = new Set(["duplicates", "reclaimable"])
|
|
710
711
|
el("views-label").textContent = COPY.views
|
|
711
712
|
const views = IS_APP_MODE
|
|
712
713
|
? ["all", "duplicates", "unavailable", "shared", "tracked", "activity"]
|
|
713
714
|
: ["all", "duplicates", "unavailable", "shared", "tracked", "reclaimable", "activity"]
|
|
714
|
-
el("vault-views").innerHTML = views.map((view) =>
|
|
715
|
-
|
|
715
|
+
el("vault-views").innerHTML = views.map((view) => {
|
|
716
|
+
const needsAttention = attentionViews.has(view) && Number(counts[view]) > 0
|
|
717
|
+
return `
|
|
718
|
+
<button class="vault-nav-row ${state.view === view ? "selected" : ""} ${needsAttention ? "attention" : ""}" type="button" data-view="${view}" ${state.view === view ? 'aria-current="page"' : ""}>
|
|
716
719
|
<i class="${viewIcon[view]}"></i>
|
|
717
720
|
<span class="vault-nav-copy"><span class="vault-nav-name">${esc(viewLabel[view])}</span></span>
|
|
718
|
-
<span class="vault-nav-count ${
|
|
719
|
-
</button>`
|
|
721
|
+
<span class="vault-nav-count ${needsAttention ? "attention" : ""}">${counts[view]}</span>
|
|
722
|
+
</button>`
|
|
723
|
+
}).join("")
|
|
720
724
|
}
|
|
721
725
|
|
|
722
726
|
const renderSourceNode = (source, depth, counts) => {
|