revert: remove deploy UI, controller, actions, tasks, and routes

- Remove Deploy/Index.vue, DeployController, DeploySiteAction, DeployTask
- Remove deploy routes from web.php
- Remove deploy menu item from menuConfig
- Remove deploy button and methods from Main.vue
- Remove deploy logging channel from logging.php
- Keep _deploy/ folder for future CI/CD setup
This commit is contained in:
F4ilji
2026-07-02 12:00:48 +05:00
parent cc98d509e5
commit 8bc5c367d6
8 changed files with 0 additions and 752 deletions
@@ -1,97 +0,0 @@
<?php
namespace App\Containers\Dashboard\Actions\Posts;
use App\Containers\Dashboard\Tasks\DeployTask;
use Illuminate\Support\Facades\Log;
class DeploySiteAction
{
public function __construct(
private readonly DeployTask $deployTask,
) {}
public function run(): array
{
try {
if (!$this->deployTask->scriptExists()) {
Log::channel('deploy')->warning('[DeployAction] Script not found');
return [
'success' => false,
'message' => 'Скрипт деплоя не найден на сервере',
];
}
if ($this->deployTask->isDeployRunning()) {
Log::channel('deploy')->warning('[DeployAction] Deploy already running', [
'user_id' => auth()->id(),
]);
return [
'success' => false,
'message' => 'Деплой уже запущен! Подождите завершения текущего процесса.',
];
}
$started = $this->deployTask->startDeploy();
if (!$started) {
Log::channel('deploy')->error('[DeployAction] Failed to start deploy script', [
'user_id' => auth()->id(),
]);
return [
'success' => false,
'message' => 'Скрипт деплоя не запустился. Проверьте лог.',
];
}
Log::channel('deploy')->info('[DeployAction] Deploy triggered successfully', [
'user_id' => auth()->id(),
'user_email' => auth()->user()?->email,
]);
return [
'success' => true,
'message' => 'Деплой запущен! Процесс обновления сайта начнётся в течение нескольких секунд.',
];
} catch (\Exception $e) {
Log::channel('deploy')->error('[DeployAction] Exception', [
'user_id' => auth()->id(),
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return [
'success' => false,
'message' => 'Ошибка при запуске деплоя: ' . $e->getMessage(),
];
}
}
public function getStatus(): array
{
return $this->deployTask->getDeployStatus();
}
public function getLog(int $lines = 50): array
{
return [
'log' => $this->deployTask->getLogTail($lines),
'full_log' => $this->deployTask->getLog(),
];
}
public function getHistory(): array
{
return [
'history' => $this->deployTask->getHistory(),
];
}
public function clearStatus(): void
{
Log::channel('deploy')->info('[DeployAction] Log cleared', [
'user_id' => auth()->id(),
]);
$this->deployTask->clearLog();
}
}
@@ -1,149 +0,0 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Support\Facades\Log;
class DeployTask
{
private string $deployScript = '/var/www/_deploy/deploy.sh';
private string $logFile = '/var/www/_deploy/deploy.log';
private string $historyDir = '/var/www/_deploy/history';
public function scriptExists(): bool
{
$exists = file_exists($this->deployScript);
$this->phpLog('check_script', $exists ? 'found' : 'not_found');
return $exists;
}
public function isDeployRunning(): bool
{
$output = shell_exec('pgrep -f "deploy\\.sh"');
$running = !empty(trim($output ?? ''));
return $running;
}
public function startDeploy(): bool
{
if ($this->isDeployRunning()) {
$this->phpLog('start', 'already_running');
return false;
}
if (file_exists($this->logFile)) {
unlink($this->logFile);
}
$this->phpLog('start', 'launching deploy.sh');
$command = sprintf(
'bash %s > /dev/null 2>&1 &',
escapeshellarg($this->deployScript)
);
shell_exec($command);
usleep(500000);
$started = $this->isDeployRunning();
$this->phpLog('start', $started ? 'process_started' : 'process_failed_to_start');
return $started;
}
public function getDeployStatus(): array
{
if (!$this->isDeployRunning()) {
if (!file_exists($this->logFile)) {
$this->phpLog('status', 'idle');
return ['status' => 'idle', 'message' => 'Деплой не запущен'];
}
$log = $this->getLog();
$lastLines = array_slice(explode("\n", trim($log)), -5);
$lastOutput = implode("\n", $lastLines);
$isSuccess = stripos($lastOutput, '✅') !== false
|| stripos($lastOutput, 'completed successfully') !== false;
$isFailed = stripos($lastOutput, '❌') !== false
|| stripos($lastOutput, 'ERROR') !== false;
$status = $isSuccess ? 'completed' : ($isFailed ? 'failed' : 'unknown');
$this->phpLog('status', $status);
return [
'status' => $status,
'message' => $isSuccess ? 'Деплой завершён успешно!' : 'Деплой завершён',
'log' => $log,
];
}
$this->phpLog('status', 'running');
return [
'status' => 'running',
'message' => 'Деплой выполняется...',
'log' => $this->getLog(),
];
}
public function getLog(): string
{
if (!file_exists($this->logFile)) {
return '';
}
return file_get_contents($this->logFile);
}
public function getLogTail(int $lines = 50): string
{
$log = $this->getLog();
if (empty($log)) {
return '';
}
$allLines = explode("\n", trim($log));
return implode("\n", array_slice($allLines, -$lines));
}
public function clearLog(): void
{
if (file_exists($this->logFile)) {
unlink($this->logFile);
}
$this->phpLog('clear', 'deploy log cleared');
}
public function getHistory(): array
{
if (!is_dir($this->historyDir)) {
return [];
}
$files = glob($this->historyDir . '/*.json');
usort($files, fn($a, $b) => strcmp($b, $a));
$history = [];
foreach (array_slice($files, 0, 50) as $file) {
$content = file_get_contents($file);
$data = json_decode($content, true);
if ($data) {
$history[] = $data;
}
}
return $history;
}
private function phpLog(string $event, string $message): void
{
Log::channel('deploy')->info('[Deploy] {event}: {message}', [
'event' => $event,
'message' => $message,
]);
}
}
@@ -1,103 +0,0 @@
<?php
namespace App\Containers\Dashboard\UI\WEB\Controllers;
use App\Containers\Dashboard\Actions\Posts\DeploySiteAction;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Inertia\Inertia;
class DeployController extends Controller
{
public function __construct(
private readonly DeploySiteAction $deploySiteAction,
) {}
public function index(): \Inertia\Response
{
$history = $this->deploySiteAction->getHistory();
$status = $this->deploySiteAction->getStatus();
return Inertia::render('Dashboard/Deploy/Index', [
'history' => $history['history'],
'status' => $status,
]);
}
public function deploy(Request $request): JsonResponse
{
if (app()->environment() !== 'production') {
return response()->json([
'success' => false,
'message' => 'Деплой доступен только на production',
], 403);
}
if (!$request->user()->hasRole('super_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('super_admin')) {
return response()->json([
'success' => false,
'message' => 'Недостаточно прав',
], 403);
}
$status = $this->deploySiteAction->getStatus();
return response()->json($status);
}
public function log(Request $request): JsonResponse
{
if (!$request->user()->hasRole('super_admin')) {
return response()->json(['success' => false], 403);
}
$lines = (int) $request->query('lines', 50);
$result = $this->deploySiteAction->getLog($lines);
return response()->json($result);
}
public function history(Request $request): JsonResponse
{
if (!$request->user()->hasRole('super_admin')) {
return response()->json(['success' => false], 403);
}
$result = $this->deploySiteAction->getHistory();
return response()->json($result);
}
public function clear(Request $request): JsonResponse
{
if (!$request->user()->hasRole('super_admin')) {
return response()->json([
'success' => false,
'message' => 'Недостаточно прав',
], 403);
}
$this->deploySiteAction->clearStatus();
return response()->json(['success' => true]);
}
}
@@ -10,7 +10,6 @@ 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;
@@ -63,13 +62,6 @@ 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::get('/dashboard/deploy', [DeployController::class, 'index'])->name('dashboard.deploy.index');
Route::post('/dashboard/deploy', [DeployController::class, 'deploy'])->name('dashboard.deploy');
Route::get('/dashboard/deploy/status', [DeployController::class, 'status'])->name('dashboard.deploy.status');
Route::get('/dashboard/deploy/log', [DeployController::class, 'log'])->name('dashboard.deploy.log');
Route::get('/dashboard/deploy/history', [DeployController::class, 'history'])->name('dashboard.deploy.history');
Route::post('/dashboard/deploy/clear', [DeployController::class, 'clear'])->name('dashboard.deploy.clear');
Route::get('/dashboard/sveden', [SvedenController::class, 'index'])->name('dashboard.sveden');
Route::post('/dashboard/sveden', [SvedenController::class, 'store'])->name('dashboard.sveden.store');
-7
View File
@@ -118,13 +118,6 @@ return [
'replace_placeholders' => true,
],
'deploy' => [
'driver' => 'single',
'path' => storage_path('logs/deploy.log'),
'level' => 'info',
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
@@ -121,13 +121,6 @@ export const menuItems = [
{ label: 'Все пользователи', route: 'dashboard.users.index' },
],
},
{
key: 'deploy',
label: 'Деплой',
icon: 'arrow-up-tray',
route: 'dashboard.deploy.index',
activePrefixes: ['dashboard.deploy'],
},
{
key: 'vikon-updates',
label: 'Обновления VIKON',
@@ -1,266 +0,0 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { router } from '@inertiajs/vue3'
import DashboardLayout from '../Components/DashboardLayout.vue'
import FlashMessages from '../Components/shared/FlashMessages.vue'
const props = defineProps({
history: { type: Array, default: () => [] },
status: { type: Object, default: () => ({}) },
})
const deployStatus = ref(props.status)
const deployLog = ref('')
const isDeploying = ref(false)
const isPolling = ref(false)
let pollInterval = null
const statusColors = {
idle: 'text-gray-500',
running: 'text-blue-600',
completed: 'text-green-600',
failed: 'text-red-600',
unknown: 'text-yellow-600',
disabled: 'text-gray-400',
}
const statusLabels = {
idle: 'Ожидание',
running: 'Выполняется',
completed: 'Завершён',
failed: 'Ошибка',
unknown: 'Неизвестно',
disabled: 'Отключено',
}
const statusBgColors = {
idle: 'bg-gray-100',
running: 'bg-blue-100',
completed: 'bg-green-100',
failed: 'bg-red-100',
unknown: 'bg-yellow-100',
disabled: 'bg-gray-100',
}
async function startDeploy() {
if (!confirm('Запустить деплой? Приложение будет обновлено.')) {
return
}
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 data = await response.json()
if (data.success) {
isDeploying.value = true
deployStatus.value = { status: 'running', message: 'Деплой запущен...' }
startPolling()
} else {
alert(data.message || 'Ошибка запуска деплоя')
}
} catch (error) {
console.error('Deploy error:', error)
alert('Ошибка соединения с сервером: ' + error)
}
}
function startPolling() {
if (isPolling.value) return
isPolling.value = true
pollInterval = setInterval(async () => {
try {
const statusRes = await fetch(route('dashboard.deploy.status'), {
headers: { 'Accept': 'application/json' },
})
const statusData = await statusRes.json()
deployStatus.value = statusData
const logRes = await fetch(route('dashboard.deploy.log') + '?lines=100', {
headers: { 'Accept': 'application/json' },
})
const logData = await logRes.json()
deployLog.value = logData.full_log || logData.log || ''
if (statusData.status !== 'running') {
stopPolling()
isDeploying.value = false
router.reload({ only: ['history'] })
}
} catch (error) {
console.error('Polling error:', error)
}
}, 2000)
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
isPolling.value = false
}
async function clearLog() {
try {
await fetch(route('dashboard.deploy.clear'), {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
})
deployLog.value = ''
deployStatus.value = { status: 'idle', message: 'Деплой не запущен' }
} catch (error) {
console.error('Clear error:', error)
}
}
function formatDate(dateStr) {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
onMounted(() => {
if (deployStatus.value.status === 'running') {
startPolling()
}
if (deployStatus.value.log) {
deployLog.value = deployStatus.value.log
}
})
onUnmounted(() => {
stopPolling()
})
</script>
<template>
<DashboardLayout>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<FlashMessages />
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Деплой сайта</h1>
<p class="mt-1 text-sm text-gray-600">
Управление обновлением сайта на production сервере
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-sm font-medium text-gray-500 mb-2">Статус</h3>
<span
:class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
statusBgColors[deployStatus.status] || 'bg-gray-100',
statusColors[deployStatus.status] || 'text-gray-500',
]"
>
{{ statusLabels[deployStatus.status] || deployStatus.status }}
</span>
<p class="mt-2 text-sm text-gray-600">{{ deployStatus.message }}</p>
</div>
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-sm font-medium text-gray-500 mb-2">Действия</h3>
<div class="flex gap-3">
<button
@click="startDeploy"
:disabled="isDeploying || deployStatus.status === 'running'"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg v-if="isDeploying" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{{ isDeploying ? 'Выполняется...' : 'Запустить деплой' }}
</button>
<button
@click="clearLog"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Очистить лог
</button>
</div>
</div>
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-sm font-medium text-gray-500 mb-2">Информация</h3>
<dl class="space-y-1">
<div class="flex justify-between text-sm">
<dt class="text-gray-500">Деплоев выполнено:</dt>
<dd class="font-medium text-gray-900">{{ history.length }}</dd>
</div>
<div v-if="history.length > 0" class="flex justify-between text-sm">
<dt class="text-gray-500">Последний деплой:</dt>
<dd class="font-medium text-gray-900">{{ formatDate(history[0]?.timestamp) }}</dd>
</div>
</dl>
</div>
</div>
<div v-if="deployLog" class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Лог деплоя</h3>
</div>
<div class="p-6">
<pre class="bg-gray-900 text-green-400 rounded-lg p-4 overflow-auto max-h-96 text-sm font-mono whitespace-pre-wrap">{{ deployLog }}</pre>
</div>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">История деплоев</h3>
</div>
<div v-if="history.length === 0" class="p-6 text-center text-gray-500">
Деплои ещё не выполнялись
</div>
<table v-else class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дата</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Статус</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Коммит</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Запущен</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(item, index) in history" :key="index">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ formatDate(item.timestamp) }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
:class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
item.status === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800',
]"
>
{{ item.status === 'success' ? 'Успешно' : 'Ошибка' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{{ item.commit }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ item.triggered_by }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</DashboardLayout>
</template>
-115
View File
@@ -6,43 +6,6 @@
<!-- Flash Messages (shared component) -->
<FlashMessages />
<!-- Deploy Button -->
<div v-if="isProduction" class="mb-6 bg-gradient-to-r from-primary/10 to-info/10 border border-primary/20 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
<DashboardIcon name="arrow-path" size="5" class="text-primary" />
</div>
<div>
<h3 class="text-sm font-semibold text-foreground">Обновление сайта</h3>
<p class="text-xs text-muted-foreground-1">Запуск скрипта деплоя и пересборка сервера</p>
</div>
</div>
<button
type="button"
@click="deploySite"
:disabled="deploying"
class="inline-flex items-center gap-2 px-5 py-2.5 bg-amber-500 text-white text-sm font-medium rounded-lg hover:bg-amber-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md"
>
<svg v-if="deploying" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<DashboardIcon v-else name="rocket-launch" size="4" />
{{ deploying ? 'Обновление...' : 'Обновить сайт' }}
</button>
</div>
<!-- Deploy Output -->
<div v-if="deployOutput" class="mt-4 p-3 bg-surface border border-layer-line rounded-lg">
<div class="flex items-start gap-2 mb-2">
<DashboardIcon :name="deploySuccess ? 'check-circle' : 'exclamation-circle'" size="4" :class="deploySuccess ? 'text-success' : 'text-rose-500'" />
<span class="text-sm font-medium" :class="deploySuccess ? 'text-success' : 'text-rose-500'">{{ deployMessage }}</span>
</div>
<pre v-if="deployOutput" class="mt-2 p-2 bg-muted/30 rounded text-xs text-foreground overflow-auto max-h-48">{{ deployOutput }}</pre>
</div>
</div>
<!-- Stats Overview -->
<StatsOverview :stats="stats" class="mb-6" />
@@ -124,11 +87,6 @@ export default {
},
data() {
return {
deploying: false,
deployOutput: null,
deploySuccess: false,
deployStatus: null,
deployPolling: null,
domainSections: [
{
title: 'Контент сайта',
@@ -167,80 +125,7 @@ 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',