Skip to content

[Improvement] Email updates + more notifications#1168

Merged
saeedvaziry merged 6 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/email-improvements
Jun 18, 2026
Merged

[Improvement] Email updates + more notifications#1168
saeedvaziry merged 6 commits into
vitodeploy:4.xfrom
RichardAnderson:feat/email-improvements

Conversation

@RichardAnderson

@RichardAnderson RichardAnderson commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • New Features

    • Notifications now support server auto-update completion details (upgraded package counts and reboot requirement).
    • Added SSL notifications for expiring certificates (including renewal state handling) and wildcard renewal failures.
    • Added failure notifications for backup runs and webhook-triggered deployments.
  • Improvements

    • Enhanced email content with localised text, clearer action links, and consistent success/error styling.
    • Updated SSL expiry tracking to avoid duplicate reminders and re-arm after renewal.
  • Tests

    • Expanded coverage for SSL expiry reminders, auto-update notification behaviour, and rendered email output.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 99b20444-228b-4763-b5b8-64c0448ba399

📥 Commits

Reviewing files that changed from the base of the PR and between 140d351 and c5212ad.

📒 Files selected for processing (1)
  • resources/views/ssh/os/upgrade.blade.php

📝 Walkthrough

Walkthrough

Adds five new notification classes (ServerAutoUpdateCompleted, SslCertificateExpiring, SslRenewalFailed, BackupFailed, WebhookDeploymentFailed) triggered from jobs and commands, refactors the OS upgrade SSH script to emit structured parsed output, introduces SSL expiry state tracking via a new expiry_notified_at database column, publishes a custom vendor mail HTML/CSS template system, and migrates all existing notification emails to translated strings with consistent error/success styling and action buttons.

Changes

Notification System and Email Template Overhaul

Layer / File(s) Summary
OS upgrade structured output and ServerAutoUpdateCompleted notification
resources/views/ssh/os/upgrade.blade.php, app/SSH/OS/OS.php, app/Jobs/Server/UpdateJob.php, app/Actions/Server/Update.php, app/Console/Commands/RunServerAutoUpdatesCommand.php, app/Notifications/ServerAutoUpdateCompleted.php
The upgrade shell script gains pipefail strictness, locale forcing, temporary file logging, and prints Packages upgraded and Reboot required markers. OS::upgrade() changes from void to array, parsing those markers. UpdateJob accepts a notify flag and dispatches ServerAutoUpdateCompleted when upgrades or a reboot are detected. Update::update() and RunServerAutoUpdatesCommand wire the flag end-to-end.
SSL expiry notification tracking — migration, model, job, and SslCertificateExpiring
database/migrations/2026_06_17_074137_add_expiry_notified_at_to_ssls_table.php, app/Models/Ssl.php, app/Jobs/SSL/CheckSslExpiryJob.php, app/Notifications/SslCertificateExpiring.php
A new migration adds expiry_notified_at to ssls. The Ssl model gains the property and cast. CheckSslExpiryJob is refactored with a dirty flag, EXPIRY_WARNING_DAYS = 14 constant, eager-loaded site.server, and handleExpiryNotification() which resets, deduplicates, or sends SslCertificateExpiring notifications. The notification class computes domain and expiry state labels across three cases (null, past, future).
SSL wildcard renewal failure notification
app/Console/Commands/RenewSslCertificatesCommand.php, app/Jobs/SSL/CreateLetsEncryptWildcardSslJob.php, app/Notifications/SslRenewalFailed.php
RenewSslCertificatesCommand switches to lazy() iteration and passes isRenewal: true when dispatching the wildcard job. CreateLetsEncryptWildcardSslJob gains the isRenewal constructor flag and conditionally sends SslRenewalFailed in its failed() handler. The new notification renders plain text and an error email with domain label and management link.
BackupFailed and WebhookDeploymentFailed notifications
app/Jobs/Backup/RunJob.php, app/Notifications/BackupFailed.php, app/Jobs/Site/TriggerDeployFromWebhookJob.php, app/Notifications/WebhookDeploymentFailed.php
RunJob dispatches BackupFailed at the end of its failed() handler. TriggerDeployFromWebhookJob dispatches WebhookDeploymentFailed in its catch block after logging. Both new notification classes implement rawText() and error-styled toEmail() with action buttons.
Custom vendor mail HTML templates, CSS theme, and mail config
resources/views/vendor/mail/html/header.blade.php, resources/views/vendor/mail/html/message.blade.php, resources/views/vendor/mail/html/themes/default.css, config/mail.php, app/NotificationChannels/Email.php
New vendor mail overrides: logo-linked header template, complete message layout with header/slot/subcopy/footer, and 295 lines of email-safe CSS covering containers, typography, button colour variants (primary/success/error), panels, and break-all utility. config/mail.php defaults are updated to noreply@example.com and the app name. The Email channel connect() constructs email content via MailMessage.
Existing notification content updated to i18n, success/error styling, and action buttons
app/Notifications/DeploymentCompleted.php, app/Notifications/Server*.php, app/Notifications/Site*.php, app/Notifications/FailedToDelete*.php, app/Notifications/SourceControlDisconnected.php, resources/views/emails/project-invitation.blade.php
All existing toEmail() methods are updated to use __() translated strings, consistent error()/success() styling, and action() buttons with resource-specific URLs. Queueable trait usage is removed from two notification classes. DeploymentCompleted gains a commitSummary() helper and status-branched email levels. SiteInstallationFailed redirects users to retry from the site page. ServerInstallationSucceed removes its subject() method. The project invitation template copy and button colour are updated.
Feature and unit tests
tests/Feature/ServerTest.php, tests/Feature/CheckSslExpiriesCommandTest.php, tests/Unit/Notifications/NotificationMailTest.php
ServerTest adds tests for OS::upgrade() marker parsing, auto-update notification dispatch, manual-update silence, and no-change silence. CheckSslExpiriesCommandTest adds three tests covering send-once, skip-when-not-expiring, and rearm-after-renewal. NotificationMailTest adds eight unit tests verifying level, actionUrl routing, branding HTML, commit summary rendering, and project invitation URL.

