Skip to content

Commit dd8cf2c

Browse files
committed
Rhythm recorder and widget.
1 parent ece1f8f commit dd8cf2c

3 files changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Scheduling\Recorder;
5+
6+
use Cake\Event\EventListenerInterface;
7+
use Cake\ORM\TableRegistry;
8+
use Rhythm\Event\SharedBeat;
9+
use Rhythm\Recorder\BaseRecorder;
10+
use Rhythm\Recorder\Trait\ThrottlingTrait;
11+
12+
/**
13+
* Scheduled Tasks Recorder
14+
*
15+
* Records monitored scheduled tasks status to Rhythm.
16+
*/
17+
class ScheduledTasksRecorder extends BaseRecorder implements EventListenerInterface
18+
{
19+
use ThrottlingTrait;
20+
21+
/**
22+
* Implemented events.
23+
*
24+
* @return array<string, mixed>
25+
*/
26+
public function implementedEvents(): array
27+
{
28+
if (!$this->isEnabled()) {
29+
return [];
30+
}
31+
32+
return [
33+
SharedBeat::class => 'record',
34+
];
35+
}
36+
37+
/**
38+
* Record scheduled tasks status.
39+
*
40+
* @param mixed $data The shared beat event
41+
* @return void
42+
*/
43+
public function record(mixed $data): void
44+
{
45+
if (!$data instanceof SharedBeat) {
46+
return;
47+
}
48+
49+
if (!$this->isEnabled()) {
50+
return;
51+
}
52+
53+
$this->throttle(15, $data, function (SharedBeat $event): void {
54+
$timestamp = $event->getTimestamp()->getTimestamp();
55+
56+
try {
57+
/** @var \Scheduling\Model\Table\MonitoredScheduledTasksTable $monitoredTasksTable */
58+
$monitoredTasksTable = TableRegistry::getTableLocator()->get('Scheduling.MonitoredScheduledTasks');
59+
$stats = $monitoredTasksTable->getStatistics();
60+
$taskDetails = $monitoredTasksTable->getAllTasksStatusInfo();
61+
$this->rhythm->set(
62+
'scheduled_tasks',
63+
'summary',
64+
json_encode([
65+
'total' => $stats['total'],
66+
'running' => $stats['running'],
67+
'failed' => $stats['failed'],
68+
'completed' => $stats['completed'],
69+
'overdue' => $stats['overdue'],
70+
'timestamp' => $timestamp,
71+
], JSON_THROW_ON_ERROR),
72+
$timestamp
73+
);
74+
75+
$this->rhythm->set(
76+
'scheduled_tasks',
77+
'details',
78+
json_encode($taskDetails, JSON_THROW_ON_ERROR),
79+
$timestamp
80+
);
81+
} catch (\Exception $e) {
82+
debug('ScheduledTasksRecorder error: ' . $e->getMessage());
83+
}
84+
});
85+
}
86+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Scheduling\Widget;
5+
6+
use Exception;
7+
use Rhythm\Widget\BaseWidget;
8+
9+
/**
10+
* Scheduled Tasks Widget
11+
*
12+
* Displays monitored scheduled tasks status from Rhythm data.
13+
*/
14+
class ScheduledTasksWidget extends BaseWidget
15+
{
16+
/**
17+
* Get widget data
18+
*
19+
* @param array $options Widget options
20+
* @return array
21+
*/
22+
public function getData(array $options = []): array
23+
{
24+
return $this->remember(function () {
25+
try {
26+
$summaryValues = $this->rhythm->getStorage()->values('scheduled_tasks', ['summary']);
27+
28+
$summary = [];
29+
30+
if (!empty($summaryValues)) {
31+
$summaryData = $summaryValues->first();
32+
if ($summaryData && $summaryData->value) {
33+
$summary = json_decode($summaryData->value, true) ?: [];
34+
}
35+
}
36+
37+
$detailsValues = $this->rhythm->getStorage()->values('scheduled_tasks', ['details']);
38+
39+
$taskDetails = [];
40+
if (!empty($detailsValues)) {
41+
$detailsData = $detailsValues->first();
42+
if ($detailsData && $detailsData->value) {
43+
$taskDetails = json_decode($detailsData->value, true) ?: [];
44+
}
45+
}
46+
47+
return [
48+
'summary' => $summary,
49+
'tasks' => $taskDetails,
50+
'total_tasks' => $summary['total'] ?? 0,
51+
'running_tasks' => $summary['running'] ?? 0,
52+
'failed_tasks' => $summary['failed'] ?? 0,
53+
'completed_tasks' => $summary['completed'] ?? 0,
54+
'overdue_tasks' => $summary['overdue'] ?? 0,
55+
'last_updated' => $summary['timestamp'] ?? null,
56+
];
57+
} catch (Exception $e) {
58+
return [
59+
'summary' => [],
60+
'tasks' => [],
61+
'total_tasks' => 0,
62+
'running_tasks' => 0,
63+
'failed_tasks' => 0,
64+
'completed_tasks' => 0,
65+
'overdue_tasks' => 0,
66+
'last_updated' => null,
67+
'error' => $e->getMessage(),
68+
];
69+
}
70+
}, 'scheduled_tasks_widget', $this->getRefreshInterval());
71+
}
72+
73+
/**
74+
* Get recorder name
75+
*
76+
* @return string
77+
*/
78+
protected function getRecorderName(): string
79+
{
80+
return 'scheduled_tasks';
81+
}
82+
83+
/**
84+
* Get template name
85+
*
86+
* @return string
87+
*/
88+
public function getTemplate(): string
89+
{
90+
return 'Scheduling.widgets/scheduled_tasks';
91+
}
92+
93+
/**
94+
* Get refresh interval in seconds
95+
*
96+
* @return int
97+
*/
98+
public function getRefreshInterval(): int
99+
{
100+
return $this->getConfigValue('refreshInterval', 30);
101+
}
102+
103+
/**
104+
* Get default icon for this widget
105+
*
106+
* @return string|null
107+
*/
108+
protected function getDefaultIcon(): ?string
109+
{
110+
return 'fas fa-clock';
111+
}
112+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
/**
3+
* Scheduled Tasks Widget
4+
*
5+
* This template displays monitored scheduled tasks status from Rhythm data.
6+
*
7+
* @var \App\View\AppView $this
8+
* @var mixed $config
9+
* @var object $widget
10+
* @var mixed $widgetName
11+
*/
12+
$this->set('widget', $widget);
13+
$this->set('config', $config);
14+
$this->set('widgetName', $widgetName);
15+
16+
17+
/**
18+
* Get CSS class for status badge
19+
*
20+
* @param string $status Task status
21+
* @return string
22+
*/
23+
function getStatusClass($status): string
24+
{
25+
return match ($status) {
26+
'running' => 'info',
27+
'completed' => 'success',
28+
'failed' => 'danger',
29+
'skipped' => 'warning',
30+
default => 'secondary',
31+
};
32+
}
33+
34+
$data = $widget->getData();
35+
$this->set('data', $data);
36+
37+
$this->extend('Rhythm.widgets/widget_base');
38+
39+
$this->start('widget_body');
40+
41+
$tasksData = $data['tasks'] ?? [];
42+
$summary = $data['summary'] ?? [];
43+
44+
if (empty($tasksData)) {
45+
echo $this->element('Rhythm.components/widget_placeholder', [
46+
'message' => 'No monitored scheduled tasks data available.'
47+
]);
48+
} else {
49+
$summaryStats = [
50+
['label' => 'Total', 'value' => $data['total_tasks'] ?? 0],
51+
['label' => 'Running', 'value' => $data['running_tasks'] ?? 0],
52+
['label' => 'Completed', 'value' => $data['completed_tasks'] ?? 0],
53+
['label' => 'Failed', 'value' => $data['failed_tasks'] ?? 0],
54+
['label' => 'Overdue', 'value' => $data['overdue_tasks'] ?? 0],
55+
];
56+
57+
$head = ['Task', 'Type', 'Status', 'Last Started', 'Last Finished', 'Cron'];
58+
$body = [];
59+
60+
foreach ($tasksData as $task) {
61+
$statusClass = getStatusClass($task['status'] ?? 'unknown');
62+
$statusBadge = $this->Rhythm->badge(ucfirst($task['status'] ?? 'unknown'), $statusClass);
63+
64+
if (!empty($task['is_overdue'])) {
65+
$statusBadge .= ' ' . $this->Rhythm->badge('Overdue', 'critical');
66+
}
67+
68+
$taskName = '<code>' . h($task['name'] ?? 'Unknown') . '</code>';
69+
$type = $this->Rhythm->badge(h($task['type'] ?? 'N/A'), 'info');
70+
71+
$body[] = [
72+
$taskName,
73+
$type,
74+
$statusBadge,
75+
$task['last_started'] ?? 'Never',
76+
$task['last_finished'] ?? 'Never',
77+
'<code>' . h($task['cron_expression'] ?? 'N/A') . '</code>',
78+
];
79+
}
80+
81+
$lastUpdated = '';
82+
if (!empty($data['last_updated'])) {
83+
$lastUpdated = '<small class="text-muted">Last updated: ' . date('Y-m-d H:i:s', $data['last_updated']) . '</small>';
84+
}
85+
?>
86+
<div class="widget-content">
87+
<?= $this->Rhythm->summaryStats($summaryStats) ?>
88+
<?= $this->Rhythm->scroll($this->Rhythm->table($head, $body)) ?>
89+
<?= $lastUpdated ?>
90+
</div>
91+
<?php
92+
}
93+
$this->end();
94+

0 commit comments

Comments
 (0)