From 6f59200e2e0a9c11fbf80c41c352456517bf9cd3 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 17 Mar 2026 22:15:53 +0100 Subject: [PATCH 01/20] Add modern isolated admin dashboard with Bootstrap 5 - Create QueueAppController as isolated base for admin controllers - Add queue.php layout with Bootstrap 5.3.3 and Font Awesome 6.7.2 via CDN - Add reusable elements: sidebar, stats_card, status_badge, flash messages - Update admin templates with card-based layout and modern styling - Add new configuration options: adminLayout, dashboardAutoRefresh, ignoreAuthorization - Update icon fallback to use Font Awesome 6 icons - Add documentation for admin dashboard features --- docs/README.md | 1 + docs/sections/admin_dashboard.md | 123 +++ src/Controller/Admin/QueueAppController.php | 51 ++ src/Controller/Admin/QueueController.php | 53 +- .../Admin/QueueProcessesController.php | 14 +- src/Controller/Admin/QueuedJobsController.php | 6 +- templates/Admin/Queue/index.php | 698 +++++++++++------- templates/Admin/Queue/processes.php | 154 ++-- templates/Admin/QueueProcesses/index.php | 170 +++-- templates/Admin/QueuedJobs/index.php | 337 +++++---- templates/Admin/QueuedJobs/view.php | 468 ++++++++---- templates/element/Queue/sidebar.php | 92 +++ templates/element/Queue/stats_card.php | 45 ++ templates/element/Queue/status_badge.php | 37 + templates/element/flash/error.php | 20 + templates/element/flash/info.php | 20 + templates/element/flash/success.php | 20 + templates/element/flash/warning.php | 20 + templates/element/icon.php | 23 +- templates/element/ok.php | 6 +- templates/element/yes_no.php | 6 +- templates/layout/queue.php | 410 ++++++++++ 22 files changed, 2086 insertions(+), 688 deletions(-) create mode 100644 docs/sections/admin_dashboard.md create mode 100644 src/Controller/Admin/QueueAppController.php create mode 100644 templates/element/Queue/sidebar.php create mode 100644 templates/element/Queue/stats_card.php create mode 100644 templates/element/Queue/status_badge.php create mode 100644 templates/element/flash/error.php create mode 100644 templates/element/flash/info.php create mode 100644 templates/element/flash/success.php create mode 100644 templates/element/flash/warning.php create mode 100644 templates/layout/queue.php diff --git a/docs/README.md b/docs/README.md index f2f3e184..8624e564 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ * [Custom Tasks](sections/custom_tasks.md) ## Detailed documentation +* [Admin Dashboard](sections/admin_dashboard.md) [**NEW**] * [Configuration](sections/configuration.md) * [Cron](sections/cron.md) for Cronjob management * [Real-Time Progress](sections/realtime_progress.md) with Mercure/FrankenPHP diff --git a/docs/sections/admin_dashboard.md b/docs/sections/admin_dashboard.md new file mode 100644 index 00000000..7b4ea6cb --- /dev/null +++ b/docs/sections/admin_dashboard.md @@ -0,0 +1,123 @@ +# Admin Dashboard + +The Queue plugin includes a modern, self-contained admin dashboard for managing your queued jobs. The dashboard is completely isolated from your application's CSS/JS, using Bootstrap 5 and Font Awesome 6 via CDN. + +## Features + +- **Statistics Overview**: Real-time counts of pending, scheduled, running, and failed jobs +- **Status Banner**: Visual indicator showing whether the queue is running or idle +- **Job Management**: View, reset, and remove jobs directly from the dashboard +- **Worker Management**: Monitor active workers and terminate them if needed +- **Process History**: Track all queue processes with pagination +- **Trigger Jobs**: Manually add jobs that implement `AddFromBackendInterface` +- **Configuration View**: See current runtime configuration at a glance + +## Enabling the Isolated Layout + +By default, the admin controllers use your application's layout. To enable the isolated Bootstrap 5 layout, add this to your configuration: + +```php +// In config/app.php or config/app_local.php +'Queue' => [ + 'adminLayout' => 'Queue.queue', + // other Queue config... +], +``` + +## Configuration Options + +### Layout Configuration + +```php +'Queue' => [ + // Enable the isolated Bootstrap 5 layout + 'adminLayout' => 'Queue.queue', + + // Auto-refresh dashboard every N seconds (0 = disabled) + 'dashboardAutoRefresh' => 30, + + // Skip authorization checks in admin controllers (useful for development) + 'ignoreAuthorization' => true, +], +``` + +### Authorization + +If you're using the CakePHP Authorization plugin and want to skip authorization checks for the Queue admin controllers, you can set: + +```php +'Queue' => [ + 'ignoreAuthorization' => true, +], +``` + +This is useful during development or when the admin section is already protected by other means (e.g., IP restriction, basic auth). + +## Accessing the Dashboard + +Navigate to `/admin/queue` to access the dashboard. The main pages are: + +- `/admin/queue` - Dashboard with overview statistics +- `/admin/queue/processes` - Active workers management +- `/admin/queued-jobs` - Full job listing with search +- `/admin/queue-processes` - Process history + +## Customization + +### Overriding Templates + +You can override any template by creating the same file structure in your application's `templates/plugin/Queue/` directory: + +``` +templates/ +└── plugin/ + └── Queue/ + ├── layout/ + │ └── queue.php + └── Admin/ + └── Queue/ + └── index.php +``` + +### Custom Elements + +The dashboard uses several reusable elements that you can override: + +- `Queue.Queue/sidebar` - Sidebar navigation +- `Queue.Queue/stats_card` - Statistics cards +- `Queue.Queue/status_badge` - Job status badges +- `Queue.flash/success` - Success flash messages +- `Queue.flash/error` - Error flash messages +- `Queue.flash/warning` - Warning flash messages +- `Queue.flash/info` - Info flash messages + +### CSS Variables + +The isolated layout uses CSS variables that you can override: + +```css +:root { + --queue-primary: #0d6efd; + --queue-success: #198754; + --queue-warning: #ffc107; + --queue-danger: #dc3545; + --queue-info: #0dcaf0; + --queue-secondary: #6c757d; + --queue-dark: #212529; + --queue-light: #f8f9fa; + --queue-sidebar-bg: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%); + --queue-sidebar-width: 260px; +} +``` + +## Screenshots + +The dashboard provides: + +1. **Status Banner** - Shows queue status (Running/Idle) with last activity timestamp +2. **Stats Cards** - Quick overview of job counts by status +3. **Pending Jobs Table** - List of pending/running jobs with inline actions +4. **Scheduled Jobs** - Jobs scheduled for future execution +5. **Statistics** - Aggregated statistics for completed jobs +6. **Trigger Jobs** - Buttons to manually trigger addable jobs +7. **Configuration** - Current runtime configuration display diff --git a/src/Controller/Admin/QueueAppController.php b/src/Controller/Admin/QueueAppController.php new file mode 100644 index 00000000..c633aa1a --- /dev/null +++ b/src/Controller/Admin/QueueAppController.php @@ -0,0 +1,51 @@ +loadComponent('Flash'); + + $this->loadHelpers(); + + $layout = Configure::read('Queue.adminLayout'); + if ($layout) { + $this->viewBuilder()->setLayout($layout); + } + } + + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ + public function beforeFilter(EventInterface $event) { + parent::beforeFilter($event); + + if (Configure::read('Queue.ignoreAuthorization') && $this->components()->has('Authorization')) { + /** @var \Authorization\Controller\Component\AuthorizationComponent $authorization */ + $authorization = $this->components()->get('Authorization'); + $authorization->skipAuthorization(); + } + } + +} diff --git a/src/Controller/Admin/QueueController.php b/src/Controller/Admin/QueueController.php index 170a27f7..47e23d69 100644 --- a/src/Controller/Admin/QueueController.php +++ b/src/Controller/Admin/QueueController.php @@ -3,8 +3,8 @@ namespace Queue\Controller\Admin; -use App\Controller\AppController; use Cake\Core\App; +use Cake\Core\Configure; use Cake\Http\Exception\NotFoundException; use Queue\Queue\AddFromBackendInterface; use Queue\Queue\AddInterface; @@ -14,24 +14,13 @@ * @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs * @property \Queue\Model\Table\QueueProcessesTable $QueueProcesses */ -class QueueController extends AppController { - - use LoadHelperTrait; +class QueueController extends QueueAppController { /** * @var string|null */ protected ?string $defaultTable = 'Queue.QueuedJobs'; - /** - * @return void - */ - public function initialize(): void { - parent::initialize(); - - $this->loadHelpers(); - } - /** * Admin center. * Manage queues from admin backend (without the need to open ssh console window). @@ -61,7 +50,43 @@ public function index() { $addableTasks = $taskFinder->allAddable(AddFromBackendInterface::class); $servers = $QueueProcesses->serverList(); - $this->set(compact('new', 'current', 'data', 'pendingDetails', 'scheduledDetails', 'status', 'tasks', 'addableTasks', 'servers')); + $workers = $status ? $status['workers'] : 0; + + $pendingJobs = count($pendingDetails); + $scheduledJobs = count($scheduledDetails); + $runningJobs = $this->QueuedJobs->find() + ->where([ + 'completed IS' => null, + 'fetched IS NOT' => null, + 'failure_message IS' => null, + ]) + ->count(); + $failedJobs = $this->QueuedJobs->find() + ->where([ + 'completed IS' => null, + 'failure_message IS NOT' => null, + ]) + ->count(); + + $configurations = (array)Configure::read('Queue'); + + $this->set(compact( + 'new', + 'current', + 'data', + 'pendingDetails', + 'scheduledDetails', + 'status', + 'tasks', + 'addableTasks', + 'servers', + 'workers', + 'pendingJobs', + 'scheduledJobs', + 'runningJobs', + 'failedJobs', + 'configurations', + )); } /** diff --git a/src/Controller/Admin/QueueProcessesController.php b/src/Controller/Admin/QueueProcessesController.php index bef5b0da..9c2d3cc5 100644 --- a/src/Controller/Admin/QueueProcessesController.php +++ b/src/Controller/Admin/QueueProcessesController.php @@ -3,7 +3,6 @@ namespace Queue\Controller\Admin; -use App\Controller\AppController; use Cake\Core\Configure; use Exception; use const SIGTERM; @@ -13,9 +12,7 @@ * @method \Cake\Datasource\ResultSetInterface<\Queue\Model\Entity\QueueProcess> paginate(\Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface|string|null $object = null, array $settings = []) * @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs */ -class QueueProcessesController extends AppController { - - use LoadHelperTrait; +class QueueProcessesController extends QueueAppController { /** * @var array @@ -26,15 +23,6 @@ class QueueProcessesController extends AppController { ], ]; - /** - * @return void - */ - public function initialize(): void { - parent::initialize(); - - $this->loadHelpers(); - } - /** * Index method * diff --git a/src/Controller/Admin/QueuedJobsController.php b/src/Controller/Admin/QueuedJobsController.php index ada5d711..78b665c2 100644 --- a/src/Controller/Admin/QueuedJobsController.php +++ b/src/Controller/Admin/QueuedJobsController.php @@ -3,7 +3,6 @@ namespace Queue\Controller\Admin; -use App\Controller\AppController; use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\Http\Exception\NotFoundException; @@ -17,9 +16,7 @@ * @method \Cake\Datasource\ResultSetInterface<\Queue\Model\Entity\QueuedJob> paginate($object = null, array $settings = []) * @property \Search\Controller\Component\SearchComponent $Search */ -class QueuedJobsController extends AppController { - - use LoadHelperTrait; +class QueuedJobsController extends QueueAppController { /** * @var array @@ -37,7 +34,6 @@ public function initialize(): void { parent::initialize(); $this->enableSearch(); - $this->loadHelpers(); } /** diff --git a/templates/Admin/Queue/index.php b/templates/Admin/Queue/index.php index 2269079f..9e48b423 100644 --- a/templates/Admin/Queue/index.php +++ b/templates/Admin/Queue/index.php @@ -1,5 +1,4 @@ $configurations */ + use Cake\Core\Configure; ?> - - -
-

