Skip to content

Backup hosts#2125

Open
Boy132 wants to merge 33 commits into
mainfrom
boy132/backup-hosts
Open

Backup hosts#2125
Boy132 wants to merge 33 commits into
mainfrom
boy132/backup-hosts

Conversation

@Boy132

@Boy132 Boy132 commented Jan 20, 2026

Copy link
Copy Markdown
Member

Closes #1919, #2163
Also allows to register custom adapters (#1600)

Largely untested!!!

@Boy132 Boy132 self-assigned this Jan 20, 2026
@coderabbitai

coderabbitai Bot commented Jan 20, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduce a pluggable BackupHost / BackupAdapter system: new adapter interface, registry and abstract schema, S3 and Wings adapter implementations, BackupHost model/migration/factory, provider wiring, controller/service refactors to delegate to adapters, Filament UI for BackupHosts, and removal of legacy BackupManager/S3Filesystem and disk-based backup config.

Changes

Backup Adapter & Hosts

Layer / File(s) Summary
Data Shape / DB
database/migrations/..._create_backup_hosts_table.php, app/Models/BackupHost.php, database/Factories/BackupHostFactory.php, app/Models/Backup.php
Add BackupHost model, factory and migration; add backup_host_id to backups, drop disk; Backup model gains backupHost() relation and validation updates.
Core Adapter Types
app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php, app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php, app/Extensions/BackupAdapter/BackupAdapterService.php
Introduce adapter interface, abstract base schema (getName), and registry service for adapter schemas with register/get/mappings.
Adapter Implementations (S3)
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
S3 schema: create/delete, presigned GetObject download link, multipart upload start/part URL generation, and multipart completion/abort; exposes configuration form.
Adapter Implementations (Wings)
app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php
Wings schema: delegate create/delete to DaemonBackupRepository (suppress 404 on delete), generate short-lived JWT download tokens, minimal/no-config form.
Provider & Bootstrap Wiring
app/Providers/Extensions/BackupAdapterServiceProvider.php, bootstrap/providers.php
Register BackupAdapterService singleton and default adapters (Wings, S3); replace old BackupsServiceProvider in bootstrap.
Legacy Removal & Config
app/Extensions/Backups/BackupManager.php (removed), app/Extensions/Filesystem/S3Filesystem.php (removed), app/Providers/BackupsServiceProvider.php (removed), config/backups.php
Remove legacy BackupManager, S3Filesystem and old provider; remove disk-based backup config and default driver; remove About entry and related settings UI driver toggle.
Daemon Repository
app/Repositories/Daemon/DaemonBackupRepository.php
Rename backup()create(), remove repository-level adapter state and use backup->backupHost->schema in requests.
Services Refactored
app/Services/Backups/InitiateBackupService.php, app/Services/Backups/DeleteBackupService.php, app/Services/Backups/DownloadLinkService.php
Services switch to resolving schemas through BackupAdapterService and delegate create/delete/download logic to adapters; constructors and DB payloads updated to use BackupHost/schema.
Controllers & Remote APIs
app/Http/Controllers/Api/Client/Servers/BackupController.php, app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php, app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Controllers now inject BackupAdapterService, resolve schemas via backupHost->schema, validate schema presence, and delegate upload/download/multipart operations to adapter schemas.
Filament Admin UI
app/Filament/Admin/Resources/BackupHosts/..., lang/en/admin/backuphost.php, app/Filament/Admin/Pages/Settings.php
Add BackupHost resource, pages, relation manager and translations; remove APP_BACKUP_DRIVER / S3 UI fields; integrate adapter selection into forms and dynamic configuration section.
Transfer / Server Flows
app/Services/Servers/TransferServerService.php, app/Filament/Admin/Resources/Servers/Pages/EditServer.php, app/Extensions/Tasks/Schemas/CreateBackupSchema.php
Transfer and server edit flows updated to accept backup UUID arrays and to filter/handle Wings backups via BackupAdapterService; CreateBackupSchema constructor/property renamed.
Policies, Node Relations, Tests
app/Policies/BackupHostPolicy.php, app/Models/Node.php, tests/.../Backup/*
Add BackupHost policy; add Node::backupHosts relation; update tests to create/assign BackupHost; remove S3-specific deletion test.
Enum update
app/Enums/RolePermissionModels.php
Add enum case BackupHost = 'backupHost'.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Initiator as InitiateBackupService
    participant AdapterSvc as BackupAdapterService
    participant Schema as BackupSchema (S3/Wings)
    participant DB as Database
    participant External as External Storage/Daemon

    User->>Initiator: request create backup
    Initiator->>DB: insert Backup (backup_host_id)
    Initiator->>AdapterSvc: get(backupHost.schema)
    AdapterSvc-->>Initiator: BackupSchema instance
    Initiator->>Schema: createBackup(backup)
    Schema->>External: perform storage action (daemon or S3)
    External-->>Schema: confirmation
    Schema-->>Initiator: completion
    Initiator-->>User: respond backup created
Loading
sequenceDiagram
    participant Client as Client
    participant Controller as BackupController
    participant AdapterSvc as BackupAdapterService
    participant Schema as BackupSchema (S3/Wings)
    participant External as External Storage/Daemon

    Client->>Controller: request download link
    Controller->>AdapterSvc: get(backup.backupHost.schema)
    AdapterSvc-->>Controller: BackupSchema instance
    Controller->>Schema: getDownloadLink(backup, user)
    Schema->>External: generate presigned URL or JWT token
    External-->>Schema: url/token
    Schema-->>Controller: download URL
    Controller-->>Client: return URL
Loading

Possibly related PRs

  • pelican-dev/panel#1911 — touches CreateBackupSchema constructor/property changes related to InitiateBackupService naming.
  • pelican-dev/panel#2313 — related to BackupStatusController and multipart/backup-completion flows.
  • pelican-dev/panel#2068 — related changes to transfer flow and backup UUID forwarding/deletion during server transfers.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Backup hosts' accurately reflects the main feature addition: introducing a new BackupHost model and related infrastructure for managing multiple backup configurations.
Description check ✅ Passed The description references closed issues (#1919, #2163) and mentions the ability to register custom adapters, which are related to the changeset's backup host implementation.
Linked Issues check ✅ Passed The PR successfully implements the requirements from #1919: moves backup configuration from settings to a dedicated resource (BackupHostResource), enables multiple backup buckets (BackupAdapterService with S3BackupSchema), supports node assignment (BackupHost.nodes relation), and preserves local storage (WingsBackupSchema).
Out of Scope Changes check ✅ Passed All changes are focused on the backup host feature: adding the BackupHost model and relationships, creating adapter schemas and services, updating existing backup services to use the new system, and removing the old BackupManager. No unrelated changes detected.

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


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.

@Boy132

Boy132 commented Jan 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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: 12

🤖 Fix all issues with AI agents
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 106-154: In getUploadParts, accessing
$backup->backupHost->configuration['storage_class'] directly can emit warnings
when that key is missing; change the $storageClass assignment to safely read the
key (e.g. $storageClass = $backup->backupHost->configuration['storage_class'] ??
null or use array_key_exists) and keep the subsequent null check (if
(!is_null($storageClass)) { $params['StorageClass'] = $storageClass; }) so
missing config no longer triggers PHP notices.
- Around line 19-36: The S3 client is using wrong config keys and a single
cached client for all hosts; in S3BackupSchema::createClient(BackupHost
$backupHost) map the form fields to AWS SDK names (map access_key → key,
secret_key → secret, default_region → region) and preserve token if present,
build credentials via Arr::only on those mapped keys, and include region/version
in $config before constructing S3Client; change the client cache to be per-host
(keyed by $backupHost->id) instead of the single ?S3Client $client to avoid
cross-host credential leakage; also ensure any use sites like getUploadParts
read and respect storage_class from the host configuration (defaulting if
absent).

In `@app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php`:
- Around line 27-31: The DeleteAction in EditBackupHost.php currently only
disables deletion when the BackupHost has associated backups; update
DeleteAction::make() so it also disables (and adjusts its label) when
BackupHost::count() === 1 to prevent deleting the last host. Specifically,
change the ->disabled(...) callback to return true if
$backupHost->backups()->count() > 0 OR BackupHost::count() === 1, and update the
->label(...) callback to return a distinct translatable message when
BackupHost::count() === 1 (e.g., a "cannot delete last host" message) while
preserving the existing backups-related message; reference DeleteAction::make(),
BackupHost::backups(), and BackupHost::count() to locate the code.

In `@app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php`:
- Around line 57-63: The code in BackupRemoteUploadController currently resolves
the schema from collect($node->backupHosts)->first(), which can pick the wrong
host; instead resolve the schema from the Backup model's host property (use
$backup->host or equivalent) and validate it is present before using it. Replace
the get(collect($node->backupHosts)->first()->schema) call with fetching the
host associated with $backup (guarding against null), then call
$this->backupService->get($backupHost->schema) and keep the instanceof
S3BackupSchema check; if the backup's host is null or the schema lookup fails,
throw a BadRequestHttpException.

In `@app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`:
- Around line 72-76: The current logic in BackupStatusController resolves the S3
schema using collect($node->backupHosts)->first(), which can pick the wrong host
or be null; instead resolve the schema from the backup's own host and guard
against missing hosts: call $this->backupService->get(...) using the host
associated with the backup model (the host/backupHost property on $model) and
only invoke S3BackupSchema->completeMultipartUpload($model, $successful,
$request->input('parts')) if the resolved schema is an S3BackupSchema and the
host/schema is not null.

In `@app/Services/Backups/DeleteBackupService.php`:
- Around line 34-37: The code in DeleteBackupService uses
$backup->backupHost->schema without checking whether $backup->backupHost is
null, which can cause a fatal error; update the method in DeleteBackupService to
first verify $backup->backupHost is not null (e.g., if null throw a clear
Exception or handle gracefully) before calling
$this->backupService->get($backup->backupHost->schema), and then proceed to
retrieve $schema and the subsequent logic only when the host exists.

In `@app/Services/Backups/DownloadLinkService.php`:
- Around line 22-25: The code accesses $backup->backupHost->schema without
checking if backupHost exists; add a null check in DownloadLinkService before
calling $this->backupService->get(...) so that if $backup->backupHost is null
you throw a specific exception (create e.g., MissingBackupHostException or
BackupAdapterException) instead of a generic Exception; update the method to
validate $backup->backupHost, throw the new exception with a clear message when
missing, and ensure any callers/handlers are adjusted to catch the new exception
type.

In `@app/Services/Backups/InitiateBackupService.php`:
- Around line 113-133: The current BackupHost selection (using
BackupHost::doesntHave('nodes')->orWhereHas('nodes', fn($q) => $q->where('id',
$server->node->id))->firstOrFail()) can pick a global host even when a
node-specific host exists; change the logic to first attempt to find a host
assigned to the server's node (e.g. BackupHost::whereHas('nodes', fn($q) =>
$q->where('id', $server->node->id))->first()), and if that returns null then
fall back to a global host (e.g.
BackupHost::doesntHave('nodes')->firstOrFail()); keep the remaining flow
(loading $schema, transaction, Backup::create, and $schema->createBackup) the
same but use the explicitly chosen $backupHost variable.

In `@app/Services/Servers/TransferServerService.php`:
- Line 31: The notify() method in TransferServerService.php currently ignores
the incoming $backup_uuids by overwriting them with $backups = [], causing
transfers to omit selected backups; update notify() to map/validate the provided
$backup_uuids into the $backups payload instead of emptying it (preserve
$backup_uuids, transform to the structure expected by the daemon API), ensure
you reference the $backup_uuids parameter and $backups variable inside notify(),
and adjust the payload format to match the daemon API spec (e.g., array of
objects or keyed field) before sending the transfer notification.

In `@database/migrations/2026_01_16_081858_create_backup_hosts_table.php`:
- Line 52: The migration's config array sets the 'prefix' key using the wrong
env var AWS_BACKUPS_BUCKET (copy-paste); update the 'prefix' entry to use the
dedicated environment variable (e.g., AWS_BACKUPS_PREFIX) instead of
AWS_BACKUPS_BUCKET in the migration file (create_backup_hosts_table.php) so the
'prefix' value is read from env('AWS_BACKUPS_PREFIX', '') and not from the
bucket variable.
- Around line 72-83: In the down() method the migration drops backup_hosts
before removing the backup_host_id foreign key on the backups table, which will
violate the FK constraint; update down() to first modify the backups table (in
the Schema::table('backups', ...) block) by adding the disk column, then remove
the foreign key (use $table->dropForeign(['backup_host_id']) or the explicit
constraint name) and then dropColumn('backup_host_id'), and only after that drop
the pivot table (backup_host_node) and finally drop backup_hosts (i.e., reorder
operations so foreign key is removed before dropping the referenced table).

In `@lang/en/admin/backuphost.php`:
- Line 5: The translation string 'model_label_plural' currently reads "Database
Hosts" but should be "Backup Hosts"; update the value for the
'model_label_plural' key in this translation file to "Backup Hosts" (preserve
surrounding quotes and comma) so the plural label matches the backup host
context.
🧹 Nitpick comments (7)
app/Policies/BackupHostPolicy.php (1)

21-24: Avoid N+1 queries in node authorization checks.
canTarget() can hit the DB per node; a set-based check reduces queries for hosts with many nodes.

♻️ Example set-based check
-        foreach ($backupHost->nodes as $node) {
-            if (!$user->canTarget($node)) {
-                return false;
-            }
-        }
+        $nodeIds = $backupHost->nodes->modelKeys();
+        if ($nodeIds !== [] && $user->accessibleNodes()->whereIn('id', $nodeIds)->count() !== count($nodeIds)) {
+            return false;
+        }
app/Models/Node.php (1)

279-282: Use canonical belongsToMany casing for consistency.

PHP is case‑insensitive here, but consistent casing avoids IDE/static analysis confusion.

♻️ Proposed tweak
-    public function backupHosts(): BelongsToMany
-    {
-        return $this->BelongsToMany(BackupHost::class);
-    }
+    /** `@return` BelongsToMany<BackupHost, $this> */
+    public function backupHosts(): BelongsToMany
+    {
+        return $this->belongsToMany(BackupHost::class);
+    }
database/migrations/2026_01_16_081858_create_backup_hosts_table.php (2)

60-64: Avoid using Eloquent models in migrations.

Using BackupHost::create() in a migration is fragile. If the model's $fillable, validation rules, or other attributes change in the future, this migration may break when run on a fresh database. Use query builder instead.

Proposed fix
-        $backupHost = BackupHost::create([
-            'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
-            'schema' => $oldDriver,
-            'configuration' => $oldConfiguration,
-        ]);
+        $backupHostId = DB::table('backup_hosts')->insertGetId([
+            'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
+            'schema' => $oldDriver,
+            'configuration' => $oldConfiguration ? json_encode($oldConfiguration) : null,
+            'created_at' => now(),
+            'updated_at' => now(),
+        ]);

-        DB::table('backups')->update(['backup_host_id' => $backupHost->id]);
+        DB::table('backups')->update(['backup_host_id' => $backupHostId]);

Also remove the use App\Models\BackupHost; import at the top.


36-41: Consider making backup_host_id nullable initially during migration.

Adding a non-nullable foreign key column and then immediately updating all rows works, but if the backups table is large, this could cause issues. Additionally, if the migration fails partway, you may end up with rows having a 0 or invalid backup_host_id.

A safer pattern is to:

  1. Add the column as nullable
  2. Create the backup host and update existing records
  3. Alter the column to be non-nullable

This is a minor concern given the migration context, but worth considering for robustness.

app/Services/Backups/DeleteBackupService.php (1)

16-19: Update docblock to reflect adapter-based architecture.

The comment mentions "If the backup is stored in S3" but the implementation is now adapter-agnostic. Consider updating the documentation to reflect the new architecture.

Proposed update
     /**
-     * Deletes a backup from the system. If the backup is stored in S3 a request
-     * will be made to delete that backup from the disk as well.
+     * Deletes a backup from the system. The backup adapter handles
+     * removing the backup data from the configured storage backend.
      *
      * `@throws` Throwable
      */
app/Models/BackupHost.php (1)

46-50: Align casts with CarbonImmutable docblocks.

Consider casting id, created_at, and updated_at to match the declared types and other models’ patterns.

♻️ Suggested adjustment
     protected function casts(): array
     {
         return [
+            'id' => 'int',
             'configuration' => 'array',
+            'created_at' => 'immutable_datetime',
+            'updated_at' => 'immutable_datetime',
         ];
     }
app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php (1)

57-62: Use a Form/Schema component for the “no configuration” message.

TextEntry is an Infolists component and TextEntry::make(trans(...)) uses a translation string as the state path. If this schema is rendered in a form, it may not display correctly. Prefer a Forms component like Placeholder (or a Schema-compatible view field).

♻️ Suggested adjustment
-use Filament\Infolists\Components\TextEntry;
+use Filament\Forms\Components\Placeholder;
@@
     /** `@return` Component[] */
     public function getConfigurationForm(): array
     {
         return [
-            TextEntry::make(trans('admin/backuphost.no_configuration')),
+            Placeholder::make('no_configuration')
+                ->content(trans('admin/backuphost.no_configuration')),
         ];
     }

Comment thread app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
Comment thread app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
Comment thread app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php Outdated
Comment thread app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Comment thread app/Services/Backups/InitiateBackupService.php Outdated
Comment thread app/Services/Servers/TransferServerService.php Outdated
Comment thread database/migrations/2026_01_16_081858_create_backup_hosts_table.php
Comment thread database/migrations/2026_01_16_081858_create_backup_hosts_table.php
Comment thread lang/en/admin/backuphost.php Outdated
@Boy132

Boy132 commented Jan 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

@Boy132

Boy132 commented Feb 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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: 3

Caution

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

⚠️ Outside diff range comments (1)
app/Filament/Server/Resources/Backups/BackupResource.php (1)

205-216: ⚠️ Potential issue | 🟡 Minor

Handle download-link failures gracefully before starting the restore.

DownloadLinkService::handle() can throw (e.g., unknown adapter). Since this is now unconditional, a failure will surface as a generic UI error and may still log a restore attempt. Consider precomputing the URL with a friendly notification and only logging/continuing on success.

💡 Suggested adjustment
-                            $log = Activity::event('server:backup.restore')
-                                ->subject($backup)
-                                ->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
-
-                            $log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
-                                $url = $downloadLinkService->handle($backup, user());
+                            try {
+                                $url = $downloadLinkService->handle($backup, user());
+                            } catch (Throwable $e) {
+                                return Notification::make()
+                                    ->title(trans('server/backup.actions.restore.notification_fail'))
+                                    ->body(trans('server/backup.actions.restore.notification_fail_body_adapter'))
+                                    ->danger()
+                                    ->send();
+                            }
+
+                            $log = Activity::event('server:backup.restore')
+                                ->subject($backup)
+                                ->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
+
+                            $log->transaction(function () use ($daemonRepository, $backup, $server, $data, $url) {
                                 // Update the status right away for the server so that we know not to allow certain
                                 // actions against it via the Panel API.
                                 $server->update(['status' => ServerState::RestoringBackup]);
 
                                 $daemonRepository->setServer($server)->restore($backup, $url, $data['truncate']);
                             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Server/Resources/Backups/BackupResource.php` around lines 205 -
216, Wrap the call to DownloadLinkService::handle($backup, user()) in a
try/catch before you create/commit the Activity log and before changing server
state: if handle() throws, catch the exception, return a friendly
notification/error to the user and abort the restore flow so no
Activity::event('server:backup.restore') is committed and
$server->update(['status' => ServerState::RestoringBackup]) is not executed;
only after successfully obtaining $url should you create the $log, call
$log->transaction(...) and invoke
$daemonRepository->setServer($server)->restore($backup, $url,
$data['truncate']).
🧹 Nitpick comments (1)
app/Models/Node.php (1)

281-284: Fix method call casing and add missing @return annotation.

$this->BelongsToMany(...) at Line 283 has an uppercase B. PHP method calls are case-insensitive so this works at runtime, but it's inconsistent with databaseHosts() (and every other relation in the file) that use lowercase $this->belongsToMany(...). The method also lacks the generic @return annotation that was just added to databaseHosts().

♻️ Proposed fix
+   /** `@return` BelongsToMany<BackupHost, $this> */
    public function backupHosts(): BelongsToMany
    {
-       return $this->BelongsToMany(BackupHost::class);
+       return $this->belongsToMany(BackupHost::class);
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Node.php` around lines 281 - 284, The relation method
backupHosts() uses incorrect casing and lacks a return annotation: replace the
call to $this->BelongsToMany(BackupHost::class) with the conventional
$this->belongsToMany(BackupHost::class) and add the docblock `@return`
\Illuminate\Database\Eloquent\Relations\BelongsToMany (or the project's
BelongsToMany import) above the backupHosts() method to match databaseHosts()
and other relations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 1088-1094: Extract the repeated filter into a private helper on
the EditServer class to avoid null dereferences and DRY: add a private function
getWingsBackups(Server $server, BackupAdapterService $backupService) that
returns $server->backups->filter(fn($backup) => $backup->backupHost !== null &&
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema);
then update the three closures to call this helper: for options use
$this->getWingsBackups($server, $backupService)->mapWithKeys(...), for columns
compute ceil($this->getWingsBackups($record, $backupService)->count() / 4), and
for hidden use $this->getWingsBackups($server, $backupService)->count() === 0 so
you only check backupHost once and avoid null-dereference.
- Around line 987-992: The closure that iterates backups accesses
$backup->backupHost->schema which can be null and causes a fatal error; update
the closure in the ->each(...) block to first fetch $host = $backup->backupHost
and guard it (if !$host then call $backup->delete() and return) before calling
$backupService->get($host->schema), then continue with the existing check for
instanceof WingsBackupSchema and $backup->delete() as needed.
- Around line 987-992: The current iterator calls $backup->delete() directly
which only removes the DB record and leaves files on the Wings daemon; retrieve
the schema via $backupService->get(...) and if it's an instance of
WingsBackupSchema call $schema->deleteBackup($backup) before deleting the DB
record, and perform both actions inside a transaction (follow the pattern in
DeleteBackupService::handle()) so the daemon DELETE is attempted before calling
$backup->delete().

---

Outside diff comments:
In `@app/Filament/Server/Resources/Backups/BackupResource.php`:
- Around line 205-216: Wrap the call to DownloadLinkService::handle($backup,
user()) in a try/catch before you create/commit the Activity log and before
changing server state: if handle() throws, catch the exception, return a
friendly notification/error to the user and abort the restore flow so no
Activity::event('server:backup.restore') is committed and
$server->update(['status' => ServerState::RestoringBackup]) is not executed;
only after successfully obtaining $url should you create the $log, call
$log->transaction(...) and invoke
$daemonRepository->setServer($server)->restore($backup, $url,
$data['truncate']).

---

Nitpick comments:
In `@app/Models/Node.php`:
- Around line 281-284: The relation method backupHosts() uses incorrect casing
and lacks a return annotation: replace the call to
$this->BelongsToMany(BackupHost::class) with the conventional
$this->belongsToMany(BackupHost::class) and add the docblock `@return`
\Illuminate\Database\Eloquent\Relations\BelongsToMany (or the project's
BelongsToMany import) above the backupHosts() method to match databaseHosts()
and other relations.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7da9d8c and c215e95.

📒 Files selected for processing (6)
  • app/Enums/RolePermissionModels.php
  • app/Filament/Admin/Pages/Settings.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Models/Node.php
  • app/Providers/AppServiceProvider.php
💤 Files with no reviewable changes (2)
  • app/Filament/Admin/Pages/Settings.php
  • app/Providers/AppServiceProvider.php

Comment thread app/Filament/Admin/Resources/Servers/Pages/EditServer.php
Comment thread app/Filament/Admin/Resources/Servers/Pages/EditServer.php
@Boy132 Boy132 marked this pull request as ready for review February 24, 2026 12:38

@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

♻️ Duplicate comments (3)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: ⚠️ Potential issue | 🟠 Major

Call the Wings schema delete before removing the DB record.

Deleting only the DB row can leave orphaned backups on the daemon. Use the schema delete first (or the existing delete service) and then remove the record.

🔧 Suggested fix
                                                         ->each(function ($backup) use ($backupService) {
                                                             $schema = $backupService->get($backup->backupHost->schema);

                                                             // Wings backups that aren't transferred only need to be delete on the panel, wings will cleanup the backup files automatically
                                                             if ($schema instanceof WingsBackupSchema) {
+                                                                $schema->deleteBackup($backup);
                                                                 $backup->delete();
                                                             }
                                                         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, In the loop over $server->backups (the block using
$backupService->get($backup->backupHost->schema) and checking "instanceof
WingsBackupSchema"), call the schema's deletion routine (or the project-wide
backup delete service) to remove the backup from the daemon before calling
$backup->delete() on the DB record; i.e., for WingsBackupSchema invoke its
delete/cleanup method (or use the existing backup deletion service) and only
after that succeeds remove the backup row with $backup->delete() to avoid
leaving orphaned files on the daemon.
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php (2)

24-33: ⚠️ Potential issue | 🟠 Major

Filter S3 client config to avoid invalid options and isolate credentials.

Line 26 passes the entire backup host configuration into S3Client. If the AWS SDK validates options, unknown keys like bucket, key, and secret can trigger InvalidArgumentException at runtime. Build a clean config array and move credentials under credentials.

🛠️ Suggested fix
-    private function createClient(BackupHost $backupHost): S3Client
-    {
-        $config = $backupHost->configuration;
-        $config['version'] = 'latest';
-
-        if (!empty($config['key']) && !empty($config['secret'])) {
-            $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
-        }
-
-        return new S3Client($config);
-    }
+    private function createClient(BackupHost $backupHost): S3Client
+    {
+        $cfg = $backupHost->configuration;
+        $config = [
+            'version' => 'latest',
+            'region' => $cfg['region'] ?? null,
+            'endpoint' => $cfg['endpoint'] ?? null,
+            'use_path_style_endpoint' => $cfg['use_path_style_endpoint'] ?? false,
+        ];
+
+        if (!empty($cfg['key']) && !empty($cfg['secret'])) {
+            $config['credentials'] = [
+                'key' => $cfg['key'],
+                'secret' => $cfg['secret'],
+                'token' => $cfg['token'] ?? null,
+            ];
+        }
+
+        return new S3Client(array_filter($config, fn ($v) => $v !== null));
+    }
AWS SDK for PHP S3Client configuration options: does it reject unknown keys like "bucket", "key", or "secret" at the top level?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 24 -
33, The createClient(BackupHost $backupHost) currently passes the entire
$backupHost->configuration into new S3Client which can include invalid top-level
keys (e.g., bucket, key, secret); instead, build a sanitized $config array
containing only allowed AWS options (at minimum 'version' and 'region' if
present), move credentials into a 'credentials' sub-array (use
Arr::only($configRaw, ['key','secret','token']) to populate it) and then pass
that sanitized $config to new S3Client; update the createClient method to read
$backupHost->configuration into a temp, pick allowed keys, set 'version' =>
'latest', add 'credentials' when keys exist, and then instantiate S3Client with
the cleaned config.

103-151: ⚠️ Potential issue | 🟡 Minor

Guard storage_class access to prevent PHP notices.

Line 115 directly indexes configuration['storage_class']. If the key is missing, PHP emits a notice. Use null coalescing before the check.

🔧 Suggested fix
-        $storageClass = $backup->backupHost->configuration['storage_class'];
-        if (!is_null($storageClass)) {
+        $storageClass = $backup->backupHost->configuration['storage_class'] ?? null;
+        if ($storageClass !== null) {
             $params['StorageClass'] = $storageClass;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 103 -
151, The code in getUploadParts accesses
$backup->backupHost->configuration['storage_class'] directly which can emit a
PHP notice if the key is missing; update the access to use a null-coalescing
lookup (e.g. $backup->backupHost->configuration['storage_class'] ?? null) or
fetch into a variable via $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null before the if-check,
then use that $storageClass for the subsequent if (!is_null($storageClass)) and
when setting $params['StorageClass'] to prevent notices.
🧹 Nitpick comments (1)
app/Models/Node.php (1)

281-284: Add @return generic annotation for consistency with databaseHosts().

databaseHosts() at line 275 was just updated to carry @return BelongsToMany<DatabaseHost, $this>, but the new backupHosts() method is missing the equivalent annotation.

♻️ Proposed fix
+    /** `@return` BelongsToMany<BackupHost, $this> */
     public function backupHosts(): BelongsToMany
     {
         return $this->belongsToMany(BackupHost::class);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Node.php` around lines 281 - 284, Add the same generic `@return`
docblock to the backupHosts relation as used on databaseHosts: update the PHPDoc
for the backupHosts() method to declare `@return BelongsToMany<BackupHost,
$this>` so IDEs and static analyzers get the precise relation types; locate the
backupHosts() method and add the matching annotation referencing BackupHost and
$this.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 56-70: The getDownloadLink(Backup $backup, User $user): string
method currently ignores the $user parameter which trips PHPMD; to fix, either
use $user for audit/logging inside getDownloadLink (e.g., record who requested
the URL) or explicitly mark it as intentionally unused to silence PHPMD by
adding a no-op usage like casting (void)$user; at the top of the getDownloadLink
body; update the PHPDoc if you choose suppression so reviewers know it's
intentional.

---

Duplicate comments:
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 24-33: The createClient(BackupHost $backupHost) currently passes
the entire $backupHost->configuration into new S3Client which can include
invalid top-level keys (e.g., bucket, key, secret); instead, build a sanitized
$config array containing only allowed AWS options (at minimum 'version' and
'region' if present), move credentials into a 'credentials' sub-array (use
Arr::only($configRaw, ['key','secret','token']) to populate it) and then pass
that sanitized $config to new S3Client; update the createClient method to read
$backupHost->configuration into a temp, pick allowed keys, set 'version' =>
'latest', add 'credentials' when keys exist, and then instantiate S3Client with
the cleaned config.
- Around line 103-151: The code in getUploadParts accesses
$backup->backupHost->configuration['storage_class'] directly which can emit a
PHP notice if the key is missing; update the access to use a null-coalescing
lookup (e.g. $backup->backupHost->configuration['storage_class'] ?? null) or
fetch into a variable via $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null before the if-check,
then use that $storageClass for the subsequent if (!is_null($storageClass)) and
when setting $params['StorageClass'] to prevent notices.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: In the loop over $server->backups (the block using
$backupService->get($backup->backupHost->schema) and checking "instanceof
WingsBackupSchema"), call the schema's deletion routine (or the project-wide
backup delete service) to remove the backup from the daemon before calling
$backup->delete() on the DB record; i.e., for WingsBackupSchema invoke its
delete/cleanup method (or use the existing backup deletion service) and only
after that succeeds remove the backup row with $backup->delete() to avoid
leaving orphaned files on the daemon.

---

Nitpick comments:
In `@app/Models/Node.php`:
- Around line 281-284: Add the same generic `@return` docblock to the
backupHosts relation as used on databaseHosts: update the PHPDoc for the
backupHosts() method to declare `@return BelongsToMany<BackupHost, $this>` so
IDEs and static analyzers get the precise relation types; locate the
backupHosts() method and add the matching annotation referencing BackupHost and
$this.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c215e95 and c3b597d.

📒 Files selected for processing (6)
  • app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/ListBackupHosts.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Models/Node.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Filament/Admin/Resources/BackupHosts/Pages/ListBackupHosts.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php

Comment thread app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/pelican-dev/panel/issues/comments/3771874444","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nReplaces disk-based backups with a pluggable BackupHost / BackupAdapter system: adds adapter interfaces, registry and S3/Wings schemas; introduces BackupHost model/resource/migration; rewires services/controllers to delegate to adapters; removes legacy BackupManager/S3Filesystem and related config/UI; updates tests and providers.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Adapter Core** <br> `app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php`, `app/Extensions/BackupAdapter/BackupAdapterService.php`, `app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php`|Adds adapter interface, a registry service for schemas, and an abstract base schema implementing getName.|\n|**Adapter Implementations** <br> `app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`, `app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php`|New S3 and Wings schema implementations handling create/delete, download link generation; S3 implements multipart upload/presigned URLs and client caching.|\n|**BackupHost Domain** <br> `app/Models/BackupHost.php`, `database/migrations/..._create_backup_hosts_table.php`, `database/Factories/BackupHostFactory.php`, `lang/en/admin/backuphost.php`|Adds BackupHost model, migration (backup_hosts + pivot), factory and translations; migration migrates existing backups to a BackupHost.|\n|**Backup Model & Node** <br> `app/Models/Backup.php`, `app/Models/Node.php`, `database/Factories/BackupFactory.php`|Replaces Backup.disk with backup_host_id, adds backupHost relation; Node gets backupHosts relation; factory removes disk fixture.|\n|**Filament Admin UI** <br> `app/Filament/Admin/Resources/BackupHosts/*`, `app/Filament/Admin/Pages/Settings.php`, `app/Filament/Admin/Resources/Servers/Pages/EditServer.php`, `app/Filament/Server/Resources/Backups/BackupResource.php`|Adds BackupHost Filament resource, pages and relation manager; removes APP_BACKUP_DRIVER/S3 UI; server edit and backup UI wired to BackupAdapterService; download link generation consolidated.|\n|**Service Providers & Bootstrap** <br> `app/Providers/Extensions/BackupAdapterServiceProvider.php`, `app/Providers/BackupsServiceProvider.php` (removed), `bootstrap/providers.php`|Registers BackupAdapterService and default schemas; removes old BackupsServiceProvider and updates bootstrap providers.|\n|**Services Refactored** <br> `app/Services/Backups/InitiateBackupService.php`, `app/Services/Backups/DeleteBackupService.php`, `app/Services/Backups/DownloadLinkService.php`|Services now resolve schemas via BackupAdapterService and delegate create/delete/download to adapter schemas; constructors and payloads updated to use BackupHost/schema.|\n|**Controllers Updated** <br> `app/Http/Controllers/Api/Client/Servers/BackupController.php`, `app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php`, `app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`|Controllers inject BackupAdapterService, resolve schemas, validate schema presence, and delegate upload/download/multipart operations to schemas; removed adapter-specific checks.|\n|**Legacy Removal** <br> `app/Extensions/Backups/BackupManager.php`, `app/Extensions/Filesystem/S3Filesystem.php`|Removed legacy BackupManager and S3Filesystem class and their provider binding.|\n|**Daemon Repository** <br> `app/Repositories/Daemon/DaemonBackupRepository.php`|Renamed backup()→create(), removed internal adapter state; daemon payloads now use backupHost->schema.|\n|**Tasks & Transfer Flow** <br> `app/Extensions/Tasks/Schemas/CreateBackupSchema.php`, `app/Services/Servers/TransferServerService.php`, `app/Filament/Admin/Resources/Servers/Pages/EditServer.php`|Small ctor/property rename; transfer flows updated to filter Wings-backed backups via BackupAdapterService and to pass backup UUIDs.|\n|**Config** <br> `config/backups.php`|Removed default driver and disks block; retains presigned/max part/prune/throttles settings only.|\n|**Tests** <br> `tests/Integration/.../Backup/*`, `tests/Integration/Services/Backups/DeleteBackupServiceTest.php`|Tests updated to create and associate BackupHost with backups; S3-specific deletion test removed.|\n|**Policies** <br> `app/Policies/BackupHostPolicy.php`|New policy gating BackupHost actions by linked nodes.|\n|**Enum update** <br> `app/Enums/RolePermissionModels.php`|Added enum case `BackupHost = 'backupHost'`.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant User as User/Client\n    participant Initiator as InitiateBackupService\n    participant AdapterSvc as BackupAdapterService\n    participant Schema as BackupSchema (S3 or Wings)\n    participant DB as Database\n    participant External as External System\n\n    User->>Initiator: initiate backup request\n    Initiator->>DB: create Backup record (backup_host_id)\n    Initiator->>AdapterSvc: get(backupHost.schema)\n    AdapterSvc-->>Initiator: BackupSchema instance\n    Initiator->>Schema: createBackup(backup)\n    Schema->>External: perform storage action (upload/daemon)\n    External-->>Schema: confirm\n    Schema-->>Initiator: completion\n    Initiator-->>User: backup created\n```\n\n```mermaid\nsequenceDiagram\n    participant Client as Client/User\n    participant Controller as BackupController\n    participant AdapterSvc as BackupAdapterService\n    participant Schema as BackupSchema (S3 or Wings)\n    participant External as External System\n\n    Client->>Controller: request download link\n    Controller->>AdapterSvc: get(backup.backupHost.schema)\n    AdapterSvc-->>Controller: BackupSchema instance\n    Controller->>Schema: getDownloadLink(backup, user)\n    alt S3\n        Schema->>External: generate presigned URL\n    else Wings\n        Schema->>External: generate JWT and daemon URL\n    end\n    External-->>Schema: url\n    Schema-->>Controller: download URL\n    Controller-->>Client: return URL\n```\n\n## Possibly related PRs\n\n- pelican-dev/panel#2068 — modifies server transfer flow and backup filtering in a way that overlaps this PR's TransferServerService/EditServer changes.\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 45.98% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                                                                                                                                                          |\n| :------------------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|         Title check        | ✅ Passed | The title 'Backup hosts' is concise and directly reflects the primary change: introducing a new BackupHost model and infrastructure to manage backup configurations.                                                                                                                                                                                 |\n|      Description check     | ✅ Passed | The description mentions closing issue `#1919` and enabling custom adapter registration, which align with the changeset's introduction of BackupAdapterService and adapter-based architecture.                                                                                                                                                         |\n|     Linked Issues check    | ✅ Passed | The PR implements all primary objectives from issue `#1919`: creates BackupHost model with schema/configuration fields [BackupHostResource, BackupHost model], enables multiple backup configurations [BackupAdapterService, S3BackupSchema], allows node assignment [BelongsToMany relation], and preserves Wings (local) option [WingsBackupSchema]. |\n| Out of Scope Changes check | ✅ Passed | All changes align with objectives: BackupAdapter interfaces and implementations support schema-based adapter management; migrations and models support BackupHost; UI resources enable backup host management; service layer refactoring redirects to adapter schemas. No unrelated changes detected.                                                |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=pelican-dev/panel&utm_content=2125)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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= -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

@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: 2

♻️ Duplicate comments (1)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: ⚠️ Potential issue | 🟠 Major

Verify Wings cleanup before deleting backup DB rows.
Line 991–992 deletes only the panel record. If Wings doesn’t auto-prune untransferred backups, this leaves orphaned files. Consider calling deleteBackup() before delete(), or confirm the daemon’s cleanup behavior.

🧩 Suggested fix (if cleanup is not automatic)
-                                                            if ($schema instanceof WingsBackupSchema) {
-                                                                $backup->delete();
-                                                            }
+                                                            if ($schema instanceof WingsBackupSchema) {
+                                                                $schema->deleteBackup($backup);
+                                                                $backup->delete();
+                                                            }
Pelican Wings server transfer: does the daemon automatically delete backups not included in a transfer, or must the panel call the backup delete API?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, The code currently deletes only the DB row for Wings backups (inside the
closure using $backupService->get(...) and checking instanceof
WingsBackupSchema) which may leave orphaned files; update the logic to verify
daemon cleanup and if not guaranteed, call the backup model's remote deletion
method (e.g., deleteBackup() or the appropriate API method on the backup
model/service) before calling $backup->delete(), ensuring you still use the same
WingsBackupSchema check and $backupService->get(...) to locate Wings backups.
🧹 Nitpick comments (1)
app/Filament/Server/Resources/Backups/BackupResource.php (1)

173-173: user() passes a nullable type to DownloadLinkService::handle

user() returns Authenticatable|null, and while the ->authorize() guard ensures the user is authenticated when this callback runs, passing user() without asserting non-null may trip static analysis. The same pattern applies at line 210.

🛡️ Optional null assertion
-->url(fn (DownloadLinkService $downloadLinkService, Backup $backup) => $downloadLinkService->handle($backup, user()), true)
+->url(fn (DownloadLinkService $downloadLinkService, Backup $backup) => $downloadLinkService->handle($backup, user()!), true)

Or, if the project uses PHPDoc assertions:

-$url = $downloadLinkService->handle($backup, user());
+/** `@var` \App\Models\User $user */
+$user = user();
+$url = $downloadLinkService->handle($backup, $user);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Server/Resources/Backups/BackupResource.php` at line 173, The
callback passes user() (nullable) into DownloadLinkService::handle which can
trigger static-analysis errors; capture the result into a local variable, assert
it's non-null (e.g. $authUser = user(); assert($authUser instanceof
\Illuminate\Contracts\Auth\Authenticatable); or throw if null) and then pass
$authUser to DownloadLinkService::handle; apply the same change for the second
occurrence referenced (the other callback around line 210) so both calls use an
asserted non-null $authUser variable before calling handle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php`:
- Around line 123-137: The configuration section always reads the persisted
model schema first (BackupHost->schema) so changing the schema in the live form
doesn't update the configuration UI; update the selection order to prefer the
live form state from Get $get('schema') and fall back to the model's schema only
if the live value is empty. Locate the closure passed to Section::make(...) that
receives (?BackupHost $backupHost, Get $get, BackupAdapterService $service) and
change the logic that sets $schema so it checks $get('schema') first, then
$backupHost->schema as a fallback, then call $service->get($schema) and return
$schema->getConfigurationForm() as before.

In `@app/Services/Servers/TransferServerService.php`:
- Around line 30-45: The backup lookup in the notify method is using
Backup::find($uuid) which looks up by integer PK and returns null for UUID
strings; change the lookup in notify (method name: notify, class:
TransferServerService) to resolve by the uuid column (e.g., query Backup where
'uuid' equals the $uuid and take first()) so $backup is correctly loaded and
WingsBackupSchema checks run; update any related logic that uses $backup
accordingly.

---

Duplicate comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: The code currently deletes only the DB row for Wings
backups (inside the closure using $backupService->get(...) and checking
instanceof WingsBackupSchema) which may leave orphaned files; update the logic
to verify daemon cleanup and if not guaranteed, call the backup model's remote
deletion method (e.g., deleteBackup() or the appropriate API method on the
backup model/service) before calling $backup->delete(), ensuring you still use
the same WingsBackupSchema check and $backupService->get(...) to locate Wings
backups.

---

Nitpick comments:
In `@app/Filament/Server/Resources/Backups/BackupResource.php`:
- Line 173: The callback passes user() (nullable) into
DownloadLinkService::handle which can trigger static-analysis errors; capture
the result into a local variable, assert it's non-null (e.g. $authUser = user();
assert($authUser instanceof \Illuminate\Contracts\Auth\Authenticatable); or
throw if null) and then pass $authUser to DownloadLinkService::handle; apply the
same change for the second occurrence referenced (the other callback around line
210) so both calls use an asserted non-null $authUser variable before calling
handle.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6ba81e and f300259.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/RelationManagers/BackupsRelationManager.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Services/Servers/TransferServerService.php

Comment thread app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
Comment thread app/Services/Servers/TransferServerService.php Outdated

@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: 2

♻️ Duplicate comments (1)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: Confirm Wings cleanup before deleting the DB record.
If Wings does not automatically remove backup archives during transfer, $backup->delete() will orphan storage. Please verify the cleanup behavior; otherwise call $schema->deleteBackup($backup) before deleting the model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, The current loop deletes backup DB records for backups with Wings schema
without ensuring storage cleanup; update the logic in the block handling
$server->backups->whereNotIn('uuid', $selectedBackupUuids)->each(...) to verify
Wings actually removed the archive and, if not, call the schema-level cleanup
method (use $backupService->get($backup->backupHost->schema) to obtain $schema
and then call $schema->deleteBackup($backup) or equivalent) before invoking
$backup->delete(); ensure you still only call $backup->delete() after successful
cleanup or on a confirmed Wings-auto-cleanup path (keep usage of
WingsBackupSchema to branch the behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Services/Servers/TransferServerService.php`:
- Around line 67-68: The handle method's signature allows ?array $backup_uuids
but later passes it into notify() which requires array, causing a TypeError; fix
by making $backup_uuids non-nullable (change signature to array $backup_uuids =
[]) or, inside TransferServerService::handle, coerce null to an empty array
(e.g. $backup_uuids = $backup_uuids ?? []) before calling notify(); update the
same pattern for the other occurrence mentioned (line ~118) to ensure notify()
always receives an array.
- Around line 36-45: The loop is doing an unscoped, per-UUID lookup
(Backup::where('uuid', ...) inside the foreach over $backup_uuids) causing
potential cross-server matches and N+1 queries; replace it by querying all
backups for the transfer server once (e.g. fetch Backup::where('server_id',
$server->id)->whereIn('uuid', $backup_uuids')->get()), then iterate that
collection and call $this->backupService->get(...) to check instanceof
WingsBackupSchema and populate $backups, ensuring you only consider backups
belonging to the target server and avoid repeated DB calls.

---

Duplicate comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: The current loop deletes backup DB records for backups
with Wings schema without ensuring storage cleanup; update the logic in the
block handling $server->backups->whereNotIn('uuid',
$selectedBackupUuids)->each(...) to verify Wings actually removed the archive
and, if not, call the schema-level cleanup method (use
$backupService->get($backup->backupHost->schema) to obtain $schema and then call
$schema->deleteBackup($backup) or equivalent) before invoking $backup->delete();
ensure you still only call $backup->delete() after successful cleanup or on a
confirmed Wings-auto-cleanup path (keep usage of WingsBackupSchema to branch the
behavior).

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6ba81e and 5057d72.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/RelationManagers/BackupsRelationManager.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Services/Servers/TransferServerService.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Filament/Server/Resources/Backups/BackupResource.php

Comment thread app/Services/Servers/TransferServerService.php Outdated
Comment thread app/Services/Servers/TransferServerService.php Outdated

@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.

🧹 Nitpick comments (2)
app/Services/Servers/TransferServerService.php (2)

33-39: Avoid N+1 on backupHost during filtering.
Each $backup->backupHost access can trigger an extra query; eager load the relation before filtering.

♻️ Proposed tweak
-        $backups = Backup::where('server_id', $transfer->server_id)
-            ->whereIn('uuid', $backup_uuids)
-            ->get()
+        $backups = Backup::where('server_id', $transfer->server_id)
+            ->whereIn('uuid', $backup_uuids)
+            ->with('backupHost')
+            ->get()
             ->filter(fn (Backup $backup) => $this->backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
             ->pluck('uuid')
             ->all();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/TransferServerService.php` around lines 33 - 39, The
code filters backups by accessing $backup->backupHost inside a collection
filter, causing N+1 queries; eager load the backupHost relation before
retrieving/filtering to prevent extra queries. Update the Backup query used to
build $backups (the chain that calls Backup::where(...)->whereIn(...)) to
include the backupHost relation (e.g., via with('backupHost')) so the subsequent
->filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema) runs in-memory without triggering additional queries.

30-30: Track the TODO for persisting backup UUIDs.
Line 30 notes missing persistence; consider opening a follow-up issue so selected backups aren’t lost if transfers need retry/audit later.

Would you like me to draft an issue or propose a migration sketch?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/TransferServerService.php` at line 30, The comment flags
a TODO to persist backup UUIDs for transfers; open a follow-up issue and
implement it by adding a new column (e.g. backup_uuids as JSON or text) to the
ServerTransfer model via a migration, add the corresponding $casts or attribute
on ServerTransfer to treat it as an array, update the TransferServerService
logic where the docblock and TODO appear to save the provided backup_uuids into
the ServerTransfer record when creating/updating transfers, and ensure any
validation/serialization is handled consistently when reading/writing that
field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Services/Servers/TransferServerService.php`:
- Around line 33-39: The code filters backups by accessing $backup->backupHost
inside a collection filter, causing N+1 queries; eager load the backupHost
relation before retrieving/filtering to prevent extra queries. Update the Backup
query used to build $backups (the chain that calls
Backup::where(...)->whereIn(...)) to include the backupHost relation (e.g., via
with('backupHost')) so the subsequent ->filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema) runs in-memory without triggering additional queries.
- Line 30: The comment flags a TODO to persist backup UUIDs for transfers; open
a follow-up issue and implement it by adding a new column (e.g. backup_uuids as
JSON or text) to the ServerTransfer model via a migration, add the corresponding
$casts or attribute on ServerTransfer to treat it as an array, update the
TransferServerService logic where the docblock and TODO appear to save the
provided backup_uuids into the ServerTransfer record when creating/updating
transfers, and ensure any validation/serialization is handled consistently when
reading/writing that field.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5057d72 and cdd69c2.

📒 Files selected for processing (1)
  • app/Services/Servers/TransferServerService.php

@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/Models/Backup.php (1)

17-60: ⚠️ Potential issue | 🟡 Minor

Remove stale whereDisk() PHPDoc annotation.

The docblock still includes whereDisk($value) on Line 48, but disk is no longer a Backup attribute. Please replace it with whereBackupHostId($value) to keep IDE/static-analysis hints accurate.

🧩 Suggested docblock update
- * `@method` static BackupQueryBuilder<static>|Backup whereDisk($value)
+ * `@method` static BackupQueryBuilder<static>|Backup whereBackupHostId($value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Backup.php` around lines 17 - 60, Update the Backup model docblock
to remove the stale whereDisk($value) annotation and replace it with
whereBackupHostId($value); locate the class docblock (the `@method` annotations in
Backup) and swap the whereDisk entry for whereBackupHostId so IDEs and static
analysis reflect the current Backup attributes.
♻️ Duplicate comments (1)
app/Services/Servers/TransferServerService.php (1)

38-40: ⚠️ Potential issue | 🟠 Major

Add ->values() to reindex keys before JSON serialization.

After filter() on line 39, numeric keys become sparse (gappy). When serialized to JSON, sparse numeric keys will encode backups as an object instead of an array. Add ->values() before ->all() to reset keys to consecutive integers and ensure correct JSON serialization.

Patch
         $backups = Backup::where('server_id', $transfer->server_id)
             ->whereIn('uuid', $backup_uuids)
             ->with('backupHost')
             ->get()
             ->filter(fn (Backup $backup) => $this->backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
             ->pluck('uuid')
+            ->values()
             ->all();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/TransferServerService.php` around lines 38 - 40, The
collection produced by filtering backups becomes sparse and will JSON-serialize
as an object; update the chain in TransferServerService (the pipeline using
filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema)->pluck('uuid')->all()) to reindex keys by inserting
->values() before ->all() so the resulting array of UUIDs has consecutive
numeric keys and serializes as a JSON array.
🧹 Nitpick comments (2)
app/Models/Backup.php (1)

87-87: Tighten FK validation to integer.

For an id foreign key, prefer integer over numeric so only integer values are accepted before exists runs.

✅ Suggested validation tweak
-        'backup_host_id' => ['required', 'numeric', 'exists:backup_hosts,id'],
+        'backup_host_id' => ['required', 'integer', 'exists:backup_hosts,id'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Backup.php` at line 87, Update the validation rule for the
backup_host_id field in Backup.php to use integer rather than numeric: locate
the validation array (the 'backup_host_id' => ['required', 'numeric',
'exists:backup_hosts,id'] entry—commonly found in the rules() method or $rules
property of the Backup model) and replace 'numeric' with 'integer' so the rule
becomes ['required','integer','exists:backup_hosts,id'].
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-989: Eager-load backupHost in the cleanup loop to avoid N+1 queries.

Lines 985-989 iterate over backups and access $backup->backupHost->schema without eager-loading the backupHost relationship. This causes a separate database query for each backup.

Suggested refactor
-                                                    $server->backups
+                                                    $server->backups()
+                                                        ->with('backupHost')
                                                         ->whereNotIn('uuid', $selectedBackupUuids)
+                                                        ->get()
                                                         ->each(function ($backup) use ($backupService) {
                                                             $schema = $backupService->get($backup->backupHost->schema);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
989, The loop iterates $server->backups and accesses $backup->backupHost->schema
causing N+1 queries; ensure the backupHost relation is eager-loaded before
iterating (e.g., call loadMissing('backups.backupHost') on $server or use
$server->backups()->with('backupHost')->whereNotIn(...)->get()) so the closure
that calls $backupService->get($backup->backupHost->schema) does not trigger
per-backup queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/Models/Backup.php`:
- Around line 17-60: Update the Backup model docblock to remove the stale
whereDisk($value) annotation and replace it with whereBackupHostId($value);
locate the class docblock (the `@method` annotations in Backup) and swap the
whereDisk entry for whereBackupHostId so IDEs and static analysis reflect the
current Backup attributes.

---

Duplicate comments:
In `@app/Services/Servers/TransferServerService.php`:
- Around line 38-40: The collection produced by filtering backups becomes sparse
and will JSON-serialize as an object; update the chain in TransferServerService
(the pipeline using filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema)->pluck('uuid')->all()) to reindex keys by inserting
->values() before ->all() so the resulting array of UUIDs has consecutive
numeric keys and serializes as a JSON array.

---

Nitpick comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-989: The loop iterates $server->backups and accesses
$backup->backupHost->schema causing N+1 queries; ensure the backupHost relation
is eager-loaded before iterating (e.g., call loadMissing('backups.backupHost')
on $server or use
$server->backups()->with('backupHost')->whereNotIn(...)->get()) so the closure
that calls $backupService->get($backup->backupHost->schema) does not trigger
per-backup queries.

In `@app/Models/Backup.php`:
- Line 87: Update the validation rule for the backup_host_id field in Backup.php
to use integer rather than numeric: locate the validation array (the
'backup_host_id' => ['required', 'numeric', 'exists:backup_hosts,id']
entry—commonly found in the rules() method or $rules property of the Backup
model) and replace 'numeric' with 'integer' so the rule becomes
['required','integer','exists:backup_hosts,id'].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5c6fdb9f-1306-48da-8e7d-5b777e84cbf7

📥 Commits

Reviewing files that changed from the base of the PR and between cdd69c2 and 7a8aaad.

📒 Files selected for processing (7)
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Models/Backup.php
  • app/Models/Node.php
  • app/Providers/AppServiceProvider.php
  • app/Services/Servers/TransferServerService.php
  • bootstrap/providers.php
  • tests/Integration/Api/Client/Server/Backup/DeleteBackupTest.php
💤 Files with no reviewable changes (1)
  • app/Providers/AppServiceProvider.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Models/Node.php
  • tests/Integration/Api/Client/Server/Backup/DeleteBackupTest.php

@lancepioch lancepioch marked this pull request as draft April 22, 2026 02:06
@Boy132 Boy132 marked this pull request as ready for review April 28, 2026 12:05
@Boy132 Boy132 linked an issue Apr 28, 2026 that may be closed by this pull request
3 tasks

@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

🧹 Nitpick comments (3)
app/Services/Servers/ServerDeletionService.php (1)

88-94: Consider inverting the conditional to eliminate empty block.

The empty if block with a comment explaining why nothing happens is harder to scan than a simple negated check.

♻️ Suggested simplification
-                    if ($schema) {
-                        if ($schema instanceof WingsBackupSchema) {
-                            // Local wings backups are already deleted by the daemon
-                        } else {
-                            $schema->deleteBackup($backup);
-                        }
+                    if ($schema && !$schema instanceof WingsBackupSchema) {
+                        $schema->deleteBackup($backup);
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/ServerDeletionService.php` around lines 88 - 94, Invert
the conditional to remove the empty branch: instead of checking "if ($schema) {
if ($schema instanceof WingsBackupSchema) { /* empty */ } else {
$schema->deleteBackup($backup); } }", change it to "if ($schema && ! ($schema
instanceof WingsBackupSchema)) { $schema->deleteBackup($backup); }" so the empty
comment block is eliminated and the logic remains the same; update the code
around the ServerDeletionService handling of $schema and the call to
deleteBackup($backup) accordingly.
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

963-969: Extract repeated Wings backup filter to reduce duplication.

The same filter expression $server->backups->filter(fn ($backup) => $backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema) appears three times. Consider extracting to a local closure or helper.

♻️ Suggested refactor
     Grid::make()
         ->columnSpanFull()
         ->schema(fn (BackupAdapterService $backupService) => [
+            // Define filter once
+            $wingsBackups = fn (Server $server) => $server->backups->filter(
+                fn ($backup) => $backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema
+            );
             CheckboxList::make('backups')
                 ->label(trans('admin/server.backups'))
                 ->bulkToggleable()
-                ->options(fn (Server $server) => $server->backups->filter(fn ($backup) => $backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)->mapWithKeys(fn ($backup) => [$backup->uuid => $backup->name]))
-                ->columns(fn (Server $record) => (int) ceil($record->backups->filter(fn ($backup) => $backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)->count() / 4)),
+                ->options(fn (Server $server) => $wingsBackups($server)->mapWithKeys(fn ($backup) => [$backup->uuid => $backup->name]))
+                ->columns(fn (Server $record) => (int) ceil($wingsBackups($record)->count() / 4)),
             Text::make('backup_helper')
                 ->columnSpanFull()
                 ->content(trans('admin/server.warning_backups')),
         ])
-        ->hidden(fn (Server $server, BackupAdapterService $backupService) => $server->backups->filter(fn ($backup) => $backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)->count() === 0),
+        ->hidden(fn (Server $server, BackupAdapterService $backupService) => $wingsBackups($server)->isEmpty()),

Note: Exact implementation may vary based on Filament's closure scoping rules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 963 -
969, Extract the repeated filter into a single reusable variable/closure inside
the EditServer page before it's used in options(), columns(), and hidden();
specifically create e.g. $wingsBackups = fn(Server $s, BackupAdapterService
$backupService) => $s->backups->filter(fn($backup) =>
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
(or evaluate it once for the current $server and $backupService) and replace the
three occurrences of $server->backups->filter(fn ($backup) =>
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
with calls to that variable/closure when building options(), columns(), and the
hidden() predicate so the logic using Server, BackupAdapterService,
backupHost->schema and WingsBackupSchema is centralized.
app/Http/Controllers/Api/Client/Servers/BackupController.php (1)

196-201: Redundant schema validation.

The schema check at lines 196-199 is duplicated by DownloadLinkService::handle() (see app/Services/Backups/DownloadLinkService.php:22-25), which also validates the schema and throws if not found. Consider removing the pre-check here and letting the service handle validation, or keep it for the more descriptive error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/Client/Servers/BackupController.php` around lines
196 - 201, The controller currently performs a redundant schema validation by
calling $this->backupService->get($backup->backupHost->schema) and throwing a
BadRequestHttpException before delegating to
$this->downloadLinkService->handle($backup, $request->user()), but
DownloadLinkService::handle() already performs the same check; remove the
pre-check in BackupController::download (i.e., the $schema lookup and the
subsequent throw) so the service handles validation centrally, or if you prefer
a clearer error message keep the pre-check and remove/skip the duplicate check
in DownloadLinkService::handle()—choose one location (either BackupController's
$schema check or DownloadLinkService::handle()) and delete the other to
eliminate duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 859-868: The current loop over $server->backups only deletes Wings
backups and skips S3 backups; update the logic in the closure passed to each()
(the block referencing $server->backups, $selectedBackupUuids and
$backupService) to invoke the schema's deleteBackup method for non-Wings schemas
(or unconditionally call $schema->deleteBackup($backup)) and then remove the
database record via $backup->delete(); reference the $backupService->get(...)
call, the WingsBackupSchema and S3BackupSchema implementations, and the existing
DeleteBackupService pattern to ensure both remote cleanup and $backup->delete()
are performed for all backup types.

---

Nitpick comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 963-969: Extract the repeated filter into a single reusable
variable/closure inside the EditServer page before it's used in options(),
columns(), and hidden(); specifically create e.g. $wingsBackups = fn(Server $s,
BackupAdapterService $backupService) => $s->backups->filter(fn($backup) =>
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
(or evaluate it once for the current $server and $backupService) and replace the
three occurrences of $server->backups->filter(fn ($backup) =>
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
with calls to that variable/closure when building options(), columns(), and the
hidden() predicate so the logic using Server, BackupAdapterService,
backupHost->schema and WingsBackupSchema is centralized.

In `@app/Http/Controllers/Api/Client/Servers/BackupController.php`:
- Around line 196-201: The controller currently performs a redundant schema
validation by calling $this->backupService->get($backup->backupHost->schema) and
throwing a BadRequestHttpException before delegating to
$this->downloadLinkService->handle($backup, $request->user()), but
DownloadLinkService::handle() already performs the same check; remove the
pre-check in BackupController::download (i.e., the $schema lookup and the
subsequent throw) so the service handles validation centrally, or if you prefer
a clearer error message keep the pre-check and remove/skip the duplicate check
in DownloadLinkService::handle()—choose one location (either BackupController's
$schema check or DownloadLinkService::handle()) and delete the other to
eliminate duplication.

In `@app/Services/Servers/ServerDeletionService.php`:
- Around line 88-94: Invert the conditional to remove the empty branch: instead
of checking "if ($schema) { if ($schema instanceof WingsBackupSchema) { /* empty
*/ } else { $schema->deleteBackup($backup); } }", change it to "if ($schema && !
($schema instanceof WingsBackupSchema)) { $schema->deleteBackup($backup); }" so
the empty comment block is eliminated and the logic remains the same; update the
code around the ServerDeletionService handling of $schema and the call to
deleteBackup($backup) accordingly.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f320b880-18ae-4f58-92b0-86338ccc371c

📥 Commits

Reviewing files that changed from the base of the PR and between 7a8aaad and 0a04c76.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Http/Controllers/Api/Client/Servers/BackupController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
  • app/Services/Servers/ServerDeletionService.php

Comment thread app/Filament/Admin/Resources/Servers/Pages/EditServer.php
@Boy132

Boy132 commented May 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@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: 9

Caution

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

⚠️ Outside diff range comments (1)
app/Repositories/Daemon/DaemonBackupRepository.php (1)

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

Add null-safety check for backupHost in daemon payloads.

Lines 20 and 36 access $backup->backupHost->schema without null-checking the relationship. While callers may have validated this, the repository should be defensive to prevent null-pointer errors when constructing daemon API payloads.

🛡️ Proposed fix for both methods
     public function create(Backup $backup): Response
     {
+        if (!$backup->backupHost) {
+            throw new \RuntimeException('Backup is missing associated backup host.');
+        }
+
         return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/backup",
             [
                 'adapter' => $backup->backupHost->schema,

Apply similar guard in restore():

     public function restore(Backup $backup, ?string $url = null, bool $truncate = false): Response
     {
+        if (!$backup->backupHost) {
+            throw new \RuntimeException('Backup is missing associated backup host.');
+        }
+
         return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/backup/$backup->uuid/restore",
🤖 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/Repositories/Daemon/DaemonBackupRepository.php` around lines 16 - 25, The
create(Backup $backup): Response (and similarly restore(Backup $backup):
Response) should defensively handle a missing backupHost before reading
->schema; update both methods to check if $backup->backupHost is null and supply
a safe fallback (e.g. empty string or a configured default schema) when building
the daemon payload, then pass that safe value to getHttpClient()->post so no
null-property access occurs.
♻️ Duplicate comments (4)
app/Services/Backups/DeleteBackupService.php (1)

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

Null-safety check for backupHost is still needed.

Line 34 accesses $backup->backupHost->schema without verifying the relationship is non-null, which can cause a fatal error if backupHost is somehow null.

🛡️ Proposed fix
+        if (!$backup->backupHost) {
+            throw new Exception('Backup has no associated backup host.');
+        }
+
         $schema = $this->backupService->get($backup->backupHost->schema);
         if (!$schema) {
             throw new Exception('Backup has unknown backup adapter.');
🤖 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/Services/Backups/DeleteBackupService.php` at line 34, The line in
DeleteBackupService that calls
$this->backupService->get($backup->backupHost->schema) needs a null-safety guard
for $backup->backupHost; update the DeleteBackupService (the method using
$backup) to verify $backup->backupHost is not null before accessing ->schema
(e.g., if null, throw a clear exception or return early), then pass the
validated schema to $this->backupService->get; reference $backup, backupHost,
and $this->backupService->get when making the change.
database/migrations/2026_01_16_081858_create_backup_hosts_table.php (1)

52-52: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Environment variable mismatch: prefix uses bucket variable.

Line 52 uses AWS_BACKUPS_BUCKET for the prefix configuration key, which appears to be a copy-paste error from line 51. The prefix should read from a dedicated environment variable.

🐛 Proposed fix
-                'prefix' => env('AWS_BACKUPS_BUCKET', ''),
+                'prefix' => env('AWS_BACKUPS_PREFIX', ''),
🤖 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 `@database/migrations/2026_01_16_081858_create_backup_hosts_table.php` at line
52, The 'prefix' config key is reading the wrong env var
(env('AWS_BACKUPS_BUCKET', ''))—replace that copy-paste bug so 'prefix' uses the
dedicated environment variable (e.g., env('AWS_BACKUPS_PREFIX', '')) instead of
AWS_BACKUPS_BUCKET; update the 'prefix' entry in the migration (the array key
'prefix') to call the correct env variable name.
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php (2)

115-118: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard storage_class before reading it.

storage_class is optional here, so direct array access can emit notices on hosts that don't define it. Use ?? null before the null check.

Suggested fix
-        $storageClass = $backup->backupHost->configuration['storage_class'];
-        if (!is_null($storageClass)) {
+        $storageClass = $backup->backupHost->configuration['storage_class'] ?? null;
+        if ($storageClass !== null) {
             $params['StorageClass'] = $storageClass;
         }
🤖 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/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 115 -
118, The code reads $backup->backupHost->configuration['storage_class'] directly
which can trigger notices when the key is missing; change the access to use the
null-coalescing operator (e.g., assign $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null) and then keep the
existing null check before setting $params['StorageClass'] so missing keys are
safely handled.

24-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter S3Client configuration to only accept supported constructor options.

The createClient() method passes the entire $backupHost->configuration array to new S3Client($config), but this configuration includes fields like bucket and potentially storage_class that are not valid S3Client constructor options—they should only be used in per-operation parameters. The AWS SDK for PHP validates constructor options and will reject unknown keys with an InvalidArgumentException, breaking all S3 operations at client instantiation. Build the client config from only the supported keys: region, endpoint, and use_path_style_endpoint.

Suggested fix
     private function createClient(BackupHost $backupHost): S3Client
     {
-        $config = $backupHost->configuration;
+        $hostConfig = $backupHost->configuration;
+        $config = Arr::only($hostConfig, ['region', 'endpoint', 'use_path_style_endpoint']);
         $config['version'] = 'latest';
 
-        if (!empty($config['key']) && !empty($config['secret'])) {
-            $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
+        if (!empty($hostConfig['key']) && !empty($hostConfig['secret'])) {
+            $config['credentials'] = Arr::only($hostConfig, ['key', 'secret', 'token']);
         }
 
         return new S3Client($config);
     }
🤖 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/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 24 -
33, The createClient method currently forwards the entire
BackupHost->configuration to new S3Client which can include invalid keys (e.g.,
bucket, storage_class); update createClient(BackupHost $backupHost) to build a
sanitized $clientConfig array containing only supported S3Client constructor
options: 'region', 'endpoint', 'use_path_style_endpoint' and 'version' (keep
'version' => 'latest'), and, if credentials exist in BackupHost->configuration,
set 'credentials' to Arr::only($backupHost->configuration, ['key', 'secret',
'token']) before calling new S3Client($clientConfig); reference createClient,
BackupHost and S3Client when making the change.
🧹 Nitpick comments (3)
app/Models/Node.php (1)

319-322: ⚡ Quick win

Add return type annotation for consistency.

The backupHosts() method is missing a @return PHPDoc annotation, while the similar databaseHosts() method (line 313) has one. Adding it improves IDE support and documentation consistency.

📝 Suggested documentation improvement
+/** `@return` BelongsToMany<BackupHost, $this> */
 public function backupHosts(): BelongsToMany
 {
     return $this->belongsToMany(BackupHost::class);
 }
🤖 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/Models/Node.php` around lines 319 - 322, Add a PHPDoc `@return` annotation
to the backupHosts() method in the Node class to match the existing
documentation style used by databaseHosts(); specifically, add a line like
"@return \Illuminate\Database\Eloquent\Relations\BelongsToMany" above the
backupHosts() method so IDEs and docs have the same return-type info as
databaseHosts().
database/Factories/BackupHostFactory.php (1)

23-25: ⚡ Quick win

Consider making the schema configurable for testing flexibility.

The factory hard-codes schema => 'wings', which may limit test scenarios. Consider adding a factory state method to support creating S3 backup hosts:

♻️ Suggested enhancement
 public function definition(): array
 {
     return [
         'name' => $this->faker->colorName(),
         'schema' => 'wings',
         'configuration' => null,
     ];
 }
+
+public function s3(): static
+{
+    return $this->state(fn (array $attributes) => [
+        'schema' => 's3',
+        'configuration' => [
+            'bucket' => $this->faker->slug(),
+            'region' => 'us-east-1',
+        ],
+    ]);
+}
🤖 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 `@database/Factories/BackupHostFactory.php` around lines 23 - 25, The factory
currently hard-codes 'schema' => 'wings' in BackupHostFactory which reduces test
flexibility; add a state method (e.g., schema(string $schema) or
s3State()/forS3()) on the BackupHostFactory class to allow overriding the schema
when creating models in tests, update the definition used in the factory (Schema
default remains 'wings') and document usage so tests can call
BackupHostFactory::new()->schema('other')->create() or
::new()->s3State()->create() to produce hosts with different schemas.
app/Extensions/BackupAdapter/BackupAdapterService.php (1)

21-28: ⚡ Quick win

Silent duplicate registration may hide configuration errors.

The register() method silently ignores attempts to register a schema with a duplicate ID. While this prevents accidental overrides, it could mask configuration bugs where two schemas accidentally use the same ID.

Consider logging a warning when a duplicate registration is attempted:

🔍 Suggested improvement
 public function register(BackupAdapterSchemaInterface $schema): void
 {
     if (array_key_exists($schema->getId(), $this->schemas)) {
+        logger()->warning('Attempted to register duplicate backup adapter schema', [
+            'id' => $schema->getId(),
+            'existing' => get_class($this->schemas[$schema->getId()]),
+            'new' => get_class($schema),
+        ]);
         return;
     }

     $this->schemas[$schema->getId()] = $schema;
 }
🤖 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/Extensions/BackupAdapter/BackupAdapterService.php` around lines 21 - 28,
The register() method currently returns silently on duplicate IDs; change it to
emit a warning when array_key_exists($schema->getId(), $this->schemas) is true
so configuration issues are visible. Update
BackupAdapterService::register(BackupAdapterSchemaInterface $schema) to log a
warning that includes the duplicate ID ($schema->getId()) and the schema class
(get_class($schema)) before returning; prefer calling an injected logger (e.g.,
$this->logger->warning(...), ensure a Psr\Log\LoggerInterface $logger is added
to the service constructor and stored as $this->logger), and if adding a logger
is not possible use trigger_error/ error_log with the same message; then return
without overwriting the existing entry.
🤖 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/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php`:
- Around line 57-62: In getConfigurationForm() the TextEntry is being
constructed with a display string instead of a field key; change the
TextEntry::make(...) call to provide a field name (e.g. TextEntry::make('info')
or 'note') and set its displayed text via
->state(trans('admin/backuphost.no_configuration')) (or use a static text
component if available) so the form component follows Filament's TextEntry
pattern and shows the translated message correctly.

In `@app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php`:
- Line 122: The chain in the relationship call (->relationship('nodes', 'name',
fn (Builder $query) => $query->whereIn('nodes.id',
user()?->accessibleNodes()->pluck('id')))) can fatal when user() is null because
the final ->pluck('id') is called on null; change the expression to guard the
final call and default to an empty array (e.g., use the null-safe operator
before pluck and coalesce to []), so update the anonymous function used in the
relationship to call user()?->accessibleNodes()?->pluck('id') ?? [] instead of
the current chain.

In `@app/Http/Controllers/Api/Client/Servers/BackupController.php`:
- Line 196: The code in BackupController uses $backup->backupHost->schema
without guarding against a null backupHost; update the method in
BackupController to check that $backup->backupHost is not null before calling
$this->backupService->get($backup->backupHost->schema), and handle the null case
(return a suitable error/abort response or throw an exception) so you don't
dereference null; locate the occurrence of $this->backupService->get(...) and
add a defensive check around $backup->backupHost (or use optional() / null
coalescing) and ensure downstream logic handles the absence of schema.

In `@app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php`:
- Around line 39-41: The code currently uses empty($size) which allows
non-positive integers like -1; change the validation in
BackupRemoteUploadController to require a strictly positive integer for $size
(e.g., cast/parse $size from $request->query and check $size > 0) and throw the
existing BadRequestHttpException when $size is not > 0 so you never pass invalid
sizes into getUploadParts().

In `@app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`:
- Around line 75-77: The code currently proceeds when
$this->backupService->get($model->backupHost->schema) returns null or an
unsupported schema, causing the backup to be marked completed without adapter
finalization; change the logic in BackupStatusController so after calling
$this->backupService->get(...) you explicitly check for null or unsupported
types and abort the request (return an error response / throw an exception)
instead of continuing; specifically, ensure you only call
S3BackupSchema::completeMultipartUpload($model, $successful,
$request->input('parts')) when the returned $schema is an instance of
S3BackupSchema, and if $schema is null or not an expected adapter, do not mark
the backup completed or emit BackupCompleted—log the mismatch and return a
non-success response so the caller knows adapter finalization failed.

In `@app/Models/Backup.php`:
- Line 87: The validation rule for the foreign key 'backup_host_id' currently
uses 'numeric' which allows non-integer formats; update the rule for
'backup_host_id' to use 'integer' instead of 'numeric' (in the validation rules
array where 'backup_host_id' => ['required', 'numeric',
'exists:backup_hosts,id'] is defined) so only whole IDs are accepted.

In `@app/Models/BackupHost.php`:
- Around line 40-43: The schema field in BackupHost::$validationRules must be
constrained to only registered adapter IDs; update the validation for 'schema'
(on class BackupHost, property $validationRules) to include an allowlist check
using the current registry of adapter IDs (e.g., Rule::in(...) or a custom
validation rule/closure that calls the adapter registry/service to fetch valid
IDs) so that writes fail if the schema value is not one of the registered
adapters; ensure the adapter list is retrieved from the canonical
registry/provider used elsewhere in the codebase rather than a hardcoded array.

In `@app/Services/Servers/ServerDeletionService.php`:
- Line 86: In ServerDeletionService, the code dereferences
$backup->backupHost->schema without a null check; update the logic around the
call to $this->backupService->get($backup->backupHost->schema) to first verify
$backup->backupHost exists (e.g. if ($backup->backupHost === null) { /* log or
skip this backup */ continue; }) and only call $this->backupService->get(...)
when backupHost is non-null; ensure you handle the null case consistently (skip
the backup or handle error) and adjust any surrounding control flow accordingly.

In `@app/Services/Servers/TransferServerService.php`:
- Around line 61-63: The handle method's signature incorrectly allows null for
$additional_allocations while defaulting it to [] and immediately calling
array_map on it; remove the nullable hint so change the parameter from ?array
$additional_allocations to array $additional_allocations in the
TransferServerService::handle method, and update any related docblock/type
annotations and callers if necessary to ensure they never pass null (so
array_map(intval(...), $additional_allocations) is safe).

---

Outside diff comments:
In `@app/Repositories/Daemon/DaemonBackupRepository.php`:
- Around line 16-25: The create(Backup $backup): Response (and similarly
restore(Backup $backup): Response) should defensively handle a missing
backupHost before reading ->schema; update both methods to check if
$backup->backupHost is null and supply a safe fallback (e.g. empty string or a
configured default schema) when building the daemon payload, then pass that safe
value to getHttpClient()->post so no null-property access occurs.

---

Duplicate comments:
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 115-118: The code reads
$backup->backupHost->configuration['storage_class'] directly which can trigger
notices when the key is missing; change the access to use the null-coalescing
operator (e.g., assign $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null) and then keep the
existing null check before setting $params['StorageClass'] so missing keys are
safely handled.
- Around line 24-33: The createClient method currently forwards the entire
BackupHost->configuration to new S3Client which can include invalid keys (e.g.,
bucket, storage_class); update createClient(BackupHost $backupHost) to build a
sanitized $clientConfig array containing only supported S3Client constructor
options: 'region', 'endpoint', 'use_path_style_endpoint' and 'version' (keep
'version' => 'latest'), and, if credentials exist in BackupHost->configuration,
set 'credentials' to Arr::only($backupHost->configuration, ['key', 'secret',
'token']) before calling new S3Client($clientConfig); reference createClient,
BackupHost and S3Client when making the change.

In `@app/Services/Backups/DeleteBackupService.php`:
- Line 34: The line in DeleteBackupService that calls
$this->backupService->get($backup->backupHost->schema) needs a null-safety guard
for $backup->backupHost; update the DeleteBackupService (the method using
$backup) to verify $backup->backupHost is not null before accessing ->schema
(e.g., if null, throw a clear exception or return early), then pass the
validated schema to $this->backupService->get; reference $backup, backupHost,
and $this->backupService->get when making the change.

In `@database/migrations/2026_01_16_081858_create_backup_hosts_table.php`:
- Line 52: The 'prefix' config key is reading the wrong env var
(env('AWS_BACKUPS_BUCKET', ''))—replace that copy-paste bug so 'prefix' uses the
dedicated environment variable (e.g., env('AWS_BACKUPS_PREFIX', '')) instead of
AWS_BACKUPS_BUCKET; update the 'prefix' entry in the migration (the array key
'prefix') to call the correct env variable name.

---

Nitpick comments:
In `@app/Extensions/BackupAdapter/BackupAdapterService.php`:
- Around line 21-28: The register() method currently returns silently on
duplicate IDs; change it to emit a warning when
array_key_exists($schema->getId(), $this->schemas) is true so configuration
issues are visible. Update
BackupAdapterService::register(BackupAdapterSchemaInterface $schema) to log a
warning that includes the duplicate ID ($schema->getId()) and the schema class
(get_class($schema)) before returning; prefer calling an injected logger (e.g.,
$this->logger->warning(...), ensure a Psr\Log\LoggerInterface $logger is added
to the service constructor and stored as $this->logger), and if adding a logger
is not possible use trigger_error/ error_log with the same message; then return
without overwriting the existing entry.

In `@app/Models/Node.php`:
- Around line 319-322: Add a PHPDoc `@return` annotation to the backupHosts()
method in the Node class to match the existing documentation style used by
databaseHosts(); specifically, add a line like "@return
\Illuminate\Database\Eloquent\Relations\BelongsToMany" above the backupHosts()
method so IDEs and docs have the same return-type info as databaseHosts().

In `@database/Factories/BackupHostFactory.php`:
- Around line 23-25: The factory currently hard-codes 'schema' => 'wings' in
BackupHostFactory which reduces test flexibility; add a state method (e.g.,
schema(string $schema) or s3State()/forS3()) on the BackupHostFactory class to
allow overriding the schema when creating models in tests, update the definition
used in the factory (Schema default remains 'wings') and document usage so tests
can call BackupHostFactory::new()->schema('other')->create() or
::new()->s3State()->create() to produce hosts with different schemas.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f0dded3e-d0ca-409e-a68c-40b752a36ecc

📥 Commits

Reviewing files that changed from the base of the PR and between 38620a9 and 8a2397b.

📒 Files selected for processing (43)
  • app/Enums/RolePermissionModels.php
  • app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php
  • app/Extensions/BackupAdapter/BackupAdapterService.php
  • app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php
  • app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
  • app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php
  • app/Extensions/Backups/BackupManager.php
  • app/Extensions/Filesystem/S3Filesystem.php
  • app/Extensions/Tasks/Schemas/CreateBackupSchema.php
  • app/Filament/Admin/Pages/Settings.php
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/CreateBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/ListBackupHosts.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/ViewBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/RelationManagers/BackupsRelationManager.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Http/Controllers/Api/Client/Servers/BackupController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
  • app/Models/Backup.php
  • app/Models/BackupHost.php
  • app/Models/Node.php
  • app/Policies/BackupHostPolicy.php
  • app/Providers/AppServiceProvider.php
  • app/Providers/BackupsServiceProvider.php
  • app/Providers/Extensions/BackupAdapterServiceProvider.php
  • app/Repositories/Daemon/DaemonBackupRepository.php
  • app/Services/Backups/DeleteBackupService.php
  • app/Services/Backups/DownloadLinkService.php
  • app/Services/Backups/InitiateBackupService.php
  • app/Services/Servers/ServerDeletionService.php
  • app/Services/Servers/TransferServerService.php
  • bootstrap/providers.php
  • config/backups.php
  • database/Factories/BackupFactory.php
  • database/Factories/BackupHostFactory.php
  • database/migrations/2026_01_16_081858_create_backup_hosts_table.php
  • lang/en/admin/backuphost.php
  • tests/Integration/Api/Client/Server/Backup/BackupAuthorizationTest.php
  • tests/Integration/Api/Client/Server/Backup/DeleteBackupTest.php
  • tests/Integration/Services/Backups/DeleteBackupServiceTest.php
💤 Files with no reviewable changes (7)
  • app/Providers/BackupsServiceProvider.php
  • app/Providers/AppServiceProvider.php
  • config/backups.php
  • app/Extensions/Backups/BackupManager.php
  • app/Extensions/Filesystem/S3Filesystem.php
  • database/Factories/BackupFactory.php
  • app/Filament/Admin/Pages/Settings.php

Comment on lines +57 to +62
/** @return Component[] */
public function getConfigurationForm(): array
{
return [
TextEntry::make(trans('admin/backuphost.no_configuration')),
];

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file in question
head -n 75 app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php

Repository: pelican-dev/panel

Length of output: 2109


🏁 Script executed:

# Search for TextEntry usage in the codebase to understand the pattern
rg "TextEntry::make" -B 2 -A 2

Repository: pelican-dev/panel

Length of output: 40819


🏁 Script executed:

# Check Filament imports to understand which package/version is in use
rg "use.*TextEntry|use.*Filament" app/Extensions/BackupAdapter/ -A 1

Repository: pelican-dev/panel

Length of output: 1021


🌐 Web query:

Filament TextEntry::make() API documentation correct usage

💡 Result:

In Filament infolists, TextEntry::make is used correctly as a static constructor that takes the entry’s unique name (usually the model attribute), and you then chain configuration methods. Correct basic usage (4.x docs): TextEntry::make('title') [1] Naming / record state - The argument to make is the entry name; it usually corresponds to an Eloquent model attribute, and you can use “dot notation” for relationship attributes (e.g. author.name). [2][3] Correct chaining for formatting (do not try to “set the value” via make) - To format the displayed value without changing the underlying state, use formatStateUsing, which accepts a callback that receives the current state and returns the formatted output. [1] Example: TextEntry::make('status') ->formatStateUsing(fn (string $state): string => ("statuses.{$state}")) [1] To set the state directly (instead of relying on the record attribute) - Use state(...) and (optionally) default(...) for null values. [2] Common, documented configuration methods for display The 4.x TextEntry docs show chaining methods like badge, color, icon/iconPosition, etc. Example patterns include: - badge and color(...) for “badge-like” statuses. [1] Answering the likely “API documentation correct usage” confusion - If you’re seeing TextEntry::make used as if it should be passed a value (e.g., TextEntry::make('title', $value)), that is not how Filament documents it; the documented signature is make(string $name) (the “name” you pass in), followed by chained methods such as state, formatStateUsing, default, and formatting/display helpers. [1][2] If you tell me your Filament major version (3/4/5) and paste the snippet you’re unsure about, I can map it directly to the correct documented method(s).

Citations:


Use a static text component or follow the correct TextEntry pattern.

TextEntry::make() requires a field/state key as its argument, not a display value. Based on Filament's documented API and codebase patterns, use TextEntry::make('info') (or similar) with .state(trans('admin/backuphost.no_configuration')) to display the message.

🤖 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/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php` around lines 57 -
62, In getConfigurationForm() the TextEntry is being constructed with a display
string instead of a field key; change the TextEntry::make(...) call to provide a
field name (e.g. TextEntry::make('info') or 'note') and set its displayed text
via ->state(trans('admin/backuphost.no_configuration')) (or use a static text
component if available) so the form component follows Filament's TextEntry
pattern and shows the translated message correctly.

Comment thread app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
Comment thread app/Http/Controllers/Api/Client/Servers/BackupController.php
Comment thread app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Comment thread app/Models/Backup.php Outdated
Comment thread app/Models/BackupHost.php
Comment thread app/Services/Servers/ServerDeletionService.php
Comment thread app/Services/Servers/TransferServerService.php
Boy132 and others added 3 commits May 19, 2026 13:10
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
# Conflicts:
#	app/Services/Backups/DownloadLinkService.php
#	app/Services/Servers/TransferServerService.php
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.

S3 backup is not deleted after deletion of server Refactor Backup Options

2 participants