Repository: LycheeOrg/Lychee (language: php)

Issue:
[Enhancement] Re-introduce the ability to host lychee in a subfolder of a domain
Hello
I have been hosting v4 of Lychee on a subfolder of my domain (e.g. https://mydomin.org/lychee/). 

However, after upgrading to Lychee v5 and enabling the new frontend, this is not longer working correctly and the subfolder seems to be ignored in some paths (mydomin.org/livewire/* instead of mydomin.org/lychee/livewire/*). 
After digging through other bug reports like #2120 I found that this type of configuration is not supported any more by the new front-end.

_Therefore, I wanted to make a request to reintroduce the possibility to host Lychee v5 in a subfolder._ 

Also, I think it would be nice to mention this new behavoir somewhere in the changelog, because for anyone using a setup like me, this is results in a breaking change.



Discussion / hints:

Fix patch:
diff --git a/.env.example b/.env.example
index fe8f76839ca..108e324240f 100644
--- a/.env.example
+++ b/.env.example
@@ -2,9 +2,17 @@ APP_NAME=Lychee
 APP_ENV=production
 APP_KEY=
 APP_DEBUG=false
+# This MUST contain the host name up to the Top Level Domain (tld) e.g. .com, .org etc.
 APP_URL=http://localhost
 APP_FORCE_HTTPS=false
 
+# If using Lychee in a sub folder, specify the path after the tld here.
+# For example for https://lychee.test/path/to/lychee
+# Set APP_URL=https://lychee.test
+# and APP_DIR=/path/to/lychee
+# We (LycheeOrg) do not recommend the use of APP_DIR.
+# APP_DIR=
+
 # enable or disable debug bar. By default it is disabled.
 # Do note that this disable CSP!!
 DEBUGBAR_ENABLED=false
@@ -37,7 +45,7 @@ DB_LOG_SQL=false
 DB_LOG_SQL_EXPLAIN=false #only for MySQL
 
 # List foreign keys in diagnostic page
-DB_LIST_FOREIGN_KEYS=true
+DB_LIST_FOREIGN_KEYS=false
 
 # Application timezone. If not specified, the server's default timezone is used.
 # Requires a named timezone identifier.
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 04e42082272..06656b298b5 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -5,6 +5,7 @@
 		__DIR__ . '/app/',
 		__DIR__ . '/database/',
 		__DIR__ . '/lang/',
+		__DIR__ . '/config/',
 		__DIR__ . '/resources/',
 		__DIR__ . '/routes/',
 		__DIR__ . '/tests/',
@@ -41,7 +42,7 @@ function (PhpCsFixer\Finder $finder, $dir) {
 	],
 	'operator_linebreak' => [
 		'only_booleans' => true,
-		'position' => 'end'
+		'position' => 'end',
 	],
 ];
 $config = new PhpCsFixer\Config();
diff --git a/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php b/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php
index 8566a616e16..0cf68859306 100644
--- a/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php
+++ b/app/Actions/Diagnostics/Pipes/Checks/AppUrlMatchCheck.php
@@ -3,11 +3,15 @@
 namespace App\Actions\Diagnostics\Pipes\Checks;
 
 use App\Contracts\DiagnosticPipe;
+use App\Facades\Helpers;
 use Safe\Exceptions\PcreException;
 use function Safe\preg_match;
 
 class AppUrlMatchCheck implements DiagnosticPipe
 {
+	public const INVISIBLE_ERROR = '      ';
+	public const INVISIBLE_WARNING = '        ';
+
 	/**
 	 * We check:
 	 * 1. if APP_URL is even set.
@@ -20,29 +24,47 @@ class AppUrlMatchCheck implements DiagnosticPipe
 	public function handle(array &$data, \Closure $next): array
 	{
 		$config_url = config('app.url');
+		$dir_url = config('app.dir_url');
 		if (config('app.url') === 'http://localhost') {
-			$data[] = 'Warning: APP_URL is still set to default, this will break access to all your images and assets if you are using Lychee behind a sub-domain.';
+			$data[] = 'Warning: APP_URL is still set to default, this will break access to all your images';
+			$data[] = self::INVISIBLE_WARNING . 'and assets if you are using Lychee behind a sub-domain.';
 		}
 
 		$bad = $this->splitUrl($config_url)[3];
 
-		$censored_bad = $this->censor($bad);
+		$censored_bad = Helpers::censor($bad);
 		$censored_app_url = $this->getCensorAppUrl();
 		$censored_current = $this->getCensorCurrentUrl();
 
 		if ($bad !== '') {
 			$data[] = sprintf(
-				'Warning: APP_URL (%s) contains a sub-path (%s). This may impact your WebAuthn authentication.',
+				'Error: APP_URL (%s) contains a sub-path (%s).',
+				$censored_app_url,
+				$censored_bad,
+			);
+			$data[] = sprintf(
+				self::INVISIBLE_ERROR . 'Instead set APP_DIR to (%s) and APP_URL to (%s) in your .env',
+				$censored_bad,
+				$censored_current,
+			);
+		}
+
+		if ($bad !== '') {
+			$data[] = sprintf(
+				'Warning: APP_URL (%s) contains a sub-path (%s).',
 				$censored_app_url,
-				$censored_bad);
+				$censored_bad
+			);
+			$data[] = self::INVISIBLE_WARNING . 'This may impact your WebAuthn authentication.';
 		}
 
 		if (!$this->checkUrlMatchCurrentHost()) {
 			$data[] = sprintf(
-				'Error: APP_URL (%s) does not match the current url (%s). This will break WebAuthn authentication.',
+				'Error: APP_URL (%s) does not match the current url (%s).',
 				$censored_app_url,
 				$censored_current,
 			);
+			$data[] = self::INVISIBLE_ERROR . 'This will break WebAuthn authentication.';
 		}
 
 		$config_url_imgage = config('filesystems.disks.images.url');
@@ -50,37 +72,18 @@ public function handle(array &$data, \Closure $next): array
 			$data[] = 'Error: LYCHEE_UPLOADS_URL is set and empty. This will prevent images to be displayed. Remove the line from your .env';
 		}
 
-		if (($config_url . '/uploads/') === $config_url_imgage && !$this->checkUrlMatchCurrentHost()) {
+		if (($config_url . $dir_url . '/uploads/') === $config_url_imgage && !$this->checkUrlMatchCurrentHost()) {
 			$data[] = sprintf(
-				'Error: APP_URL (%s) does not match the current url (%s). This will prevent images to be properly displayed.',
+				'Error: APP_URL (%s) does not match the current url (%s).',
 				$censored_app_url,
-				$censored_current);
+				$censored_current
+			);
+			$data[] = self::INVISIBLE_ERROR . 'This will prevent images from being properly displayed.';
 		}
 
 		return $next($data);
 	}
 
-	/**
-	 * Censore a word by replacing half of its character by stars.
-	 *
-	 * @param string $string
-	 *
-	 * @return string
-	 */
-	private function censor(string $string): string
-	{
-		$strLength = strlen($string);
-		if ($strLength === 0) {
-			return '';
-		}
-
-		$length = $strLength - (int) floor($strLength / 2);
-		$start = (int) floor($length / 2);
-		$replacement = str_repeat('*', $length);
-
-		return substr_replace($string, $replacement, $start, $length);
-	}
-
 	/**
 	 * Split url into 3 parts: http(s), host, path.
 	 *
@@ -110,7 +113,7 @@ private function getCensorCurrentUrl(): string
 		$current_url = request()->schemeAndHttpHost();
 		[$full, $prefix, $good, $bad] = $this->splitUrl($current_url);
 
-		return $prefix . $this->censor($good) . $this->censor($bad);
+		return $prefix . Helpers::censor($good) . Helpers::censor($bad);
 	}
 
 	/**
@@ -123,7 +126,7 @@ private function getCensorAppUrl(): string
 		$config_url = config('app.url');
 		[$full, $prefix, $good, $bad] = $this->splitUrl($config_url);
 
-		return $prefix . $this->censor($good) . $this->censor($bad);
+		return $prefix . Helpers::censor($good) . Helpers::censor($bad);
 	}
 
 	/**
diff --git a/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php b/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php
index 4ef6189280f..f8bbf827f05 100644
--- a/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php
+++ b/app/Actions/Diagnostics/Pipes/Checks/UpdatableCheck.php
@@ -85,7 +85,7 @@ public static function assertUpdatability(): void
 
 		if (!$gitHubFunctions->hasPermissions()) {
 			// @codeCoverageIgnoreStart
-			throw new InsufficientFilesystemPermissions(base_path('.git') . ' (and subdirectories) are not executable, check the permissions');
+			throw new InsufficientFilesystemPermissions(Helpers::censor(base_path('.git'), 1 / 4) . ' (and subdirectories) are not executable, check the permissions');
 			// @codeCoverageIgnoreEnd
 		}
 	}
diff --git a/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php b/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php
index 3e71f204ea8..4973b97c21b 100644
--- a/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php
+++ b/app/Actions/Diagnostics/Pipes/Infos/InstallTypeInfo.php
@@ -5,7 +5,6 @@
 use App\Actions\Diagnostics\Diagnostics;
 use App\Contracts\DiagnosticPipe;
 use App\Metadata\Versions\InstalledVersion;
-use Illuminate\Support\Facades\Config;
 
 /**
  * What kind of Lychee install we are looking at?
@@ -26,9 +25,12 @@ public function __construct(InstalledVersion $installedVersion)
 	public function handle(array &$data, \Closure $next): array
 	{
 		$data[] = Diagnostics::line('composer install:', $this->installedVersion->isDev() ? 'dev' : '--no-dev');
-		$data[] = Diagnostics::line('APP_ENV:', Config::get('app.env')); // check if production
-		$data[] = Diagnostics::line('APP_DEBUG:', Config::get('app.debug') === true ? 'true' : 'false'); // check if debug is on (will help in case of error 500)
-		$data[] = Diagnostics::line('APP_URL:', Config::get('app.url') !== 'http://localhost' ? 'set' : 'default'); // Some people leave that value by default... It is now breaking their visual.
+		$data[] = Diagnostics::line('APP_ENV:', config('app.env')); // check if production
+		$data[] = Diagnostics::line('APP_DEBUG:', config('app.debug') === true ? 'true' : 'false'); // check if debug is on (will help in case of error 500)
+		$data[] = Diagnostics::line('APP_URL:', config('app.url') !== 'http://localhost' ? 'set' : 'default'); // Some people leave that value by default... It is now breaking their visual.
+		$data[] = Diagnostics::line('APP_DIR:', config('app.dir_url') !== '' ? 'set' : 'default'); // Some people leave that value by default... It is now breaking their visual.
+		$data[] = Diagnostics::line('LOG_VIEWER_ENABLED:', config('log-viewer.enabled', true) === true ? 'true' : 'false');
+		$data[] = Diagnostics::line('LIVEWIRE_ENABLED:', config('app.livewire', true) === true ? 'true' : 'false');
 		$data[] = '';
 
 		return $next($data);
diff --git a/app/Assets/Helpers.php b/app/Assets/Helpers.php
index c0281d67dbd..c40634bf1f2 100644
--- a/app/Assets/Helpers.php
+++ b/app/Assets/Helpers.php
@@ -265,4 +265,30 @@ public function decimalToDegreeMinutesSeconds(float|null $decimal, bool $type):
 
 		return $degrees . '° ' . $minutes . "' " . $seconds . '" ' . $direction;
 	}
+
+	/**
+	 * Censor a word by replacing half of its character by stars.
+	 *
+	 * @param string $string         to censor
+	 * @param float  $percentOfClear the amount of the original string that remains untouched. The lower the value, the higher the censoring.
+	 *
+	 * @return string
+	 */
+	public function censor(string $string, float $percentOfClear = 0.5): string
+	{
+		$strLength = strlen($string);
+		if ($strLength === 0) {
+			return '';
+		}
+
+		// Length of replacement
+		$censored_length = $strLength - (int) floor($strLength * $percentOfClear);
+
+		// we leave half the space in front and behind.
+		$start = (int) floor(($strLength - $censored_length) / 2);
+
+		$replacement = str_repeat('*', $censored_length);
+
+		return substr_replace($string, $replacement, $start, $censored_length);
+	}
 }
diff --git a/app/Facades/Helpers.php b/app/Facades/Helpers.php
index ff77200cf8e..b935c88cf12 100644
--- a/app/Facades/Helpers.php
+++ b/app/Facades/Helpers.php
@@ -21,6 +21,7 @@
  * @method static string secondsToHMS(int|float $d)
  * @method static int    convertSize(string $size)
  * @method static string decimalToDegreeMinutesSeconds(float $decimal, bool $type)
+ * @method static string censor(string $string, float  $percentOfClear = 0.5)
  */
 class Helpers extends Facade
 {
diff --git a/app/Http/Middleware/DisableCSP.php b/app/Http/Middleware/DisableCSP.php
index d747858bccf..e189ac8012d 100644
--- a/app/Http/Middleware/DisableCSP.php
+++ b/app/Http/Middleware/DisableCSP.php
@@ -30,14 +30,15 @@ class DisableCSP
 	 */
 	public function handle(Request $request, \Closure $next): mixed
 	{
+		$dir_url = config('app.dir_url');
 		if (
 			config('debugbar.enabled', false) === true ||
-			$request->getRequestUri() === '/docs/api'
+			$request->getRequestUri() === $dir_url . '/docs/api'
 		) {
 			config(['secure-headers.csp.enable' => false]);
 		}
 
-		if ($request->getRequestUri() === '/' . config('log-viewer.route_path', 'Logs')) {
+		if ($request->getRequestUri() === $dir_url . '/' . config('log-viewer.route_path', 'Logs')) {
 			// We must disable unsafe-eval because vue3 used by log-viewer requires it.
 			// We must disable unsafe-inline (and hashes) because log-viewer uses inline script with parameter to boot.
 			// Those parameters are not know by Lychee if someone modifies the config.
@@ -48,7 +49,7 @@ public function handle(Request $request, \Closure $next): mixed
 		}
 
 		// disable unsafe-eval if we are on a Livewire page
-		if (config('app.livewire', false) === true || Str::startsWith($request->getRequestUri(), '/livewire/')) {
+		if (config('app.livewire', false) === true || Str::startsWith($request->getRequestUri(), $dir_url . '/livewire/')) {
 			$this->handleLivewire();
 		}
 
diff --git a/app/Livewire/Components/Menus/LeftMenu.php b/app/Livewire/Components/Menus/LeftMenu.php
index 5c6e4943262..d00d87778a9 100644
--- a/app/Livewire/Components/Menus/LeftMenu.php
+++ b/app/Livewire/Components/Menus/LeftMenu.php
@@ -35,6 +35,7 @@ class LeftMenu extends Component implements Openable
 	use InteractWithModal;
 	use UseOpenable;
 
+	#[Locked] public bool $has_log_viewer = false;
 	#[Locked] public bool $has_dev_tools = false;
 	#[Locked] public ?string $clockwork_url = null;
 	#[Locked] public ?string $doc_api_url = null;
@@ -61,6 +62,7 @@ public function logout(): void
 	public function render(): View
 	{
 		$this->loadDevMenu();
+		$this->has_log_viewer = config('log-viewer.enabled', true);
 
 		return view('livewire.components.left-menu');
 	}
diff --git a/app/Livewire/Components/Modules/Diagnostics/AbstractPreSection.php b/app/Livewire/Components/Modules/Diagnostics/AbstractPreSection.php
index ab4b70fedc5..45e833f3935 100644
--- a/app/Livewire/Components/Modules/Diagnostics/AbstractPreSection.php
+++ b/app/Livewire/Components/Modules/Diagnostics/AbstractPreSection.php
@@ -2,6 +2,9 @@
 
 namespace App\Livewire\Components\Modules\Diagnostics;
 
+use App\Models\Configs;
+use App\Policies\SettingsPolicy;
+use Illuminate\Support\Facades\Gate;
 use Illuminate\View\View;
 use Livewire\Attributes\Locked;
 use Livewire\Component;
@@ -11,7 +14,12 @@
  */
 abstract class AbstractPreSection extends Component
 {
-	#[Locked] public string $title;
+	#[Locked] public bool $can;
+	final public function boot(): void
+	{
+		$this->can = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class);
+	}
+
 	/**
 	 * Rendering of the front-end.
 	 *
@@ -25,17 +33,14 @@ final public function render(): View
 	/**
 	 * Defined the data to be displayed.
 	 *
-	 * @return array
+	 * @return array<int,string>
 	 */
 	abstract public function getDataProperty(): array;
 
 	/**
-	 * Return error message because we don't want this serialized.
+	 * Defined the title to be displayed.
 	 *
 	 * @return string
 	 */
-	public function getErrorMessageProperty(): string
-	{
-		return 'Error: You must have administrator rights to see this.';
-	}
+	abstract public function getTitleProperty(): string;
 }
diff --git a/app/Livewire/Components/Modules/Diagnostics/Configurations.php b/app/Livewire/Components/Modules/Diagnostics/Configurations.php
index 127647856ee..ccde9c5517a 100644
--- a/app/Livewire/Components/Modules/Diagnostics/Configurations.php
+++ b/app/Livewire/Components/Modules/Diagnostics/Configurations.php
@@ -3,16 +3,16 @@
 namespace App\Livewire\Components\Modules\Diagnostics;
 
 use App\Actions\Diagnostics\Configuration;
-use App\Models\Configs;
-use App\Policies\SettingsPolicy;
-use Illuminate\Support\Facades\Gate;
-use Livewire\Attributes\Locked;
 
 class Configurations extends AbstractPreSection
 {
-	#[Locked] public string $title = 'Config Information';
+	public function getTitleProperty(): string
+	{
+		return 'Config Information';
+	}
+
 	public function getDataProperty(): array
 	{
-		return Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class) ? resolve(Configuration::class)->get() : [];
+		return $this->can ? resolve(Configuration::class)->get() : [];
 	}
 }
diff --git a/app/Livewire/Components/Modules/Diagnostics/Errors.php b/app/Livewire/Components/Modules/Diagnostics/Errors.php
index 44ddf51a968..8a4fa855436 100644
--- a/app/Livewire/Components/Modules/Diagnostics/Errors.php
+++ b/app/Livewire/Components/Modules/Diagnostics/Errors.php
@@ -10,16 +10,20 @@
 
 class Errors extends Component
 {
-	#[Locked] public string $title = 'Diagnostics';
-	#[Locked] public string $error_msg = 'No critical problems found. Lychee should work without problems!';
+	#[Locked] public bool $can = true;
 	public function placeholder(): string
 	{
-		return '<p class="font-mono">
+		return '<div><pre class="font-mono">
 	Diagnostics
 	-----------
 	<span class="text-primary-500 font-bold">    ' . __('lychee.LOADING') . ' ...</span>
-</p>
-';
+</pre>
+</div>';
+	}
+
+	public function getTitleProperty(): string
+	{
+		return 'Diagnostics';
 	}
 
 	/**
diff --git a/app/Livewire/Components/Modules/Diagnostics/Infos.php b/app/Livewire/Components/Modules/Diagnostics/Infos.php
index e9f437cd5f5..e49393c5c4e 100644
--- a/app/Livewire/Components/Modules/Diagnostics/Infos.php
+++ b/app/Livewire/Components/Modules/Diagnostics/Infos.php
@@ -3,16 +3,16 @@
 namespace App\Livewire\Components\Modules\Diagnostics;
 
 use App\Actions\Diagnostics\Info;
-use App\Models\Configs;
-use App\Policies\SettingsPolicy;
-use Illuminate\Support\Facades\Gate;
-use Livewire\Attributes\Locked;
 
 class Infos extends AbstractPreSection
 {
-	#[Locked] public string $title = 'System Information';
+	public function getTitleProperty(): string
+	{
+		return 'System Information';
+	}
+
 	public function getDataProperty(): array
 	{
-		return Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class) ? resolve(Info::class)->get() : [];
+		return $this->can ? resolve(Info::class)->get() : [];
 	}
 }
diff --git a/app/Livewire/Components/Modules/Diagnostics/Optimize.php b/app/Livewire/Components/Modules/Diagnostics/Optimize.php
index f1753c97dc7..c28ec93c7cd 100644
--- a/app/Livewire/Components/Modules/Diagnostics/Optimize.php
+++ b/app/Livewire/Components/Modules/Diagnostics/Optimize.php
@@ -13,9 +13,9 @@
 
 class Optimize extends Component
 {
-	#[Locked] public string $title = 'Optimize DB';
 	#[Locked] public array $result = [];
 	#[Locked] public string $action;
+	#[Locked] public bool $can;
 	private OptimizeDb $optimizeDb;
 	private OptimizeTables $optimizeTables;
 
@@ -24,6 +24,12 @@ public function boot(): void
 		$this->optimizeDb = resolve(OptimizeDb::class);
 		$this->optimizeTables = resolve(OptimizeTables::class);
 		$this->action = 'Optimize!';
+		$this->can = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class);
+	}
+
+	public function getTitleProperty(): string
+	{
+		return 'Optimize DB';
 	}
 
 	/**
@@ -33,10 +39,6 @@ public function boot(): void
 	 */
 	public function render(): View
 	{
-		if (!Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class)) {
-			$this->result[] = 'Error: You must have administrator rights to see this.';
-		}
-
 		return view('livewire.modules.diagnostics.with-action-call');
 	}
 
diff --git a/app/Livewire/Components/Modules/Diagnostics/Space.php b/app/Livewire/Components/Modules/Diagnostics/Space.php
index cedb61f0dd8..a79bec5d308 100644
--- a/app/Livewire/Components/Modules/Diagnostics/Space.php
+++ b/app/Livewire/Components/Modules/Diagnostics/Space.php
@@ -12,15 +12,21 @@
 
 class Space extends Component
 {
-	#[Locked] public string $title = 'Space Usage';
 	#[Locked] public array $result = [];
 	#[Locked] public string $action;
+	#[Locked] public bool $can;
 	private DiagnosticsSpace $diagnostics;
 
 	public function boot(): void
 	{
 		$this->diagnostics = resolve(DiagnosticsSpace::class);
 		$this->action = __('lychee.DIAGNOSTICS_GET_SIZE');
+		$this->can = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class);
+	}
+
+	public function getTitleProperty(): string
+	{
+		return 'Space Usage';
 	}
 
 	/**
@@ -30,10 +36,6 @@ public function boot(): void
 	 */
 	public function render(): View
 	{
-		if (!Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, Configs::class)) {
-			$this->result[] = 'Error: You must have administrator rights to see this.';
-		}
-
 		return view('livewire.modules.diagnostics.with-action-call');
 	}
 
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index ebc70ad9ed7..d18ff406516 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -34,6 +34,7 @@
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Gate;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Route;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Support\Str;
@@ -164,6 +165,16 @@ public function boot()
 		foreach ($this->livewireSynth as $synth) {
 			Livewire::propertySynthesizer($synth);
 		}
+
+		$dir_url = config('app.dir_url') === '' ? '' : (config('app.dir_url') . '/');
+
+		Livewire::setScriptRoute(function ($handle) use ($dir_url) {
+			return Route::get($dir_url . 'livewire/livewire.js', $handle);
+		});
+
+		Livewire::setUpdateRoute(function ($handle) use ($dir_url) {
+			return Route::post($dir_url . 'livewire/update', $handle)->middleware('web');
+		});
 	}
 
 	/**
diff --git a/config/app.php b/config/app.php
index 9dbeaaebd5c..53bbecf9bc7 100644
--- a/config/app.php
+++ b/config/app.php
@@ -1,8 +1,24 @@
 <?php
 
 use Illuminate\Support\Facades\Facade;
+use function Safe\date;
 use function Safe\scandir;
 
+/**
+ * Given a .env config constant, retrieve the env value and remove any trailing /.
+ *
+ * @param string      $cst     constant to fetch
+ * @param string|null $default default value if does not exists
+ *
+ * @return string trimmed result
+ */
+if (!function_exists('renv')) {
+	function renv(string $cst, ?string $default = null): string
+	{
+		return rtrim(env($cst, $default) ?? '', '/');
+	}
+}
+
 return [
 	/*
 	|--------------------------------------------------------------------------
@@ -64,9 +80,15 @@
 	| the Artisan command line tool. You should set this to the root of
 	| your application so that it is used when running Artisan tasks.
 	|
+	| url : the base url of your Lychee install up to the tld (end '/' will be trimmed)
+	| dir_url : the path of your Lychee install from the tld (will be prefixed by '/' and end '/' will be trimmed)
+	|
+	| asset_url : should be left to default (null).
 	*/
 
-	'url' => env('APP_URL', 'http://localhost'),
+	'url' => renv('APP_URL', 'http://localhost'),
+
+	'dir_url' => env('APP_DIR', '') === '' ? '' : ('/' . trim(env('APP_DIR'), '/')),
 
 	'asset_url' => null,
 
diff --git a/config/auth.php b/config/auth.php
index e74531036e9..e3f3e405ca9 100644
--- a/config/auth.php
+++ b/config/auth.php
@@ -39,7 +39,7 @@
 
 	'guards' => [
 		'lychee' => [
-			'driver' => env('ENABLE_TOKEN_AUTH', true) ? 'session-or-token' : 'session',
+			'driver' => env('ENABLE_TOKEN_AUTH', true) ? 'session-or-token' : 'session', // @phpstan-ignore-line
 			'provider' => 'users',
 		],
 	],
diff --git a/config/broadcasting.php b/config/broadcasting.php
index c48467a6e31..64dbf68581a 100644
--- a/config/broadcasting.php
+++ b/config/broadcasting.php
@@ -34,7 +34,7 @@
 			'secret' => env('PUSHER_APP_SECRET'),
 			'app_id' => env('PUSHER_APP_ID'),
 			'options' => [
-				'host' => env('PUSHER_HOST', 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
+				'host' => env('PUSHER_HOST', 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com', // @phpstan-ignore-line
 				'port' => env('PUSHER_PORT', 443),
 				'scheme' => env('PUSHER_SCHEME', 'https'),
 				'encrypted' => true,
diff --git a/config/database.php b/config/database.php
index 9035cf09482..ac563f5c478 100644
--- a/config/database.php
+++ b/config/database.php
@@ -205,5 +205,6 @@
 		],
 	],
 
-	'list_foreign_keys' => env('DB_LIST_FOREIGN_KEYS', false),
+	// Only list fk keys in debug mode.
+	'list_foreign_keys' => (bool) env('DB_LIST_FOREIGN_KEYS', false) && (bool) env('APP_DEBUG', false),
 ];
diff --git a/config/filesystems.php b/config/filesystems.php
index 3e2dfb87d0f..176bb760554 100644
--- a/config/filesystems.php
+++ b/config/filesystems.php
@@ -1,5 +1,34 @@
 <?php
 
+/**
+ * Given a .env config constant, retrieve the env value and remove any trailing /.
+ *
+ * @param string      $cst     constant to fetch
+ * @param string|null $default default value if does not exists
+ *
+ * @return string trimmed result
+ */
+if (!function_exists('renv')) {
+	function renv(string $cst, ?string $default = null): string
+	{
+		return rtrim(env($cst, $default) ?? '', '/');
+	}
+}
+
+/**
+ * Allow to conditionally append an env value.
+ *
+ * @param string $cst constant to fetch
+ *
+ * @return string '' or env value prefixed with '/'
+ */
+if (!function_exists('renv_cond')) {
+	function renv_cond(string $cst): string
+	{
+		return env($cst, '') === '' ? '' : ('/' . trim(env($cst), '/'));
+	}
+}
+
 return [
 	/*
 	|--------------------------------------------------------------------------
@@ -45,7 +74,9 @@
 		'images' => [
 			'driver' => 'local',
 			'root' => env('LYCHEE_UPLOADS', public_path(env('LYCHEE_UPLOADS_DIR', 'uploads/'))),
-			'url' => env('LYCHEE_UPLOADS_URL', env('APP_URL', 'http://localhost') . '/' . env('LYCHEE_UPLOADS_DIR', 'uploads/')),
+			'url' => env('LYCHEE_UPLOADS_URL', '') !== '' ? renv('LYCHEE_UPLOADS_URL')
+				: (renv('APP_URL', 'http://localhost') . renv_cond('APP_DIR') . '/' .
+					renv('LYCHEE_UPLOADS_DIR', 'uploads')),
 			'visibility' => env('LYCHEE_IMAGE_VISIBILITY', 'public'),
 			'directory_visibility' => env('LYCHEE_IMAGE_VISIBILITY', 'public'),
 			'permissions' => [
@@ -96,7 +127,8 @@
 		'symbolic' => [
 			'driver' => 'local',
 			'root' => env('LYCHEE_SYM', public_path('sym')),
-			'url' => rtrim(env('LYCHEE_SYM_URL', rtrim(env('APP_URL', 'http://localhost'), '/') . '/sym'), '/'),
+			'url' => env('LYCHEE_SYM_URL', '') !== '' ? renv('LYCHEE_SYM_URL') :
+				(renv('APP_URL', 'http://localhost') . renv_cond('APP_DIR') . '/sym'),
 			'visibility' => 'public',
 		],
 
diff --git a/config/honeypot.php b/config/honeypot.php
index d41582474d3..0628e681986 100644
--- a/config/honeypot.php
+++ b/config/honeypot.php
@@ -114,6 +114,6 @@
 				'info.php',
 				'phpinfo.php',
 			],
-		]
+		],
 	],
 ];
\ No newline at end of file
diff --git a/config/log-viewer.php b/config/log-viewer.php
index 27f4b036c5e..2450f9b545c 100644
--- a/config/log-viewer.php
+++ b/config/log-viewer.php
@@ -2,6 +2,27 @@
 
 use Opcodes\LogViewer\LogLevels\LevelClass;
 
+if (!function_exists('renv')) {
+	function renv(string $cst, ?string $default = null): string
+	{
+		return rtrim(env($cst, $default) ?? '', '/');
+	}
+}
+
+/**
+ * Allow to conditionally append an env value.
+ *
+ * @param string $cst constant to fetch
+ *
+ * @return string '' or env value prefixed with '/'
+ */
+if (!function_exists('renv_cond')) {
+	function renv_cond(string $cst): string
+	{
+		return env($cst, '') === '' ? '' : ('/' . trim(env($cst), '/'));
+	}
+}
+
 return [
 	/*
 	|--------------------------------------------------------------------------
@@ -45,7 +66,7 @@
 	|
 	*/
 
-	'back_to_system_url' => '../', // config('app.url', null),
+	'back_to_system_url' => renv('APP_URL', 'http://localhost') . renv_cond('APP_DIR'),
 
 	'back_to_system_label' => null, // Displayed by default: "Back to {{ app.name }}"
 
diff --git a/config/markdown.php b/config/markdown.php
index a2f2a5de05f..01ac337615b 100644
--- a/config/markdown.php
+++ b/config/markdown.php
@@ -12,145 +12,143 @@
  */
 
 return [
-
-    /*
-    |--------------------------------------------------------------------------
-    | Enable View Integration
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies if the view integration is enabled so you can write
-    | markdown views and have them rendered as html. The following extensions
-    | are currently supported: ".md", ".md.php", and ".md.blade.php". You may
-    | disable this integration if it is conflicting with another package.
-    |
-    | Default: true
-    |
-    */
-
-    'views' => true,
-
-    /*
-    |--------------------------------------------------------------------------
-    | CommonMark Extensions
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies what extensions will be automatically enabled.
-    | Simply provide your extension class names here.
-    |
-    | Default: [
-    |              League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
-    |              League\CommonMark\Extension\Table\TableExtension::class,
-    |          ]
-    |
-    */
-
-    'extensions' => [
-        League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
-        League\CommonMark\Extension\Table\TableExtension::class,
-    ],
-
-    /*
-    |--------------------------------------------------------------------------
-    | Renderer Configuration
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies an array of options for rendering HTML.
-    |
-    | Default: [
-    |              'block_separator' => "\n",
-    |              'inner_separator' => "\n",
-    |              'soft_break'      => "\n",
-    |          ]
-    |
-    */
-
-    'renderer' => [
-        'block_separator' => "\n",
-        'inner_separator' => "\n",
-        'soft_break'      => "\n",
-    ],
-
-    /*
-    |--------------------------------------------------------------------------
-    | Commonmark Configuration
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies an array of options for commonmark.
-    |
-    | Default: [
-    |              'enable_em' => true,
-    |              'enable_strong' => true,
-    |              'use_asterisk' => true,
-    |              'use_underscore' => true,
-    |              'unordered_list_markers' => ['-', '+', '*'],
-    |          ]
-    |
-    */
-
-    'commonmark' => [
-        'enable_em'              => true,
-        'enable_strong'          => true,
-        'use_asterisk'           => true,
-        'use_underscore'         => true,
-        'unordered_list_markers' => ['-', '+', '*'],
-    ],
-
-    /*
-    |--------------------------------------------------------------------------
-    | HTML Input
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies how to handle untrusted HTML input.
-    |
-    | Default: 'strip'
-    |
-    */
-
-    'html_input' => 'strip',
-
-    /*
-    |--------------------------------------------------------------------------
-    | Allow Unsafe Links
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies whether to allow risky image URLs and links.
-    |
-    | Default: true
-    |
-    */
-
-    'allow_unsafe_links' => true,
-
-    /*
-    |--------------------------------------------------------------------------
-    | Maximum Nesting Level
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies the maximum permitted block nesting level.
-    |
-    | Default: PHP_INT_MAX
-    |
-    */
-
-    'max_nesting_level' => PHP_INT_MAX,
-
-    /*
-    |--------------------------------------------------------------------------
-    | Slug Normalizer
-    |--------------------------------------------------------------------------
-    |
-    | This option specifies an array of options for slug normalization.
-    |
-    | Default: [
-    |              'max_length' => 255,
-    |              'unique' => 'document',
-    |          ]
-    |
-    */
-
-    'slug_normalizer' => [
-        'max_length' => 255,
-        'unique'     => 'document',
-    ],
-
+	/*
+	|--------------------------------------------------------------------------
+	| Enable View Integration
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies if the view integration is enabled so you can write
+	| markdown views and have them rendered as html. The following extensions
+	| are currently supported: ".md", ".md.php", and ".md.blade.php". You may
+	| disable this integration if it is conflicting with another package.
+	|
+	| Default: true
+	|
+	*/
+
+	'views' => true,
+
+	/*
+	|--------------------------------------------------------------------------
+	| CommonMark Extensions
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies what extensions will be automatically enabled.
+	| Simply provide your extension class names here.
+	|
+	| Default: [
+	|              League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
+	|              League\CommonMark\Extension\Table\TableExtension::class,
+	|          ]
+	|
+	*/
+
+	'extensions' => [
+		League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
+		League\CommonMark\Extension\Table\TableExtension::class,
+	],
+
+	/*
+	|--------------------------------------------------------------------------
+	| Renderer Configuration
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies an array of options for rendering HTML.
+	|
+	| Default: [
+	|              'block_separator' => "\n",
+	|              'inner_separator' => "\n",
+	|              'soft_break'      => "\n",
+	|          ]
+	|
+	*/
+
+	'renderer' => [
+		'block_separator' => "\n",
+		'inner_separator' => "\n",
+		'soft_break' => "\n",
+	],
+
+	/*
+	|--------------------------------------------------------------------------
+	| Commonmark Configuration
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies an array of options for commonmark.
+	|
+	| Default: [
+	|              'enable_em' => true,
+	|              'enable_strong' => true,
+	|              'use_asterisk' => true,
+	|              'use_underscore' => true,
+	|              'unordered_list_markers' => ['-', '+', '*'],
+	|          ]
+	|
+	*/
+
+	'commonmark' => [
+		'enable_em' => true,
+		'enable_strong' => true,
+		'use_asterisk' => true,
+		'use_underscore' => true,
+		'unordered_list_markers' => ['-', '+', '*'],
+	],
+
+	/*
+	|--------------------------------------------------------------------------
+	| HTML Input
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies how to handle untrusted HTML input.
+	|
+	| Default: 'strip'
+	|
+	*/
+
+	'html_input' => 'strip',
+
+	/*
+	|--------------------------------------------------------------------------
+	| Allow Unsafe Links
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies whether to allow risky image URLs and links.
+	|
+	| Default: true
+	|
+	*/
+
+	'allow_unsafe_links' => true,
+
+	/*
+	|--------------------------------------------------------------------------
+	| Maximum Nesting Level
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies the maximum permitted block nesting level.
+	|
+	| Default: PHP_INT_MAX
+	|
+	*/
+
+	'max_nesting_level' => PHP_INT_MAX,
+
+	/*
+	|--------------------------------------------------------------------------
+	| Slug Normalizer
+	|--------------------------------------------------------------------------
+	|
+	| This option specifies an array of options for slug normalization.
+	|
+	| Default: [
+	|              'max_length' => 255,
+	|              'unique' => 'document',
+	|          ]
+	|
+	*/
+
+	'slug_normalizer' => [
+		'max_length' => 255,
+		'unique' => 'document',
+	],
 ];
\ No newline at end of file
diff --git a/config/view.php b/config/view.php
index 13399a3a2b6..4576eb17a9d 100644
--- a/config/view.php
+++ b/config/view.php
@@ -27,5 +27,5 @@
 	|
 	*/
 
-	'compiled' => realpath(storage_path('framework/views')),
+	'compiled' => realpath(storage_path('framework/views')), // @phpstan-ignore-line
 ];
diff --git a/phpstan.neon b/phpstan.neon
index e2b0081c3d8..1d63ac31f7f 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -11,6 +11,7 @@ parameters:
 		- lang
 		- database/migrations
 		- tests
+		- config
 	excludePaths:
 		- database/migrations/2021_12_04_181200_refactor_models.php
 	stubFiles:
@@ -106,6 +107,12 @@ parameters:
 			paths:
 				- database/migrations
 
+		# Configs
+		-
+			message: '#Cast to bool is forbidden.#'
+			paths:
+				- config
+
 		# TESTS
 		- 
 			message: '#Dynamic call to static method Illuminate\\Testing\\TestResponse::assert(Forbidden|ViewIs|Unauthorized|Ok|Status)#'
diff --git a/resources/views/components/leftbar/disabled-leftbar-item.blade.php b/resources/views/components/leftbar/disabled-leftbar-item.blade.php
new file mode 100644
index 00000000000..e9a894ac94f
--- /dev/null
+++ b/resources/views/components/leftbar/disabled-leftbar-item.blade.php
@@ -0,0 +1,10 @@
+@props(['icon'])
+<li class="px-4">
+    <a
+        class="p-2 py-1 cursor-not-allowed rounded-lg text-text-main-400 whitespace-nowrap">
+		<x-icons.iconic icon="{{ $icon }}" class="w-3 h-3" />
+        <span class="ml-3 whitespace-nowrap text-base italic line-through">
+            {{ $slot }}
+        </span>
+    </a>
+</li>
diff --git a/resources/views/components/leftbar/leftbar-item.blade.php b/resources/views/components/leftbar/leftbar-item.blade.php
index d6133766d71..2cbfb608163 100644
--- a/resources/views/components/leftbar/leftbar-item.blade.php
+++ b/resources/views/components/leftbar/leftbar-item.blade.php
@@ -2,7 +2,7 @@
 <li class="px-4">
     <a
         {{ $attributes }}
-        class="p-2 py-1 cursor-pointer rounded-lg text-text-main-400 hover:text-text-main-100 whitespace-nowrap ">
+        class="p-2 py-1 cursor-pointer rounded-lg text-text-main-400 hover:text-text-main-100 whitespace-nowrap">
 		<x-icons.iconic icon="{{ $icon }}" class="w-3 h-3" />
         <span class="ml-3 whitespace-nowrap text-base">
             {{ $slot }}
diff --git a/resources/views/livewire/components/left-menu.blade.php b/resources/views/livewire/components/left-menu.blade.php
index fdb64445af1..57870d92f54 100644
--- a/resources/views/livewire/components/left-menu.blade.php
+++ b/resources/views/livewire/components/left-menu.blade.php
@@ -1,5 +1,5 @@
 <aside id="default-sidebar" x-data="{ leftMenuOpen: @entangle('isOpen') }" x-bind:class="!leftMenuOpen ? '' : 'w-full sm:w-[250px]'"
-    class="z-40 w-0 h-screen transition-width duration-500" aria-label="Sidebar">
+    class="z-40 w-0 h-screen transition-width duration-500 select-none" aria-label="Sidebar">
     <div class="h-full py-4 overflow-y-auto bg-bg-900">
         <ul class="space-y-0.5">
             <x-leftbar.leftbar-item x-on:click="leftMenuOpen = ! leftMenuOpen" icon="chevron-left">
@@ -30,9 +30,15 @@ class="z-40 w-0 h-screen transition-width duration-500" aria-label="Sidebar">
                     {{ __('lychee.SHARING') }}</x-leftbar.leftbar-item>
             @endcan
             @can(SettingsPolicy::CAN_SEE_LOGS, [App\Models\Configs::class])
+                @if($has_log_viewer)
                 <x-leftbar.leftbar-item href="{{ route('log-viewer.index') }}" icon="excerpt">
                     {{ __('lychee.LOGS') }}
                 </x-leftbar.leftbar-item>
+                @else
+                <x-leftbar.disabled-leftbar-item icon="excerpt">
+                    {{ __('lychee.LOGS') }}
+                </x-leftbar.disabled-leftbar-item>
+                @endif
                 <x-leftbar.leftbar-item href="{{ route('jobs') }}" wire:navigate icon="project">
                     {{ __('lychee.JOBS') }}
                 </x-leftbar.leftbar-item>
diff --git a/resources/views/livewire/modules/diagnostics/pre-colored.blade.php b/resources/views/livewire/modules/diagnostics/pre-colored.blade.php
index 4d7f7a21cd0..2e427d3b747 100644
--- a/resources/views/livewire/modules/diagnostics/pre-colored.blade.php
+++ b/resources/views/livewire/modules/diagnostics/pre-colored.blade.php
@@ -1,9 +1,11 @@
-<p class="font-mono">
-    {{ $title }}
-    {{ str_repeat('-', Str::length($title)) }}
-    @forelse($this->data as $line)
+<div>
+@if($can)
+<pre class="font-mono">
+    {{ $this->title }}
+    {{ str_repeat('-', Str::length($this->title)) }}
+    @foreach($this->data as $line)
     <span class="{{ $line['color'] }}">{{ $line['type'] }}</span> {{ $line['line']  }}
-    @empty
-    {{ $error_msg }}
-    @endforelse
-</p>
\ No newline at end of file
+    @endforeach
+</pre>
+@endif
+</div>
\ No newline at end of file
diff --git a/resources/views/livewire/modules/diagnostics/pre.blade.php b/resources/views/livewire/modules/diagnostics/pre.blade.php
index 98a0761dbb8..88211022d9f 100644
--- a/resources/views/livewire/modules/diagnostics/pre.blade.php
+++ b/resources/views/livewire/modules/diagnostics/pre.blade.php
@@ -1,9 +1,11 @@
-<p class="font-mono">
-    {{ $title }}
-    {{ str_repeat('-', Str::length($title)) }}
-    @forelse($this->data as $line)
+<div>
+@if($can)
+<pre class="font-mono">
+    {{ $this->title }}
+    {{ str_repeat('-', Str::length($this->title)) }}
+    @foreach($this->data as $line)
     {{ $line }}
-    @empty
-    {{ $this->errorMessage }}
-    @endforelse
-</p>
\ No newline at end of file
+    @endforeach
+</pre>
+@endif
+</div>
\ No newline at end of file
diff --git a/resources/views/livewire/modules/diagnostics/with-action-call.blade.php b/resources/views/livewire/modules/diagnostics/with-action-call.blade.php
index bdf34595ec8..1d69cb2f06d 100644
--- a/resources/views/livewire/modules/diagnostics/with-action-call.blade.php
+++ b/resources/views/livewire/modules/diagnostics/with-action-call.blade.php
@@ -1,6 +1,8 @@
-<div><p class="font-mono">
-    {{ $title }}
-    {{ str_repeat('-', Str::length($title)) }}
+<div class="select-none">
+@if($can)
+<pre class="font-mono">
+    {{ $this->title }}
+    {{ str_repeat('-', Str::length($this->title)) }}
 @if(count($result) > 0)@foreach ($result as $resultLine)
         {{ $resultLine }}
 @endforeach
@@ -13,4 +15,5 @@
 ">{{ $action }}</a><span wire:loading class="text-primary-500 font-bold">{{ __('lychee.LOADING') }} ...</span>
 @endif
 </p>
+@endif
 </div>
\ No newline at end of file
diff --git a/resources/views/livewire/pages/diagnostics.blade.php b/resources/views/livewire/pages/diagnostics.blade.php
index 9c5f1960be2..dca7b5893c1 100644
--- a/resources/views/livewire/pages/diagnostics.blade.php
+++ b/resources/views/livewire/pages/diagnostics.blade.php
@@ -1,15 +1,15 @@
 <div class="w-full">
-    <x-header.bar>
+    <x-header.bar class="h-14 select-none">
         <x-header.back @keydown.escape.window="$wire.back();" wire:click="back" />
         <x-header.title>{{ __('lychee.DIAGNOSTICS') }}</x-header.title>
     </x-header.bar>
-	<div class="overflow-x-clip overflow-y-auto h-[calc(100vh-56px)]">
-<pre class="logs_diagnostics_view text-text-main-200 text-xs mx-8">
-<livewire:modules.diagnostics.errors lazy />
-<livewire:modules.diagnostics.infos />
-<livewire:modules.diagnostics.space />
-<livewire:modules.diagnostics.optimize />
-<livewire:modules.diagnostics.configurations />
-</pre>
+	<div class="overflow-x-clip overflow-y-auto h-[calc(100vh-56px)] pt-9">
+        <div class="logs_diagnostics_view text-text-main-200 text-xs mx-8">
+        <livewire:modules.diagnostics.errors lazy />
+        <livewire:modules.diagnostics.infos />
+        <livewire:modules.diagnostics.space />
+        <livewire:modules.diagnostics.optimize />
+        <livewire:modules.diagnostics.configurations />
+        </div>
 	</div>
 </div>
\ No newline at end of file


Test patch:
diff --git a/tests/Livewire/Modules/SpaceTest.php b/tests/Livewire/Modules/SpaceTest.php
index 4f278e2d35e..14d229a5764 100644
--- a/tests/Livewire/Modules/SpaceTest.php
+++ b/tests/Livewire/Modules/SpaceTest.php
@@ -34,7 +34,7 @@ public function testLoggedOut(): void
 	{
 		Livewire::test($this->component)
 			->assertViewIs('livewire.modules.diagnostics.with-action-call')
-			->assertSee('Error: You must have administrator rights to see this.')
+			->assertSet('can', false)
 			->assertOk();
 	}
 
@@ -42,10 +42,9 @@ public function testLoggedIn(): void
 	{
 		Livewire::actingAs($this->admin)->test($this->component)
 			->assertViewIs('livewire.modules.diagnostics.with-action-call')
-			->assertDontSee('Error: You must have administrator rights to see this.')
+			->assertSet('can', true)
 			->assertOk()
 			->call('do')
-			->assertOk()
-			->assertDontSee('Error: You must have administrator rights to see this.');
+			->assertOk();
 	}
 }
diff --git a/tests/Livewire/Pages/DiagnosticsTest.php b/tests/Livewire/Pages/DiagnosticsTest.php
index 67c6924c114..0683f3688a4 100644
--- a/tests/Livewire/Pages/DiagnosticsTest.php
+++ b/tests/Livewire/Pages/DiagnosticsTest.php
@@ -15,6 +15,7 @@
 use App\Livewire\Components\Modules\Diagnostics\Configurations;
 use App\Livewire\Components\Modules\Diagnostics\Errors;
 use App\Livewire\Components\Modules\Diagnostics\Infos;
+use App\Livewire\Components\Modules\Diagnostics\Optimize;
 use App\Livewire\Components\Modules\Diagnostics\Space;
 use App\Livewire\Components\Pages\Diagnostics;
 use Livewire\Livewire;
@@ -26,24 +27,28 @@ public function testDiagnosticsLoggedOut(): void
 	{
 		Livewire::test(Diagnostics::class)
 			->assertViewIs('livewire.pages.diagnostics')
+			->assertSeeLivewire(Errors::class)
 			->assertSeeLivewire(Configurations::class)
 			->assertSeeLivewire(Space::class)
+			->assertSeeLivewire(Optimize::class)
 			->assertSeeLivewire(Infos::class)
-			->assertSeeInOrder([
-				'Error: You must have administrator rights to see this.',
-				'Error: You must have administrator rights to see this.',
-				'Error: You must have administrator rights to see this.']);
+			->assertSee('Diagnostics')
+			->assertDontSee('System Information')
+			->assertDontSee('Space Usage')
+			->assertDontSee('Optimize DB')
+			->assertDontSee('Config Information');
 	}
 
 	public function testDiagnosticsLogged(): void
 	{
 		$diagnostics = Livewire::actingAs($this->admin)->test(Diagnostics::class, ['lazy' => false])
 			->assertViewIs('livewire.pages.diagnostics')
-			->assertSeeLivewire(Configurations::class)
 			->assertSeeLivewire(Errors::class)
+			->assertSeeLivewire(Configurations::class)
 			->assertSeeLivewire(Space::class)
+			->assertSeeLivewire(Optimize::class)
 			->assertSeeLivewire(Infos::class)
-			->assertDontSee('Error: You must have administrator rights to see this.');
+			->assertSeeInOrder(['Diagnostics', 'System Information', 'Space Usage', 'Optimize DB', 'Config Information']);
 
 		$diagnostics->dispatch('reloadPage')
 			->assertStatus(200);