+ + + addMinutes(1)->isFuture(); + $relTime = method_exists($this->Time, 'relLengthOfTime') + ? $this->Time->relLengthOfTime($status['time']) + : $this->Time->timeAgoInWords($status['time']); + ?> +
+
+
+ + + + + + + +
+ +
+ + • + Html->link( + __d('queue', '{0} worker(s)', $workers), + ['action' => 'processes'], + ['class' => 'text-decoration-none'] + ) ?> + • + +
+
+
+
+ Html->link( + '' . __d('queue', 'Manage Workers'), + ['action' => 'processes'], + ['class' => 'btn btn-sm btn-outline-dark', 'escapeTitle' => false] + ) ?> +
+
+
+ +
+ + +
+ + + +
+
+ element('Queue.Queue/stats_card', [ + 'title' => __d('queue', 'Pending'), + 'count' => $pendingJobs, + 'icon' => 'clock', + 'color' => 'warning', + ]) ?> +
+
+ element('Queue.Queue/stats_card', [ + 'title' => __d('queue', 'Scheduled'), + 'count' => $scheduledJobs, + 'icon' => 'calendar', + 'color' => 'info', + ]) ?> +
+
+ element('Queue.Queue/stats_card', [ + 'title' => __d('queue', 'Running'), + 'count' => $runningJobs, + 'icon' => 'spinner', + 'color' => 'primary', + ]) ?> +
+
+ element('Queue.Queue/stats_card', [ + 'title' => __d('queue', 'Failed'), + 'count' => $failedJobs, + 'icon' => 'times-circle', + 'color' => 'danger', + ]) ?> +
+
-
- -

- - addMinutes(1)->isFuture(); - ?> - Time, 'relLengthOfTime') - ? $this->Time->relLengthOfTime($status['time']) - : $this->Time->timeAgoInWords($status['time']); - ?> - element('Queue.yes_no', ['value' => $running]); ?> () - - Currently ' . $this->Html->link($status['workers'] . ' worker(s)', ['action' => 'processes']) . ' total.
'; - ?> - ' . count($servers) . ' CLI server(s): ' . implode(', ', $servers) . '
'; - ?> - - - n/a - - -

-

- -

