mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-11 12:09:36 +00:00
Add Update Manager for automated Part-DB updates
This feature adds a comprehensive Update Manager similar to Mainsail's update system, allowing administrators to update Part-DB directly from the web interface. Features: - Web UI at /admin/update-manager showing current and available versions - Support for Git-based installations with automatic update execution - Maintenance mode during updates to prevent user access - Automatic database backup before updates - Git rollback points for recovery (tags created before each update) - Progress tracking with real-time status updates - Update history and log viewing - Downgrade support with appropriate UI messaging - CLI command `php bin/console partdb:update` for server-side updates New files: - UpdateManagerController: Handles all web UI routes - UpdateCommand: CLI command for running updates - UpdateExecutor: Core update execution logic with safety mechanisms - UpdateChecker: GitHub API integration for version checking - InstallationTypeDetector: Detects installation type (Git/Docker/ZIP) - MaintenanceModeSubscriber: Blocks user access during maintenance - UpdateExtension: Twig functions for update notifications UI improvements: - Update notification in navbar for admins when update available - Confirmation dialogs for update/downgrade actions - Downgrade-specific text throughout the interface - Progress page with auto-refresh
This commit is contained in:
parent
ae4c0786b2
commit
42fe781ef8
16 changed files with 4126 additions and 0 deletions
|
|
@ -74,6 +74,19 @@
|
|||
|
||||
|
||||
<ul class="navbar-nav ms-3" id="login-content">
|
||||
{# Update notification badge #}
|
||||
{% if is_update_available() %}
|
||||
<li class="nav-item me-2">
|
||||
<a href="{{ path('admin_update_manager') }}" class="nav-link position-relative"
|
||||
title="{% trans %}update_manager.new_version_available.title{% endtrans %}: {{ get_latest_version() }}">
|
||||
<i class="fas fa-arrow-circle-up text-success"></i>
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-success" style="font-size: 0.6rem;">
|
||||
{% trans %}update_manager.new{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="dropdown-toggle link-anchor nav-link" data-bs-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false" id="navbar-user-dropdown-btn" data-bs-reference="window">
|
||||
|
|
|
|||
374
templates/admin/update_manager/index.html.twig
Normal file
374
templates/admin/update_manager/index.html.twig
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}Part-DB {% trans %}update_manager.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-cloud-download-alt"></i> Part-DB {% trans %}update_manager.title{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div>
|
||||
|
||||
{# Maintenance Mode Warning #}
|
||||
{% if is_maintenance %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
<strong>{% trans %}update_manager.maintenance_mode_active{% endtrans %}</strong>
|
||||
{% if maintenance_info.reason is defined %}
|
||||
- {{ maintenance_info.reason }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Lock Warning #}
|
||||
{% if is_locked %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<strong>{% trans %}update_manager.update_in_progress{% endtrans %}</strong>
|
||||
{% if lock_info.started_at is defined %}
|
||||
({% trans %}update_manager.started_at{% endtrans %}: {{ lock_info.started_at }})
|
||||
{% endif %}
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="alert-link ms-2">
|
||||
{% trans %}update_manager.view_progress{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Current Version Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans %}update_manager.current_installation{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" style="width: 40%">{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<td>
|
||||
<span class="badge bg-primary fs-6">{{ status.current_version }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.installation_type{% endtrans %}</th>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ status.installation.type_name }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% if status.git.is_git_install %}
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.git_branch{% endtrans %}</th>
|
||||
<td><code>{{ status.git.branch ?? 'N/A' }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.git_commit{% endtrans %}</th>
|
||||
<td><code>{{ status.git.commit ?? 'N/A' }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.local_changes{% endtrans %}</th>
|
||||
<td>
|
||||
{% if status.git.has_local_changes %}
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>{% trans %}update_manager.yes{% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-check me-1"></i>{% trans %}update_manager.no{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.auto_update_supported{% endtrans %}</th>
|
||||
<td>
|
||||
{% if status.can_auto_update %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-check me-1"></i>{% trans %}update_manager.yes{% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fas fa-times me-1"></i>{% trans %}update_manager.no{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<form action="{{ path('admin_update_manager_refresh') }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_refresh') }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.refresh{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Latest Version / Update Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100 {{ status.update_available ? 'border-success' : '' }}">
|
||||
<div class="card-header {{ status.update_available ? 'bg-success text-white' : '' }}">
|
||||
{% if status.update_available %}
|
||||
<i class="fas fa-gift me-2"></i>{% trans %}update_manager.new_version_available.title{% endtrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans %}update_manager.latest_release{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if status.latest_version %}
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-{{ status.update_available ? 'success' : 'primary' }} fs-4 px-4 py-2">
|
||||
{{ status.latest_tag }}
|
||||
</span>
|
||||
{% if not status.update_available %}
|
||||
<p class="text-success mt-2 mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans %}update_manager.already_up_to_date{% endtrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if status.update_available and status.can_auto_update and validation.valid %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post" onsubmit="return confirm('{% trans %}update_manager.confirm_update{% endtrans %}');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ status.latest_tag }}">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% trans %}update_manager.update_to{% endtrans %} {{ status.latest_tag }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="backup" value="1" id="create-backup" checked>
|
||||
<label class="form-check-label" for="create-backup">
|
||||
<i class="fas fa-database me-1"></i> {% trans %}update_manager.create_backup{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if status.published_at %}
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-question-circle fa-3x mb-3"></i>
|
||||
<p>{% trans %}update_manager.could_not_fetch_releases{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if status.latest_tag %}
|
||||
<div class="card-footer">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: status.latest_tag}) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans %}update_manager.view_release_notes{% endtrans %}
|
||||
</a>
|
||||
{% if status.release_url %}
|
||||
<a href="{{ status.release_url }}" class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Validation Issues #}
|
||||
{% if not validation.valid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans %}update_manager.validation_issues{% endtrans %}
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Available Versions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags me-2"></i>{% trans %}update_manager.available_versions{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.released{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for release in all_releases %}
|
||||
<tr{% if release.version == status.current_version %} class="table-active"{% endif %}>
|
||||
<td>
|
||||
<code>{{ release.tag }}</code>
|
||||
{% if release.prerelease %}
|
||||
<span class="badge bg-warning text-dark ms-1">pre</span>
|
||||
{% endif %}
|
||||
{% if release.version == status.current_version %}
|
||||
<span class="badge bg-primary ms-1">{% trans %}update_manager.current{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ release.published_at|date('Y-m-d') }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: release.tag}) }}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans %}update_manager.view_release_notes{% endtrans %}">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</a>
|
||||
{% if release.version != status.current_version and status.can_auto_update and validation.valid %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post" class="d-inline"
|
||||
onsubmit="return confirm('{% if release.version > status.current_version %}{% trans %}update_manager.confirm_update{% endtrans %}{% else %}{% trans %}update_manager.confirm_downgrade{% endtrans %}{% endif %}');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ release.tag }}">
|
||||
<input type="hidden" name="backup" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-{{ release.version > status.current_version ? 'outline-success' : 'outline-warning' }}"
|
||||
title="{% trans %}update_manager.switch_to{% endtrans %} {{ release.tag }}">
|
||||
{% if release.version > status.current_version %}
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted py-3">
|
||||
{% trans %}update_manager.no_releases_found{% endtrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Update History & Backups #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#logs-tab" type="button">
|
||||
<i class="fas fa-history me-1"></i>{% trans %}update_manager.update_logs{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backups-tab" type="button">
|
||||
<i class="fas fa-archive me-1"></i>{% trans %}update_manager.backups{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="logs-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.log_file{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in update_logs %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
{{ log.date|date('Y-m-d H:i') }}
|
||||
</td>
|
||||
<td><code class="small">{{ log.file }}</code></td>
|
||||
<td>
|
||||
<a href="{{ path('admin_update_manager_log', {filename: log.file}) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted py-3">
|
||||
{% trans %}update_manager.no_logs_found{% endtrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.file{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.size{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
{{ backup.date|date('Y-m-d H:i') }}
|
||||
</td>
|
||||
<td><code class="small">{{ backup.file }}</code></td>
|
||||
<td class="text-muted small">
|
||||
{{ (backup.size / 1024 / 1024)|number_format(1) }} MB
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted py-3">
|
||||
{% trans %}update_manager.no_backups_found{% endtrans %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Non-auto-update installations info #}
|
||||
{% if not status.can_auto_update %}
|
||||
<div class="alert alert-secondary">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>{{ status.installation.type_name }}
|
||||
</h6>
|
||||
<p class="mb-0">{{ status.installation.update_instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
templates/admin/update_manager/log_viewer.html.twig
Normal file
40
templates/admin/update_manager/log_viewer.html.twig
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}{{ filename }} - {% trans %}update_manager.log_viewer{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-file-code"></i> {{ filename }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div class="mb-4">
|
||||
<a href="{{ path('admin_update_manager') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans %}update_manager.back_to_update_manager{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-terminal me-2"></i>{% trans %}update_manager.update_log{% endtrans %}
|
||||
</span>
|
||||
<span class="badge bg-secondary">{{ content|length }} {% trans %}update_manager.bytes{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<pre class="bg-dark text-light p-3 mb-0" style="max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;"><code>{{ content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
pre code {
|
||||
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Highlight different log levels */
|
||||
pre code {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
196
templates/admin/update_manager/progress.html.twig
Normal file
196
templates/admin/update_manager/progress.html.twig
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_title{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.title{% endtrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
{% if progress and progress.status == 'running' %}
|
||||
<i class="fas fa-sync-alt fa-spin"></i>
|
||||
{% elseif progress and progress.status == 'completed' %}
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
{% elseif progress and progress.status == 'failed' %}
|
||||
<i class="fas fa-times-circle text-danger"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-hourglass-start"></i>
|
||||
{% endif %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_title{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.title{% endtrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
{# Auto-refresh while update is running - also refresh when 'starting' status #}
|
||||
{% if not progress or progress.status == 'running' or progress.status == 'starting' %}
|
||||
<meta http-equiv="refresh" content="2">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div id="update-progress">
|
||||
|
||||
{# Progress Header #}
|
||||
<div class="text-center mb-4">
|
||||
<div class="mb-3">
|
||||
{% if progress and progress.status == 'completed' %}
|
||||
<i class="fas fa-check-circle fa-3x text-success"></i>
|
||||
{% elseif progress and progress.status == 'failed' %}
|
||||
<i class="fas fa-times-circle fa-3x text-danger"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-cog fa-spin fa-3x text-primary"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4>
|
||||
{% if progress and progress.status == 'running' %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrading{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.updating{% endtrans %}
|
||||
{% endif %}
|
||||
{% elseif progress and progress.status == 'completed' %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_completed{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.completed{% endtrans %}
|
||||
{% endif %}
|
||||
{% elseif progress and progress.status == 'failed' %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_failed{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.failed{% endtrans %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.initializing{% endtrans %}
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p class="text-muted">
|
||||
{% if progress %}
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans with {'%version%': progress.target_version|default('unknown')} %}update_manager.progress.downgrading_to{% endtrans %}
|
||||
{% else %}
|
||||
{% trans with {'%version%': progress.target_version|default('unknown')} %}update_manager.progress.updating_to{% endtrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Progress Bar #}
|
||||
{% set percent = progress ? ((progress.current_step|default(0) / progress.total_steps|default(12)) * 100)|round : 0 %}
|
||||
{% if progress and progress.status == 'completed' %}
|
||||
{% set percent = 100 %}
|
||||
{% endif %}
|
||||
<div class="progress mb-4" style="height: 25px;">
|
||||
<div class="progress-bar {% if progress and progress.status == 'completed' %}bg-success{% elseif progress and progress.status == 'failed' %}bg-danger{% else %}progress-bar-striped progress-bar-animated{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ percent }}%"
|
||||
aria-valuenow="{{ progress.current_step|default(0) }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="{{ progress.total_steps|default(12) }}">
|
||||
{{ percent }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Current Step - shows what's currently being worked on #}
|
||||
{% if progress and (progress.status == 'running' or progress.status == 'starting') %}
|
||||
<div class="alert alert-info mb-4">
|
||||
<strong>{{ progress.step_name|default('initializing')|replace({'_': ' '})|capitalize }}</strong>:
|
||||
{{ progress.step_message|default('Processing...') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Error Message #}
|
||||
{% if progress and progress.status == 'failed' %}
|
||||
<div class="alert alert-danger mb-4">
|
||||
<strong>{% trans %}update_manager.progress.error{% endtrans %}:</strong>
|
||||
{{ progress.error|default('An unknown error occurred') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Success Message #}
|
||||
{% if progress and progress.status == 'completed' %}
|
||||
<div class="alert alert-success mb-4">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_success_message{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.success_message{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Steps Timeline #}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-list-ol me-2"></i>
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_steps{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.steps{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if progress and progress.steps %}
|
||||
{% for step in progress.steps %}
|
||||
<li class="list-group-item d-flex align-items-center">
|
||||
{% if step.success %}
|
||||
<i class="fas fa-check-circle text-success me-3"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-times-circle text-danger me-3"></i>
|
||||
{% endif %}
|
||||
<div class="flex-grow-1">
|
||||
<strong>{{ step.step|replace({'_': ' '})|capitalize }}</strong>
|
||||
<br><small class="text-muted">{{ step.message }}</small>
|
||||
</div>
|
||||
<small class="text-muted">{{ step.timestamp|date('H:i:s') }}</small>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item text-center text-muted py-3">
|
||||
<i class="fas fa-clock me-2"></i>{% trans %}update_manager.progress.waiting{% endtrans %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Actions #}
|
||||
<div class="text-center">
|
||||
{% if progress and (progress.status == 'completed' or progress.status == 'failed') %}
|
||||
<a href="{{ path('admin_update_manager') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans %}update_manager.progress.back{% endtrans %}
|
||||
</a>
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.progress.refresh_page{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Warning Notice #}
|
||||
{% if not progress or progress.status == 'running' or progress.status == 'starting' %}
|
||||
<div class="alert alert-warning mt-4">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>{% trans %}update_manager.progress.warning{% endtrans %}:</strong>
|
||||
{% if is_downgrade|default(false) %}
|
||||
{% trans %}update_manager.progress.downgrade_do_not_close{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}update_manager.progress.do_not_close{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# JavaScript refresh - more reliable than meta refresh #}
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
110
templates/admin/update_manager/release_notes.html.twig
Normal file
110
templates/admin/update_manager/release_notes.html.twig
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}{{ release.name }} - {% trans %}update_manager.release_notes{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-file-alt"></i> {{ release.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div class="mb-4">
|
||||
<a href="{{ path('admin_update_manager') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans %}update_manager.back_to_update_manager{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th style="width: 30%">{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<td>
|
||||
<span class="badge bg-primary fs-6">{{ release.version }}</span>
|
||||
{% if release.prerelease %}
|
||||
<span class="badge bg-warning text-dark ms-1">{% trans %}update_manager.prerelease{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.tag{% endtrans %}</th>
|
||||
<td><code>{{ release.tag }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.released{% endtrans %}</th>
|
||||
<td>{{ release.published_at|date('Y-m-d H:i') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.status{% endtrans %}</th>
|
||||
<td>
|
||||
{% if release.version == current_version %}
|
||||
<span class="badge bg-primary">{% trans %}update_manager.current{% endtrans %}</span>
|
||||
{% elseif release.version > current_version %}
|
||||
<span class="badge bg-success">{% trans %}update_manager.newer{% endtrans %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% trans %}update_manager.older{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<a href="{{ release.url }}" class="btn btn-primary" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> {% trans %}update_manager.view_on_github{% endtrans %}
|
||||
</a>
|
||||
{% if release.zipball_url %}
|
||||
<a href="{{ release.zipball_url }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-download me-1"></i> ZIP
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if release.assets is not empty %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-paperclip me-2"></i>{% trans %}update_manager.download_assets{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for asset in release.assets %}
|
||||
<li class="mb-2">
|
||||
<a href="{{ asset.url }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-download me-1"></i> {{ asset.name }}
|
||||
</a>
|
||||
<span class="text-muted ms-2">({{ (asset.size / 1024 / 1024)|number_format(1) }} MB)</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-list-ul me-2"></i>{% trans %}update_manager.changelog{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if release.body %}
|
||||
<div class="markdown-body">
|
||||
{{ release.body|markdown_to_html }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">{% trans %}update_manager.no_release_notes{% endtrans %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if release.version > current_version %}
|
||||
<div class="card mt-4 border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="fas fa-arrow-up me-2"></i>{% trans %}update_manager.update_to_this_version{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{% trans %}update_manager.run_command_to_update{% endtrans %}</p>
|
||||
<div class="bg-dark text-light p-3 rounded">
|
||||
<code class="text-info">php bin/console partdb:update {{ release.tag }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
251
templates/maintenance/maintenance.html.twig
Normal file
251
templates/maintenance/maintenance.html.twig
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="refresh" content="15">
|
||||
<title>Part-DB - {% trans %}update_manager.maintenance.title{% endtrans %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.maintenance-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
padding: 50px;
|
||||
max-width: 550px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 30px;
|
||||
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.icon-container i {
|
||||
font-size: 50px;
|
||||
color: #1a1a2e;
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.4); }
|
||||
50% { transform: scale(1.05); box-shadow: 0 0 30px 10px rgba(0, 212, 255, 0.2); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.reason-badge {
|
||||
background: rgba(0, 212, 255, 0.15);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
display: inline-block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
||||
animation: progressAnim 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progressAnim {
|
||||
0% { width: 0%; margin-left: 0%; }
|
||||
50% { width: 40%; margin-left: 30%; }
|
||||
100% { width: 0%; margin-left: 100%; }
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 2rem;
|
||||
color: #00ff88;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-steps {
|
||||
text-align: left;
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.status-step {
|
||||
padding: 8px 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-step i {
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.status-step.active {
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.status-step.completed {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.refresh-info {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.refresh-info i {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.logo {
|
||||
opacity: 0.3;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="maintenance-card">
|
||||
<div class="icon-container">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
|
||||
<h1>{% trans %}update_manager.maintenance.heading{% endtrans %}</h1>
|
||||
|
||||
<p class="text-secondary fs-5">
|
||||
{% trans %}update_manager.maintenance.description{% endtrans %}
|
||||
</p>
|
||||
|
||||
<div class="reason-badge">
|
||||
<i class="fas fa-arrow-up me-2"></i>
|
||||
{{ reason }}
|
||||
</div>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-steps">
|
||||
<div class="status-step" id="step-backup">
|
||||
<i class="fas fa-database"></i>
|
||||
<span>{% trans %}update_manager.maintenance.step_backup{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="status-step" id="step-download">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>{% trans %}update_manager.maintenance.step_download{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="status-step" id="step-install">
|
||||
<i class="fas fa-box-open"></i>
|
||||
<span>{% trans %}update_manager.maintenance.step_install{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="status-step" id="step-migrate">
|
||||
<i class="fas fa-database"></i>
|
||||
<span>{% trans %}update_manager.maintenance.step_migrate{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="status-step" id="step-cache">
|
||||
<i class="fas fa-sync"></i>
|
||||
<span>{% trans %}update_manager.maintenance.step_cache{% endtrans %}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if duration is not null %}
|
||||
<div class="timer" id="timer">
|
||||
{{ duration // 60 }}:{{ '%02d'|format(duration % 60) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="refresh-info">
|
||||
<i class="fas fa-sync-alt me-1"></i>
|
||||
{% trans %}update_manager.maintenance.auto_refresh{% endtrans %}
|
||||
</p>
|
||||
|
||||
<div class="logo">
|
||||
<i class="fa fa-microchip me-2"></i> Part-DB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simulate step progression
|
||||
const steps = ['step-backup', 'step-download', 'step-install', 'step-migrate', 'step-cache'];
|
||||
let currentStep = 0;
|
||||
|
||||
function updateSteps() {
|
||||
steps.forEach((stepId, index) => {
|
||||
const step = document.getElementById(stepId);
|
||||
if (index < currentStep) {
|
||||
step.classList.add('completed');
|
||||
step.classList.remove('active');
|
||||
step.querySelector('i').className = 'fas fa-check-circle';
|
||||
} else if (index === currentStep) {
|
||||
step.classList.add('active');
|
||||
step.querySelector('i').className = 'fas fa-spinner fa-spin';
|
||||
}
|
||||
});
|
||||
|
||||
currentStep = (currentStep + 1) % (steps.length + 1);
|
||||
if (currentStep === 0) {
|
||||
steps.forEach(stepId => {
|
||||
const step = document.getElementById(stepId);
|
||||
step.classList.remove('completed', 'active');
|
||||
step.querySelector('i').className = step.querySelector('i').className.replace('fa-check-circle', 'fas');
|
||||
step.querySelector('i').className = step.querySelector('i').className.replace('fa-spinner fa-spin', 'fas');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(updateSteps, 3000);
|
||||
|
||||
// Update timer
|
||||
{% if duration is not null %}
|
||||
let seconds = {{ duration }};
|
||||
const timerEl = document.getElementById('timer');
|
||||
|
||||
setInterval(() => {
|
||||
seconds++;
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
timerEl.textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}, 1000);
|
||||
{% endif %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue