feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
273
app/templates/pages/deploy/index.html
Normal file
273
app/templates/pages/deploy/index.html
Normal file
@@ -0,0 +1,273 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user