baqueue 0.1.0__py3-none-any.whl
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.
- baqueue/__init__.py +19 -0
- baqueue/balancer.py +108 -0
- baqueue/batch.py +159 -0
- baqueue/cli.py +459 -0
- baqueue/config.py +79 -0
- baqueue/dashboard/__init__.py +1 -0
- baqueue/dashboard/api.py +193 -0
- baqueue/dashboard/server.py +263 -0
- baqueue/dashboard/static/app.js +450 -0
- baqueue/dashboard/static/index.html +580 -0
- baqueue/dashboard/static/style.css +1415 -0
- baqueue/drivers/__init__.py +1 -0
- baqueue/drivers/base.py +212 -0
- baqueue/drivers/memory_driver.py +318 -0
- baqueue/drivers/postgres_driver.py +656 -0
- baqueue/drivers/redis_driver.py +656 -0
- baqueue/drivers/sqlite_driver.py +706 -0
- baqueue/events.py +64 -0
- baqueue/job.py +128 -0
- baqueue/pruner.py +128 -0
- baqueue/queue.py +225 -0
- baqueue/retry.py +55 -0
- baqueue/scheduler.py +101 -0
- baqueue/serializer.py +124 -0
- baqueue/supervisor.py +206 -0
- baqueue/worker.py +165 -0
- baqueue-0.1.0.dist-info/METADATA +609 -0
- baqueue-0.1.0.dist-info/RECORD +32 -0
- baqueue-0.1.0.dist-info/WHEEL +5 -0
- baqueue-0.1.0.dist-info/entry_points.txt +2 -0
- baqueue-0.1.0.dist-info/licenses/LICENSE +21 -0
- baqueue-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>BaQueue Dashboard</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
+
<link rel="stylesheet" href="/style.css">
|
|
10
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
11
|
+
<script src="/app.js"></script>
|
|
12
|
+
</head>
|
|
13
|
+
<body x-data="dashboard">
|
|
14
|
+
|
|
15
|
+
<!-- Mobile Sidebar Backdrop -->
|
|
16
|
+
<div class="sidebar-backdrop" :class="{ visible: sidebarMobileOpen }" @click="sidebarMobileOpen = false"></div>
|
|
17
|
+
|
|
18
|
+
<!-- Sidebar -->
|
|
19
|
+
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed, 'mobile-open': sidebarMobileOpen }">
|
|
20
|
+
<div class="sidebar-brand">
|
|
21
|
+
<div class="brand-icon">
|
|
22
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
23
|
+
</div>
|
|
24
|
+
<span class="brand-text nav-label">BaQueue</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<nav class="sidebar-nav">
|
|
28
|
+
<button class="nav-item" :class="{ active: tab === 'overview' }" @click="switchTab('overview')">
|
|
29
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
|
30
|
+
<span class="nav-label">Overview</span>
|
|
31
|
+
</button>
|
|
32
|
+
<button class="nav-item" :class="{ active: tab === 'jobs' }" @click="switchTab('jobs')">
|
|
33
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
34
|
+
<span class="nav-label">Jobs</span>
|
|
35
|
+
</button>
|
|
36
|
+
<button class="nav-item" :class="{ active: tab === 'queues' }" @click="switchTab('queues')">
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
38
|
+
<span class="nav-label">Queues</span>
|
|
39
|
+
</button>
|
|
40
|
+
<button class="nav-item" :class="{ active: tab === 'workers' }" @click="switchTab('workers')">
|
|
41
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
42
|
+
<span class="nav-label">Workers</span>
|
|
43
|
+
</button>
|
|
44
|
+
</nav>
|
|
45
|
+
|
|
46
|
+
<div class="sidebar-footer">
|
|
47
|
+
<button class="nav-item" @click="sidebarCollapsed = !sidebarCollapsed">
|
|
48
|
+
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :style="sidebarCollapsed ? 'transform:rotate(180deg)' : ''"><polyline points="15 18 9 12 15 6"/></svg>
|
|
49
|
+
<span class="nav-label">Collapse</span>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</aside>
|
|
53
|
+
|
|
54
|
+
<!-- Main Content -->
|
|
55
|
+
<main class="main-content" :class="{ expanded: sidebarCollapsed }">
|
|
56
|
+
<!-- Top Bar -->
|
|
57
|
+
<header class="topbar">
|
|
58
|
+
<div class="topbar-left">
|
|
59
|
+
<button class="menu-toggle" @click="sidebarMobileOpen = !sidebarMobileOpen" aria-label="Toggle menu">
|
|
60
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
61
|
+
</button>
|
|
62
|
+
<h1 class="page-title" x-text="pageTitle"></h1>
|
|
63
|
+
<div class="connection-badge" :class="connected ? 'online' : 'offline'">
|
|
64
|
+
<span class="conn-dot"></span>
|
|
65
|
+
<span x-text="connected ? 'Live' : 'Offline'"></span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="topbar-right">
|
|
69
|
+
<div class="date-filter-group">
|
|
70
|
+
<label>From</label>
|
|
71
|
+
<input type="datetime-local" class="date-input" x-model="dateFrom" @change="onDateFilterChange()">
|
|
72
|
+
<label>To</label>
|
|
73
|
+
<input type="datetime-local" class="date-input" x-model="dateTo" @change="onDateFilterChange()">
|
|
74
|
+
<button class="btn-icon" @click="clearDateFilter()" title="Clear date filter" x-show="dateFrom || dateTo">
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
76
|
+
</button>
|
|
77
|
+
<button class="btn-preset" @click="setPreset('1h')">1h</button>
|
|
78
|
+
<button class="btn-preset" @click="setPreset('24h')">24h</button>
|
|
79
|
+
<button class="btn-preset" @click="setPreset('7d')">7d</button>
|
|
80
|
+
<button class="btn-preset" @click="setPreset('30d')">30d</button>
|
|
81
|
+
</div>
|
|
82
|
+
<button class="theme-btn" @click="toggleTheme">
|
|
83
|
+
<svg x-show="theme === 'dark'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
84
|
+
<svg x-show="theme !== 'dark'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</header>
|
|
88
|
+
|
|
89
|
+
<div class="content-area">
|
|
90
|
+
<!-- ═══════════════ OVERVIEW ═══════════════ -->
|
|
91
|
+
<div x-show="tab === 'overview'" x-transition.opacity>
|
|
92
|
+
<!-- Stat Cards -->
|
|
93
|
+
<div class="metric-cards">
|
|
94
|
+
<div class="metric-card">
|
|
95
|
+
<div class="metric-header">
|
|
96
|
+
<span class="metric-label">Total Jobs</span>
|
|
97
|
+
<div class="metric-icon total">
|
|
98
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="metric-value" x-text="formatNumber(totals.total || 0)"></div>
|
|
102
|
+
<div class="metric-sub" x-text="totals.queues + ' queue(s) active'"></div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="metric-card accent-blue">
|
|
105
|
+
<div class="metric-header">
|
|
106
|
+
<span class="metric-label">Pending</span>
|
|
107
|
+
<div class="metric-icon pending">
|
|
108
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="metric-value" x-text="formatNumber(totals.pending)"></div>
|
|
112
|
+
<div class="metric-sub" x-text="'+' + formatNumber(rates.pending_per_min || 0) + ' / min'"></div>
|
|
113
|
+
<div class="metric-bar"><div class="metric-bar-fill pending" :style="'width:' + pctOf(totals.pending, totals.total)"></div></div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="metric-card accent-purple">
|
|
116
|
+
<div class="metric-header">
|
|
117
|
+
<span class="metric-label">Processing</span>
|
|
118
|
+
<div class="metric-icon processing">
|
|
119
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="metric-value" x-text="formatNumber(totals.processing)"></div>
|
|
123
|
+
<div class="metric-sub" x-text="'+' + formatNumber(rates.processing_per_min || 0) + ' / min'"></div>
|
|
124
|
+
<div class="metric-bar"><div class="metric-bar-fill processing" :style="'width:' + pctOf(totals.processing, totals.total)"></div></div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="metric-card accent-green">
|
|
127
|
+
<div class="metric-header">
|
|
128
|
+
<span class="metric-label">Completed</span>
|
|
129
|
+
<div class="metric-icon completed">
|
|
130
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="metric-value" x-text="formatNumber(totals.completed)"></div>
|
|
134
|
+
<div class="metric-sub" x-text="'+' + formatNumber(rates.completed_per_min || 0) + ' / min'"></div>
|
|
135
|
+
<div class="metric-bar"><div class="metric-bar-fill completed" :style="'width:' + pctOf(totals.completed, totals.total)"></div></div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="metric-card accent-red">
|
|
138
|
+
<div class="metric-header">
|
|
139
|
+
<span class="metric-label">Failed</span>
|
|
140
|
+
<div class="metric-icon failed">
|
|
141
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="metric-value" x-text="formatNumber(totals.failed)"></div>
|
|
145
|
+
<div class="metric-sub" x-text="'+' + formatNumber(rates.failed_per_min || 0) + ' / min'"></div>
|
|
146
|
+
<div class="metric-bar"><div class="metric-bar-fill failed" :style="'width:' + pctOf(totals.failed, totals.total)"></div></div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Queue Breakdown -->
|
|
151
|
+
<div class="panel">
|
|
152
|
+
<div class="panel-header">
|
|
153
|
+
<h2>Queue Breakdown</h2>
|
|
154
|
+
<span class="panel-badge" x-text="queues.length + ' queues'"></span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="panel-body">
|
|
157
|
+
<template x-if="queues.length === 0">
|
|
158
|
+
<div class="empty-state">
|
|
159
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/></svg>
|
|
160
|
+
<p>No queues found</p>
|
|
161
|
+
<span>Push some jobs to get started</span>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
164
|
+
<template x-for="q in queues" :key="q.name">
|
|
165
|
+
<div class="queue-row">
|
|
166
|
+
<div class="queue-info">
|
|
167
|
+
<span class="queue-name" x-text="q.name"></span>
|
|
168
|
+
<span class="queue-total" x-text="queueTotal(q) + ' total'"></span>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="queue-bars">
|
|
171
|
+
<div class="queue-bar-track">
|
|
172
|
+
<div class="queue-bar-seg completed" :style="'width:' + queuePct(q, 'completed')"></div>
|
|
173
|
+
<div class="queue-bar-seg processing" :style="'width:' + queuePct(q, 'processing')"></div>
|
|
174
|
+
<div class="queue-bar-seg pending" :style="'width:' + queuePct(q, 'pending')"></div>
|
|
175
|
+
<div class="queue-bar-seg failed" :style="'width:' + queuePct(q, 'failed')"></div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="queue-stats">
|
|
179
|
+
<span class="qs-item pending"><span class="qs-dot pending"></span><span x-text="q.pending"></span></span>
|
|
180
|
+
<span class="qs-item processing"><span class="qs-dot processing"></span><span x-text="q.processing"></span></span>
|
|
181
|
+
<span class="qs-item completed"><span class="qs-dot completed"></span><span x-text="q.completed"></span></span>
|
|
182
|
+
<span class="qs-item failed"><span class="qs-dot failed"></span><span x-text="q.failed"></span></span>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</template>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Recent Jobs -->
|
|
190
|
+
<div class="panel">
|
|
191
|
+
<div class="panel-header">
|
|
192
|
+
<h2>Recent Activity</h2>
|
|
193
|
+
<button class="btn-sm" @click="switchTab('jobs')">View All</button>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="panel-body no-pad">
|
|
196
|
+
<template x-if="recentJobs.length === 0">
|
|
197
|
+
<div class="empty-state small">
|
|
198
|
+
<p>No recent activity</p>
|
|
199
|
+
</div>
|
|
200
|
+
</template>
|
|
201
|
+
<div class="activity-list">
|
|
202
|
+
<template x-for="job in recentJobs" :key="job.id">
|
|
203
|
+
<div class="activity-item" @click="viewJob(job.id)">
|
|
204
|
+
<div class="activity-status" :class="job.status"></div>
|
|
205
|
+
<div class="activity-info">
|
|
206
|
+
<div class="activity-title">
|
|
207
|
+
<span class="activity-class" x-text="shortClass(job.job_class)"></span>
|
|
208
|
+
<span class="activity-queue" x-text="job.queue"></span>
|
|
209
|
+
<span class="scheduled-badge tooltip-host"
|
|
210
|
+
x-show="isScheduled(job)"
|
|
211
|
+
:data-tooltip="'Scheduled: ' + formatTimeFull(job.delay_until)">
|
|
212
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
213
|
+
<span x-text="scheduledIn(job.delay_until)"></span>
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="activity-meta">
|
|
217
|
+
<span class="mono" x-text="shortId(job.id)"></span>
|
|
218
|
+
<span x-text="'Attempt ' + job.attempts + '/' + job.max_attempts"></span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="activity-time" x-text="timeAgo(job.updated_at || job.created_at)"></div>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<!-- ═══════════════ JOBS ═══════════════ -->
|
|
230
|
+
<div x-show="tab === 'jobs'" x-transition.opacity>
|
|
231
|
+
<div class="panel">
|
|
232
|
+
<div class="panel-header">
|
|
233
|
+
<h2>All Jobs</h2>
|
|
234
|
+
<div class="filter-bar">
|
|
235
|
+
<div class="filter-group">
|
|
236
|
+
<select class="filter-select" x-model="jobsFilter.status" @change="jobsPage=1; fetchJobs()">
|
|
237
|
+
<option value="">All Status</option>
|
|
238
|
+
<option value="pending">Pending</option>
|
|
239
|
+
<option value="processing">Processing</option>
|
|
240
|
+
<option value="completed">Completed</option>
|
|
241
|
+
<option value="failed">Failed</option>
|
|
242
|
+
</select>
|
|
243
|
+
<select class="filter-select" x-model="jobsFilter.queue" @change="jobsPage=1; fetchJobs()">
|
|
244
|
+
<option value="">All Queues</option>
|
|
245
|
+
<template x-for="q in queues" :key="q.name">
|
|
246
|
+
<option :value="q.name" x-text="q.name"></option>
|
|
247
|
+
</template>
|
|
248
|
+
</select>
|
|
249
|
+
<input class="filter-input" type="text" placeholder="Tag..." x-model="jobsFilter.tag" @input.debounce.300ms="jobsPage=1; fetchJobs()">
|
|
250
|
+
</div>
|
|
251
|
+
<div class="jobs-count">
|
|
252
|
+
<button class="btn-bulk-retry"
|
|
253
|
+
x-show="jobsFilter.status === 'failed' && jobsTotal > 0"
|
|
254
|
+
@click="retryAllFailed()"
|
|
255
|
+
title="Retry all failed jobs matching current filters">
|
|
256
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
|
257
|
+
Retry All (<span x-text="jobsTotal"></span>)
|
|
258
|
+
</button>
|
|
259
|
+
<span class="live-indicator" :class="{ on: jobsWSConnected }" x-show="jobsWSConnected" title="Live updates via WebSocket">
|
|
260
|
+
<span class="live-dot"></span>LIVE
|
|
261
|
+
</span>
|
|
262
|
+
<span x-show="jobsTotal > 0" x-text="jobsTotal + ' job(s) found'"></span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="panel-body no-pad">
|
|
267
|
+
<template x-if="jobs.length === 0">
|
|
268
|
+
<div class="empty-state">
|
|
269
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
270
|
+
<p>No jobs match the current filters</p>
|
|
271
|
+
</div>
|
|
272
|
+
</template>
|
|
273
|
+
<table x-show="jobs.length > 0" class="jobs-table">
|
|
274
|
+
<thead>
|
|
275
|
+
<tr>
|
|
276
|
+
<th>Status</th>
|
|
277
|
+
<th>Job ID</th>
|
|
278
|
+
<th>Job Class</th>
|
|
279
|
+
<th>Queue</th>
|
|
280
|
+
<th>Attempts</th>
|
|
281
|
+
<th>Created</th>
|
|
282
|
+
<th>Duration</th>
|
|
283
|
+
<th></th>
|
|
284
|
+
</tr>
|
|
285
|
+
</thead>
|
|
286
|
+
<tbody>
|
|
287
|
+
<template x-for="job in jobs" :key="job.id">
|
|
288
|
+
<tr @click="viewJob(job.id)"
|
|
289
|
+
class="clickable-row"
|
|
290
|
+
:class="{ 'row-enter': job._entered, 'row-flash': job._flash }">
|
|
291
|
+
<td>
|
|
292
|
+
<div class="status-cell">
|
|
293
|
+
<span class="status-pill" :class="job.status">
|
|
294
|
+
<span class="status-dot-sm"></span>
|
|
295
|
+
<span x-text="job.status"></span>
|
|
296
|
+
</span>
|
|
297
|
+
<span class="scheduled-badge tooltip-host"
|
|
298
|
+
x-show="isScheduled(job)"
|
|
299
|
+
:data-tooltip="'Scheduled: ' + formatTimeFull(job.delay_until)">
|
|
300
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
301
|
+
<span x-text="scheduledIn(job.delay_until)"></span>
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
</td>
|
|
305
|
+
<td class="mono cell-id" x-text="shortId(job.id)"></td>
|
|
306
|
+
<td class="cell-class" x-text="shortClass(job.job_class)"></td>
|
|
307
|
+
<td><span class="queue-pill" x-text="job.queue"></span></td>
|
|
308
|
+
<td class="cell-attempts">
|
|
309
|
+
<span x-text="job.attempts + '/' + job.max_attempts"></span>
|
|
310
|
+
<div class="attempt-bar">
|
|
311
|
+
<div class="attempt-fill"
|
|
312
|
+
:class="{ over: job.attempts > job.max_attempts }"
|
|
313
|
+
:style="'width:' + Math.min(job.attempts / job.max_attempts * 100, 100) + '%'"></div>
|
|
314
|
+
</div>
|
|
315
|
+
</td>
|
|
316
|
+
<td class="cell-time" x-text="formatTime(job.created_at)"></td>
|
|
317
|
+
<td class="cell-time" x-text="jobDuration(job)"></td>
|
|
318
|
+
<td>
|
|
319
|
+
<button class="btn-action" @click.stop="viewJob(job.id)">
|
|
320
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="9 18 15 12 9 6"/></svg>
|
|
321
|
+
</button>
|
|
322
|
+
</td>
|
|
323
|
+
</tr>
|
|
324
|
+
</template>
|
|
325
|
+
</tbody>
|
|
326
|
+
</table>
|
|
327
|
+
</div>
|
|
328
|
+
<!-- Pagination -->
|
|
329
|
+
<div class="panel-footer" x-show="jobs.length > 0">
|
|
330
|
+
<span class="page-info" x-text="'Page ' + jobsPage + ' of ' + Math.ceil(jobsTotal / 25)"></span>
|
|
331
|
+
<div class="page-btns">
|
|
332
|
+
<button class="btn-page" @click="prevPage()" :disabled="jobsPage <= 1">
|
|
333
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 18 9 12 15 6"/></svg>
|
|
334
|
+
Prev
|
|
335
|
+
</button>
|
|
336
|
+
<button class="btn-page" @click="nextPage()" :disabled="jobsPage >= Math.ceil(jobsTotal / 25)">
|
|
337
|
+
Next
|
|
338
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="9 18 15 12 9 6"/></svg>
|
|
339
|
+
</button>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- ═══════════════ QUEUES ═══════════════ -->
|
|
346
|
+
<div x-show="tab === 'queues'" x-transition.opacity>
|
|
347
|
+
<template x-if="queues.length === 0">
|
|
348
|
+
<div class="panel">
|
|
349
|
+
<div class="empty-state">
|
|
350
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/></svg>
|
|
351
|
+
<p>No queues found</p>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</template>
|
|
355
|
+
<div class="queue-cards">
|
|
356
|
+
<template x-for="q in queues" :key="q.name">
|
|
357
|
+
<div class="queue-card" @click="jobsFilter.queue = q.name; switchTab('jobs')">
|
|
358
|
+
<div class="qc-header">
|
|
359
|
+
<h3 x-text="q.name"></h3>
|
|
360
|
+
<span class="qc-total" x-text="queueTotal(q)"></span>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="qc-bar-track">
|
|
363
|
+
<div class="queue-bar-seg completed" :style="'width:' + queuePct(q, 'completed')"></div>
|
|
364
|
+
<div class="queue-bar-seg processing" :style="'width:' + queuePct(q, 'processing')"></div>
|
|
365
|
+
<div class="queue-bar-seg pending" :style="'width:' + queuePct(q, 'pending')"></div>
|
|
366
|
+
<div class="queue-bar-seg failed" :style="'width:' + queuePct(q, 'failed')"></div>
|
|
367
|
+
</div>
|
|
368
|
+
<div class="qc-stats">
|
|
369
|
+
<div class="qc-stat">
|
|
370
|
+
<span class="qc-stat-val pending" x-text="q.pending"></span>
|
|
371
|
+
<span class="qc-stat-lbl">Pending</span>
|
|
372
|
+
<span class="qc-stat-rate" x-text="'+' + ((q.rates && q.rates.pending_per_min) || 0) + '/m'"></span>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="qc-stat">
|
|
375
|
+
<span class="qc-stat-val processing" x-text="q.processing"></span>
|
|
376
|
+
<span class="qc-stat-lbl">Processing</span>
|
|
377
|
+
<span class="qc-stat-rate" x-text="'+' + ((q.rates && q.rates.processing_per_min) || 0) + '/m'"></span>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="qc-stat">
|
|
380
|
+
<span class="qc-stat-val completed" x-text="q.completed"></span>
|
|
381
|
+
<span class="qc-stat-lbl">Completed</span>
|
|
382
|
+
<span class="qc-stat-rate" x-text="'+' + ((q.rates && q.rates.completed_per_min) || 0) + '/m'"></span>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="qc-stat">
|
|
385
|
+
<span class="qc-stat-val failed" x-text="q.failed"></span>
|
|
386
|
+
<span class="qc-stat-lbl">Failed</span>
|
|
387
|
+
<span class="qc-stat-rate" x-text="'+' + ((q.rates && q.rates.failed_per_min) || 0) + '/m'"></span>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</template>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<!-- ═══════════════ WORKERS ═══════════════ -->
|
|
396
|
+
<div x-show="tab === 'workers'" x-transition.opacity>
|
|
397
|
+
<template x-if="supervisors.length === 0">
|
|
398
|
+
<div class="panel">
|
|
399
|
+
<div class="empty-state">
|
|
400
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
401
|
+
<p>No active supervisors</p>
|
|
402
|
+
<span>Start a worker process to see stats here</span>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</template>
|
|
406
|
+
<template x-for="sup in supervisors" :key="sup.name">
|
|
407
|
+
<div class="panel">
|
|
408
|
+
<div class="panel-header">
|
|
409
|
+
<div>
|
|
410
|
+
<h2 x-text="sup.name"></h2>
|
|
411
|
+
<span class="panel-badge" :class="sup.running ? 'green' : 'red'" x-text="sup.running ? 'Running' : 'Stopped'"></span>
|
|
412
|
+
</div>
|
|
413
|
+
<span class="text-muted" x-text="sup.workers + ' worker(s)'"></span>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="workers-grid">
|
|
416
|
+
<template x-for="w in (sup.worker_stats || [])" :key="w.name">
|
|
417
|
+
<div class="worker-tile" :class="{ active: w.running }">
|
|
418
|
+
<div class="wt-header">
|
|
419
|
+
<span class="wt-name" x-text="w.name"></span>
|
|
420
|
+
<span class="wt-status" :class="w.running ? 'running' : 'idle'" x-text="w.running ? 'Active' : 'Idle'"></span>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="wt-stats">
|
|
423
|
+
<div class="wt-stat">
|
|
424
|
+
<span class="wt-stat-val" x-text="w.jobs_processed"></span>
|
|
425
|
+
<span class="wt-stat-lbl">Processed</span>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="wt-stat">
|
|
428
|
+
<span class="wt-stat-val failed" x-text="w.jobs_failed"></span>
|
|
429
|
+
<span class="wt-stat-lbl">Failed</span>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="wt-current" x-show="w.current_job">
|
|
433
|
+
<span class="wt-current-label">Processing:</span>
|
|
434
|
+
<span class="mono" x-text="shortId(w.current_job || '')"></span>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</template>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</template>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</main>
|
|
444
|
+
|
|
445
|
+
<!-- ═══════════════ JOB DETAIL MODAL ═══════════════ -->
|
|
446
|
+
<template x-if="selectedJob">
|
|
447
|
+
<div class="modal-overlay" @click.self="closeModal()" x-transition.opacity>
|
|
448
|
+
<div class="modal-panel" @click.stop>
|
|
449
|
+
<div class="modal-top">
|
|
450
|
+
<div>
|
|
451
|
+
<h2>Job Details</h2>
|
|
452
|
+
<span class="mono text-muted" x-text="selectedJob.id"></span>
|
|
453
|
+
</div>
|
|
454
|
+
<button class="btn-icon" @click="closeModal()">
|
|
455
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div class="modal-content">
|
|
460
|
+
<div class="detail-grid">
|
|
461
|
+
<div class="detail-card">
|
|
462
|
+
<span class="dc-label">Status</span>
|
|
463
|
+
<span class="status-pill lg" :class="selectedJob.status">
|
|
464
|
+
<span class="status-dot-sm"></span>
|
|
465
|
+
<span x-text="selectedJob.status"></span>
|
|
466
|
+
</span>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="detail-card">
|
|
469
|
+
<span class="dc-label">Queue</span>
|
|
470
|
+
<span class="queue-pill lg" x-text="selectedJob.queue"></span>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="detail-card">
|
|
473
|
+
<span class="dc-label">Attempts</span>
|
|
474
|
+
<span class="dc-value" x-text="selectedJob.attempts + ' / ' + selectedJob.max_attempts"></span>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="detail-card">
|
|
477
|
+
<span class="dc-label">Backoff</span>
|
|
478
|
+
<span class="dc-value" x-text="JSON.stringify(selectedJob.backoff)"></span>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="detail-card" x-show="selectedJob.delay_until" style="grid-column: 1 / -1;">
|
|
481
|
+
<span class="dc-label">Scheduled For</span>
|
|
482
|
+
<div class="dc-scheduled">
|
|
483
|
+
<span class="scheduled-badge">
|
|
484
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
485
|
+
<span x-text="scheduledIn(selectedJob.delay_until)"></span>
|
|
486
|
+
</span>
|
|
487
|
+
<span class="dc-value" x-text="formatTimeFull(selectedJob.delay_until)"></span>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<div class="detail-section">
|
|
493
|
+
<h4>Job Class</h4>
|
|
494
|
+
<code class="code-block" x-text="selectedJob.job_class"></code>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div class="detail-section">
|
|
498
|
+
<h4>Timeline</h4>
|
|
499
|
+
<div class="timeline">
|
|
500
|
+
<div class="tl-item">
|
|
501
|
+
<div class="tl-dot created"></div>
|
|
502
|
+
<div class="tl-content">
|
|
503
|
+
<span class="tl-label">Created</span>
|
|
504
|
+
<span class="tl-time" x-text="formatTimeFull(selectedJob.created_at)"></span>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="tl-item" x-show="selectedJob.delay_until">
|
|
508
|
+
<div class="tl-dot scheduled"></div>
|
|
509
|
+
<div class="tl-content">
|
|
510
|
+
<span class="tl-label">Scheduled For</span>
|
|
511
|
+
<span class="tl-time" x-text="formatTimeFull(selectedJob.delay_until)"></span>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="tl-item" x-show="selectedJob.started_at">
|
|
515
|
+
<div class="tl-dot processing"></div>
|
|
516
|
+
<div class="tl-content">
|
|
517
|
+
<span class="tl-label">Started</span>
|
|
518
|
+
<span class="tl-time" x-text="formatTimeFull(selectedJob.started_at)"></span>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="tl-item" x-show="selectedJob.completed_at">
|
|
522
|
+
<div class="tl-dot completed"></div>
|
|
523
|
+
<div class="tl-content">
|
|
524
|
+
<span class="tl-label">Completed</span>
|
|
525
|
+
<span class="tl-time" x-text="formatTimeFull(selectedJob.completed_at)"></span>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
<div class="tl-item" x-show="selectedJob.failed_at">
|
|
529
|
+
<div class="tl-dot failed"></div>
|
|
530
|
+
<div class="tl-content">
|
|
531
|
+
<span class="tl-label">Failed</span>
|
|
532
|
+
<span class="tl-time" x-text="formatTimeFull(selectedJob.failed_at)"></span>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<div class="detail-section" x-show="selectedJob.tags && selectedJob.tags.length">
|
|
539
|
+
<h4>Tags</h4>
|
|
540
|
+
<div class="tags-list">
|
|
541
|
+
<template x-for="t in (selectedJob.tags || [])" :key="t">
|
|
542
|
+
<span class="tag-chip" x-text="t"></span>
|
|
543
|
+
</template>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<div class="detail-section" x-show="selectedJob.batch_id">
|
|
548
|
+
<h4>Batch ID</h4>
|
|
549
|
+
<code class="code-block" x-text="selectedJob.batch_id"></code>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div class="detail-section">
|
|
553
|
+
<h4>Payload Data</h4>
|
|
554
|
+
<pre class="code-pre" x-text="JSON.stringify(selectedJob.data, null, 2)"></pre>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<template x-if="selectedJob.error">
|
|
558
|
+
<div class="detail-section error-section">
|
|
559
|
+
<h4>Error</h4>
|
|
560
|
+
<pre class="code-pre error" x-text="selectedJob.error"></pre>
|
|
561
|
+
</div>
|
|
562
|
+
</template>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div class="modal-actions">
|
|
566
|
+
<button class="btn-primary" x-show="selectedJob.status === 'failed'" @click="retryJob(selectedJob.id)">
|
|
567
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
|
568
|
+
Retry Job
|
|
569
|
+
</button>
|
|
570
|
+
<button class="btn-danger" @click="deleteJob(selectedJob.id)">
|
|
571
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
|
572
|
+
Delete Job
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</template>
|
|
578
|
+
|
|
579
|
+
</body>
|
|
580
|
+
</html>
|