Impment webcron and internal scheduling methods and change settings to a key value store
This commit is contained in:
@@ -3,20 +3,27 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Services\CaldavService;
|
use App\Services\CaldavService;
|
||||||
use App\Models\Todo;
|
use App\Models\Todo;
|
||||||
use App\Models\TodoType;
|
|
||||||
use Sabre\VObject\Component\VCalendar;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class CaldavSyncCommand extends Command
|
class CaldavSyncCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'caldav:sync {--calendar= : optional calendar path}';
|
protected $signature = 'caldav:sync';
|
||||||
protected $description = 'Sync CalDAV VTODOs into local todos table';
|
protected $description = 'Sync CalDAV VTODOs into local todos table';
|
||||||
|
|
||||||
public function handle(CaldavService $service)
|
public function handle(CaldavService $service)
|
||||||
{
|
{
|
||||||
$this->info('Starting CalDAV sync...');
|
// only run every 5 minutes although the task is called every minute
|
||||||
|
$cacheKey = 'caldav_sync_last_run';
|
||||||
|
if (\Illuminate\Support\Facades\Cache::has($cacheKey)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Cache::put($cacheKey, true, 300);
|
||||||
|
|
||||||
|
Log::info('Running CalDAV sync');
|
||||||
|
$this->info('Starting CalDAV sync');
|
||||||
|
|
||||||
$todos = $service->getTodos();
|
$todos = $service->getTodos();
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ class Kernel extends ConsoleKernel
|
|||||||
*/
|
*/
|
||||||
protected function schedule(Schedule $schedule): void
|
protected function schedule(Schedule $schedule): void
|
||||||
{
|
{
|
||||||
// Beispiel: CalDAV Sync jede Stunde
|
|
||||||
$schedule->command('caldav:sync')->hourly();
|
|
||||||
|
|
||||||
// Alternativen:
|
|
||||||
// $schedule->command('caldav:sync')->daily();
|
|
||||||
// $schedule->command('caldav:sync --calendar=/calendars/me/default/')->dailyAt('02:00');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,21 +7,22 @@
|
|||||||
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Models\LineItem;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Mail\Reminder;
|
||||||
|
use App\Support\ApiDataTransformer;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use tbQuar\Facades\Quar;
|
||||||
use horstoeko\zugferdlaravel\Facades\ZugferdLaravel;
|
use horstoeko\zugferdlaravel\Facades\ZugferdLaravel;
|
||||||
// use horstoeko\zugferd\codelists\ZugferdInvoiceType;
|
// use horstoeko\zugferd\codelists\ZugferdInvoiceType;
|
||||||
use horstoeko\zugferd\codelists\ZugferdUnitCodes;
|
use horstoeko\zugferd\codelists\ZugferdUnitCodes;
|
||||||
use horstoeko\zugferd\codelists\ZugferdVatCategoryCodes;
|
use horstoeko\zugferd\codelists\ZugferdVatCategoryCodes;
|
||||||
use horstoeko\zugferd\codelists\ZugferdVatTypeCodes;
|
use horstoeko\zugferd\codelists\ZugferdVatTypeCodes;
|
||||||
use horstoeko\zugferd\codelists\ZugferdDocumentType;
|
use horstoeko\zugferd\codelists\ZugferdDocumentType;
|
||||||
use App\Models\LineItem;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use App\Support\ApiDataTransformer;
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use tbQuar\Facades\Quar;
|
|
||||||
use App\Mail\Reminder;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceController extends Controller
|
class InvoiceController extends Controller
|
||||||
@@ -48,7 +49,7 @@ public function summaryAll()
|
|||||||
|
|
||||||
public function summaryThisYear()
|
public function summaryThisYear()
|
||||||
{
|
{
|
||||||
$dt = new DateTime('first day of january this year');
|
$dt = new DateTime('first day of january this year');
|
||||||
$invoices = Invoice::select()
|
$invoices = Invoice::select()
|
||||||
->where('invoice_date', '>=', $dt->format('Y-m-d'))
|
->where('invoice_date', '>=', $dt->format('Y-m-d'))
|
||||||
->orderBy('invoice_date', 'asc')
|
->orderBy('invoice_date', 'asc')
|
||||||
@@ -62,7 +63,7 @@ public function summaryThisYear()
|
|||||||
|
|
||||||
public function summaryBeforeThisYear()
|
public function summaryBeforeThisYear()
|
||||||
{
|
{
|
||||||
$dt = new DateTime('first day of january this year');
|
$dt = new DateTime('first day of january this year');
|
||||||
$invoices = Invoice::select()
|
$invoices = Invoice::select()
|
||||||
->where('invoice_date', '<', $dt->format('Y-m-d'))
|
->where('invoice_date', '<', $dt->format('Y-m-d'))
|
||||||
->orderBy('invoice_date', 'asc')
|
->orderBy('invoice_date', 'asc')
|
||||||
@@ -691,19 +692,53 @@ public function remind(Request $request, int $id)
|
|||||||
*/
|
*/
|
||||||
protected function generateInvoiceNumber()
|
protected function generateInvoiceNumber()
|
||||||
{
|
{
|
||||||
$settings = \App\Models\Setting::firstOrCreate([]);
|
// Key/value Einstellungen
|
||||||
$lastInvoice = Invoice::orderByRaw("CAST(SUBSTRING(nr, 4) AS UNSIGNED) DESC")
|
$format = Setting::where('key', 'invoice.number_format')->value('value') ?? 'RE-{number}';
|
||||||
->first();
|
$start = (int) (Setting::where('key', 'invoice.number_start')->value('value') ?? 1);
|
||||||
|
|
||||||
if ($lastInvoice) {
|
// prefix / suffix aus dem Format ableiten (erwartet genau ein {number})
|
||||||
$lastNumber = (int) str_replace('RE-', '', $lastInvoice->nr);
|
$parts = explode('{number}', $format);
|
||||||
$newNumber = $lastNumber + 1;
|
$prefix = $parts[0] ?? '';
|
||||||
} else {
|
$suffix = $parts[1] ?? '';
|
||||||
$newNumber = $settings->invoice_number_start;
|
|
||||||
|
// Query: nur Rechnungen mit Prefix (falls vorhanden) - reduziert Datensatz
|
||||||
|
$query = \App\Models\Invoice::query();
|
||||||
|
if ($prefix !== '') {
|
||||||
|
$query->where('nr', 'like', $prefix . '%');
|
||||||
|
}
|
||||||
|
// optional: further restrict by suffix if known
|
||||||
|
if ($suffix !== '') {
|
||||||
|
$query->where('nr', 'like', '%' . $suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
return str_replace('{number}', $newNumber, $settings->invoice_number_format);
|
// Hole die Nummern (nur Spalte 'nr') — bei großen Datenmengen ggf. limit/stream verwenden
|
||||||
|
$numbers = $query->pluck('nr')->toArray();
|
||||||
|
|
||||||
|
$max = 0;
|
||||||
|
foreach ($numbers as $nr) {
|
||||||
|
// Entferne Prefix/Suffix falls vorhanden
|
||||||
|
if ($prefix !== '' && str_starts_with($nr, $prefix)) {
|
||||||
|
$nrCore = substr($nr, strlen($prefix));
|
||||||
|
} else {
|
||||||
|
$nrCore = $nr;
|
||||||
|
}
|
||||||
|
if ($suffix !== '' && str_ends_with($nrCore, $suffix)) {
|
||||||
|
$nrCore = substr($nrCore, 0, -strlen($suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrahiere erste Ziffernfolge (handhabt auch führende Nullen)
|
||||||
|
if (preg_match('/\d+/', $nrCore, $m)) {
|
||||||
|
$val = intval($m[0]);
|
||||||
|
if ($val > $max) $max = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newNumber = $max > 0 ? $max + 1 : $start;
|
||||||
|
|
||||||
|
// Setze neue Nummer ins Format (bei Bedarf Padding hier ergänzen)
|
||||||
|
return str_replace('{number}', (string)$newNumber, $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a GiroCode for the invoice
|
* Generate a GiroCode for the invoice
|
||||||
|
|||||||
@@ -9,19 +9,27 @@ class SettingController extends Controller
|
|||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return Setting::firstOrCreate([]);
|
return response()->json(Setting::allKeyValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request)
|
public function update(Request $request)
|
||||||
{
|
{
|
||||||
$validatedData = $request->validate([
|
// Batch: {"settings": {"invoice_number_start": 2, "foo": "bar"}}
|
||||||
'invoice_number_format' => 'required|string',
|
if ($request->has('settings') && is_array($request->input('settings'))) {
|
||||||
'invoice_number_start' => 'required|integer',
|
foreach ($request->input('settings') as $k => $v) {
|
||||||
|
Setting::updateOrCreate(['key' => $k], ['value' => is_scalar($v) ? (string)$v : json_encode($v)]);
|
||||||
|
}
|
||||||
|
return response()->json(Setting::allKeyValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single: {"key":"invoice_number_start","value":2}
|
||||||
|
$validated = $request->validate([
|
||||||
|
'key' => 'required|string',
|
||||||
|
'value' => 'nullable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$setting = Setting::firstOrCreate([]);
|
Setting::updateOrCreate(['key' => $validated['key']], ['value' => isset($validated['value']) && !is_scalar($validated['value']) ? json_encode($validated['value']) : (string)($validated['value'] ?? null)]);
|
||||||
$setting->update($validatedData);
|
|
||||||
|
|
||||||
return response()->json($setting, 200);
|
return response()->json(Setting::allKeyValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Listeners;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use App\Jobs\CheckInvoiceDueDatesJob;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class CheckInvoiceDueDatesListener
|
|
||||||
{
|
|
||||||
public function handle($event)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$today = now()->toDateString();
|
|
||||||
$cacheKey = 'invoice_due_dates_checked_' . $today;
|
|
||||||
|
|
||||||
if (!Cache::has($cacheKey)) {
|
|
||||||
// Cache a timestamped key, so we only check once a day
|
|
||||||
Cache::put($cacheKey, true, now()->endOfDay());
|
|
||||||
Log::info("Starting daily job to check for due invoices.");
|
|
||||||
(new CheckInvoiceDueDatesJob())->handle();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error("Error in CheckInvoiceDueDatesListener: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use App\Models\Setting;
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* This listener is attached to RouteMatched when app.schedule_method == 'internal'.
|
||||||
|
* It will trigger the scheduler at most once per minute (throttle via cache).
|
||||||
|
*/
|
||||||
|
public function handle($event)
|
||||||
|
{
|
||||||
|
// only run when internal scheduling is enabled (defensive)
|
||||||
|
$method = Setting::where('key', 'app.schedule_method')->value('value') ?? 'internal';
|
||||||
|
if ($method !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// throttle key: run at most once every 55 seconds
|
||||||
|
$cacheKey = 'caramel_scheduler_last_run';
|
||||||
|
$ttlSeconds = 55;
|
||||||
|
|
||||||
|
if (Cache::has($cacheKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark as run
|
||||||
|
Cache::put($cacheKey, true, $ttlSeconds);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info('Triggering scheduler via ScheduleListener');
|
||||||
|
Artisan::call('schedule:run');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error running scheduler: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-2
@@ -9,8 +9,18 @@ class Setting extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $primaryKey = 'key';
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'invoice_number_format',
|
'key',
|
||||||
'invoice_number_start',
|
'value',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Hilfsmethode: alle Einstellungen als key=>value Array
|
||||||
|
public static function allKeyValue(): array
|
||||||
|
{
|
||||||
|
return self::query()->pluck('value', 'key')->toArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
use App\Jobs\CheckInvoiceDueDatesJob;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -37,4 +39,4 @@ public function boot(): void
|
|||||||
return "<?php echo str_replace('.', ',', $expression); ?>";
|
return "<?php echo str_replace('.', ',', $expression); ?>";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Routing\Events\RouteMatched;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Listeners\ScheduleListener;
|
||||||
|
use App\Jobs\CheckInvoiceDueDatesJob;
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
protected $listen = [
|
protected $listen = [];
|
||||||
'Illuminate\Routing\Events\RouteMatched' => [
|
|
||||||
\App\Listeners\CheckInvoiceDueDatesListener::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap services.
|
* Bootstrap services.
|
||||||
@@ -18,5 +21,21 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
|
$method = Setting::where('key', 'app.schedule_method')->value('value') ?? 'internal';
|
||||||
|
if ($method === 'internal') {
|
||||||
|
Event::listen(RouteMatched::class, [ScheduleListener::class, 'handle']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: read where to put these
|
||||||
|
// it seems to work here, but where is the apropriate place?
|
||||||
|
// Kernel::schedule did not work
|
||||||
|
Schedule::command('caldav:sync')
|
||||||
|
->everyMinute()
|
||||||
|
->withoutOverlapping();
|
||||||
|
|
||||||
|
Schedule::job(new CheckInvoiceDueDatesJob())
|
||||||
|
->daily()
|
||||||
|
->withoutOverlapping();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,19 @@
|
|||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
|
Schema::dropIfExists('settings');
|
||||||
|
|
||||||
Schema::create('settings', function (Blueprint $table) {
|
Schema::create('settings', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->string('key')->primary();
|
||||||
$table->string('invoice_number_format')->default('RE-{number}');
|
$table->text('value')->nullable();
|
||||||
$table->integer('invoice_number_start')->default(1);
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('settings');
|
Schema::dropIfExists('settings');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,14 +7,10 @@
|
|||||||
|
|
||||||
class SettingsTableSeeder extends Seeder
|
class SettingsTableSeeder extends Seeder
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the database seeds.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
Setting::firstOrCreate([], [
|
Setting::updateOrCreate(['key' => 'invoices.number_format'], ['value' => 'RE-{number}']);
|
||||||
'invoice_number_format' => 'RE-{number}',
|
Setting::updateOrCreate(['key' => 'invoices.number_start'], ['value' => '1']);
|
||||||
'invoice_number_start' => 1,
|
Setting::updateOrCreate(['key' => 'app.schedule_method'], ['value' => '1']);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+50
-2
@@ -1,8 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use App\Http\Controllers\TodoController;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Http\Controllers\InvoiceController;
|
use App\Http\Controllers\InvoiceController;
|
||||||
use App\Http\Controllers\CustomerController;
|
use App\Http\Controllers\CustomerController;
|
||||||
use App\Http\Controllers\ProductController;
|
use App\Http\Controllers\ProductController;
|
||||||
@@ -61,6 +64,51 @@
|
|||||||
Route::get('proceduralDocumentation', function () {
|
Route::get('proceduralDocumentation', function () {
|
||||||
return Inertia::render('ProceduralDocumentation');
|
return Inertia::render('ProceduralDocumentation');
|
||||||
})->name('proceduralDocumentation');
|
})->name('proceduralDocumentation');
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Web cron route
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Example: GET /webcron?token=SECRET or set header X-WEBCron-Token: SECRET
|
||||||
|
| Configure secret in .env as WEBCRON_SECRET (optional). If no secret is set,
|
||||||
|
| the route is open (not recommended in production).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
Route::get('/webcron', function (Request $request) {
|
||||||
|
// only allow if scheduling method is webcron
|
||||||
|
$method = \App\Models\Setting::where('key', 'app.schedule_method')->value('value') ?? 'internal';
|
||||||
|
if ($method !== 'webcron') {
|
||||||
|
return response('Not Found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = env('WEBCRON_SECRET', null);
|
||||||
|
|
||||||
|
// basic token protection
|
||||||
|
if ($secret) {
|
||||||
|
$token = $request->query('token') ?? $request->header('X-WEBCron-Token');
|
||||||
|
if (!$token || !hash_equals((string)$secret, (string)$token)) {
|
||||||
|
return response('Forbidden', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick throttle to avoid abuse (server-side)
|
||||||
|
$cacheKey = 'caramel_webcron_last_run';
|
||||||
|
if (\Illuminate\Support\Facades\Cache::has($cacheKey)) {
|
||||||
|
return response('Throttled', 429);
|
||||||
|
}
|
||||||
|
Cache::put($cacheKey, true, 55);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info('Triggering scheduler via /webcron route');
|
||||||
|
Artisan::call('schedule:run');
|
||||||
|
return response('OK', 200);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error running scheduler: ' . $e->getMessage());
|
||||||
|
return response('Error', 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user