sidekiq_queue_manager 1.0.1 → 1.1.0

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.
@@ -62,7 +62,13 @@ class SidekiqQueueManagerUI {
62
62
  retryCount: 0,
63
63
  lastUpdate: null,
64
64
  livePullEnabled: false,
65
- currentTheme: this.getStoredTheme() || this.gemConfig.theme
65
+ currentTheme: this.getStoredTheme() || this.gemConfig.theme,
66
+ activeTab: 'queues',
67
+ pagination: {
68
+ scheduled: { page: 1, per_page: 25, filter: '' },
69
+ retries: { page: 1, per_page: 25, filter: '' },
70
+ dead: { page: 1, per_page: 25, filter: '' }
71
+ }
66
72
  };
67
73
 
68
74
  this.currentMenuCloseHandler = null;
@@ -81,6 +87,7 @@ class SidekiqQueueManagerUI {
81
87
  this.initializeTheme();
82
88
  this.cacheElements();
83
89
  this.setupEventListeners();
90
+ this.setupTabSystem();
84
91
  this.initializeRefreshControl();
85
92
  this.loadInitialData();
86
93
  this.injectActionsMenuStyles();
@@ -215,10 +222,19 @@ class SidekiqQueueManagerUI {
215
222
  failed: '#sqm-failed',
216
223
  busy: '#sqm-busy',
217
224
  enqueued: '#sqm-enqueued',
225
+ 'scheduled-jobs': '#sqm-scheduled-jobs',
226
+ 'retry-jobs': '#sqm-retry-jobs',
227
+ 'dead-jobs': '#sqm-dead-jobs',
218
228
  totalQueues: '#sqm-total-queues',
219
229
  pausedQueues: '#sqm-paused-queues',
220
230
  totalJobs: '#sqm-total-jobs',
221
231
 
232
+ // Tab counts
233
+ 'sqm-queues-count': '#sqm-queues-count',
234
+ 'sqm-scheduled-count': '#sqm-scheduled-count',
235
+ 'sqm-retries-count': '#sqm-retries-count',
236
+ 'sqm-dead-count': '#sqm-dead-count',
237
+
222
238
  // Table elements
223
239
  tableBody: '#sqm-table-body',
224
240
  table: '#sqm-queues-table',
@@ -263,6 +279,15 @@ class SidekiqQueueManagerUI {
263
279
  }
264
280
  }
265
281
 
282
+ addEventListener(element, event, handler) {
283
+ if (element) {
284
+ element.addEventListener(event, handler);
285
+ // Generate a unique key for tracking
286
+ const key = `dynamic_${Date.now()}_${Math.random()}`;
287
+ this.eventHandlers.set(key, { element, event, handler });
288
+ }
289
+ }
290
+
266
291
  initializeRefreshControl() {
267
292
  const liveToggle = this.elements.get('liveToggleBtn');
268
293
  const statusText = this.elements.get('statusText');
@@ -491,25 +516,65 @@ class SidekiqQueueManagerUI {
491
516
  this.updateQueuesTable(data.queues);
492
517
  }
493
518
 
519
+ // Update tab counts
520
+ this.updateTabCounts(data);
521
+
494
522
  this.updateTimestamp(data.timestamp);
495
523
  }
496
524
 
497
525
  updateGlobalStats(stats) {
498
- const elements = {
499
- processed: stats.processed?.toLocaleString() || '0',
500
- failed: stats.failed?.toLocaleString() || '0',
501
- busy: stats.busy?.toString() || '0',
502
- enqueued: stats.enqueued?.toLocaleString() || '0'
526
+ const statsData = {
527
+ processed: stats.processed || 0,
528
+ failed: stats.failed || 0,
529
+ busy: stats.busy || 0,
530
+ enqueued: stats.enqueued || 0,
531
+ 'scheduled-jobs': stats.scheduled_size || 0,
532
+ 'retry-jobs': stats.retry_size || 0,
533
+ 'dead-jobs': stats.dead_size || 0
503
534
  };
504
535
 
505
- Object.entries(elements).forEach(([key, value]) => {
536
+ Object.entries(statsData).forEach(([key, rawValue]) => {
506
537
  const element = this.elements.get(key);
507
538
  if (element) {
508
- element.textContent = value;
539
+ const formattedValue = this.formatLargeNumber(rawValue);
540
+ element.textContent = formattedValue;
541
+
542
+ // Add data-length attribute for responsive font sizing
543
+ const valueLength = formattedValue.replace(/,/g, '').length;
544
+ if (valueLength >= 15) {
545
+ element.setAttribute('data-length', 'long');
546
+ } else if (valueLength >= 8) {
547
+ element.setAttribute('data-length', valueLength.toString());
548
+ } else {
549
+ element.removeAttribute('data-length');
550
+ }
509
551
  }
510
552
  });
511
553
  }
512
554
 
555
+ // Format large numbers with appropriate abbreviations and locale formatting
556
+ formatLargeNumber(num) {
557
+ const number = parseInt(num) || 0;
558
+
559
+ // Handle zero and negative numbers
560
+ if (number === 0) return '0';
561
+ if (number < 0) return number.toLocaleString();
562
+
563
+ // For very large numbers, use abbreviations
564
+ if (number >= 1000000000000) { // Trillion
565
+ return (number / 1000000000000).toFixed(1).replace(/\.0$/, '') + 'T';
566
+ } else if (number >= 1000000000) { // Billion
567
+ return (number / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
568
+ } else if (number >= 1000000) { // Million
569
+ return (number / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
570
+ } else if (number >= 100000) { // For numbers 100k+, use abbreviation
571
+ return (number / 1000).toFixed(0) + 'K';
572
+ } else {
573
+ // For smaller numbers, use locale formatting with commas
574
+ return number.toLocaleString();
575
+ }
576
+ }
577
+
513
578
  updateQueuesTable(queues) {
514
579
  const tbody = this.elements.get('tableBody');
515
580
  if (!tbody) return;
@@ -519,9 +584,9 @@ class SidekiqQueueManagerUI {
519
584
  const pausedQueues = queueArray.filter(q => q.paused).length;
520
585
 
521
586
  // Update counters
522
- this.updateElement('totalQueues', totalQueues.toString());
523
- this.updateElement('pausedQueues', pausedQueues.toString());
524
- this.updateElement('totalJobs', queueArray.reduce((sum, q) => sum + (q.size || 0), 0).toLocaleString());
587
+ this.updateElement('totalQueues', totalQueues);
588
+ this.updateElement('pausedQueues', pausedQueues);
589
+ this.updateElement('totalJobs', queueArray.reduce((sum, q) => sum + (q.size || 0), 0));
525
590
 
526
591
  // Update table rows
527
592
  tbody.innerHTML = queueArray.map(queue => this.renderQueueRow(queue)).join('');
@@ -1791,7 +1856,30 @@ class SidekiqQueueManagerUI {
1791
1856
  updateElement(key, value) {
1792
1857
  const element = this.elements.get(key);
1793
1858
  if (element) {
1794
- element.textContent = value;
1859
+ // Format numbers if the element is a stat value
1860
+ if (element.classList.contains('sqm-stat-value') || element.classList.contains('sqm-tab-count')) {
1861
+ const numValue = parseInt(value);
1862
+ if (!isNaN(numValue)) {
1863
+ const formattedValue = this.formatLargeNumber(numValue);
1864
+ element.textContent = formattedValue;
1865
+
1866
+ // Add data-length attribute for responsive font sizing (only for stat values)
1867
+ if (element.classList.contains('sqm-stat-value')) {
1868
+ const valueLength = formattedValue.replace(/,/g, '').length;
1869
+ if (valueLength >= 15) {
1870
+ element.setAttribute('data-length', 'long');
1871
+ } else if (valueLength >= 8) {
1872
+ element.setAttribute('data-length', valueLength.toString());
1873
+ } else {
1874
+ element.removeAttribute('data-length');
1875
+ }
1876
+ }
1877
+ } else {
1878
+ element.textContent = value;
1879
+ }
1880
+ } else {
1881
+ element.textContent = value;
1882
+ }
1795
1883
  }
1796
1884
  }
1797
1885
 
@@ -1829,6 +1917,765 @@ class SidekiqQueueManagerUI {
1829
1917
  }
1830
1918
  }
1831
1919
 
1920
+ // ========================================
1921
+ // Tab Management System
1922
+ // ========================================
1923
+
1924
+ setupTabSystem() {
1925
+ const tabButtons = document.querySelectorAll('.sqm-tab');
1926
+ const tabPanels = document.querySelectorAll('.sqm-tab-panel');
1927
+
1928
+ tabButtons.forEach(button => {
1929
+ this.addEventListener(button, 'click', (e) => {
1930
+ e.preventDefault();
1931
+ const tabName = button.getAttribute('data-tab');
1932
+ this.switchTab(tabName);
1933
+ });
1934
+ });
1935
+
1936
+ // Setup filter and action buttons for each tab
1937
+ this.setupScheduledJobHandlers();
1938
+ this.setupRetryJobHandlers();
1939
+ this.setupDeadJobHandlers();
1940
+ }
1941
+
1942
+ switchTab(tabName) {
1943
+ if (this.state.activeTab === tabName) return;
1944
+
1945
+ // Update active tab state
1946
+ this.state.activeTab = tabName;
1947
+
1948
+ // Update tab button states
1949
+ document.querySelectorAll('.sqm-tab').forEach(button => {
1950
+ const isActive = button.getAttribute('data-tab') === tabName;
1951
+ button.classList.toggle('sqm-tab-active', isActive);
1952
+ button.setAttribute('aria-selected', isActive);
1953
+ });
1954
+
1955
+ // Update panel visibility
1956
+ document.querySelectorAll('.sqm-tab-panel').forEach(panel => {
1957
+ const panelTab = panel.id.replace('sqm-', '').replace('-panel', '');
1958
+ const isActive = panelTab === tabName;
1959
+ panel.classList.toggle('sqm-hidden', !isActive);
1960
+ panel.classList.toggle('sqm-tab-panel-active', isActive);
1961
+ });
1962
+
1963
+ // Load data for the active tab
1964
+ this.loadTabData(tabName);
1965
+ }
1966
+
1967
+ loadTabData(tabName) {
1968
+ switch (tabName) {
1969
+ case 'queues':
1970
+ this.refreshQueues();
1971
+ break;
1972
+ case 'scheduled':
1973
+ this.loadScheduledJobs();
1974
+ break;
1975
+ case 'retries':
1976
+ this.loadRetryJobs();
1977
+ break;
1978
+ case 'dead':
1979
+ this.loadDeadJobs();
1980
+ break;
1981
+ }
1982
+ }
1983
+
1984
+ updateTabCounts(data) {
1985
+ // Update tab counts from data
1986
+ if (data.queues) {
1987
+ const queuesCount = Object.keys(data.queues).length;
1988
+ this.updateElement('sqm-queues-count', queuesCount);
1989
+ }
1990
+
1991
+ if (data.global_stats) {
1992
+ const scheduledCount = data.global_stats.scheduled_size || 0;
1993
+ const retriesCount = data.global_stats.retry_size || 0;
1994
+ const deadCount = data.global_stats.dead_size || 0;
1995
+
1996
+ this.updateElement('sqm-scheduled-count', scheduledCount);
1997
+ this.updateElement('sqm-retries-count', retriesCount);
1998
+ this.updateElement('sqm-dead-count', deadCount);
1999
+ }
2000
+ }
2001
+
2002
+ // ========================================
2003
+ // Scheduled Jobs Management
2004
+ // ========================================
2005
+
2006
+ setupScheduledJobHandlers() {
2007
+ // Filter button
2008
+ const filterBtn = document.getElementById('sqm-scheduled-apply-filter');
2009
+ const filterInput = document.getElementById('sqm-scheduled-filter');
2010
+ const clearBtn = document.getElementById('sqm-clear-scheduled-btn');
2011
+
2012
+ if (filterBtn && filterInput) {
2013
+ this.addEventListener(filterBtn, 'click', () => {
2014
+ this.state.pagination.scheduled.filter = filterInput.value;
2015
+ this.state.pagination.scheduled.page = 1;
2016
+ this.loadScheduledJobs();
2017
+ });
2018
+
2019
+ this.addEventListener(filterInput, 'keypress', (e) => {
2020
+ if (e.key === 'Enter') {
2021
+ this.state.pagination.scheduled.filter = filterInput.value;
2022
+ this.state.pagination.scheduled.page = 1;
2023
+ this.loadScheduledJobs();
2024
+ }
2025
+ });
2026
+ }
2027
+
2028
+ if (clearBtn) {
2029
+ this.addEventListener(clearBtn, 'click', async () => {
2030
+ await this.showConfirmation('Are you sure you want to clear all scheduled jobs?', () => {
2031
+ this.clearScheduledJobs();
2032
+ });
2033
+ });
2034
+ }
2035
+ }
2036
+
2037
+ async loadScheduledJobs() {
2038
+ try {
2039
+ const params = new URLSearchParams(this.state.pagination.scheduled);
2040
+ const response = await fetch(`${this.gemConfig.mountPath}/scheduled?${params}`);
2041
+ const result = await response.json();
2042
+
2043
+ if (result.success) {
2044
+ this.renderScheduledJobs(result.data);
2045
+ this.renderPagination('scheduled', result.data.pagination);
2046
+ // Update tab count
2047
+ this.updateElement('sqm-scheduled-count', result.data.total_count || 0);
2048
+ } else {
2049
+ this.showNotification(result.message, 'error');
2050
+ }
2051
+ } catch (error) {
2052
+ this.showNotification('Failed to load scheduled jobs', 'error');
2053
+ }
2054
+ }
2055
+
2056
+ renderScheduledJobs(data) {
2057
+ const tbody = document.getElementById('sqm-scheduled-table-body');
2058
+ if (!tbody) return;
2059
+
2060
+ if (!data.jobs || data.jobs.length === 0) {
2061
+ tbody.innerHTML = `
2062
+ <tr>
2063
+ <td colspan="5" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
2064
+ No scheduled jobs found
2065
+ </td>
2066
+ </tr>
2067
+ `;
2068
+ return;
2069
+ }
2070
+
2071
+ tbody.innerHTML = data.jobs.map(job => `
2072
+ <tr>
2073
+ <td>
2074
+ <div class="sqm-job-class">${job.class}</div>
2075
+ <div class="sqm-job-args" title="${JSON.stringify(job.args)}">${JSON.stringify(job.args)}</div>
2076
+ </td>
2077
+ <td>${job.queue}</td>
2078
+ <td style="text-align: right;">
2079
+ <div>${job.scheduled_at}</div>
2080
+ <div class="sqm-time-relative">${job.time_until_execution}</div>
2081
+ </td>
2082
+ <td style="text-align: right;">
2083
+ <span class="sqm-time-relative">${job.time_until_execution}</span>
2084
+ </td>
2085
+ <td style="text-align: right;">
2086
+ <div class="sqm-action-buttons">
2087
+ <button class="sqm-btn sqm-btn-sm sqm-btn-enqueue"
2088
+ onclick="window.sidekiqQueueManager.enqueueScheduledJob('${job.jid}')"
2089
+ title="Enqueue now">
2090
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2091
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
2092
+ </svg>
2093
+ </button>
2094
+ <button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
2095
+ onclick="window.sidekiqQueueManager.deleteScheduledJob('${job.jid}')"
2096
+ title="Delete">
2097
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2098
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
2099
+ </svg>
2100
+ </button>
2101
+ </div>
2102
+ </td>
2103
+ </tr>
2104
+ `).join('');
2105
+ }
2106
+
2107
+ async enqueueScheduledJob(jobId) {
2108
+ try {
2109
+ const response = await fetch(`${this.gemConfig.mountPath}/scheduled/${jobId}/enqueue`, {
2110
+ method: 'POST',
2111
+ headers: { 'Content-Type': 'application/json' }
2112
+ });
2113
+ const result = await response.json();
2114
+
2115
+ if (result.success) {
2116
+ this.showNotification('Job enqueued successfully', 'success');
2117
+ this.loadScheduledJobs();
2118
+ } else {
2119
+ this.showNotification(result.message, 'error');
2120
+ }
2121
+ } catch (error) {
2122
+ this.showNotification('Failed to enqueue job', 'error');
2123
+ }
2124
+ }
2125
+
2126
+ async deleteScheduledJob(jobId) {
2127
+ try {
2128
+ const response = await fetch(`${this.gemConfig.mountPath}/scheduled/${jobId}`, {
2129
+ method: 'DELETE',
2130
+ headers: { 'Content-Type': 'application/json' }
2131
+ });
2132
+ const result = await response.json();
2133
+
2134
+ if (result.success) {
2135
+ this.showNotification('Job deleted successfully', 'success');
2136
+ this.loadScheduledJobs();
2137
+ } else {
2138
+ this.showNotification(result.message, 'error');
2139
+ }
2140
+ } catch (error) {
2141
+ this.showNotification('Failed to delete job', 'error');
2142
+ }
2143
+ }
2144
+
2145
+ async clearScheduledJobs() {
2146
+ try {
2147
+ const filter = this.state.pagination.scheduled.filter;
2148
+ const body = filter ? JSON.stringify({ filter }) : null;
2149
+
2150
+ const response = await fetch(`${this.gemConfig.mountPath}/scheduled/clear`, {
2151
+ method: 'POST',
2152
+ headers: { 'Content-Type': 'application/json' },
2153
+ body
2154
+ });
2155
+ const result = await response.json();
2156
+
2157
+ if (result.success) {
2158
+ this.showNotification(result.message, 'success');
2159
+ this.loadScheduledJobs();
2160
+ } else {
2161
+ this.showNotification(result.message, 'error');
2162
+ }
2163
+ } catch (error) {
2164
+ this.showNotification('Failed to clear scheduled jobs', 'error');
2165
+ }
2166
+ }
2167
+
2168
+ // ========================================
2169
+ // Retry Jobs Management
2170
+ // ========================================
2171
+
2172
+ setupRetryJobHandlers() {
2173
+ // Filter button
2174
+ const filterBtn = document.getElementById('sqm-retries-apply-filter');
2175
+ const filterInput = document.getElementById('sqm-retries-filter');
2176
+ const retryAllBtn = document.getElementById('sqm-retry-all-btn');
2177
+ const clearBtn = document.getElementById('sqm-clear-retries-btn');
2178
+
2179
+ if (filterBtn && filterInput) {
2180
+ this.addEventListener(filterBtn, 'click', () => {
2181
+ this.state.pagination.retries.filter = filterInput.value;
2182
+ this.state.pagination.retries.page = 1;
2183
+ this.loadRetryJobs();
2184
+ });
2185
+
2186
+ this.addEventListener(filterInput, 'keypress', (e) => {
2187
+ if (e.key === 'Enter') {
2188
+ this.state.pagination.retries.filter = filterInput.value;
2189
+ this.state.pagination.retries.page = 1;
2190
+ this.loadRetryJobs();
2191
+ }
2192
+ });
2193
+ }
2194
+
2195
+ if (retryAllBtn) {
2196
+ this.addEventListener(retryAllBtn, 'click', async () => {
2197
+ await this.showConfirmation('Are you sure you want to retry all jobs?', () => {
2198
+ this.retryAllJobs();
2199
+ });
2200
+ });
2201
+ }
2202
+
2203
+ if (clearBtn) {
2204
+ this.addEventListener(clearBtn, 'click', async () => {
2205
+ await this.showConfirmation('Are you sure you want to clear all retry jobs?', () => {
2206
+ this.clearRetryJobs();
2207
+ });
2208
+ });
2209
+ }
2210
+ }
2211
+
2212
+ async loadRetryJobs() {
2213
+ try {
2214
+ const params = new URLSearchParams(this.state.pagination.retries);
2215
+ const response = await fetch(`${this.gemConfig.mountPath}/retries?${params}`);
2216
+ const result = await response.json();
2217
+
2218
+ if (result.success) {
2219
+ this.renderRetryJobs(result.data);
2220
+ this.renderPagination('retries', result.data.pagination);
2221
+ // Update tab count
2222
+ this.updateElement('sqm-retries-count', result.data.total_count || 0);
2223
+ } else {
2224
+ this.showNotification(result.message, 'error');
2225
+ }
2226
+ } catch (error) {
2227
+ this.showNotification('Failed to load retry jobs', 'error');
2228
+ }
2229
+ }
2230
+
2231
+ renderRetryJobs(data) {
2232
+ const tbody = document.getElementById('sqm-retries-table-body');
2233
+ if (!tbody) return;
2234
+
2235
+ if (!data.jobs || data.jobs.length === 0) {
2236
+ tbody.innerHTML = `
2237
+ <tr>
2238
+ <td colspan="6" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
2239
+ No retry jobs found
2240
+ </td>
2241
+ </tr>
2242
+ `;
2243
+ return;
2244
+ }
2245
+
2246
+ tbody.innerHTML = data.jobs.map(job => `
2247
+ <tr>
2248
+ <td>
2249
+ <div class="sqm-job-class">${job.class}</div>
2250
+ <div class="sqm-error-preview" title="${job.error_message || 'No error message'}">${job.error_class || 'Unknown Error'}</div>
2251
+ </td>
2252
+ <td>${job.queue}</td>
2253
+ <td style="text-align: right;">
2254
+ <div>${job.failed_at || 'Unknown'}</div>
2255
+ <div class="sqm-time-relative">${job.failed_at_relative || ''}</div>
2256
+ </td>
2257
+ <td style="text-align: right;">
2258
+ <div>${job.retry_at || 'Now'}</div>
2259
+ <div class="sqm-time-relative">${job.next_retry_relative || ''}</div>
2260
+ </td>
2261
+ <td style="text-align: right;">
2262
+ <div class="sqm-retry-count">
2263
+ ${job.retry_count}/${job.retry_limit}
2264
+ <div class="sqm-retry-progress">
2265
+ <div class="sqm-retry-progress-bar" style="width: ${(job.retry_count / job.retry_limit) * 100}%"></div>
2266
+ </div>
2267
+ </div>
2268
+ </td>
2269
+ <td style="text-align: right;">
2270
+ <div class="sqm-action-buttons">
2271
+ <button class="sqm-btn sqm-btn-sm sqm-btn-success"
2272
+ onclick="window.sidekiqQueueManager.retryJobNow('${job.jid}')"
2273
+ title="Retry now">
2274
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2275
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
2276
+ </svg>
2277
+ </button>
2278
+ <button class="sqm-btn sqm-btn-sm sqm-btn-kill"
2279
+ onclick="window.sidekiqQueueManager.killRetryJob('${job.jid}')"
2280
+ title="Move to dead queue">
2281
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2282
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
2283
+ </svg>
2284
+ </button>
2285
+ <button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
2286
+ onclick="window.sidekiqQueueManager.deleteRetryJob('${job.jid}')"
2287
+ title="Delete">
2288
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2289
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
2290
+ </svg>
2291
+ </button>
2292
+ </div>
2293
+ </td>
2294
+ </tr>
2295
+ `).join('');
2296
+ }
2297
+
2298
+ async retryJobNow(jobId) {
2299
+ try {
2300
+ const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}/retry`, {
2301
+ method: 'POST',
2302
+ headers: { 'Content-Type': 'application/json' }
2303
+ });
2304
+ const result = await response.json();
2305
+
2306
+ if (result.success) {
2307
+ this.showNotification('Job retried successfully', 'success');
2308
+ this.loadRetryJobs();
2309
+ } else {
2310
+ this.showNotification(result.message, 'error');
2311
+ }
2312
+ } catch (error) {
2313
+ this.showNotification('Failed to retry job', 'error');
2314
+ }
2315
+ }
2316
+
2317
+ async killRetryJob(jobId) {
2318
+ try {
2319
+ const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}/kill`, {
2320
+ method: 'POST',
2321
+ headers: { 'Content-Type': 'application/json' }
2322
+ });
2323
+ const result = await response.json();
2324
+
2325
+ if (result.success) {
2326
+ this.showNotification('Job moved to dead queue', 'success');
2327
+ this.loadRetryJobs();
2328
+ } else {
2329
+ this.showNotification(result.message, 'error');
2330
+ }
2331
+ } catch (error) {
2332
+ this.showNotification('Failed to kill job', 'error');
2333
+ }
2334
+ }
2335
+
2336
+ async deleteRetryJob(jobId) {
2337
+ try {
2338
+ const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}`, {
2339
+ method: 'DELETE',
2340
+ headers: { 'Content-Type': 'application/json' }
2341
+ });
2342
+ const result = await response.json();
2343
+
2344
+ if (result.success) {
2345
+ this.showNotification('Job deleted successfully', 'success');
2346
+ this.loadRetryJobs();
2347
+ } else {
2348
+ this.showNotification(result.message, 'error');
2349
+ }
2350
+ } catch (error) {
2351
+ this.showNotification('Failed to delete job', 'error');
2352
+ }
2353
+ }
2354
+
2355
+ async retryAllJobs() {
2356
+ try {
2357
+ const filter = this.state.pagination.retries.filter;
2358
+ const body = filter ? JSON.stringify({ filter }) : null;
2359
+
2360
+ const response = await fetch(`${this.gemConfig.mountPath}/retries/retry_all`, {
2361
+ method: 'POST',
2362
+ headers: { 'Content-Type': 'application/json' },
2363
+ body
2364
+ });
2365
+ const result = await response.json();
2366
+
2367
+ if (result.success) {
2368
+ this.showNotification(result.message, 'success');
2369
+ this.loadRetryJobs();
2370
+ } else {
2371
+ this.showNotification(result.message, 'error');
2372
+ }
2373
+ } catch (error) {
2374
+ this.showNotification('Failed to retry all jobs', 'error');
2375
+ }
2376
+ }
2377
+
2378
+ async clearRetryJobs() {
2379
+ try {
2380
+ const filter = this.state.pagination.retries.filter;
2381
+ const body = filter ? JSON.stringify({ filter }) : null;
2382
+
2383
+ const response = await fetch(`${this.gemConfig.mountPath}/retries/clear`, {
2384
+ method: 'POST',
2385
+ headers: { 'Content-Type': 'application/json' },
2386
+ body
2387
+ });
2388
+ const result = await response.json();
2389
+
2390
+ if (result.success) {
2391
+ this.showNotification(result.message, 'success');
2392
+ this.loadRetryJobs();
2393
+ } else {
2394
+ this.showNotification(result.message, 'error');
2395
+ }
2396
+ } catch (error) {
2397
+ this.showNotification('Failed to clear retry jobs', 'error');
2398
+ }
2399
+ }
2400
+
2401
+ // ========================================
2402
+ // Dead Jobs Management
2403
+ // ========================================
2404
+
2405
+ setupDeadJobHandlers() {
2406
+ // Filter button
2407
+ const filterBtn = document.getElementById('sqm-dead-apply-filter');
2408
+ const filterInput = document.getElementById('sqm-dead-filter');
2409
+ const resurrectAllBtn = document.getElementById('sqm-resurrect-all-btn');
2410
+ const clearBtn = document.getElementById('sqm-clear-dead-btn');
2411
+
2412
+ if (filterBtn && filterInput) {
2413
+ this.addEventListener(filterBtn, 'click', () => {
2414
+ this.state.pagination.dead.filter = filterInput.value;
2415
+ this.state.pagination.dead.page = 1;
2416
+ this.loadDeadJobs();
2417
+ });
2418
+
2419
+ this.addEventListener(filterInput, 'keypress', (e) => {
2420
+ if (e.key === 'Enter') {
2421
+ this.state.pagination.dead.filter = filterInput.value;
2422
+ this.state.pagination.dead.page = 1;
2423
+ this.loadDeadJobs();
2424
+ }
2425
+ });
2426
+ }
2427
+
2428
+ if (resurrectAllBtn) {
2429
+ this.addEventListener(resurrectAllBtn, 'click', async () => {
2430
+ await this.showConfirmation('Are you sure you want to resurrect all dead jobs?', () => {
2431
+ this.resurrectAllDeadJobs();
2432
+ });
2433
+ });
2434
+ }
2435
+
2436
+ if (clearBtn) {
2437
+ this.addEventListener(clearBtn, 'click', async () => {
2438
+ await this.showConfirmation('Are you sure you want to permanently delete all dead jobs?', () => {
2439
+ this.clearDeadJobs();
2440
+ });
2441
+ });
2442
+ }
2443
+ }
2444
+
2445
+ async loadDeadJobs() {
2446
+ try {
2447
+ const params = new URLSearchParams(this.state.pagination.dead);
2448
+ const response = await fetch(`${this.gemConfig.mountPath}/dead?${params}`);
2449
+ const result = await response.json();
2450
+
2451
+ if (result.success) {
2452
+ this.renderDeadJobs(result.data);
2453
+ this.renderPagination('dead', result.data.pagination);
2454
+ // Update tab count
2455
+ this.updateElement('sqm-dead-count', result.data.total_count || 0);
2456
+ } else {
2457
+ this.showNotification(result.message, 'error');
2458
+ }
2459
+ } catch (error) {
2460
+ this.showNotification('Failed to load dead jobs', 'error');
2461
+ }
2462
+ }
2463
+
2464
+ renderDeadJobs(data) {
2465
+ const tbody = document.getElementById('sqm-dead-table-body');
2466
+ if (!tbody) return;
2467
+
2468
+ if (!data.jobs || data.jobs.length === 0) {
2469
+ tbody.innerHTML = `
2470
+ <tr>
2471
+ <td colspan="6" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
2472
+ No dead jobs found
2473
+ </td>
2474
+ </tr>
2475
+ `;
2476
+ return;
2477
+ }
2478
+
2479
+ tbody.innerHTML = data.jobs.map(job => `
2480
+ <tr>
2481
+ <td>
2482
+ <div class="sqm-job-class">${job.class}</div>
2483
+ <div class="sqm-job-args" title="${JSON.stringify(job.args)}">${JSON.stringify(job.args)}</div>
2484
+ </td>
2485
+ <td>${job.queue}</td>
2486
+ <td style="text-align: right;">
2487
+ <div>${job.failed_at || 'Unknown'}</div>
2488
+ <div class="sqm-time-relative">${job.failed_at_relative || ''}</div>
2489
+ </td>
2490
+ <td style="text-align: right;">
2491
+ <span class="sqm-retry-count">${job.retry_count}</span>
2492
+ </td>
2493
+ <td style="text-align: right;">
2494
+ <div class="sqm-error-preview" title="${job.error_message || 'No error message'}">${job.error_class || 'Unknown Error'}</div>
2495
+ </td>
2496
+ <td style="text-align: right;">
2497
+ <div class="sqm-action-buttons">
2498
+ <button class="sqm-btn sqm-btn-sm sqm-btn-resurrect"
2499
+ onclick="window.sidekiqQueueManager.resurrectDeadJob('${job.jid}')"
2500
+ title="Resurrect to retry queue">
2501
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2502
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
2503
+ </svg>
2504
+ </button>
2505
+ <button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
2506
+ onclick="window.sidekiqQueueManager.deleteDeadJob('${job.jid}')"
2507
+ title="Delete permanently">
2508
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
2509
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
2510
+ </svg>
2511
+ </button>
2512
+ </div>
2513
+ </td>
2514
+ </tr>
2515
+ `).join('');
2516
+ }
2517
+
2518
+ async resurrectDeadJob(jobId) {
2519
+ try {
2520
+ const response = await fetch(`${this.gemConfig.mountPath}/dead/${jobId}/resurrect`, {
2521
+ method: 'POST',
2522
+ headers: { 'Content-Type': 'application/json' }
2523
+ });
2524
+ const result = await response.json();
2525
+
2526
+ if (result.success) {
2527
+ this.showNotification('Job resurrected successfully', 'success');
2528
+ this.loadDeadJobs();
2529
+ } else {
2530
+ this.showNotification(result.message, 'error');
2531
+ }
2532
+ } catch (error) {
2533
+ this.showNotification('Failed to resurrect job', 'error');
2534
+ }
2535
+ }
2536
+
2537
+ async deleteDeadJob(jobId) {
2538
+ try {
2539
+ const response = await fetch(`${this.gemConfig.mountPath}/dead/${jobId}`, {
2540
+ method: 'DELETE',
2541
+ headers: { 'Content-Type': 'application/json' }
2542
+ });
2543
+ const result = await response.json();
2544
+
2545
+ if (result.success) {
2546
+ this.showNotification('Job deleted permanently', 'success');
2547
+ this.loadDeadJobs();
2548
+ } else {
2549
+ this.showNotification(result.message, 'error');
2550
+ }
2551
+ } catch (error) {
2552
+ this.showNotification('Failed to delete job', 'error');
2553
+ }
2554
+ }
2555
+
2556
+ async resurrectAllDeadJobs() {
2557
+ try {
2558
+ const filter = this.state.pagination.dead.filter;
2559
+ const body = filter ? JSON.stringify({ filter }) : null;
2560
+
2561
+ const response = await fetch(`${this.gemConfig.mountPath}/dead/resurrect_all`, {
2562
+ method: 'POST',
2563
+ headers: { 'Content-Type': 'application/json' },
2564
+ body
2565
+ });
2566
+ const result = await response.json();
2567
+
2568
+ if (result.success) {
2569
+ this.showNotification(result.message, 'success');
2570
+ this.loadDeadJobs();
2571
+ } else {
2572
+ this.showNotification(result.message, 'error');
2573
+ }
2574
+ } catch (error) {
2575
+ this.showNotification('Failed to resurrect all jobs', 'error');
2576
+ }
2577
+ }
2578
+
2579
+ async clearDeadJobs() {
2580
+ try {
2581
+ const filter = this.state.pagination.dead.filter;
2582
+ const body = filter ? JSON.stringify({ filter }) : null;
2583
+
2584
+ const response = await fetch(`${this.gemConfig.mountPath}/dead/clear`, {
2585
+ method: 'POST',
2586
+ headers: { 'Content-Type': 'application/json' },
2587
+ body
2588
+ });
2589
+ const result = await response.json();
2590
+
2591
+ if (result.success) {
2592
+ this.showNotification(result.message, 'success');
2593
+ this.loadDeadJobs();
2594
+ } else {
2595
+ this.showNotification(result.message, 'error');
2596
+ }
2597
+ } catch (error) {
2598
+ this.showNotification('Failed to clear dead jobs', 'error');
2599
+ }
2600
+ }
2601
+
2602
+ // ========================================
2603
+ // Pagination System
2604
+ // ========================================
2605
+
2606
+ renderPagination(tabName, pagination) {
2607
+ const container = document.getElementById(`sqm-${tabName}-pagination`);
2608
+ if (!container || !pagination) return;
2609
+
2610
+ const { current_page, total_pages, per_page, total_jobs, has_previous, has_next } = pagination;
2611
+
2612
+ if (total_pages <= 1) {
2613
+ container.innerHTML = '';
2614
+ return;
2615
+ }
2616
+
2617
+ container.innerHTML = `
2618
+ <div class="sqm-pagination-info">
2619
+ Showing ${((current_page - 1) * per_page) + 1}-${Math.min(current_page * per_page, total_jobs)} of ${total_jobs} jobs
2620
+ </div>
2621
+ <div class="sqm-pagination-controls">
2622
+ <button class="sqm-pagination-btn" ${!has_previous ? 'disabled' : ''}
2623
+ onclick="window.sidekiqQueueManager.changePage('${tabName}', ${current_page - 1})">
2624
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
2625
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
2626
+ </svg>
2627
+ </button>
2628
+ ${this.generatePageNumbers(current_page, total_pages, tabName)}
2629
+ <button class="sqm-pagination-btn" ${!has_next ? 'disabled' : ''}
2630
+ onclick="window.sidekiqQueueManager.changePage('${tabName}', ${current_page + 1})">
2631
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
2632
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
2633
+ </svg>
2634
+ </button>
2635
+ </div>
2636
+ `;
2637
+ }
2638
+
2639
+ generatePageNumbers(current, total, tabName) {
2640
+ const pages = [];
2641
+ const maxVisible = 5;
2642
+
2643
+ let start = Math.max(1, current - Math.floor(maxVisible / 2));
2644
+ let end = Math.min(total, start + maxVisible - 1);
2645
+
2646
+ if (end - start + 1 < maxVisible) {
2647
+ start = Math.max(1, end - maxVisible + 1);
2648
+ }
2649
+
2650
+ for (let i = start; i <= end; i++) {
2651
+ const isActive = i === current;
2652
+ pages.push(`
2653
+ <button class="sqm-pagination-btn ${isActive ? 'sqm-pagination-current' : ''}"
2654
+ onclick="window.sidekiqQueueManager.changePage('${tabName}', ${i})">
2655
+ ${i}
2656
+ </button>
2657
+ `);
2658
+ }
2659
+
2660
+ return pages.join('');
2661
+ }
2662
+
2663
+ changePage(tabName, page) {
2664
+ this.state.pagination[tabName].page = page;
2665
+ this.loadTabData(tabName);
2666
+ }
2667
+
2668
+ // ========================================
2669
+ // Confirmation Dialog
2670
+ // ========================================
2671
+
2672
+ async showConfirmation(message, onConfirm) {
2673
+ const confirmed = await this.showCustomConfirm('Confirm Action', message);
2674
+ if (confirmed) {
2675
+ onConfirm();
2676
+ }
2677
+ }
2678
+
1832
2679
  // ========================================
1833
2680
  // Notification System
1834
2681
  // ========================================