Add settings-driven Artisan scheduler registry with Etsy sync and token refresh jobs

Introduces App\Support\DashboardSchedule (modelled on LHITS_Dashboard's
pattern) as a single settings-driven registry for recurring Artisan jobs,
replacing the two hard-coded Schedule::command() lines in
routes/console.php.

Migrated unchanged:
- integrations_refresh (integrations:refresh), every_30_seconds, withoutOverlapping(5)
- alerts_scan (alerts:scan), every_30_seconds, withoutOverlapping(5)

New:
- etsy_sync (etsy:sync), every_15_minutes - forces a fresh pull of Etsy
  receipts/listings into the existing IntegrationCache entries, decoupled
  from the bundled integrations:refresh job so it can be tuned/disabled
  independently of Parcel2Go/BamBuddy/dryer refreshes.
- etsy_token_refresh (etsy:refresh-token), every_45_minutes - proactively
  refreshes the Etsy OAuth token (tokens last ~1hr) via a new public
  Etsy::refreshAccessToken() wrapper around the existing on-demand
  accessToken() refresh logic, so the connection survives quiet periods.

Frequency is persisted to storage/app/dashboard-schedules.json (no
app_settings table exists in this app, unlike LHITS's AppSetting-backed
store) and surfaced in a new 'Scheduler' section on the existing Settings
page (ScheduleSettingsController + routes), with per-job frequency
dropdowns and manual 'Run now' buttons, matching the page's existing
Tailwind conventions.

Tests: SchedulerTest (registry/frequency behavior), ScheduleSettingsTest
(settings UI + authorization), EtsySyncScheduledJobsTest (new commands,
Http::fake only, no live Etsy calls).
This commit is contained in:
2026-11-07 17:38:11 +00:00
parent 9b13640571
commit 960189d14b
12 changed files with 681 additions and 5 deletions
+41
View File
@@ -12,6 +12,7 @@
['parcel2go', 'Parcel2Go', $integrations['parcel2go']['configured'], $integrations['parcel2go']['stored'], 'Shipping and labels'],
['bambuddy', 'BamBuddy', $integrations['bambuddy']['configured'], $integrations['bambuddy']['stored'], 'Print farm'],
['dryer', 'Filament dryer', $integrations['dryer']['configured'], $integrations['dryer']['stored'], 'Drying controls'],
['scheduler', 'Scheduler', true, true, 'Job frequencies'],
] as [$section, $name, $isConfigured, $isStored, $description])
<a href="#{{ $section }}" class="group rounded-2xl border border-slate-800 bg-slate-900 p-4 transition-colors hover:border-brand/60 hover:bg-slate-800/70">
<span class="flex items-center justify-between gap-2"><strong class="text-sm text-white">{{ $name }}</strong><span aria-hidden="true" class="text-slate-500 group-hover:text-brand">↗</span></span>
@@ -127,4 +128,44 @@
</section>
</div>
<section id="scheduler" class="scroll-mt-6 rounded-2xl border border-slate-800 bg-slate-900 p-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-lg font-semibold">Scheduler</h3>
<p class="mt-1 text-sm text-slate-400">Tune how often recurring Artisan jobs run. Laravel applies a changed frequency on its next scheduler tick, no redeploy needed.</p>
</div>
</div>
<form method="post" action="{{ route('settings.schedules.update') }}" class="mt-6 grid gap-4">
@csrf
@method('PUT')
<div class="grid gap-3">
@foreach($scheduleJobs as $job)
<div class="grid gap-3 rounded-xl border border-slate-700 bg-slate-950 p-4 sm:grid-cols-[1fr_auto_auto] sm:items-center">
<div>
<p class="text-sm font-semibold text-white">{{ $job['name'] }}</p>
<p class="mt-1 text-xs text-slate-400">{{ $job['description'] }}</p>
<p class="mt-1 text-xs font-mono text-slate-500">{{ $job['command'] }}</p>
</div>
<select name="schedules[{{ $job['key'] }}]" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
@foreach($scheduleFrequencyOptions as $value => $label)
<option value="{{ $value }}" @selected(old('schedules.'.$job['key'], $job['frequency']) === $value)>{{ $label }}</option>
@endforeach
</select>
@if($job['manual'] ?? false)
<button type="submit" form="schedule-run-{{ $job['key'] }}" class="w-fit rounded-xl border border-slate-700 px-4 py-3 text-sm font-semibold text-slate-200 hover:border-brand">Run now</button>
@endif
</div>
@endforeach
</div>
<div><button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save schedule settings</button></div>
</form>
@foreach($scheduleJobs as $job)
@if($job['manual'] ?? false)
<form id="schedule-run-{{ $job['key'] }}" method="post" action="{{ route('settings.schedules.run', $job['key']) }}" class="hidden">@csrf</form>
@endif
@endforeach
</section>
@endsection