@necrolab/dashboard 0.5.15 → 0.5.17

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