Files
DevOpsLab/app/templates/pages/deploy/index.html
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

274 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Деплой - DevOpsLab{% endblock %}
{% block page_title %}Деплой на живые серверы{% endblock %}
{% block header_actions %}
<a href="/deploy/inventory" class="btn btn-secondary btn-sm">
<i class="fas fa-list me-2"></i>
Управление Inventory
</a>
{% endblock %}
{% block content %}
<div x-data="deployManager()">
<!-- Проверка inventory -->
{% if not inventory_exists %}
<div class="alert alert-warning mb-3">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Внимание:</strong> Файл inventory/hosts.ini не найден.
<a href="/deploy/inventory" class="alert-link">Создайте его</a> перед запуском деплоя.
</div>
{% endif %}
<!-- Настройки деплоя -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Настройки деплоя</h5>
</div>
<div class="card-body">
<form @submit.prevent="startDeploy">
<div class="mb-3">
<label class="form-label">Роль для деплоя</label>
<select
x-model="deployConfig.role"
class="form-select"
>
<option value="all">Все роли</option>
{% for role in roles %}
<option value="{{ role }}">{{ role }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Теги (через запятую, опционально)</label>
<input
type="text"
x-model="deployConfig.tags"
class="form-control"
placeholder="web, database, nginx"
>
</div>
<div class="mb-3">
<label class="form-label">Лимит хостов (опционально)</label>
<input
type="text"
x-model="deployConfig.limit"
class="form-control"
placeholder="webservers или web1,web2"
>
<div class="form-text">
Ограничить выполнение определенными хостами или группами
</div>
</div>
<div class="mb-3">
<label class="form-label">Опции</label>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
x-model="deployConfig.check"
id="deploy-check"
>
<label class="form-check-label" for="deploy-check">
Dry-run (проверка без изменений)
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
x-model="deployConfig.verbose"
id="deploy-verbose"
>
<label class="form-check-label" for="deploy-verbose">
Verbose режим (-vvv)
</label>
</div>
</div>
<button
type="submit"
class="btn btn-success"
:disabled="deployRunning || !inventoryExists"
>
<i class="fas fa-rocket me-2" x-show="!deployRunning"></i>
<i class="fas fa-spinner fa-spin me-2" x-show="deployRunning"></i>
<span x-show="!deployRunning">Запустить деплой</span>
<span x-show="deployRunning">Деплой выполняется...</span>
</button>
</form>
</div>
</div>
<!-- Информация о inventory -->
{% if inventory_exists and inventory_data %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Информация о Inventory</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<h6 class="fw-semibold mb-2">Группы</h6>
<ul class="list-unstyled">
{% for group, hosts in inventory_data.groups.items() %}
<li class="mb-1">
<i class="fas fa-server me-2 text-muted"></i>
<strong>{{ group }}</strong>: {{ hosts|length }} хост(ов)
</li>
{% endfor %}
</ul>
</div>
<div class="col-12 col-md-6">
<h6 class="fw-semibold mb-2">Всего хостов</h6>
<p class="display-6 fw-bold">{{ inventory_data.hosts|length }}</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Live логи -->
<div class="card" x-show="deployRunning || logs.length > 0">
<div class="card-header">
<h5 class="mb-0">Логи деплоя</h5>
</div>
<div class="card-body">
<div class="log-container" id="deploy-logs" x-ref="logContainer" style="max-height: 500px; overflow-y: auto;">
<template x-for="log in logs" :key="log.id">
<div
class="log-line"
:class="{
'log-error': log.type === 'error',
'log-warning': log.type === 'warning',
'log-info': log.type === 'info'
}"
x-text="log.data"
></div>
</template>
</div>
<div class="mt-3 d-flex gap-2">
<button
@click="clearLogs"
class="btn btn-outline-secondary"
>
<i class="fas fa-trash me-2"></i>
Очистить
</button>
<button
@click="downloadLogs"
class="btn btn-outline-secondary"
>
<i class="fas fa-download me-2"></i>
Скачать логи
</button>
</div>
</div>
</div>
</div>
<script>
function deployManager() {
return {
deployRunning: false,
logs: [],
inventoryExists: {{ 'true' if inventory_exists else 'false' }},
deployConfig: {
role: 'all',
tags: '',
limit: '',
check: false,
verbose: false
},
ws: null,
async startDeploy() {
if (!this.inventoryExists) {
alert('Сначала создайте inventory файл!');
return;
}
this.deployRunning = true;
this.logs = [];
// Формирование deploy_id
const deployId = `deploy-${this.deployConfig.role}-${this.deployConfig.tags || 'none'}`;
// Создание WebSocket подключения
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws/deploy/${deployId}`);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log' || data.type === 'info') {
this.logs.push({
id: Date.now() + Math.random(),
type: data.level || this.detectLogLevel(data.data),
data: data.data
});
// Автоскролл
this.$nextTick(() => {
const container = this.$refs.logContainer;
if (container) {
container.scrollTop = container.scrollHeight;
}
});
} else if (data.type === 'complete') {
this.deployRunning = false;
this.logs.push({
id: Date.now(),
type: 'info',
data: data.data || `✅ Деплой завершен: ${data.status}`
});
this.ws.close();
} else if (data.type === 'error') {
this.deployRunning = false;
this.logs.push({
id: Date.now(),
type: 'error',
data: data.data || `❌ Ошибка`
});
this.ws.close();
}
};
this.ws.onerror = (error) => {
this.deployRunning = false;
this.logs.push({
id: Date.now(),
type: 'error',
data: `❌ Ошибка подключения: ${error}`
});
};
},
detectLogLevel(line) {
const lower = line.toLowerCase();
if (lower.includes('error') || lower.includes('failed') || lower.includes('fatal')) return 'error';
if (lower.includes('warning') || lower.includes('warn')) return 'warning';
if (lower.includes('changed') || lower.includes('ok')) return 'info';
return 'debug';
},
clearLogs() {
this.logs = [];
},
downloadLogs() {
const content = this.logs.map(l => l.data).join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `deploy-${Date.now()}.log`;
a.click();
URL.revokeObjectURL(url);
}
}
}
</script>
{% endblock %}