-
    - ' . $this->Html->link($pendingJob->job_task, ['controller' => 'QueuedJobs', 'action' => 'view', $pendingJob->id]) . ' (ref ' . h($pendingJob->reference ?: '-') . ', prio ' . $pendingJob->priority . '):'; - echo '
      '; - - $reset = ''; - if ($this->Queue->hasFailed($pendingJob)) { - $reset = ' ' . $this->Form->postLink(__d('queue', 'Soft reset'), ['action' => 'resetJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button primary btn margin btn-primary']); - $reset .= ' ' . $this->Form->postLink(__d('queue', 'Remove'), ['action' => 'removeJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']); - } elseif ($pendingJob->fetched) { - $reset .= ' ' . $this->Form->postLink(__d('queue', 'Remove'), ['action' => 'removeJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']); - } - - $notBefore = ''; - if ($pendingJob->notbefore) { - $notBefore = ' (' . __d('queue', 'scheduled {0}', $this->Time->nice($pendingJob->notbefore)) . ')'; - } - - echo '
    • ' . __d('queue', 'Created') . ': ' . $this->Time->nice($pendingJob->created) . $notBefore . '
    • '; - - if ($pendingJob->fetched) { - echo '
    • ' . __d('queue', 'Fetched') . ': ' . $this->Time->nice($pendingJob->fetched) . '
    • '; - - $status = ''; - if ($pendingJob->status) { - $status = ' (' . __d('queue', 'status') . ': ' . h($pendingJob->status) . ')'; - } - - if (!$pendingJob->failure_message) { - echo '
    • '; - echo __d('queue', 'Progress') . ': '; - echo $this->QueueProgress->progress($pendingJob) . $status; - $textProgressBar = $this->QueueProgress->progressBar($pendingJob, 18); - echo '
      ' . $this->QueueProgress->htmlProgressBar($pendingJob, $textProgressBar); - echo '
    • '; - } else { - echo '
    • ' . $this->Queue->failureStatus($pendingJob) . ''; - echo '
      ' . __d('queue', 'Attempts') . ': ' . $this->Queue->attempts($pendingJob) . $reset . '
      '; - echo '
    • '; - if ($pendingJob->failure_message) { - echo '
    • ' . __d('queue', 'Failure Message') . ': ' . $this->Text->truncate($pendingJob->failure_message, 200) . '
    • '; - } - } - } - - echo '
    '; - echo ''; - } - ?> -
- -

- -

-
    - ' . $this->Html->link($pendingJob->job_task, ['controller' => 'QueuedJobs', 'action' => 'view', $pendingJob->id]) . ' (ref ' . h($pendingJob->reference ?: '-') . ', prio ' . $pendingJob->priority . '):'; - echo '
      '; - - $reset = ''; - if ($this->Queue->hasFailed($pendingJob)) { - $reset = ' ' . $this->Form->postLink(__d('queue', 'Soft reset'), ['action' => 'resetJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button primary btn margin btn-primary']); - $reset .= ' ' . $this->Form->postLink(__d('queue', 'Remove'), ['action' => 'removeJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']); - } elseif ($pendingJob->fetched) { - $reset .= ' ' . $this->Form->postLink(__d('queue', 'Remove'), ['action' => 'removeJob', $pendingJob->id], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']); - } - - $notBefore = ''; - if ($pendingJob->notbefore) { - $notBefore = ' (' . __d('queue', 'scheduled {0}', $this->Time->nice($pendingJob->notbefore)) . ')'; - } - - echo '
    • ' . __d('queue', 'Created') . ': ' . $this->Time->nice($pendingJob->created) . $notBefore . '
    • '; - - if ($pendingJob->fetched) { - echo '
    • ' . __d('queue', 'Fetched') . ': ' . $this->Time->nice($pendingJob->fetched) . '
    • '; - - $status = ''; - if ($pendingJob->status) { - $status = ' (' . __d('queue', 'status') . ': ' . h($pendingJob->status) . ')'; - } - - if (!$pendingJob->failure_message) { - echo '
    • '; - echo __d('queue', 'Progress') . ': '; - echo $this->QueueProgress->progress($pendingJob) . $status; - $textProgressBar = $this->QueueProgress->progressBar($pendingJob, 18); - echo '
      ' . $this->QueueProgress->htmlProgressBar($pendingJob, $textProgressBar); - echo '
    • '; - } else { - echo '
    • ' . $this->Queue->failureStatus($pendingJob) . ''; - echo '
      ' . __d('queue', 'Attempts') . ': ' . $this->Queue->attempts($pendingJob) . $reset . '
      '; - echo '
    • '; - if ($pendingJob->failure_message) { - echo '
    • ' . __d('queue', 'Failure Message') . ': ' . $this->Text->truncate($pendingJob->failure_message, 200) . '
    • '; - } - } - } - - echo '
    '; - echo ''; - } - ?> -
- -

-
    - ' . h($row['job_task']) . ':'; - echo '
      '; - echo '
    • Finished Jobs in Database: ' . $row['num'] . '
    • '; - echo '
    • Average Job existence: ' . $row['alltime'] . 's
    • '; - echo '
    • Average Execution delay: ' . $row['fetchdelay'] . 's
    • '; - echo '
    • Average Execution time: ' . $row['runtime'] . 's
    • '; - echo '
    '; - echo ''; - } - if (empty($data)) { - echo 'n/a'; - } - ?> -
- - -

Html->link(__d('queue', 'Detailed Statistics'), ['controller' => 'QueuedJobs', 'action' => 'stats']); ?>

