@@ -9,18 +9,43 @@ <h3>System Status</h3>
99 < div id ="celery-status-panel " class ="col-md-5 ">
1010 < div class ="panel panel-default ">
1111 < div class ="panel-heading ">
12- < h4 > Celery < span id =" celery-status-badge " class =" label label-default " > Loading... </ span > </ h4 >
12+ < h4 > Celery Status </ h4 >
1313 </ div >
14- < div class ="panel-body text-left " id ="celery-status-msg "> —</ div >
1514 < div class ="panel-body text-left ">
16- < span id ="celery-queue-msg "> —</ span >
15+ < table class ="table table-condensed " style ="margin-bottom: 0 ">
16+ < tbody >
17+ < tr >
18+ < td > Redis Broker</ td >
19+ < td >
20+ < i id ="broker-loading " class ="fa-solid fa-spinner fa-spin text-muted "> </ i >
21+ < span id ="broker-status-badge " class ="label label-default " style ="display:none "> </ span >
22+ </ td >
23+ </ tr >
24+ < tr >
25+ < td > Celery Worker</ td >
26+ < td >
27+ < i id ="worker-loading " class ="fa-solid fa-spinner fa-spin text-muted "> </ i >
28+ < span id ="worker-status-badge " class ="label label-default " style ="display:none "> </ span >
29+ </ td >
30+ </ tr >
31+ </ tbody >
32+ </ table >
33+ </ div >
34+ < div class ="panel-body text-left ">
35+ < span id ="celery-queue-msg "> < i class ="fa-solid fa-spinner fa-spin text-muted "> </ i > </ span >
1736 < div style ="margin-top: 8px; ">
1837 < button id ="purge-queue-btn " class ="btn btn-danger btn-xs " disabled title ="Loading... ">
1938 Purge queue
2039 </ button >
2140 < button id ="refresh-status-btn " class ="btn btn-default btn-xs " style ="margin-left: 6px; ">
2241 < span class ="glyphicon glyphicon-refresh "> </ span > Refresh
2342 </ button >
43+ < button id ="view-details-btn " class ="btn btn-default btn-xs " disabled title ="Loading... " style ="margin-left: 6px; ">
44+ View Details
45+ </ button >
46+ < span class ="text-muted " style ="font-size: 0.85em; margin-left: 6px; ">
47+ Inspects every message in the queue — may be slow for large queues.
48+ </ span >
2449 </ div >
2550 < p class ="text-muted " style ="margin-top: 8px; font-size: 0.9em ">
2651 < strong > Note:</ strong > Purging the queue removes pending tasks that have not yet been executed,
@@ -29,6 +54,32 @@ <h4>Celery <span id="celery-status-badge" class="label label-default">Loading...
2954 < code > python manage.py dedupe</ code >
3055 </ p >
3156 </ div >
57+ < div class ="panel-body text-left ">
58+ < div id ="queue-details-container " style ="display: none; ">
59+ < table class ="table table-condensed table-bordered table-hover " style ="margin-bottom: 0; font-size: 0.9em; ">
60+ < thead >
61+ < tr >
62+ < th > #</ th >
63+ < th > Task name</ th >
64+ < th > Count</ th >
65+ < th > Oldest (queue pos.)</ th >
66+ < th > Newest (queue pos.)</ th >
67+ < th > Expires</ th >
68+ < th > </ th >
69+ </ tr >
70+ </ thead >
71+ < tbody id ="queue-details-tbody "> </ tbody >
72+ </ table >
73+ < p class ="text-muted " style ="font-size: 0.8em; margin-top: 4px; ">
74+ Position 1 = oldest task in queue. ETA shown in parentheses when set.
75+ </ p >
76+ </ div >
77+ </ div >
78+ </ div >
79+ < div class ="panel panel-default ">
80+ < div class ="panel-heading ">
81+ < h4 > Celery Settings</ h4 >
82+ </ div >
3283 < div class ="panel-body text-left ">
3384 < table class ="table table-condensed " style ="margin-bottom: 0 ">
3485 < thead > < tr > < th > Setting</ th > < th > Value</ th > </ tr > </ thead >
@@ -56,24 +107,32 @@ <h4>Celery <span id="celery-status-badge" class="label label-default">Loading...
56107 ( function ( ) {
57108 var purgeUrl = "{% url 'celery_queue_purge_api' %}" ;
58109 var statusUrl = "{% url 'celery_status_api' %}" ;
110+ var detailsUrl = "{% url 'celery_queue_details_api' %}" ;
111+ var taskPurgeUrl = "{% url 'celery_queue_task_purge_api' %}" ;
59112
60- function renderCeleryStatus ( data ) {
61- var badge = $ ( '#celery-status-badge' ) ;
62- if ( data . worker_status ) {
63- badge . text ( 'Running' ) . removeClass ( 'label-default label-danger' ) . addClass ( 'label-success' ) ;
64- $ ( '#celery-status-msg ') . text ( 'Celery is processing tasks. ') ;
113+ function setStatusBadge ( loadingId , badgeId , isOk , okText , failText ) {
114+ $ ( '#' + loadingId ) . hide ( ) ;
115+ var badge = $ ( '#' + badgeId ) . show ( ) ;
116+ if ( isOk ) {
117+ badge . text ( okText ) . removeClass ( 'label-default label-danger ') . addClass ( 'label-success ') ;
65118 } else {
66- badge . text ( 'Not Running' ) . removeClass ( 'label-default label-success' ) . addClass ( 'label-danger' ) ;
67- $ ( '#celery-status-msg' ) . text ( 'Celery does not appear to be running.' ) ;
119+ badge . text ( failText ) . removeClass ( 'label-default label-success' ) . addClass ( 'label-danger' ) ;
68120 }
121+ }
122+
123+ function renderCeleryStatus ( data ) {
124+ setStatusBadge ( 'broker-loading' , 'broker-status-badge' , data . broker_status , 'Reachable' , 'Unreachable' ) ;
125+ setStatusBadge ( 'worker-loading' , 'worker-status-badge' , data . worker_status , 'Running' , 'Not Running' ) ;
69126
70127 var qLen = data . queue_length ;
71128 if ( qLen === null ) {
72129 $ ( '#celery-queue-msg' ) . text ( 'It is not possible to identify the number of waiting tasks.' ) ;
73130 $ ( '#purge-queue-btn' ) . prop ( 'disabled' , true ) . attr ( 'title' , 'Broker unreachable' ) ;
131+ $ ( '#view-details-btn' ) . prop ( 'disabled' , true ) . attr ( 'title' , 'Broker unreachable' ) ;
74132 } else {
75133 $ ( '#celery-queue-msg' ) . text ( qLen + ' task(s) waiting to be processed.' ) ;
76134 $ ( '#purge-queue-btn' ) . prop ( 'disabled' , false ) . removeAttr ( 'title' ) ;
135+ $ ( '#view-details-btn' ) . prop ( 'disabled' , false ) . removeAttr ( 'title' ) ;
77136 }
78137
79138 function humanDuration ( seconds ) {
@@ -101,13 +160,78 @@ <h4>Celery <span id="celery-status-badge" class="label label-default">Loading...
101160 } ) ;
102161 }
103162
163+ function resetToLoadingState ( ) {
164+ [ 'broker-loading' , 'worker-loading' ] . forEach ( function ( id ) { $ ( '#' + id ) . show ( ) ; } ) ;
165+ [ 'broker-status-badge' , 'worker-status-badge' ] . forEach ( function ( id ) { $ ( '#' + id ) . hide ( ) ; } ) ;
166+ $ ( '#celery-queue-msg' ) . html ( '<i class="fa-solid fa-spinner fa-spin text-muted"></i>' ) ;
167+ $ ( '#purge-queue-btn' ) . prop ( 'disabled' , true ) . attr ( 'title' , 'Loading...' ) ;
168+ $ ( '#view-details-btn' ) . prop ( 'disabled' , true ) . attr ( 'title' , 'Loading...' ) ;
169+ }
170+
104171 function loadCeleryStatus ( ) {
172+ resetToLoadingState ( ) ;
105173 $ . get ( statusUrl ) . done ( renderCeleryStatus ) . fail ( function ( ) {
106- $ ( '#celery-status-badge' ) . text ( 'Error' ) . removeClass ( 'label-default label-success' ) . addClass ( 'label-danger' ) ;
107- $ ( '#celery-status-msg' ) . text ( 'Failed to load Celery status.' ) ;
174+ [ 'broker-loading' , 'worker-loading' ] . forEach ( function ( id ) { $ ( '#' + id ) . hide ( ) ; } ) ;
175+ setStatusBadge ( 'broker-loading' , 'broker-status-badge' , false , '' , 'Error' ) ;
176+ setStatusBadge ( 'worker-loading' , 'worker-status-badge' , false , '' , 'Error' ) ;
177+ $ ( '#celery-queue-msg' ) . text ( 'Failed to load Celery status.' ) ;
108178 } ) ;
109179 }
110180
181+ var MONTHS = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
182+ function formatExpiry ( isoStr ) {
183+ if ( ! isoStr ) return '—' ;
184+ var d = new Date ( isoStr ) ;
185+ var hh = ( '0' + d . getHours ( ) ) . slice ( - 2 ) ;
186+ var mm = ( '0' + d . getMinutes ( ) ) . slice ( - 2 ) ;
187+ var ss = ( '0' + d . getSeconds ( ) ) . slice ( - 2 ) ;
188+ return d . getDate ( ) + ' ' + MONTHS [ d . getMonth ( ) ] + ' ' + hh + ':' + mm + ':' + ss ;
189+ }
190+
191+ function timeLeft ( isoStr ) {
192+ if ( ! isoStr ) return '' ;
193+ var ms = new Date ( isoStr ) - Date . now ( ) ;
194+ if ( ms <= 0 ) return ' <span class="label label-danger">expired</span>' ;
195+ var s = Math . floor ( ms / 1000 ) ;
196+ var parts = [ ] ;
197+ var d = Math . floor ( s / 86400 ) ; if ( d ) { parts . push ( d + 'd' ) ; s -= d * 86400 ; }
198+ var h = Math . floor ( s / 3600 ) ; if ( h ) { parts . push ( h + 'h' ) ; s -= h * 3600 ; }
199+ var m = Math . floor ( s / 60 ) ; if ( m ) { parts . push ( ( '0' + m ) . slice ( - 2 ) + 'm' ) ; s -= m * 60 ; }
200+ if ( ! parts . length ) parts . push ( s + 's' ) ;
201+ return ' <span class="text-muted">(' + parts . join ( ' ' ) + ' left)</span>' ;
202+ }
203+
204+ function renderQueueDetails ( tasks ) {
205+ var tbody = $ ( '#queue-details-tbody' ) . empty ( ) ;
206+ if ( tasks . length === 0 ) {
207+ tbody . append ( '<tr><td colspan="7" class="text-muted text-center">Queue is empty.</td></tr>' ) ;
208+ } else {
209+ tasks . forEach ( function ( t , idx ) {
210+ var oldest = '#' + t . oldest_position + ( t . oldest_eta ? ' <span class="text-muted">(' + t . oldest_eta + ')</span>' : '' ) ;
211+ var newest = '#' + t . newest_position + ( t . newest_eta ? ' <span class="text-muted">(' + t . newest_eta + ')</span>' : '' ) ;
212+ var expires = '—' ;
213+ if ( t . earliest_expires || t . latest_expires ) {
214+ expires = '<span class="text-muted">Oldest:</span> ' + formatExpiry ( t . earliest_expires ) + timeLeft ( t . earliest_expires ) +
215+ '<br><span class="text-muted">Newest:</span> ' + formatExpiry ( t . latest_expires ) + timeLeft ( t . latest_expires ) ;
216+ }
217+ var safeTaskName = $ ( '<span>' ) . text ( t . task_name ) . html ( ) ;
218+ var purgeBtn = '<button class="btn btn-danger btn-xs task-purge-btn" data-task-name="' + safeTaskName + '">Purge</button>' ;
219+ tbody . append (
220+ '<tr>' +
221+ '<td>' + ( idx + 1 ) + '</td>' +
222+ '<td><code>' + safeTaskName + '</code></td>' +
223+ '<td>' + t . count + '</td>' +
224+ '<td>' + oldest + '</td>' +
225+ '<td>' + newest + '</td>' +
226+ '<td>' + expires + '</td>' +
227+ '<td>' + purgeBtn + '</td>' +
228+ '</tr>'
229+ ) ;
230+ } ) ;
231+ }
232+ $ ( '#queue-details-container' ) . show ( ) ;
233+ }
234+
111235 $ ( function ( ) {
112236 loadCeleryStatus ( ) ;
113237
@@ -126,6 +250,41 @@ <h4>Celery <span id="celery-status-badge" class="label label-default">Loading...
126250 loadCeleryStatus ( ) ;
127251 } ) ;
128252 } ) ;
253+
254+ $ ( '#queue-details-tbody' ) . on ( 'click' , '.task-purge-btn' , function ( ) {
255+ var btn = $ ( this ) ;
256+ var taskName = btn . data ( 'task-name' ) ;
257+ if ( ! confirm ( 'Purge all pending "' + taskName + '" tasks from the queue? This cannot be undone.' ) ) return ;
258+ btn . prop ( 'disabled' , true ) . html ( '<i class="fa-solid fa-spinner fa-spin"></i>' ) ;
259+ $ . ajax ( {
260+ url : taskPurgeUrl ,
261+ method : 'POST' ,
262+ contentType : 'application/json' ,
263+ data : JSON . stringify ( { task_name : taskName } ) ,
264+ headers : { 'X-CSRFToken' : '{{ csrf_token }}' } ,
265+ } ) . done ( function ( data ) {
266+ btn . closest ( 'tr' ) . fadeOut ( 300 , function ( ) { $ ( this ) . remove ( ) ; } ) ;
267+ var remaining = parseInt ( $ ( '#celery-queue-msg' ) . text ( ) ) - data . purged ;
268+ if ( ! isNaN ( remaining ) ) {
269+ $ ( '#celery-queue-msg' ) . text ( remaining + ' task(s) waiting to be processed.' ) ;
270+ }
271+ } ) . fail ( function ( ) {
272+ alert ( 'Purge failed. Check server logs.' ) ;
273+ btn . prop ( 'disabled' , false ) . text ( 'Purge' ) ;
274+ } ) ;
275+ } ) ;
276+
277+ $ ( '#view-details-btn' ) . on ( 'click' , function ( ) {
278+ var btn = $ ( this ) ;
279+ btn . prop ( 'disabled' , true ) . html ( '<i class="fa-solid fa-spinner fa-spin"></i> Loading...' ) ;
280+ $ . get ( detailsUrl ) . done ( function ( data ) {
281+ renderQueueDetails ( data ) ;
282+ btn . prop ( 'disabled' , false ) . text ( 'View Details' ) ;
283+ } ) . fail ( function ( ) {
284+ alert ( 'Failed to load queue details. Check server logs.' ) ;
285+ btn . prop ( 'disabled' , false ) . text ( 'View Details' ) ;
286+ } ) ;
287+ } ) ;
129288 } ) ;
130289 } ) ( ) ;
131290 </ script >
0 commit comments