diff --git a/app/Modules/Platform/Http/Controllers/SchedulerMonitoringController.php b/app/Modules/Platform/Http/Controllers/SchedulerMonitoringController.php index 8efb786b..6c8210d4 100644 --- a/app/Modules/Platform/Http/Controllers/SchedulerMonitoringController.php +++ b/app/Modules/Platform/Http/Controllers/SchedulerMonitoringController.php @@ -6,6 +6,9 @@ namespace App\Modules\Platform\Http\Controllers; use App\Http\Controllers\Controller; use App\Modules\Platform\Scheduling\ScheduledTaskRun; +use App\Modules\Platform\Settings\Setting; +use App\Modules\Platform\Settings\Settings; +use App\Modules\Platform\Updates\LatestReleaseInfo; use App\Support\Pagination; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -48,13 +51,16 @@ class SchedulerMonitoringController extends Controller public function __construct( private readonly FailedJobProviderInterface $failer, + private readonly LatestReleaseInfo $latestRelease, + private readonly Settings $settings, ) {} public function index(Request $request): Response|RedirectResponse { $runs = ScheduledTaskRun::query()->get()->keyBy('command'); + $details = $this->details(); - $tasks = collect(self::KNOWN_COMMANDS)->map(function (string $label, string $command) use ($runs): array { + $tasks = collect(self::KNOWN_COMMANDS)->map(function (string $label, string $command) use ($runs, $details): array { $run = $runs->get($command); return [ @@ -62,6 +68,7 @@ class SchedulerMonitoringController extends Controller 'label' => $label, 'status' => $run?->status->value, 'message' => $run?->message, + 'detail' => $details[$command] ?? null, 'duration_ms' => $run?->duration_ms, 'ran_at' => $run?->ran_at?->toIso8601String(), ]; @@ -137,4 +144,32 @@ class SchedulerMonitoringController extends Controller return back()->with('success', __('All failed jobs deleted.')); } + + /** + * What a task actually found, for the tasks that find something. + * + * The run rows answer "did it work"; they cannot answer "and what did + * it say", because Laravel's ScheduledTaskFinished event fires after + * the command returns and carries no output — which is why a + * successful check for updates has always shown an empty Message. + * Joined here at render time instead, from what the command itself + * wrote to the settings, so the line stays true whether the daily job + * or somebody pressing the button did the work. + * + * @return array + */ + private function details(): array + { + $release = $this->latestRelease->current(); + $checkedAt = $this->settings->get(Setting::LatestVersionCheckedAt); + $everChecked = is_string($checkedAt) && $checkedAt !== ''; + + $updates = match (true) { + $release !== null => (string) __(':version is available', ['version' => $release['version']]), + $everChecked => (string) __('Up to date'), + default => null, + }; + + return array_filter(['projectsend:check-for-updates' => $updates]); + } } diff --git a/resources/js/pages/system/settings/scheduler.tsx b/resources/js/pages/system/settings/scheduler.tsx index 5088d762..84e82d7c 100644 --- a/resources/js/pages/system/settings/scheduler.tsx +++ b/resources/js/pages/system/settings/scheduler.tsx @@ -16,6 +16,8 @@ interface ScheduledTask { label: string; status: 'success' | 'failed' | null; message: string | null; + /** What the task found, for the tasks that find something. Never set on a failure. */ + detail: string | null; duration_ms: number | null; ran_at: string | null; } @@ -130,7 +132,10 @@ export default function SchedulerSettings({ {task.duration_ms !== null ? `${task.duration_ms} ms` : '—'} - {task.message ?? '—'} + {/* A failure's own message wins: what the + last successful run found is not the + answer to why this one broke. */} + {task.message ?? task.detail ?? '—'} ))} diff --git a/tests/Feature/Platform/SchedulerMonitoringTest.php b/tests/Feature/Platform/SchedulerMonitoringTest.php index d951c4a7..e5d680b0 100644 --- a/tests/Feature/Platform/SchedulerMonitoringTest.php +++ b/tests/Feature/Platform/SchedulerMonitoringTest.php @@ -7,6 +7,8 @@ use App\Modules\Identity\Models\Role; use App\Modules\Platform\Capabilities\Edition; use App\Modules\Platform\Scheduling\ScheduledTaskRun; use App\Modules\Platform\Scheduling\TaskRunStatus; +use App\Modules\Platform\Settings\Setting; +use App\Modules\Platform\Settings\Settings; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Illuminate\Testing\TestResponse; @@ -167,3 +169,38 @@ test('staff without edit_settings cannot access the scheduler page', function () $this->actingAs($staffer)->get('/system/settings/scheduler')->assertForbidden(); }); + +test('the update check reports what it found, not just that it ran', function () { + config()->set('projectsend.version', '2.0.0'); + $settings = app(Settings::class); + $settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String()); + $settings->set(Setting::LatestKnownVersion, '2.5.0'); + + $response = $this->actingAs($this->admin)->get('/system/settings/scheduler'); + $tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command'); + + expect($tasks->get('projectsend:check-for-updates')['detail'])->toContain('2.5.0'); +}); + +test('a check that found nothing newer says so', function () { + config()->set('projectsend.version', '2.0.0'); + $settings = app(Settings::class); + $settings->set(Setting::LatestVersionCheckedAt, now()->toIso8601String()); + $settings->set(Setting::LatestKnownVersion, '2.0.0'); + + $response = $this->actingAs($this->admin)->get('/system/settings/scheduler'); + $tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command'); + + expect($tasks->get('projectsend:check-for-updates')['detail'])->toBe('Up to date'); +}); + +test('a check that has never run has nothing to report', function () { + $settings = app(Settings::class); + $settings->set(Setting::LatestVersionCheckedAt, ''); + $settings->set(Setting::LatestKnownVersion, ''); + + $response = $this->actingAs($this->admin)->get('/system/settings/scheduler'); + $tasks = collect(schedulerPageProps($response)['tasks'])->keyBy('command'); + + expect($tasks->get('projectsend:check-for-updates')['detail'])->toBeNull(); +});