- + +
+ +
+
+ (/) + Html->link( + __d('queue', 'View All'), + ['controller' => 'QueuedJobs', 'action' => 'index'], + ['class' => 'btn btn-sm btn-outline-primary'] + ) ?> +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ Html->link( + h($pendingJob->job_task), + ['controller' => 'QueuedJobs', 'action' => 'view', $pendingJob->id], + ['class' => 'text-decoration-none fw-medium'] + ) ?> + + reference ?: '-') ?> + + Time->nice($pendingJob->created) ?> + notbefore): ?> +
Time->nice($pendingJob->notbefore)) ?> + +
+ Queue->hasFailed($pendingJob)): ?> + + + +
: Queue->attempts($pendingJob) ?>
+ fetched): ?> + + + + failure_message): ?> +
+ QueueProgress->htmlProgressBar($pendingJob, $this->QueueProgress->progressBar($pendingJob, 18)) ?> +
+ + + + + + +
+ Queue->hasFailed($pendingJob)): ?> + Form->postLink( + '', + ['action' => 'resetJob', $pendingJob->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-primary', + 'confirm' => __d('queue', 'Sure?'), + 'title' => __d('queue', 'Reset'), + 'block' => true, + ] + ) ?> + Form->postLink( + '', + ['action' => 'removeJob', $pendingJob->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-danger', + 'confirm' => __d('queue', 'Sure?'), + 'title' => __d('queue', 'Remove'), + 'block' => true, + ] + ) ?> + fetched): ?> + Form->postLink( + '', + ['action' => 'removeJob', $pendingJob->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-danger', + 'confirm' => __d('queue', 'Sure?'), + 'title' => __d('queue', 'Remove'), + 'block' => true, + ] + ) ?> + +
+
+ +
+ +

+
+ +
+
+ + + +
+
+ () +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ Html->link( + h($scheduledJob->job_task), + ['controller' => 'QueuedJobs', 'action' => 'view', $scheduledJob->id] + ) ?> + reference ?: '-') ?> + Time->nice($scheduledJob->notbefore) ?> + notbefore): ?> +
+ Time, 'relLengthOfTime') + ? $this->Time->relLengthOfTime($scheduledJob->notbefore) + : $this->Time->timeAgoInWords($scheduledJob->notbefore) ?> +
+ +
+ Form->postLink( + '', + ['action' => 'removeJob', $scheduledJob->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-danger', + 'confirm' => __d('queue', 'Sure?'), + 'title' => __d('queue', 'Remove'), + 'block' => true, + ] + ) ?> +
+
+
+
+ + + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
sss
+
+
+ + + +
+
-
- -

Settings

- Server: -
    -
  • - posix extension enabled (optional, recommended): element('Queue.yes_no', ['value' => function_exists('posix_kill')]); ?> -
  • -
- - Current runtime configuration: -
    - No configuration found'; - } - - $timeConfigKeys = [ - 'workerLifetime', - 'workerPhpTimeout', - 'defaultRequeueTimeout', - 'cleanuptimeout', - 'sleeptime', - 'workermaxruntime', - 'workertimeout', - 'defaultworkertimeout', - ]; - foreach ($configurations as $key => $configuration) { - echo '
  • '; - if (is_string($configuration) && is_dir($configuration)) { - $configuration = str_replace(ROOT . DS, 'ROOT' . DS, $configuration); - $configuration = str_replace(DS, '/', $configuration); - } elseif (is_bool($configuration)) { - $configuration = $configuration ? 'true' : 'false'; - } elseif (is_array($configuration)) { - $configuration = implode(', ', $configuration); - } elseif (is_int($configuration) && in_array($key, $timeConfigKeys, true)) { - $configuration = $this->Queue->secondsToHumanReadable($configuration); - } - echo h($key) . ': ' . h($configuration); - echo '
  • '; - } - - ?> -
- -

Trigger Jobs

-

These jobs implement the AddFromBackendInterface

-
    - $className) { - if (substr($task, 0, 6) === 'Queue.' && (substr($task, -7) === 'Example' || $task === 'Queue.Execute')) { - continue; - } - - echo '
  • '; - echo $this->Form->postLink($task, ['action' => 'addJob', '?' => ['task' => $task]], ['confirm' => 'Sure?']); - echo '
  • '; - } - ?> -
-

Jobs just implementing AddInterface can be added from CLI instead.

- -

Trigger Test/Demo Jobs

-
    - $className) { - if (substr($task, -7) !== 'Example') { - continue; - } - - echo '
  • '; - echo $this->Form->postLink($task, ['action' => 'addJob', '?' => ['task' => $task]], ['confirm' => 'Sure?']); - echo '
  • '; - } - ?> -
- -

Html->link(__d('queue', 'Trigger Delayed Test/Demo Job'), ['controller' => 'QueuedJobs', 'action' => 'test']); ?>

- -

Html->link(__d('queue', 'Trigger Execute Job(s)'), ['controller' => 'QueuedJobs', 'action' => 'execute']); ?>

- + +
+ +
+
+ +
+
+

+ +
+ $className): ?> + + Form->postLink( + '' . h($task), + ['action' => 'addJob', '?' => ['task' => $task]], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-outline-primary btn-sm text-start', + 'confirm' => __d('queue', 'Sure?'), + 'block' => true, + ] + ) ?> + +
+ +

+ +
+

+
+ $className): ?> + + Form->postLink( + '' . h($task), + ['action' => 'addJob', '?' => ['task' => $task]], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-outline-secondary btn-sm text-start', + 'confirm' => __d('queue', 'Sure?'), + 'block' => true, + ] + ) ?> + +
+
+
+ Html->link( + '' . __d('queue', 'Trigger Delayed Test Job'), + ['controller' => 'QueuedJobs', 'action' => 'test'], + ['class' => 'btn btn-outline-info btn-sm', 'escapeTitle' => false] + ) ?> + + Html->link( + '' . __d('queue', 'Execute Job'), + ['controller' => 'QueuedJobs', 'action' => 'execute'], + ['class' => 'btn btn-outline-warning btn-sm', 'escapeTitle' => false] + ) ?> + +
+
+
+ + +
+
+ +
+
+
+ +
    +
  • + posix + element('Queue.yes_no', ['value' => function_exists('posix_kill')]) ?> +
  • +
+
+
+ + + +
    + $configuration): ?> +
  • + + + Queue->secondsToHumanReadable($configuration); + } + echo '' . h($configuration) . ''; + ?> + +
  • + +
+ +

+ +
+
+
- -
diff --git a/templates/Admin/Queue/processes.php b/templates/Admin/Queue/processes.php index 7b6378ab..ba35f196 100644 --- a/templates/Admin/Queue/processes.php +++ b/templates/Admin/Queue/processes.php @@ -11,57 +11,119 @@ ?> +

+ + +

- + +
+
+ + +
+
+ +
+ +
+
+
+
+ + PID: pid) ?> +
+ +
-
-

+
+ : + active_job): ?> + Html->link( + h($process->active_job->job_task), + ['controller' => 'QueuedJobs', 'action' => 'view', $process->active_job->id], + ['class' => 'text-decoration-none'] + ) ?> + + + +
-

-

:

+
+ + : Time->nice(new DateTime($process->modified)) ?> +
-
    -' . $process->pid . ':'; - echo '
      '; - echo '
    • Current active job: ' . ($process->active_job ? $this->Html->link($process->active_job->job_task, [ - 'controller' => 'QueuedJobs', - 'action' => 'view', - $process->active_job->id - ]) : 'Currently no job is being processed by this worker') . '
    • '; - echo '
    • Last run: ' . $this->Time->nice(new DateTime($process->modified)) . '
    • '; - - echo '
    • End: ' . $this->Form->postLink(__d('queue', 'Finish current job and end'), ['action' => 'processes', '?' => ['end' => $process->pid]], ['confirm' => 'Sure?', 'class' => 'button secondary btn margin btn-secondary']) . ' (next loop run)
    • '; - if ($process->workerkey === $key || !$this->Configure->read('Queue.multiserver')) { - echo '
    • ' . __d('queue', 'Kill') . ': ' . $this->Form->postLink(__d('queue', 'Soft kill'), ['action' => 'processes', '?' => ['kill' => $process->pid]], ['confirm' => 'Sure?']) . ' (termination SIGTERM = 15)
    • '; - } +
      + Form->postLink( + '' . __d('queue', 'Finish & End'), + ['action' => 'processes', '?' => ['end' => $process->pid]], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-warning', + 'confirm' => __d('queue', 'Sure?'), + 'title' => __d('queue', 'Finish current job and end worker'), + 'block' => true, + ] + ) ?> - echo '
    '; - echo ''; -} -if (empty($processes)) { - echo 'n/a'; -} -?> -
+ workerkey === $key || !$this->Configure->read('Queue.multiserver')): ?> + Form->postLink( + '' . __d('queue', 'Kill'), + ['action' => 'processes', '?' => ['kill' => $process->pid]], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-sm btn-outline-danger', + 'confirm' => __d('queue', 'Sure? This sends SIGTERM to the process.'), + 'title' => __d('queue', 'Send SIGTERM to terminate immediately'), + 'block' => true, + ] + ) ?> + +
+
+
+ +
+ +
+ +

+

+
+ +
+
- -

-

:

-
    - ' . $queuedJob->pid; - echo ''; - } - ?> -
- + + +
+
+ + +
+
+

:

+
    + +
  • + + + PID: pid) ?> + + +
  • + +
