user()->isAdmin(), 403); $logs = ReplicationLog::query() ->latest('id') ->limit(50) ->get() ->map(fn (ReplicationLog $l) => [ 'id' => $l->id, 'status' => $l->status, 'schedule_id' => $l->schedule_id, 'tables_success' => $l->tables_success, 'tables_failed' => $l->tables_failed, 'errors' => $l->errors, 'received_at' => optional($l->received_at)->toIso8601String(), ]); $schedules = ReplicationSchedule::orderBy('name')->get()->map(fn (ReplicationSchedule $s) => [ 'id' => $s->id, 'schedule_id' => $s->schedule_id, 'name' => $s->name, 'is_active' => $s->is_active, ]); return Inertia::render('Admin/Replication/Index', [ 'logs' => $logs, 'schedules' => $schedules, 'configured' => (bool) config('services.syncio.base_url'), ]); } public function storeSchedule(Request $request) { abort_unless(auth()->user()->isAdmin(), 403); $data = $request->validate([ 'schedule_id' => ['required', 'string', 'max:255', 'unique:replication_schedules,schedule_id'], 'name' => ['nullable', 'string', 'max:255'], ]); ReplicationSchedule::create([ 'schedule_id' => $data['schedule_id'], 'name' => $data['name'] ?? null, 'is_active' => true, ]); return back()->with('success', 'Расписание добавлено.'); } public function updateSchedule(Request $request, ReplicationSchedule $schedule) { abort_unless(auth()->user()->isAdmin(), 403); $data = $request->validate([ 'name' => ['nullable', 'string', 'max:255'], 'is_active' => ['required', 'boolean'], ]); $schedule->update($data); return back()->with('success', 'Расписание обновлено.'); } public function destroySchedule(ReplicationSchedule $schedule) { abort_unless(auth()->user()->isAdmin(), 403); $schedule->delete(); return back()->with('success', 'Расписание удалено.'); } public function run(Request $request) { abort_unless(auth()->user()->isAdmin(), 403); $data = $request->validate([ 'schedule_id' => ['required', 'string', 'max:255'], ]); $baseUrl = rtrim((string) config('services.syncio.base_url'), '/'); if ($baseUrl === '') { return back()->with('error', 'Не задан SYNCIO_BASE_URL — запуск репликации недоступен.'); } $scheduleId = $data['schedule_id']; $url = "{$baseUrl}/migrations/schedules/{$scheduleId}/run"; try { $response = Http::acceptJson() ->when( config('services.syncio.token'), fn ($http) => $http->withToken(config('services.syncio.token')) ) ->timeout(30) ->post($url); if ($response->failed()) { Log::warning('Syncio run failed', ['status' => $response->status(), 'body' => $response->body()]); return back()->with('error', "Репликатор вернул ошибку ({$response->status()})."); } return back()->with('success', 'Репликация запущена. Результат придёт по вебхуку.'); } catch (\Throwable $e) { Log::error('Syncio run exception', ['message' => $e->getMessage()]); return back()->with('error', 'Не удалось связаться с репликатором: '.$e->getMessage()); } } }