@necrolab/dashboard 0.5.14 → 0.5.16

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.
Files changed (120) hide show
  1. package/backend/api.js +2 -3
  2. package/eslint.config.js +46 -0
  3. package/index.html +2 -1
  4. package/package.json +5 -2
  5. package/src/App.vue +140 -170
  6. package/src/assets/css/base/mixins.scss +72 -0
  7. package/src/assets/css/base/reset.scss +0 -2
  8. package/src/assets/css/base/scroll.scss +43 -36
  9. package/src/assets/css/base/typography.scss +9 -10
  10. package/src/assets/css/base/variables.scss +43 -0
  11. package/src/assets/css/components/accessibility.scss +37 -0
  12. package/src/assets/css/components/buttons.scss +58 -15
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +119 -0
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -19
  17. package/src/assets/css/components/tables.scss +5 -7
  18. package/src/assets/css/components/toasts.scss +7 -7
  19. package/src/assets/css/components/utilities.scss +220 -0
  20. package/src/assets/css/main.scss +72 -75
  21. package/src/components/Auth/LoginForm.vue +5 -84
  22. package/src/components/Editors/Account/Account.vue +8 -10
  23. package/src/components/Editors/Account/AccountCreator.vue +28 -59
  24. package/src/components/Editors/Account/AccountView.vue +38 -86
  25. package/src/components/Editors/Account/CreateAccount.vue +8 -50
  26. package/src/components/Editors/Profile/CreateProfile.vue +74 -131
  27. package/src/components/Editors/Profile/Profile.vue +15 -17
  28. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  29. package/src/components/Editors/Profile/ProfileView.vue +46 -96
  30. package/src/components/Editors/TagLabel.vue +16 -55
  31. package/src/components/Editors/TagToggle.vue +20 -8
  32. package/src/components/Filter/Filter.vue +62 -75
  33. package/src/components/Filter/FilterPreview.vue +161 -135
  34. package/src/components/Filter/PriceSortToggle.vue +36 -43
  35. package/src/components/Table/Header.vue +1 -1
  36. package/src/components/Table/Table.vue +61 -12
  37. package/src/components/Tasks/CheckStock.vue +7 -16
  38. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  39. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  40. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  41. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  42. package/src/components/Tasks/EventDetailRow.vue +21 -0
  43. package/src/components/Tasks/MassEdit.vue +6 -16
  44. package/src/components/Tasks/QuickSettings.vue +140 -216
  45. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  46. package/src/components/Tasks/Stats.vue +19 -38
  47. package/src/components/Tasks/Task.vue +65 -268
  48. package/src/components/Tasks/TaskLabel.vue +9 -3
  49. package/src/components/Tasks/TaskView.vue +43 -63
  50. package/src/components/Tasks/Utilities.vue +10 -42
  51. package/src/components/Tasks/ViewTask.vue +23 -107
  52. package/src/components/icons/Close.vue +2 -8
  53. package/src/components/icons/Gear.vue +8 -8
  54. package/src/components/icons/Hash.vue +5 -0
  55. package/src/components/icons/Key.vue +2 -8
  56. package/src/components/icons/Pencil.vue +2 -8
  57. package/src/components/icons/Profile.vue +2 -8
  58. package/src/components/icons/Sell.vue +2 -8
  59. package/src/components/icons/Spinner.vue +4 -7
  60. package/src/components/icons/SquareCheck.vue +2 -8
  61. package/src/components/icons/SquareUncheck.vue +2 -8
  62. package/src/components/icons/Wildcard.vue +2 -8
  63. package/src/components/icons/index.js +3 -1
  64. package/src/components/ui/ActionButtonGroup.vue +113 -52
  65. package/src/components/ui/BalanceIndicator.vue +60 -0
  66. package/src/components/ui/EmptyState.vue +24 -0
  67. package/src/components/ui/EnableDisableToggle.vue +23 -0
  68. package/src/components/ui/FormField.vue +48 -48
  69. package/src/components/ui/IconLabel.vue +23 -0
  70. package/src/components/ui/InfoRow.vue +21 -54
  71. package/src/components/ui/Modal.vue +78 -37
  72. package/src/components/ui/Navbar.vue +60 -41
  73. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  74. package/src/components/ui/ReconnectIndicator.vue +111 -124
  75. package/src/components/ui/SectionCard.vue +6 -14
  76. package/src/components/ui/Splash.vue +2 -10
  77. package/src/components/ui/StatusBadge.vue +26 -28
  78. package/src/components/ui/TaskToggle.vue +54 -0
  79. package/src/components/ui/controls/CountryChooser.vue +27 -64
  80. package/src/components/ui/controls/EyeToggle.vue +1 -1
  81. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  82. package/src/components/ui/controls/atomic/Dropdown.vue +102 -95
  83. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -94
  84. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  85. package/src/composables/useColorMapping.js +15 -0
  86. package/src/composables/useCopyToClipboard.js +1 -1
  87. package/src/composables/useDateFormatting.js +21 -0
  88. package/src/composables/useDeviceDetection.js +14 -0
  89. package/src/composables/useDropdownPosition.js +5 -6
  90. package/src/composables/useDynamicTableHeight.js +31 -0
  91. package/src/composables/useRowSelection.js +0 -3
  92. package/src/composables/useTicketPricing.js +16 -0
  93. package/src/composables/useWindowDimensions.js +21 -0
  94. package/src/libs/Filter.js +14 -20
  95. package/src/libs/panzoom.js +1 -5
  96. package/src/libs/utils/array.js +60 -0
  97. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  98. package/src/libs/utils/eventUrl.js +40 -0
  99. package/src/libs/utils/string.js +28 -0
  100. package/src/libs/utils/time.js +20 -0
  101. package/src/libs/utils/validation.js +88 -0
  102. package/src/main.js +0 -2
  103. package/src/stores/connection.js +1 -4
  104. package/src/stores/logger.js +6 -12
  105. package/src/stores/sampleData.js +1 -2
  106. package/src/stores/ui.js +59 -36
  107. package/src/views/Accounts.vue +17 -31
  108. package/src/views/Console.vue +76 -176
  109. package/src/views/Editor.vue +217 -383
  110. package/src/views/FilterBuilder.vue +190 -373
  111. package/src/views/Login.vue +3 -28
  112. package/src/views/Profiles.vue +12 -22
  113. package/src/views/Tasks.vue +51 -38
  114. package/tailwind.config.js +82 -71
  115. package/workbox-config.cjs +47 -5
  116. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2416
  117. package/exit +0 -209
  118. package/run +0 -177
  119. package/switch-branch.sh +0 -41
  120. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -1,9 +1,11 @@
