feat(changelog): implement automated changelog fetching from GitHub and enhance changelog read tracking
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
@@ -64,6 +65,7 @@ class Init extends Command
|
||||
try {
|
||||
$this->cleanupUnnecessaryDynamicProxyConfiguration();
|
||||
$this->pullTemplatesFromCDN();
|
||||
$this->pullChangelogFromGitHub();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
@@ -74,6 +76,7 @@ class Init extends Command
|
||||
try {
|
||||
$this->cleanupInProgressApplicationDeployments();
|
||||
$this->pullTemplatesFromCDN();
|
||||
$this->pullChangelogFromGitHub();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
@@ -109,6 +112,16 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private function pullChangelogFromGitHub()
|
||||
{
|
||||
try {
|
||||
PullChangelogFromGitHub::dispatch();
|
||||
echo "Changelog fetch initiated\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not fetch changelog from GitHub: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function optimize()
|
||||
{
|
||||
Artisan::call('optimize:clear');
|
||||
|
@@ -6,6 +6,7 @@ use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
@@ -67,6 +68,7 @@ class Kernel extends ConsoleKernel
|
||||
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
$this->scheduleInstance->job(new PullChangelogFromGitHub)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
$this->scheduleUpdates();
|
||||
|
110
app/Jobs/PullChangelogFromGitHub.php
Normal file
110
app/Jobs/PullChangelogFromGitHub.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullChangelogFromGitHub implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 30;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)
|
||||
->timeout(30)
|
||||
->get('https://api.github.com/repos/coollabsio/coolify/releases?per_page=10');
|
||||
|
||||
if ($response->successful()) {
|
||||
$releases = $response->json();
|
||||
$changelog = $this->transformReleasesToChangelog($releases);
|
||||
|
||||
// Group entries by month and save them
|
||||
$this->saveChangelogEntries($changelog);
|
||||
} else {
|
||||
send_internal_notification('PullChangelogFromGitHub failed with: '.$response->status().' '.$response->body());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('PullChangelogFromGitHub failed with: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function transformReleasesToChangelog(array $releases): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
foreach ($releases as $release) {
|
||||
// Skip drafts and pre-releases if desired
|
||||
if ($release['draft']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$publishedAt = Carbon::parse($release['published_at']);
|
||||
|
||||
$entry = [
|
||||
'tag_name' => $release['tag_name'],
|
||||
'title' => $release['name'] ?: $release['tag_name'],
|
||||
'content' => $release['body'] ?: 'No release notes available.',
|
||||
'published_at' => $publishedAt->toISOString(),
|
||||
];
|
||||
|
||||
$entries[] = $entry;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function saveChangelogEntries(array $entries): void
|
||||
{
|
||||
// Create changelogs directory if it doesn't exist
|
||||
$changelogsDir = base_path('changelogs');
|
||||
if (! File::exists($changelogsDir)) {
|
||||
File::makeDirectory($changelogsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Group entries by year-month
|
||||
$groupedEntries = [];
|
||||
foreach ($entries as $entry) {
|
||||
$date = Carbon::parse($entry['published_at']);
|
||||
$monthKey = $date->format('Y-m');
|
||||
|
||||
if (! isset($groupedEntries[$monthKey])) {
|
||||
$groupedEntries[$monthKey] = [];
|
||||
}
|
||||
|
||||
$groupedEntries[$monthKey][] = $entry;
|
||||
}
|
||||
|
||||
// Save each month's entries to separate files
|
||||
foreach ($groupedEntries as $month => $monthEntries) {
|
||||
// Sort entries by published date (newest first)
|
||||
usort($monthEntries, function ($a, $b) {
|
||||
return Carbon::parse($b['published_at'])->timestamp - Carbon::parse($a['published_at'])->timestamp;
|
||||
});
|
||||
|
||||
$monthData = [
|
||||
'entries' => $monthEntries,
|
||||
'last_updated' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$filePath = base_path("changelogs/{$month}.json");
|
||||
File::put($filePath, json_encode($monthData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\PullChangelogFromGitHub;
|
||||
use App\Services\ChangelogService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
@@ -42,6 +43,20 @@ class SettingsDropdown extends Component
|
||||
app(ChangelogService::class)->markAllAsReadForUser(Auth::user());
|
||||
}
|
||||
|
||||
public function manualFetchChangelog()
|
||||
{
|
||||
if (! isDev()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PullChangelogFromGitHub::dispatch();
|
||||
$this->dispatch('success', 'Changelog fetch initiated! Check back in a few moments.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('error', 'Failed to fetch changelog: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings-dropdown', [
|
||||
|
@@ -75,7 +75,7 @@ class Index extends Component
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log the error
|
||||
logger()->error('Stripe API error: ' . $e->getMessage());
|
||||
logger()->error('Stripe API error: '.$e->getMessage());
|
||||
// Set a flag to show an error message to the user
|
||||
$this->addError('stripe', 'Could not retrieve subscription information. Please try again later.');
|
||||
} finally {
|
||||
|
@@ -9,7 +9,7 @@ class UserChangelogRead extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'changelog_identifier',
|
||||
'release_tag',
|
||||
'read_at',
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@ class UserChangelogRead extends Model
|
||||
{
|
||||
self::firstOrCreate([
|
||||
'user_id' => $userId,
|
||||
'changelog_identifier' => $identifier,
|
||||
'release_tag' => $identifier,
|
||||
], [
|
||||
'read_at' => now(),
|
||||
]);
|
||||
@@ -35,14 +35,14 @@ class UserChangelogRead extends Model
|
||||
public static function isReadByUser(int $userId, string $identifier): bool
|
||||
{
|
||||
return self::where('user_id', $userId)
|
||||
->where('changelog_identifier', $identifier)
|
||||
->where('release_tag', $identifier)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function getReadIdentifiersForUser(int $userId): array
|
||||
{
|
||||
return self::where('user_id', $userId)
|
||||
->pluck('changelog_identifier')
|
||||
->pluck('release_tag')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Spatie\LaravelMarkdown\MarkdownRenderer;
|
||||
|
||||
class ChangelogService
|
||||
{
|
||||
@@ -63,7 +64,7 @@ class ChangelogService
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->map(function ($entry) use ($readIdentifiers) {
|
||||
$entry->is_read = in_array($entry->version, $readIdentifiers);
|
||||
$entry->is_read = in_array($entry->tag_name, $readIdentifiers);
|
||||
|
||||
return $entry;
|
||||
})->sortBy([
|
||||
@@ -78,7 +79,7 @@ class ChangelogService
|
||||
$entries = $this->getEntries();
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->reject(fn ($entry) => in_array($entry->version, $readIdentifiers))->count();
|
||||
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
|
||||
} else {
|
||||
return Cache::remember(
|
||||
'user_unread_changelog_count_'.$user->id,
|
||||
@@ -87,7 +88,7 @@ class ChangelogService
|
||||
$entries = $this->getEntries();
|
||||
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
|
||||
|
||||
return $entries->reject(fn ($entry) => in_array($entry->version, $readIdentifiers))->count();
|
||||
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -200,7 +201,7 @@ class ChangelogService
|
||||
$entries = $this->getEntries();
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
UserChangelogRead::markAsRead($user->id, $entry->version);
|
||||
UserChangelogRead::markAsRead($user->id, $entry->tag_name);
|
||||
}
|
||||
|
||||
Cache::forget('user_unread_changelog_count_'.$user->id);
|
||||
@@ -208,7 +209,7 @@ class ChangelogService
|
||||
|
||||
private function validateEntryData(array $data): bool
|
||||
{
|
||||
$required = ['version', 'title', 'content', 'published_at'];
|
||||
$required = ['tag_name', 'title', 'content', 'published_at'];
|
||||
|
||||
foreach ($required as $field) {
|
||||
if (! isset($data[$field]) || empty($data[$field])) {
|
||||
@@ -253,41 +254,46 @@ class ChangelogService
|
||||
|
||||
private function parseMarkdown(string $content): string
|
||||
{
|
||||
// Convert markdown to HTML using simple regex patterns
|
||||
$html = $content;
|
||||
$renderer = app(MarkdownRenderer::class);
|
||||
|
||||
$html = $renderer->toHtml($content);
|
||||
|
||||
// Apply custom Tailwind CSS classes for dark mode compatibility
|
||||
$html = $this->applyCustomStyling($html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function applyCustomStyling(string $html): string
|
||||
{
|
||||
// Headers
|
||||
$html = preg_replace('/^### (.*?)$/m', '<h3 class="text-md font-semibold dark:text-white mb-1">$1</h3>', $html);
|
||||
$html = preg_replace('/^## (.*?)$/m', '<h2 class="text-lg font-semibold dark:text-white mb-2">$1</h2>', $html);
|
||||
$html = preg_replace('/^# (.*?)$/m', '<h1 class="text-xl font-bold dark:text-white mb-2">$1</h1>', $html);
|
||||
$html = preg_replace('/<h1[^>]*>/', '<h1 class="text-xl font-bold dark:text-white mb-2">', $html);
|
||||
$html = preg_replace('/<h2[^>]*>/', '<h2 class="text-lg font-semibold dark:text-white mb-2">', $html);
|
||||
$html = preg_replace('/<h3[^>]*>/', '<h3 class="text-md font-semibold dark:text-white mb-1">', $html);
|
||||
|
||||
// Bold text
|
||||
$html = preg_replace('/\*\*(.*?)\*\*/', '<strong class="font-semibold">$1</strong>', $html);
|
||||
$html = preg_replace('/__(.*?)__/', '<strong class="font-semibold">$1</strong>', $html);
|
||||
// Paragraphs
|
||||
$html = preg_replace('/<p[^>]*>/', '<p class="mb-2 dark:text-neutral-300">', $html);
|
||||
|
||||
// Italic text
|
||||
$html = preg_replace('/\*(.*?)\*/', '<em class="italic">$1</em>', $html);
|
||||
$html = preg_replace('/_(.*?)_/', '<em class="italic">$1</em>', $html);
|
||||
// Lists
|
||||
$html = preg_replace('/<ul[^>]*>/', '<ul class="mb-2 ml-4 list-disc">', $html);
|
||||
$html = preg_replace('/<ol[^>]*>/', '<ol class="mb-2 ml-4 list-decimal">', $html);
|
||||
$html = preg_replace('/<li[^>]*>/', '<li class="dark:text-neutral-300">', $html);
|
||||
|
||||
// Code blocks
|
||||
$html = preg_replace('/```(.*?)```/s', '<pre class="bg-gray-100 dark:bg-coolgray-300 p-2 rounded text-sm overflow-x-auto my-2"><code>$1</code></pre>', $html);
|
||||
// Code blocks and inline code
|
||||
$html = preg_replace('/<pre[^>]*>/', '<pre class="bg-gray-100 dark:bg-coolgray-300 p-2 rounded text-sm overflow-x-auto my-2">', $html);
|
||||
$html = preg_replace('/<code[^>]*>/', '<code class="bg-gray-100 dark:bg-coolgray-300 px-1 py-0.5 rounded text-sm">', $html);
|
||||
|
||||
// Inline code
|
||||
$html = preg_replace('/`([^`]+)`/', '<code class="bg-gray-100 dark:bg-coolgray-300 px-1 py-0.5 rounded text-sm">$1</code>', $html);
|
||||
// Links - Apply styling to existing markdown links
|
||||
$html = preg_replace('/<a([^>]*)>/', '<a$1 class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">', $html);
|
||||
|
||||
// Links
|
||||
$html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2" class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">$1</a>', $html);
|
||||
// Convert plain URLs to clickable links (that aren't already in <a> tags)
|
||||
$html = preg_replace('/(?<!href="|href=\')(?<!>)(?<!\/)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/', '<a href="$1" class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">$1</a>', $html);
|
||||
|
||||
// Line breaks (convert double newlines to paragraphs)
|
||||
$paragraphs = preg_split('/\n\s*\n/', trim($html));
|
||||
$html = '<p class="mb-2">'.implode('</p><p class="mb-2">', $paragraphs).'</p>';
|
||||
// Strong/bold text
|
||||
$html = preg_replace('/<strong[^>]*>/', '<strong class="font-semibold dark:text-white">', $html);
|
||||
|
||||
// Single line breaks
|
||||
$html = preg_replace('/\n/', '<br>', $html);
|
||||
|
||||
// Unordered lists
|
||||
$html = preg_replace('/^\- (.*)$/m', '<li class="ml-4">• $1</li>', $html);
|
||||
$html = preg_replace('/(<li class="ml-4">.*<\/li>)/s', '<ul class="mb-2">$1</ul>', $html);
|
||||
// Emphasis/italic text
|
||||
$html = preg_replace('/<em[^>]*>/', '<em class="italic dark:text-neutral-300">', $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
17
changelogs/2025-05.json
Normal file
17
changelogs/2025-05.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.418",
|
||||
"title": "v4.0.0-beta.418",
|
||||
"content": "- fix(core): Clean up Horizon Redis data every hour.\r\n- fix(ui): Infinite loop on requesting a png file on new resource page.\r\n\r\n## What's Changed\r\n* v4.0.0-beta.418 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/5793\r\n\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.417...v4.0.0-beta.418",
|
||||
"published_at": "2025-05-09T06:30:49.000000Z"
|
||||
},
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.417",
|
||||
"title": "v4.0.0-beta.417",
|
||||
"content": "- fix(core): Cleanup redis data every 10 minutes.\r\n- fix(core): Revert jobs to be `dontRelease` as they were before the redis/job problems started.\r\n\r\n## What's Changed\r\n* v4.0.0-beta.417 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/5784\r\n\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.416...v4.0.0-beta.417",
|
||||
"published_at": "2025-05-07T21:14:23.000000Z"
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-10T10:20:51.376736Z"
|
||||
}
|
23
changelogs/2025-06.json
Normal file
23
changelogs/2025-06.json
Normal file
File diff suppressed because one or more lines are too long
35
changelogs/2025-07.json
Normal file
35
changelogs/2025-07.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.420.6",
|
||||
"title": "v4.0.0-beta.420.6",
|
||||
"content": "## Changes\r\n- chore(deps): update composer dependencies to fix https://github.com/advisories/GHSA-29cq-5w36-x7w3\r\n\r\n## What's Changed\r\n* 4.0.0-beta.420.6 by @peaklabs-dev in https://github.com/coollabsio/coolify/pull/6221\r\n\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.420.5...v4.0.0-beta.420.6",
|
||||
"published_at": "2025-07-18T18:45:44.000000Z"
|
||||
},
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.420.5",
|
||||
"title": "v4.0.0-beta.420.5",
|
||||
"content": "- refactor(postgresql): improve layout and spacing in SSL and Proxy configuration sections for better UI consistency\r\n- fix(database): ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy\r\n\r\n## What's Changed\r\n* v4.0.0-beta.420.5 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/6156\r\n\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.420.4...v4.0.0-beta.420.5",
|
||||
"published_at": "2025-07-08T19:14:04.000000Z"
|
||||
},
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.420.4",
|
||||
"title": "v4.0.0-beta.420.4",
|
||||
"content": "# POSSIBLE BREAKING CHANGE\r\n- fix(envs): enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility for applications and services.\r\n - **Switched URL with FQDN in case of applications, so they are correct now.**\r\n\r\n--- \r\n- feat(envs): New environment variables for `docker compose` based application. You can use `SERVICE_FQDN_<serviceName>` and `SERVICE_URL_<serviceName>` the same way as in services. For details check this: https://github.com/coollabsio/coolify/issues/6124#issuecomment-3045186382\r\n- fix(redis): Cleanup old jobs on Coolify start.\r\n- fix(database): Unsupported database with SSL.\r\n- fix(jobs): Sentinel update job fails if compose is invalid for a service/app.\r\n- fix(installscript): Add Cachyos support + reuse env variable for public ip.\r\n- fix(envs): Generate literal env variables better.\r\n- fix(scheduling): change redis cleanup command frequency from hourly to weekly for better resource management\r\n\r\n# Issues\r\n- fix https://github.com/coollabsio/coolify/issues/6142\r\n- fix https://github.com/coollabsio/coolify/issues/6134\r\n- fix https://github.com/coollabsio/coolify/issues/6141\r\n- fix https://github.com/coollabsio/coolify/issues/2470\r\n\r\n## What's Changed\r\n* fix(service): Postiz shows no available server on latest version by @ShadowArcanist in https://github.com/coollabsio/coolify/pull/6144\r\n* Typo Correction on modal by @Nathanjms in https://github.com/coollabsio/coolify/pull/6130\r\n* fix(install.sh): use IPV4_PUBLIC_IP variable in output instead of repeated curl by @dewstouh in https://github.com/coollabsio/coolify/pull/6129\r\n* interpret cachyos as arch in the install script by @flickowoa in https://github.com/coollabsio/coolify/pull/6127\r\n* v4.0.0-beta.420.4 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/6146\r\n\r\n## New Contributors\r\n* @dewstouh made their first contribution in https://github.com/coollabsio/coolify/pull/6129\r\n* @flickowoa made their first contribution in https://github.com/coollabsio/coolify/pull/6127\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.420.3...v4.0.0-beta.420.4",
|
||||
"published_at": "2025-07-08T08:58:14.000000Z"
|
||||
},
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.420.3",
|
||||
"title": "v4.0.0-beta.420.3",
|
||||
"content": "- fix(shared): enhance FQDN generation logic for services in newParser function\r\n- fix(ui): light mode, configuration changed popup fixed\r\n\r\n## What's Changed\r\n* v4.0.0-beta.420.3 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/6120\r\n\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.420.2...v4.0.0-beta.420.3",
|
||||
"published_at": "2025-07-03T19:31:13.000000Z"
|
||||
},
|
||||
{
|
||||
"tag_name": "v4.0.0-beta.420.2",
|
||||
"title": "v4.0.0-beta.420.2",
|
||||
"content": "- fix(supabase): Fix supabase template\r\n- fix(database): proxy ssl port if ssl is enabled\r\n- fix(ui): enhance terminal access messaging to clarify server functionality and terminal status\r\n- fix(server): prepend 'mux_' to UUID in muxFilename method for consistent naming\r\n- fix(jobs): update middleware to use expireAfter for WithoutOverlapping in multiple job classes\r\n- fix(ui): improve destination selection description for clarity in resource segregation\r\n- fix(terminal): ensure shell execution only uses valid shell if available in terminal command\r\n- refactor(ui): remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code\r\n- refactor(ui): separate views for instance settings to separate paths to make it cleaner\r\n- refactor(ui): enhance project cloning interface with improved table layout for server and resource selection\r\n\r\n# Issues\r\n- fix #6074\r\n- fix #6022\r\n\r\n## What's Changed\r\n* fix: 500 status code when trying to clone an environment by @HicaroD in https://github.com/coollabsio/coolify/pull/6071\r\n* Update Authentik to version 2025.6.3 by @Datenschmutz in https://github.com/coollabsio/coolify/pull/6100\r\n* Update Appwrite services and image version (1.7.4) by @pujan-modha in https://github.com/coollabsio/coolify/pull/6099\r\n* feat(template): added excalidraw by @nktnet1 in https://github.com/coollabsio/coolify/pull/6095\r\n* v4.0.0-beta.420.2 by @andrasbacsai in https://github.com/coollabsio/coolify/pull/6096\r\n\r\n## New Contributors\r\n* @smad-bro made their first contribution in https://github.com/coollabsio/coolify/pull/6031\r\n* @HicaroD made their first contribution in https://github.com/coollabsio/coolify/pull/6071\r\n\r\n**Full Changelog**: https://github.com/coollabsio/coolify/compare/v4.0.0-beta.420.1...v4.0.0-beta.420.2",
|
||||
"published_at": "2025-07-03T13:55:03.000000Z"
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-10T10:20:51.373867Z"
|
||||
}
|
@@ -47,6 +47,7 @@
|
||||
"socialiteproviders/zitadel": "^4.2",
|
||||
"spatie/laravel-activitylog": "^4.10.2",
|
||||
"spatie/laravel-data": "^4.17.0",
|
||||
"spatie/laravel-markdown": "^2.7",
|
||||
"spatie/laravel-ray": "^1.40.2",
|
||||
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
||||
"spatie/url": "^2.4",
|
||||
@@ -127,4 +128,4 @@
|
||||
"@php artisan key:generate --ansi"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
203
composer.lock
generated
203
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "52a680a0eb446dcaa74bc35e158aca57",
|
||||
"content-hash": "a78cf8fdfec25eac43de77c05640dc91",
|
||||
"packages": [
|
||||
{
|
||||
"name": "amphp/amp",
|
||||
@@ -7902,6 +7902,66 @@
|
||||
],
|
||||
"time": "2025-05-08T15:41:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/commonmark-shiki-highlighter",
|
||||
"version": "2.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/commonmark-shiki-highlighter.git",
|
||||
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/commonmark-shiki-highlighter/zipball/595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
|
||||
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/commonmark": "^2.4.2",
|
||||
"php": "^8.0",
|
||||
"spatie/shiki-php": "^2.2.2",
|
||||
"symfony/process": "^5.4|^6.4|^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.19|^v3.49.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.7",
|
||||
"spatie/ray": "^1.28"
|
||||
},
|
||||
"type": "commonmark-extension",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\CommonMarkShikiHighlighter\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Highlight code blocks with league/commonmark and Shiki",
|
||||
"homepage": "https://github.com/spatie/commonmark-shiki-highlighter",
|
||||
"keywords": [
|
||||
"commonmark-shiki-highlighter",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/commonmark-shiki-highlighter/tree/2.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-13T11:25:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-activitylog",
|
||||
"version": "4.10.2",
|
||||
@@ -8076,6 +8136,82 @@
|
||||
],
|
||||
"time": "2025-06-25T11:36:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-markdown",
|
||||
"version": "2.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-markdown.git",
|
||||
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/353e7f9fae62826e26cbadef58a12ecf39685280",
|
||||
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^9.0|^10.0|^11.0|^12.0",
|
||||
"league/commonmark": "^2.6.0",
|
||||
"php": "^8.1",
|
||||
"spatie/commonmark-shiki-highlighter": "^2.5",
|
||||
"spatie/laravel-package-tools": "^1.4.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^6.2|^7.8",
|
||||
"nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
|
||||
"orchestra/testbench": "^6.15|^7.0|^8.0|^10.0",
|
||||
"pestphp/pest": "^1.22|^2.0|^3.7",
|
||||
"phpunit/phpunit": "^9.3|^11.5.3",
|
||||
"spatie/laravel-ray": "^1.23",
|
||||
"spatie/pest-plugin-snapshots": "^1.1|^2.2|^3.0",
|
||||
"vimeo/psalm": "^4.8|^6.7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\LaravelMarkdown\\MarkdownServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\LaravelMarkdown\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A highly configurable markdown renderer and Blade component for Laravel",
|
||||
"homepage": "https://github.com/spatie/laravel-markdown",
|
||||
"keywords": [
|
||||
"Laravel-Markdown",
|
||||
"laravel",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/laravel-markdown/tree/2.7.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-21T13:43:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.92.7",
|
||||
@@ -8515,6 +8651,71 @@
|
||||
],
|
||||
"time": "2025-04-18T08:17:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/shiki-php",
|
||||
"version": "2.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/shiki-php.git",
|
||||
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/shiki-php/zipball/a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
|
||||
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^8.0",
|
||||
"symfony/process": "^5.4|^6.4|^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^v3.0",
|
||||
"pestphp/pest": "^1.8",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/pest-plugin-snapshots": "^1.1",
|
||||
"spatie/ray": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\ShikiPhp\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rias Van der Veken",
|
||||
"email": "rias@spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Highlight code using Shiki in PHP",
|
||||
"homepage": "https://github.com/spatie/shiki-php",
|
||||
"keywords": [
|
||||
"shiki",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/shiki-php/tree/2.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-21T14:16:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/url",
|
||||
"version": "2.4.0",
|
||||
|
@@ -14,13 +14,13 @@ return new class extends Migration
|
||||
Schema::create('user_changelog_reads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('changelog_identifier');
|
||||
$table->string('release_tag'); // GitHub tag_name (e.g., "v4.0.0-beta.420.6")
|
||||
$table->timestamp('read_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'changelog_identifier']);
|
||||
$table->unique(['user_id', 'release_tag']);
|
||||
$table->index('user_id');
|
||||
$table->index('changelog_identifier');
|
||||
$table->index('release_tag');
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -7,14 +7,14 @@
|
||||
// Load all entries when component initializes
|
||||
this.allEntries = @js($entries->toArray());
|
||||
},
|
||||
markEntryAsRead(version) {
|
||||
markEntryAsRead(tagName) {
|
||||
// Update the entry in our local Alpine data
|
||||
const entry = this.allEntries.find(e => e.version === version);
|
||||
const entry = this.allEntries.find(e => e.tag_name === tagName);
|
||||
if (entry) {
|
||||
entry.is_read = true;
|
||||
}
|
||||
// Call Livewire to update server-side
|
||||
$wire.markAsRead(version);
|
||||
$wire.markAsRead(tagName);
|
||||
},
|
||||
markAllEntriesAsRead() {
|
||||
// Update all entries in our local Alpine data
|
||||
@@ -73,7 +73,7 @@
|
||||
entries = entries.filter(entry => {
|
||||
return (entry.title?.toLowerCase().includes(searchLower) ||
|
||||
entry.content?.toLowerCase().includes(searchLower) ||
|
||||
entry.version?.toLowerCase().includes(searchLower));
|
||||
entry.tag_name?.toLowerCase().includes(searchLower));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
class="px-1 dropdown-item-no-padding flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>What's New</span>
|
||||
</div>
|
||||
@@ -137,7 +138,8 @@
|
||||
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span>Changelog</span>
|
||||
</button>
|
||||
@@ -152,21 +154,24 @@
|
||||
<button @click="setTheme('dark'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
<button @click="setTheme('light'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
<button @click="setTheme('system'); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>System</span>
|
||||
</button>
|
||||
@@ -175,17 +180,19 @@
|
||||
<div
|
||||
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
|
||||
Width</div>
|
||||
<button @click="switchWidth(); dropdownOpen = false" class="px-1 dropdown-item-no-padding flex items-center gap-2"
|
||||
x-show="full === 'full'">
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'full'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
<span>Center</span>
|
||||
</button>
|
||||
<button @click="switchWidth(); dropdownOpen = false" class="px-1 dropdown-item-no-padding flex items-center gap-2"
|
||||
x-show="full === 'center'">
|
||||
<button @click="switchWidth(); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'center'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span>Full</span>
|
||||
</button>
|
||||
@@ -197,14 +204,16 @@
|
||||
<button @click="setZoom(100); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<span>100%</span>
|
||||
</button>
|
||||
<button @click="setZoom(90); dropdownOpen = false"
|
||||
class="px-1 dropdown-item-no-padding flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
<span>90%</span>
|
||||
</button>
|
||||
@@ -215,14 +224,14 @@
|
||||
|
||||
<!-- What's New Modal -->
|
||||
@if ($showWhatsNewModal)
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-6 sm:p-8">
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center py-6 px-4" @keydown.escape.window="$wire.closeWhatsNewModal()">
|
||||
<!-- Background overlay -->
|
||||
<div class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs" wire:click="closeWhatsNewModal">
|
||||
</div>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div
|
||||
class="relative w-full max-w-4xl py-6 border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
|
||||
class="relative w-full h-full max-w-7xl py-6 border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300 flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between pb-3">
|
||||
<div>
|
||||
@@ -234,6 +243,16 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isDev())
|
||||
<x-forms.button wire:click="manualFetchChangelog"
|
||||
class="bg-coolgray-200 hover:bg-coolgray-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Fetch Latest
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($unreadCount > 0)
|
||||
<x-forms.button @click="markAllEntriesAsRead">
|
||||
Mark all as read
|
||||
@@ -250,7 +269,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="pb-4 border-b dark:border-coolgray-200">
|
||||
<div class="pb-4 border-b dark:border-coolgray-200 flex-shrink-0">
|
||||
<div class="relative">
|
||||
<input x-model="search" placeholder="Search updates..." class="input pl-10" />
|
||||
<svg class="absolute left-3 top-2 w-4 h-4 dark:text-neutral-400" fill="none"
|
||||
@@ -262,20 +281,28 @@
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="py-4 max-h-96 overflow-y-auto scrollbar">
|
||||
<div class="py-4 flex-1 overflow-y-auto scrollbar">
|
||||
<div x-show="filteredEntries.length > 0">
|
||||
<div class="space-y-4">
|
||||
<template x-for="entry in filteredEntries" :key="entry.version">
|
||||
<template x-for="entry in filteredEntries" :key="entry.tag_name">
|
||||
<div class="relative p-4 border dark:border-coolgray-300 rounded-sm"
|
||||
:class="!entry.is_read ? 'dark:bg-coolgray-200 border-warning' : 'dark:bg-coolgray-100'">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span x-show="entry.version"
|
||||
class="px-2 py-1 text-xs font-semibold dark:bg-coolgray-300 dark:text-neutral-200 rounded-sm"
|
||||
x-text="entry.version"></span>
|
||||
<span class="text-xs dark:text-neutral-400 font-bold"
|
||||
x-text="entry.title"></span>
|
||||
<span x-show="entry.title"
|
||||
class="px-2 py-1 text-xs font-semibold dark:bg-coolgray-300 dark:text-neutral-200 rounded-sm"><a
|
||||
:href="`https://github.com/coollabsio/coolify/releases/tag/${entry.tag_name}`"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 hover:text-coolgray-500">
|
||||
<span x-text="entry.title"></span>
|
||||
<svg class="w-3 h-3 text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a></span>
|
||||
<span class="text-xs dark:text-neutral-400"
|
||||
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
|
||||
</div>
|
||||
@@ -284,7 +311,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button x-show="!entry.is_read" @click="markEntryAsRead(entry.version)"
|
||||
<button x-show="!entry.is_read" @click="markEntryAsRead(entry.tag_name)"
|
||||
class="ml-4 px-3 py-1 text-xs dark:text-neutral-400 hover:dark:text-white border dark:border-neutral-600 rounded hover:dark:bg-neutral-700 transition-colors cursor-pointer"
|
||||
title="Mark as read">
|
||||
mark as read
|
||||
|
Reference in New Issue
Block a user