@necrolab/dashboard 0.5.32 → 0.5.34
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/backend/endpoints.js +1 -1
- package/package.json +1 -1
- package/src/assets/img/reconnect-logo.png +0 -0
- package/src/components/Tasks/Stats.vue +348 -25
- package/src/components/Tasks/TaskView.vue +20 -10
- package/src/components/ui/ReconnectIndicator.vue +1 -1
- package/src/stores/connection.js +4 -2
- package/src/stores/ui.js +83 -0
- package/src/views/Tasks.vue +7 -5
- package/workbox-config.cjs +12 -0
package/backend/endpoints.js
CHANGED
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,73 +1,396 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="queue-stats"
|
|
4
|
+
:class="{ 'queue-stats--full': visibleCards === 4 }"
|
|
5
|
+
:style="statsGridStyle"
|
|
6
|
+
v-if="ui.queueStats.show"
|
|
7
|
+
:key="key">
|
|
3
8
|
<div
|
|
4
9
|
v-if="ui.queueStats.total"
|
|
5
|
-
class="stat-badge">
|
|
6
|
-
<h2 class="
|
|
10
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
11
|
+
<h2 class="queue-stats__header">
|
|
7
12
|
<img width="14px" src="@/assets/img/wildcard.svg" />
|
|
8
|
-
<span class="hidden md:inline">Total</span>
|
|
9
13
|
</h2>
|
|
10
|
-
<span class="
|
|
14
|
+
<span class="queue-stats__value">
|
|
11
15
|
{{ ui.queueStats.total }}
|
|
12
16
|
</span>
|
|
13
17
|
</div>
|
|
14
18
|
<div
|
|
15
19
|
v-if="ui.queueStats.queued"
|
|
16
|
-
class="stat-badge">
|
|
17
|
-
<h2 class="
|
|
20
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
21
|
+
<h2 class="queue-stats__header">
|
|
18
22
|
<SkiIcon />
|
|
19
|
-
<span class="hidden md:inline">Queued</span>
|
|
20
23
|
</h2>
|
|
21
|
-
<span class="
|
|
24
|
+
<span class="queue-stats__value">
|
|
22
25
|
{{ ui.queueStats.queued }}
|
|
23
26
|
</span>
|
|
24
27
|
</div>
|
|
25
28
|
<div
|
|
26
29
|
v-if="ui.queueStats.sleeping"
|
|
27
|
-
class="stat-badge">
|
|
28
|
-
<h2 class="
|
|
30
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
31
|
+
<h2 class="queue-stats__header">
|
|
29
32
|
<TimerIcon />
|
|
30
|
-
<span class="hidden md:inline">Sleeping</span>
|
|
31
33
|
</h2>
|
|
32
|
-
<span class="
|
|
34
|
+
<span class="queue-stats__value">
|
|
33
35
|
{{ ui.queueStats.sleeping }}
|
|
34
36
|
</span>
|
|
35
37
|
</div>
|
|
36
38
|
<div
|
|
37
39
|
v-if="ui.queueStats.nextQueuePasses.length > 0"
|
|
38
|
-
class="
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
<span class="block sm:hidden">Pass</span>
|
|
40
|
+
class="stat-badge queue-stats__card queue-stats__card--passes"
|
|
41
|
+
:title="ui.queueStats.nextQueuePasses.join(', ')">
|
|
42
|
+
<h2 class="queue-stats__header">
|
|
43
|
+
<SandclockIcon />
|
|
43
44
|
</h2>
|
|
44
|
-
<span class="
|
|
45
|
-
{{
|
|
45
|
+
<span class="queue-stats__value queue-stats__value--passes">
|
|
46
|
+
<span class="queue-stats__pass-text">{{ queuePassPreviewText }}</span>
|
|
47
|
+
<button
|
|
48
|
+
v-if="hiddenPassCount > 0"
|
|
49
|
+
type="button"
|
|
50
|
+
class="queue-stats__more-btn"
|
|
51
|
+
@click.stop="openPassesOverlay">
|
|
52
|
+
+{{ hiddenPassCount }}
|
|
53
|
+
</button>
|
|
46
54
|
</span>
|
|
47
55
|
</div>
|
|
48
56
|
</div>
|
|
57
|
+
|
|
58
|
+
<Teleport to="body">
|
|
59
|
+
<div
|
|
60
|
+
v-if="showPassesOverlay"
|
|
61
|
+
class="queue-pass-overlay"
|
|
62
|
+
@click.self="closePassesOverlay">
|
|
63
|
+
<div class="queue-pass-modal">
|
|
64
|
+
<div class="queue-pass-modal__header">
|
|
65
|
+
<h3>Next Queue Passes</h3>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
class="queue-pass-modal__close"
|
|
69
|
+
@click="closePassesOverlay">
|
|
70
|
+
Close
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="queue-pass-modal__body">
|
|
74
|
+
<ul class="queue-pass-modal__list">
|
|
75
|
+
<li
|
|
76
|
+
v-for="(pass, index) in ui.queueStats.nextQueuePasses"
|
|
77
|
+
:key="`${pass}-${index}`"
|
|
78
|
+
class="queue-pass-modal__item">
|
|
79
|
+
<span>#{{ index + 1 }}</span>
|
|
80
|
+
<span>{{ pass }}</span>
|
|
81
|
+
</li>
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</Teleport>
|
|
49
87
|
</template>
|
|
50
88
|
|
|
51
89
|
<script setup>
|
|
52
|
-
import { SkiIcon, TimerIcon,
|
|
90
|
+
import { SkiIcon, TimerIcon, SandclockIcon } from "@/components/icons";
|
|
53
91
|
import { useUIStore } from "@/stores/ui";
|
|
54
|
-
import { ref, onUnmounted } from "vue";
|
|
92
|
+
import { ref, onUnmounted, computed, onMounted } from "vue";
|
|
55
93
|
|
|
56
94
|
const ui = useUIStore();
|
|
57
95
|
|
|
58
96
|
const getQueuePassAmount = (width) => {
|
|
59
|
-
if (width
|
|
60
|
-
if (width
|
|
97
|
+
if (width >= 1536) return 10;
|
|
98
|
+
if (width >= 1280) return 8;
|
|
99
|
+
if (width >= 1024) return 6;
|
|
100
|
+
if (width >= 768) return 4;
|
|
101
|
+
if (width >= 640) return 2;
|
|
61
102
|
return 1;
|
|
62
103
|
};
|
|
104
|
+
const getQueuePassCharBudget = (width) => {
|
|
105
|
+
if (width >= 1536) return 56;
|
|
106
|
+
if (width >= 1280) return 42;
|
|
107
|
+
if (width >= 1024) return 32;
|
|
108
|
+
if (width >= 768) return 24;
|
|
109
|
+
if (width >= 640) return 16;
|
|
110
|
+
return 10;
|
|
111
|
+
};
|
|
112
|
+
const buildQueuePassPreview = (passes, maxItems, maxChars) => {
|
|
113
|
+
const values = passes.slice(0, maxItems).map((value) => String(value));
|
|
114
|
+
if (!values.length) return "";
|
|
115
|
+
|
|
116
|
+
let currentLength = 0;
|
|
117
|
+
const output = [];
|
|
118
|
+
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
const separator = output.length ? 2 : 0;
|
|
121
|
+
const nextLength = currentLength + separator + value.length;
|
|
122
|
+
if (nextLength > maxChars) break;
|
|
123
|
+
output.push(value);
|
|
124
|
+
currentLength = nextLength;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (output.length === 0) output.push(values[0]);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
visible: output,
|
|
131
|
+
hiddenCount: Math.max(passes.length - output.length, 0)
|
|
132
|
+
};
|
|
133
|
+
};
|
|
63
134
|
|
|
64
135
|
let key = ref(0);
|
|
136
|
+
const width = ref(window.innerWidth);
|
|
65
137
|
let queuePassAmount = ref(getQueuePassAmount(window.innerWidth));
|
|
138
|
+
const queuePassCharBudget = ref(getQueuePassCharBudget(window.innerWidth));
|
|
139
|
+
const showPassesOverlay = ref(false);
|
|
140
|
+
const visibleCards = computed(
|
|
141
|
+
() =>
|
|
142
|
+
Number(Boolean(ui.queueStats.total)) +
|
|
143
|
+
Number(Boolean(ui.queueStats.queued)) +
|
|
144
|
+
Number(Boolean(ui.queueStats.sleeping)) +
|
|
145
|
+
Number(Boolean(ui.queueStats.nextQueuePasses.length))
|
|
146
|
+
);
|
|
147
|
+
const statsGridStyle = computed(() => ({
|
|
148
|
+
"--stats-card-count": `${Math.max(visibleCards.value, 1)}`
|
|
149
|
+
}));
|
|
150
|
+
const queuePassPreviewData = computed(() =>
|
|
151
|
+
buildQueuePassPreview(ui.queueStats.nextQueuePasses, queuePassAmount.value, queuePassCharBudget.value)
|
|
152
|
+
);
|
|
153
|
+
const queuePassPreviewText = computed(() => queuePassPreviewData.value.visible.join(", "));
|
|
154
|
+
const hiddenPassCount = computed(() => queuePassPreviewData.value.hiddenCount);
|
|
155
|
+
|
|
156
|
+
const openPassesOverlay = () => {
|
|
157
|
+
if (!hiddenPassCount.value) return;
|
|
158
|
+
showPassesOverlay.value = true;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const closePassesOverlay = () => {
|
|
162
|
+
showPassesOverlay.value = false;
|
|
163
|
+
};
|
|
66
164
|
|
|
67
|
-
const handleResize = () =>
|
|
165
|
+
const handleResize = () => {
|
|
166
|
+
width.value = window.innerWidth;
|
|
167
|
+
queuePassAmount.value = getQueuePassAmount(width.value);
|
|
168
|
+
queuePassCharBudget.value = getQueuePassCharBudget(width.value);
|
|
169
|
+
};
|
|
68
170
|
window.addEventListener("resize", handleResize);
|
|
69
171
|
|
|
172
|
+
const handleKeyDown = (event) => {
|
|
173
|
+
if (event.key === "Escape" && showPassesOverlay.value) {
|
|
174
|
+
closePassesOverlay();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
onMounted(() => {
|
|
179
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
180
|
+
});
|
|
181
|
+
|
|
70
182
|
onUnmounted(() => {
|
|
71
183
|
window.removeEventListener("resize", handleResize);
|
|
184
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
72
185
|
});
|
|
73
186
|
</script>
|
|
187
|
+
|
|
188
|
+
<style scoped>
|
|
189
|
+
.queue-stats {
|
|
190
|
+
display: grid;
|
|
191
|
+
width: 100%;
|
|
192
|
+
min-width: 0;
|
|
193
|
+
grid-template-columns: repeat(var(--stats-card-count), minmax(0, 1fr));
|
|
194
|
+
gap: 0.35rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.queue-stats__card {
|
|
198
|
+
@apply bg-dark-400 border-2 border-dark-550;
|
|
199
|
+
height: 2.5rem;
|
|
200
|
+
min-width: 0;
|
|
201
|
+
gap: 0.35rem;
|
|
202
|
+
border-radius: 0.55rem;
|
|
203
|
+
padding: 0 0.5rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.queue-stats__header {
|
|
207
|
+
min-width: 1rem;
|
|
208
|
+
flex: 0 0 auto;
|
|
209
|
+
display: flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
gap: 0.25rem;
|
|
212
|
+
color: rgb(212 212 219 / 1);
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
font-size: 0.7rem;
|
|
215
|
+
line-height: 1.1;
|
|
216
|
+
white-space: nowrap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.queue-stats__value {
|
|
220
|
+
min-width: 0;
|
|
221
|
+
flex: 1 1 auto;
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: flex-end;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
text-align: right;
|
|
227
|
+
text-overflow: clip;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
color: rgb(212 212 219 / 1);
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
font-size: 0.7rem;
|
|
232
|
+
font-variant-numeric: tabular-nums;
|
|
233
|
+
line-height: 1.1;
|
|
234
|
+
margin-left: 0.25rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.queue-stats__card--metric .queue-stats__header {
|
|
238
|
+
min-width: 1rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.queue-stats__card--metric .queue-stats__value {
|
|
242
|
+
flex: 0 0 auto;
|
|
243
|
+
min-width: 3.4rem;
|
|
244
|
+
margin-left: 0.35rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.queue-stats__header :deep(svg),
|
|
248
|
+
.queue-stats__header img {
|
|
249
|
+
width: 12px;
|
|
250
|
+
height: 12px;
|
|
251
|
+
flex-shrink: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.queue-stats__card--passes {
|
|
255
|
+
padding-right: 0.4rem;
|
|
256
|
+
gap: 0.3rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.queue-stats__card--passes .queue-stats__header {
|
|
260
|
+
min-width: 1.1rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.queue-stats__value--passes {
|
|
264
|
+
font-size: 0.67rem;
|
|
265
|
+
gap: 0.35rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.queue-stats__pass-text {
|
|
269
|
+
min-width: 0;
|
|
270
|
+
flex: 1 1 auto;
|
|
271
|
+
overflow: hidden;
|
|
272
|
+
white-space: nowrap;
|
|
273
|
+
text-overflow: ellipsis;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.queue-stats__more-btn {
|
|
277
|
+
@apply bg-dark-550 border border-dark-650 text-light-300;
|
|
278
|
+
flex-shrink: 0;
|
|
279
|
+
border-radius: 9999px;
|
|
280
|
+
padding: 0.1rem 0.4rem;
|
|
281
|
+
font-size: 0.62rem;
|
|
282
|
+
font-weight: 700;
|
|
283
|
+
line-height: 1.2;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.queue-pass-overlay {
|
|
287
|
+
position: fixed;
|
|
288
|
+
inset: 0;
|
|
289
|
+
z-index: 90;
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: center;
|
|
293
|
+
background: rgba(0, 0, 0, 0.45);
|
|
294
|
+
padding: 1rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.queue-pass-modal {
|
|
298
|
+
@apply bg-dark-400 border border-dark-550;
|
|
299
|
+
width: min(34rem, 95vw);
|
|
300
|
+
border-radius: 0.75rem;
|
|
301
|
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.queue-pass-modal__header {
|
|
305
|
+
@apply border-b border-dark-550 text-light-200;
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
justify-content: space-between;
|
|
309
|
+
gap: 0.5rem;
|
|
310
|
+
padding: 0.75rem 0.9rem;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.queue-pass-modal__header h3 {
|
|
314
|
+
font-size: 0.9rem;
|
|
315
|
+
font-weight: 700;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.queue-pass-modal__close {
|
|
319
|
+
@apply bg-dark-500 border border-dark-650 text-light-300;
|
|
320
|
+
border-radius: 0.45rem;
|
|
321
|
+
padding: 0.25rem 0.5rem;
|
|
322
|
+
font-size: 0.72rem;
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.queue-pass-modal__body {
|
|
327
|
+
max-height: min(60vh, 28rem);
|
|
328
|
+
overflow-y: auto;
|
|
329
|
+
padding: 0.55rem;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.queue-pass-modal__list {
|
|
333
|
+
display: grid;
|
|
334
|
+
gap: 0.35rem;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.queue-pass-modal__item {
|
|
338
|
+
@apply bg-dark-500 border border-dark-650 text-light-300;
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
justify-content: space-between;
|
|
342
|
+
border-radius: 0.5rem;
|
|
343
|
+
padding: 0.35rem 0.55rem;
|
|
344
|
+
font-size: 0.78rem;
|
|
345
|
+
font-variant-numeric: tabular-nums;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@media (min-width: 768px) {
|
|
349
|
+
.queue-stats {
|
|
350
|
+
gap: 0.45rem;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.queue-stats__card {
|
|
354
|
+
padding: 0 0.7rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.queue-stats__header {
|
|
358
|
+
gap: 0.35rem;
|
|
359
|
+
font-size: 0.72rem;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.queue-stats__value {
|
|
363
|
+
font-size: 0.72rem;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.queue-stats__value--passes {
|
|
367
|
+
font-size: 0.7rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.queue-stats__card--metric .queue-stats__value {
|
|
371
|
+
min-width: 3.7rem;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@media (min-width: 960px) {
|
|
376
|
+
.queue-stats--full {
|
|
377
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.35fr);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@media (min-width: 1280px) {
|
|
382
|
+
.queue-stats--full {
|
|
383
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.queue-stats--full .queue-stats__card--passes {
|
|
387
|
+
grid-column: span 2;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@media (min-width: 1280px) {
|
|
392
|
+
.queue-stats__value--passes {
|
|
393
|
+
font-size: 0.75rem;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
</style>
|
|
@@ -270,16 +270,26 @@ const maxTableHeight = computed(() => {
|
|
|
270
270
|
const headerHeight = windowWidth.value >= 1024 ? 80 : 64; // Navbar padding (lg:pt-20 vs pt-16)
|
|
271
271
|
const titleHeight = 45; // Tasks title and mobile controls
|
|
272
272
|
const controlsHeight = windowWidth.value >= 650 ? 55 : 0; // Desktop controls
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
273
|
+
// Keep this in sync with Tasks.vue controls layout:
|
|
274
|
+
// - >= 1280px: stats stays in same row as filters
|
|
275
|
+
// - 768px..1279px: stats takes its own wrapped row when visible
|
|
276
|
+
// - < 768px: mobile has dedicated stats row + controls/search rows
|
|
277
|
+
const isDesktopInlineStats = windowWidth.value >= 1280;
|
|
278
|
+
const isTabletWrappedStats = windowWidth.value >= 768 && windowWidth.value < 1280;
|
|
279
|
+
|
|
280
|
+
const filtersAndStatsHeight = (() => {
|
|
281
|
+
if (isDesktopInlineStats) {
|
|
282
|
+
return ui.queueStats.show ? 50 : 45;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (isTabletWrappedStats) {
|
|
286
|
+
const filterRowHeight = 45;
|
|
287
|
+
const wrappedStatsRowHeight = ui.queueStats.show ? 54 : 0;
|
|
288
|
+
return filterRowHeight + wrappedStatsRowHeight;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return ui.queueStats.show ? 155 : 110;
|
|
292
|
+
})();
|
|
283
293
|
|
|
284
294
|
// Reserve more space for UTILS on iPhone PWA to prevent overflow
|
|
285
295
|
const utilitiesHeight = isPWA && windowWidth.value <= 768
|
package/src/stores/connection.js
CHANGED
|
@@ -3,6 +3,7 @@ import WebsocketHeartbeatJs from "websocket-heartbeat-js";
|
|
|
3
3
|
import { decode } from "@msgpack/msgpack";
|
|
4
4
|
import router from "@/router/index";
|
|
5
5
|
import { sortTaskIds } from "@/libs/utils/array";
|
|
6
|
+
import { DEBUG } from "@/utils/debug";
|
|
6
7
|
|
|
7
8
|
const TASK_UPDATE_EVENTS = new Set(["task-update", "task-setstatus", "task-setinfo"]);
|
|
8
9
|
const QUEUE_STAT_FIELDS = new Set(["siteId", "eventId", "inQueue", "status", "queuePosition"]);
|
|
@@ -568,6 +569,7 @@ export class ConnectionHandler {
|
|
|
568
569
|
if (msg?.data) msg.data.module = this.ui.currentModule;
|
|
569
570
|
this.socket.send(JSON.stringify(msg));
|
|
570
571
|
} catch {
|
|
572
|
+
if (DEBUG) return;
|
|
571
573
|
this.ui.startSpinner("Reconnecting");
|
|
572
574
|
}
|
|
573
575
|
}
|
|
@@ -604,13 +606,13 @@ export class ConnectionHandler {
|
|
|
604
606
|
};
|
|
605
607
|
|
|
606
608
|
this.socket.onclose = () => {
|
|
607
|
-
this.ui.startSpinner("Reconnecting");
|
|
609
|
+
if (!DEBUG) this.ui.startSpinner("Reconnecting");
|
|
608
610
|
hasShownDisconnect = true;
|
|
609
611
|
this.clearTaskUpdateQueue();
|
|
610
612
|
};
|
|
611
613
|
|
|
612
614
|
this.socket.onerror = () => {
|
|
613
|
-
this.ui.startSpinner("Reconnecting");
|
|
615
|
+
if (!DEBUG) this.ui.startSpinner("Reconnecting");
|
|
614
616
|
hasShownDisconnect = true;
|
|
615
617
|
this.clearTaskUpdateQueue();
|
|
616
618
|
};
|
package/src/stores/ui.js
CHANGED
|
@@ -24,6 +24,16 @@ import { DEBUG } from "@/utils/debug";
|
|
|
24
24
|
const ALL_SELECTED = 0;
|
|
25
25
|
const SOME_SELECTED = 1;
|
|
26
26
|
const NO_SELECTED = 2;
|
|
27
|
+
const DEV_QUEUE_STATS_STORAGE_KEY = "dev-queue-stats-mock";
|
|
28
|
+
const DEV_QUEUE_STATS_PRESETS = {
|
|
29
|
+
heavy: {
|
|
30
|
+
show: true,
|
|
31
|
+
total: 10000,
|
|
32
|
+
queued: 5000,
|
|
33
|
+
sleeping: 3000,
|
|
34
|
+
nextQueuePasses: [11, 18, 26, 35, 49, 57, 72, 88, 103, 121, 147, 169]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
27
37
|
|
|
28
38
|
export const useUIStore = defineStore("ui", () => {
|
|
29
39
|
const logger = createLogger("UI");
|
|
@@ -98,6 +108,55 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
98
108
|
sleeping: 0,
|
|
99
109
|
nextQueuePasses: []
|
|
100
110
|
});
|
|
111
|
+
const devQueueStatsMock = ref(null);
|
|
112
|
+
|
|
113
|
+
const normalizeQueueStats = (input = {}) => {
|
|
114
|
+
const total = Number(input.total) || 0;
|
|
115
|
+
const queued = Number(input.queued) || 0;
|
|
116
|
+
const sleeping = Number(input.sleeping) || 0;
|
|
117
|
+
const nextQueuePasses = Array.isArray(input.nextQueuePasses)
|
|
118
|
+
? input.nextQueuePasses.map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
119
|
+
: [];
|
|
120
|
+
const show = Boolean(input.show ?? (queued > 0 || sleeping > 0 || nextQueuePasses.length > 0));
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
show,
|
|
124
|
+
total,
|
|
125
|
+
queued,
|
|
126
|
+
sleeping,
|
|
127
|
+
nextQueuePasses: nextQueuePasses.sort((a, b) => a - b)
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const applyQueueStats = (stats) => {
|
|
132
|
+
queueStats.value.show = stats.show;
|
|
133
|
+
queueStats.value.total = stats.total;
|
|
134
|
+
queueStats.value.queued = stats.queued;
|
|
135
|
+
queueStats.value.sleeping = stats.sleeping;
|
|
136
|
+
queueStats.value.nextQueuePasses = stats.nextQueuePasses;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const setDevQueueStatsMock = (stats) => {
|
|
140
|
+
if (!DEBUG) return;
|
|
141
|
+
if (!stats) {
|
|
142
|
+
devQueueStatsMock.value = null;
|
|
143
|
+
localStorage.removeItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
144
|
+
refreshQueueStats();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const normalized = normalizeQueueStats(stats);
|
|
149
|
+
devQueueStatsMock.value = normalized;
|
|
150
|
+
localStorage.setItem(DEV_QUEUE_STATS_STORAGE_KEY, JSON.stringify(normalized));
|
|
151
|
+
applyQueueStats(normalized);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const setDevQueueStatsPreset = (preset = "heavy") => {
|
|
155
|
+
if (!DEBUG) return;
|
|
156
|
+
const data = DEV_QUEUE_STATS_PRESETS[preset];
|
|
157
|
+
if (!data) return;
|
|
158
|
+
setDevQueueStatsMock(data);
|
|
159
|
+
};
|
|
101
160
|
|
|
102
161
|
const router = useRouter();
|
|
103
162
|
const openContextMenu = ref("");
|
|
@@ -246,6 +305,11 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
246
305
|
};
|
|
247
306
|
|
|
248
307
|
const refreshQueueStats = () => {
|
|
308
|
+
if (DEBUG && devQueueStatsMock.value) {
|
|
309
|
+
applyQueueStats(devQueueStatsMock.value);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
249
313
|
const siteId = currentCountry.value.siteId;
|
|
250
314
|
const eventId = currentEvent.value;
|
|
251
315
|
const sleepingStatuses = new Set([
|
|
@@ -284,6 +348,23 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
284
348
|
queueStats.value.show = queued > 0 || sleeping > 0 || positions.length > 0;
|
|
285
349
|
};
|
|
286
350
|
|
|
351
|
+
if (DEBUG) {
|
|
352
|
+
try {
|
|
353
|
+
const saved = localStorage.getItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
354
|
+
if (saved) {
|
|
355
|
+
const parsed = JSON.parse(saved);
|
|
356
|
+
devQueueStatsMock.value = normalizeQueueStats(parsed);
|
|
357
|
+
applyQueueStats(devQueueStatsMock.value);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
localStorage.removeItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
window.__setQueueStatsMock = (stats) => setDevQueueStatsMock(stats);
|
|
364
|
+
window.__setQueueStatsPreset = (preset = "heavy") => setDevQueueStatsPreset(preset);
|
|
365
|
+
window.__clearQueueStatsMock = () => setDevQueueStatsMock(null);
|
|
366
|
+
}
|
|
367
|
+
|
|
287
368
|
const toggleModal = (name, clearValue = false) => {
|
|
288
369
|
if (disabledButtons.value["add-tasks"] && name === "create-task") return;
|
|
289
370
|
if (clearValue) currentlyEditing.value = {};
|
|
@@ -551,6 +632,8 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
551
632
|
|
|
552
633
|
disabledButtons,
|
|
553
634
|
queueStats,
|
|
635
|
+
setDevQueueStatsMock,
|
|
636
|
+
setDevQueueStatsPreset,
|
|
554
637
|
|
|
555
638
|
refreshQueueStats,
|
|
556
639
|
|
package/src/views/Tasks.vue
CHANGED
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
@deleteAll="ui.deleteTasks()" />
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
-
<div class="mb-2 flex
|
|
41
|
-
<Stats class="stats-component
|
|
42
|
-
<div class="flex-gap-2 items-center">
|
|
40
|
+
<div class="mb-2 flex flex-col gap-2 md:hidden">
|
|
41
|
+
<Stats class="stats-component w-full" />
|
|
42
|
+
<div class="flex-gap-2 items-center justify-end">
|
|
43
43
|
<div
|
|
44
44
|
class="flex h-8 items-center justify-between rounded-md border border-dark-650 bg-dark-400 px-2 text-xs font-medium text-white">
|
|
45
45
|
<p class="text-2xs">Name</p>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
56
|
|
|
57
|
-
<div class="mb-1 hidden items-center gap-2 md:flex lg:mb-2">
|
|
57
|
+
<div class="mb-1 hidden flex-wrap items-center gap-2 md:flex xl:flex-nowrap lg:mb-2">
|
|
58
58
|
<div v-if="uniqEventIds.length > 1" class="w-52 flex-shrink-0">
|
|
59
59
|
<Dropdown
|
|
60
60
|
:onClick="(f) => ui.setCurrentEvent(f)"
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
:darker="true"
|
|
88
88
|
:current="ui.taskFilter"
|
|
89
89
|
@change="(e) => ui.setTaskFilter(e)" />
|
|
90
|
-
<
|
|
90
|
+
<div class="stats-component min-w-0 w-full md:basis-full xl:ml-auto xl:basis-auto xl:flex-1 xl:max-w-2xl">
|
|
91
|
+
<Stats class="w-full" />
|
|
92
|
+
</div>
|
|
91
93
|
</div>
|
|
92
94
|
|
|
93
95
|
<div class="mb-1 flex flex-col gap-2 md:hidden">
|
package/workbox-config.cjs
CHANGED
|
@@ -50,6 +50,18 @@ module.exports = {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
// Critical UI images used during reconnect/offline states
|
|
54
|
+
{
|
|
55
|
+
urlPattern: /\/img\/(?:reconnect-logo\.png|logo_trans\.png|pwa\/.+\.png)$/,
|
|
56
|
+
handler: "CacheFirst",
|
|
57
|
+
options: {
|
|
58
|
+
cacheName: "critical-ui-images",
|
|
59
|
+
expiration: {
|
|
60
|
+
maxEntries: 20,
|
|
61
|
+
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
53
65
|
// Images - Cache first for best performance
|
|
54
66
|
{
|
|
55
67
|
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/,
|