+
+
+ + +
+ Html->link( + '' . __d('queue', 'View Process History'), + ['controller' => 'QueueProcesses', 'action' => 'index'], + ['class' => 'btn btn-outline-secondary', 'escapeTitle' => false] + ) ?>
diff --git a/templates/Admin/QueueProcesses/index.php b/templates/Admin/QueueProcesses/index.php index 20889b77..f6550576 100644 --- a/templates/Admin/QueueProcesses/index.php +++ b/templates/Admin/QueueProcesses/index.php @@ -3,66 +3,120 @@ * @var \App\View\AppView $this * @var iterable<\Queue\Model\Entity\QueueProcess> $queueProcesses */ + use Queue\Queue\Config; + ?> - -
-

- - - - - - - - - - - - - - - - - - - - - - - -
Paginator->sort('pid') ?>Paginator->sort('created', __d('queue', 'Started'), ['direction' => 'desc']) ?>Paginator->sort('modified', __d('queue', 'Last Run'), ['direction' => 'desc']) ?>Paginator->sort('terminate', __d('queue', 'Active')) ?>Paginator->sort('server') ?>
- pid) ?> - workerkey && $queueProcess->workerkey !== $queueProcess->pid) { ?> -
workerkey); ?>
- -
- Time->nice($queueProcess->created) ?> - created->addSeconds(Config::workermaxruntime())->isFuture()) { - echo $this->element('Queue.icon', ['name' => 'exclamation-triangle', 'attributes' => ['title' => 'Long running (!)']]); - } ?> - - Time->nice($queueProcess->modified); - if (!$queueProcess->created->addSeconds(Config::defaultworkertimeout())->isFuture()) { - $modified = '' . $modified . ''; - } - echo $modified; - ?> - element('Queue.yes_no', ['value' => !$queueProcess->terminate]) ?>server) ?> - Html->link($this->element('Queue.icon', ['name' => 'view']), ['action' => 'view', $queueProcess->id], ['escapeTitle' => false]); ?> - terminate) { ?> - Form->postLink($this->element('Queue.icon', ['name' => 'times', 'attributes' => ['title' => __d('queue', 'Terminate')]]), ['action' => 'terminate', $queueProcess->id], ['escapeTitle' => false, 'confirm' => __d('queue', 'Are you sure you want to terminate # {0}?', $queueProcess->id)]); ?> - -
+
+

+ + +

+
+ Html->link( + '' . __d('queue', 'Active Workers'), + ['controller' => 'Queue', 'action' => 'processes'], + ['class' => 'btn btn-outline-primary btn-sm', 'escapeTitle' => false] + ) ?> + Form->postLink( + '' . __d('queue', 'Cleanup'), + ['action' => 'cleanup'], + [ + 'class' => 'btn btn-outline-warning btn-sm', + 'escapeTitle' => false, + 'confirm' => __d('queue', 'Sure to remove all outdated ones (>{0}s)?', Config::defaultworkertimeout() * 2), + 'block' => true, + ] + ) ?> +
+
- element('Tools.pagination'); ?> +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('pid', __d('queue', 'PID')) ?>Paginator->sort('created', __d('queue', 'Started'), ['direction' => 'desc']) ?>Paginator->sort('modified', __d('queue', 'Last Run'), ['direction' => 'desc']) ?>Paginator->sort('terminate', __d('queue', 'Active')) ?>Paginator->sort('server', __d('queue', 'Server')) ?>
+ pid) ?> + workerkey && $queueProcess->workerkey !== $queueProcess->pid): ?> +
+ workerkey) ?> +
+ +
+ Time->nice($queueProcess->created) ?> + created->addSeconds(Config::workermaxruntime())->isFuture()): ?> + + + + + + Time->nice($queueProcess->modified); + $isStale = !$queueProcess->modified->addSeconds(Config::defaultworkertimeout())->isFuture(); + ?> + + + + + + + + + element('Queue.yes_no', ['value' => !$queueProcess->terminate]) ?> + + server): ?> + server) ?> + + --- + + +
+ Html->link( + '', + ['action' => 'view', $queueProcess->id], + ['escapeTitle' => false, 'class' => 'btn btn-outline-primary', 'title' => __d('queue', 'View')] + ) ?> + terminate): ?> + Form->postLink( + '', + ['action' => 'terminate', $queueProcess->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-outline-warning', + 'confirm' => __d('queue', 'Are you sure you want to terminate # {0}?', $queueProcess->id), + 'title' => __d('queue', 'Terminate'), + 'block' => true, + ] + ) ?> + +
+
+
+
+
diff --git a/templates/Admin/QueuedJobs/index.php b/templates/Admin/QueuedJobs/index.php index f2063d5a..dbf707c1 100644 --- a/templates/Admin/QueuedJobs/index.php +++ b/templates/Admin/QueuedJobs/index.php @@ -9,144 +9,215 @@ use Cake\Core\Plugin; ?> - -
- - element('Queue.search'); - } - ?> - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +
Paginator->sort('job_task') ?>Paginator->sort('job_group') ?>Paginator->sort('reference') ?>Paginator->sort('created', null, ['direction' => 'desc']) ?>Paginator->sort('notbefore', null, ['direction' => 'desc']) ?>Paginator->sort('fetched', null, ['direction' => 'desc']) ?>Paginator->sort('completed', null, ['direction' => 'desc']) ?>Paginator->sort('attempts') ?>Paginator->sort('status') ?>Paginator->sort('priority', null, ['direction' => 'desc']) ?>
job_task) ?>job_group) ?: '---' ?> - reference) ?: '---' ?> - data) { - $data = $queuedJob->data; - if ($data && !is_array($data)) { - $data = json_decode($queuedJob->data, true); - } - $data = VarExporter::export($data, VarExporter::TRAILING_COMMA_IN_ARRAY); - echo $this->element('Queue.icon', ['name' => 'cubes', 'attributes' => ['title' => $this->Text->truncate($data, 1000)]]); - } - ?> - Time->nice($queuedJob->created) ?> - Time->nice($queuedJob->notbefore) ?> -
- QueueProgress->timeoutProgressBar($queuedJob, 8); ?> - notbefore && $queuedJob->notbefore->isFuture()) { - echo '
'; - echo method_exists($this->Time, 'relLengthOfTime') - ? $this->Time->relLengthOfTime($queuedJob->notbefore) - : $this->Time->timeAgoInWords($queuedJob->notbefore); - echo '
'; - } ?> -
- Time->nice($queuedJob->fetched) ?> +
+

+ + +

+
+ + + Time->nice(new \Cake\I18n\DateTime()) ?> + + Configure->read('debug')): ?> + Html->link( + '' . __d('queue', 'Import'), + ['action' => 'import'], + ['class' => 'btn btn-outline-secondary btn-sm', 'escapeTitle' => false] + ) ?> + +
+
- fetched) { - echo '
'; - echo method_exists($this->Time, 'relLengthOfTime') - ? $this->Time->relLengthOfTime($queuedJob->fetched) - : $this->Time->timeAgoInWords($queuedJob->fetched); - echo '
'; - } ?> +element('Queue.search'); +} +?> - workerkey) { ?> -
workerkey); ?>
- -
- element('Queue.ok', ['value' => $this->Time->nice($queuedJob->completed), 'ok' => (bool)$queuedJob->completed]) ?> - completed) { ?> -
- ' . $this->Time->duration($queuedJob->completed->diff($queuedJob->fetched)) . ''; - ?> -
- -
- element('Queue.ok', ['value' => $this->Queue->attempts($queuedJob), 'ok' => $queuedJob->completed || $queuedJob->attempts < 1]); ?> - - status) ?> - completed && $queuedJob->fetched) { ?> -
- failure_message) { ?> - QueueProgress->progress($queuedJob) ?> -
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + - - + + + + + + - - - -
Paginator->sort('job_task', __d('queue', 'Task')) ?>Paginator->sort('job_group', __d('queue', 'Group')) ?>Paginator->sort('reference', __d('queue', 'Reference')) ?>Paginator->sort('created', __d('queue', 'Created'), ['direction' => 'desc']) ?>Paginator->sort('notbefore', __d('queue', 'Scheduled'), ['direction' => 'desc']) ?>Paginator->sort('fetched', __d('queue', 'Fetched'), ['direction' => 'desc']) ?>Paginator->sort('completed', __d('queue', 'Completed'), ['direction' => 'desc']) ?>Paginator->sort('attempts', __d('queue', 'Attempts')) ?>Paginator->sort('status', __d('queue', 'Status')) ?>Paginator->sort('priority', __d('queue', 'Priority'), ['direction' => 'desc']) ?>
+ job_task) ?> + + job_group): ?> + job_group) ?> + + --- + + + reference): ?> + reference) ?> + + --- + + data): ?> QueueProgress->progressBar($queuedJob, 8); - echo $this->QueueProgress->htmlProgressBar($queuedJob, $textProgressBar); + $data = $queuedJob->data; + if ($data && !is_array($data)) { + $data = json_decode($queuedJob->data, true); + } + $dataStr = VarExporter::export($data, VarExporter::TRAILING_COMMA_IN_ARRAY); ?> - - Queue->failureStatus($queuedJob); ?> - - - - - completed) { ?> - - - - memory) { ?> -
Number->format($queuedJob->memory); ?> MB
- -
Number->format($queuedJob->priority) ?> - Html->link($this->element('Queue.icon', ['name' => 'view']), ['action' => 'view', $queuedJob->id], ['escapeTitle' => false]); ?> + + + + + + Time->nice($queuedJob->created) ?> + + notbefore): ?> + Time->nice($queuedJob->notbefore) ?> + QueueProgress->timeoutProgressBar($queuedJob, 8) ?> + notbefore->isFuture()): ?> +
+ Time, 'relLengthOfTime') + ? $this->Time->relLengthOfTime($queuedJob->notbefore) + : $this->Time->timeAgoInWords($queuedJob->notbefore) ?> +
+ + + --- + +
+ fetched): ?> + Time->nice($queuedJob->fetched) ?> +
+ Time, 'relLengthOfTime') + ? $this->Time->relLengthOfTime($queuedJob->fetched) + : $this->Time->timeAgoInWords($queuedJob->fetched) ?> +
+ workerkey): ?> +
workerkey) ?>
+ + + --- + +
+ completed): ?> + + + Time->nice($queuedJob->completed) ?> + +
+ + Time->duration($queuedJob->completed->diff($queuedJob->fetched)) ?> +
+ + --- + +
+ element('Queue.ok', [ + 'value' => $this->Queue->attempts($queuedJob), + 'ok' => $queuedJob->completed || $queuedJob->attempts < 1, + ]) ?> + + completed): ?> + + + + failure_message): ?> + + + + fetched): ?> + + + + failure_message): ?> +
+ QueueProgress->progress($queuedJob) ?> +
+ QueueProgress->htmlProgressBar($queuedJob, $this->QueueProgress->progressBar($queuedJob, 8)) ?> +
+ + notbefore && $queuedJob->notbefore->isFuture()): ?> + + + + + + + + - completed) { ?> - Html->link($this->element('Queue.icon', ['name' => 'edit']), ['action' => 'edit', $queuedJob->id], ['escapeTitle' => false]); ?> - - Form->postLink($this->element('Queue.icon', ['name' => 'delete']), ['action' => 'delete', $queuedJob->id], ['escapeTitle' => false, 'confirm' => __d('queue', 'Are you sure you want to delete # {0}?', $queuedJob->id)]); ?> -
+ status): ?> +
status) ?>
+ - element('Tools.pagination'); ?> + memory): ?> +
+ + Number->format($queuedJob->memory) ?> MB +
+ +
+ Number->format($queuedJob->priority) ?> + +
+ Html->link( + '', + ['action' => 'view', $queuedJob->id], + ['escapeTitle' => false, 'class' => 'btn btn-outline-primary', 'title' => __d('queue', 'View')] + ) ?> + completed): ?> + Html->link( + '', + ['action' => 'edit', $queuedJob->id], + ['escapeTitle' => false, 'class' => 'btn btn-outline-secondary', 'title' => __d('queue', 'Edit')] + ) ?> + + Form->postLink( + '', + ['action' => 'delete', $queuedJob->id], + [ + 'escapeTitle' => false, + 'class' => 'btn btn-outline-danger', + 'confirm' => __d('queue', 'Are you sure you want to delete # {0}?', $queuedJob->id), + 'title' => __d('queue', 'Delete'), + 'block' => true, + ] + ) ?> +
+
+
+
+ diff --git a/templates/Admin/QueuedJobs/view.php b/templates/Admin/QueuedJobs/view.php index 10f355b3..b20cfc3d 100644 --- a/templates/Admin/QueuedJobs/view.php +++ b/templates/Admin/QueuedJobs/view.php @@ -7,160 +7,330 @@ use Brick\VarExporter\VarExporter; ?> - -
-

