feat(scheduler): add pagination to skipped jobs and filter manager start events

- Implement pagination for skipped jobs display with 20 items per page
- Add pagination controls (previous/next buttons) to the scheduled jobs view
- Exclude ScheduledJobManager "started" events from run logs, keeping only "completed" events
- Add ShouldBeEncrypted interface to ScheduledTaskJob for secure queue handling
- Update log filtering to fetch 500 recent skips and slice for pagination
- Use Log facade instead of fully qualified class name
This commit is contained in:
Andras Bacsai 2026-02-28 16:23:58 +01:00
parent a0c177f6f2
commit 63be5928ab
7 changed files with 101 additions and 5 deletions

View file

@ -478,7 +478,7 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
throw new \Exception('MongoDB credentials not found. Ensure MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD environment variables are available in the container.');
}
}
\Log::info('MongoDB backup URL configured', ['has_url' => filled($url), 'using_env_vars' => blank($this->database->internal_db_url)]);
Log::info('MongoDB backup URL configured', ['has_url' => filled($url), 'using_env_vars' => blank($this->database->internal_db_url)]);
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4')) {

View file

@ -91,6 +91,8 @@ public function handle(): void
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
return;
}
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {

View file

@ -14,13 +14,14 @@
use App\Notifications\ScheduledTask\TaskSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ScheduledTaskJob implements ShouldQueue
class ScheduledTaskJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View file

@ -16,6 +16,18 @@ class ScheduledJobs extends Component
public string $filterDate = 'last_24h';
public int $skipPage = 0;
public int $skipDefaultTake = 20;
public bool $showSkipNext = false;
public bool $showSkipPrev = false;
public int $skipCurrentPage = 1;
public int $skipTotalCount = 0;
protected Collection $executions;
protected Collection $skipLogs;
@ -42,11 +54,30 @@ public function mount(): void
public function updatedFilterType(): void
{
$this->skipPage = 0;
$this->loadData();
}
public function updatedFilterDate(): void
{
$this->skipPage = 0;
$this->loadData();
}
public function skipNextPage(): void
{
$this->skipPage += $this->skipDefaultTake;
$this->showSkipPrev = true;
$this->loadData();
}
public function skipPreviousPage(): void
{
$this->skipPage -= $this->skipDefaultTake;
if ($this->skipPage < 0) {
$this->skipPage = 0;
}
$this->showSkipPrev = $this->skipPage > 0;
$this->loadData();
}
@ -69,7 +100,12 @@ private function loadData(?int $teamId = null): void
$this->executions = $this->getExecutions($teamId);
$parser = new SchedulerLogParser;
$this->skipLogs = $parser->getRecentSkips(50, $teamId);
$allSkips = $parser->getRecentSkips(500, $teamId);
$this->skipTotalCount = $allSkips->count();
$this->skipLogs = $allSkips->slice($this->skipPage, $this->skipDefaultTake)->values();
$this->showSkipPrev = $this->skipPage > 0;
$this->showSkipNext = ($this->skipPage + $this->skipDefaultTake) < $this->skipTotalCount;
$this->skipCurrentPage = intval($this->skipPage / $this->skipDefaultTake) + 1;
$this->managerRuns = $parser->getRecentRuns(30, $teamId);
}

View file

@ -64,7 +64,7 @@ public function getRecentRuns(int $limit = 60, ?int $teamId = null): Collection
continue;
}
if (! str_contains($entry['message'], 'ScheduledJobManager')) {
if (! str_contains($entry['message'], 'ScheduledJobManager') || str_contains($entry['message'], 'started')) {
continue;
}

View file

@ -34,7 +34,7 @@ class="flex flex-col gap-8">
])
:class="activeTab === 'skipped-jobs' && 'dark:bg-coollabs bg-coollabs text-white'"
@click="activeTab = 'skipped-jobs'; window.location.hash = 'skipped-jobs'">
Skipped Jobs ({{ $skipLogs->count() }})
Skipped Jobs ({{ $skipTotalCount }})
</div>
</div>
@ -186,6 +186,27 @@ class="border-b border-gray-200 dark:border-coolgray-400">
{{-- Skipped Jobs Tab --}}
<div x-show="activeTab === 'skipped-jobs'" x-cloak>
<div class="pb-4 text-sm text-gray-500">Jobs that were not dispatched because conditions were not met.</div>
@if($skipTotalCount > $skipDefaultTake)
<div class="flex items-center gap-2 mb-4">
<x-forms.button disabled="{{ !$showSkipPrev }}" wire:click="skipPreviousPage">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 19l-7-7 7-7" />
</svg>
</x-forms.button>
<span class="text-sm">
Page {{ $skipCurrentPage }} of {{ ceil($skipTotalCount / $skipDefaultTake) }}
</span>
<x-forms.button disabled="{{ !$showSkipNext }}" wire:click="skipNextPage">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7" />
</svg>
</x-forms.button>
</div>
@endif
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">

View file

@ -173,6 +173,42 @@
@unlink($logPath);
});
test('scheduler log parser excludes started events from runs', function () {
$logPath = storage_path('logs/scheduled-test-started-filter.log');
$logDir = dirname($logPath);
if (! is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
// Temporarily rename existing logs so they don't interfere
$existingLogs = glob(storage_path('logs/scheduled-*.log'));
$renamed = [];
foreach ($existingLogs as $log) {
$tmp = $log.'.bak';
rename($log, $tmp);
$renamed[$tmp] = $log;
}
$logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
$lines = [
'['.now()->format('Y-m-d H:i:s').'] production.INFO: ScheduledJobManager started {}',
'['.now()->format('Y-m-d H:i:s').'] production.INFO: ScheduledJobManager completed {"duration_ms":74,"dispatched":1,"skipped":13}',
];
file_put_contents($logPath, implode("\n", $lines)."\n");
$parser = new SchedulerLogParser;
$runs = $parser->getRecentRuns();
expect($runs)->toHaveCount(1);
expect($runs->first()['message'])->toContain('completed');
// Cleanup
@unlink($logPath);
foreach ($renamed as $tmp => $original) {
rename($tmp, $original);
}
});
test('scheduler log parser filters by team id', function () {
$logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
$logDir = dirname($logPath);