feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View File

@@ -0,0 +1,268 @@
{% extends "base.html" %}
{% block title %}{{ preset.name }} - DevOpsLab{% endblock %}
{% block page_title %}Preset: {{ preset.name }}{% endblock %}
{% block header_actions %}
<div class="btn-group">
<button
type="button"
class="btn btn-success btn-sm"
onclick="startPresetTest()"
title="Запустить тест preset'а"
>
<i class="fas fa-play me-2"></i>
Запустить
</button>
<button
type="button"
class="btn btn-warning btn-sm"
onclick="stopPresetTest()"
title="Остановить тест"
id="stop-btn"
style="display: none;"
>
<i class="fas fa-stop me-2"></i>
Остановить
</button>
<button
type="button"
class="btn btn-info btn-sm"
onclick="restartPresetTest()"
title="Перезапустить тест"
id="restart-btn"
style="display: none;"
>
<i class="fas fa-redo me-2"></i>
Перезапустить
</button>
</div>
<a href="/presets/{{ preset.name }}/edit?category={{ preset.category }}" class="btn btn-primary btn-sm">
<i class="fas fa-edit me-2"></i>
Редактировать
</a>
<a href="/presets" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12 col-lg-8">
<!-- Информация о preset'е -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Информация о preset'е</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<strong>Имя:</strong> {{ preset.name }}<br>
<strong>Категория:</strong>
<span class="badge bg-{% if preset.category == 'k8s' %}info{% else %}primary{% endif %}">
{{ preset.category }}
</span>
</div>
<div class="col-md-6">
{% if preset.data %}
<strong>Docker сеть:</strong> {{ preset.data.get('docker_network', 'labnet') }}<br>
<strong>Хостов:</strong> {{ preset.data.get('hosts', [])|length }}
{% endif %}
</div>
</div>
{% if preset.data and preset.data.get('description') %}
<div class="mb-3">
<strong>Описание:</strong>
<p class="text-muted mb-0">{{ preset.data.description }}</p>
</div>
{% endif %}
{% if preset.data and preset.data.get('hosts') %}
<div class="mb-3">
<strong>Хосты:</strong>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Имя</th>
<th>Семейство</th>
<th>Группы</th>
<th>Тип</th>
</tr>
</thead>
<tbody>
{% for host in preset.data.hosts %}
<tr>
<td>{{ host.name }}</td>
<td>
<span class="badge bg-secondary">{{ host.family }}</span>
</td>
<td>
{% if host.groups %}
{% for group in host.groups %}
<span class="badge bg-info me-1">{{ group }}</span>
{% endfor %}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>{{ host.type or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if preset.data and preset.data.get('images') %}
<div class="mb-3">
<strong>Docker образы:</strong>
<div class="d-flex flex-wrap gap-2 mt-2">
{% for key, value in preset.data.images.items() %}
<span class="badge bg-success">
<i class="fas fa-cube me-1"></i>
{{ key }}: {{ value.split(':')[-1] if ':' in value else value }}
</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- YAML содержимое -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">YAML содержимое</h5>
</div>
<div class="card-body p-0">
<pre class="mb-0" style="background: #1e1e1e; color: #d4d4d4; padding: 1rem; margin: 0; overflow-x: auto;"><code>{{ preset.content }}</code></pre>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<!-- Логи тестирования -->
<div class="card" id="test-logs-card" style="display: none;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Логи тестирования</h5>
<button
type="button"
class="btn btn-sm btn-outline-light"
onclick="clearTestLogs()"
title="Очистить логи"
>
<i class="fas fa-trash"></i>
</button>
</div>
<div class="card-body p-0">
<div class="log-container" id="test-logs" style="height: 600px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 1rem; font-family: 'Courier New', monospace; font-size: 0.75rem;">
<!-- Логи будут добавлены через WebSocket -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let testWebSocket = null;
let testRunning = false;
function startPresetTest() {
if (testRunning) {
alert('Тест уже запущен');
return;
}
// Показываем логи
const logsCard = document.getElementById('test-logs-card');
const logsContainer = document.getElementById('test-logs');
logsCard.style.display = 'block';
logsContainer.innerHTML = '';
// Показываем кнопки управления
document.getElementById('stop-btn').style.display = 'inline-block';
document.getElementById('restart-btn').style.display = 'inline-block';
testRunning = true;
// Создаем WebSocket подключение
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/preset/test/{{ preset.name }}?category={{ preset.category }}`);
testWebSocket = ws;
ws.onopen = () => {
ws.send(JSON.stringify({
action: 'start',
preset_name: '{{ preset.name }}',
preset_category: '{{ preset.category }}'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log' || data.type === 'info') {
const logLine = document.createElement('div');
logLine.className = `log-line log-${data.level || 'info'}`;
logLine.textContent = data.data;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
} else if (data.type === 'error') {
const errorLine = document.createElement('div');
errorLine.className = 'log-line log-error';
errorLine.textContent = data.data;
logsContainer.appendChild(errorLine);
} else if (data.type === 'complete') {
const completeLine = document.createElement('div');
completeLine.className = 'log-line log-info';
completeLine.textContent = data.data || '✅ Тестирование завершено';
logsContainer.appendChild(completeLine);
testRunning = false;
document.getElementById('stop-btn').style.display = 'none';
document.getElementById('restart-btn').style.display = 'none';
ws.close();
}
};
ws.onerror = (error) => {
const errorLine = document.createElement('div');
errorLine.className = 'log-line log-error';
errorLine.textContent = `❌ Ошибка подключения: ${error}`;
logsContainer.appendChild(errorLine);
testRunning = false;
};
ws.onclose = () => {
testRunning = false;
console.log('WebSocket закрыт');
};
}
function stopPresetTest() {
if (testWebSocket && testWebSocket.readyState === WebSocket.OPEN) {
testWebSocket.send(JSON.stringify({ action: 'stop' }));
testWebSocket.close();
}
testRunning = false;
document.getElementById('stop-btn').style.display = 'none';
document.getElementById('restart-btn').style.display = 'none';
}
function restartPresetTest() {
stopPresetTest();
setTimeout(() => {
startPresetTest();
}, 1000);
}
function clearTestLogs() {
document.getElementById('test-logs').innerHTML = '';
}
</script>
{% endblock %}