workmatic 1.0.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.
@@ -0,0 +1,249 @@
1
+ // Dashboard state
2
+ const state = {
3
+ stats: { ready: 0, running: 0, done: 0, failed: 0, dead: 0, total: 0 },
4
+ jobs: [],
5
+ queues: [],
6
+ workers: [],
7
+ currentPage: 0,
8
+ pageSize: 50,
9
+ queueFilter: '',
10
+ statusFilter: '',
11
+ };
12
+
13
+ // Refresh interval (ms)
14
+ const REFRESH_INTERVAL = 2000;
15
+
16
+ // DOM elements
17
+ const elements = {
18
+ statReady: document.getElementById('stat-ready'),
19
+ statRunning: document.getElementById('stat-running'),
20
+ statDone: document.getElementById('stat-done'),
21
+ statFailed: document.getElementById('stat-failed'),
22
+ statDead: document.getElementById('stat-dead'),
23
+ workersSection: document.getElementById('workers-section'),
24
+ workersGrid: document.getElementById('workers-grid'),
25
+ jobsTbody: document.getElementById('jobs-tbody'),
26
+ queueFilter: document.getElementById('queue-filter'),
27
+ statusFilter: document.getElementById('status-filter'),
28
+ prevPage: document.getElementById('prev-page'),
29
+ nextPage: document.getElementById('next-page'),
30
+ pageInfo: document.getElementById('page-info'),
31
+ refreshIndicator: document.getElementById('refresh-indicator'),
32
+ };
33
+
34
+ // Format timestamp to relative time
35
+ function formatTime(timestamp) {
36
+ if (!timestamp) return '-';
37
+
38
+ const now = Date.now();
39
+ const diff = now - timestamp;
40
+
41
+ if (diff < 1000) return 'just now';
42
+ if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
43
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
44
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
45
+
46
+ return new Date(timestamp).toLocaleDateString();
47
+ }
48
+
49
+ // Fetch stats from API
50
+ async function fetchStats() {
51
+ try {
52
+ const params = new URLSearchParams();
53
+ if (state.queueFilter) params.set('queue', state.queueFilter);
54
+
55
+ const response = await fetch(`/api/stats?${params}`);
56
+ const data = await response.json();
57
+
58
+ state.stats = data.stats;
59
+ state.queues = data.queues || [];
60
+ state.workers = data.workers || [];
61
+
62
+ updateStatsUI();
63
+ updateQueueFilterOptions();
64
+ updateWorkersUI();
65
+ } catch (error) {
66
+ console.error('Failed to fetch stats:', error);
67
+ }
68
+ }
69
+
70
+ // Fetch jobs from API
71
+ async function fetchJobs() {
72
+ try {
73
+ const params = new URLSearchParams({
74
+ limit: state.pageSize.toString(),
75
+ offset: (state.currentPage * state.pageSize).toString(),
76
+ });
77
+
78
+ if (state.queueFilter) params.set('queue', state.queueFilter);
79
+ if (state.statusFilter) params.set('status', state.statusFilter);
80
+
81
+ const response = await fetch(`/api/jobs?${params}`);
82
+ const data = await response.json();
83
+
84
+ state.jobs = data.jobs || [];
85
+
86
+ updateJobsUI();
87
+ updatePaginationUI();
88
+ } catch (error) {
89
+ console.error('Failed to fetch jobs:', error);
90
+ }
91
+ }
92
+
93
+ // Update stats UI
94
+ function updateStatsUI() {
95
+ elements.statReady.textContent = state.stats.ready.toLocaleString();
96
+ elements.statRunning.textContent = state.stats.running.toLocaleString();
97
+ elements.statDone.textContent = state.stats.done.toLocaleString();
98
+ elements.statFailed.textContent = state.stats.failed.toLocaleString();
99
+ elements.statDead.textContent = state.stats.dead.toLocaleString();
100
+ }
101
+
102
+ // Update queue filter options
103
+ function updateQueueFilterOptions() {
104
+ const currentValue = elements.queueFilter.value;
105
+ const options = ['<option value="">All Queues</option>'];
106
+
107
+ for (const queue of state.queues) {
108
+ const selected = queue === currentValue ? ' selected' : '';
109
+ options.push(`<option value="${queue}"${selected}>${queue}</option>`);
110
+ }
111
+
112
+ elements.queueFilter.innerHTML = options.join('');
113
+ }
114
+
115
+ // Update workers UI
116
+ function updateWorkersUI() {
117
+ if (state.workers.length === 0) {
118
+ elements.workersSection.style.display = 'none';
119
+ return;
120
+ }
121
+
122
+ elements.workersSection.style.display = 'block';
123
+
124
+ const cards = state.workers.map(worker => {
125
+ const statusClass = !worker.running ? 'stopped' : worker.paused ? 'paused' : 'running';
126
+ const statusText = !worker.running ? 'Stopped' : worker.paused ? 'Paused' : 'Running';
127
+
128
+ return `
129
+ <div class="worker-card">
130
+ <div class="worker-info">
131
+ <span class="worker-queue">${worker.queue}</span>
132
+ <span class="worker-status">
133
+ <span class="dot ${statusClass}"></span>
134
+ ${statusText}
135
+ </span>
136
+ </div>
137
+ <div class="worker-controls">
138
+ ${worker.paused
139
+ ? `<button onclick="resumeWorker('${worker.queue}')">Resume</button>`
140
+ : `<button onclick="pauseWorker('${worker.queue}')">Pause</button>`
141
+ }
142
+ </div>
143
+ </div>
144
+ `;
145
+ });
146
+
147
+ elements.workersGrid.innerHTML = cards.join('');
148
+ }
149
+
150
+ // Update jobs UI
151
+ function updateJobsUI() {
152
+ if (state.jobs.length === 0) {
153
+ elements.jobsTbody.innerHTML = `
154
+ <tr>
155
+ <td colspan="7" class="empty-state">
156
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
157
+ <rect x="2" y="5" width="20" height="14" rx="2"/>
158
+ <line x1="2" y1="10" x2="22" y2="10"/>
159
+ </svg>
160
+ <p>No jobs found</p>
161
+ </td>
162
+ </tr>
163
+ `;
164
+ return;
165
+ }
166
+
167
+ const rows = state.jobs.map(job => `
168
+ <tr>
169
+ <td><span class="job-id">${job.id}</span></td>
170
+ <td><span class="job-queue">${job.queue}</span></td>
171
+ <td><span class="job-status ${job.status}">${job.status}</span></td>
172
+ <td class="job-priority">${job.priority}</td>
173
+ <td class="job-attempts">${job.attempts}/${job.maxAttempts}</td>
174
+ <td class="job-time">${formatTime(job.createdAt)}</td>
175
+ <td class="job-error" title="${job.lastError || ''}">${job.lastError || ''}</td>
176
+ </tr>
177
+ `);
178
+
179
+ elements.jobsTbody.innerHTML = rows.join('');
180
+ }
181
+
182
+ // Update pagination UI
183
+ function updatePaginationUI() {
184
+ elements.prevPage.disabled = state.currentPage === 0;
185
+ elements.nextPage.disabled = state.jobs.length < state.pageSize;
186
+ elements.pageInfo.textContent = `Page ${state.currentPage + 1}`;
187
+ }
188
+
189
+ // Pause worker
190
+ async function pauseWorker(queue) {
191
+ try {
192
+ await fetch(`/api/workers/${encodeURIComponent(queue)}/pause`, { method: 'POST' });
193
+ await fetchStats();
194
+ } catch (error) {
195
+ console.error('Failed to pause worker:', error);
196
+ }
197
+ }
198
+
199
+ // Resume worker
200
+ async function resumeWorker(queue) {
201
+ try {
202
+ await fetch(`/api/workers/${encodeURIComponent(queue)}/resume`, { method: 'POST' });
203
+ await fetchStats();
204
+ } catch (error) {
205
+ console.error('Failed to resume worker:', error);
206
+ }
207
+ }
208
+
209
+ // Event handlers
210
+ elements.queueFilter.addEventListener('change', (e) => {
211
+ state.queueFilter = e.target.value;
212
+ state.currentPage = 0;
213
+ fetchStats();
214
+ fetchJobs();
215
+ });
216
+
217
+ elements.statusFilter.addEventListener('change', (e) => {
218
+ state.statusFilter = e.target.value;
219
+ state.currentPage = 0;
220
+ fetchJobs();
221
+ });
222
+
223
+ elements.prevPage.addEventListener('click', () => {
224
+ if (state.currentPage > 0) {
225
+ state.currentPage--;
226
+ fetchJobs();
227
+ }
228
+ });
229
+
230
+ elements.nextPage.addEventListener('click', () => {
231
+ if (state.jobs.length >= state.pageSize) {
232
+ state.currentPage++;
233
+ fetchJobs();
234
+ }
235
+ });
236
+
237
+ // Make functions available globally for onclick handlers
238
+ window.pauseWorker = pauseWorker;
239
+ window.resumeWorker = resumeWorker;
240
+
241
+ // Initial fetch
242
+ fetchStats();
243
+ fetchJobs();
244
+
245
+ // Auto-refresh
246
+ setInterval(() => {
247
+ fetchStats();
248
+ fetchJobs();
249
+ }, REFRESH_INTERVAL);
@@ -0,0 +1,143 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Workmatic Dashboard</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <div class="container">
14
+ <header>
15
+ <h1>
16
+ <span class="logo">W</span>
17
+ Workmatic
18
+ </h1>
19
+ <div class="header-controls">
20
+ <select id="queue-filter">
21
+ <option value="">All Queues</option>
22
+ </select>
23
+ <span class="refresh-indicator" id="refresh-indicator"></span>
24
+ </div>
25
+ </header>
26
+
27
+ <section class="stats-section">
28
+ <div class="stats-grid">
29
+ <div class="stat-card ready">
30
+ <div class="stat-icon">
31
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
32
+ <circle cx="12" cy="12" r="10"/>
33
+ <polyline points="12,6 12,12 16,14"/>
34
+ </svg>
35
+ </div>
36
+ <div class="stat-content">
37
+ <span class="stat-value" id="stat-ready">0</span>
38
+ <span class="stat-label">Ready</span>
39
+ </div>
40
+ </div>
41
+ <div class="stat-card running">
42
+ <div class="stat-icon">
43
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <polygon points="5,3 19,12 5,21"/>
45
+ </svg>
46
+ </div>
47
+ <div class="stat-content">
48
+ <span class="stat-value" id="stat-running">0</span>
49
+ <span class="stat-label">Running</span>
50
+ </div>
51
+ </div>
52
+ <div class="stat-card done">
53
+ <div class="stat-icon">
54
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
55
+ <polyline points="20,6 9,17 4,12"/>
56
+ </svg>
57
+ </div>
58
+ <div class="stat-content">
59
+ <span class="stat-value" id="stat-done">0</span>
60
+ <span class="stat-label">Done</span>
61
+ </div>
62
+ </div>
63
+ <div class="stat-card failed">
64
+ <div class="stat-icon">
65
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
66
+ <circle cx="12" cy="12" r="10"/>
67
+ <line x1="12" y1="8" x2="12" y2="12"/>
68
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
69
+ </svg>
70
+ </div>
71
+ <div class="stat-content">
72
+ <span class="stat-value" id="stat-failed">0</span>
73
+ <span class="stat-label">Failed</span>
74
+ </div>
75
+ </div>
76
+ <div class="stat-card dead">
77
+ <div class="stat-icon">
78
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
79
+ <circle cx="12" cy="12" r="10"/>
80
+ <line x1="15" y1="9" x2="9" y2="15"/>
81
+ <line x1="9" y1="9" x2="15" y2="15"/>
82
+ </svg>
83
+ </div>
84
+ <div class="stat-content">
85
+ <span class="stat-value" id="stat-dead">0</span>
86
+ <span class="stat-label">Dead</span>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </section>
91
+
92
+ <section class="workers-section" id="workers-section">
93
+ <h2>Workers</h2>
94
+ <div class="workers-grid" id="workers-grid">
95
+ <!-- Workers will be populated by JS -->
96
+ </div>
97
+ </section>
98
+
99
+ <section class="jobs-section">
100
+ <div class="jobs-header">
101
+ <h2>Jobs</h2>
102
+ <div class="jobs-filters">
103
+ <select id="status-filter">
104
+ <option value="">All Statuses</option>
105
+ <option value="ready">Ready</option>
106
+ <option value="running">Running</option>
107
+ <option value="done">Done</option>
108
+ <option value="failed">Failed</option>
109
+ <option value="dead">Dead</option>
110
+ </select>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="jobs-table-container">
115
+ <table class="jobs-table">
116
+ <thead>
117
+ <tr>
118
+ <th>ID</th>
119
+ <th>Queue</th>
120
+ <th>Status</th>
121
+ <th>Priority</th>
122
+ <th>Attempts</th>
123
+ <th>Created</th>
124
+ <th>Error</th>
125
+ </tr>
126
+ </thead>
127
+ <tbody id="jobs-tbody">
128
+ <!-- Jobs will be populated by JS -->
129
+ </tbody>
130
+ </table>
131
+ </div>
132
+
133
+ <div class="pagination">
134
+ <button id="prev-page" disabled>Previous</button>
135
+ <span id="page-info">Page 1</span>
136
+ <button id="next-page">Next</button>
137
+ </div>
138
+ </section>
139
+ </div>
140
+
141
+ <script src="app.js"></script>
142
+ </body>
143
+ </html>