Sequence Diagram

sequenceDiagram
  participant RunServerAutoUpdatesCommand
  participant UpdateJob
  participant OS
  participant Notifier
  participant CheckSslExpiryJob
  participant SslCertificateExpiring

  rect rgba(100, 149, 237, 0.5)
    Note over RunServerAutoUpdatesCommand,Notifier: Auto-update flow
    RunServerAutoUpdatesCommand->>UpdateJob: dispatch(server, notify=true)
    UpdateJob->>OS: upgrade()
    OS-->>UpdateJob: {upgraded: int, reboot_required: bool}
    UpdateJob->>Notifier: send(server, ServerAutoUpdateCompleted)
  end

  rect rgba(60, 179, 113, 0.5)
    Note over CheckSslExpiryJob,SslCertificateExpiring: SSL expiry check flow
    CheckSslExpiryJob->>CheckSslExpiryJob: checkCertificate(ssl) — parse and dirty flag
    CheckSslExpiryJob->>CheckSslExpiryJob: handleExpiryNotification(ssl)
    alt within 14-day window and not yet notified
      CheckSslExpiryJob->>Notifier: send(server, SslCertificateExpiring)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • saeedvaziry
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title '[Improvement] Email updates + more notifications' clearly relates to the main changes in the changeset, which involve substantial email notification improvements and new notification classes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/Jobs/Site/TriggerDeployFromWebhookJob.php (1)

38-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow deploy exceptions in the webhook flow

This catch block logs and notifies, but then suppresses the original failure. That marks the job as successful, so retry/failure handling is skipped for real deploy errors. Re-throw after side effects (or move notification to failed() to avoid per-attempt duplicates).

