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

994 lines
44 KiB
HTML
Raw Permalink 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 %}Сборка Dockerfile - {{ dockerfile.name }} - DevOpsLab{% endblock %}
{% block page_title %}Сборка Dockerfile: {{ dockerfile.name }}{% endblock %}
{% block content %}
<input type="hidden" id="dockerfile-content-hidden" value="{{ dockerfile.content | e }}">
<div class="row">
<!-- Левая колонка: Форма сборки и текущие логи -->
<div class="col-lg-8">
<!-- Форма сборки -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-hammer me-2"></i>
Параметры сборки
</h5>
</div>
<div class="card-body">
<form id="build-form">
<div class="mb-3">
<label class="form-label">Имя образа (без тега)</label>
<input type="text" id="build-image-name" class="form-control"
value="inecs/ansible-lab:{{ dockerfile.name }}" required>
<div class="form-text">Тег будет добавлен автоматически</div>
</div>
<div class="mb-3">
<label class="form-label">Тег</label>
<input type="text" id="build-tag" class="form-control" value="latest" required>
</div>
<div class="mb-3">
<label class="form-label">Платформы для сборки</label>
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/amd64" id="build-platform-amd64" checked>
<label class="form-check-label" for="build-platform-amd64">
<i class="fab fa-linux me-1"></i>linux/amd64 (x86_64)
</label>
</div>
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/386" id="build-platform-386" checked>
<label class="form-check-label" for="build-platform-386">
<i class="fab fa-linux me-1"></i>linux/386 (x86)
</label>
</div>
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/arm64" id="build-platform-arm64" checked>
<label class="form-check-label" for="build-platform-arm64">
<i class="fab fa-apple me-1"></i>linux/arm64 (macOS M1/M2)
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/arm/v7" id="build-platform-armv7">
<label class="form-check-label" for="build-platform-armv7">
<i class="fas fa-microchip me-1"></i>linux/arm/v7
</label>
</div>
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/riscv64" id="build-platform-riscv64">
<label class="form-check-label" for="build-platform-riscv64">
<i class="fas fa-server me-1"></i>linux/riscv64
</label>
</div>
<div class="form-check">
<input class="form-check-input build-platform-checkbox" type="checkbox" name="build-platforms" value="linux/ppc64le" id="build-platform-ppc64le">
<label class="form-check-label" for="build-platform-ppc64le">
<i class="fas fa-server me-1"></i>linux/ppc64le
</label>
</div>
</div>
</div>
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
По умолчанию выбраны: linux/amd64, linux/386, linux/arm64
</div>
<input type="hidden" name="build-platforms-json" id="build-platforms-json-hidden">
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="build-no-cache" name="no_cache">
<label class="form-check-label" for="build-no-cache">
<i class="fas fa-ban me-1"></i>
Сборка без кеша (--no-cache)
</label>
</div>
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
При включении сборка будет выполняться без использования кеша Docker
</div>
</div>
<button type="button" class="btn btn-primary" onclick="startBuild()">
<i class="fas fa-hammer me-2"></i>
Начать сборку
</button>
<a href="/dockerfiles/{{ dockerfile.id }}/edit" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
</form>
</div>
</div>
<!-- Логи сборки -->
<div class="card" id="build-logs-card" style="display: none;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Логи сборки</h5>
<div class="d-flex gap-2">
<button type="button" id="push-image-btn" class="btn btn-sm btn-primary" onclick="startPush()" style="display: none;">
<i class="fas fa-upload me-1"></i>
Отправить образ
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="clearLogs()">
<i class="fas fa-times me-1"></i>
Очистить
</button>
</div>
</div>
<div class="card-body p-0">
<div class="log-container" id="build-logs" style="height: 500px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 1rem; font-family: 'Courier New', monospace; font-size: 0.875rem;">
<!-- Логи будут добавлены через WebSocket -->
</div>
</div>
</div>
</div>
<!-- Правая колонка: История сборок -->
<div class="col-lg-4">
<div class="card" id="build-history-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-history me-2"></i>
История сборок
</h5>
<a href="/dockerfiles/{{ dockerfile.id }}/build-logs" class="btn btn-sm btn-primary">
<i class="fas fa-list me-1"></i>
Все логи
</a>
</div>
<div class="card-body">
{% if recent_logs %}
<div class="list-group list-group-flush">
{% for log in recent_logs %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">
<code>{{ log.image_name }}{% if log.tag %}:{{ log.tag }}{% endif %}</code>
</h6>
<small class="text-muted">
{% if log.started_at %}
{{ log.started_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
{% if log.duration %}
• {{ log.duration }} сек
{% endif %}
</small>
<div class="mt-1">
{% if log.extra_data and log.extra_data.get('type') == 'push' %}
<span class="badge bg-info me-1">
<i class="fas fa-upload me-1"></i>Push
</span>
{% else %}
<span class="badge bg-secondary me-1">
<i class="fas fa-hammer me-1"></i>Build
</span>
{% endif %}
{% if log.status == "success" %}
<span class="badge bg-success">Успешно</span>
{% elif log.status == "failed" %}
<span class="badge bg-danger">Ошибка</span>
{% else %}
<span class="badge bg-warning">Выполняется</span>
{% endif %}
</div>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="showLogDetail({{ log.id }})" title="Просмотр логов">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteLog({{ log.id }})" title="Удалить лог">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle me-2"></i>
История сборок пуста
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Модальное окно для просмотра лога -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Детали лога сборки</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="log-detail-content" class="log-container">
Загрузка...
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора registry -->
<div class="modal fade" id="pushRegistryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Выбор Registry для отправки образа</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Registry</label>
<select id="push-registry-select" class="form-select">
<option value="docker.io">Docker Hub (hub.docker.com)</option>
<option value="harbor">Harbor</option>
</select>
</div>
<div id="push-registry-info" class="alert alert-info">
<small>Будут использованы настройки из вашего профиля</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" onclick="confirmPush()">
<i class="fas fa-upload me-1"></i>
Отправить
</button>
</div>
</div>
</div>
</div>
<script>
// Сохраненные платформы из БД
const savedPlatforms = {{ dockerfile.platforms | tojson if dockerfile.platforms else '["linux/amd64", "linux/386", "linux/arm64"]' }};
// Загружаем последние логи при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
loadRecentLogs();
// Устанавливаем высоту после загрузки DOM
});
// Функция для обновления JSON платформ
function updateBuildPlatformsJson() {
const checkboxes = document.querySelectorAll('.build-platform-checkbox:checked');
const platforms = Array.from(checkboxes).map(cb => cb.value);
document.getElementById('build-platforms-json-hidden').value = JSON.stringify(platforms);
return platforms;
}
// Инициализация чекбоксов платформ
document.querySelectorAll('.build-platform-checkbox').forEach(cb => {
cb.addEventListener('change', updateBuildPlatformsJson);
// Устанавливаем checked для сохраненных платформ
if (savedPlatforms.includes(cb.value)) {
cb.checked = true;
}
});
updateBuildPlatformsJson(); // Инициализация
// Функция для запуска сборки
window.startBuild = function() {
const imageName = document.getElementById('build-image-name').value;
const tag = document.getElementById('build-tag').value;
const dockerfileContentHidden = document.getElementById('dockerfile-content-hidden');
const dockerfileContent = dockerfileContentHidden ? dockerfileContentHidden.value : '';
const noCache = document.getElementById('build-no-cache').checked;
// Получаем выбранные платформы из чекбоксов
const platforms = updateBuildPlatformsJson();
if (platforms.length === 0) {
alert('Выберите хотя бы одну платформу для сборки!');
return;
}
// Показываем карточку с логами и прокручиваем к ней
const logsCard = document.getElementById('build-logs-card');
logsCard.style.display = 'block';
logsCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
const logsContainer = document.getElementById('build-logs');
logsContainer.innerHTML = '<div class="text-info">🔗 Подключение к WebSocket...</div>';
// Скрываем кнопку отправки при начале новой сборки
const pushButton = document.getElementById('push-image-btn');
if (pushButton) {
pushButton.style.display = 'none';
}
// Сбрасываем глобальные переменные
currentImageName = null;
currentImageTag = null;
// Подключаемся к WebSocket
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/dockerfile/build/{{ dockerfile.id }}`);
currentWebSocket = ws;
ws.onopen = () => {
logsContainer.innerHTML = '<div class="text-info">🚀 Запуск сборки...</div>';
ws.send(JSON.stringify({
image_name: imageName,
tag: tag,
platforms: platforms,
dockerfile_content: dockerfileContent,
no_cache: noCache
}));
};
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';
if (data.level === 'error') {
logLine.classList.add('log-error');
} else if (data.level === 'warning') {
logLine.classList.add('log-warning');
} else if (data.level === 'info') {
logLine.classList.add('log-info');
}
logLine.textContent = data.data;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
} else if (data.type === 'info' && data.data) {
// Сохраняем build_log_id и celery_task_id если они есть
if (data.build_log_id) {
currentBuildLogId = data.build_log_id;
}
if (data.celery_task_id) {
currentCeleryTaskId = data.celery_task_id;
}
// Показываем кнопку остановки если сборка запущена
if (data.build_log_id || data.data.includes('запущена в фоне')) {
// Проверяем, не добавлена ли уже кнопка
if (!document.getElementById('stop-build-button-container')) {
const stopButtonContainer = document.createElement('div');
stopButtonContainer.className = 'mt-2';
stopButtonContainer.id = 'stop-build-button-container';
stopButtonContainer.innerHTML = `
<button id="stop-build-btn" class="btn btn-danger btn-sm" onclick="stopBuild()">
<i class="fas fa-stop me-1"></i>
Остановить сборку
</button>
`;
logsContainer.appendChild(stopButtonContainer);
}
}
// Показываем сообщение как обычный лог
const logLine = document.createElement('div');
logLine.className = 'log-line log-info';
logLine.textContent = data.data;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
} else if (data.type === 'complete') {
const logLine = document.createElement('div');
logLine.className = 'log-line log-info';
logLine.textContent = data.data || `✅ Сборка завершена: ${data.status}`;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
ws.close();
currentWebSocket = null;
// Удаляем кнопку остановки
const stopButtonContainer = document.getElementById('stop-build-button-container');
if (stopButtonContainer) {
stopButtonContainer.remove();
}
// Если сборка успешна, показываем кнопку Push в заголовке
if (data.status === 'success' && data.image_name && data.tag) {
currentImageName = data.image_name;
currentImageTag = data.tag;
const pushButton = document.getElementById('push-image-btn');
if (pushButton) {
pushButton.style.display = 'inline-block';
}
}
// Обновляем историю сборок без перезагрузки страницы
loadRecentLogs();
} else if (data.type === 'error') {
const logLine = document.createElement('div');
logLine.className = 'log-line log-error';
logLine.textContent = data.data || '❌ Ошибка при сборке';
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
ws.close();
}
};
ws.onerror = (error) => {
const logLine = document.createElement('div');
logLine.className = 'log-line log-error';
logLine.textContent = `❌ Ошибка подключения: ${error}`;
logsContainer.appendChild(logLine);
};
ws.onclose = () => {
const logLine = document.createElement('div');
logLine.className = 'log-line log-info';
logLine.textContent = '🔌 Соединение закрыто';
logsContainer.appendChild(logLine);
};
};
function clearLogs() {
const logsContainer = document.getElementById('build-logs');
logsContainer.innerHTML = '';
// Скрываем кнопку отправки при очистке логов
const pushButton = document.getElementById('push-image-btn');
if (pushButton) {
pushButton.style.display = 'none';
}
// Сбрасываем глобальные переменные
currentImageName = null;
currentImageTag = null;
}
async function showLogDetail(logId) {
const modalElement = document.getElementById('logDetailModal');
const modal = new bootstrap.Modal(modalElement);
const content = document.getElementById('log-detail-content');
content.textContent = 'Загрузка...';
// Функция для установки высоты
const setHeight = () => {
const modalBody = document.querySelector('#logDetailModal .modal-body');
const modalHeader = document.querySelector('#logDetailModal .modal-header');
if (modalBody && modalHeader) {
const headerHeight = modalHeader.offsetHeight;
const availableHeight = window.innerHeight - headerHeight;
modalBody.style.height = availableHeight + 'px';
modalBody.style.minHeight = availableHeight + 'px';
modalBody.style.maxHeight = availableHeight + 'px';
content.style.height = availableHeight + 'px';
content.style.minHeight = availableHeight + 'px';
content.style.maxHeight = availableHeight + 'px';
}
};
// Устанавливаем высоту при открытии модального окна
modalElement.addEventListener('shown.bs.modal', function onShown() {
setHeight();
// Также устанавливаем при изменении размера окна
window.addEventListener('resize', setHeight);
modalElement.removeEventListener('shown.bs.modal', onShown);
});
// Убираем обработчик resize при закрытии
modalElement.addEventListener('hidden.bs.modal', function onHidden() {
window.removeEventListener('resize', setHeight);
modalElement.removeEventListener('hidden.bs.modal', onHidden);
});
modal.show();
try {
const response = await fetch(`/api/v1/dockerfiles/build-logs/${logId}`);
const data = await response.json();
if (data.logs) {
content.textContent = data.logs;
} else {
content.textContent = 'Логи не найдены';
}
} catch (error) {
content.textContent = `Ошибка загрузки: ${error.message}`;
}
}
// Глобальные переменные для управления сборкой
let currentWebSocket = null;
let currentBuildLogId = null;
let currentCeleryTaskId = null;
let currentImageName = null;
let currentImageTag = null;
// Функция для установки высоты правой колонки равной левой
// Функция для загрузки последних логов сборки
async function loadRecentLogs() {
try {
const response = await fetch(`/api/v1/dockerfiles/{{ dockerfile.id }}/build-logs/recent?limit=5`);
const logs = await response.json();
// Находим контейнер для истории сборок
const cardBody = document.querySelector('#build-history-card .card-body');
if (!cardBody) return;
// Проверяем, есть ли уже список
let logsContainer = cardBody.querySelector('.list-group');
if (logs.length === 0) {
// Если логов нет, показываем сообщение
cardBody.innerHTML = `
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle me-2"></i>
История сборок пуста
</div>
`;
return;
}
// Если списка нет, создаем его
if (!logsContainer) {
cardBody.innerHTML = '<div class="list-group list-group-flush"></div>';
logsContainer = cardBody.querySelector('.list-group');
}
// Очищаем текущий список
logsContainer.innerHTML = '';
// Добавляем логи
logs.forEach(log => {
const logItem = document.createElement('div');
logItem.className = 'list-group-item';
// Определяем тип операции (build или push)
const operationType = log.extra_data && log.extra_data.type === 'push' ? 'push' : 'build';
const operationBadge = operationType === 'push'
? '<span class="badge bg-info me-1"><i class="fas fa-upload me-1"></i>Push</span>'
: '<span class="badge bg-secondary me-1"><i class="fas fa-hammer me-1"></i>Build</span>';
const statusBadge = log.status === 'success'
? '<span class="badge bg-success">Успешно</span>'
: log.status === 'failed'
? '<span class="badge bg-danger">Ошибка</span>'
: '<span class="badge bg-warning">Выполняется</span>';
const startedAt = log.started_at
? new Date(log.started_at).toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
: '';
const duration = log.duration ? `${log.duration} сек` : '';
logItem.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">
<code>${log.image_name}${log.tag ? ':' + log.tag : ''}</code>
</h6>
<small class="text-muted">
${startedAt}${duration}
</small>
<div class="mt-1">
${operationBadge}
${statusBadge}
</div>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="showLogDetail(${log.id})" title="Просмотр логов">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteLog(${log.id})" title="Удалить лог">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
logsContainer.appendChild(logItem);
});
} catch (error) {
console.error('Ошибка загрузки истории сборок:', error);
}
}
// Функция для остановки сборки
async function stopBuild() {
if (!currentBuildLogId) {
alert('ID сборки не найден');
return;
}
const confirmed = await showConfirmModal('Вы уверены, что хотите остановить сборку?');
if (!confirmed) {
return;
}
try {
const response = await fetch(`/api/v1/dockerfiles/build-logs/${currentBuildLogId}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const logsContainer = document.getElementById('build-logs');
const logLine = document.createElement('div');
logLine.className = 'log-line log-warning';
logLine.textContent = '⚠️ Сборка остановлена пользователем';
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
// Удаляем кнопку остановки
const stopButtonContainer = document.getElementById('stop-build-button-container');
if (stopButtonContainer) {
stopButtonContainer.remove();
}
// Закрываем WebSocket
if (currentWebSocket) {
currentWebSocket.close();
currentWebSocket = null;
}
// Обновляем историю
setTimeout(() => {
loadRecentLogs();
}, 1000);
} else {
const errorData = await response.json();
alert(`Ошибка при остановке сборки: ${errorData.detail || 'Неизвестная ошибка'}`);
}
} catch (error) {
alert(`Ошибка при остановке сборки: ${error.message}`);
}
}
// Периодическое обновление истории сборок (каждые 5 секунд)
setInterval(() => {
loadRecentLogs();
}, 5000);
let selectedRegistry = 'docker.io';
function startPush() {
if (!currentImageName || !currentImageTag) {
alert('Информация об образе не найдена. Запустите сборку заново.');
return;
}
// Показываем модальное окно для выбора registry
const modal = new bootstrap.Modal(document.getElementById('pushRegistryModal'));
modal.show();
}
async function confirmPush() {
// Получаем выбранный registry
selectedRegistry = document.getElementById('push-registry-select').value;
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('pushRegistryModal'));
modal.hide();
// Запускаем push
await executePush();
}
async function executePush() {
if (!currentImageName || !currentImageTag) {
alert('Информация об образе не найдена. Запустите сборку заново.');
return;
}
const dockerfileId = {{ dockerfile.id }};
const pushButton = document.getElementById('push-image-btn');
if (pushButton) {
pushButton.disabled = true;
pushButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Отправка...';
}
const logsContainer = document.getElementById('build-logs');
const logLine = document.createElement('div');
logLine.className = 'log-line log-info';
logLine.textContent = `🚀 Запуск отправки образа ${currentImageName}:${currentImageTag} в ${selectedRegistry === 'docker.io' ? 'Docker Hub' : 'Harbor'}...`;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
try {
const response = await fetch(`/api/v1/dockerfiles/${dockerfileId}/push`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image_name: currentImageName,
tag: currentImageTag,
registry: selectedRegistry
})
});
// Проверяем, успешен ли ответ
if (!response.ok) {
// Пытаемся получить JSON с ошибкой
let errorMessage = 'Неизвестная ошибка';
try {
const errorData = await response.json();
// Обрабатываем разные форматы ошибок
if (Array.isArray(errorData)) {
// Если это массив ошибок (например, от Pydantic)
errorMessage = errorData.map(err => {
if (typeof err === 'string') return err;
if (err.msg) return err.msg;
if (err.message) return err.message;
if (err.detail) return err.detail;
return JSON.stringify(err);
}).join(', ');
} else if (errorData.detail) {
// Стандартный формат FastAPI
if (Array.isArray(errorData.detail)) {
errorMessage = errorData.detail.map(err => {
if (typeof err === 'string') return err;
if (err.msg) return err.msg;
if (err.message) return err.message;
return JSON.stringify(err);
}).join(', ');
} else {
errorMessage = errorData.detail;
}
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.error) {
errorMessage = errorData.error;
} else {
// Пытаемся извлечь любую строку из объекта
errorMessage = JSON.stringify(errorData);
}
} catch (e) {
// Если не удалось распарсить JSON, используем текст ответа
try {
errorMessage = await response.text() || `HTTP ${response.status}: ${response.statusText}`;
} catch (textError) {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
}
throw new Error(errorMessage);
}
const data = await response.json();
if (data.success) {
const successLine = document.createElement('div');
successLine.className = 'log-line log-success';
successLine.textContent = `${data.message}`;
logsContainer.appendChild(successLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
// Подключаемся к WebSocket для получения логов push
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/api/v1/dockerfiles/${dockerfileId}/push/ws`);
ws.onopen = () => {
// Отправляем данные образа при подключении, включая push_log_id если есть
ws.send(JSON.stringify({
image_name: currentImageName,
tag: currentImageTag,
registry: selectedRegistry,
push_log_id: data.push_log_id // Передаем ID лога из POST ответа
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log') {
const logLine = document.createElement('div');
logLine.className = 'log-line';
logLine.textContent = data.data;
logsContainer.appendChild(logLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
} else if (data.type === 'status') {
if (data.status === 'success') {
const successLine = document.createElement('div');
successLine.className = 'log-line log-success';
successLine.textContent = '✅ Образ успешно отправлен в registry';
logsContainer.appendChild(successLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
ws.close();
} else if (data.status === 'failed') {
const errorLine = document.createElement('div');
errorLine.className = 'log-line log-error';
errorLine.textContent = `❌ Ошибка при отправке: ${data.message || 'Неизвестная ошибка'}`;
logsContainer.appendChild(errorLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
ws.close();
}
}
};
ws.onerror = (error) => {
const errorLine = document.createElement('div');
errorLine.className = 'log-line log-error';
errorLine.textContent = `❌ Ошибка подключения WebSocket`;
logsContainer.appendChild(errorLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
};
ws.onclose = () => {
if (pushButton) {
pushButton.disabled = false;
pushButton.innerHTML = '<i class="fas fa-upload me-1"></i>Отправить образ';
}
};
} else {
throw new Error(data.detail || data.message || 'Неизвестная ошибка');
}
} catch (error) {
const errorLine = document.createElement('div');
errorLine.className = 'log-line log-error';
// Правильно обрабатываем ошибку
let errorMessage = 'Неизвестная ошибка';
if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
} else if (error && typeof error === 'object') {
// Пытаемся извлечь сообщение из объекта
if (error.message) {
errorMessage = error.message;
} else if (error.detail) {
errorMessage = error.detail;
} else if (Array.isArray(error)) {
errorMessage = error.map(err => {
if (typeof err === 'string') return err;
if (err.msg) return err.msg;
if (err.message) return err.message;
return JSON.stringify(err);
}).join(', ');
} else {
// Последняя попытка - преобразовать в строку
try {
errorMessage = JSON.stringify(error);
} catch (e) {
errorMessage = String(error);
}
}
} else {
errorMessage = String(error);
}
errorLine.textContent = `❌ Ошибка при запуске отправки: ${errorMessage}`;
logsContainer.appendChild(errorLine);
logsContainer.scrollTop = logsContainer.scrollHeight;
if (pushButton) {
pushButton.disabled = false;
pushButton.innerHTML = '<i class="fas fa-upload me-1"></i>Отправить образ';
}
}
}
async function deleteLog(logId) {
const confirmed = await showConfirmModal('Вы уверены, что хотите удалить этот лог сборки?');
if (!confirmed) {
return;
}
try {
const response = await fetch(`/api/v1/dockerfiles/build-logs/${logId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// Обновляем историю вместо удаления из DOM
loadRecentLogs();
} else {
const data = await response.json();
alert(`Ошибка при удалении: ${data.detail || 'Неизвестная ошибка'}`);
}
} catch (error) {
alert(`Ошибка при удалении: ${error.message}`);
}
}
</script>
<style>
.log-line {
margin-bottom: 0.25rem;
word-wrap: break-word;
}
.log-error {
color: #f48771;
}
.log-warning {
color: #dcdcaa;
}
.log-info {
color: #4ec9b0;
}
/* Стили для модального окна с логами на весь экран */
#logDetailModal .modal-dialog {
margin: 0 !important;
max-width: 100% !important;
width: 100% !important;
height: 100vh !important;
max-height: 100vh !important;
}
#logDetailModal .modal-content {
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
display: flex !important;
flex-direction: column !important;
}
#logDetailModal .modal-header {
flex-shrink: 0 !important;
padding: 1rem !important;
height: auto !important;
}
#logDetailModal .modal-body {
flex: 1 1 0 !important;
overflow: hidden !important;
padding: 0 !important;
margin: 0 !important;
width: 100% !important;
min-height: 0 !important;
max-height: none !important;
height: auto !important;
display: flex !important;
flex-direction: column !important;
position: relative !important;
}
#log-detail-content {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
min-height: 0 !important;
max-height: none !important;
box-sizing: border-box !important;
overflow: auto !important;
background: #1e1e1e !important;
color: #d4d4d4 !important;
padding: 1rem !important;
font-family: 'Courier New', monospace !important;
font-size: 0.875rem !important;
white-space: pre !important;
word-wrap: normal !important;
overflow-wrap: normal !important;
}
/* Дополнительные стили для гарантии полной высоты */
#logDetailModal.show .modal-body,
#logDetailModal.showing .modal-body {
height: calc(100vh - 120px) !important;
min-height: calc(100vh - 120px) !important;
max-height: calc(100vh - 120px) !important;
}
#logDetailModal.show #log-detail-content,
#logDetailModal.showing #log-detail-content {
height: calc(100vh - 120px) !important;
min-height: calc(100vh - 120px) !important;
max-height: calc(100vh - 120px) !important;
}
</style>
{% endblock %}