1
1
  <template>
2
- <div class="console-page">
3
- <h4 class="mb-2 flex items-center gap-2 pt-5 text-sm font-bold text-white lg:mt-1">
4
- Console
5
- <ConsoleIcon />
6
- </h4>
2
+ <div class="mb-8 pb-16 md:mb-0 md:pb-0 mobile-portrait:mb-12 mobile-portrait:pb-24">
3
+ <div class="page-header" style="padding-bottom: 0.75rem;">
4
+ <div class="page-header-card">
5
+ <ConsoleIcon />
6
+ <h4>Console</h4>
7
+ </div>
8
+ </div>
7
9
 
8
10
  <div>
9
11
  <div class="mb-3 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
@@ -28,21 +30,23 @@
28
30
  v-model="searchQuery"
29
31
  type="text"
30
32
  placeholder="Search logs..."
31
- class="h-full w-full bg-transparent text-sm text-white outline-none" />
33
+ aria-label="Search logs"
34
+ class="transparent-input" />
32
35
  <span v-if="searchQuery" class="ml-2 text-xs text-light-500">{{ filteredCount }}</span>
33
36
  </div>
34
37
  <!-- Scroll buttons on mobile - inline with search -->
35
38
  <button
36
- class="console-scroll-btn flex h-10 w-10 items-center justify-center rounded border-2 bg-dark-400 shadow-sm md:hidden"
39
+ class="console-scroll-btn md:hidden"
37
40
  @mousedown="startScrolling('up')"
38
41
  @mouseup="stopScrolling"
39
42
  @mouseleave="stopScrolling"
40
43
  @touchstart="startScrolling('up')"
41
- @touchend="stopScrolling">
44
+ @touchend="stopScrolling"
45
+ aria-label="Scroll up">
42
46
  <UpIcon class="pointer-events-none h-5 w-5" />
43
47
  </button>
44
48
  <button
45
- class="console-scroll-btn flex h-10 w-10 items-center justify-center rounded border-2 bg-dark-400 shadow-sm md:hidden"
49
+ class="console-scroll-btn md:hidden"
46
50
  @mousedown="startScrolling('down')"
47
51
  @mouseup="stopScrolling"