Suggested fix
             } catch (Throwable $e) {
                 ServerLog::log($site->server, 'deploy-failed', $e->getMessage(), $site);
                 Log::error('webhook-deploy-failed', [
                     'site_id' => $site->id,
                     'error' => $e->getMessage(),
                 ]);
                 Notifier::send($site, new WebhookDeploymentFailed($site));
+                throw $e;
             }

As per coding guidelines, "Let provider/service errors bubble up — don't silently catch and suppress exceptions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/Site/TriggerDeployFromWebhookJob.php` around lines 38 - 45, The
catch block in TriggerDeployFromWebhookJob that handles Throwable exceptions is
suppressing the original failure by not re-throwing it, which causes the job to
be marked as successful even when deployment fails. After performing the side
effects (ServerLog::log, Log::error, and Notifier::send with
WebhookDeploymentFailed), re-throw the exception to allow the queue system's
retry and failure handling to work properly. This ensures that real deploy
errors are not silently swallowed and the job failure is properly recorded.

Source: Coding guidelines

app/Console/Commands/RenewSslCertificatesCommand.php (1)

19-28: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Eager-load server to avoid N+1 during renewal sweep.

$ssl->server inside the cursor callback issues an extra query per SSL record. Add eager loading on the base query.

Proposed fix
         Ssl::query()
+            ->with('server')
             ->where('type', SslType::LETSENCRYPT)
             ->where('is_wildcard', true)
             ->whereNotNull('server_id')

As per coding guidelines: “Eager load relationships to prevent N+1 queries.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Console/Commands/RenewSslCertificatesCommand.php` around lines 19 - 28,
The query in the RenewSslCertificatesCommand is accessing the server
relationship on each Ssl model inside the cursor callback without eager loading
it, which causes an N+1 query problem. Add eager loading by chaining
->with('server') to the Ssl::query() builder before calling ->cursor() to load
all server relationships in a single query instead of issuing a separate query
for each $ssl->server access within the callback function.

Source: Coding guidelines

app/Jobs/SSL/CheckSslExpiryJob.php (1)

48-71: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not silently suppress notification/provider failures in this job path.

Line 69 catches every throwable and returns, which now also hides failures from Notifier::send(...) and persistence. Narrow the catch to certificate-read/parse failures only, and let provider/notification/save errors bubble so queue retries and visibility work correctly.

Proposed fix
-        try {
-            $certificate = trim($ssh->exec("sudo cat {$ssl->certificate_path}"));
+        try {
+            $certificate = trim($ssh->exec("sudo cat {$ssl->certificate_path}"));
 
             if (empty($certificate) || ! str_contains($certificate, 'BEGIN CERTIFICATE')) {
                 return;
             }
 
             $parsed = CertificateParser::parse($certificate);
-            $dirty = false;
-
-            if (! $ssl->expires_at?->equalTo($parsed['expires_at'])) {
-                $ssl->expires_at = $parsed['expires_at'];
-                $ssl->domains = $parsed['domains'];
-                $dirty = true;
-            }
-
-            $dirty = $this->handleExpiryNotification($ssl) || $dirty;
-
-            if ($dirty) {
-                $ssl->save();
-            }
-        } catch (Throwable) {
+        } catch (Throwable) {
             return;
         }
+
+        $dirty = false;
+        if (! $ssl->expires_at?->equalTo($parsed['expires_at'])) {
+            $ssl->expires_at = $parsed['expires_at'];
+            $ssl->domains = $parsed['domains'];
+            $dirty = true;
+        }
+
+        $dirty = $this->handleExpiryNotification($ssl) || $dirty;
+        if ($dirty) {
+            $ssl->save();
+        }

As per coding guidelines: “Let provider/service errors bubble up — flag silently-swallowed exceptions.”

Also applies to: 100-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/SSL/CheckSslExpiryJob.php` around lines 48 - 71, The current catch
(Throwable) block is too broad and suppresses all exceptions including failures
from handleExpiryNotification() and $ssl->save(), preventing queue retries and
error visibility. Narrow the try-catch scope to only wrap the certificate
reading and parsing operations (the SSH exec call and CertificateParser::parse()
call), and move the handleExpiryNotification() call and $ssl->save() invocation
outside the try-catch block so that notification and persistence failures bubble
up and are properly handled by the queue system. Specify a more narrow exception
type in the catch block if appropriate for certificate-related failures only.

