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.
- package/README.md +508 -0
- package/dashboard/app.js +249 -0
- package/dashboard/index.html +143 -0
- package/dashboard/style.css +464 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +505 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +99 -0
- package/dist/client.js.map +1 -0
- package/dist/dashboard.d.ts +48 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +412 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/database.d.ts +29 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +99 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +218 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +64 -0
- package/dist/utils.js.map +1 -0
- package/dist/worker.d.ts +26 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +331 -0
- package/dist/worker.js.map +1 -0
- package/package.json +55 -0
package/dashboard/app.js
ADDED
|
@@ -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>
|