48
52
  @mouseleave="stopScrolling"
@@ -94,16 +98,15 @@
94
98
 
95
99
  <Smoothie
96
100
  :weight="0.2"
97
- class="console scrollable smooth-scroll overflow-x-auto overflow-y-auto font-mono text-white"
98
- style="min-height: 12rem !important"
101
+ class="console-main"
99
102
  ref="$autoscroll"
100
103
  @wheel.stop
101
104
  @touchmove.stop
102
105
  @scroll="handleScroll">
103
106
  <div
104
107
  v-if="displayedLogs.length === 0"
105
- class="empty-state flex h-full flex-col items-center justify-center text-center">
106
- <ConsoleIcon class="mb-3 h-12 w-12 text-dark-400 opacity-50" />
108
+ class="flex h-full min-h-56 flex-col items-center justify-center text-center font-sans">
109
+ <ConsoleIcon class="empty-state-icon" />
107
110
  <p class="text-sm text-light-400">
108
111
  {{ searchQuery ? "No logs match your search" : "No logs yet" }}
109
112
  </p>
@@ -113,12 +116,12 @@
113
116
  </div>
114
117
  <pre
115
118
  v-else
116
- class="hidden-scrollbars log-entry"
119
+ class="hidden-scrollbars log-entry opacity-0 transition-all duration-150 hover:bg-white/2"
117
120
  v-for="(line, index) in displayedLogs"
118
121
  v-bind:key="`log-${index}`"
119
- :style="{ '--index': index }"><code class="md:text-sm lg:text-base" v-html="line"></code></pre>
122
+ :style="{ '--index': index }"><code class="md:text-sm lg:text-base mobile-portrait:text-xs+ mobile-portrait:leading-tight" v-html="line"></code></pre>
120
123
  </Smoothie>
121
- <div class="console-switches mb-6 mt-4 flex justify-between md:hidden">
124
+ <div class="mb-6 mt-4 flex justify-between md:hidden mobile-portrait:mb-16 mobile-portrait:mt-6">
122
125
  <button
123
126
  class="flex h-10 items-center justify-center gap-3 rounded border border-dark-650 bg-dark-400 px-2 shadow-sm">
124
127
  <h3 class="text-sm text-white">Hide Monitors</h3>
@@ -138,156 +141,57 @@
138
141
  </div>
139
142
  </template>
140
143
  <style lang="scss" scoped>
141
- .console-page {
142
- @apply pb-16 mb-8 md:pb-0 md:mb-0;
143
-
144
- @media (max-width: 480px) and (orientation: portrait) {
145
- @apply pb-24 mb-12;
146
- }
147
- }
148
-
149
- .console-switches {
150
- @media (max-width: 480px) and (orientation: portrait) {
151
- @apply mt-6 mb-16;
152
- }
153
- }
154
-
144
+ /* Webkit scrollbar customization (cannot be done with Tailwind utilities) */
155
145
  .console {
156
- @apply relative rounded border-2 border-dark-550 bg-dark-400 p-2 lg:p-5;
157
- @apply touch-pan-x touch-pan-y;
158
- height: calc(100vh - 18rem);
159
146
  scrollbar-width: thin;
160
- scrollbar-color: oklch(0.35 0 0) oklch(0.19 0 0);
147
+ scrollbar-color: oklch(0.35 0 0) oklch(0.1822 0 0);
161
148
  -webkit-overflow-scrolling: touch;
162
149
 
163
- // Use fixed height on mobile portrait to ensure switches are visible
164
- @media (max-width: 768px) {
165
- max-height: 60vh;
166
- height: auto;
167
- }
168
-
169
- @media (max-width: 480px) and (orientation: portrait) {
170
- max-height: 50vh;
171
- height: auto;
172
- }
173
-
174
- @media (min-width: 769px) and (max-width: 1023px) {
175
- height: calc(100vh - 16rem);
176
- }
177
-
178
- @media (min-width: 1024px) {
179
- height: calc(100vh - 14rem);
180
- }
181
-
182
150
  &::-webkit-scrollbar {
183
- width: 8px;
151
+ @apply w-2;
184
152
  }
185
153
 
186
154
  &::-webkit-scrollbar-track {
187
- background: oklch(0.19 0 0);
188
- border-radius: 4px;
155
+ @apply rounded bg-dark-300;
189
156
  }
190
157
 
191
158
  &::-webkit-scrollbar-thumb {
159
+ @apply rounded transition-colors duration-200;
192
160
  background: oklch(0.35 0 0);
193
- border-radius: 4px;
194
- transition: background-color 0.2s ease;
195
161
  }
196
162
 
197
163
  &::-webkit-scrollbar-thumb:hover {
198
164
  background: oklch(0.45 0 0);
199
165
  }
200
166
 
201
- // Smooth scrolling behavior with momentum
202
167
  &.smooth-scroll {
203
- scroll-behavior: smooth;
168
+ @apply scroll-smooth;
204
169
  scroll-padding: 0.5rem;
205
170
  -webkit-overflow-scrolling: touch;
206
171
  overscroll-behavior: contain;
207
172
  }
208
-
209
- // Improved log entry animations
210
- .log-entry {
211
- opacity: 0;
212
- transform: translateY(4px);
213
- animation: slideInLog 0.2s ease-out forwards;
214
- transition: all 0.15s ease;
215
-
216
- &:hover {
217
- background-color: rgba(255, 255, 255, 0.02);
218
- transform: translateX(2px);
219
- }
220
- }
221
-
222
- // Stagger animation for new logs
223
- .log-entry:last-child {
224
- animation-delay: 0.05s;
225
- }
226
-
227
- @keyframes slideInLog {
228
- to {
229
- opacity: 1;
230
- transform: translateY(0);
231
- }
232
- }
233
-
234
- // Empty state styling
235
- .empty-state {
236
- min-height: 14rem;
237
- font-family:
238
- "Inter",
239
- -apple-system,
240
- BlinkMacSystemFont,
241
- "Segoe UI",
242
- Helvetica,
243
- Arial,
244
- sans-serif;
245
- }
246
-
247
- textarea {
248
- background: transparent;
249
- resize: none;
250
- @apply w-full text-white focus:outline-none;
251
- }
252
173
  }
