Only use the simple maintenance page, and made this a bit more generic

This commit is contained in:
Jan Böhmer 2026-02-02 22:10:52 +01:00
parent 1adfec16e2
commit 58d574a33a
3 changed files with 10 additions and 346 deletions

View file

@ -37,8 +37,7 @@ use Twig\Environment;
*/
readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
{
public function __construct(private UpdateExecutor $updateExecutor,
private Environment $twig)
public function __construct(private UpdateExecutor $updateExecutor)
{
}
@ -48,7 +47,6 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
return [
// High priority to run before other listeners
KernelEvents::REQUEST => ['onKernelRequest', 512], //High priority to run before other listeners
KernelEvents::RESPONSE => ['onKernelResponse', -512] // Low priority to run after other listeners
];
}
@ -71,13 +69,12 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
// Get maintenance info
$maintenanceInfo = $this->updateExecutor->getMaintenanceInfo();
$lockInfo = $this->updateExecutor->getLockInfo();
// Calculate how long the update has been running
$duration = null;
if ($lockInfo && isset($lockInfo['started_at'])) {
if ($maintenanceInfo && isset($maintenanceInfo['enabled_at'])) {
try {
$startedAt = new \DateTime($lockInfo['started_at']);
$startedAt = new \DateTime($maintenanceInfo['enabled_at']);
$now = new \DateTime();
$duration = $now->getTimestamp() - $startedAt->getTimestamp();
} catch (\Exception) {
@ -85,17 +82,7 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
}
}
// Try to render the Twig template, fall back to simple HTML
try {
$content = $this->twig->render('maintenance/maintenance.html.twig', [
'reason' => $maintenanceInfo['reason'] ?? 'Maintenance in progress',
'started_at' => $maintenanceInfo['enabled_at'] ?? null,
'duration' => $duration,
]);
} catch (\Exception) {
// Fallback to simple HTML if Twig fails
$content = $this->getSimpleMaintenanceHtml($maintenanceInfo, $duration);
}
$content = $this->getSimpleMaintenanceHtml($maintenanceInfo, $duration);
$response = new Response($content, Response::HTTP_SERVICE_UNAVAILABLE);
$response->headers->set('Retry-After', '30');
@ -104,28 +91,6 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
$event->setResponse($response);
}
public function onKernelResponse(ResponseEvent $event)
{
// Only handle main requests
if (!$event->isMainRequest()) {
return;
}
// Skip if not in maintenance mode
if (!$this->updateExecutor->isMaintenanceMode()) {
return;
}
// Allow CLI requests
if (PHP_SAPI === 'cli') {
return;
}
//Remove all Content-Security-Policy headers to allow loading resources during maintenance
$response = $event->getResponse();
$response->headers->remove('Content-Security-Policy');
}
/**
* Generate a simple maintenance page HTML without Twig.
*/
@ -134,6 +99,8 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
$reason = htmlspecialchars($maintenanceInfo['reason'] ?? 'Update in progress');
$durationText = $duration !== null ? sprintf('%d seconds', $duration) : 'a moment';
$startDateStr = $maintenanceInfo['enabled_at'] ?? 'unknown time';
return <<<HTML
<!DOCTYPE html>
<html lang="en">
@ -233,7 +200,7 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
<div class="icon">
<span class="spinner">⚙️</span>
</div>
<h1>Part-DB is Updating</h1>
<h1>Part-DB is under maintenance</h1>
<p>We're making things better. This should only take a moment.</p>
<div class="reason">
@ -245,7 +212,9 @@ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface
</div>
<p class="info">
Update running for <span class="duration">{$durationText}</span><br>
Maintenance mode active since <span class="duration">{$startDateStr}</span><br>
<br>
Started <span class="duration">{$durationText}</span> ago<br>
<small>This page will automatically refresh every 15 seconds.</small>
</p>
</div>

View file

@ -1,251 +0,0 @@
<!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>

View file

@ -14718,60 +14718,6 @@ Buerklin-API Authentication server:
<target>bytes</target>
</segment>
</unit>
<unit id="kVfrQQ_" name="update_manager.maintenance.title">
<segment state="translated">
<source>update_manager.maintenance.title</source>
<target>Maintenance</target>
</segment>
</unit>
<unit id="DJPbvVc" name="update_manager.maintenance.heading">
<segment state="translated">
<source>update_manager.maintenance.heading</source>
<target>Part-DB is Updating</target>
</segment>
</unit>
<unit id="x2kQd7v" name="update_manager.maintenance.description">
<segment state="translated">
<source>update_manager.maintenance.description</source>
<target>We're installing updates to make Part-DB even better. This should only take a moment.</target>
</segment>
</unit>
<unit id="LF4Ebik" name="update_manager.maintenance.step_backup">
<segment state="translated">
<source>update_manager.maintenance.step_backup</source>
<target>Creating backup</target>
</segment>
</unit>
<unit id="uioqI08" name="update_manager.maintenance.step_download">
<segment state="translated">
<source>update_manager.maintenance.step_download</source>
<target>Downloading updates</target>
</segment>
</unit>
<unit id="lNjhpKA" name="update_manager.maintenance.step_install">
<segment state="translated">
<source>update_manager.maintenance.step_install</source>
<target>Installing files</target>
</segment>
</unit>
<unit id="SfP.cFa" name="update_manager.maintenance.step_migrate">
<segment state="translated">
<source>update_manager.maintenance.step_migrate</source>
<target>Running migrations</target>
</segment>
</unit>
<unit id="u_8lAEw" name="update_manager.maintenance.step_cache">
<segment state="translated">
<source>update_manager.maintenance.step_cache</source>
<target>Clearing cache</target>
</segment>
</unit>
<unit id="ys_sMwM" name="update_manager.maintenance.auto_refresh">
<segment state="translated">
<source>update_manager.maintenance.auto_refresh</source>
<target>This page will refresh automatically when the update is complete.</target>
</segment>
</unit>
<unit id="Gt.91s_" name="perm.system.manage_updates">
<segment state="translated">
<source>perm.system.manage_updates</source>