From 4aa4b9cadf81bdea41bfdd926e4908a95b25b62f Mon Sep 17 00:00:00 2001 From: F4ilji Date: Thu, 9 Apr 2026 17:23:10 +0500 Subject: [PATCH] changes --- app/Containers/AppStructure/Models/Page.php | 60 ++++++++- .../Actions/Pages/CreatePageAction.php | 9 -- .../Actions/Pages/GeneratePagePathAction.php | 27 ---- .../Actions/Pages/UpdatePageAction.php | 12 -- .../Actions/Posts/DeploySiteAction.php | 98 +++++++++++++++ .../UI/WEB/Controllers/DeployController.php | 77 ++++++++++++ .../Dashboard/UI/WEB/Routes/web.php | 4 + app/Http/Middleware/HandleInertiaRequests.php | 3 + app/Ship/Middleware/HandleInertiaRequests.php | 3 + .../ContentBuilder/blocks/TabBlock.vue | 4 +- resources/js/Pages/Dashboard/Main.vue | 115 ++++++++++++++++++ resources/js/Pages/Dashboard/Pages/Create.vue | 75 ++++++------ resources/js/Pages/Dashboard/Pages/Edit.vue | 75 ++++++------ 13 files changed, 437 insertions(+), 125 deletions(-) delete mode 100644 app/Containers/Dashboard/Actions/Pages/GeneratePagePathAction.php create mode 100644 app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php create mode 100644 app/Containers/Dashboard/UI/WEB/Controllers/DeployController.php diff --git a/app/Containers/AppStructure/Models/Page.php b/app/Containers/AppStructure/Models/Page.php index bf78c2a..60d9cb3 100755 --- a/app/Containers/AppStructure/Models/Page.php +++ b/app/Containers/AppStructure/Models/Page.php @@ -14,7 +14,24 @@ class Page extends Model protected $guarded = false; - public function section() : BelongsTo + protected static function boot(): void + { + parent::boot(); + + // Auto-generate path when page is created + static::creating(function (Page $page) { + $page->generatePath(); + }); + + // Only regenerate path if slug or parent section changed + static::updating(function (Page $page) { + if ($page->isDirty(['slug', 'sub_section_id'])) { + $page->generatePath(); + } + }); + } + + public function section(): BelongsTo { return $this->belongsTo(SubSection::class, 'sub_section_id'); } @@ -28,4 +45,45 @@ class Page extends Model 'content' => 'array', 'settings' => 'array', ]; + + /** + * Generate path for the page based on slug and parent section + */ + protected function generatePath(): void + { + // Don't regenerate path for registered pages (system routes) + if ($this->exists && $this->is_registered) { + return; + } + + // Only generate path if slug is present + if (empty($this->slug)) { + return; + } + + // Get sub_section_id from model attributes + $subSectionId = $this->sub_section_id; + + if ($subSectionId === null) { + $this->path = $this->slug; + return; + } + + // Fetch subSection with mainSection relationship + $subSection = SubSection::with('mainSection')->find($subSectionId); + + if ($subSection === null) { + $this->path = $this->slug; + return; + } + + $mainSection = $subSection->mainSection; + + if ($mainSection === null) { + $this->path = $subSection->slug . '/' . $this->slug; + return; + } + + $this->path = $mainSection->slug . '/' . $subSection->slug . '/' . $this->slug; + } } diff --git a/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php b/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php index 76cd325..9ad9019 100644 --- a/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php +++ b/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php @@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask; class CreatePageAction { public function __construct( - private readonly GeneratePagePathAction $generatePagePathAction, private readonly GenerateSearchDataTask $generateSearchDataTask, ) {} @@ -20,19 +19,11 @@ class CreatePageAction */ public function run(array $data): Page { - $subSectionId = $data['sub_section_id'] ?? null; - - // Генерируем path на основе subSection - $data['path'] = $this->generatePagePathAction->run($data['slug'], $subSectionId); - // Генерируем search_data из контента if (!empty($data['content'])) { $data['search_data'] = $this->generateSearchDataTask->run($data['content']); } - // Удаляем sub_section_id — он не является полем модели Page - unset($data['sub_section_id']); - return Page::create($data); } } diff --git a/app/Containers/Dashboard/Actions/Pages/GeneratePagePathAction.php b/app/Containers/Dashboard/Actions/Pages/GeneratePagePathAction.php deleted file mode 100644 index 8165392..0000000 --- a/app/Containers/Dashboard/Actions/Pages/GeneratePagePathAction.php +++ /dev/null @@ -1,27 +0,0 @@ -find($subSectionId); - - if ($subSection === null) { - return $slug; - } - - if ($subSection->mainSection === null) { - return $subSection->slug . '/' . $slug; - } - - return $subSection->mainSection->slug . '/' . $subSection->slug . '/' . $slug; - } -} diff --git a/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php b/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php index d2fdae9..1f2ef20 100644 --- a/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php +++ b/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php @@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask; class UpdatePageAction { public function __construct( - private readonly GeneratePagePathAction $generatePagePathAction, private readonly GenerateSearchDataTask $generateSearchDataTask, ) {} @@ -26,17 +25,6 @@ class UpdatePageAction $data['search_data'] = $this->generateSearchDataTask->run($data['content']); } - // Если страница не зарегистрирована, перегенерируем path - if ($page->is_registered == false) { - $subSectionId = $data['sub_section_id'] ?? $page->sub_section_id; - $slug = $data['slug'] ?? $page->slug; - - $data['path'] = $this->generatePagePathAction->run($slug, $subSectionId); - } - - // Удаляем sub_section_id — он не является полем модели Page - unset($data['sub_section_id']); - $page->update($data); return $page->fresh(); diff --git a/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php b/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php new file mode 100644 index 0000000..30ecfed --- /dev/null +++ b/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php @@ -0,0 +1,98 @@ + bool, 'message' => string] + */ + public function run(): array + { + try { + // Проверяем, не запущен ли уже деплой + if (file_exists($this->lockFile)) { + $lockContent = file_get_contents($this->lockFile); + $lockData = json_decode($lockContent, true); + + if ($lockData && isset($lockData['status']) && $lockData['status'] === 'running') { + return [ + 'success' => false, + 'message' => 'Деплой уже запущен! Подождите завершения текущего процесса.', + ]; + } + } + + // Создаём файл-триггер + $lockData = [ + 'status' => 'pending', + 'started_at' => now()->toDateTimeString(), + 'started_by' => auth()->id(), + ]; + + file_put_contents($this->lockFile, json_encode($lockData, JSON_PRETTY_PRINT)); + + Log::info('Deploy triggered', ['user_id' => auth()->id()]); + + return [ + 'success' => true, + 'message' => 'Деплой запущен! Процесс обновления сайта начнётся в течение 1 минуты.', + ]; + } catch (\Exception $e) { + Log::error('Deploy trigger failed', ['exception' => $e->getMessage()]); + + return [ + 'success' => false, + 'message' => 'Ошибка при запуске деплоя: ' . $e->getMessage(), + ]; + } + } + + /** + * Проверяет статус деплоя + */ + public function getStatus(): array + { + if (!file_exists($this->lockFile)) { + return ['status' => 'idle', 'message' => 'Деплой не запущен']; + } + + $content = file_get_contents($this->lockFile); + $data = json_decode($content, true); + + if (!$data) { + return ['status' => 'error', 'message' => 'Ошибка чтения статуса']; + } + + // Читаем лог если есть + $log = ''; + if (file_exists($this->logFile)) { + $log = file_get_contents($this->logFile); + } + + return [ + 'status' => $data['status'] ?? 'unknown', + 'started_at' => $data['started_at'] ?? null, + 'completed_at' => $data['completed_at'] ?? null, + 'message' => $data['message'] ?? '', + 'log' => $log, + ]; + } + + /** + * Очищает статус деплоя + */ + public function clearStatus(): void + { + if (file_exists($this->lockFile)) { + unlink($this->lockFile); + } + } +} diff --git a/app/Containers/Dashboard/UI/WEB/Controllers/DeployController.php b/app/Containers/Dashboard/UI/WEB/Controllers/DeployController.php new file mode 100644 index 0000000..8884d8e --- /dev/null +++ b/app/Containers/Dashboard/UI/WEB/Controllers/DeployController.php @@ -0,0 +1,77 @@ +environment() !== 'production') { + return response()->json([ + 'success' => false, + 'message' => 'Деплой доступен только на production', + ], 403); + } + + if (!$request->user()->hasRole('admin')) { + return response()->json([ + 'success' => false, + 'message' => 'Недостаточно прав для выполнения этой операции', + ], 403); + } + + $result = $this->deploySiteAction->run(); + + return response()->json($result); + } + + /** + * Проверяет статус деплоя + */ + public function status(Request $request): JsonResponse + { + if (app()->environment() !== 'production') { + return response()->json(['status' => 'disabled'], 403); + } + + if (!$request->user()->hasRole('admin')) { + return response()->json([ + 'success' => false, + 'message' => 'Недостаточно прав', + ], 403); + } + + $status = $this->deploySiteAction->getStatus(); + + return response()->json($status); + } + + /** + * Очищает статус деплоя + */ + public function clear(Request $request): JsonResponse + { + if (!$request->user()->hasRole('admin')) { + return response()->json([ + 'success' => false, + 'message' => 'Недостаточно прав', + ], 403); + } + + $this->deploySiteAction->clearStatus(); + + return response()->json(['success' => true]); + } +} diff --git a/app/Containers/Dashboard/UI/WEB/Routes/web.php b/app/Containers/Dashboard/UI/WEB/Routes/web.php index fc81e6a..f94387f 100755 --- a/app/Containers/Dashboard/UI/WEB/Routes/web.php +++ b/app/Containers/Dashboard/UI/WEB/Routes/web.php @@ -10,6 +10,7 @@ use App\Containers\Dashboard\UI\WEB\Controllers\CategoryController as NewsCatego use App\Containers\Dashboard\UI\WEB\Controllers\ContactWidgetController; use App\Containers\Dashboard\UI\WEB\Controllers\CreateSliderController; use App\Containers\Dashboard\UI\WEB\Controllers\CustomFormController; +use App\Containers\Dashboard\UI\WEB\Controllers\DeployController; use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentController; use App\Containers\Dashboard\UI\WEB\Controllers\PageReferenceListController; use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentProgramController; @@ -60,6 +61,9 @@ Route::post('/dashboard/logout', [AuthenticatedSessionController::class, 'destro // Authenticated dashboard routes Route::middleware(['access-check', 'dashboard.auth'])->group(function () { Route::get('/dashboard', IndexDashboardController::class)->name('dashboard.index'); + Route::post('/dashboard/deploy', [DeployController::class, 'deploy'])->name('dashboard.deploy'); + Route::get('/dashboard/deploy/status', [DeployController::class, 'status'])->name('dashboard.deploy.status'); + Route::post('/dashboard/deploy/clear', [DeployController::class, 'clear'])->name('dashboard.deploy.clear'); // CRUD постов Route::prefix('/dashboard/posts')->name('dashboard.posts.')->group(function () { diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index c766982..3c76b92 100755 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -28,6 +28,9 @@ class HandleInertiaRequests extends Middleware return [ ...parent::share($request), + 'app' => [ + 'env' => app()->environment(), + ], 'auth' => [ 'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null, ], diff --git a/app/Ship/Middleware/HandleInertiaRequests.php b/app/Ship/Middleware/HandleInertiaRequests.php index 49cea60..511a1d4 100755 --- a/app/Ship/Middleware/HandleInertiaRequests.php +++ b/app/Ship/Middleware/HandleInertiaRequests.php @@ -29,6 +29,9 @@ class HandleInertiaRequests extends Middleware return [ ...parent::share($request), + 'app' => [ + 'env' => app()->environment(), + ], 'auth' => [ 'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null, ], diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/TabBlock.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/TabBlock.vue index 6bc27ee..3e226ea 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/TabBlock.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/TabBlock.vue @@ -4,7 +4,7 @@
@@ -97,7 +97,7 @@ export default { this.$emit('update:modelValue', { ...this.modelValue, [field]: value }); }, updateSettings(field, value) { - this.update('settings', { ...this.modelValue.settings, [field]: value }); + this.update('settings', { ...(this.modelValue.settings || {}), [field]: value }); }, updateTab(index, field, value) { const tabs = [...this.tabs]; diff --git a/resources/js/Pages/Dashboard/Main.vue b/resources/js/Pages/Dashboard/Main.vue index 49eacf8..7888e01 100755 --- a/resources/js/Pages/Dashboard/Main.vue +++ b/resources/js/Pages/Dashboard/Main.vue @@ -6,6 +6,43 @@ + +
+
+
+
+ +
+
+

Обновление сайта

+

Запуск скрипта деплоя и пересборка сервера

+
+
+ +
+ + +
+
+ + {{ deployMessage }} +
+
{{ deployOutput }}
+
+
+ @@ -87,6 +124,11 @@ export default { }, data() { return { + deploying: false, + deployOutput: null, + deploySuccess: false, + deployStatus: null, + deployPolling: null, domainSections: [ { title: 'Контент сайта', @@ -125,7 +167,80 @@ export default { mounted() { this.SET_DOCUMENT_TITLE('Главная'); }, + beforeUnmount() { + if (this.deployPolling) { + clearInterval(this.deployPolling); + } + }, + computed: { + isProduction() { + return this.$page.props.app?.env === 'production'; + }, + }, methods: { + async deploySite() { + if (!confirm('Вы уверены, что хотите обновить сайт? Это запустит скрипт деплоя.')) { + return; + } + + this.deploying = true; + this.deployOutput = null; + this.deploySuccess = false; + + try { + const response = await fetch(route('dashboard.deploy'), { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + 'Accept': 'application/json', + }, + }); + + const result = await response.json(); + + this.deploySuccess = result.success; + this.deployOutput = result.message; + + // Если деплой запущен — начинаем polling статуса + if (result.success) { + this.startDeployPolling(); + } + } catch (error) { + this.deploySuccess = false; + this.deployOutput = 'Ошибка при выполнении запроса: ' + error.message; + } finally { + this.deploying = false; + } + }, + + startDeployPolling() { + // Проверяем статус каждые 3 секунды + this.deployPolling = setInterval(async () => { + try { + const response = await fetch(route('dashboard.deploy.status')); + const status = await response.json(); + + this.deployStatus = status; + + if (status.status === 'completed' || status.status === 'failed' || status.status === 'idle') { + clearInterval(this.deployPolling); + this.deployPolling = null; + this.deployOutput = status.message || 'Деплой завершён'; + this.deploySuccess = status.status === 'completed'; + + if (status.log) { + this.deployOutput += '\n\n' + status.log; + } + } + } catch (error) { + console.error('Error polling deploy status:', error); + } + }, 3000); + }, + get deployMessage() { + if (!this.deployOutput) return ''; + return this.deploySuccess ? 'Сайт успешно обновлён!' : 'Ошибка при обновлении сайта'; + }, getBgClass(color) { const map = { primary: 'bg-primary/10', diff --git a/resources/js/Pages/Dashboard/Pages/Create.vue b/resources/js/Pages/Dashboard/Pages/Create.vue index f758b2d..d3eba5c 100644 --- a/resources/js/Pages/Dashboard/Pages/Create.vue +++ b/resources/js/Pages/Dashboard/Pages/Create.vue @@ -1,24 +1,47 @@