diff --git a/app/Actions/NotificationChannels/AddChannel.php b/app/Actions/NotificationChannels/AddChannel.php
index 11e98aef..5e3ef93c 100644
--- a/app/Actions/NotificationChannels/AddChannel.php
+++ b/app/Actions/NotificationChannels/AddChannel.php
@@ -4,7 +4,7 @@
 
 use App\Models\NotificationChannel;
 use App\Models\User;
-use Illuminate\Support\Facades\Validator;
+use Illuminate\Validation\Rule;
 use Illuminate\Validation\ValidationException;
 
 class AddChannel
@@ -14,14 +14,12 @@ class AddChannel
      */
     public function add(User $user, array $input): void
     {
-        $this->validate($input);
         $channel = new NotificationChannel([
             'user_id' => $user->id,
             'provider' => $input['provider'],
             'label' => $input['label'],
             'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
         ]);
-        $this->validateType($channel, $input);
         $channel->data = $channel->provider()->createData($input);
         $channel->save();
 
@@ -43,23 +41,29 @@ public function add(User $user, array $input): void
         $channel->save();
     }
 
-    /**
-     * @throws ValidationException
-     */
-    protected function validate(array $input): void
+    public static function rules(array $input): array
     {
-        Validator::make($input, [
-            'provider' => 'required|in:'.implode(',', config('core.notification_channels_providers')),
+        $rules = [
+            'provider' => [
+                'required',
+                Rule::in(config('core.notification_channels_providers')),
+            ],
             'label' => 'required',
-        ])->validate();
+        ];
+
+        return array_merge($rules, static::providerRules($input));
     }
 
-    /**
-     * @throws ValidationException
-     */
-    protected function validateType(NotificationChannel $channel, array $input): void
+    private static function providerRules(array $input): array
     {
-        Validator::make($input, $channel->provider()->createRules($input))
-            ->validate();
+        if (! isset($input['provider'])) {
+            return [];
+        }
+
+        $notificationChannel = new NotificationChannel([
+            'provider' => $input['provider'],
+        ]);
+
+        return $notificationChannel->provider()->createRules($input);
     }
 }
diff --git a/app/Actions/NotificationChannels/EditChannel.php b/app/Actions/NotificationChannels/EditChannel.php
index fbba3de7..1404930e 100644
--- a/app/Actions/NotificationChannels/EditChannel.php
+++ b/app/Actions/NotificationChannels/EditChannel.php
@@ -4,31 +4,22 @@
 
 use App\Models\NotificationChannel;
 use App\Models\User;