Source: Coding guidelines

app/Notifications/ServerInstallationSucceed.php (1)

16-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove plaintext SSH password from notification content.

Line 20 and Line 35 expose authentication['pass'] in outbound notification text/email. That is a credential disclosure path if inboxes are compromised or forwarded.

Suggested fix
-        return __("Installation succeed for server [:server] \nServer IP: :ip \nUser: :user\nPassword: :password\n:link", [
+        return __("Installation succeed for server [:server] \nServer IP: :ip \nUser: :user\n:link", [
             'server' => $this->server->name,
             'ip' => $this->server->ip,
             'user' => $this->server->authentication['user'],
-            'password' => $this->server->authentication['pass'],
             'link' => url('/servers/'.$this->server->id),
         ]);
@@
             ->line(__('Your server [:server] has been installed successfully and is ready to use.', ['server' => $this->server->name]))
             ->line(__('Server IP: :ip', ['ip' => $this->server->ip]))
             ->line(__('SSH user: :user', ['user' => $this->server->authentication['user']]))
-            ->line(__('SSH password: :password', ['password' => $this->server->authentication['pass']]))
             ->action(__('Manage your server'), url('/servers/'.$this->server->id));

Also applies to: 33-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Notifications/ServerInstallationSucceed.php` around lines 16 - 21, The
ServerInstallationSucceed notification exposes the plaintext SSH password in the
notification message, which is a security risk. Remove the password field from
the notification content by deleting the line that contains 'password' =>
$this->server->authentication['pass'] from both occurrences in the method (the
one around line 20 and the duplicate around line 35), and also remove the
corresponding :password placeholder from the notification message string itself.
This ensures credentials are not disclosed through outbound notifications or
emails.
app/NotificationChannels/Email.php (1)

45-55: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Do not swallow mail provider exceptions in connect() (Line 45).

catch (Throwable) { return false; } suppresses provider/service failures and hides root-cause errors from callers.

Suggested fix
     public function connect(): bool
     {
-        try {
-            $message = (new MailMessage)
-                ->success()
-                ->subject(__('Email notifications connected'))
-                ->line(__('This confirms that email notifications are now connected for your Vito instance.'));
-            Mail::to($this->data()['email'])->send(
-                new NotificationMail($message->subject, $message->render())
-            );
-        } catch (Throwable) {
-            return false;
-        }
+        $message = (new MailMessage)
+            ->success()
+            ->subject(__('Email notifications connected'))
+            ->line(__('This confirms that email notifications are now connected for your Vito instance.'));
+        Mail::to($this->data()['email'])->send(
+            new NotificationMail($message->subject, $message->render())
+        );

         return true;
     }

As per coding guidelines, “Let provider/service errors bubble up — don't silently catch and suppress exceptions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/NotificationChannels/Email.php` around lines 45 - 55, Remove the catch
block that is catching Throwable exceptions in the connect() method. Instead of
silently catching all exceptions and returning false when Mail::to() and send()
fail, allow these exceptions to propagate up to the caller so that
provider/service errors can be properly handled and logged. Delete the entire
try-catch wrapper and keep only the Mail creation and sending logic.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Jobs/SSL/CheckSslExpiryJob.php`:
- Around line 58-61: In the CheckSslExpiryJob.php file, the condition checking
if `expires_at` needs updating only updates the `domains` field when the expiry
date changes. To prevent stale domains in the database, add a separate check to
update `domains` whenever they differ from the parsed domains, independently of
whether `expires_at` has changed. This ensures that domain/SAN changes are
reflected in the database even if the expiration timestamp remains the same.

---

Outside diff comments:
In `@app/Console/Commands/RenewSslCertificatesCommand.php`:
- Around line 19-28: The query in the RenewSslCertificatesCommand is accessing
the server relationship on each Ssl model inside the cursor callback without
eager loading it, which causes an N+1 query problem. Add eager loading by
chaining ->with('server') to the Ssl::query() builder before calling ->cursor()
to load all server relationships in a single query instead of issuing a separate
query for each $ssl->server access within the callback function.

In `@app/Jobs/Site/TriggerDeployFromWebhookJob.php`:
- Around line 38-45: The catch block in TriggerDeployFromWebhookJob that handles
Throwable exceptions is suppressing the original failure by not re-throwing it,
which causes the job to be marked as successful even when deployment fails.
After performing the side effects (ServerLog::log, Log::error, and
Notifier::send with WebhookDeploymentFailed), re-throw the exception to allow
the queue system's retry and failure handling to work properly. This ensures
that real deploy errors are not silently swallowed and the job failure is
properly recorded.

In `@app/Jobs/SSL/CheckSslExpiryJob.php`:
- Around line 48-71: The current catch (Throwable) block is too broad and
suppresses all exceptions including failures from handleExpiryNotification() and
$ssl->save(), preventing queue retries and error visibility. Narrow the
try-catch scope to only wrap the certificate reading and parsing operations (the
SSH exec call and CertificateParser::parse() call), and move the
handleExpiryNotification() call and $ssl->save() invocation outside the
try-catch block so that notification and persistence failures bubble up and are
properly handled by the queue system. Specify a more narrow exception type in
the catch block if appropriate for certificate-related failures only.

In `@app/NotificationChannels/Email.php`:
- Around line 45-55: Remove the catch block that is catching Throwable
exceptions in the connect() method. Instead of silently catching all exceptions
and returning false when Mail::to() and send() fail, allow these exceptions to
propagate up to the caller so that provider/service errors can be properly
handled and logged. Delete the entire try-catch wrapper and keep only the Mail
creation and sending logic.

In `@app/Notifications/ServerInstallationSucceed.php`:
- Around line 16-21: The ServerInstallationSucceed notification exposes the
plaintext SSH password in the notification message, which is a security risk.
Remove the password field from the notification content by deleting the line
that contains 'password' => $this->server->authentication['pass'] from both
occurrences in the method (the one around line 20 and the duplicate around line
35), and also remove the corresponding :password placeholder from the
notification message string itself. This ensures credentials are not disclosed
through outbound notifications or emails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b46920f-0984-4fe2-af44-e97cdfe5e4e4

📥 Commits

Reviewing files that changed from the base of the PR and between 9f829ac and c27778c.

⛔ Files ignored due to path filters (1)
  • public/email/logo.png is excluded by !**/*.png
📒 Files selected for processing (38)
  • app/Actions/Server/Update.php
  • app/Console/Commands/RenewSslCertificatesCommand.php
  • app/Console/Commands/RunServerAutoUpdatesCommand.php
  • app/Jobs/Backup/RunJob.php
  • app/Jobs/SSL/CheckSslExpiryJob.php
  • app/Jobs/SSL/CreateLetsEncryptWildcardSslJob.php
  • app/Jobs/Server/UpdateJob.php
  • app/Jobs/Site/TriggerDeployFromWebhookJob.php
  • app/Models/Ssl.php
  • app/NotificationChannels/Email.php
  • app/Notifications/BackupFailed.php
  • app/Notifications/DeploymentCompleted.php
  • app/Notifications/FailedToDeleteBackupFileFromProvider.php
  • app/Notifications/FailedToDeleteServerFromProvider.php
  • app/Notifications/ServerAutoUpdateCompleted.php
  • app/Notifications/ServerConnected.php
  • app/Notifications/ServerDisconnected.php
  • app/Notifications/ServerInstallationFailed.php
  • app/Notifications/ServerInstallationStarted.php
  • app/Notifications/ServerInstallationSucceed.php
  • app/Notifications/ServerUpdateFailed.php
  • app/Notifications/SiteInstallationFailed.php
  • app/Notifications/SiteInstallationSucceed.php
  • app/Notifications/SourceControlDisconnected.php
  • app/Notifications/SslCertificateExpiring.php
  • app/Notifications/SslRenewalFailed.php
  • app/Notifications/WebhookDeploymentFailed.php
  • app/SSH/OS/OS.php
  • config/mail.php
  • database/migrations/2026_06_17_074137_add_expiry_notified_at_to_ssls_table.php
  • resources/views/emails/project-invitation.blade.php
  • resources/views/ssh/os/upgrade.blade.php
  • resources/views/vendor/mail/html/header.blade.php
  • resources/views/vendor/mail/html/message.blade.php
  • resources/views/vendor/mail/html/themes/default.css
  • tests/Feature/CheckSslExpiriesCommandTest.php
  • tests/Feature/ServerTest.php
  • tests/Unit/Notifications/NotificationMailTest.php

Comment thread app/Jobs/SSL/CheckSslExpiryJob.php Outdated
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/Jobs/SSL/CheckSslExpiryJob.php (1)

16-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing UniqueQueue trait required by coding guidelines.

Queued jobs in this codebase must use both Queueable and UniqueQueue traits, and wrap work in $this->run($key, callable) to prevent duplicate jobs via cache locks.

Proposed fix
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Queue\Queueable;
+use App\Traits\UniqueQueue;
 use Throwable;

 class CheckSslExpiryJob implements ShouldQueue
 {
     use Queueable;
+    use UniqueQueue;

     private const EXPIRY_WARNING_DAYS = 14;

     public function __construct(protected Server $server) {}

     public function handle(): void
     {
-        $ssls = Ssl::query()
+        $this->run("check-ssl-expiry-{$this->server->id}", function () {
+            $ssls = Ssl::query()
             // ... rest of handle logic
+        });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/SSL/CheckSslExpiryJob.php` around lines 16 - 18, The
CheckSslExpiryJob class is missing the UniqueQueue trait which is required by
coding guidelines. Add the UniqueQueue trait to the class alongside the existing
Queueable trait. Additionally, wrap any job work logic inside the handle method
(or equivalent entry point) with a call to $this->run($key, callable) where the
key uniquely identifies the job, to ensure duplicate jobs are prevented via
cache locks.

Source: Coding guidelines

app/Notifications/SslCertificateExpiring.php (1)

63-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Malformed URL when site relationship is null.

If $ssl->site is null (possible due to nullOnDelete() foreign key), link() produces /servers//sites/{id}/domains. Consider returning an empty string or a fallback URL, or guard against this case upstream.

Proposed fix
     private function link(): string
     {
+        if ($this->ssl->site === null) {
+            return url('/');
+        }
+
         return url('/servers/'.$this->ssl->site?->server_id.'/sites/'.$this->ssl->site_id.'/domains');
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Notifications/SslCertificateExpiring.php` around lines 63 - 66, The
link() method constructs a URL using $this->ssl->site?->server_id, which will
result in a malformed URL with double slashes like /servers//sites/{id}/domains
if the site relationship is null. Add a null check for $this->ssl->site before
building the URL in the link() method. If the site relationship is null, return
an empty string or a fallback URL instead of attempting to construct the URL
with missing server_id data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Jobs/SSL/CheckSslExpiryJob.php`:
- Around line 56-58: The catch block for Throwable in CheckSslExpiryJob is
silently swallowing exceptions by returning without any logging, which prevents
visibility of failures. Replace the silent return with proper error logging that
captures the actual exception details before returning, so that provider and
service errors are visible and can be debugged effectively.

---

Outside diff comments:
In `@app/Jobs/SSL/CheckSslExpiryJob.php`:
- Around line 16-18: The CheckSslExpiryJob class is missing the UniqueQueue
trait which is required by coding guidelines. Add the UniqueQueue trait to the
class alongside the existing Queueable trait. Additionally, wrap any job work
logic inside the handle method (or equivalent entry point) with a call to
$this->run($key, callable) where the key uniquely identifies the job, to ensure
duplicate jobs are prevented via cache locks.

In `@app/Notifications/SslCertificateExpiring.php`:
- Around line 63-66: The link() method constructs a URL using
$this->ssl->site?->server_id, which will result in a malformed URL with double
slashes like /servers//sites/{id}/domains if the site relationship is null. Add
a null check for $this->ssl->site before building the URL in the link() method.
If the site relationship is null, return an empty string or a fallback URL
instead of attempting to construct the URL with missing server_id data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6d9dd3e0-5fb0-4868-b8ba-74a4c4af012a

📥 Commits

Reviewing files that changed from the base of the PR and between c27778c and 2dbb806.

📒 Files selected for processing (3)
  • app/Console/Commands/RenewSslCertificatesCommand.php
  • app/Jobs/SSL/CheckSslExpiryJob.php
  • app/Notifications/SslCertificateExpiring.php

Comment thread app/Jobs/SSL/CheckSslExpiryJob.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands Vito’s notification system by adding new notification types (auto-update completion, SSL expiry warnings, backup/webhook/renewal failures) and modernizing email rendering/branding through custom Markdown mail templates.

Changes:

  • Added new notifications and triggers for server auto-updates, SSL expiry checks, backup failures, webhook deploy failures, and SSL renewal failures.
  • Updated multiple existing notifications with improved wording, localization, and success/error levels.
  • Introduced a custom Laravel Markdown mail theme (CSS + header/message templates) and added tests covering key notification email behaviors.

Reviewed changes

Copilot reviewed 35 out of 39 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Unit/Notifications/NotificationMailTest.php Adds unit coverage to assert email rendering, branding, levels, and action URLs across notifications/mailables.
tests/Feature/ServerTest.php Adds feature coverage for OS upgrade marker parsing and auto-update notification behavior.
tests/Feature/CheckSslExpiriesCommandTest.php Adds feature coverage ensuring SSL expiry notifications trigger once and re-arm after renewal.
resources/views/vendor/mail/html/themes/default.css Adds a custom HTML email theme stylesheet for consistent branding.
resources/views/vendor/mail/html/message.blade.php Overrides Laravel’s HTML message template for the new branding/layout.
resources/views/vendor/mail/html/header.blade.php Overrides the mail header to use a branded logo.
resources/views/ssh/os/upgrade.blade.php Enhances OS upgrade script to emit upgrade/reboot markers for parsing.
resources/views/emails/project-invitation.blade.php Updates invitation email content/structure to match improved template styling and subcopy usage.
database/migrations/2026_06_17_074137_add_expiry_notified_at_to_ssls_table.php Adds expiry_notified_at to support one-time SSL expiry warnings.
config/mail.php Adjusts default “from” address/name to better defaults for branded email.
app/SSH/OS/OS.php Changes upgrade() to return parsed upgrade results (upgraded count, reboot required).
app/Notifications/WebhookDeploymentFailed.php Adds a new notification for webhook-triggered deployment failures.
app/Notifications/SslRenewalFailed.php Adds a new notification for wildcard SSL renewal failures.
app/Notifications/SslCertificateExpiring.php Adds a new notification for expiring/expired SSL certificates.
app/Notifications/SourceControlDisconnected.php Improves email formatting and severity (error) and adds a management CTA.
app/Notifications/SiteInstallationSucceed.php Improves copy and sets success level + clearer CTA.
app/Notifications/SiteInstallationFailed.php Improves copy, sets error level, and links to the site for retry.
app/Notifications/ServerUpdateFailed.php Improves copy and sets error level + clearer CTA.
app/Notifications/ServerInstallationSucceed.php Improves copy, sets success level, and links directly to the server.
app/Notifications/ServerInstallationStarted.php Improves wording and CTA for installation-in-progress emails.
app/Notifications/ServerInstallationFailed.php Improves wording and sets error level + clearer CTA.
app/Notifications/ServerDisconnected.php Improves wording, sets error level, and adds CTA.
app/Notifications/ServerConnected.php Improves wording, sets success level, and adds CTA.
app/Notifications/ServerAutoUpdateCompleted.php Adds a new notification describing auto-update results and next steps.
app/Notifications/FailedToDeleteServerFromProvider.php Improves copy and sets error level for provider deletion failures.
app/Notifications/FailedToDeleteBackupFileFromProvider.php Improves copy and sets error level for provider backup file deletion failures.
app/Notifications/DeploymentCompleted.php Enhances deployment completion emails with commit summary/author and status-based level.
app/Notifications/BackupFailed.php Adds a new notification for backup job failure.
app/NotificationChannels/Email.php Updates email channel connection test email to use MailMessage rendering and the new template.
app/Models/Ssl.php Adds expiry_notified_at casting + PHPDoc to support the new expiry notification logic.
app/Jobs/SSL/CreateLetsEncryptWildcardSslJob.php Adds renewal context and sends renewal-failure notifications when appropriate.
app/Jobs/SSL/CheckSslExpiryJob.php Adds expiry warning logic, notification throttling via expiry_notified_at, and eager loads needed relations.
app/Jobs/Site/TriggerDeployFromWebhookJob.php Sends a failure notification when a webhook-triggered deploy cannot start.
app/Jobs/Server/UpdateJob.php Captures OS upgrade results and optionally notifies on meaningful auto-update changes.
app/Jobs/Backup/RunJob.php Sends a failure notification when the backup job ultimately fails.
app/Console/Commands/RunServerAutoUpdatesCommand.php Enables notifications for scheduled auto-updates.
app/Console/Commands/RenewSslCertificatesCommand.php Switches to lazy iteration and marks renewal jobs to enable renewal-failure notifications.
app/Actions/Server/Update.php Adds a notify flag and passes it through to the update job.

Comment thread resources/views/ssh/os/upgrade.blade.php Outdated
@RichardAnderson RichardAnderson marked this pull request as ready for review June 17, 2026 09:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Jobs/SSL/CheckSslExpiryJob.php (1)

17-26: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enforce the job queue contract required for app/Jobs/**.

This job currently misses the mandated UniqueQueue lock pattern (use UniqueQueue + $this->run($key, callable)) and a failed(Exception $e) handler. That leaves duplicate runs and inconsistent failure handling on the table.

As per coding guidelines: “Jobs implement ShouldQueue with BOTH Queueable and UniqueQueue traits; wrap work in $this->run($key, callable); implement failed(Exception $e) for status updates.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/SSL/CheckSslExpiryJob.php` around lines 17 - 26, The
CheckSslExpiryJob class is missing the required UniqueQueue trait and pattern
enforcement. Add the UniqueQueue trait alongside the existing Queueable trait in
the class declaration, wrap the existing logic in the handle() method using
$this->run($key, callable) to enforce unique job processing and prevent
duplicates, and implement a failed(Exception $e) method to handle job failures
with appropriate error logging and status updates according to the coding
guidelines for jobs in the app/Jobs directory.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/Jobs/SSL/CheckSslExpiryJob.php`:
- Around line 17-26: The CheckSslExpiryJob class is missing the required
UniqueQueue trait and pattern enforcement. Add the UniqueQueue trait alongside
the existing Queueable trait in the class declaration, wrap the existing logic
in the handle() method using $this->run($key, callable) to enforce unique job
processing and prevent duplicates, and implement a failed(Exception $e) method
to handle job failures with appropriate error logging and status updates
according to the coding guidelines for jobs in the app/Jobs directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 53160301-cdde-4b21-b259-045663e783a8

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbb806 and 140d351.

📒 Files selected for processing (2)
  • app/Jobs/SSL/CheckSslExpiryJob.php
  • resources/views/ssh/os/upgrade.blade.php

@saeedvaziry saeedvaziry merged commit e414e6b into vitodeploy:4.x Jun 18, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants