feat(proxy): add Traefik version tracking with notifications and dismissible UI warnings

- Add automated Traefik version checking job running weekly on Sundays
- Implement version detection from running containers and comparison with versions.json
- Add notifications across all channels (Email, Discord, Slack, Telegram, Pushover, Webhook) for outdated versions
- Create dismissible callout component with localStorage persistence
- Display cross-branch upgrade warnings (e.g., v3.5 -> v3.6) with changelog links
- Show patch update notifications within same branch
- Add warning icon that appears when callouts are dismissed
- Prevent duplicate notifications during proxy restart by adding restarting parameter
- Fix notification spam with transition-based logic for status changes
- Enable system email settings by default in development mode
- Track last saved/applied proxy settings to detect configuration drift
This commit is contained in:
Andras Bacsai
2025-11-13 13:38:57 +01:00
parent a4d07ab050
commit 0bd4ffb2d7
48 changed files with 1488 additions and 48 deletions
+16 -6
View File
@@ -1,13 +1,13 @@
@props(['type' => 'warning', 'title' => 'Warning', 'class' => ''])
@props(['type' => 'warning', 'title' => 'Warning', 'class' => '', 'dismissible' => false, 'onDismiss' => null])
@php
$icons = [
'warning' => '<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>',
'danger' => '<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
'info' => '<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>',
'success' => '<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
];
@@ -42,12 +42,12 @@
$icon = $icons[$type] ?? $icons['warning'];
@endphp
<div {{ $attributes->merge(['class' => 'p-4 border rounded-lg ' . $colorScheme['bg'] . ' ' . $colorScheme['border'] . ' ' . $class]) }}>
<div {{ $attributes->merge(['class' => 'relative p-4 border rounded-lg ' . $colorScheme['bg'] . ' ' . $colorScheme['border'] . ' ' . $class]) }}>
<div class="flex items-start">
<div class="flex-shrink-0">
{!! $icon !!}
</div>
<div class="ml-3">
<div class="ml-3 {{ $dismissible ? 'pr-8' : '' }}">
<div class="text-base font-bold {{ $colorScheme['title'] }}">
{{ $title }}
</div>
@@ -55,5 +55,15 @@
{{ $slot }}
</div>
</div>
@if($dismissible && $onDismiss)
<button type="button" @click.stop="{{ $onDismiss }}"
class="absolute top-2 right-2 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
aria-label="Dismiss">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-4 h-4 {{ $colorScheme['text'] }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@endif
</div>
</div>
@@ -0,0 +1,43 @@
<x-emails.layout>
{{ $count }} server(s) are running outdated Traefik proxy. Update recommended for security and features.
**Note:** This check is based on the actual running container version, not the configuration file.
## Affected Servers
@foreach ($servers as $server)
@php
$info = $server->outdatedInfo ?? [];
$current = $info['current'] ?? 'unknown';
$latest = $info['latest'] ?? 'unknown';
$type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)';
$hasUpgrades = $hasUpgrades ?? false;
if ($type === 'upgrade') {
$hasUpgrades = true;
}
// Add 'v' prefix for display
$current = str_starts_with($current, 'v') ? $current : "v{$current}";
$latest = str_starts_with($latest, 'v') ? $latest : "v{$latest}";
@endphp
- **{{ $server->name }}**: {{ $current }} {{ $latest }} {{ $type }}
@endforeach
## Recommendation
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration through your [Coolify Dashboard]({{ config('app.url') }}).
@if ($hasUpgrades ?? false)
**Important for major/minor upgrades:** Before upgrading to a new major or minor version, please read the [Traefik changelog](https://github.com/traefik/traefik/releases) to understand breaking changes and new features.
@endif
## Next Steps
1. Review the [Traefik release notes](https://github.com/traefik/traefik/releases) for changes
2. Test the new version in a non-production environment
3. Update your proxy configuration when ready
4. Monitor services after the update
---
You can manage your server proxy settings in your Coolify Dashboard.
</x-emails.layout>
@@ -80,6 +80,8 @@
label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchDiscordNotifications"
label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedDiscordNotifications"
label="Traefik Proxy Outdated" />
</div>
</div>
</div>
@@ -161,6 +161,8 @@
label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchEmailNotifications"
label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedEmailNotifications"
label="Traefik Proxy Outdated" />
</div>
</div>
</div>
@@ -82,6 +82,8 @@
label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchPushoverNotifications"
label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedPushoverNotifications"
label="Traefik Proxy Outdated" />
</div>
</div>
</div>
@@ -74,6 +74,7 @@
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverUnreachableSlackNotifications"
label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchSlackNotifications" label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedSlackNotifications" label="Traefik Proxy Outdated" />
</div>
</div>
</div>
@@ -169,6 +169,15 @@
<x-forms.input canGate="update" :canResource="$settings" type="password" placeholder="Custom Telegram Thread ID"
id="telegramNotificationsServerPatchThreadId" />
</div>
<div class="pl-1 flex gap-2">
<div class="w-96">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedTelegramNotifications"
label="Traefik Proxy Outdated" />
</div>
<x-forms.input canGate="update" :canResource="$settings" type="password" placeholder="Custom Telegram Thread ID"
id="telegramNotificationsTraefikOutdatedThreadId" />
</div>
</div>
</div>
</div>
@@ -83,6 +83,8 @@
id="serverUnreachableWebhookNotifications" label="Server Unreachable" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="serverPatchWebhookNotifications" label="Server Patching" />
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
id="traefikOutdatedWebhookNotifications" label="Traefik Proxy Outdated" />
</div>
</div>
</div>
@@ -1,5 +1,5 @@
<div class="pb-6">
<x-slide-over @startproxy.window="slideOverOpen = true" fullScreen>
<x-slide-over @startproxy.window="slideOverOpen = true" fullScreen closeWithX>
<x-slot:title>Proxy Startup Logs</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" fullHeight />
@@ -97,12 +97,6 @@
<div class="order-first sm:order-last">
<div>
@if ($server->proxySet())
<x-slide-over fullScreen @startproxy.window="slideOverOpen = true">
<x-slot:title>Proxy Status</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" />
</x-slot:content>
</x-slide-over>
@if ($proxyStatus === 'running')
<div class="flex gap-2">
<div class="mt-1" wire:loading wire:target="loadProxyConfiguration">
@@ -181,6 +175,7 @@
});
$wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Initiating proxy restart.');
window.dispatchEvent(new CustomEvent('startproxy'))
$wire.$call('restart');
});
$wire.$on('startProxy', () => {
+85 -22
View File
@@ -21,7 +21,15 @@
@endif
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
</div>
<div class="subtitle">Configure your proxy settings and advanced options.</div>
<div class="pb-4">Configure your proxy settings and advanced options.</div>
@if (
$server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
<x-callout type="warning" title="Configuration Out of Sync" class="my-4">
The saved proxy configuration differs from the currently running configuration. Restart the
proxy to apply your changes.
</x-callout>
@endif
<h3>Advanced</h3>
<div class="pb-6 w-96">
<x-forms.checkbox canGate="update" :canResource="$server"
@@ -43,32 +51,87 @@
: 'Caddy (Coolify Proxy)';
@endphp
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
<div class="flex items-center gap-2">
<h3>{{ $proxyTitle }}</h3>
@if ($proxySettings)
<div @if($server->proxyType() === ProxyTypes::TRAEFIK->value) x-data="{ traefikWarningsDismissed: localStorage.getItem('callout-dismissed-traefik-warnings-{{ $server->id }}') === 'true' }" @endif>
<div class="flex items-center gap-2">
<h3>{{ $proxyTitle }}</h3>
@can('update', $server)
<x-modal-confirmation title="Reset Proxy Configuration?"
buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration"
:actions="[
'Reset proxy configuration to default settings',
'All custom configurations will be lost',
'Custom ports and entrypoints will be removed',
]" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm by entering the server name below"
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
:confirmWithPassword="false" :confirmWithText="true">
</x-modal-confirmation>
<div wire:loading wire:target="loadProxyConfiguration">
<x-forms.button disabled>Reset Configuration</x-forms.button>
</div>
<div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxySettings)
<x-modal-confirmation title="Reset Proxy Configuration?"
buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration"
:actions="[
'Reset proxy configuration to default settings',
'All custom configurations will be lost',
'Custom ports and entrypoints will be removed',
]" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm by entering the server name below"
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
:confirmWithPassword="false" :confirmWithText="true">
</x-modal-confirmation>
@endif
</div>
@endcan
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
<button type="button" x-show="traefikWarningsDismissed"
@click="traefikWarningsDismissed = false; localStorage.removeItem('callout-dismissed-traefik-warnings-{{ $server->id }}')"
class="p-1.5 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors"
title="Show Traefik warnings">
<svg class="w-4 h-4 text-yellow-600 dark:text-yellow-400" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16"></path>
</svg>
</button>
@endif
</div>
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
<div x-show="!traefikWarningsDismissed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2">
@if ($server->detected_traefik_version === 'latest')
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="warning" title="Using 'latest' Traefik Tag" class="my-4">
Your proxy container is running the <span class="font-mono">latest</span> tag. While
this ensures you always have the newest version, it may introduce unexpected breaking
changes.
<br><br>
<strong>Recommendation:</strong> Pin to a specific version (e.g., <span
class="font-mono">traefik:{{ $this->latestTraefikVersion }}</span>) to ensure
stability and predictable updates.
</x-callout>
@elseif($this->isTraefikOutdated)
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="warning" title="Traefik Patch Update Available" class="my-4">
Your Traefik proxy container is running version <span
class="font-mono">v{{ $server->detected_traefik_version }}</span>, but version <span
class="font-mono">{{ $this->latestTraefikVersion }}</span> is available.
<br><br>
<strong>Recommendation:</strong> Update to the latest patch version for security fixes
and
bug fixes. Please test in a non-production environment first.
</x-callout>
@endif
@if ($this->newerTraefikBranchAvailable)
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="info" title="Newer Traefik Version Available" class="my-4">
A newer version of Traefik is available: <span
class="font-mono">{{ $this->newerTraefikBranchAvailable }}</span>
<br><br>
<strong>Important:</strong> Before upgrading to a new major or minor version, please
read
the <a href="https://github.com/traefik/traefik/releases" target="_blank"
class="underline text-white">Traefik changelog</a> to understand breaking changes
and new features.
<br><br>
<strong>Recommendation:</strong> Test the upgrade in a non-production environment first.
</x-callout>
@endif
</div>
@endif
</div>
@endif
@if (
$server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
<div class="text-red-500 ">Configuration out of sync. Restart the proxy to apply the new
configurations.
</div>
@endif
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
<x-loading text="Loading proxy configuration..." />
</div>