pinokiod 8.0.37 → 8.0.39
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/hash_worker.js +6 -1
- package/kernel/vault/index.js +238 -69
- package/kernel/vault/registry.js +2 -2
- package/kernel/vault/sweeper.js +208 -47
- package/package.json +1 -1
- package/server/index.js +29 -0
- package/server/views/vault.ejs +479 -198
- package/test/vault-engine.test.js +96 -9
- package/test/vault-sweep.test.js +103 -0
- package/test/vault-ui.test.js +108 -0
package/server/views/vault.ejs
CHANGED
|
@@ -17,21 +17,50 @@
|
|
|
17
17
|
--vault-warning: #b06a09;
|
|
18
18
|
--vault-warning-soft: color-mix(in srgb, var(--vault-warning) 8%, var(--task-panel));
|
|
19
19
|
--vault-selected: color-mix(in srgb, var(--task-accent) 10%, var(--task-panel));
|
|
20
|
+
--vault-inline: 18px;
|
|
21
|
+
--vault-control-height: 28px;
|
|
22
|
+
--vault-row-height: 44px;
|
|
23
|
+
--vault-tree-row: 26px;
|
|
24
|
+
--vault-tree-path-row: 34px;
|
|
20
25
|
}
|
|
21
26
|
body.dark.vault-page {
|
|
22
27
|
--vault-warning: #dca451;
|
|
23
28
|
--vault-warning-soft: color-mix(in srgb, var(--vault-warning) 8%, var(--task-panel));
|
|
24
29
|
--vault-selected: color-mix(in srgb, var(--task-accent) 9%, var(--task-panel));
|
|
25
30
|
}
|
|
26
|
-
.vault-shell
|
|
27
|
-
.vault-shell
|
|
28
|
-
.vault-
|
|
29
|
-
|
|
31
|
+
.vault-shell,
|
|
32
|
+
.vault-shell * { box-sizing: border-box; }
|
|
33
|
+
body.vault-page .task-container {
|
|
34
|
+
display: flex;
|
|
35
|
+
min-height: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
.vault-shell {
|
|
39
|
+
display: flex;
|
|
40
|
+
min-height: 0;
|
|
41
|
+
flex: 1 1 auto;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
.vault-shell .task-shell-body {
|
|
45
|
+
display: block;
|
|
46
|
+
min-height: 0;
|
|
47
|
+
flex: 1 1 auto;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
padding: 0;
|
|
50
|
+
}
|
|
51
|
+
.vault-body {
|
|
52
|
+
display: flex;
|
|
53
|
+
height: 100%;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
min-height: 0;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
color: var(--task-text);
|
|
58
|
+
}
|
|
30
59
|
.vault-overview {
|
|
31
60
|
display: flex;
|
|
32
|
-
align-items:
|
|
33
|
-
gap:
|
|
34
|
-
padding:
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 10px;
|
|
63
|
+
padding: 7px var(--vault-inline);
|
|
35
64
|
border-bottom: 1px solid var(--task-border);
|
|
36
65
|
}
|
|
37
66
|
.vault-metrics {
|
|
@@ -41,32 +70,34 @@ body.dark.vault-page {
|
|
|
41
70
|
align-items: stretch;
|
|
42
71
|
}
|
|
43
72
|
.vault-metric {
|
|
44
|
-
min-width:
|
|
45
|
-
padding:
|
|
73
|
+
min-width: 0;
|
|
74
|
+
padding: 0 12px;
|
|
75
|
+
border: 0;
|
|
46
76
|
border-left: 1px solid var(--task-border);
|
|
47
77
|
}
|
|
48
78
|
.vault-metric:first-child { padding-left: 0; border-left: 0; }
|
|
49
79
|
.vault-metric-value {
|
|
50
80
|
display: block;
|
|
51
81
|
color: var(--task-text);
|
|
52
|
-
font-size:
|
|
53
|
-
line-height: 1.
|
|
82
|
+
font-size: 15px;
|
|
83
|
+
line-height: 1.15;
|
|
54
84
|
font-weight: 650;
|
|
55
85
|
letter-spacing: -0.015em;
|
|
56
86
|
white-space: nowrap;
|
|
57
87
|
}
|
|
58
88
|
.vault-metric-label {
|
|
59
89
|
display: block;
|
|
60
|
-
margin-top:
|
|
90
|
+
margin-top: 1px;
|
|
61
91
|
color: var(--task-muted);
|
|
62
|
-
font-size:
|
|
63
|
-
line-height: 1.
|
|
92
|
+
font-size: 10.5px;
|
|
93
|
+
line-height: 1.2;
|
|
64
94
|
white-space: nowrap;
|
|
65
95
|
}
|
|
66
96
|
button.vault-metric {
|
|
67
97
|
appearance: none;
|
|
68
98
|
background: transparent;
|
|
69
99
|
color: inherit;
|
|
100
|
+
font: inherit;
|
|
70
101
|
text-align: left;
|
|
71
102
|
cursor: pointer;
|
|
72
103
|
}
|
|
@@ -91,13 +122,13 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
91
122
|
}
|
|
92
123
|
.vault-button {
|
|
93
124
|
display: inline-flex;
|
|
94
|
-
min-height:
|
|
125
|
+
min-height: var(--vault-control-height);
|
|
95
126
|
align-items: center;
|
|
96
127
|
justify-content: center;
|
|
97
|
-
gap:
|
|
98
|
-
padding:
|
|
99
|
-
border-radius:
|
|
100
|
-
font-size:
|
|
128
|
+
gap: 7px;
|
|
129
|
+
padding: 6px 12px;
|
|
130
|
+
border-radius: 7px;
|
|
131
|
+
font-size: 12px;
|
|
101
132
|
line-height: 1;
|
|
102
133
|
white-space: nowrap;
|
|
103
134
|
}
|
|
@@ -109,9 +140,40 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
109
140
|
}
|
|
110
141
|
.vault-button.primary:hover { background: var(--task-accent-hover); }
|
|
111
142
|
.vault-button:disabled { opacity: .5; cursor: default; }
|
|
143
|
+
.vault-icon-button {
|
|
144
|
+
width: var(--vault-control-height);
|
|
145
|
+
height: var(--vault-control-height);
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
padding: 0;
|
|
150
|
+
border-radius: 7px;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
}
|
|
153
|
+
.vault-icon-button:hover { background: var(--task-soft); }
|
|
154
|
+
.vault-advanced { position: relative; }
|
|
155
|
+
.vault-advanced > summary { list-style: none; cursor: pointer; }
|
|
156
|
+
.vault-advanced > summary::-webkit-details-marker { display: none; }
|
|
157
|
+
.vault-advanced-menu {
|
|
158
|
+
position: absolute;
|
|
159
|
+
z-index: 20;
|
|
160
|
+
top: calc(100% + 6px);
|
|
161
|
+
right: 0;
|
|
162
|
+
width: 272px;
|
|
163
|
+
box-sizing: border-box;
|
|
164
|
+
padding: 12px;
|
|
165
|
+
border: 1px solid var(--task-border-strong);
|
|
166
|
+
border-radius: 9px;
|
|
167
|
+
background: var(--task-panel);
|
|
168
|
+
box-shadow: 0 12px 30px color-mix(in srgb, var(--task-text) 10%, transparent);
|
|
169
|
+
}
|
|
170
|
+
.vault-advanced-title { color: var(--task-text); font-size: 12.5px; font-weight: 650; }
|
|
171
|
+
.vault-advanced-copy { margin: 4px 0 10px; color: var(--task-muted); font-size: 11px; line-height: 1.45; }
|
|
112
172
|
.vault-button:focus-visible,
|
|
113
173
|
.vault-icon-button:focus-visible,
|
|
114
174
|
.vault-text-button:focus-visible,
|
|
175
|
+
button.vault-metric:focus-visible,
|
|
176
|
+
.vault-rail-add:focus-visible,
|
|
115
177
|
.vault-nav-row:focus-visible,
|
|
116
178
|
.vault-search input:focus-visible,
|
|
117
179
|
.vault-select:focus-visible {
|
|
@@ -123,12 +185,16 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
123
185
|
.vault-feedback {
|
|
124
186
|
display: none;
|
|
125
187
|
align-items: center;
|
|
126
|
-
min-height:
|
|
127
|
-
gap:
|
|
128
|
-
padding:
|
|
188
|
+
min-height: 38px;
|
|
189
|
+
gap: 9px;
|
|
190
|
+
padding: 6px var(--vault-inline);
|
|
129
191
|
border-bottom: 1px solid var(--task-border);
|
|
130
192
|
font-size: 12.5px;
|
|
131
193
|
}
|
|
194
|
+
.vault-scan-state {
|
|
195
|
+
position: relative;
|
|
196
|
+
overflow: hidden;
|
|
197
|
+
}
|
|
132
198
|
.vault-scan-state.show,
|
|
133
199
|
.vault-result.show,
|
|
134
200
|
.vault-feedback.show { display: flex; }
|
|
@@ -137,39 +203,117 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
137
203
|
.vault-result strong { font-weight: 650; }
|
|
138
204
|
.vault-result-detail,
|
|
139
205
|
.vault-scan-detail { color: var(--task-muted); }
|
|
140
|
-
.vault-
|
|
141
|
-
|
|
206
|
+
.vault-scan-detail {
|
|
207
|
+
min-width: 0;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
text-overflow: ellipsis;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
}
|
|
212
|
+
.vault-scan-percent {
|
|
213
|
+
flex: 0 0 auto;
|
|
214
|
+
margin-left: auto;
|
|
215
|
+
color: var(--task-muted);
|
|
216
|
+
font-size: 11.5px;
|
|
217
|
+
font-variant-numeric: tabular-nums;
|
|
218
|
+
font-weight: 650;
|
|
219
|
+
}
|
|
220
|
+
.vault-progress-track {
|
|
221
|
+
position: absolute;
|
|
222
|
+
right: 0;
|
|
223
|
+
bottom: 0;
|
|
224
|
+
left: 0;
|
|
225
|
+
height: 2px;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
background: color-mix(in srgb, var(--task-accent) 12%, transparent);
|
|
228
|
+
}
|
|
229
|
+
.vault-progress-bar {
|
|
230
|
+
display: block;
|
|
231
|
+
width: 100%;
|
|
232
|
+
height: 100%;
|
|
233
|
+
background: var(--task-accent);
|
|
234
|
+
transform-origin: left center;
|
|
235
|
+
}
|
|
236
|
+
.vault-progress-bar.determinate {
|
|
237
|
+
transform: scaleX(var(--vault-progress, 0));
|
|
238
|
+
transition: transform 320ms cubic-bezier(.22, 1, .36, 1);
|
|
239
|
+
}
|
|
240
|
+
.vault-progress-bar.indeterminate {
|
|
241
|
+
transform: scaleX(1);
|
|
242
|
+
animation: vault-progress-pulse 1.4s ease-in-out infinite;
|
|
243
|
+
}
|
|
244
|
+
@keyframes vault-progress-pulse {
|
|
245
|
+
0%, 100% { opacity: .22; }
|
|
246
|
+
50% { opacity: .78; }
|
|
247
|
+
}
|
|
248
|
+
.vault-result .vault-button { min-height: 28px; margin-left: auto; padding: 4px 10px; }
|
|
249
|
+
.vault-feedback { min-height: 34px; background: var(--task-soft); }
|
|
142
250
|
.vault-feedback.error { color: var(--vault-warning); }
|
|
143
251
|
.vault-explorer {
|
|
144
252
|
display: grid;
|
|
145
|
-
|
|
146
|
-
|
|
253
|
+
min-height: 0;
|
|
254
|
+
flex: 1 1 auto;
|
|
255
|
+
grid-template-columns: 248px minmax(0, 1fr);
|
|
256
|
+
overflow: hidden;
|
|
147
257
|
}
|
|
148
258
|
.vault-rail {
|
|
149
259
|
min-width: 0;
|
|
260
|
+
min-height: 0;
|
|
150
261
|
border-right: 1px solid var(--task-border);
|
|
151
262
|
background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
|
|
152
|
-
overflow:
|
|
263
|
+
overflow-x: hidden;
|
|
264
|
+
overflow-y: auto;
|
|
265
|
+
overscroll-behavior: contain;
|
|
266
|
+
}
|
|
267
|
+
.vault-rail-section {
|
|
268
|
+
display: block;
|
|
269
|
+
min-height: 0;
|
|
270
|
+
padding: 12px 9px;
|
|
153
271
|
}
|
|
154
|
-
.vault-rail-section { padding: 18px 12px; }
|
|
155
272
|
.vault-rail-section + .vault-rail-section { border-top: 1px solid var(--task-border); }
|
|
156
273
|
.vault-rail-label {
|
|
157
|
-
margin: 0
|
|
274
|
+
margin: 0 7px 6px;
|
|
158
275
|
color: var(--task-muted);
|
|
159
276
|
font-size: 10.5px;
|
|
160
277
|
font-weight: 700;
|
|
161
278
|
letter-spacing: .08em;
|
|
162
279
|
}
|
|
163
|
-
.vault-
|
|
280
|
+
.vault-rail-heading {
|
|
281
|
+
display: flex;
|
|
282
|
+
height: 20px;
|
|
283
|
+
align-items: center;
|
|
284
|
+
margin: 0 5px 4px 7px;
|
|
285
|
+
}
|
|
286
|
+
.vault-rail-heading .vault-rail-label { flex: 1 1 auto; margin: 0; }
|
|
287
|
+
.vault-rail-add {
|
|
288
|
+
width: 20px;
|
|
289
|
+
height: 20px;
|
|
290
|
+
display: inline-flex;
|
|
291
|
+
flex: 0 0 20px;
|
|
292
|
+
align-items: center;
|
|
293
|
+
justify-content: center;
|
|
294
|
+
padding: 0;
|
|
295
|
+
border: 0;
|
|
296
|
+
border-radius: 5px;
|
|
297
|
+
background: transparent;
|
|
298
|
+
color: var(--task-muted);
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
font: inherit;
|
|
301
|
+
font-size: 10px;
|
|
302
|
+
}
|
|
303
|
+
.vault-rail-add:hover { background: var(--task-soft); color: var(--task-text); }
|
|
304
|
+
.vault-rail-add:disabled { opacity: .45; cursor: default; }
|
|
305
|
+
.vault-nav-list { display: grid; gap: 0; }
|
|
164
306
|
.vault-nav-row {
|
|
165
307
|
width: 100%;
|
|
166
308
|
min-width: 0;
|
|
167
|
-
|
|
309
|
+
height: var(--vault-tree-row);
|
|
310
|
+
min-height: var(--vault-tree-row);
|
|
311
|
+
max-height: var(--vault-tree-row);
|
|
168
312
|
display: grid;
|
|
169
313
|
grid-template-columns: 18px minmax(0, 1fr) auto;
|
|
170
314
|
align-items: center;
|
|
171
|
-
gap:
|
|
172
|
-
padding: 6px
|
|
315
|
+
gap: 6px;
|
|
316
|
+
padding: 2px 6px;
|
|
173
317
|
border: 1px solid transparent;
|
|
174
318
|
border-radius: 7px;
|
|
175
319
|
background: transparent;
|
|
@@ -183,12 +327,12 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
183
327
|
border-color: color-mix(in srgb, var(--task-accent) 72%, var(--task-border-strong));
|
|
184
328
|
background: var(--vault-selected);
|
|
185
329
|
}
|
|
186
|
-
.vault-nav-row > i { width:
|
|
330
|
+
.vault-nav-row > i { width: 15px; color: var(--task-muted); text-align: center; font-size: 11.5px; }
|
|
187
331
|
.vault-nav-copy { min-width: 0; }
|
|
188
332
|
.vault-nav-name {
|
|
189
333
|
display: block;
|
|
190
334
|
overflow: hidden;
|
|
191
|
-
font-size:
|
|
335
|
+
font-size: 11.75px;
|
|
192
336
|
font-weight: 520;
|
|
193
337
|
line-height: 1.25;
|
|
194
338
|
text-overflow: ellipsis;
|
|
@@ -197,9 +341,9 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
197
341
|
.vault-nav-path {
|
|
198
342
|
display: block;
|
|
199
343
|
overflow: hidden;
|
|
200
|
-
margin-top:
|
|
344
|
+
margin-top: 1px;
|
|
201
345
|
color: var(--task-muted);
|
|
202
|
-
font-size:
|
|
346
|
+
font-size: 10px;
|
|
203
347
|
line-height: 1.25;
|
|
204
348
|
text-overflow: ellipsis;
|
|
205
349
|
white-space: nowrap;
|
|
@@ -210,16 +354,35 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
210
354
|
font-variant-numeric: tabular-nums;
|
|
211
355
|
}
|
|
212
356
|
.vault-nav-count.attention { color: var(--vault-warning); font-weight: 650; }
|
|
357
|
+
#vault-locations {
|
|
358
|
+
display: flex;
|
|
359
|
+
min-height: 0;
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
align-items: stretch;
|
|
362
|
+
justify-content: flex-start;
|
|
363
|
+
gap: 0;
|
|
364
|
+
}
|
|
213
365
|
.vault-source-line {
|
|
214
366
|
display: grid;
|
|
367
|
+
flex: 0 0 auto;
|
|
368
|
+
height: var(--vault-tree-row);
|
|
369
|
+
min-height: var(--vault-tree-row);
|
|
370
|
+
max-height: var(--vault-tree-row);
|
|
215
371
|
grid-template-columns: 18px minmax(0, 1fr);
|
|
216
372
|
align-items: start;
|
|
373
|
+
align-self: stretch;
|
|
374
|
+
margin: 0;
|
|
375
|
+
}
|
|
376
|
+
.vault-source-line.has-path {
|
|
377
|
+
height: var(--vault-tree-path-row);
|
|
378
|
+
min-height: var(--vault-tree-path-row);
|
|
379
|
+
max-height: var(--vault-tree-path-row);
|
|
217
380
|
}
|
|
218
|
-
.vault-source-line.depth-1 { padding-left:
|
|
219
|
-
.vault-source-line.depth-2 { padding-left:
|
|
381
|
+
.vault-source-line.depth-1 { padding-left: 12px; }
|
|
382
|
+
.vault-source-line.depth-2 { padding-left: 24px; }
|
|
220
383
|
.vault-source-toggle {
|
|
221
384
|
width: 18px;
|
|
222
|
-
height:
|
|
385
|
+
height: 100%;
|
|
223
386
|
padding: 0;
|
|
224
387
|
border: 0;
|
|
225
388
|
background: transparent;
|
|
@@ -229,7 +392,12 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
229
392
|
}
|
|
230
393
|
.vault-source-toggle.placeholder { cursor: default; }
|
|
231
394
|
.vault-source-line .vault-nav-row { grid-template-columns: 18px minmax(0, 1fr) auto; }
|
|
232
|
-
.vault-
|
|
395
|
+
.vault-source-line.has-path .vault-nav-row {
|
|
396
|
+
height: var(--vault-tree-path-row);
|
|
397
|
+
min-height: var(--vault-tree-path-row);
|
|
398
|
+
max-height: var(--vault-tree-path-row);
|
|
399
|
+
}
|
|
400
|
+
.vault-rail-footer { padding: 0 16px 12px; }
|
|
233
401
|
.vault-text-button {
|
|
234
402
|
padding: 2px 0;
|
|
235
403
|
border: 0;
|
|
@@ -238,49 +406,27 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
238
406
|
font-size: 11.5px;
|
|
239
407
|
}
|
|
240
408
|
.vault-text-button:hover { color: var(--task-text); }
|
|
241
|
-
.vault-pane {
|
|
242
|
-
.vault-pane-head {
|
|
409
|
+
.vault-pane {
|
|
243
410
|
display: grid;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
padding: 18px 24px 16px;
|
|
249
|
-
border-bottom: 1px solid var(--task-border);
|
|
250
|
-
}
|
|
251
|
-
.vault-breadcrumb {
|
|
252
|
-
margin-bottom: 8px;
|
|
253
|
-
color: var(--task-muted);
|
|
254
|
-
font-size: 11.5px;
|
|
255
|
-
line-height: 1.4;
|
|
256
|
-
}
|
|
257
|
-
.vault-pane-title {
|
|
258
|
-
margin: 0;
|
|
259
|
-
color: var(--task-text);
|
|
260
|
-
font-size: 18px;
|
|
261
|
-
font-weight: 680;
|
|
262
|
-
letter-spacing: -.015em;
|
|
263
|
-
}
|
|
264
|
-
.vault-pane-subtitle {
|
|
265
|
-
margin: 5px 0 0;
|
|
266
|
-
color: var(--task-muted);
|
|
267
|
-
font-size: 12px;
|
|
268
|
-
line-height: 1.45;
|
|
411
|
+
min-width: 0;
|
|
412
|
+
min-height: 0;
|
|
413
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
414
|
+
overflow: hidden;
|
|
269
415
|
}
|
|
270
|
-
.vault-pane-action-note { color: var(--task-muted); font-size: 11.5px;
|
|
416
|
+
.vault-pane-action-note { color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
|
|
271
417
|
.vault-toolbar {
|
|
272
418
|
display: flex;
|
|
273
419
|
align-items: center;
|
|
274
|
-
gap:
|
|
275
|
-
min-height:
|
|
276
|
-
padding:
|
|
420
|
+
gap: 8px;
|
|
421
|
+
min-height: 44px;
|
|
422
|
+
padding: 6px var(--vault-inline);
|
|
277
423
|
border-bottom: 1px solid var(--task-border);
|
|
278
424
|
}
|
|
279
425
|
.vault-search { position: relative; width: min(320px, 55%); }
|
|
280
426
|
.vault-search i {
|
|
281
427
|
position: absolute;
|
|
282
428
|
top: 50%;
|
|
283
|
-
left:
|
|
429
|
+
left: 10px;
|
|
284
430
|
color: var(--task-muted);
|
|
285
431
|
font-size: 12px;
|
|
286
432
|
transform: translateY(-50%);
|
|
@@ -288,21 +434,25 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
288
434
|
}
|
|
289
435
|
.vault-search input,
|
|
290
436
|
.vault-select {
|
|
291
|
-
height:
|
|
437
|
+
height: 30px;
|
|
292
438
|
box-sizing: border-box;
|
|
293
439
|
border: 1px solid var(--task-border-strong);
|
|
294
440
|
border-radius: 7px;
|
|
295
441
|
background: var(--task-panel);
|
|
296
442
|
color: var(--task-text);
|
|
297
443
|
font: inherit;
|
|
298
|
-
font-size:
|
|
444
|
+
font-size: 12px;
|
|
299
445
|
}
|
|
300
|
-
.vault-search input { width: 100%; padding:
|
|
446
|
+
.vault-search input { width: 100%; padding: 5px 10px 5px 30px; }
|
|
301
447
|
.vault-search input::placeholder { color: var(--task-muted); }
|
|
302
|
-
.vault-select { padding:
|
|
448
|
+
.vault-select { padding: 4px 28px 4px 9px; }
|
|
303
449
|
.vault-toolbar-count { margin-left: auto; color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
|
|
304
450
|
.vault-table { min-width: 660px; }
|
|
305
|
-
.vault-table-wrap {
|
|
451
|
+
.vault-table-wrap {
|
|
452
|
+
min-height: 0;
|
|
453
|
+
overflow: auto;
|
|
454
|
+
overscroll-behavior: contain;
|
|
455
|
+
}
|
|
306
456
|
.vault-columns,
|
|
307
457
|
.vault-file-row,
|
|
308
458
|
.vault-group-row {
|
|
@@ -317,24 +467,24 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
317
467
|
grid-template-columns: minmax(210px, 1.5fr) minmax(64px, .4fr) minmax(140px, 1fr) minmax(96px, .55fr) minmax(76px, auto);
|
|
318
468
|
}
|
|
319
469
|
.vault-columns {
|
|
320
|
-
min-height:
|
|
321
|
-
padding: 0
|
|
470
|
+
min-height: 34px;
|
|
471
|
+
padding: 0 var(--vault-inline);
|
|
322
472
|
border-bottom: 1px solid var(--task-border);
|
|
323
473
|
color: var(--task-muted);
|
|
324
474
|
font-size: 10.5px;
|
|
325
475
|
font-weight: 600;
|
|
326
476
|
}
|
|
327
477
|
.vault-file-row {
|
|
328
|
-
min-height:
|
|
329
|
-
padding:
|
|
478
|
+
min-height: var(--vault-row-height);
|
|
479
|
+
padding: 6px var(--vault-inline);
|
|
330
480
|
border-bottom: 1px solid var(--task-border);
|
|
331
481
|
font-size: 12px;
|
|
332
482
|
}
|
|
333
483
|
.vault-file-row:hover { background: color-mix(in srgb, var(--task-soft) 64%, transparent); }
|
|
334
|
-
.vault-file-row.directory { min-height:
|
|
484
|
+
.vault-file-row.directory { min-height: 38px; padding-top: 4px; padding-bottom: 4px; background: color-mix(in srgb, var(--task-soft) 38%, transparent); }
|
|
335
485
|
.vault-group-row {
|
|
336
|
-
min-height:
|
|
337
|
-
padding:
|
|
486
|
+
min-height: 48px;
|
|
487
|
+
padding: 6px var(--vault-inline);
|
|
338
488
|
border-bottom: 1px solid var(--task-border-strong);
|
|
339
489
|
background: color-mix(in srgb, var(--task-soft) 56%, transparent);
|
|
340
490
|
}
|
|
@@ -343,18 +493,18 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
343
493
|
display: flex;
|
|
344
494
|
min-width: 0;
|
|
345
495
|
align-items: center;
|
|
346
|
-
gap:
|
|
496
|
+
gap: 8px;
|
|
347
497
|
color: var(--task-text);
|
|
348
498
|
font-size: 12.5px;
|
|
349
499
|
font-weight: 600;
|
|
350
500
|
}
|
|
351
501
|
.vault-group-title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
352
|
-
.vault-group-meta { margin:
|
|
502
|
+
.vault-group-meta { margin: 2px 0 0 23px; color: var(--task-muted); font-size: 10px; }
|
|
353
503
|
.vault-group-action { grid-column: 5; justify-self: end; }
|
|
354
|
-
.vault-group-action .vault-button { min-height:
|
|
355
|
-
.vault-name-cell { display: flex; min-width: 0; align-items: center; gap:
|
|
356
|
-
.vault-name-cell.indent-1 { padding-left:
|
|
357
|
-
.vault-name-cell.indent-2 { padding-left:
|
|
504
|
+
.vault-group-action .vault-button { min-height: 28px; padding: 4px 9px; }
|
|
505
|
+
.vault-name-cell { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
|
506
|
+
.vault-name-cell.indent-1 { padding-left: 18px; }
|
|
507
|
+
.vault-name-cell.indent-2 { padding-left: 36px; }
|
|
358
508
|
.vault-name-icon { width: 16px; flex: 0 0 16px; color: var(--task-muted); text-align: center; }
|
|
359
509
|
.vault-disclosure {
|
|
360
510
|
width: 16px;
|
|
@@ -372,9 +522,9 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
372
522
|
display: block;
|
|
373
523
|
overflow: hidden;
|
|
374
524
|
color: var(--task-text);
|
|
375
|
-
font-size:
|
|
525
|
+
font-size: 12px;
|
|
376
526
|
font-weight: 520;
|
|
377
|
-
line-height: 1.
|
|
527
|
+
line-height: 1.3;
|
|
378
528
|
text-overflow: ellipsis;
|
|
379
529
|
white-space: nowrap;
|
|
380
530
|
}
|
|
@@ -383,7 +533,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
383
533
|
overflow: hidden;
|
|
384
534
|
color: var(--task-muted);
|
|
385
535
|
font-size: 10.5px;
|
|
386
|
-
line-height: 1.
|
|
536
|
+
line-height: 1.3;
|
|
387
537
|
text-overflow: ellipsis;
|
|
388
538
|
white-space: nowrap;
|
|
389
539
|
}
|
|
@@ -392,7 +542,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
392
542
|
min-width: 0;
|
|
393
543
|
color: var(--task-muted);
|
|
394
544
|
font-size: 10.5px;
|
|
395
|
-
line-height: 1.
|
|
545
|
+
line-height: 1.35;
|
|
396
546
|
overflow-wrap: anywhere;
|
|
397
547
|
white-space: normal;
|
|
398
548
|
word-break: break-word;
|
|
@@ -406,47 +556,65 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
406
556
|
.vault-row-action { justify-self: end; }
|
|
407
557
|
.vault-row-action .vault-text-button { color: var(--task-text); font-size: 11.5px; }
|
|
408
558
|
.vault-detail {
|
|
409
|
-
padding:
|
|
559
|
+
padding: 8px var(--vault-inline) 9px 56px;
|
|
410
560
|
border-bottom: 1px solid var(--task-border);
|
|
411
561
|
background: color-mix(in srgb, var(--task-soft) 48%, transparent);
|
|
412
562
|
}
|
|
413
|
-
.vault-detail-label { margin-bottom:
|
|
414
|
-
.vault-location-detail { display: flex; align-items: center; gap:
|
|
563
|
+
.vault-detail-label { margin-bottom: 5px; color: var(--task-muted); font-size: 10px; font-weight: 650; }
|
|
564
|
+
.vault-location-detail { display: flex; align-items: center; gap: 7px; padding: 2px 0; color: var(--task-text); font-size: 10.5px; }
|
|
415
565
|
.vault-location-detail i { color: var(--task-muted); }
|
|
416
566
|
.vault-empty {
|
|
417
567
|
display: grid;
|
|
418
|
-
min-height:
|
|
568
|
+
min-height: 190px;
|
|
419
569
|
place-items: center;
|
|
420
|
-
padding:
|
|
570
|
+
padding: 28px;
|
|
421
571
|
text-align: center;
|
|
422
572
|
}
|
|
423
573
|
.vault-empty-inner { max-width: 420px; }
|
|
424
574
|
.vault-empty i { color: var(--task-muted); font-size: 18px; }
|
|
425
|
-
.vault-empty h3 { margin:
|
|
575
|
+
.vault-empty h3 { margin: 9px 0 4px; font-size: 14px; }
|
|
426
576
|
.vault-empty p { margin: 0; color: var(--task-muted); font-size: 12px; line-height: 1.55; }
|
|
427
|
-
.vault-empty .vault-button { margin-top:
|
|
577
|
+
.vault-empty .vault-button { margin-top: 10px; }
|
|
428
578
|
.vault-pane-footer {
|
|
429
|
-
padding:
|
|
579
|
+
padding: 9px var(--vault-inline) 11px;
|
|
430
580
|
color: var(--task-muted);
|
|
431
581
|
font-size: 10.5px;
|
|
432
582
|
}
|
|
433
583
|
.vault-event-kind { font-weight: 570; }
|
|
434
584
|
.vault-event-time { color: var(--task-muted); font-size: 10.5px; }
|
|
435
585
|
.vault-unavailable { color: var(--task-muted); font-size: 11px; }
|
|
436
|
-
@media (max-width:
|
|
586
|
+
@media (max-width: 1140px) {
|
|
437
587
|
.vault-overview { align-items: flex-start; flex-direction: column; }
|
|
438
588
|
.vault-overview-actions { width: 100%; justify-content: flex-start; }
|
|
439
|
-
.vault-metric { padding-right:
|
|
440
|
-
.vault-explorer { grid-template-columns:
|
|
589
|
+
.vault-metric { padding-right: 12px; padding-left: 12px; }
|
|
590
|
+
.vault-explorer { grid-template-columns: 224px minmax(0, 1fr); }
|
|
441
591
|
}
|
|
442
592
|
@media (max-width: 820px) {
|
|
593
|
+
body.vault-page .task-container { display: block; overflow-y: auto; }
|
|
594
|
+
.vault-shell,
|
|
595
|
+
.vault-shell .task-shell-body,
|
|
596
|
+
.vault-body { height: auto; overflow: visible; }
|
|
443
597
|
.vault-metrics { width: 100%; overflow-x: auto; }
|
|
444
|
-
.vault-explorer { display: block; }
|
|
598
|
+
.vault-explorer { display: block; overflow: visible; }
|
|
445
599
|
.vault-rail { max-height: 330px; border-right: 0; border-bottom: 1px solid var(--task-border); }
|
|
446
|
-
.vault-pane
|
|
600
|
+
.vault-pane { display: block; overflow: visible; }
|
|
601
|
+
.vault-table-wrap { overflow-x: auto; overflow-y: visible; }
|
|
602
|
+
}
|
|
603
|
+
@media (pointer: coarse) {
|
|
604
|
+
.vault-button,
|
|
605
|
+
.vault-result .vault-button,
|
|
606
|
+
.vault-group-action .vault-button { min-height: 40px; }
|
|
607
|
+
.vault-icon-button { width: 40px; height: 40px; }
|
|
608
|
+
.vault-rail-add { width: 28px; height: 28px; flex-basis: 28px; }
|
|
447
609
|
}
|
|
448
610
|
@media (prefers-reduced-motion: reduce) {
|
|
449
611
|
.vault-page * { scroll-behavior: auto !important; }
|
|
612
|
+
.vault-progress-bar { transition: none; }
|
|
613
|
+
.vault-progress-bar.indeterminate {
|
|
614
|
+
animation: none;
|
|
615
|
+
opacity: .5;
|
|
616
|
+
transform: scaleX(1);
|
|
617
|
+
}
|
|
450
618
|
}
|
|
451
619
|
</style>
|
|
452
620
|
</head>
|
|
@@ -454,20 +622,21 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
454
622
|
<%- include('partials/app_navheader', { agent }) %>
|
|
455
623
|
<main>
|
|
456
624
|
<div class='task-container'>
|
|
457
|
-
|
|
458
|
-
<header class='task-shell-header'>
|
|
459
|
-
<div class='task-shell-header-main'>
|
|
460
|
-
<h1 class='task-title' id='vault-title'></h1>
|
|
461
|
-
<p class='task-description' id='vault-subtitle'></p>
|
|
462
|
-
</div>
|
|
463
|
-
</header>
|
|
625
|
+
<section class='task-shell vault-shell'>
|
|
464
626
|
<div class='task-shell-body'>
|
|
465
627
|
<div class='vault-body'>
|
|
466
628
|
<section class='vault-overview'>
|
|
467
629
|
<div class='vault-metrics' id='vault-metrics'></div>
|
|
468
630
|
<div class='vault-overview-actions'>
|
|
469
631
|
<button class='vault-button primary' id='btn-scan' type='button'></button>
|
|
470
|
-
<
|
|
632
|
+
<details class='vault-advanced' id='vault-advanced'>
|
|
633
|
+
<summary class='vault-icon-button' id='btn-vault-options'><i class='fa-solid fa-ellipsis'></i></summary>
|
|
634
|
+
<div class='vault-advanced-menu'>
|
|
635
|
+
<div class='vault-advanced-title' id='vault-repair-title'></div>
|
|
636
|
+
<p class='vault-advanced-copy' id='vault-repair-description'></p>
|
|
637
|
+
<button class='vault-button' id='btn-repair' type='button'></button>
|
|
638
|
+
</div>
|
|
639
|
+
</details>
|
|
471
640
|
</div>
|
|
472
641
|
</section>
|
|
473
642
|
<div class='vault-scan-state' id='vault-scan-state'></div>
|
|
@@ -480,13 +649,15 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
480
649
|
<div class='vault-nav-list' id='vault-views'></div>
|
|
481
650
|
</div>
|
|
482
651
|
<div class='vault-rail-section'>
|
|
483
|
-
<div class='vault-rail-
|
|
652
|
+
<div class='vault-rail-heading'>
|
|
653
|
+
<div class='vault-rail-label' id='locations-label'></div>
|
|
654
|
+
<button class='vault-rail-add' id='btn-add-source' type='button' aria-label='Add external folder' title='Add external folder'><i class='fa-solid fa-plus'></i></button>
|
|
655
|
+
</div>
|
|
484
656
|
<div id='vault-locations'></div>
|
|
485
657
|
</div>
|
|
486
658
|
<div class='vault-rail-footer' id='vault-rail-footer'></div>
|
|
487
659
|
</aside>
|
|
488
|
-
<section class='vault-pane'>
|
|
489
|
-
<header class='vault-pane-head' id='vault-pane-head'></header>
|
|
660
|
+
<section class='vault-pane' aria-label='Vault files'>
|
|
490
661
|
<div class='vault-toolbar' id='vault-toolbar'></div>
|
|
491
662
|
<div class='vault-table-wrap' id='vault-table-wrap'></div>
|
|
492
663
|
<footer class='vault-pane-footer' id='vault-pane-footer'></footer>
|
|
@@ -498,6 +669,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
|
|
|
498
669
|
</div>
|
|
499
670
|
<%- include('partials/main_sidebar', { selected: 'vault' }) %>
|
|
500
671
|
</main>
|
|
672
|
+
<script src="/Socket.js"></script>
|
|
501
673
|
<script>
|
|
502
674
|
const COPY = {
|
|
503
675
|
title: "Vault",
|
|
@@ -513,8 +685,16 @@ const COPY = {
|
|
|
513
685
|
pinokio: "Pinokio",
|
|
514
686
|
apps: "Apps",
|
|
515
687
|
external: "External folders",
|
|
688
|
+
add_external_folder: "Add external folder",
|
|
689
|
+
external_added: "Added to Locations. Run a scan when you’re ready.",
|
|
690
|
+
external_exists: "That folder is already in Locations.",
|
|
691
|
+
external_other_disk: "Added to Locations. It can be scanned, but this disk cannot share space with the Vault.",
|
|
516
692
|
on_disk: "On disk",
|
|
517
693
|
saved: "Saved",
|
|
694
|
+
already_shared: "Already shared",
|
|
695
|
+
already_shared_help: "Space already avoided by files sharing the same data",
|
|
696
|
+
saved_by_vault: "Saved by Vault",
|
|
697
|
+
saved_by_vault_help: "Space saved through your Deduplicate actions",
|
|
518
698
|
can_save: "Can save",
|
|
519
699
|
pinokio_folder: "Pinokio folder",
|
|
520
700
|
last_scanned: "Last scanned",
|
|
@@ -522,9 +702,18 @@ const COPY = {
|
|
|
522
702
|
scan: "Scan now",
|
|
523
703
|
scan_again: "Scan again",
|
|
524
704
|
scanning: "Scanning…",
|
|
525
|
-
|
|
705
|
+
vault_options: "Vault options",
|
|
706
|
+
repair_index: "Repair Vault index",
|
|
707
|
+
repair_action: "Repair index",
|
|
708
|
+
repair_description: "Reconstruct Vault’s internal records if tracked files or sharing status look incorrect. This does not scan for new duplicates.",
|
|
709
|
+
repairing: "Repairing…",
|
|
710
|
+
repair_done: "Vault index repaired",
|
|
526
711
|
scan_progress: "Scanning your configured locations",
|
|
712
|
+
scan_analyzing: "Analyzing large files",
|
|
713
|
+
scan_finishing: "Finishing scan",
|
|
714
|
+
scan_estimate_help: "Estimated from your last completed scan",
|
|
527
715
|
scan_folders: "folders checked",
|
|
716
|
+
scan_files: "files checked",
|
|
528
717
|
analyzing: "analyzing",
|
|
529
718
|
waiting: "files waiting",
|
|
530
719
|
scan_complete: "Scan complete",
|
|
@@ -571,9 +760,11 @@ const COPY = {
|
|
|
571
760
|
reclaim: "Reclaim",
|
|
572
761
|
reclaim_all: "Reclaim all",
|
|
573
762
|
undo: "Undo",
|
|
574
|
-
|
|
763
|
+
identical_contents_at: "Identical contents at",
|
|
575
764
|
no_files: "Nothing tracked yet",
|
|
576
765
|
no_files_hint: "Run a scan to find large files. Scanning never changes them.",
|
|
766
|
+
scan_waiting: "Waiting for scan results",
|
|
767
|
+
scan_waiting_hint: "Tracked files will appear here when this scan finishes.",
|
|
577
768
|
no_duplicates: "Everything is already shared",
|
|
578
769
|
no_duplicates_hint: "There are no files waiting for your review.",
|
|
579
770
|
no_shared: "Nothing is shared yet",
|
|
@@ -616,8 +807,7 @@ const state = {
|
|
|
616
807
|
scanRequested: false,
|
|
617
808
|
scanBaseline: null,
|
|
618
809
|
scanResult: null,
|
|
619
|
-
feedback: null
|
|
620
|
-
priorScanning: false
|
|
810
|
+
feedback: null
|
|
621
811
|
}
|
|
622
812
|
|
|
623
813
|
const el = (id) => document.getElementById(id)
|
|
@@ -664,7 +854,7 @@ const sourcePath = (source) => {
|
|
|
664
854
|
if (!source) return ""
|
|
665
855
|
if (source.kind === "pinokio") return `~/${basename(source.root)}`
|
|
666
856
|
if (source.kind === "external") return source.target_path || source.display_path || ""
|
|
667
|
-
if (source.kind === "app") return "
|
|
857
|
+
if (source.kind === "app") return ""
|
|
668
858
|
return source.display_path || source.root || ""
|
|
669
859
|
}
|
|
670
860
|
const isDescendantSource = (candidateId, parentId) => {
|
|
@@ -710,7 +900,7 @@ const buildItems = () => {
|
|
|
710
900
|
items.push({
|
|
711
901
|
kind: "file", path: independent.path, relative_path: independent.relative_path || basename(independent.path),
|
|
712
902
|
source_id: independent.source_id, source_kind: independent.source_kind, source_label: independent.source_label,
|
|
713
|
-
size: 0, status: "independent", saved: 0
|
|
903
|
+
size: independent.size || 0, status: "independent", saved: 0
|
|
714
904
|
})
|
|
715
905
|
}
|
|
716
906
|
return items
|
|
@@ -780,7 +970,7 @@ const renderSourceNode = (source, depth, items) => {
|
|
|
780
970
|
const pathText = source.kind === "virtual" ? (source.id === "apps" ? "api" : "") : sourcePath(source)
|
|
781
971
|
const count = state.view === "duplicates" ? duplicateCount : trackedCount
|
|
782
972
|
const icon = source.kind === "app" ? "fa-regular fa-folder-open" : "fa-regular fa-folder"
|
|
783
|
-
let html = `<div class="vault-source-line depth-${Math.min(depth, 2)}">`
|
|
973
|
+
let html = `<div class="vault-source-line depth-${Math.min(depth, 2)} ${pathText ? "has-path" : ""}">`
|
|
784
974
|
if (children.length) {
|
|
785
975
|
html += `<button class="vault-source-toggle" type="button" data-toggle-source="${attr(source.id)}" aria-label="${collapsed ? COPY.expand : COPY.collapse}"><i class="fa-solid fa-chevron-${collapsed ? "right" : "down"}"></i></button>`
|
|
786
976
|
} else {
|
|
@@ -808,24 +998,6 @@ const renderLocations = (items) => {
|
|
|
808
998
|
}
|
|
809
999
|
|
|
810
1000
|
const selectedSource = () => state.sourceId ? sourceById(state.sourceId) : null
|
|
811
|
-
const viewTitle = () => {
|
|
812
|
-
const source = selectedSource()
|
|
813
|
-
if (state.view === "all" && source) return source.label
|
|
814
|
-
return viewLabel[state.view]
|
|
815
|
-
}
|
|
816
|
-
const breadcrumb = () => {
|
|
817
|
-
const source = selectedSource()
|
|
818
|
-
if (!source) return ""
|
|
819
|
-
const parts = [source.label]
|
|
820
|
-
let current = source
|
|
821
|
-
const seen = new Set()
|
|
822
|
-
while (current && current.parent_id && !seen.has(current.id)) {
|
|
823
|
-
seen.add(current.id)
|
|
824
|
-
current = sourceById(current.parent_id)
|
|
825
|
-
if (current) parts.unshift(current.label)
|
|
826
|
-
}
|
|
827
|
-
return parts.join(" / ")
|
|
828
|
-
}
|
|
829
1001
|
const scopeDuplicates = (sourceId) => state.data.duplicates.filter((item) => item.source_id === sourceId)
|
|
830
1002
|
const batchAction = (source) => {
|
|
831
1003
|
if (!source || source.kind === "virtual" || source.kind === "pinokio") return ""
|
|
@@ -836,29 +1008,24 @@ const batchAction = (source) => {
|
|
|
836
1008
|
return `<button class="vault-button" type="button" data-deduplicate-scope="${attr(source.id)}">${esc(COPY.deduplicate)} ${countLabel(shareable.length)}</button>`
|
|
837
1009
|
}
|
|
838
1010
|
|
|
839
|
-
const
|
|
840
|
-
const source = selectedSource()
|
|
841
|
-
const title = viewTitle()
|
|
842
|
-
let subtitle = ""
|
|
1011
|
+
const toolbarSummary = (visibleItems) => {
|
|
843
1012
|
if (state.view === "duplicates") {
|
|
844
1013
|
const locations = new Set(visibleItems.map((item) => item.source_id).filter(Boolean)).size
|
|
845
1014
|
const bytes = visibleItems.filter((item) => item.shareable).reduce((sum, item) => sum + item.size, 0)
|
|
846
|
-
|
|
1015
|
+
return `${countLabel(visibleItems.length)} · ${countLabel(locations, COPY.location, COPY.locations_lower)} · ${fmt(bytes)} ${COPY.can_save_suffix}`
|
|
847
1016
|
} else if (state.view === "activity") {
|
|
848
|
-
|
|
1017
|
+
return countLabel(visibleItems.length, COPY.event, COPY.events)
|
|
849
1018
|
} else if (state.view === "reclaimable") {
|
|
850
|
-
|
|
851
|
-
} else {
|
|
852
|
-
const duplicateCount = visibleItems.filter((item) => item.status === "duplicate").length
|
|
853
|
-
const saveable = visibleItems.filter((item) => item.status === "duplicate" && item.shareable).reduce((sum, item) => sum + item.size, 0)
|
|
854
|
-
subtitle = `${countLabel(visibleItems.length)}${duplicateCount ? ` · ${countLabel(duplicateCount, COPY.duplicate.toLowerCase(), COPY.duplicates.toLowerCase())} · ${fmt(saveable)} ${COPY.can_save_suffix}` : ""}`
|
|
1019
|
+
return `${countLabel(visibleItems.length)} · ${fmt(state.data.reclaimable)}`
|
|
855
1020
|
}
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1021
|
+
const duplicateCount = visibleItems.filter((item) => item.status === "duplicate").length
|
|
1022
|
+
const saveable = visibleItems.filter((item) => item.status === "duplicate" && item.shareable).reduce((sum, item) => sum + item.size, 0)
|
|
1023
|
+
return `${countLabel(visibleItems.length)}${duplicateCount ? ` · ${countLabel(duplicateCount, COPY.duplicate.toLowerCase(), COPY.duplicates.toLowerCase())} · ${fmt(saveable)} ${COPY.can_save_suffix}` : ""}`
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const updateToolbarSummary = (visibleItems) => {
|
|
1027
|
+
const summary = el("vault-toolbar-summary")
|
|
1028
|
+
if (summary) summary.textContent = toolbarSummary(visibleItems)
|
|
862
1029
|
}
|
|
863
1030
|
|
|
864
1031
|
const searchPlaceholder = () => {
|
|
@@ -869,14 +1036,15 @@ const searchPlaceholder = () => {
|
|
|
869
1036
|
const source = selectedSource()
|
|
870
1037
|
return source && source.kind === "app" ? `Search in ${source.label}` : COPY.search_all
|
|
871
1038
|
}
|
|
872
|
-
const renderToolbar = (
|
|
1039
|
+
const renderToolbar = (visibleItems) => {
|
|
873
1040
|
if (state.view === "reclaimable") {
|
|
874
1041
|
const count = state.data.blobs.filter((blob) => blob.orphan).length
|
|
875
1042
|
el("vault-toolbar").innerHTML = count
|
|
876
|
-
? `<span class="vault-toolbar-count">${
|
|
1043
|
+
? `<span class="vault-toolbar-count" id="vault-toolbar-summary">${esc(toolbarSummary(visibleItems))}</span><button class="vault-button" type="button" id="btn-reclaim-all">${esc(COPY.reclaim_all)}</button>`
|
|
877
1044
|
: ""
|
|
878
1045
|
return
|
|
879
1046
|
}
|
|
1047
|
+
const source = selectedSource()
|
|
880
1048
|
el("vault-toolbar").innerHTML = `<label class="vault-search"><i class="fa-solid fa-magnifying-glass"></i><input id="vault-search" value="${attr(state.query)}" placeholder="${attr(searchPlaceholder())}" /></label>
|
|
881
1049
|
${state.view === "all" ? `<select class="vault-select" id="vault-status-filter" aria-label="${attr(COPY.all_statuses)}">
|
|
882
1050
|
<option value="all" ${state.statusFilter === "all" ? "selected" : ""}>${esc(COPY.all_statuses)}</option>
|
|
@@ -885,7 +1053,8 @@ const renderToolbar = (visibleCount) => {
|
|
|
885
1053
|
<option value="tracked" ${state.statusFilter === "tracked" ? "selected" : ""}>${esc(COPY.tracked)}</option>
|
|
886
1054
|
<option value="independent" ${state.statusFilter === "independent" ? "selected" : ""}>${esc(COPY.independent)}</option>
|
|
887
1055
|
</select>` : state.view === "duplicates" ? `<span class="vault-select">${esc(COPY.by_location)}</span>` : ""}
|
|
888
|
-
<span class="vault-toolbar-count">${
|
|
1056
|
+
<span class="vault-toolbar-count" id="vault-toolbar-summary">${esc(toolbarSummary(visibleItems))}</span>
|
|
1057
|
+
${state.view === "all" ? batchAction(source) : ""}`
|
|
889
1058
|
}
|
|
890
1059
|
|
|
891
1060
|
const statusMarkup = (item) => {
|
|
@@ -911,7 +1080,7 @@ const rowAction = (item) => {
|
|
|
911
1080
|
|
|
912
1081
|
const fileDetail = (item) => {
|
|
913
1082
|
if (!state.expandedFiles.has(item.path) || !item.locations || item.locations.length < 2) return ""
|
|
914
|
-
return `<div class="vault-detail"><div class="vault-detail-label">${esc(COPY.
|
|
1083
|
+
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) => `
|
|
915
1084
|
<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>`
|
|
916
1085
|
}
|
|
917
1086
|
|
|
@@ -1032,15 +1201,18 @@ const renderInventoryGroups = (items) => {
|
|
|
1032
1201
|
}
|
|
1033
1202
|
|
|
1034
1203
|
const emptyState = (view) => {
|
|
1204
|
+
const scanning = scanActive(state.data && state.data.scan)
|
|
1035
1205
|
const content = {
|
|
1036
|
-
all:
|
|
1206
|
+
all: scanning
|
|
1207
|
+
? [COPY.scan_waiting, COPY.scan_waiting_hint, "fa-solid fa-circle-notch fa-spin"]
|
|
1208
|
+
: [COPY.no_files, COPY.no_files_hint, "fa-regular fa-folder-open"],
|
|
1037
1209
|
duplicates: [COPY.no_duplicates, COPY.no_duplicates_hint, "fa-regular fa-circle-check"],
|
|
1038
1210
|
shared: [COPY.no_shared, COPY.no_shared_hint, "fa-solid fa-link"],
|
|
1039
1211
|
independent: [COPY.no_independent, COPY.no_independent_hint, "fa-solid fa-code-branch"],
|
|
1040
1212
|
reclaimable: [COPY.no_reclaimable, COPY.no_reclaimable_hint, "fa-regular fa-circle-check"],
|
|
1041
1213
|
activity: [COPY.no_activity, COPY.no_activity_hint, "fa-solid fa-wave-square"]
|
|
1042
1214
|
}[view]
|
|
1043
|
-
return `<div class="vault-empty"><div class="vault-empty-inner"><i class="${content[2]}"></i><h3>${esc(content[0])}</h3><p>${esc(content[1])}</p>${view === "duplicates" ? `<button class="vault-button" type="button" data-view="all">${esc(COPY.view_all)}</button>` : view === "all" ? `<button class="vault-button" type="button" id="btn-empty-scan">${esc(COPY.scan)}</button>` : ""}</div></div>`
|
|
1215
|
+
return `<div class="vault-empty"><div class="vault-empty-inner"><i class="${content[2]}"></i><h3>${esc(content[0])}</h3><p>${esc(content[1])}</p>${view === "duplicates" ? `<button class="vault-button" type="button" data-view="all">${esc(COPY.view_all)}</button>` : view === "all" && !scanning ? `<button class="vault-button" type="button" id="btn-empty-scan">${esc(COPY.scan)}</button>` : ""}</div></div>`
|
|
1044
1216
|
}
|
|
1045
1217
|
|
|
1046
1218
|
const renderReclaimable = () => {
|
|
@@ -1090,26 +1262,47 @@ const renderTable = (items) => {
|
|
|
1090
1262
|
const renderOverview = () => {
|
|
1091
1263
|
const data = state.data
|
|
1092
1264
|
const last = data.last_scan
|
|
1093
|
-
el("vault-title").textContent = COPY.title
|
|
1094
|
-
el("vault-subtitle").textContent = data.enabled ? COPY.subtitle : COPY.disabled
|
|
1095
1265
|
const folderMetric = last ? `<div class="vault-metric"><span class="vault-metric-value">${fmt(last.home_bytes_total == null ? last.bytes_total : last.home_bytes_total)}</span><span class="vault-metric-label">${esc(COPY.pinokio_folder)}</span></div>` : ""
|
|
1096
1266
|
el("vault-metrics").innerHTML = `${folderMetric}
|
|
1097
1267
|
<div class="vault-metric" title="${attr(`${fmt(data.bytes_without_sharing)} ${COPY.without_sharing}`)}"><span class="vault-metric-value">${fmt(data.bytes_on_disk)}</span><span class="vault-metric-label">${esc(COPY.on_disk)}</span></div>
|
|
1098
|
-
<div class="vault-metric" title="${attr(
|
|
1268
|
+
<div class="vault-metric" title="${attr(COPY.already_shared_help)}"><span class="vault-metric-value">${fmt(data.saved_by_sharing)}</span><span class="vault-metric-label">${esc(COPY.already_shared)}</span></div>
|
|
1269
|
+
<div class="vault-metric" title="${attr(COPY.saved_by_vault_help)}"><span class="vault-metric-value">${fmt(data.lifetime_bytes_saved)}</span><span class="vault-metric-label">${esc(COPY.saved_by_vault)}</span></div>
|
|
1099
1270
|
<button class="vault-metric" type="button" id="btn-review-metric"><span class="vault-metric-value">${fmt(data.pending_bytes)}</span><span class="vault-metric-label">${esc(COPY.can_save)}</span></button>
|
|
1100
1271
|
<div class="vault-metric"><span class="vault-metric-value">${esc(timeAgo(last && last.ts))}</span><span class="vault-metric-label">${esc(COPY.last_scanned)}</span></div>`
|
|
1101
1272
|
const scanning = data.scan && (data.scan.active || data.scan.queued > 0)
|
|
1102
1273
|
el("btn-scan").innerHTML = scanning ? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}` : `<i class="fa-solid fa-rotate"></i>${esc(last ? COPY.scan_again : COPY.scan)}`
|
|
1103
1274
|
el("btn-scan").disabled = !!scanning
|
|
1104
|
-
el("btn-
|
|
1275
|
+
el("btn-vault-options").setAttribute("aria-label", COPY.vault_options)
|
|
1276
|
+
el("vault-repair-title").textContent = COPY.repair_index
|
|
1277
|
+
el("vault-repair-description").textContent = COPY.repair_description
|
|
1278
|
+
el("btn-repair").textContent = COPY.repair_action
|
|
1279
|
+
el("btn-repair").disabled = !!scanning
|
|
1105
1280
|
const scanState = el("vault-scan-state")
|
|
1106
1281
|
if (scanning) {
|
|
1107
1282
|
const scan = data.scan
|
|
1108
|
-
const
|
|
1283
|
+
const walking = scan.walk_duration_ms == null
|
|
1284
|
+
const hashTotal = scan.hash_total || 0
|
|
1285
|
+
const hashDone = Math.min(scan.hashed || 0, hashTotal)
|
|
1286
|
+
const hashRatio = hashTotal ? Math.min(1, hashDone / hashTotal) : 1
|
|
1287
|
+
const phase = walking ? COPY.scan_progress : hashTotal ? COPY.scan_analyzing : COPY.scan_finishing
|
|
1288
|
+
const hasEstimate = Number.isFinite(scan.estimated_files) && scan.estimated_files > 0
|
|
1289
|
+
const walkWeight = Number.isFinite(scan.estimated_walk_weight) ? scan.estimated_walk_weight : 0.8
|
|
1290
|
+
const walkRatio = hasEstimate ? Math.min(1, (scan.files || 0) / scan.estimated_files) : null
|
|
1291
|
+
const progressRatio = walking
|
|
1292
|
+
? (hasEstimate ? walkRatio * walkWeight : null)
|
|
1293
|
+
: (hasEstimate ? walkWeight + ((1 - walkWeight) * hashRatio) : hashRatio)
|
|
1294
|
+
const progressValue = progressRatio == null ? null : Math.max(0, Math.min(100, Math.round(progressRatio * 100)))
|
|
1295
|
+
const details = [`${scan.dirs || 0} ${COPY.scan_folders}`, `${scan.files || 0} ${COPY.scan_files}`, fmt(scan.bytes_total || 0)]
|
|
1109
1296
|
if (scan.current_file) details.push(`${COPY.analyzing} ${scan.current_file}`)
|
|
1110
|
-
if (scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
|
|
1297
|
+
if (walking && scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
|
|
1298
|
+
const estimated = hasEstimate && progressValue < 100
|
|
1299
|
+
const percent = progressValue == null ? "" : `<span class="vault-scan-percent"${estimated ? ` title="${attr(COPY.scan_estimate_help)}"` : ""}>${estimated ? "~" : ""}${progressValue}%</span>`
|
|
1300
|
+
const progressAttrs = progressValue == null
|
|
1301
|
+
? `role="progressbar" aria-label="${attr(phase)}"`
|
|
1302
|
+
: `role="progressbar" aria-label="${attr(phase)}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${progressValue}"${estimated ? ` aria-valuetext="${attr(`About ${progressValue} percent. ${COPY.scan_estimate_help}.`)}"` : ""}`
|
|
1303
|
+
const progressStyle = progressValue == null ? "" : ` style="--vault-progress:${progressRatio}"`
|
|
1111
1304
|
scanState.classList.add("show")
|
|
1112
|
-
scanState.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i><strong>${esc(
|
|
1305
|
+
scanState.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i><strong>${esc(phase)}</strong><span class="vault-scan-detail">${esc(details.join(" · "))}</span>${percent}<span class="vault-progress-track" ${progressAttrs}><span class="vault-progress-bar ${progressValue == null ? "indeterminate" : "determinate"}"${progressStyle}></span></span>`
|
|
1113
1306
|
} else {
|
|
1114
1307
|
scanState.classList.remove("show")
|
|
1115
1308
|
scanState.innerHTML = ""
|
|
@@ -1153,35 +1346,58 @@ const render = () => {
|
|
|
1153
1346
|
renderViews(items)
|
|
1154
1347
|
renderLocations(items)
|
|
1155
1348
|
const visible = activeItems(items)
|
|
1156
|
-
|
|
1157
|
-
renderToolbar(state.view === "reclaimable" ? state.data.blobs.filter((blob) => blob.orphan).length : state.view === "activity" ? state.data.events.length : visible.length)
|
|
1349
|
+
renderToolbar(visible)
|
|
1158
1350
|
renderTable(visible)
|
|
1159
1351
|
el("vault-pane-footer").textContent = state.view === "duplicates" ? COPY.duplicate_note : COPY.tracked_note
|
|
1160
1352
|
}
|
|
1161
1353
|
|
|
1354
|
+
const scanActive = (scan) => !!(scan && (scan.active || scan.queued > 0))
|
|
1355
|
+
const fetchJson = async (url) => {
|
|
1356
|
+
const response = await fetch(url)
|
|
1357
|
+
if (!response.ok) throw new Error(`Vault status request failed (${response.status})`)
|
|
1358
|
+
return response.json()
|
|
1359
|
+
}
|
|
1360
|
+
const applyFullData = (data) => {
|
|
1361
|
+
const scanning = scanActive(data.scan)
|
|
1362
|
+
const completed = state.scanRequested && !scanning && data.last_scan && data.last_scan.ts !== state.scanBaseline
|
|
1363
|
+
state.data = data
|
|
1364
|
+
if (completed) {
|
|
1365
|
+
state.scanRequested = false
|
|
1366
|
+
const shareable = data.duplicates.filter((item) => item.shareable !== false)
|
|
1367
|
+
state.scanResult = {
|
|
1368
|
+
count: shareable.length,
|
|
1369
|
+
locations: new Set(shareable.map((item) => item.source_id).filter(Boolean)).size,
|
|
1370
|
+
bytes: shareable.reduce((sum, item) => sum + item.size, 0)
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
render()
|
|
1374
|
+
return scanning
|
|
1375
|
+
}
|
|
1162
1376
|
const refresh = async () => {
|
|
1377
|
+
let delay = 15000
|
|
1163
1378
|
try {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
bytes: shareable.reduce((sum, item) => sum + item.size, 0)
|
|
1379
|
+
const progressOnly = !!(state.data && scanActive(state.data.scan))
|
|
1380
|
+
if (progressOnly) {
|
|
1381
|
+
const progress = await fetchJson("/info/dedup?progress=1")
|
|
1382
|
+
state.data.scan = progress.scan
|
|
1383
|
+
state.data.last_scan = progress.last_scan
|
|
1384
|
+
if (scanActive(progress.scan)) {
|
|
1385
|
+
delay = 1500
|
|
1386
|
+
renderOverview()
|
|
1387
|
+
renderFeedback()
|
|
1388
|
+
} else {
|
|
1389
|
+
delay = applyFullData(await fetchJson("/info/dedup")) ? 1500 : 15000
|
|
1176
1390
|
}
|
|
1391
|
+
} else {
|
|
1392
|
+
delay = applyFullData(await fetchJson("/info/dedup")) ? 1500 : 15000
|
|
1177
1393
|
}
|
|
1178
|
-
state.priorScanning = !!scanning
|
|
1179
|
-
render()
|
|
1180
|
-
clearTimeout(window.__vaultRefresh)
|
|
1181
|
-
window.__vaultRefresh = setTimeout(refresh, scanning ? 1500 : 15000)
|
|
1182
1394
|
} catch (error) {
|
|
1183
1395
|
state.feedback = { error: true, message: error && error.message ? error.message : String(error) }
|
|
1184
1396
|
renderFeedback()
|
|
1397
|
+
delay = 5000
|
|
1398
|
+
} finally {
|
|
1399
|
+
clearTimeout(window.__vaultRefresh)
|
|
1400
|
+
window.__vaultRefresh = setTimeout(refresh, delay)
|
|
1185
1401
|
}
|
|
1186
1402
|
}
|
|
1187
1403
|
|
|
@@ -1194,7 +1410,38 @@ const runAction = async (payload, success) => {
|
|
|
1194
1410
|
await refresh()
|
|
1195
1411
|
}
|
|
1196
1412
|
|
|
1413
|
+
const chooseExternalFolder = () => new Promise((resolve, reject) => {
|
|
1414
|
+
const picker = new Socket()
|
|
1415
|
+
let settled = false
|
|
1416
|
+
const finish = (value) => {
|
|
1417
|
+
if (settled) return
|
|
1418
|
+
settled = true
|
|
1419
|
+
resolve(value)
|
|
1420
|
+
}
|
|
1421
|
+
const fail = (error) => {
|
|
1422
|
+
if (settled) return
|
|
1423
|
+
settled = true
|
|
1424
|
+
reject(error)
|
|
1425
|
+
}
|
|
1426
|
+
picker.run({
|
|
1427
|
+
method: "kernel.bin.filepicker",
|
|
1428
|
+
params: { title: COPY.add_external_folder, type: "folder" }
|
|
1429
|
+
}, (packet) => {
|
|
1430
|
+
if (packet.type === "result") {
|
|
1431
|
+
const paths = packet.data && Array.isArray(packet.data.paths) ? packet.data.paths : []
|
|
1432
|
+
finish(paths[0] || null)
|
|
1433
|
+
picker.close()
|
|
1434
|
+
} else if (packet.type === "error") {
|
|
1435
|
+
const message = packet.data && packet.data.message ? packet.data.message : "The folder picker could not be opened."
|
|
1436
|
+
fail(new Error(message))
|
|
1437
|
+
picker.close()
|
|
1438
|
+
}
|
|
1439
|
+
}).then(() => finish(null)).catch(fail)
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1197
1442
|
document.addEventListener("click", async (event) => {
|
|
1443
|
+
const advanced = el("vault-advanced")
|
|
1444
|
+
if (advanced.open && !advanced.contains(event.target)) advanced.open = false
|
|
1198
1445
|
const target = event.target.closest("button")
|
|
1199
1446
|
if (!target) return
|
|
1200
1447
|
if (target.dataset.view) {
|
|
@@ -1222,6 +1469,34 @@ document.addEventListener("click", async (event) => {
|
|
|
1222
1469
|
if (state.expandedFiles.has(file)) state.expandedFiles.delete(file)
|
|
1223
1470
|
else state.expandedFiles.add(file)
|
|
1224
1471
|
render()
|
|
1472
|
+
} else if (target.id === "btn-add-source") {
|
|
1473
|
+
target.disabled = true
|
|
1474
|
+
try {
|
|
1475
|
+
const folderPath = await chooseExternalFolder()
|
|
1476
|
+
if (!folderPath) return
|
|
1477
|
+
const result = await post({ action: "add_source", path: folderPath })
|
|
1478
|
+
if (result.error) {
|
|
1479
|
+
state.feedback = { error: true, message: result.error }
|
|
1480
|
+
} else {
|
|
1481
|
+
state.view = "all"
|
|
1482
|
+
state.sourceId = result.source && result.source.id ? result.source.id : null
|
|
1483
|
+
state.query = ""
|
|
1484
|
+
state.feedback = {
|
|
1485
|
+
error: false,
|
|
1486
|
+
message: result.created === false
|
|
1487
|
+
? COPY.external_exists
|
|
1488
|
+
: result.source && result.source.shareable === false
|
|
1489
|
+
? COPY.external_other_disk
|
|
1490
|
+
: COPY.external_added
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
await refresh()
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
state.feedback = { error: true, message: error && error.message ? error.message : String(error) }
|
|
1496
|
+
renderFeedback()
|
|
1497
|
+
} finally {
|
|
1498
|
+
target.disabled = false
|
|
1499
|
+
}
|
|
1225
1500
|
} else if (target.id === "btn-scan" || target.id === "btn-empty-scan") {
|
|
1226
1501
|
state.scanRequested = true
|
|
1227
1502
|
state.scanBaseline = state.data.last_scan ? state.data.last_scan.ts : null
|
|
@@ -1229,8 +1504,11 @@ document.addEventListener("click", async (event) => {
|
|
|
1229
1504
|
state.feedback = null
|
|
1230
1505
|
await post({ action: "scan" })
|
|
1231
1506
|
await refresh()
|
|
1232
|
-
} else if (target.id === "btn-
|
|
1233
|
-
|
|
1507
|
+
} else if (target.id === "btn-repair") {
|
|
1508
|
+
target.disabled = true
|
|
1509
|
+
target.textContent = COPY.repairing
|
|
1510
|
+
await runAction({ action: "repair" }, COPY.repair_done)
|
|
1511
|
+
advanced.open = false
|
|
1234
1512
|
} else if (target.id === "btn-review-result" || target.id === "btn-review-metric") {
|
|
1235
1513
|
state.view = "duplicates"
|
|
1236
1514
|
state.sourceId = null
|
|
@@ -1256,7 +1534,7 @@ document.addEventListener("input", (event) => {
|
|
|
1256
1534
|
if (event.target.id !== "vault-search") return
|
|
1257
1535
|
state.query = event.target.value
|
|
1258
1536
|
const items = activeItems(buildItems())
|
|
1259
|
-
|
|
1537
|
+
updateToolbarSummary(items)
|
|
1260
1538
|
renderTable(items)
|
|
1261
1539
|
})
|
|
1262
1540
|
document.addEventListener("change", (event) => {
|
|
@@ -1265,10 +1543,13 @@ document.addEventListener("change", (event) => {
|
|
|
1265
1543
|
render()
|
|
1266
1544
|
})
|
|
1267
1545
|
|
|
1268
|
-
el("vault-title").textContent = COPY.title
|
|
1269
|
-
el("vault-subtitle").textContent = COPY.subtitle
|
|
1270
1546
|
el("btn-scan").textContent = COPY.scan
|
|
1271
|
-
el("btn-
|
|
1547
|
+
el("btn-add-source").setAttribute("aria-label", COPY.add_external_folder)
|
|
1548
|
+
el("btn-add-source").setAttribute("title", COPY.add_external_folder)
|
|
1549
|
+
el("btn-vault-options").setAttribute("aria-label", COPY.vault_options)
|
|
1550
|
+
el("vault-repair-title").textContent = COPY.repair_index
|
|
1551
|
+
el("vault-repair-description").textContent = COPY.repair_description
|
|
1552
|
+
el("btn-repair").textContent = COPY.repair_action
|
|
1272
1553
|
refresh()
|
|
1273
1554
|
</script>
|
|
1274
1555
|
</body>
|