253
174
 
254
- .console-scroll-btn {
255
- border-color: oklch(0.2809 0 0);
256
- transition: all 0.15s ease;
175
+ /* Animation for log entries (keyframes cannot be done with Tailwind) */
176
+ .log-entry {
177
+ transform: translateY(4px);
178
+ animation: slideInLog 0.2s ease-out forwards;
257
179
 
258
- &:hover,
259
- &:active {
260
- border-color: oklch(0.72 0.15 145) !important;
261
- outline: 1px solid oklch(0.72 0.15 145);
262
- outline-offset: 0;
180
+ &:hover {
181
+ transform: translateX(2px);
263
182
  }
264
183
  }
265
184
 
266
- /* Mobile portrait console optimizations */
267
- @screen mobile-portrait {
268
- .console {
269
- height: calc(100vh - 19.5rem);
270
- @apply p-1 text-xs;
271
- overflow: auto;
272
-
273
- pre {
274
- line-height: 1.2;
275
- }
276
-
277
- code {
278
- font-size: 0.7rem !important;
279
- }
280
- }
185
+ .log-entry:last-child {
186
+ animation-delay: 0.05s;
281
187
  }
282
188
 
283
- /* Mobile landscape console optimizations */
284
- @media (max-width: 1024px) and (orientation: landscape) {
285
- .console {
286
- height: calc(100vh - 10.5rem);
189
+ @keyframes slideInLog {
190
+ to {
191
+ @apply opacity-100;
192
+ transform: translateY(0);
287
193
  }
288
194
  }
289
-
290
- /* Console-specific styles handled by utilities */
291
195
  </style>
292
196
  <script setup>
293
197
  import { Smoothie } from "vue-smoothie";
@@ -299,11 +203,6 @@ import Switch from "@/components/ui/controls/atomic/Switch.vue";
299
203
  import WebsocketHeartbeatJs from "websocket-heartbeat-js";
300
204
  import { onMounted, onUnmounted, ref, nextTick, computed, watch } from "vue";
301
205
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
302
- import { sortAlphaNum } from "@/stores/utils";
303
-
304
- import { useUIStore } from "@/stores/ui";
305
-
306
- const ui = useUIStore();
307
206
 
308
207
  const $autoscroll = ref(null);
309
208
  const logLines = ref([]);
@@ -313,12 +212,13 @@ const taskLogMapping = ref({});
313
212
  const currentTaskLog = ref("");
314
213
  const filteredLogs = ref(true);
315
214
  const userScrolledUp = ref(false);
316
- const lastScrollTime = ref(0);
317
215
  const scrollInterval = ref(null);
318
216
  const isScrolling = ref(false);
319
217
  const searchQuery = ref("");
320
218
 
321
- // Computed filtered logs based on search query
219
+ // Optimized: Cache stripped versions to avoid regex on every filter
220
+ const logPlainTextCache = new Map();
221
+
322
222
  const displayedLogs = computed(() => {
323
223
  let logs =
324
224
  currentTaskLog.value && currentTaskLog.value !== ""
@@ -328,8 +228,12 @@ const displayedLogs = computed(() => {
328
228
  if (searchQuery.value.trim()) {
329
229
  const query = searchQuery.value.toLowerCase();
330
230
  logs = logs.filter((log) => {
331
- // Remove HTML tags for search
332
- const plainText = log.replace(/<[^>]*>/g, "").toLowerCase();
231
+ // Use cached plain text or compute and cache it
232
+ let plainText = logPlainTextCache.get(log);
233
+ if (plainText === undefined) {
234
+ plainText = log.replace(/<[^>]*>/g, "").toLowerCase();
235
+ logPlainTextCache.set(log, plainText);
236
+ }
333
237
  return plainText.includes(query);
334
238
  });
335
239
  }
@@ -344,28 +248,29 @@ const filteredCount = computed(() => {
344
248
 
345
249
  const path = "/api/updates?type=console";
346
250
  const url = (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + path;
347
- // Handle manual scroll detection
251
+
252
+ // Scroll thresholds
253
+ const SCROLL_THRESHOLD = 50; // pixels from bottom to consider "near bottom"
254
+ const SCROLL_AMOUNT = 100; // pixels to scroll per interval
255
+
348
256
  const handleScroll = (event) => {
349
257
  if (!autoscrollToggled.value) return;
350
258
 
351
259
  const element = event.target;
352
260
  if (!element) return;
353
261
 
354
- // Check if user is near bottom (within 50px)
355
- const threshold = 50;
356
- const isNearBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - threshold;
262
+ // Check if user is near bottom
263
+ const isNearBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - SCROLL_THRESHOLD;
357
264
 
358
- // Only update if there's an actual change
359
265
  if (userScrolledUp.value === isNearBottom) {
360
266
  userScrolledUp.value = !isNearBottom;
361
267
  }
362
268
  };
363
269
 
364
- // Bulletproof scroll function with multiple fallbacks
365
270
  const performScroll = (direction, smooth = true) => {
366
271
  try {
367
272
  if (!$autoscroll.value?.el) {
368
- if (DEBUG) console.log("Autoscroll element not ready");
273
+ if (DEBUG) return false;
369
274
  return false;
370
275
  }
371
276
 
@@ -389,7 +294,7 @@ const performScroll = (direction, smooth = true) => {
389
294
 
390
295
  return true;
391
296
  } catch (e) {
392
- if (DEBUG) console.log("Error scrolling", e);
297
+ if (DEBUG) return false;
393
298
  return false;
394
299
  }
395
300
  };
@@ -399,64 +304,54 @@ const startScrolling = (direction) => {
399
304
  if (isScrolling.value) return;
400
305
 
401
306
  isScrolling.value = true;
402
-
403
- // Immediate scroll
404
307
  performScroll(direction, true);
405
308
 
406
- // Continue scrolling while held (for long content)
407
- scrollInterval.value = setInterval(() => {
309
+ // Optimized: Use requestAnimationFrame instead of setInterval for smoother scrolling
310
+ const continuousScroll = () => {
408
311
  if (!isScrolling.value) return;
409
312
 
410
313
  const element = $autoscroll.value?.el;
411
314
  if (!element) return;
412
315
 
413
- const scrollAmount = 100;
414
316
  if (direction === "up") {
415
- element.scrollTop = Math.max(0, element.scrollTop - scrollAmount);
317
+ element.scrollTop = Math.max(0, element.scrollTop - SCROLL_AMOUNT);
416
318
  } else {
417
- element.scrollTop = Math.min(element.scrollHeight - element.clientHeight, element.scrollTop + scrollAmount);
319
+ element.scrollTop = Math.min(element.scrollHeight - element.clientHeight, element.scrollTop + SCROLL_AMOUNT);
418
320
  }
419
- }, 50);
321
+
322
+ scrollInterval.value = requestAnimationFrame(continuousScroll);
323
+ };
324
+
325
+ scrollInterval.value = requestAnimationFrame(continuousScroll);
420
326
  };
421
327
 
422
328
  const stopScrolling = () => {
423
329
  isScrolling.value = false;
424
330
  if (scrollInterval.value) {
425
- clearInterval(scrollInterval.value);
331
+ cancelAnimationFrame(scrollInterval.value);
426
332
  scrollInterval.value = null;
427
333
  }
428
334
  };
429
335
 
430
- // Legacy function for compatibility
431
- const scrollTo = (dir) => {
432
- performScroll(dir, true);
433
- };
434
-
435
- // Simple autoscroll to bottom
436
336
  const autoScrollToBottom = () => {
437
337
  if (!$autoscroll.value?.el || !autoscrollToggled.value) return;
438
338
 
439
339
  // Only scroll if user hasn't manually scrolled up
440
340
  if (!userScrolledUp.value) {
441
341
  const element = $autoscroll.value.el;
442
-
443
- // Calculate the target scroll position to show the last log
444
342
  const targetScrollTop = element.scrollHeight - element.clientHeight;
445
343
 
446
- // Use smooth scrolling to ensure new logs are visible
447
344
  if (element.scrollTo && Math.abs(element.scrollTop - targetScrollTop) > 5) {
448
345
  element.scrollTo({
449
346
  top: targetScrollTop,
450
347
  behavior: "smooth"
451
348
  });
452
349
  } else {
453
- // For small differences or fallback, use instant scroll
454
350
  element.scrollTop = targetScrollTop;
455
351
  }
456
352
  }
457
353
  };
458
354
 
459
- // Handle autoscroll toggle
460
355
  const onAutoscrollToggle = () => {
461
356
  if (autoscrollToggled.value) {
462
357
  userScrolledUp.value = false;
@@ -468,6 +363,10 @@ const addAnsiToOutput = (a) => {
468
363
  const html = ansii.toHtml(a?.log || a);
469
364
  logLines.value.push(html);
470
365
 
366
+ // Optimized: Pre-cache the plain text version when adding logs
367
+ const plainText = html.replace(/<[^>]*>/g, "").toLowerCase();
368
+ logPlainTextCache.set(html, plainText);
369
+
471
370
  // Auto scroll after adding new content with proper timing
472
371
  nextTick().then(() => {
473
372
  // Use a small delay to ensure DOM is fully updated
@@ -495,13 +394,17 @@ const handleWebsocketMessages = (msg) => {
495
394
  const makeTaskLogMapping = (lines) => {
496
395
  lines.forEach((l) => {
497
396
  if (!l.metadata) {
498
- if (DEBUG) console.log("Error getting metadata", l);
499
397
  return;
500
398
  }
501
399
  const region = l.metadata.siteId?.split("_")?.[1];
502
400
  const n = l.metadata.global ? "Global" : `${region}-${l.metadata.taskId}`;
503
401
  if (!taskLogMapping.value[n]) taskLogMapping.value[n] = [];
504
- taskLogMapping.value[n].push(ansii.toHtml(l.log));
402
+ const html = ansii.toHtml(l.log);
403
+ taskLogMapping.value[n].push(html);
404
+
405
+ // Optimized: Pre-cache plain text for task log mapping too
406
+ const plainText = html.replace(/<[^>]*>/g, "").toLowerCase();
407
+ logPlainTextCache.set(html, plainText);
505
408
  });
506
409
  };
507
410
 
@@ -523,7 +426,6 @@ window.startDebugConsoleMessages = () => {
523
426
 
524
427
  if (DEBUG) window.startDebugConsoleMessages();
525
428
 
526
- // Watch for log filter changes and reset scroll state
527
429
  watch([currentTaskLog, filteredLogs], () => {
528
430
  userScrolledUp.value = false;
529
431
  nextTick().then(() => {
@@ -531,20 +433,18 @@ watch([currentTaskLog, filteredLogs], () => {
531
433
  });
532
434
  });
533
435
 
534
- // Listen for messages
535
436
  onMounted(() => {
536
437
  const socket = new WebsocketHeartbeatJs({ url, pingMsg: "ping" });
537
438
 
538
439
  socket.onmessage = (event) => {
539
440
  const msg = JSON.parse(event.data);
540
- if (DEBUG) console.log("Received message", msg);
441
+ ;
541
442
  msg.forEach((e) => {
542
443
  handleWebsocketMessages(e);
543
444
  });
544
445
  };
545
446
  });
546
447
 
547
- // Cleanup on unmount
548
448
  onUnmounted(() => {
549
449
  stopScrolling();
550
450
  });