ID id) ?>

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
job_task) ?>
job_group) ?: '---' ?>
reference) ?: '---' ?>
Time->nice($queuedJob->created) ?>
- Time->nice($queuedJob->notbefore) ?> -
- QueueProgress->timeoutProgressBar($queuedJob, 18); ?> - notbefore && $queuedJob->notbefore->isFuture()) { - echo '
'; - echo method_exists($this->Time, 'relLengthOfTime') - ? $this->Time->relLengthOfTime($queuedJob->notbefore) - : $this->Time->timeAgoInWords($queuedJob->notbefore); - echo '
'; - } ?> -
- Time->nice($queuedJob->fetched) ?> - fetched) { - echo '
'; - echo __d('queue', 'Delay') . ': ' . $this->Time->duration($queuedJob->fetched->diff($queuedJob->created)); - echo '
'; - } ?> -
- element('Queue.ok', ['value' => $this->Time->nice($queuedJob->completed), 'ok' => (bool)$queuedJob->completed]) ?> - completed) { - echo '
'; - echo __d('queue', 'Duration') . ': ' . $this->Time->duration($queuedJob->completed->diff($queuedJob->fetched)); - echo '
'; - } ?> -
status) ?>
- completed && $queuedJob->fetched) { ?> - failure_message) { ?> - QueueProgress->progress($queuedJob) ?> -
- QueueProgress->progressBar($queuedJob, 18); - echo $this->QueueProgress->htmlProgressBar($queuedJob, $textProgressBar); - ?> - - Queue->failureStatus($queuedJob); ?> - - +
+

+ + #id) ?> +

+
+ completed): ?> + Html->link( + '' . __d('queue', 'Edit'), + ['action' => 'edit', $queuedJob->id], + ['class' => 'btn btn-outline-primary', 'escapeTitle' => false] + ) ?> + + Form->postLink( + '' . __d('queue', 'Clone & Re-run'), + ['action' => 'clone', $queuedJob->id], + [ + 'class' => 'btn btn-outline-success', + 'escapeTitle' => false, + 'confirm' => __d('queue', 'Sure?'), + 'block' => true, + ] + ) ?> + + Html->link( + '' . __d('queue', 'Export'), + ['action' => 'view', $queuedJob->id, '_ext' => 'json', '?' => ['download' => true]], + ['class' => 'btn btn-outline-secondary', 'escapeTitle' => false] + ) ?> + Form->postLink( + '' . __d('queue', 'Delete'), + ['action' => 'delete', $queuedJob->id], + [ + 'class' => 'btn btn-outline-danger', + 'escapeTitle' => false, + 'confirm' => __d('queue', 'Are you sure you want to delete # {0}?', $queuedJob->id), + 'block' => true, + ] + ) ?> +
+
- completed) { ?> - - +
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
job_task) ?>
+ job_group): ?> + job_group) ?> + + --- + +
+ reference): ?> + reference) ?> + + --- + +
Number->format($queuedJob->priority) ?>
+ workerkey): ?> + workerkey) ?> + worker_process): ?> + Html->link( + '', + ['controller' => 'QueueProcesses', 'action' => 'view', $queuedJob->worker_process->id], + ['escapeTitle' => false, 'title' => $queuedJob->worker_process->server ?: $queuedJob->worker_process->pid] + ) ?> + + + --- + +
+
+
- memory) { ?> -
: Number->format($queuedJob->memory); ?> MB
- -
- attempts ? $this->element('Queue.ok', ['value' => $this->Queue->attempts($queuedJob), 'ok' => $queuedJob->completed || $queuedJob->attempts < 1]) : '' ?> - Queue->hasFailed($queuedJob)) { - echo ' ' . $this->Form->postLink(__d('queue', 'Soft reset'), ['controller' => 'Queue', 'action' => 'resetJob', $queuedJob->id], ['confirm' => 'Sure?', 'class' => 'button button-primary btn margin btn-primary']); - } elseif (!$queuedJob->completed && $queuedJob->fetched && $queuedJob->attempts && $queuedJob->failure_message) { - echo ' ' . $this->Form->postLink(__d('queue', 'Force reset'), ['controller' => 'Queue', 'action' => 'resetJob', $queuedJob->id], ['confirm' => 'Sure? This job is currently waiting to be re-queued.', 'class' => 'button button-primary btn margin btn-primary']); - } - ?> -
- workerkey) ?> - worker_process) { ?> - [Html->link($queuedJob->worker_process->server ?: $queuedJob->worker_process->pid, ['controller' => 'QueueProcesses', 'action' => 'view', $queuedJob->worker_process->id]); ?>] - -
Number->format($queuedJob->priority) ?>
-
-
-

- data) { - $data = $queuedJob->data; - if ($data && !is_array($data)) { - $data = json_decode($queuedJob->data, true); - } - echo '
' . h(VarExporter::export($data, VarExporter::TRAILING_COMMA_IN_ARRAY)) . '
'; - } - ?> + +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
Time->nice($queuedJob->created) ?>
+ notbefore): ?> + Time->nice($queuedJob->notbefore) ?> + QueueProgress->timeoutProgressBar($queuedJob, 18) ?> + notbefore->isFuture()): ?> +
+ Time, 'relLengthOfTime') + ? $this->Time->relLengthOfTime($queuedJob->notbefore) + : $this->Time->timeAgoInWords($queuedJob->notbefore) ?> +
+ + + --- + +
+ fetched): ?> + Time->nice($queuedJob->fetched) ?> +
+ : Time->duration($queuedJob->fetched->diff($queuedJob->created)) ?> +
+ + --- + +
+ completed): ?> + + + Time->nice($queuedJob->completed) ?> + +
+ : Time->duration($queuedJob->completed->diff($queuedJob->fetched)) ?> +
+ + --- + +
+
+ + + data): ?> +
+
+ + completed): ?> + Html->link( + '' . __d('queue', 'Edit Data'), + ['action' => 'data', $queuedJob->id], + ['class' => 'btn btn-sm btn-outline-secondary', 'escapeTitle' => false] + ) ?> + +
+
+
data;
+					if ($data && !is_array($data)) {
+						$data = json_decode($queuedJob->data, true);
+					}
+					echo h(VarExporter::export($data, VarExporter::TRAILING_COMMA_IN_ARRAY));
+					?>
+
+
+ + + + output): ?> +
+
+ +
+
+
output) ?>
+
+
+ + + + failure_message): ?> +
+
+ +
+
+ Text->autoParagraph(h($queuedJob->failure_message)) ?> +
+
+
- output) { ?> -
-
-

-
output) ?>
+ + +
+ +
+
+ +
+
+
+ completed): ?> + + + + failure_message): ?> + + + + fetched): ?> + + + + notbefore && $queuedJob->notbefore->isFuture()): ?> + + + + + + + + + + status): ?> +
status) ?>
+ +
+ + + completed && $queuedJob->fetched && !$queuedJob->failure_message): ?> +
+ : +
+ QueueProgress->progress($queuedJob) ?> +
+ QueueProgress->htmlProgressBar($queuedJob, $this->QueueProgress->progressBar($queuedJob, 18)) ?> +
+
+ + + + memory): ?> +
+ : + Number->format($queuedJob->memory) ?> MB +
+ + + +
+ : + element('Queue.ok', [ + 'value' => $this->Queue->attempts($queuedJob), + 'ok' => $queuedJob->completed || $queuedJob->attempts < 1, + ]) ?> +
+ + + Queue->hasFailed($queuedJob)): ?> +
+ Form->postLink( + '' . __d('queue', 'Soft Reset'), + ['controller' => 'Queue', 'action' => 'resetJob', $queuedJob->id], + [ + 'class' => 'btn btn-primary w-100', + 'escapeTitle' => false, + 'confirm' => __d('queue', 'Sure?'), + 'block' => true, + ] + ) ?> + completed && $queuedJob->fetched && $queuedJob->attempts && $queuedJob->failure_message): ?> +
+ Form->postLink( + '' . __d('queue', 'Force Reset'), + ['controller' => 'Queue', 'action' => 'resetJob', $queuedJob->id], + [ + 'class' => 'btn btn-warning w-100', + 'escapeTitle' => false, + 'confirm' => __d('queue', 'Sure? This job is currently waiting to be re-queued.'), + 'block' => true, + ] + ) ?> + +
-
- -
-
-

