diff --git a/src/Model/Filter/QueuedJobsCollection.php b/src/Model/Filter/QueuedJobsCollection.php
index dd96904b..c0bcbde2 100644
--- a/src/Model/Filter/QueuedJobsCollection.php
+++ b/src/Model/Filter/QueuedJobsCollection.php
@@ -54,6 +54,55 @@ public function initialize(): void {
return true;
}
+ if ($status === 'pending') {
+ // Waiting to be picked up: never fetched, no failure, due,
+ // and not aborted. Mirrors the dashboard "Pending" card
+ // (totalPending minus running and retriable-failed).
+ $query->where([
+ 'completed IS' => null,
+ 'fetched IS' => null,
+ 'failure_message IS' => null,
+ 'AND' => [
+ [
+ 'OR' => [
+ 'notbefore <=' => new DateTime(),
+ 'notbefore IS' => null,
+ ],
+ ],
+ [
+ 'OR' => [
+ 'status IS' => null,
+ 'status !=' => QueuedJobsTable::STATUS_ABORTED,
+ ],
+ ],
+ ],
+ ]);
+
+ return true;
+ }
+ if ($status === 'running') {
+ // Picked up by a worker and not yet completed or failed.
+ $query->where([
+ 'completed IS' => null,
+ 'fetched IS NOT' => null,
+ 'failure_message IS' => null,
+ ]);
+
+ return true;
+ }
+ if ($status === 'failed') {
+ // Unfinished jobs with a recorded failure: still-retrying
+ // ones and terminally aborted ones alike.
+ $query->where(['completed IS' => null, 'failure_message IS NOT' => null]);
+
+ return true;
+ }
+ if ($status === 'aborted') {
+ // Terminally failed: retries exhausted, will never run again.
+ $query->where(['completed IS' => null, 'status' => QueuedJobsTable::STATUS_ABORTED]);
+
+ return true;
+ }
throw new NotImplementedException('Invalid status type');
},
diff --git a/src/Model/Table/QueuedJobsTable.php b/src/Model/Table/QueuedJobsTable.php
index 51b29c4b..e31df74a 100644
--- a/src/Model/Table/QueuedJobsTable.php
+++ b/src/Model/Table/QueuedJobsTable.php
@@ -327,20 +327,12 @@ public function isQueued(string $reference, ?string $jobTask = null): bool {
* @return int
*/
public function getLength(?string $type = null): int {
- $findConf = [
- 'conditions' => [
- 'completed IS' => null,
- 'OR' => [
- 'notbefore <=' => new DateTime(),
- 'notbefore IS' => null,
- ],
- ],
- ];
+ $conditions = $this->pendingConditions();
if ($type !== null) {
- $findConf['conditions']['job_task'] = $type;
+ $conditions['job_task'] = $type;
}
- return $this->find('all', ...$findConf)->count();
+ return $this->find('all', conditions: $conditions)->count();
}
/**
diff --git a/templates/Admin/Queue/index.php b/templates/Admin/Queue/index.php
index 177c971d..5ba9d6fe 100644
--- a/templates/Admin/Queue/index.php
+++ b/templates/Admin/Queue/index.php
@@ -196,6 +196,7 @@
'count' => $scheduledJobs,
'icon' => 'calendar',
'color' => 'info',
+ 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'scheduled']]),
]) ?>
@@ -204,6 +205,7 @@
'count' => $pendingJobs,
'icon' => 'clock',
'color' => 'warning',
+ 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'pending']]),
]) ?>
@@ -212,6 +214,7 @@
'count' => $runningJobs,
'icon' => 'spinner',
'color' => 'primary',
+ 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'running']]),
]) ?>
@@ -220,6 +223,7 @@
'count' => $failedJobs,
'icon' => 'times-circle',
'color' => 'danger',
+ 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'failed']]),
]) ?>
diff --git a/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php b/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php
index 5c933724..6d13cc26 100644
--- a/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php
+++ b/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php
@@ -262,6 +262,39 @@ public function testIndexSearch() {
$this->assertNotContains('bar', $jobTasks);
}
+ /**
+ * The status filter supports running, failed and aborted in addition to
+ * completed/in_progress/scheduled, so the dashboard stat cards can link to
+ * the matching job list.
+ *
+ * @return void
+ */
+ public function testIndexSearchByRunningFailedAborted() {
+ $this->createJob(['job_task' => 'waiting']);
+ $this->createJob(['job_task' => 'running', 'fetched' => new DateTime('-1 minute')]);
+ $this->createJob(['job_task' => 'failing', 'failure_message' => 'boom', 'attempts' => 1]);
+ $this->createJob(['job_task' => 'dead', 'failure_message' => 'boom', 'attempts' => 3, 'status' => 'aborted']);
+
+ $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'pending']]);
+ $this->assertResponseCode(200);
+ $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList();
+ $this->assertSame(['waiting'], $tasks);
+
+ $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'running']]);
+ $this->assertResponseCode(200);
+ $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList();
+ $this->assertSame(['running'], $tasks);
+
+ $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'failed']]);
+ $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList();
+ sort($tasks);
+ $this->assertSame(['dead', 'failing'], $tasks);
+
+ $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'aborted']]);
+ $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList();
+ $this->assertSame(['dead'], $tasks);
+ }
+
/**
* @return void
*/
diff --git a/tests/TestCase/Model/Table/QueuedJobsTableTest.php b/tests/TestCase/Model/Table/QueuedJobsTableTest.php
index 57412c85..fe78a588 100644
--- a/tests/TestCase/Model/Table/QueuedJobsTableTest.php
+++ b/tests/TestCase/Model/Table/QueuedJobsTableTest.php
@@ -903,6 +903,26 @@ public function testGetPendingCountExcludesAborted() {
$this->assertNotContains('aborted-one', $statsRefs);
}
+ /**
+ * getLength() backs the dashboard "Pending Jobs (new/current)" card; it must
+ * exclude aborted jobs so the card total stays consistent with the pending
+ * list (getPendingStats()), which already excludes them.
+ *
+ * @return void
+ */
+ public function testGetLengthExcludesAborted() {
+ $job = $this->QueuedJobs->newEntity([
+ 'key' => 'key',
+ 'job_task' => 'FooBar',
+ 'reference' => 'len-one',
+ ]);
+ $this->QueuedJobs->saveOrFail($job);
+ $this->assertSame(1, $this->QueuedJobs->getLength());
+
+ $this->QueuedJobs->markJobAborted($job);
+ $this->assertSame(0, $this->QueuedJobs->getLength());
+ }
+
/**
* Resetting an aborted job for rerun must clear its terminal status so it
* counts as pending again and gets picked up.