Say what the update check found, not just that it ran

The Scheduler screen printed "Check for updates · Succeeded · —" and left
it there. What it found — the whole reason that job exists — was in the
settings, which that screen never read. Somebody opening it to ask "is
there a new version?" got the answer to "did the job run?"

The Message column now carries "Up to date" or the version that is
waiting. A failure's own message still wins: what the last successful run
found is not the answer to why this one broke.

Joined at render time rather than recorded by the command, because
Laravel's scheduler fires its finished event after the command returns and
overwrites whatever the command wrote — which is exactly why that column
was empty in the first place. Reading the settings instead also keeps the
line true when the new Check now button did the work rather than the
nightly run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ignacionelson
2026-08-17 20:36:01 -03:00
parent d888145b21
commit e87ceb60ba
3 changed files with 79 additions and 2 deletions
@@ -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<string, string>
*/
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]);
}
}
@@ -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({
<td className="text-muted-foreground px-4 py-2.5 whitespace-nowrap">
{task.duration_ms !== null ? `${task.duration_ms} ms` : '—'}
</td>
<td className="text-muted-foreground max-w-xs px-4 py-2.5 break-words">{task.message ?? '—'}</td>
{/* A failure's own message wins: what the
last successful run found is not the
answer to why this one broke. */}
<td className="text-muted-foreground max-w-xs px-4 py-2.5 break-words">{task.message ?? task.detail ?? '—'}</td>
</tr>
))}
</tbody>
@@ -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();
});