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,153 @@
{% extends "base.html" %}
{% block title %}Настройки Docker - DevOpsLab{% endblock %}
{% block page_title %}Настройки Docker (Harbor и Docker Hub){% endblock %}
{% block header_actions %}
<a href="/profile" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад к профилю
</a>
{% endblock %}
{% block content %}
<div class="row">
<!-- Docker Hub настройки -->
<div class="col-12 col-lg-6 mb-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fab fa-docker me-2"></i>
Docker Hub
</h5>
</div>
<div class="card-body">
<form
hx-post="/api/v1/profile/docker-settings"
hx-target="#docker-result"
hx-swap="innerHTML"
>
<div class="mb-3">
<label class="form-label">Имя пользователя</label>
<input
type="text"
name="dockerhub_username"
value="{{ profile.dockerhub_username if profile and profile.dockerhub_username else '' }}"
class="form-control"
placeholder="username"
>
</div>
<div class="mb-3">
<label class="form-label">Пароль / Access Token</label>
<input
type="password"
name="dockerhub_password"
value=""
class="form-control"
placeholder="Оставьте пустым, чтобы не изменять"
>
<div class="form-text">
Используйте Access Token вместо пароля для большей безопасности
</div>
</div>
<div class="mb-3">
<label class="form-label">Репозиторий по умолчанию</label>
<input
type="text"
name="dockerhub_repository"
value="{{ profile.dockerhub_repository if profile and profile.dockerhub_repository else '' }}"
class="form-control"
placeholder="ansible-lab"
>
<div class="form-text">
Имя репозитория (без namespace)
</div>
</div>
<div id="docker-result" class="mb-3"></div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save me-2"></i>
Сохранить настройки Docker Hub
</button>
</form>
</div>
</div>
</div>
<!-- Harbor настройки -->
<div class="col-12 col-lg-6 mb-3">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-server me-2"></i>
Harbor
</h5>
</div>
<div class="card-body">
<form
hx-post="/api/v1/profile/docker-settings"
hx-target="#harbor-result"
hx-swap="innerHTML"
>
<div class="mb-3">
<label class="form-label">URL Harbor</label>
<input
type="url"
name="harbor_url"
value="{{ profile.harbor_url if profile and profile.harbor_url else '' }}"
class="form-control"
placeholder="https://harbor.example.com"
>
</div>
<div class="mb-3">
<label class="form-label">Имя пользователя</label>
<input
type="text"
name="harbor_username"
value="{{ profile.harbor_username if profile and profile.harbor_username else '' }}"
class="form-control"
placeholder="admin"
>
</div>
<div class="mb-3">
<label class="form-label">Пароль</label>
<input
type="password"
name="harbor_password"
value=""
class="form-control"
placeholder="Оставьте пустым, чтобы не изменять"
>
</div>
<div class="mb-3">
<label class="form-label">Проект</label>
<input
type="text"
name="harbor_project"
value="{{ profile.harbor_project if profile and profile.harbor_project else '' }}"
class="form-control"
placeholder="library"
>
<div class="form-text">
Имя проекта в Harbor
</div>
</div>
<div id="harbor-result" class="mb-3"></div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save me-2"></i>
Сохранить настройки Harbor
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,340 @@
{% extends "base.html" %}
{% block title %}Профиль - DevOpsLab{% endblock %}
{% block page_title %}Профиль пользователя{% endblock %}
{% block header_actions %}
<a href="/" 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-4 mb-3">
<div class="card">
<div class="card-body text-center">
<div class="mb-3">
<div class="avatar-circle mx-auto mb-3" style="width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 2.5rem; font-weight: bold;">
{{ user.username[0].upper() }}
</div>
<h4 class="mb-1">{{ profile.full_name if profile and profile.full_name else user.username }}</h4>
<p class="text-muted mb-0">@{{ user.username }}</p>
</div>
<div class="d-grid gap-2 mb-3">
<span class="badge {% if user.is_superuser %}bg-danger{% else %}bg-secondary{% endif %}">
{% if user.is_superuser %}
<i class="fas fa-crown me-1"></i>Администратор
{% else %}
<i class="fas fa-user me-1"></i>Пользователь
{% endif %}
</span>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{% if user.is_active %}
<i class="fas fa-check-circle me-1"></i>Активен
{% else %}
<i class="fas fa-times-circle me-1"></i>Неактивен
{% endif %}
</span>
</div>
<hr>
<div class="text-start">
<div class="mb-2">
<small class="text-muted d-block">Дата регистрации</small>
<strong>{{ user.created_at.strftime('%d.%m.%Y') if user.created_at else 'N/A' }}</strong>
</div>
{% if user.updated_at %}
<div class="mb-2">
<small class="text-muted d-block">Последнее обновление</small>
<strong>{{ user.updated_at.strftime('%d.%m.%Y %H:%M') }}</strong>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Статистика -->
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">Статистика</h6>
</div>
<div class="card-body">
<div class="row g-2 text-center">
<div class="col-6">
<div class="p-2 bg-light rounded">
<div class="h4 mb-0 text-primary">{{ stats.presets }}</div>
<small class="text-muted">Preset'ов</small>
</div>
</div>
<div class="col-6">
<div class="p-2 bg-light rounded">
<div class="h4 mb-0 text-info">{{ stats.dockerfiles }}</div>
<small class="text-muted">Dockerfile'ов</small>
</div>
</div>
<div class="col-6">
<div class="p-2 bg-light rounded">
<div class="h4 mb-0 text-success">{{ stats.playbooks }}</div>
<small class="text-muted">Playbook'ов</small>
</div>
</div>
<div class="col-6">
<div class="p-2 bg-light rounded">
<div class="h4 mb-0 text-warning">{{ stats.commands }}</div>
<small class="text-muted">Команд</small>
</div>
</div>
</div>
</div>
</div>
<!-- Быстрые ссылки -->
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">Быстрые действия</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/change-password" class="btn btn-outline-primary btn-sm">
<i class="fas fa-key me-2"></i>
Сменить пароль
</a>
<a href="/profile/docker-settings" class="btn btn-outline-info btn-sm">
<i class="fab fa-docker me-2"></i>
Настройки Docker
</a>
</div>
</div>
</div>
</div>
<!-- Основной контент с вкладками -->
<div class="col-12 col-lg-8">
<div class="card">
<!-- Навигация по вкладкам -->
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="view-tab"
data-bs-toggle="tab"
data-bs-target="#view"
type="button"
role="tab"
>
<i class="fas fa-eye me-2"></i>
Просмотр
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="edit-tab"
data-bs-toggle="tab"
data-bs-target="#edit"
type="button"
role="tab"
>
<i class="fas fa-edit me-2"></i>
Редактирование
</button>
</li>
</ul>
<div class="tab-content">
<!-- Вкладка просмотра -->
<div class="tab-pane fade show active" id="view" role="tabpanel">
<div class="card-body">
<h5 class="mb-4">Информация о профиле</h5>
<div class="row mb-3">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">Имя пользователя</label>
<div class="fw-semibold">{{ user.username }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">Полное имя</label>
<div class="fw-semibold">
{{ profile.full_name if profile and profile.full_name else 'Не указано' }}
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">Email</label>
<div class="fw-semibold">
{% if profile and profile.email %}
<a href="mailto:{{ profile.email }}">{{ profile.email }}</a>
{% else %}
Не указан
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">Роль</label>
<div>
{% if user.is_superuser %}
<span class="badge bg-danger">Администратор</span>
{% else %}
<span class="badge bg-secondary">Пользователь</span>
{% endif %}
</div>
</div>
</div>
</div>
<hr>
<h6 class="mb-3">Настройки Docker</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">
<i class="fab fa-docker me-1"></i>
Docker Hub
</label>
<div>
{% if profile and profile.dockerhub_username %}
<span class="badge bg-info">{{ profile.dockerhub_username }}</span>
{% if profile.dockerhub_repository %}
<span class="text-muted">/ {{ profile.dockerhub_repository }}</span>
{% endif %}
{% else %}
<span class="text-muted">Не настроено</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted small">
<i class="fas fa-server me-1"></i>
Harbor
</label>
<div>
{% if profile and profile.harbor_url %}
<span class="badge bg-success">{{ profile.harbor_url }}</span>
{% if profile.harbor_project %}
<span class="text-muted">/ {{ profile.harbor_project }}</span>
{% endif %}
{% else %}
<span class="text-muted">Не настроено</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="d-flex gap-2">
<a href="/profile/docker-settings" class="btn btn-outline-primary btn-sm">
<i class="fab fa-docker me-2"></i>
Настроить Docker
</a>
</div>
</div>
</div>
<!-- Вкладка редактирования -->
<div class="tab-pane fade" id="edit" role="tabpanel">
<div class="card-body">
<h5 class="mb-4">Редактирование профиля</h5>
<form
hx-post="/api/v1/profile"
hx-target="#result"
hx-swap="innerHTML"
>
<div class="mb-3">
<label class="form-label">Имя пользователя</label>
<input
type="text"
value="{{ user.username }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Имя пользователя нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Полное имя</label>
<input
type="text"
name="full_name"
value="{{ profile.full_name if profile and profile.full_name else '' }}"
class="form-control"
placeholder="Иван Иванов"
>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input
type="email"
name="email"
value="{{ profile.email if profile and profile.email else '' }}"
class="form-control"
placeholder="user@example.com"
>
</div>
<div id="result" class="mb-3"></div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить изменения
</button>
<button type="button" class="btn btn-secondary" onclick="location.reload()">
<i class="fas fa-times me-2"></i>
Отмена
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Обработка успешного сохранения профиля
document.body.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.target.id === 'result' && event.detail.xhr.status === 200) {
const response = JSON.parse(event.detail.xhr.responseText);
if (response.success) {
// Показываем сообщение об успехе
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = '<div class="alert alert-success alert-dismissible fade show" role="alert">' +
'<i class="fas fa-check-circle me-2"></i>' + response.message +
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' +
'</div>';
// Обновляем страницу через 1.5 секунды
setTimeout(() => {
location.reload();
}, 1500);
}
}
});
});
</script>
{% endblock %}