-use Illuminate\Support\Facades\Validator;
-use Illuminate\Validation\ValidationException;
 
 class EditChannel
 {
     public function edit(NotificationChannel $notificationChannel, User $user, array $input): void
     {
-        $this->validate($input);
-
-        $notificationChannel->label = $input['label'];
-        $notificationChannel->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
-
+        $notificationChannel->fill([
+            'label' => $input['label'],
+            'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
+        ]);
         $notificationChannel->save();
     }
 
-    /**
-     * @throws ValidationException
-     */
-    private function validate(array $input): void
+    public static function rules(array $input): array
     {
-        $rules = [
-            'label' => [
-                'required',
-            ],
+        return [
+            'label' => 'required',
         ];
-        Validator::make($input, $rules)->validate();
     }
 }
diff --git a/app/Actions/Script/CreateScript.php b/app/Actions/Script/CreateScript.php
index 7e155b08..07348687 100644
--- a/app/Actions/Script/CreateScript.php
+++ b/app/Actions/Script/CreateScript.php
@@ -4,29 +4,27 @@
 
 use App\Models\Script;
 use App\Models\User;
-use Illuminate\Support\Facades\Validator;
 
 class CreateScript
 {
     public function create(User $user, array $input): Script
     {
-        $this->validate($input);
-
         $script = new Script([
             'user_id' => $user->id,
             'name' => $input['name'],
             'content' => $input['content'],
+            'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
         ]);
         $script->save();
 
         return $script;
     }
 
-    private function validate(array $input): void
+    public static function rules(): array
     {
-        Validator::make($input, [
+        return [
             'name' => ['required', 'string', 'max:255'],
             'content' => ['required', 'string'],
-        ])->validate();
+        ];
     }
 }
diff --git a/app/Actions/Script/EditScript.php b/app/Actions/Script/EditScript.php
index b0251288..b155354f 100644
--- a/app/Actions/Script/EditScript.php
+++ b/app/Actions/Script/EditScript.php
@@ -3,26 +3,26 @@
 namespace App\Actions\Script;
 
 use App\Models\Script;
-use Illuminate\Support\Facades\Validator;
+use App\Models\User;
 
 class EditScript
 {
-    public function edit(Script $script, array $input): Script
+    public function edit(Script $script, User $user, array $input): Script
     {
-        $this->validate($input);
-
         $script->name = $input['name'];
         $script->content = $input['content'];
+        $script->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
+
         $script->save();
 
         return $script;
     }
 
-    private function validate(array $input): void
+    public static function rules(): array
     {
-        Validator::make($input, [
+        return [
             'name' => ['required', 'string', 'max:255'],
             'content' => ['required', 'string'],
-        ])->validate();
+        ];
     }
 }
diff --git a/app/Actions/Script/ExecuteScript.php b/app/Actions/Script/ExecuteScript.php
index 5c601e12..ca2dd0a8 100644
--- a/app/Actions/Script/ExecuteScript.php
+++ b/app/Actions/Script/ExecuteScript.php
@@ -7,30 +7,28 @@
 use App\Models\ScriptExecution;
 use App\Models\Server;
 use App\Models\ServerLog;
-use Illuminate\Support\Facades\Validator;
 use Illuminate\Validation\Rule;
 
 class ExecuteScript
 {
-    public function execute(Script $script, Server $server, array $input): ScriptExecution
+    public function execute(Script $script, array $input): ScriptExecution
     {
-        $this->validate($server, $input);
-
         $execution = new ScriptExecution([
             'script_id' => $script->id,
+            'server_id' => $input['server'],
             'user' => $input['user'],
             'variables' => $input['variables'] ?? [],
             'status' => ScriptExecutionStatus::EXECUTING,
         ]);
         $execution->save();
 
-        dispatch(function () use ($execution, $server, $script) {
+        dispatch(function () use ($execution, $script) {
             $content = $execution->getContent();
-            $log = ServerLog::make($server, 'script-'.$script->id.'-'.strtotime('now'));
+            $log = ServerLog::make($execution->server, 'script-'.$script->id.'-'.strtotime('now'));
             $log->save();
             $execution->server_log_id = $log->id;
             $execution->save();
-            $server->os()->runScript('~/', $content, $log, $execution->user);
+            $execution->server->os()->runScript('~/', $content, $log, $execution->user);
             $execution->status = ScriptExecutionStatus::COMPLETED;
             $execution->save();
         })->catch(function () use ($execution) {
@@ -41,14 +39,23 @@ public function execute(Script $script, Server $server, array $input): ScriptExe
         return $execution;
     }
 
-    private function validate(Server $server, array $input): void
+    public static function rules(array $input): array
     {
-        Validator::make($input, [
+        if (isset($input['server'])) {
+            /** @var ?Server $server */
+            $server = Server::query()->find($input['server']);
+        }
+
+        return [
+            'server' => [
+                'required',
+                Rule::exists('servers', 'id'),
+            ],
             'user' => [
                 'required',
                 Rule::in([
                     'root',
-                    $server->ssh_user,
+                    isset($server) ? $server?->ssh_user : null,
                 ]),
             ],
             'variables' => 'array',
@@ -57,6 +64,6 @@ private function validate(Server $server, array $input): void
                 'string',
                 'max:255',
             ],
-        ])->validate();
+        ];
     }
 }
diff --git a/app/Models/Script.php b/app/Models/Script.php
index 1f3b35ea..e5306378 100644
--- a/app/Models/Script.php
+++ b/app/Models/Script.php
@@ -3,6 +3,7 @@
 namespace App\Models;
 
 use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -18,6 +19,9 @@
  * @property Carbon $updated_at
  * @property Collection<ScriptExecution> $executions
  * @property ?ScriptExecution $lastExecution
+ * @property User $user
+ * @property int $project_id
+ * @property ?Project $project
  */
 class Script extends AbstractModel
 {
@@ -27,6 +31,12 @@ class Script extends AbstractModel
         'user_id',
         'name',
         'content',
+        'project_id',
+    ];
+
+    protected $casts = [
+        'user_id' => 'int',
+        'project_id' => 'int',
     ];
 
     public static function boot(): void
@@ -43,6 +53,11 @@ public function user(): BelongsTo
         return $this->belongsTo(User::class);
     }
 
+    public function project(): BelongsTo
+    {
+        return $this->belongsTo(Project::class);
+    }
+
     public function getVariables(): array
     {
         $variables = [];
@@ -63,4 +78,14 @@ public function lastExecution(): HasOne
     {
         return $this->hasOne(ScriptExecution::class)->latest();
     }
+
+    public static function getByProjectId(int $projectId, int $userId): Builder
+    {
+        return self::query()
+            ->where(function (Builder $query) use ($projectId, $userId) {
+                $query->where('project_id', $projectId)
+                    ->orWhere('user_id', $userId)
+                    ->orWhereNull('project_id');
+            });
+    }
 }
diff --git a/app/Models/ScriptExecution.php b/app/Models/ScriptExecution.php
index e7d5fdaa..6b8c1e9a 100644
--- a/app/Models/ScriptExecution.php
+++ b/app/Models/ScriptExecution.php
@@ -2,15 +2,16 @@
 
 namespace App\Models;
 
+use App\Enums\ScriptExecutionStatus;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
-use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 /**
  * @property int $id
  * @property int $script_id
  * @property int $server_log_id
+ * @property ?int $server_id
  * @property string $user
  * @property array $variables
  * @property string $status
@@ -18,13 +19,15 @@
  * @property Carbon $updated_at
  * @property Script $script
  * @property ?ServerLog $serverLog
+ * @property ?Server $server
  */
-class ScriptExecution extends Model
+class ScriptExecution extends AbstractModel
 {
     use HasFactory;
 
     protected $fillable = [
         'script_id',
+        'server_id',
         'server_log_id',
         'user',
         'variables',
@@ -33,10 +36,17 @@ class ScriptExecution extends Model
 
     protected $casts = [
         'script_id' => 'integer',
+        'server_id' => 'integer',
         'server_log_id' => 'integer',
         'variables' => 'array',
     ];
 
+    public static array $statusColors = [
+        ScriptExecutionStatus::EXECUTING => 'warning',
+        ScriptExecutionStatus::COMPLETED => 'success',
+        ScriptExecutionStatus::FAILED => 'danger',
+    ];
+
     public function script(): BelongsTo
     {
         return $this->belongsTo(Script::class);
@@ -58,4 +68,22 @@ public function serverLog(): BelongsTo
     {
         return $this->belongsTo(ServerLog::class);
     }
+
+    public function server(): BelongsTo
+    {
+        return $this->belongsTo(Server::class);
+    }
+
+    public function getServer(): ?Server
+    {
+        if ($this->server_id) {
+            return $this->server;
+        }
+
+        if ($this->server_log_id) {
+            return $this->serverLog?->server;
+        }
+
+        return null;
+    }
 }
diff --git a/app/Models/SshKey.php b/app/Models/SshKey.php
index dcb27e53..cf420844 100644
--- a/app/Models/SshKey.php
+++ b/app/Models/SshKey.php
@@ -5,6 +5,7 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
 
 /**
  * @property int $user_id
@@ -16,6 +17,7 @@
 class SshKey extends AbstractModel
 {
     use HasFactory;
+    use SoftDeletes;
 
     protected $fillable = [
         'user_id',
diff --git a/app/NotificationChannels/Discord.php b/app/NotificationChannels/Discord.php
index f41e2586..3807f20a 100644
--- a/app/NotificationChannels/Discord.php
+++ b/app/NotificationChannels/Discord.php
@@ -11,7 +11,10 @@ class Discord extends AbstractNotificationChannel
     public function createRules(array $input): array
     {
         return [
-            'webhook_url' => 'required|url',
+            'webhook_url' => [
+                'required',
+                'url',
+            ],
         ];
     }
 
diff --git a/app/NotificationChannels/Email.php b/app/NotificationChannels/Email.php
index 6ff6006e..389d5052 100644
--- a/app/NotificationChannels/Email.php
+++ b/app/NotificationChannels/Email.php
@@ -13,7 +13,10 @@ class Email extends AbstractNotificationChannel
     public function createRules(array $input): array
     {
         return [
-            'email' => 'required|email',
+            'email' => [
+                'required',
+                'email',
+            ],
         ];
     }
 
diff --git a/app/NotificationChannels/Slack.php b/app/NotificationChannels/Slack.php
index 3bc28cd3..e20fffa6 100644
--- a/app/NotificationChannels/Slack.php
+++ b/app/NotificationChannels/Slack.php
@@ -11,7 +11,10 @@ class Slack extends AbstractNotificationChannel
     public function createRules(array $input): array
     {
         return [
-            'webhook_url' => 'required|url',
+            'webhook_url' => [
+                'required',
+                'url',
+            ],
         ];
     }
 
diff --git a/app/NotificationChannels/Telegram.php b/app/NotificationChannels/Telegram.php
index 017399b4..0ddae2ea 100644
--- a/app/NotificationChannels/Telegram.php
+++ b/app/NotificationChannels/Telegram.php
@@ -14,8 +14,12 @@ class Telegram extends AbstractNotificationChannel
     public function createRules(array $input): array
     {
         return [
-            'bot_token' => 'required|string',
-            'chat_id' => 'required',
+            'bot_token' => [
+                'required',
+            ],
+            'chat_id' => [
+                'required',
+            ],
         ];
     }
 
diff --git a/app/Policies/NotificationChannelPolicy.php b/app/Policies/NotificationChannelPolicy.php
new file mode 100644
index 00000000..f21c0573
--- /dev/null
+++ b/app/Policies/NotificationChannelPolicy.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Policies;
+
+use App\Models\NotificationChannel;
+use App\Models\User;
+use Illuminate\Auth\Access\HandlesAuthorization;
+
+class NotificationChannelPolicy
+{
+    use HandlesAuthorization;
+
+    public function viewAny(User $user): bool
+    {
+        return $user->isAdmin();
+    }
+
+    public function view(User $user, NotificationChannel $notificationChannel): bool
+    {
+        return $user->isAdmin();
+    }
+
+    public function create(User $user): bool
+    {
+        return $user->isAdmin();
+    }
+
+    public function update(User $user, NotificationChannel $notificationChannel): bool
+    {
+        return $user->isAdmin();
+    }
+
+    public function delete(User $user, NotificationChannel $notificationChannel): bool
+    {
+        return $user->isAdmin();
+    }
+}
diff --git a/app/Policies/ScriptPolicy.php b/app/Policies/ScriptPolicy.php
index 3003d9c2..c0f9d3e4 100644
--- a/app/Policies/ScriptPolicy.php
+++ b/app/Policies/ScriptPolicy.php
@@ -18,7 +18,7 @@ public function viewAny(User $user): bool
 
     public function view(User $user, Script $script): bool
     {
-        return $user->id === $script->user_id;
+        return $user->id === $script->user_id || $script->project?->users?->contains($user);
     }
 
     public function create(User $user): bool
diff --git a/app/Providers/WebServiceProvider.php b/app/Providers/WebServiceProvider.php
index ca260d0a..717e2fac 100644
--- a/app/Providers/WebServiceProvider.php
+++ b/app/Providers/WebServiceProvider.php
@@ -104,6 +104,7 @@ public function panel(Panel $panel): Panel
                 Authenticate::class,
             ])
             ->login()
+            ->spa()
             ->globalSearchKeyBindings(['command+k', 'ctrl+k'])
             ->sidebarCollapsibleOnDesktop()
             ->globalSearchFieldKeyBindingSuffix();
diff --git a/app/Support/helpers.php b/app/Support/helpers.php
index 735f92b1..7b1a05cb 100755
--- a/app/Support/helpers.php
+++ b/app/Support/helpers.php
@@ -4,6 +4,8 @@
 use App\Helpers\HtmxResponse;
 use Filament\Notifications\Actions\Action;
 use Filament\Notifications\Notification;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Route;
 
 function generate_public_key($privateKeyPath, $publicKeyPath): void
 {
@@ -146,3 +148,22 @@ function tail($filepath, $lines = 1, $adaptive = true): string
 
     return trim($output);
 }
+
+function get_from_route(string $modelName, string $routeKey): mixed
+{
+    $model = request()->route($routeKey);
+
+    if (! $model) {
+        $model = Route::getRoutes()->match(Request::create(url()->previous()))->parameter($routeKey);
+    }
+
+    if ($model instanceof $modelName) {
+        return $model;
+    }
+
+    if ($model) {
+        return $modelName::query()->find($model);
+    }
+
+    return null;
+}
diff --git a/app/Web/Pages/Scripts/Executions.php b/app/Web/Pages/Scripts/Executions.php
new file mode 100644
index 00000000..45e51308
--- /dev/null
+++ b/app/Web/Pages/Scripts/Executions.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Web\Pages\Scripts;
+
+use App\Actions\Script\ExecuteScript;
+use App\Models\Script;
+use App\Models\Server;
+use App\Web\Components\Page;
+use Filament\Actions\Action;
+use Filament\Forms\Components\Select;
+use Filament\Forms\Components\TextInput;
+use Filament\Forms\Get;
+use Filament\Support\Enums\MaxWidth;
+use Illuminate\Contracts\Support\Htmlable;
+
+class Executions extends Page
+{
+    protected static bool $shouldRegisterNavigation = false;
+
+    protected static ?string $slug = 'scripts/{script}/executions';
+
+    public Script $script;
+
+    public function getTitle(): string|Htmlable
+    {
+        return $this->script->name.' - Executions';
+    }
+
+    public static function canAccess(): bool
+    {
+        return auth()->user()?->can('view', get_from_route(Script::class, 'script')) ?? false;
+    }
+
+    public function getWidgets(): array
+    {
+        return [
+            [Widgets\ScriptExecutionsList::class, ['script' => $this->script]],
+        ];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        $form = [
+            Select::make('server')
+                ->options(function () {
+                    return auth()->user()?->currentProject?->servers?->pluck('name', 'id') ?? [];
+                })
+                ->rules(fn (Get $get) => ExecuteScript::rules($get())['server'])
+                ->searchable()
+                ->reactive(),
+            Select::make('user')
+                ->rules(fn (Get $get) => ExecuteScript::rules($get())['user'])
+                ->native(false)
+                ->options(function (Get $get) {
+                    $options = [
+                        'root' => 'root',
+                    ];
+
+                    $server = Server::query()->find($get('server'));
+                    if ($server) {
+                        $options[$server->ssh_user] = $server->ssh_user;
+                    }
+
+                    return $options;
+                }),
+        ];
+
+        foreach ($this->script->getVariables() as $variable) {
+            $form[] = TextInput::make($variable)
+                ->label($variable)
+                ->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']);
+        }
+
+        return [
+            Action::make('execute')
+                ->icon('heroicon-o-bolt')
+                ->modalWidth(MaxWidth::Large)
+                ->form($form)
+                ->action(function (array $data) {
+                    app(ExecuteScript::class)->execute($this->script, $data);
+
+                    $this->dispatch('$refresh');
+                }),
+        ];
+    }
+}
diff --git a/app/Web/Pages/Scripts/Index.php b/app/Web/Pages/Scripts/Index.php
new file mode 100644
index 00000000..d7300801
--- /dev/null
+++ b/app/Web/Pages/Scripts/Index.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace App\Web\Pages\Scripts;
+
+use App\Actions\Script\CreateScript;
+use App\Models\Script;
+use App\Web\Components\Page;
+use App\Web\Fields\CodeEditorField;
+use Filament\Actions\Action;
+use Filament\Forms\Components\Checkbox;
+use Filament\Forms\Components\TextInput;
+use Filament\Support\Enums\MaxWidth;
+
+class Index extends Page
+{
+    protected static ?string $slug = 'scripts';
+
+    protected static ?string $navigationIcon = 'heroicon-o-bolt';
+
+    protected static ?int $navigationSort = 2;
+
+    protected static ?string $title = 'Scripts';
+
+    public static function getNavigationItemActiveRoutePattern(): string
+    {
+        return static::getRouteName().'*';
+    }
+
+    public static function canAccess(): bool
+    {
+        return auth()->user()?->can('viewAny', Script::class) ?? false;
+    }
+
+    public function getWidgets(): array
+    {
+        return [
+            [Widgets\ScriptsList::class],
+        ];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            Action::make('read-the-docs')
+                ->label('Read the Docs')
+                ->icon('heroicon-o-document-text')
+                ->color('gray')
+                ->url('https://vitodeploy.com/other/scripts.html')
+                ->openUrlInNewTab(),
+            Action::make('create')
+                ->label('Create a Script')
+                ->icon('heroicon-o-plus')
+                ->authorize('create', Script::class)
+                ->modalWidth(MaxWidth::ThreeExtraLarge)
+                ->form([
+                    TextInput::make('name')
+                        ->rules(CreateScript::rules()['name']),
+                    CodeEditorField::make('content')
+                        ->rules(CreateScript::rules()['content'])
+                        ->helperText('You can use variables like ${VARIABLE_NAME} in the script. The variables will be asked when executing the script'),
+                    Checkbox::make('global')
+                        ->label('Is Global (Accessible in all projects)'),
+                ])
+                ->modalSubmitActionLabel('Create')
+                ->action(function (array $data) {
+                    run_action($this, function () use ($data) {
+                        app(CreateScript::class)->create(auth()->user(), $data);
+
+                        $this->dispatch('$refresh');
+                    });
+                }),
+        ];
+    }
+}
diff --git a/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php b/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php
new file mode 100644
index 00000000..ce06dcd9
--- /dev/null
+++ b/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Web\Pages\Scripts\Widgets;
+
+use App\Models\Script;
+use App\Models\ScriptExecution;
+use App\Web\Pages\Servers\View;
+use Filament\Tables\Actions\Action;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Table;
+use Filament\Widgets\TableWidget as Widget;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\View\ComponentAttributeBag;
+
+class ScriptExecutionsList extends Widget
+{
+    protected $listeners = ['$refresh'];
+
+    public Script $script;
+
+    protected function getTableQuery(): Builder
+    {
+        return ScriptExecution::query()->where('script_id', $this->script->id);
+    }
+
+    protected function applyDefaultSortingToTableQuery(Builder $query): Builder
+    {
+        return $query->latest('created_at');
+    }
+
+    protected function getTableColumns(): array
+    {
+        return [
+            TextColumn::make('server')
+                ->formatStateUsing(function (ScriptExecution $record) {
+                    return $record->getServer()?->name ?? 'Unknown';
+                })
+                ->url(function (ScriptExecution $record) {
+                    $server = $record->getServer();
+
+                    return $server ? View::getUrl(['server' => $server]) : null;
+                })
+                ->searchable()
+                ->sortable(),
+            TextColumn::make('created_at')
+                ->label('Executed At')
+                ->formatStateUsing(fn (ScriptExecution $record) => $record->created_at_by_timezone)
+                ->searchable()
+                ->sortable(),
+            TextColumn::make('status')
+                ->label('Status')
+                ->badge()
+                ->color(fn (ScriptExecution $record) => ScriptExecution::$statusColors[$record->status])
+                ->sortable(),
+        ];
+    }
+
+    public function getTable(): Table
+    {
+        return $this->table
+            ->heading('')
+            ->actions([
+                Action::make('view')
+                    ->hiddenLabel()
+                    ->tooltip('View')
+                    ->icon('heroicon-o-eye')
+                    ->authorize(fn (ScriptExecution $record) => auth()->user()->can('view', $record->serverLog))
+                    ->modalHeading('View Log')
+                    ->modalContent(function (ScriptExecution $record) {
+                        return view('components.console-view', [
+                            'slot' => $record->serverLog?->getContent(),
+                            'attributes' => new ComponentAttributeBag,
+                        ]);
+                    })
+                    ->modalSubmitAction(false)
+                    ->modalCancelActionLabel('Close'),
+            ]);
+    }
+}
diff --git a/app/Web/Pages/Scripts/Widgets/ScriptsList.php b/app/Web/Pages/Scripts/Widgets/ScriptsList.php
new file mode 100644
index 00000000..aa33a7df
--- /dev/null
+++ b/app/Web/Pages/Scripts/Widgets/ScriptsList.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Web\Pages\Scripts\Widgets;
+
+use App\Actions\Script\EditScript;
+use App\Models\Script;
+use App\Web\Fields\CodeEditorField;
+use App\Web\Pages\Scripts\Executions;
+use Filament\Forms\Components\Checkbox;
+use Filament\Forms\Components\TextInput;
+use Filament\Support\Enums\MaxWidth;
+use Filament\Tables\Actions\DeleteAction;
+use Filament\Tables\Actions\EditAction;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Table;
+use Filament\Widgets\TableWidget as Widget;
+use Illuminate\Database\Eloquent\Builder;
+
+class ScriptsList extends Widget
+{
+    protected $listeners = ['$refresh'];
+
+    protected function getTableQuery(): Builder
+    {
+        return Script::getByProjectId(auth()->user()->current_project_id, auth()->user()->id);
+    }
+
+    protected function getTableColumns(): array
+    {
+        return [
+            TextColumn::make('name')
+                ->searchable()
+                ->sortable(),
+            TextColumn::make('id')
+                ->label('Global')
+                ->badge()
+                ->color(fn ($record) => $record->project_id ? 'gray' : 'success')
+                ->formatStateUsing(function (Script $record) {
+                    return $record->project_id ? 'No' : 'Yes';
+                }),
+            TextColumn::make('created_at')
+                ->label('Created At')
+                ->formatStateUsing(fn (Script $record) => $record->created_at_by_timezone)
+                ->searchable()
+                ->sortable(),
+        ];
+    }
+
+    public function getTable(): Table
+    {
+        return $this->table
+            ->heading('')
+            ->recordUrl(fn (Script $record) => Executions::getUrl(['script' => $record]))
+            ->actions([
+                EditAction::make('edit')
+                    ->label('Edit')
+                    ->modalHeading('Edit Script')
+                    ->mutateRecordDataUsing(function (array $data, Script $record) {
+                        return [
+                            'name' => $record->name,
+                            'content' => $record->content,
+                            'global' => $record->project_id === null,
+                        ];
+                    })
+                    ->form([
+                        TextInput::make('name')
+                            ->rules(EditScript::rules()['name']),
+                        CodeEditorField::make('content')
+                            ->rules(EditScript::rules()['content'])
+                            ->helperText('You can use variables like ${VARIABLE_NAME} in the script. The variables will be asked when executing the script'),
+                        Checkbox::make('global')
+                            ->label('Is Global (Accessible in all projects)'),
+                    ])
+                    ->authorize(fn (Script $record) => auth()->user()->can('update', $record))
+                    ->using(function (array $data, Script $record) {
+                        app(EditScript::class)->edit($record, auth()->user(), $data);
+                        $this->dispatch('$refresh');
+                    })
+                    ->modalWidth(MaxWidth::ThreeExtraLarge),
+                DeleteAction::make('delete')
+                    ->label('Delete')
+                    ->modalHeading('Delete Script')
+                    ->authorize(fn (Script $record) => auth()->user()->can('delete', $record))
+                    ->using(function (array $data, Script $record) {
+                        $record->delete();
+                    }),
+            ]);
+    }
+}
diff --git a/app/Web/Pages/Servers/SSHKeys/Widgets/SshKeysList.php b/app/Web/Pages/Servers/SSHKeys/Widgets/SshKeysList.php
index d34dc42e..57fcd277 100644
--- a/app/Web/Pages/Servers/SSHKeys/Widgets/SshKeysList.php
+++ b/app/Web/Pages/Servers/SSHKeys/Widgets/SshKeysList.php
@@ -7,7 +7,7 @@
 use App\Models\SshKey;
 use Exception;
 use Filament\Notifications\Notification;
-use Filament\Tables\Actions\Action;
+use Filament\Tables\Actions\DeleteAction;
 use Filament\Tables\Columns\TextColumn;
 use Filament\Tables\Table;
 use Filament\Widgets\TableWidget;
@@ -21,10 +21,11 @@ class SshKeysList extends TableWidget
 
     protected function getTableQuery(): Builder
     {
-        return SshKey::query()->whereHas(
-            'servers',
-            fn (Builder $query) => $query->where('server_id', $this->server->id)
-        );
+        return SshKey::withTrashed()
+            ->whereHas(
+                'servers',
+                fn (Builder $query) => $query->where('server_id', $this->server->id)
+            );
     }
 
     protected static ?string $heading = '';
@@ -47,14 +48,10 @@ public function getTable(): Table
     {
         return $this->table
             ->actions([
-                Action::make('delete')
-                    ->icon('heroicon-o-trash')
-                    ->tooltip('Delete')
-                    ->color('danger')
+                DeleteAction::make('delete')
                     ->hiddenLabel()
-                    ->requiresConfirmation()
                     ->authorize(fn (SshKey $record) => auth()->user()->can('deleteServer', [SshKey::class, $this->server]))
-                    ->action(function (SshKey $record) {
+                    ->using(function (SshKey $record) {
                         try {
                             app(DeleteKeyFromServer::class)->delete($this->server, $record);
                         } catch (Exception $e) {
diff --git a/app/Web/Pages/Settings/NotificationChannels/Actions/Create.php b/app/Web/Pages/Settings/NotificationChannels/Actions/Create.php
new file mode 100644
index 00000000..c7d66130
--- /dev/null
+++ b/app/Web/Pages/Settings/NotificationChannels/Actions/Create.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Web\Pages\Settings\NotificationChannels\Actions;
+
+use App\Actions\NotificationChannels\AddChannel;
+use Exception;
+use Filament\Forms\Components\Checkbox;
+use Filament\Forms\Components\Select;
+use Filament\Forms\Components\TextInput;
+use Filament\Forms\Get;
+use Filament\Notifications\Notification;
+
+class Create
+{
+    public static function form(): array
+    {
+        return [
+            Select::make('provider')
+                ->options(
+                    collect(config('core.notification_channels_providers'))
+                        ->mapWithKeys(fn ($provider) => [$provider => $provider])
+                )
+                ->live()
+                ->reactive()
+                ->rules(fn (Get $get) => AddChannel::rules($get())['provider']),
+            TextInput::make('label')
+                ->rules(fn (Get $get) => AddChannel::rules($get())['label']),
+            TextInput::make('webhook_url')
+                ->label('Webhook URL')
+                ->validationAttribute('Webhook URL')
+                ->visible(fn (Get $get) => AddChannel::rules($get())['webhook_url'] ?? false)
+                ->rules(fn (Get $get) => AddChannel::rules($get())['webhook_url']),
+            TextInput::make('email')
+                ->visible(fn (Get $get) => AddChannel::rules($get())['email'] ?? false)
+                ->rules(fn (Get $get) => AddChannel::rules($get())['email']),
+            TextInput::make('bot_token')
+                ->label('Bot Token')
+                ->visible(fn (Get $get) => AddChannel::rules($get())['bot_token'] ?? false)
+                ->rules(fn (Get $get) => AddChannel::rules($get())['bot_token']),
+            TextInput::make('chat_id')
+                ->label('Chat ID')
+                ->visible(fn (Get $get) => AddChannel::rules($get())['chat_id'] ?? false)
+                ->rules(fn (Get $get) => AddChannel::rules($get())['chat_id']),
+            Checkbox::make('global')
+                ->label('Is Global (Accessible in all projects)'),
+        ];
+    }
+
+    /**
+     * @throws Exception
+     */
+    public static function action(array $data): void
+    {
+        try {
+            app(AddChannel::class)->add(auth()->user(), $data);
+        } catch (Exception $e) {
+            Notification::make()
+                ->title($e->getMessage())
+                ->danger()
+                ->send();
+
+            throw $e;
+        }
+    }
+}
diff --git a/app/Web/Pages/Settings/NotificationChannels/Actions/Edit.php b/app/Web/Pages/Settings/NotificationChannels/Actions/Edit.php
new file mode 100644
index 00000000..4544c26e
--- /dev/null
+++ b/app/Web/Pages/Settings/NotificationChannels/Actions/Edit.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Web\Pages\Settings\NotificationChannels\Actions;
+
+use App\Actions\NotificationChannels\EditChannel;
+use App\Models\NotificationChannel;
+use Filament\Forms\Components\Checkbox;
+use Filament\Forms\Components\TextInput;
+use Filament\Forms\Get;
+
+class Edit
+{
+    public static function form(): array
+    {
+        return [
+            TextInput::make('label')
+                ->rules(fn (Get $get) => EditChannel::rules($get())['label']),
+            Checkbox::make('global')
+                ->label('Is Global (Accessible in all projects)'),
+        ];
+    }
+
+    public static function action(NotificationChannel $channel, array $data): void
+    {
+        app(EditChannel::class)->edit($channel, auth()->user(), $data);
+    }
+}
diff --git a/app/Web/Pages/Settings/NotificationChannels/Index.php b/app/Web/Pages/Settings/NotificationChannels/Index.php
new file mode 100644
index 00000000..4bd74b6a
--- /dev/null
+++ b/app/Web/Pages/Settings/NotificationChannels/Index.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Web\Pages\Settings\NotificationChannels;
+
+use App\Models\NotificationChannel;
+use App\Web\Components\Page;
+use Filament\Actions\Action;
+use Filament\Support\Enums\MaxWidth;
+
+class Index extends Page
+{
+    protected static ?string $navigationGroup = 'Settings';
+
+    protected static ?string $slug = 'settings/notification-channels';
+
+    protected static ?string $title = 'Notification Channels';
+
+    protected static ?string $navigationIcon = 'heroicon-o-bell';
+
+    protected static ?int $navigationSort = 8;
+
+    public static function canAccess(): bool
+    {
+        return auth()->user()?->can('viewAny', NotificationChannel::class) ?? false;
+    }
+
+    public function getWidgets(): array
+    {
+        return [
+            [Widgets\NotificationChannelsList::class],
+        ];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            Action::make('add')
+                ->label('Add new Channel')
+                ->icon('heroicon-o-plus')
+                ->modalHeading('Add a new Channel')
+                ->modalSubmitActionLabel('Add')
+                ->form(Actions\Create::form())
+                ->authorize('create', NotificationChannel::class)
+                ->modalWidth(MaxWidth::Large)
+                ->action(function (array $data) {
+                    Actions\Create::action($data);
+
+                    $this->dispatch('$refresh');
+                }),
+        ];
+    }
+}
diff --git a/app/Web/Pages/Settings/NotificationChannels/Widgets/NotificationChannelsList.php b/app/Web/Pages/Settings/NotificationChannels/Widgets/NotificationChannelsList.php
new file mode 100644
index 00000000..5a401c11
--- /dev/null
+++ b/app/Web/Pages/Settings/NotificationChannels/Widgets/NotificationChannelsList.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace App\Web\Pages\Settings\NotificationChannels\Widgets;
+
+use App\Models\NotificationChannel;
+use App\Web\Pages\Settings\NotificationChannels\Actions\Edit;
+use Filament\Support\Enums\MaxWidth;
+use Filament\Tables\Actions\DeleteAction;
+use Filament\Tables\Actions\EditAction;
+use Filament\Tables\Columns\IconColumn;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Table;
+use Filament\Widgets\TableWidget as Widget;
+use Illuminate\Database\Eloquent\Builder;
+
+class NotificationChannelsList extends Widget
+{
+    protected $listeners = [
+        '$refresh' => 'refreshTable',
+    ];
+
+    protected function getTableQuery(): Builder
+    {
+        return NotificationChannel::getByProjectId(auth()->user()->current_project_id);
+    }
+
+    protected function getTableColumns(): array
+    {
+        return [
+            IconColumn::make('provider')
+                ->icon(fn (NotificationChannel $record) => 'icon-'.$record->provider)
+                ->width(24),
+            TextColumn::make('label')
+                ->default(fn (NotificationChannel $record) => $record->label)
+                ->searchable()
+                ->sortable(),
+            TextColumn::make('id')
+                ->label('Global')
+                ->badge()
+                ->color(fn (NotificationChannel $record) => $record->project_id ? 'gray' : 'success')
+                ->formatStateUsing(function (NotificationChannel $record) {
+                    return $record->project_id ? 'No' : 'Yes';
+                }),
+            TextColumn::make('created_at')
+                ->label('Created At')
+                ->formatStateUsing(fn (NotificationChannel $record) => $record->created_at_by_timezone)
+                ->searchable()
+                ->sortable(),
+        ];
+    }
+
+    public function getTable(): Table
+    {
+        return $this->table
+            ->heading('')
+            ->actions([
+                EditAction::make('edit')
+                    ->modalHeading('Edit Notification Channel')
+                    ->mutateRecordDataUsing(function (array $data, NotificationChannel $record) {
+                        return [
+                            'label' => $record->label,
+                            'global' => ! $record->project_id,
+                        ];
+                    })
+                    ->form(Edit::form())
+                    ->authorize(fn (NotificationChannel $record) => auth()->user()->can('update', $record))
+                    ->using(fn (array $data, NotificationChannel $record) => Edit::action($record, $data))
+                    ->modalWidth(MaxWidth::Medium),
+                DeleteAction::make('delete')
+                    ->modalHeading('Delete Notification Channel')
+                    ->authorize(fn (NotificationChannel $record) => auth()->user()->can('delete', $record))
+                    ->using(function (array $data, NotificationChannel $record) {
+                        //
+                    }),
+            ]);
+    }
+}
diff --git a/app/Web/Pages/Settings/Projects/Settings.php b/app/Web/Pages/Settings/Projects/Settings.php
index 4a4d338b..10e51e69 100644
--- a/app/Web/Pages/Settings/Projects/Settings.php
+++ b/app/Web/Pages/Settings/Projects/Settings.php
@@ -4,7 +4,7 @@
 
 use App\Actions\Projects\DeleteProject;
 use App\Models\Project;
-use App\Web\Pages\Servers\Page;
+use App\Web\Components\Page;
 use App\Web\Pages\Settings\Projects\Widgets\AddUser;
 use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
 use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
@@ -19,9 +19,11 @@ class Settings extends Page
 
     protected static ?string $title = 'Project Settings';
 
+    protected static bool $shouldRegisterNavigation = false;
+
     public static function canAccess(): bool
     {
-        return auth()->user()?->can('update', request()->route('project')) ?? false;
+        return auth()->user()?->can('update', get_from_route(Project::class, 'project')) ?? false;
     }
 
     public Project $project;
@@ -62,7 +64,7 @@ protected function getHeaderActions(): array
                     try {
                         app(DeleteProject::class)->delete(auth()->user(), $record);
 
-                        $this->redirectRoute('filament.app.resources.projects.index');
+                        $this->redirectRoute(Index::getUrl());
                     } catch (Exception $e) {
                         Notification::make()
                             ->title($e->getMessage())
diff --git a/app/Web/Pages/Settings/SSHKeys/Index.php b/app/Web/Pages/Settings/SSHKeys/Index.php
new file mode 100644
index 00000000..63576853
--- /dev/null
+++ b/app/Web/Pages/Settings/SSHKeys/Index.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Web\Pages\Settings\SSHKeys;
+
+use App\Actions\SshKey\CreateSshKey;
+use App\Models\SshKey;
+use App\Web\Components\Page;
+use Filament\Actions\CreateAction;
+use Filament\Forms\Components\Textarea;
+use Filament\Forms\Components\TextInput;
+use Filament\Support\Enums\MaxWidth;
+
+class Index extends Page
+{
+    protected static ?string $navigationGroup = 'Settings';
+
+    protected static ?string $slug = 'settings/ssh-keys';
+
+    protected static ?string $title = 'SSH Keys';
+
+    protected static ?string $navigationIcon = 'heroicon-o-key';
+
+    protected static ?int $navigationSort = 9;
+
+    public static function canAccess(): bool
+    {
+        return auth()->user()?->can('viewAny', SshKey::class) ?? false;
+    }
+
+    public function getWidgets(): array
+    {
+        return [
+            [Widgets\SshKeysList::class],
+        ];
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            CreateAction::make('add')
+                ->label('Add Key')
+                ->icon('heroicon-o-plus')
+                ->modalHeading('Add a new Key')
+                ->modalSubmitActionLabel('Add')
+                ->createAnother(false)
+                ->form([
+                    TextInput::make('name')
+                        ->label('Name')
+                        ->rules(CreateSshKey::rules()['name']),
+                    Textarea::make('public_key')
+                        ->label('Public Key')
+                        ->rules(CreateSshKey::rules()['public_key']),
+                ])
+                ->authorize('create', SshKey::class)
+                ->modalWidth(MaxWidth::Large)
+                ->using(function (array $data) {
+                    app(CreateSshKey::class)->create(auth()->user(), $data);
+
+                    $this->dispatch('$refresh');
+                }),
+        ];
+    }
+}
diff --git a/app/Web/Pages/Settings/SSHKeys/Widgets/SshKeysList.php b/app/Web/Pages/Settings/SSHKeys/Widgets/SshKeysList.php
new file mode 100644
index 00000000..2a7e5ef4
--- /dev/null
+++ b/app/Web/Pages/Settings/SSHKeys/Widgets/SshKeysList.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Web\Pages\Settings\SSHKeys\Widgets;
+
+use App\Models\SshKey;
+use Filament\Tables\Actions\DeleteAction;
+use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Table;
+use Filament\Widgets\TableWidget;
+use Illuminate\Database\Eloquent\Builder;
+
+class SshKeysList extends TableWidget
+{
+    protected $listeners = ['$refresh'];
+
+    protected function getTableQuery(): Builder
+    {
+        return SshKey::query()->where('user_id', auth()->id());
+    }
+
+    protected static ?string $heading = '';
+
+    protected function getTableColumns(): array
+    {
+        return [
+            TextColumn::make('name')
+                ->sortable()
+                ->searchable(),
+            TextColumn::make('public_key')
+                ->tooltip('Copy')
+                ->limit(20)
+                ->copyable(),
+            TextColumn::make('created_at')
+                ->sortable(),
+        ];
+    }
+
+    public function getTable(): Table
+    {
+        return $this->table
+            ->actions([
+                DeleteAction::make('delete')
+                    ->requiresConfirmation()
+                    ->authorize(fn (SshKey $record) => auth()->user()->can('delete', $record))
+                    ->action(function (SshKey $record) {
+                        run_action($this, function () use ($record) {
+                            $record->delete();
+                            $this->dispatch('$refresh');
+                        });
+                    }),
+            ]);
+    }
+}
diff --git a/app/Web/Pages/Settings/Tags/Index.php b/app/Web/Pages/Settings/Tags/Index.php
index 431201e5..fca9a642 100644
--- a/app/Web/Pages/Settings/Tags/Index.php
+++ b/app/Web/Pages/Settings/Tags/Index.php
@@ -18,7 +18,7 @@ class Index extends Page
 
     protected static ?string $navigationIcon = 'heroicon-o-tag';
 
-    protected static ?int $navigationSort = 7;
+    protected static ?int $navigationSort = 10;
 
     public static function canAccess(): bool
     {
diff --git a/database/migrations/2024_10_06_160213_add_soft_deletes_to_ssh_keys.php b/database/migrations/2024_10_06_160213_add_soft_deletes_to_ssh_keys.php
new file mode 100644
index 00000000..50b8092b
--- /dev/null
+++ b/database/migrations/2024_10_06_160213_add_soft_deletes_to_ssh_keys.php
@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('ssh_keys', function (Blueprint $table) {
+            $table->softDeletes();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('ssh_keys', function (Blueprint $table) {
+            $table->dropSoftDeletes();
+        });
+    }
+};
diff --git a/database/migrations/2024_10_06_162253_add_project_id_to_scripts.php b/database/migrations/2024_10_06_162253_add_project_id_to_scripts.php
new file mode 100644
index 00000000..3739255e
--- /dev/null
+++ b/database/migrations/2024_10_06_162253_add_project_id_to_scripts.php
@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('scripts', function (Blueprint $table) {
+            $table->unsignedInteger('project_id')->nullable();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('scripts', function (Blueprint $table) {
+            $table->dropColumn('project_id');
+        });
+    }
+};
diff --git a/database/migrations/2024_10_06_174115_add_server_id_to_script_executions.php b/database/migrations/2024_10_06_174115_add_server_id_to_script_executions.php
new file mode 100644
index 00000000..8dd0fc6c
--- /dev/null
+++ b/database/migrations/2024_10_06_174115_add_server_id_to_script_executions.php
@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('script_executions', function (Blueprint $table) {
+            $table->unsignedInteger('server_id')->nullable();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('script_executions', function (Blueprint $table) {
+            $table->dropColumn('server_id');
+        });
+    }
+};