- failure_message ? $this->Text->autoParagraph(h($queuedJob->failure_message)) : ''; ?> -
-
+ +
+
+ +
+
+ Html->link( + '' . __d('queue', 'Back to Jobs List'), + ['action' => 'index'], + ['class' => 'list-group-item list-group-item-action', 'escapeTitle' => false] + ) ?> + Html->link( + '' . __d('queue', 'Dashboard'), + ['controller' => 'Queue', 'action' => 'index'], + ['class' => 'list-group-item list-group-item-action', 'escapeTitle' => false] + ) ?> +
+
+
diff --git a/templates/element/Queue/sidebar.php b/templates/element/Queue/sidebar.php new file mode 100644 index 00000000..634dc26d --- /dev/null +++ b/templates/element/Queue/sidebar.php @@ -0,0 +1,92 @@ +getRequest()->getParam('controller'); +$action = $this->getRequest()->getParam('action'); + +$isActive = function (string $c, ?array $actions = null) use ($controller, $action): string { + if ($controller !== $c) { + return ''; + } + if ($actions === null) { + return 'active'; + } + + return in_array($action, $actions, true) ? 'active' : ''; +}; +?> + diff --git a/templates/element/Queue/stats_card.php b/templates/element/Queue/stats_card.php new file mode 100644 index 00000000..fef96c84 --- /dev/null +++ b/templates/element/Queue/stats_card.php @@ -0,0 +1,45 @@ + ['bg' => 'bg-primary bg-opacity-10', 'text' => 'text-primary'], + 'success' => ['bg' => 'bg-success bg-opacity-10', 'text' => 'text-success'], + 'warning' => ['bg' => 'bg-warning bg-opacity-10', 'text' => 'text-warning'], + 'danger' => ['bg' => 'bg-danger bg-opacity-10', 'text' => 'text-danger'], + 'info' => ['bg' => 'bg-info bg-opacity-10', 'text' => 'text-info'], + 'secondary' => ['bg' => 'bg-secondary bg-opacity-10', 'text' => 'text-secondary'], +]; + +$classes = $colorClasses[$color] ?? $colorClasses['primary']; +?> + diff --git a/templates/element/Queue/status_badge.php b/templates/element/Queue/status_badge.php new file mode 100644 index 00000000..da1d4ef4 --- /dev/null +++ b/templates/element/Queue/status_badge.php @@ -0,0 +1,37 @@ +completed) { + $status = 'completed'; + $icon = 'check'; +} elseif ($job->failure_message) { + $status = 'failed'; + $icon = 'times'; +} elseif ($job->fetched) { + $status = 'running'; + $icon = 'spinner fa-spin'; +} elseif ($job->notbefore && $job->notbefore->isFuture()) { + $status = 'scheduled'; + $icon = 'calendar'; +} + +$statusLabels = [ + 'pending' => __d('queue', 'Pending'), + 'running' => __d('queue', 'Running'), + 'completed' => __d('queue', 'Completed'), + 'failed' => __d('queue', 'Failed'), + 'scheduled' => __d('queue', 'Scheduled'), +]; +?> + + + + diff --git a/templates/element/flash/error.php b/templates/element/flash/error.php new file mode 100644 index 00000000..ee9b6e15 --- /dev/null +++ b/templates/element/flash/error.php @@ -0,0 +1,20 @@ + + diff --git a/templates/element/flash/info.php b/templates/element/flash/info.php new file mode 100644 index 00000000..705362a7 --- /dev/null +++ b/templates/element/flash/info.php @@ -0,0 +1,20 @@ + + diff --git a/templates/element/flash/success.php b/templates/element/flash/success.php new file mode 100644 index 00000000..df01cd58 --- /dev/null +++ b/templates/element/flash/success.php @@ -0,0 +1,20 @@ + + diff --git a/templates/element/flash/warning.php b/templates/element/flash/warning.php new file mode 100644 index 00000000..41052507 --- /dev/null +++ b/templates/element/flash/warning.php @@ -0,0 +1,20 @@ + + diff --git a/templates/element/icon.php b/templates/element/icon.php index e745f679..d025c2a7 100644 --- a/templates/element/icon.php +++ b/templates/element/icon.php @@ -1,6 +1,6 @@ 'View', - 'edit' => 'Edit', - 'delete' => 'Del', - 'times' => 'X', - 'exclamation-triangle' => '(!)', - 'cubes' => 'Data', + 'view' => ['icon' => 'eye', 'text' => 'View'], + 'edit' => ['icon' => 'edit', 'text' => 'Edit'], + 'delete' => ['icon' => 'trash', 'text' => 'Del'], + 'times' => ['icon' => 'times', 'text' => 'X'], + 'exclamation-triangle' => ['icon' => 'exclamation-triangle', 'text' => '(!)'], + 'cubes' => ['icon' => 'database', 'text' => 'Data'], ]; if ($this->helpers()->has('Icon')) { echo $this->Icon->render($name, $options, $attributes); } else { - $title = $attributes['title'] ?? $fallbackMap[$name] ?? ucfirst($name); - echo '[' . h($fallbackMap[$name] ?? ucfirst($name)) . ']'; + $mapping = $fallbackMap[$name] ?? ['icon' => $name, 'text' => ucfirst($name)]; + $title = $attributes['title'] ?? $mapping['text']; + $class = 'fas fa-' . $mapping['icon']; + if (isset($attributes['class'])) { + $class .= ' ' . $attributes['class']; + } + echo ''; } diff --git a/templates/element/ok.php b/templates/element/ok.php index 55ee5829..0d0373d7 100644 --- a/templates/element/ok.php +++ b/templates/element/ok.php @@ -13,6 +13,10 @@ } elseif ($this->helpers()->has('Format')) { echo $this->Format->ok($value, $ok); } else { - echo $ok ? '' . h($value) . '' : '' . h($value) . ''; + if ($ok) { + echo '' . h($value) . ''; + } else { + echo '' . h($value) . ''; + } } ?> diff --git a/templates/element/yes_no.php b/templates/element/yes_no.php index a0789469..6203e4fd 100644 --- a/templates/element/yes_no.php +++ b/templates/element/yes_no.php @@ -14,6 +14,10 @@ } elseif ($this->helpers()->has('Format')) { echo $this->Format->yesNo($value); } else { - echo $value ? 'Yes' : 'No'; + if ($value) { + echo 'Yes'; + } else { + echo 'No'; + } } ?> diff --git a/templates/layout/queue.php b/templates/layout/queue.php new file mode 100644 index 00000000..0c70a939 --- /dev/null +++ b/templates/layout/queue.php @@ -0,0 +1,410 @@ + + + + + + + <?= $this->fetch('title') ? strip_tags($this->fetch('title')) . ' - ' : '' ?>Queue Admin + + + + + + + + + + fetch('meta') ?> + fetch('css') ?> + + + + + +
+ + element('Queue.Queue/sidebar') ?> + + +
+ +
+ Flash->render() ?> +
+ + fetch('content') ?> +
+
+ + +
+
+ Queue Plugin for CakePHP + + + PHP + +
+
+ + + + + + + fetch('script') ?> + + From b410e2f10c5346793507f0f9359201ee74af2dcd Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 17 Mar 2026 22:22:35 +0100 Subject: [PATCH 02/20] Fix PHPStan error for optional Authorization dependency --- src/Controller/Admin/QueueAppController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Admin/QueueAppController.php b/src/Controller/Admin/QueueAppController.php index c633aa1a..d4980c0c 100644 --- a/src/Controller/Admin/QueueAppController.php +++ b/src/Controller/Admin/QueueAppController.php @@ -42,8 +42,8 @@ public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); if (Configure::read('Queue.ignoreAuthorization') && $this->components()->has('Authorization')) { - /** @var \Authorization\Controller\Component\AuthorizationComponent $authorization */ $authorization = $this->components()->get('Authorization'); + /** @phpstan-ignore method.notFound */ $authorization->skipAuthorization(); } } From 085ba926dabcc3c75532a4d56fb38890e4f7326b Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Tue, 17 Mar 2026 22:49:21 +0100 Subject: [PATCH 03/20] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- templates/element/flash/error.php | 2 +- templates/element/icon.php | 3 ++- templates/layout/queue.php | 12 +++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/templates/element/flash/error.php b/templates/element/flash/error.php index ee9b6e15..182a5636 100644 --- a/templates/element/flash/error.php +++ b/templates/element/flash/error.php @@ -13,7 +13,7 @@ $message = h($message); } ?> -