diff --git a/composer.json b/composer.json
index b0d8c2b..b1a257f 100644
--- a/composer.json
+++ b/composer.json
@@ -20,6 +20,7 @@
"illuminate/bus": "^9.0",
"lorisleiva/cron-translator": "^0.3.0",
"nesbot/carbon": "^2.41.3",
+ "nunomaduro/termwind": "^1.9",
"spatie/laravel-package-tools": "^1.9"
},
"require-dev": {
diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/resources/views/alert.blade.php b/resources/views/alert.blade.php
new file mode 100644
index 0000000..7a2bf24
--- /dev/null
+++ b/resources/views/alert.blade.php
@@ -0,0 +1,3 @@
+
+ {!! $message !!}
+
diff --git a/resources/views/components/duplicate-tasks.blade.php b/resources/views/components/duplicate-tasks.blade.php
new file mode 100644
index 0000000..b24bc16
--- /dev/null
+++ b/resources/views/components/duplicate-tasks.blade.php
@@ -0,0 +1,16 @@
+@props(['tasks'])
+
+
Duplicate Tasks
+
+
These tasks could not be monitored because they have a duplicate name.
+
+
+ @foreach ($tasks as $task)
+
+ @endforeach
+
+
+
+ To monitor these tasks you should add ->monitorName() in the schedule to manually specify a unique name.
+
+
diff --git a/resources/views/components/monitored-tasks.blade.php b/resources/views/components/monitored-tasks.blade.php
new file mode 100644
index 0000000..63de8db
--- /dev/null
+++ b/resources/views/components/monitored-tasks.blade.php
@@ -0,0 +1,63 @@
+@props(['tasks', 'dateFormat', 'usingOhDear'])
+
+
Monitored Tasks
+
+
+ @forelse ($tasks as $task)
+
+
+
+
+
+ ⇁ Started at:
+
+ {{ optional($task->lastRunStartedAt())->format($dateFormat) ?? '--' }}
+
+
+
+ ⇁ Finished at:
+
+ {{ optional($task->lastRunFinishedAt())->format($dateFormat) ?? '--' }}
+
+
+
+
+ ⇁ Failed at:
+
+ {{ optional($task->lastRunFailedAt())->format($dateFormat) ?? '--' }}
+
+
+
+
+ ⇁ Next run:
+ {{ $task->nextRunAt()->format($dateFormat) }}
+
+
+
+ ⇁ Grace time:
+ {{ $task->graceTimeInMinutes() }} minutes
+
+ @if ($usingOhDear)
+
+ ⇁ Registered at Oh Dear:
+ @if ($task->isBeingMonitoredAtOhDear())
+ Yes
+ @else
+ No
+ @endif
+
+ @endif
+
+
+
+ @empty
+
There currently are no tasks being monitored!
+ @endforelse
+
+ @if ($usingOhDear)
+
+ Some tasks are not registered on oh dear. You will not be notified when they do not run on time.
+ Run php artisan schedule-monitor:sync to register them and receive notifications.
+
+ @endif
+
diff --git a/resources/views/components/ready-for-monitoring-tasks.blade.php b/resources/views/components/ready-for-monitoring-tasks.blade.php
new file mode 100644
index 0000000..7bc8fab
--- /dev/null
+++ b/resources/views/components/ready-for-monitoring-tasks.blade.php
@@ -0,0 +1,12 @@
+@props(['tasks'])
+
+
Run sync to start monitoring
+
+
These tasks will be monitored after running: php artisan schedule-monitor:sync
+
+
+ @foreach ($tasks as $task)
+
+ @endforeach
+
+
diff --git a/resources/views/components/task.blade.php b/resources/views/components/task.blade.php
new file mode 100644
index 0000000..266c7dc
--- /dev/null
+++ b/resources/views/components/task.blade.php
@@ -0,0 +1,15 @@
+@props(['task'])
+
+ @if ($task->name())
+ {{ $task->name() }}
+ ({{ $task->type() }})
+ @else
+ {{ $task->type() }}
+ @endif
+
+ {{ str_repeat('.', (new \Termwind\Terminal)->width() - (
+ strlen($task->name() . $task->type() . $task->humanReadableCron()) + ($task->name() && $task->type() ? 9 : 6)
+ )) }}
+
+ {{ $task->humanReadableCron() }}
+
diff --git a/resources/views/components/title.blade.php b/resources/views/components/title.blade.php
new file mode 100644
index 0000000..7629362
--- /dev/null
+++ b/resources/views/components/title.blade.php
@@ -0,0 +1,3 @@
+
+ {{ $slot }}
+
diff --git a/resources/views/components/unnamed-tasks.blade.php b/resources/views/components/unnamed-tasks.blade.php
new file mode 100644
index 0000000..7022342
--- /dev/null
+++ b/resources/views/components/unnamed-tasks.blade.php
@@ -0,0 +1,14 @@
+@props(['tasks'])
+
+
Unnamed Tasks
+
+
These tasks cannot be monitored because no name could be determined for them.
+
+
+ @foreach ($tasks as $task)
+
+ @endforeach
+
+
+
To monitor these tasks you should add ->monitorName() in the schedule to manually specify a name.
+
diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php
new file mode 100644
index 0000000..ba95681
--- /dev/null
+++ b/resources/views/list.blade.php
@@ -0,0 +1,22 @@
+
+
+ @if (! $readyForMonitoringTasks->isEmpty())
+
+ @endif
+ @if (! $unnamedTasks->isEmpty())
+
+ @endif
+ @if (! $duplicateTasks->isEmpty())
+
+ @endif
+
diff --git a/resources/views/sync.blade.php b/resources/views/sync.blade.php
new file mode 100644
index 0000000..9902ee7
--- /dev/null
+++ b/resources/views/sync.blade.php
@@ -0,0 +1,4 @@
+
+
All done! Now monitoring {{ $monitoredScheduledTasksCount }} {{ str()->plural('scheduled task', $monitoredScheduledTasksCount) }}.
+
Run php artisan schedule-monitor:list to see which jobs are now monitored.
+
diff --git a/src/Commands/ListCommand.php b/src/Commands/ListCommand.php
index 4768fa3..bfbaaa9 100644
--- a/src/Commands/ListCommand.php
+++ b/src/Commands/ListCommand.php
@@ -3,10 +3,10 @@
namespace Spatie\ScheduleMonitor\Commands;
use Illuminate\Console\Command;
-use Spatie\ScheduleMonitor\Commands\Tables\DuplicateTasksTable;
-use Spatie\ScheduleMonitor\Commands\Tables\MonitoredTasksTable;
-use Spatie\ScheduleMonitor\Commands\Tables\ReadyForMonitoringTasksTable;
-use Spatie\ScheduleMonitor\Commands\Tables\UnnamedTasksTable;
+use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
+use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
+use function Termwind\render;
+use function Termwind\style;
class ListCommand extends Command
{
@@ -16,11 +16,33 @@ class ListCommand extends Command
public function handle()
{
- (new MonitoredTasksTable($this))->render();
- (new ReadyForMonitoringTasksTable($this))->render();
- (new UnnamedTasksTable($this))->render();
- (new DuplicateTasksTable($this))->render();
+ $dateFormat = config('schedule-monitor.date_format');
+ style('date-width')->apply('w-' . strlen(date($dateFormat)));
- $this->line('');
+ render(view('schedule-monitor::list', [
+ 'monitoredTasks' => ScheduledTasks::createForSchedule()->monitoredTasks(),
+ 'readyForMonitoringTasks' => ScheduledTasks::createForSchedule()->readyForMonitoringTasks(),
+ 'unnamedTasks' => ScheduledTasks::createForSchedule()->unnamedTasks(),
+ 'duplicateTasks' => ScheduledTasks::createForSchedule()->duplicateTasks(),
+ 'usingOhDear' => $this->usingOhDear(),
+ 'dateFormat' => $dateFormat,
+ ]));
+ }
+
+ protected function usingOhDear(): bool
+ {
+ if (! class_exists(OhDear::class)) {
+ return false;
+ }
+
+ if (empty(config('schedule-monitor.oh_dear.api_token'))) {
+ return false;
+ }
+
+ if (empty(config('schedule-monitor.oh_dear.site_id'))) {
+ return false;
+ }
+
+ return true;
}
}
diff --git a/src/Commands/SyncCommand.php b/src/Commands/SyncCommand.php
index e4fd5f6..503a835 100644
--- a/src/Commands/SyncCommand.php
+++ b/src/Commands/SyncCommand.php
@@ -10,6 +10,7 @@
use Spatie\ScheduleMonitor\Support\Concerns\UsesScheduleMonitoringModels;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
+use function Termwind\render;
class SyncCommand extends Command
{
@@ -21,22 +22,27 @@ class SyncCommand extends Command
public function handle()
{
- $this->info('Start syncing schedule...' . PHP_EOL);
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Start syncing schedule...',
+ 'class' => 'text-green'
+ ]));
+
$this
->syncScheduledTasksWithDatabase()
->syncMonitoredScheduledTaskWithOhDear();
$monitoredScheduledTasksCount = $this->getMonitoredScheduleTaskModel()->count();
- $this->info('');
- $this->info('All done! Now monitoring ' . $monitoredScheduledTasksCount . ' ' . Str::plural('scheduled task', $monitoredScheduledTasksCount) . '.');
- $this->info('');
- $this->info('Run `php artisan schedule-monitor:list` to see which jobs are now monitored.');
+ render(view('schedule-monitor::sync', [
+ 'monitoredScheduledTasksCount' => $monitoredScheduledTasksCount,
+ ]));
}
protected function syncScheduledTasksWithDatabase(): self
{
- $this->comment('Start syncing schedule with database...');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Start syncing schedule with database...',
+ ]));
$monitoredScheduledTasks = ScheduledTasks::createForSchedule()
->uniqueTasks()
@@ -68,12 +74,25 @@ protected function syncMonitoredScheduledTaskWithOhDear(): self
$siteId = config('schedule-monitor.oh_dear.site_id');
if (! $siteId) {
- $this->warn('Not syncing schedule with Oh Dear because not `site_id` is not set in the `oh-dear` config file. Learn how to set this up at https://ohdear.app/docs/general/cron-job-monitoring/php#cron-monitoring-in-laravel-php');
+ render(view('schedule-monitor::alert', [
+ 'message' => <<
+ Not syncing schedule with oh dear because not site_id
+ is not set in the oh-dear config file.
+
+
+ HTML,
+ 'class' => 'text-yellow',
+ ]));
return $this;
}
- $this->comment('Start syncing schedule with Oh Dear...');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Start syncing schedule with Oh Dear...',
+ ]));
$monitoredScheduledTasks = $this->getMonitoredScheduleTaskModel()->get();
@@ -91,7 +110,11 @@ protected function syncMonitoredScheduledTaskWithOhDear(): self
->toArray();
$cronChecks = app(OhDear::class)->site($siteId)->syncCronChecks($cronChecks);
- $this->comment('Successfully synced schedule with Oh Dear!');
+
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Successfully synced schedule with Oh Dear!',
+ 'class' => 'text-green',
+ ]));
collect($cronChecks)
->each(
diff --git a/src/Commands/Tables/DuplicateTasksTable.php b/src/Commands/Tables/DuplicateTasksTable.php
deleted file mode 100644
index ea327ba..0000000
--- a/src/Commands/Tables/DuplicateTasksTable.php
+++ /dev/null
@@ -1,38 +0,0 @@
-duplicateTasks();
-
- if ($duplicateTasks->isEmpty()) {
- return;
- }
-
- $this->command->line('');
- $this->command->line('Duplicate tasks');
- $this->command->line('---------------');
- $this->command->line('These tasks could not be monitored because they have a duplicate name.');
- $this->command->line('');
-
- $headers = ['Type', 'Frequency'];
- $rows = $duplicateTasks->map(function (Task $task) {
- return [
- 'name' => $task->name(),
- 'type' => ucfirst($task->type()),
- 'cron_expression' => $task->humanReadableCron(),
- ];
- });
-
- $this->command->table($headers, $rows);
-
- $this->command->line('');
- $this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a unique name.');
- }
-}
diff --git a/src/Commands/Tables/MonitoredTasksTable.php b/src/Commands/Tables/MonitoredTasksTable.php
deleted file mode 100644
index 8844823..0000000
--- a/src/Commands/Tables/MonitoredTasksTable.php
+++ /dev/null
@@ -1,125 +0,0 @@
-command->line('');
- $this->command->line('Monitored tasks');
- $this->command->line('---------------');
-
- $tasks = ScheduledTasks::createForSchedule()
- ->uniqueTasks()
- ->filter(fn (Task $task) => $task->isBeingMonitored());
-
- if ($tasks->isEmpty()) {
- $this->command->line('');
- $this->command->warn('There currently are no tasks being monitored!');
-
- return;
- }
-
- $headers = [
- 'Name',
- 'Type',
- 'Frequency',
- 'Last started at',
- 'Last finished at',
- 'Last failed at',
- 'Next run date',
- 'Grace time',
- ];
-
- if ($this->usingOhDear()) {
- $headers = array_merge($headers, [
- 'Registered at Oh Dear',
- ]);
- }
-
- $dateFormat = config('schedule-monitor.date_format');
-
- $rows = $tasks->map(function (Task $task) use ($dateFormat) {
- $row = [
- 'name' => $task->name(),
- 'type' => ucfirst($task->type()),
- 'cron_expression' => $task->humanReadableCron(),
- 'started_at' => optional($task->lastRunStartedAt())->format($dateFormat) ?? 'Did not start yet',
- 'finished_at' => $this->getLastRunFinishedAt($task),
- 'failed_at' => $this->getLastRunFailedAt($task),
- 'next_run' => $task->nextRunAt()->format($dateFormat),
- 'grace_time' => $task->graceTimeInMinutes(),
- ];
-
- if ($this->usingOhDear()) {
- $row = array_merge($row, [
- 'registered_at_oh_dear' => $task->isBeingMonitoredAtOhDear() ? '✅' : '❌',
- ]);
- }
-
- return $row;
- });
-
- $this->command->table($headers, $rows);
-
- if ($this->usingOhDear()) {
- if ($tasks->contains(fn (Task $task) => ! $task->isBeingMonitoredAtOhDear())) {
- $this->command->line('');
- $this->command->line('Some tasks are not registered on Oh Dear. You will not be notified when they do not run on time.');
- $this->command->line('Run `php artisan schedule-monitor:sync` to register them and receive notifications.');
- }
- }
- }
-
- public function getLastRunFinishedAt(Task $task): string
- {
- $dateFormat = config('schedule-monitor.date_format');
-
- $formattedLastRunFinishedAt = optional($task->lastRunFinishedAt())->format($dateFormat) ?? '';
-
- if ($task->lastRunFinishedTooLate()) {
- $formattedLastRunFinishedAt = "{$formattedLastRunFinishedAt}>";
- }
-
- return $formattedLastRunFinishedAt;
- }
-
- public function getLastRunFailedAt(Task $task): string
- {
- if (! $lastRunFailedAt = $task->lastRunFailedAt()) {
- return '';
- }
-
- $dateFormat = config('schedule-monitor.date_format');
-
- $formattedLastFailedAt = $lastRunFailedAt->format($dateFormat);
-
- if ($task->lastRunFailed()) {
- $formattedLastFailedAt = "{$formattedLastFailedAt}>";
- }
-
- return $formattedLastFailedAt;
- }
-
- protected function usingOhDear(): bool
- {
- if (! class_exists(OhDear::class)) {
- return false;
- }
-
- if (empty(config('schedule-monitor.oh_dear.api_token'))) {
- return false;
- }
-
- if (empty(config('schedule-monitor.oh_dear.site_id'))) {
- return false;
- }
-
- return true;
- }
-}
diff --git a/src/Commands/Tables/ReadyForMonitoringTasksTable.php b/src/Commands/Tables/ReadyForMonitoringTasksTable.php
deleted file mode 100644
index 92e18f2..0000000
--- a/src/Commands/Tables/ReadyForMonitoringTasksTable.php
+++ /dev/null
@@ -1,41 +0,0 @@
-uniqueTasks()
- ->reject(fn (Task $task) => $task->isBeingMonitored());
-
- if ($tasks->isEmpty()) {
- return;
- }
-
- $this->command->line('');
- $this->command->line('Run sync to start monitoring');
- $this->command->line('----------------------------');
- $this->command->line('');
- $this->command->line('These tasks will be monitored after running `php artisan schedule-monitor:sync`');
- $this->command->line('');
-
- $tasks = ScheduledTasks::createForSchedule()
- ->uniqueTasks()
- ->reject(fn (Task $task) => $task->isBeingMonitored());
-
- $headers = ['Name', 'Type', 'Frequency'];
- $rows = $tasks->map(function (Task $task) {
- return [
- 'name' => $task->name(),
- 'type' => ucfirst($task->type()),
- 'cron_expression' => $task->humanReadableCron(),
- ];
- });
- $this->command->table($headers, $rows);
- }
-}
diff --git a/src/Commands/Tables/ScheduledTasksTable.php b/src/Commands/Tables/ScheduledTasksTable.php
deleted file mode 100644
index 2bc26ff..0000000
--- a/src/Commands/Tables/ScheduledTasksTable.php
+++ /dev/null
@@ -1,17 +0,0 @@
-command = $command;
- }
-
- abstract public function render(): void;
-}
diff --git a/src/Commands/Tables/UnnamedTasksTable.php b/src/Commands/Tables/UnnamedTasksTable.php
deleted file mode 100644
index f4052ed..0000000
--- a/src/Commands/Tables/UnnamedTasksTable.php
+++ /dev/null
@@ -1,38 +0,0 @@
-unnamedTasks();
-
- if ($unnamedTasks->isEmpty()) {
- return;
- }
-
- $this->command->line('');
- $this->command->line('Unnamed tasks');
- $this->command->line('-------------');
- $this->command->line('These tasks cannot be monitored because no name could be determined for them.');
- $this->command->line('');
-
-
- $headers = ['Type', 'Frequency'];
- $rows = $unnamedTasks->map(function (Task $task) {
- return [
- 'type' => ucfirst($task->type()),
- 'cron_expression' => $task->humanReadableCron(),
- ];
- });
-
- $this->command->table($headers, $rows);
-
- $this->command->line('');
- $this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a name.');
- }
-}
diff --git a/src/Commands/VerifyCommand.php b/src/Commands/VerifyCommand.php
index 4799027..dcaf97f 100644
--- a/src/Commands/VerifyCommand.php
+++ b/src/Commands/VerifyCommand.php
@@ -5,6 +5,7 @@
use Exception;
use Illuminate\Console\Command;
use OhDear\PhpSdk\OhDear;
+use function Termwind\render;
class VerifyCommand extends Command
{
@@ -16,8 +17,9 @@ public function handle()
{
$ohDearConfig = config('schedule-monitor.oh_dear');
- $this->info('Verifying if Oh Dear is configured correctly...');
- $this->line('');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Verifying if Oh Dear is configured correctly...',
+ ]));
$this
->verifySdkInstalled()
@@ -25,9 +27,12 @@ public function handle()
->verifySiteId($ohDearConfig)
->verifyConnection($ohDearConfig);
- $this->line('');
- $this->info('All ok!');
- $this->info('Run `php artisan schedule-monitor:sync` to sync your scheduled tasks with Oh Dear.');
+ render(view('schedule-monitor::alert', [
+ 'message' => <<All ok! Run php artisan schedule-monitor:sync
+ to sync your scheduled tasks with oh dear.
+ HTML,
+ ]));
}
public function verifySdkInstalled(): self
@@ -36,7 +41,9 @@ public function verifySdkInstalled(): self
throw new Exception("You must install the Oh Dear SDK in order to sync your schedule with Oh Dear. Run `composer require ohdearapp/ohdear-php-sdk`.");
}
- $this->comment('The Oh Dear SDK is installed.');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'The Oh Dear SDK is installed.',
+ ]));
return $this;
}
@@ -47,7 +54,9 @@ protected function verifyApiToken(array $ohDearConfig): self
throw new Exception('No API token found. Make sure you added an API token to the `api_token` key of the `schedule-monitor` config file. You can generate a new token here: https://ohdear.app/user/api-tokens');
}
- $this->comment('Oh Dear API token found.');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Oh Dear API token found.',
+ ]));
return $this;
}
@@ -58,7 +67,9 @@ protected function verifySiteId(array $ohDearConfig): self
throw new Exception('No site id found. Make sure you added an site id to the `site_id` key of the `schedule-monitor` config file. You can found your site id on the settings page of a site on Oh Dear.');
}
- $this->comment('Oh Dear site id found.');
+ render(view('schedule-monitor::alert', [
+ 'message' => 'Oh Dear site id found.',
+ ]));
return $this;
}
@@ -69,7 +80,9 @@ protected function verifyConnection(array $ohDearConfig)
$site = app(OhDear::class)->site($ohDearConfig['site_id']);
- $this->comment("Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}");
+ render(view('schedule-monitor::alert', [
+ 'message' => "Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}",
+ ]));
return $this;
}
diff --git a/src/ScheduleMonitorServiceProvider.php b/src/ScheduleMonitorServiceProvider.php
index 11632b3..f5f816c 100644
--- a/src/ScheduleMonitorServiceProvider.php
+++ b/src/ScheduleMonitorServiceProvider.php
@@ -23,6 +23,7 @@ public function configurePackage(Package $package): void
{
$package
->name('laravel-schedule-monitor')
+ ->hasViews()
->hasConfigFile()
->hasMigrations('create_schedule_monitor_tables')
->hasCommands([
diff --git a/src/Support/ScheduledTasks/ScheduledTasks.php b/src/Support/ScheduledTasks/ScheduledTasks.php
index de6bd9a..24bd497 100644
--- a/src/Support/ScheduledTasks/ScheduledTasks.php
+++ b/src/Support/ScheduledTasks/ScheduledTasks.php
@@ -55,6 +55,18 @@ public function duplicateTasks(): Collection
->values();
}
+ public function readyForMonitoringTasks(): Collection
+ {
+ return $this->uniqueTasks()
+ ->reject(fn (Task $task) => $task->isBeingMonitored());
+ }
+
+ public function monitoredTasks(): Collection
+ {
+ return $this->uniqueTasks()
+ ->filter(fn (Task $task) => $task->isBeingMonitored());
+ }
+
public function unmonitoredTasks(): Collection
{
return $this->tasks->reject(fn (Task $task) => $task->shouldMonitor());
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 37a96c9..f3c5fd6 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -10,6 +10,8 @@
use Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider;
use Spatie\ScheduleMonitor\Tests\TestClasses\FakeOhDear;
use Spatie\ScheduleMonitor\Tests\TestClasses\TestKernel;
+use Symfony\Component\Console\Output\BufferedOutput;
+use function Termwind\renderUsing;
class TestCase extends Orchestra
{
@@ -38,6 +40,8 @@ protected function setUp(): void
Factory::guessFactoryNamesUsing(
fn (string $modelName) => 'Spatie\\ScheduleMonitor\\Database\\Factories\\' . class_basename($modelName) . 'Factory'
);
+
+ renderUsing(new BufferedOutput());
}
protected function getPackageProviders($app)