Repository: unraid/api
Pull request #1368: [WIP] fix: authentication when booting into GUI mode
Author: login: pujitm
Created: 2025-04-24T15:53:57Z | Merged: 2025-05-01T03:31:26Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Added support for Single Sign-On (SSO) authentication during login.
	- Introduced a new notification toaster component for improved alert display.
	- Added detection of GUI mode to conditionally enhance session handling.
- **Bug Fixes**
	- Simplified login flow by removing all two-factor authentication (2FA) requirements and related UI elements.
- **Improvements**
	- Enhanced session initialization to ensure smoother access in specific scenarios.
	- Updated page reload behavior on tab/window focus based on server configuration.
	- Removed legacy notification pop-ups for a cleaner interface.
- **Chores**
	- Updated internal timestamps and added a "prepare" script to streamline build processes.
	- Removed execution of the unraid-ui setup step from the main setup recipe.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg
index 42c67a36b0..1c664dad22 100644
--- a/api/dev/Unraid.net/myservers.cfg
+++ b/api/dev/Unraid.net/myservers.cfg
@@ -1,5 +1,5 @@
 [api]
-version="4.6.6"
+version="4.4.1"
 extraOrigins="https://google.com,https://test.com"
 [local]
 sandbox="yes"
diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg
index 42ccb6be12..103160cf35 100644
--- a/api/dev/states/myservers.cfg
+++ b/api/dev/states/myservers.cfg
@@ -1,5 +1,5 @@
 [api]
-version="4.6.6"
+version="4.7.0"
 extraOrigins="https://google.com,https://test.com"
 [local]
 sandbox="yes"
diff --git a/api/src/core/utils/validation/is-gui-mode.ts b/api/src/core/utils/validation/is-gui-mode.ts
new file mode 100644
index 0000000000..e7165ef6cd
--- /dev/null
+++ b/api/src/core/utils/validation/is-gui-mode.ts
@@ -0,0 +1,21 @@
+import { execa } from 'execa';
+
+import { internalLogger } from '@app/core/log.js';
+
+/**
+ * Check if Unraid is in GUI mode by looking for the slim process.
+ * @returns true if Unraid is in GUI mode, false otherwise.
+ */
+const isGuiMode = async (): Promise<boolean> => {
+    try {
+        // Use pgrep to check if slim process is running
+        const { exitCode } = await execa('pgrep', ['slim'], { reject: false });
+        // exitCode 0 means process was found, 1 means not found
+        return exitCode === 0;
+    } catch (error) {
+        internalLogger.error('Error checking GUI mode: %s', error);
+        return false;
+    }
+};
+
+export default isGuiMode;
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php
index 73b6b554ba..e6367dcbd4 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php
@@ -2,10 +2,12 @@
 // Included in login.php
 
 // Only start a session to check if they have a cookie that looks like our session
-$server_name = strtok($_SERVER['HTTP_HOST'],":");
+$server_name = strtok($_SERVER['HTTP_HOST'], ":");
 if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
     // Start the session so we can check if $_SESSION has data
-    if (session_status()==PHP_SESSION_NONE) session_start();
+    if (session_status() == PHP_SESSION_NONE) {
+        session_start();
+    }
 
     // Check if the user is already logged in
     if ($_SESSION && !empty($_SESSION['unraid_user'])) {
@@ -15,10 +17,11 @@
     }
 }
 
-function readFromFile($file): string {
+function readFromFile($file): string
+{
     $text = "";
     if (file_exists($file) && filesize($file) > 0) {
-        $fp = fopen($file,"r");
+        $fp = fopen($file, "r");
         if (flock($fp, LOCK_EX)) {
             $text = fread($fp, filesize($file));
             flock($fp, LOCK_UN);
@@ -28,8 +31,9 @@ function readFromFile($file): string {
     return $text;
 }
 
-function appendToFile($file, $text): void {
-    $fp = fopen($file,"a");
+function appendToFile($file, $text): void
+{
+    $fp = fopen($file, "a");
     if (flock($fp, LOCK_EX)) {
         fwrite($fp, $text);
         fflush($fp);
@@ -38,8 +42,9 @@ function appendToFile($file, $text): void {
     }
 }
 
-function writeToFile($file, $text): void {
-    $fp = fopen($file,"w");
+function writeToFile($file, $text): void
+{
+    $fp = fopen($file, "w");
     if (flock($fp, LOCK_EX)) {
         fwrite($fp, $text);
         fflush($fp);
@@ -56,7 +61,8 @@ function isValidTimeStamp($timestamp)
         && ($timestamp >= ~PHP_INT_MAX);
 }
 
-function cleanupFails(string $failFile, int $time): int {
+function cleanupFails(string $failFile, int $time): int
+{
     global $cooldown;
 
     // Read existing fails
@@ -67,8 +73,8 @@ function cleanupFails(string $failFile, int $time): int {
     // Remove entries older than $cooldown minutes, and entries that are not timestamps
     $updateFails = false;
     foreach ((array) $fails as $key => $value) {
-        if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) {
-            unset ($fails[$key]);
+        if (!isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time)) {
+            unset($fails[$key]);
             $updateFails = true;
         }
     }
@@ -81,94 +87,19 @@ function cleanupFails(string $failFile, int $time): int {
     return count($fails);
 }
 
-function verifyUsernamePassword(string $username, string $password): bool {
-    if ($username != "root") return false;
+function verifyUsernamePassword(string $username, string $password): bool
+{
+    if ($username != "root") {
+        return false;
+    }
 
     $output = exec("/usr/bin/getent shadow $username");
-    if ($output === false) return false;
-    $credentials = explode(":", $output);
-    return password_verify($password, $credentials[1]);
-}
-
-function verifyTwoFactorToken(string $username, string $token): bool {
-    try {
-        // Create curl client
-        $curlClient = curl_init();
-        curl_setopt($curlClient, CURLOPT_HEADER, true);
-        curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock');
-        curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify');
-        curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256);
-        curl_setopt($curlClient, CURLOPT_TIMEOUT, 5);
-        curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock'));
-        curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([
-            'username' => $username,
-            'token' => $token
-        ]));
-
-        // Send the request
-        curl_exec($curlClient);
-
-        // Get the http status code
-        $httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE);
-
-        // Close the connection
-        curl_close($curlClient);
-
-        // Error
-        // This should accept 200 or 204 status codes
-        if ($httpCode !== 200 && $httpCode !== 204) {
-            // Log error to syslog
-            my_logger("2FA code for {$username} is invalid, blocking access!");
-            return false;
-        }
-
-        // Log success to syslog
-        my_logger("2FA code for {$username} is valid, allowing login!");
-
-        // Success
-        return true;
-    } catch (Exception $exception) {
-        // Error
+    if ($output === false) {
         return false;
     }
+    $credentials = explode(":", $output);
+    return password_verify($password, $credentials[1]);
 }
-
-// Check if a haystack ends in a needle
-function endsWith($haystack, $needle): bool {
-    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
-}
-
-// Check if we're accessing this via a wildcard cert
-function isWildcardCert(): bool {
-    global $server_name;
-    return endsWith($server_name, '.myunraid.net');
-}
-
-// Check if we're accessing this locally via the expected myunraid.net url
-function isLocalAccess(): bool {
-    global $nginx, $server_name;
-    return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name;
-}
-
-// Check if we're accessing this remotely via the expected myunraid.net url
-function isRemoteAccess(): bool {
-    global $nginx, $server_name;
-    return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name;
-}
-
-// Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server)
-function isLocalTwoFactorEnabled(): bool {
-    global $nginx, $my_servers;
-    return $nginx['NGINX_USESSL'] === "auto" && ($my_servers['local']['2Fa']??'') === 'yes';
-}
-
-// Check if 2fa is enabled for remote
-function isRemoteTwoFactorEnabled(): bool {
-    global $my_servers;
-    return ($my_servers['remote']['2Fa']??'') === 'yes';
-}
-
 // Load configs into memory
 $my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
 $nginx = @parse_ini_file('/var/local/emhttp/nginx.ini');
@@ -180,38 +111,34 @@ function isRemoteTwoFactorEnabled(): bool {
 $failFile = "/var/log/pwfail/{$remote_addr}";
 
 // Get the credentials
-$username = $_POST['username']??'';
-$password = $_POST['password']??'';
-$token = $_REQUEST['token']??'';
-
-// Check if we need 2fa
-$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled());
+$username = $_POST['username'] ?? '';
+$password = $_POST['password'] ?? '';
 
 // If we have a username + password combo attempt to login
 if (!empty($username) && !empty($password)) {
     try {
-        // Bail if we're missing the 2FA token and we expect one
-        if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected'));
-
         // Read existing fails, cleanup expired ones
         $time = time();
         $failCount = cleanupFails($failFile, $time);
 
         // Check if we're limited
         if ($failCount >= $maxFails) {
-            if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            if ($failCount == $maxFails) {
+                my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            }
             throw new Exception(_('Too many invalid login attempts'));
         }
 
         // Bail if username + password combo doesn't work
-        if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
-
-        // Bail if we need a token but it's invalid
-        if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
+        if (!verifyUsernamePassword($username, $password)) {
+            throw new Exception(_('Invalid username or password'));
+        }
 
         // Successful login, start session
         @unlink($failFile);
-        if (session_status()==PHP_SESSION_NONE) session_start();
+        if (session_status() == PHP_SESSION_NONE) {
+            session_start();
+        }
         $_SESSION['unraid_login'] = time();
         $_SESSION['unraid_user'] = $username;
         session_regenerate_id(true);
@@ -274,8 +201,8 @@ function isRemoteTwoFactorEnabled(): bool {
     /
     /************************/
     body {
-        background: <?=$theme_dark?'#1C1B1B':'#F2F2F2'?>;
-        color: <?=$theme_dark?'#fff':'#1c1b1b'?>;
+        background: <?=$theme_dark ? '#1C1B1B' : '#F2F2F2'?>;
+        color: <?=$theme_dark ? '#fff' : '#1c1b1b'?>;
         font-family: clear-sans, sans-serif;
         font-size: .875rem;
         padding: 0;
@@ -359,7 +286,7 @@ function isRemoteTwoFactorEnabled(): bool {
         width: 500px;
         margin: 6rem auto;
         border-radius: 10px;
-        background: <?=$theme_dark?'#2B2A29':'#fff'?>;
+        background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
     }
     #login::after {
         content: "";
@@ -392,7 +319,7 @@ function isRemoteTwoFactorEnabled(): bool {
     }
     #login .error {
         color: red;
-        margin-top: -20px;
+        margin-top: 1rem;
     }
     #login .content {
         padding: 2rem;
@@ -451,7 +378,7 @@ function isRemoteTwoFactorEnabled(): bool {
     /************************/
     @media (max-width: 500px) {
         body {
-            background: <?=$theme_dark?'#2B2A29':'#fff'?>;
+            background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
         }
         [type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea {
             font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
@@ -483,7 +410,7 @@ function isRemoteTwoFactorEnabled(): bool {
                 <?=htmlspecialchars($var['NAME'])?>
             </h1>
             <h2>
-               	<?=htmlspecialchars($var['COMMENT'])?>
+                <?=htmlspecialchars($var['COMMENT'])?>
             </h2>
 
             <div class="case">
@@ -497,82 +424,38 @@ function isRemoteTwoFactorEnabled(): bool {
             </div>
 
             <div class="form">
-                <form class="js-removeTimeout" action="/login" method="POST">
-                    <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                        <p>
-                            <input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
-                            <input name="password" type="password" placeholder="<?=_('Password')?>" required>
-                            <? if ($twoFactorRequired && !empty($token)) { ?>
-                            <input name="token" type="hidden" value="<?= $token ?>">
-                            <? } ?>
-                        </p>
-                        <? if ($error) echo "<p class='error'>$error</p>"; ?>
-                        <p>
-                            <button type="submit" class="button button--small"><?=_('Login')?></button>
-                        </p>
-                    <? } else { ?>
-                        <? if ($error) { ?>
-                            <div>
-                                <p class="error" style="padding-top:10px;"><?= $error ?></p>
-                            </div>
-                        <? } else { ?>
-                            <div>
-                                <p class="error" style="padding-top:10px;" title="<?= _('Please access this server via the My Servers Dashboard') ?>"><?= _('No 2FA token detected') ?></p>
-                            </div>
-                        <? } ?>
-                        <div>
-                            <a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
-                        </div>
-                    <? } ?>
-                    <script type="text/javascript">
-                        document.cookie = "cookietest=1";
-                        cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
-                        document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
-                        if (!cookieEnabled) {
-                            const errorElement = document.createElement('p');
-                            errorElement.classList.add('error');
-                            errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
-
-                            document.body.textContent = '';
-                            document.body.appendChild(errorElement);
-                        }
-                    </script>
+                <form action="/login" method="POST">
+                    <p>
+                        <input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
+                        <input name="password" type="password" placeholder="<?=_('Password')?>" required>
+                    </p>
+                    <p>
+                        <button type="submit" class="button button--small"><?=_('Login')?></button>
+                    </p>
+                    <?php if ($error) { ?>
+                        <p class="error"><?= $error ?></p>
+                    <?php } ?>
                 </form>
-
-                <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                    <div class="js-addTimeout hidden">
-                        <p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
-                        <a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
-                    </div>
-                <? } ?>
             </div>
 
-            <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                <p class="js-removeTimeout"><a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a></p>
-            <? } ?>
+            <a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a>
 
         </div>
     </section>
-    <? if ($twoFactorRequired && !empty($token)) { ?>
-        <script type="text/javascript">
-            const $elsToRemove = document.querySelectorAll('.js-removeTimeout');
-            const $elsToShow = document.querySelectorAll('.js-addTimeout');
-            /**
-             * A user can manually refresh the page or submit with the wrong username/password
-             * the t2fa token will be re-used on these page refreshes. We need to keep track of the timeout across potential page
-             * loads rather than setting the timer with a fresh timeout each page load
-             */
-            const tokenName = '<?=$token?>'.slice(-20);
-            const ts = Date.now();
-            const timeoutStarted = sessionStorage.getItem(tokenName) ? Number(sessionStorage.getItem(tokenName)) : ts;
-            const timeoutDiff = ts - timeoutStarted; // current timestamp minus timestamp when token first set
-            const timeoutMS = 297000 - timeoutDiff; // 5 minutes minus 3seconds or (5*60)*1000ms - 3000ms = 297000
-            sessionStorage.setItem(tokenName, timeoutStarted);
-            const tokenTimeout = setTimeout(() => {
-                $elsToRemove.forEach(z => z.remove()); // remove elements
-                $elsToShow.forEach(z => z.classList.remove('hidden')); // add elements
-            }, timeoutMS); // if timeoutMS is negative value the timeout will trigger immediately
-        </script>
-    <? } ?>
+
+    <script type="text/javascript">
+        document.cookie = "cookietest=1";
+        cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
+        document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
+        if (!cookieEnabled) {
+            const formParentElement = document.querySelector('.form');
+            const errorElement = document.createElement('p');
+            errorElement.classList.add('error');
+            errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
+
+            document.body.textContent = '';
+            document.body.appendChild(errorElement);
+        }
+    </script>
 </body>
 </html>
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time
index 3f9a43ff4f..41ec194694 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time
@@ -1 +1 @@
-1744137872623
+1745509523866
\ No newline at end of file
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php
index e71f307c16..65eb45cfd9 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php
@@ -54,7 +54,6 @@ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-color-palette.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-base.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-dynamix.css")?>">
-<link type="text/css" rel="stylesheet" href="<?autov("/plugins/dynamix/styles/dynamix-jquery-ui.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/themes/{$display['theme']}.css")?>">
 
 <style>
@@ -1292,7 +1291,11 @@ function isValidURL(url) {
   if (document.hidden) {
     nchanFocusStop();
   } else {
-    nchanFocusStart();
+    <? if (isset($myPage['Load']) && $myPage['Load'] > 0):?>
+      window.location.reload();
+    <?else:?>
+      nchanFocusStart();
+    <?endif;?>
   }
 <?endif;?>
 });
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time
index c3d2ff4fd5..06efd72a2f 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time
@@ -1 +1 @@
-1744137872211
+1745509523160
\ No newline at end of file
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time
index 107a6c6420..503c5831be 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time
@@ -1 +1 @@
-1744137872410
+1745509523526
\ No newline at end of file
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time
index f16d0a4af6..f34cf351fd 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time
@@ -1 +1 @@
-1744137872839
+1745509524137
\ No newline at end of file
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
index 67863ba1d6..5a76a3e97c 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
@@ -53,10 +53,12 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
 // Included in login.php
 
 // Only start a session to check if they have a cookie that looks like our session
-$server_name = strtok($_SERVER['HTTP_HOST'],":");
+$server_name = strtok($_SERVER['HTTP_HOST'], ":");
 if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
     // Start the session so we can check if $_SESSION has data
-    if (session_status()==PHP_SESSION_NONE) session_start();
+    if (session_status() == PHP_SESSION_NONE) {
+        session_start();
+    }
 
     // Check if the user is already logged in
     if ($_SESSION && !empty($_SESSION['unraid_user'])) {
@@ -66,10 +68,11 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
     }
 }
 
-function readFromFile($file): string {
+function readFromFile($file): string
+{
     $text = "";
     if (file_exists($file) && filesize($file) > 0) {
-        $fp = fopen($file,"r");
+        $fp = fopen($file, "r");
         if (flock($fp, LOCK_EX)) {
             $text = fread($fp, filesize($file));
             flock($fp, LOCK_UN);
@@ -79,8 +82,9 @@ function readFromFile($file): string {
     return $text;
 }
 
-function appendToFile($file, $text): void {
-    $fp = fopen($file,"a");
+function appendToFile($file, $text): void
+{
+    $fp = fopen($file, "a");
     if (flock($fp, LOCK_EX)) {
         fwrite($fp, $text);
         fflush($fp);
@@ -89,8 +93,9 @@ function appendToFile($file, $text): void {
     }
 }
 
-function writeToFile($file, $text): void {
-    $fp = fopen($file,"w");
+function writeToFile($file, $text): void
+{
+    $fp = fopen($file, "w");
     if (flock($fp, LOCK_EX)) {
         fwrite($fp, $text);
         fflush($fp);
@@ -107,7 +112,8 @@ function isValidTimeStamp($timestamp)
         && ($timestamp >= ~PHP_INT_MAX);
 }
 
-function cleanupFails(string $failFile, int $time): int {
+function cleanupFails(string $failFile, int $time): int
+{
     global $cooldown;
 
     // Read existing fails
@@ -118,8 +124,8 @@ function cleanupFails(string $failFile, int $time): int {
     // Remove entries older than $cooldown minutes, and entries that are not timestamps
     $updateFails = false;
     foreach ((array) $fails as $key => $value) {
-        if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) {
-            unset ($fails[$key]);
+        if (!isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time)) {
+            unset($fails[$key]);
             $updateFails = true;
         }
     }
@@ -132,94 +138,19 @@ function cleanupFails(string $failFile, int $time): int {
     return count($fails);
 }
 
-function verifyUsernamePassword(string $username, string $password): bool {
-    if ($username != "root") return false;
+function verifyUsernamePassword(string $username, string $password): bool
+{
+    if ($username != "root") {
+        return false;
+    }
 
     $output = exec("/usr/bin/getent shadow $username");
-    if ($output === false) return false;
-    $credentials = explode(":", $output);
-    return password_verify($password, $credentials[1]);
-}
-
-function verifyTwoFactorToken(string $username, string $token): bool {
-    try {
-        // Create curl client
-        $curlClient = curl_init();
-        curl_setopt($curlClient, CURLOPT_HEADER, true);
-        curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock');
-        curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify');
-        curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256);
-        curl_setopt($curlClient, CURLOPT_TIMEOUT, 5);
-        curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock'));
-        curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([
-            'username' => $username,
-            'token' => $token
-        ]));
-
-        // Send the request
-        curl_exec($curlClient);
-
-        // Get the http status code
-        $httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE);
-
-        // Close the connection
-        curl_close($curlClient);
-
-        // Error
-        // This should accept 200 or 204 status codes
-        if ($httpCode !== 200 && $httpCode !== 204) {
-            // Log error to syslog
-            my_logger("2FA code for {$username} is invalid, blocking access!");
-            return false;
-        }
-
-        // Log success to syslog
-        my_logger("2FA code for {$username} is valid, allowing login!");
-
-        // Success
-        return true;
-    } catch (Exception $exception) {
-        // Error
+    if ($output === false) {
         return false;
     }
+    $credentials = explode(":", $output);
+    return password_verify($password, $credentials[1]);
 }
-
-// Check if a haystack ends in a needle
-function endsWith($haystack, $needle): bool {
-    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
-}
-
-// Check if we're accessing this via a wildcard cert
-function isWildcardCert(): bool {
-    global $server_name;
-    return endsWith($server_name, '.myunraid.net');
-}
-
-// Check if we're accessing this locally via the expected myunraid.net url
-function isLocalAccess(): bool {
-    global $nginx, $server_name;
-    return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name;
-}
-
-// Check if we're accessing this remotely via the expected myunraid.net url
-function isRemoteAccess(): bool {
-    global $nginx, $server_name;
-    return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name;
-}
-
-// Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server)
-function isLocalTwoFactorEnabled(): bool {
-    global $nginx, $my_servers;
-    return $nginx['NGINX_USESSL'] === "auto" && ($my_servers['local']['2Fa']??'') === 'yes';
-}
-
-// Check if 2fa is enabled for remote
-function isRemoteTwoFactorEnabled(): bool {
-    global $my_servers;
-    return ($my_servers['remote']['2Fa']??'') === 'yes';
-}
-
 // Load configs into memory
 $my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
 $nginx = @parse_ini_file('/var/local/emhttp/nginx.ini');
@@ -231,38 +162,34 @@ function isRemoteTwoFactorEnabled(): bool {
 $failFile = "/var/log/pwfail/{$remote_addr}";
 
 // Get the credentials
-$username = $_POST['username']??'';
-$password = $_POST['password']??'';
-$token = $_REQUEST['token']??'';
-
-// Check if we need 2fa
-$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled());
+$username = $_POST['username'] ?? '';
+$password = $_POST['password'] ?? '';
 
 // If we have a username + password combo attempt to login
 if (!empty($username) && !empty($password)) {
     try {
-        // Bail if we're missing the 2FA token and we expect one
-        if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected'));
-
         // Read existing fails, cleanup expired ones
         $time = time();
         $failCount = cleanupFails($failFile, $time);
 
         // Check if we're limited
         if ($failCount >= $maxFails) {
-            if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            if ($failCount == $maxFails) {
+                my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            }
             throw new Exception(_('Too many invalid login attempts'));
         }
 
         // Bail if username + password combo doesn't work
-        if (!verifyUsernamePasswordAndSSO($username, $password)) throw new Exception(_('Invalid username or password'));
-
-        // Bail if we need a token but it's invalid
-        if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
+        if (!verifyUsernamePasswordAndSSO($username, $password)) {
+            throw new Exception(_('Invalid username or password'));
+        }
 
         // Successful login, start session
         @unlink($failFile);
-        if (session_status()==PHP_SESSION_NONE) session_start();
+        if (session_status() == PHP_SESSION_NONE) {
+            session_start();
+        }
         $_SESSION['unraid_login'] = time();
         $_SESSION['unraid_user'] = $username;
         session_regenerate_id(true);
@@ -325,8 +252,8 @@ function isRemoteTwoFactorEnabled(): bool {
     /
     /************************/
     body {
-        background: <?=$theme_dark?'#1C1B1B':'#F2F2F2'?>;
-        color: <?=$theme_dark?'#fff':'#1c1b1b'?>;
+        background: <?=$theme_dark ? '#1C1B1B' : '#F2F2F2'?>;
+        color: <?=$theme_dark ? '#fff' : '#1c1b1b'?>;
         font-family: clear-sans, sans-serif;
         font-size: .875rem;
         padding: 0;
@@ -410,7 +337,7 @@ function isRemoteTwoFactorEnabled(): bool {
         width: 500px;
         margin: 6rem auto;
         border-radius: 10px;
-        background: <?=$theme_dark?'#2B2A29':'#fff'?>;
+        background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
     }
     #login::after {
         content: "";
@@ -443,7 +370,7 @@ function isRemoteTwoFactorEnabled(): bool {
     }
     #login .error {
         color: red;
-        margin-top: -20px;
+        margin-top: 1rem;
     }
     #login .content {
         padding: 2rem;
@@ -502,7 +429,7 @@ function isRemoteTwoFactorEnabled(): bool {
     /************************/
     @media (max-width: 500px) {
         body {
-            background: <?=$theme_dark?'#2B2A29':'#fff'?>;
+            background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
         }
         [type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea {
             font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
@@ -534,7 +461,7 @@ function isRemoteTwoFactorEnabled(): bool {
                 <?=htmlspecialchars($var['NAME'])?>
             </h1>
             <h2>
-               	<?=htmlspecialchars($var['COMMENT'])?>
+                <?=htmlspecialchars($var['COMMENT'])?>
             </h2>
 
             <div class="case">
@@ -548,83 +475,39 @@ function isRemoteTwoFactorEnabled(): bool {
             </div>
 
             <div class="form">
-                <form class="js-removeTimeout" action="/login" method="POST">
-                    <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                        <p>
-                            <input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
-                            <input name="password" type="password" placeholder="<?=_('Password')?>" required>
-                            <? if ($twoFactorRequired && !empty($token)) { ?>
-                            <input name="token" type="hidden" value="<?= $token ?>">
-                            <? } ?>
-                        </p>
-                        <? if ($error) echo "<p class='error'>$error</p>"; ?>
-                        <p>
-                            <button type="submit" class="button button--small"><?=_('Login')?></button>
-                        </p>
-                    <? } else { ?>
-                        <? if ($error) { ?>
-                            <div>
-                                <p class="error" style="padding-top:10px;"><?= $error ?></p>
-                            </div>
-                        <? } else { ?>
-                            <div>
-                                <p class="error" style="padding-top:10px;" title="<?= _('Please access this server via the My Servers Dashboard') ?>"><?= _('No 2FA token detected') ?></p>
-                            </div>
-                        <? } ?>
-                        <div>
-                            <a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
-                        </div>
-                    <? } ?>
-                    <script type="text/javascript">
-                        document.cookie = "cookietest=1";
-                        cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
-                        document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
-                        if (!cookieEnabled) {
-                            const errorElement = document.createElement('p');
-                            errorElement.classList.add('error');
-                            errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
-
-                            document.body.textContent = '';
-                            document.body.appendChild(errorElement);
-                        }
-                    </script>
+                <form action="/login" method="POST">
+                    <p>
+                        <input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
+                        <input name="password" type="password" placeholder="<?=_('Password')?>" required>
+                    </p>
+                    <p>
+                        <button type="submit" class="button button--small"><?=_('Login')?></button>
+                    </p>
+                    <?php if ($error) { ?>
+                        <p class="error"><?= $error ?></p>
+                    <?php } ?>
                 </form>
 <?php include "$docroot/plugins/dynamix.my.servers/include/sso-login.php"; ?>
-
-                <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                    <div class="js-addTimeout hidden">
-                        <p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
-                        <a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
-                    </div>
-                <? } ?>
             </div>
 
-            <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                <p class="js-removeTimeout"><a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a></p>
-            <? } ?>
+            <a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a>
 
         </div>
     </section>
-    <? if ($twoFactorRequired && !empty($token)) { ?>
-        <script type="text/javascript">
-            const $elsToRemove = document.querySelectorAll('.js-removeTimeout');
-            const $elsToShow = document.querySelectorAll('.js-addTimeout');
-            /**
-             * A user can manually refresh the page or submit with the wrong username/password
-             * the t2fa token will be re-used on these page refreshes. We need to keep track of the timeout across potential page
-             * loads rather than setting the timer with a fresh timeout each page load
-             */
-            const tokenName = '<?=$token?>'.slice(-20);
-            const ts = Date.now();
-            const timeoutStarted = sessionStorage.getItem(tokenName) ? Number(sessionStorage.getItem(tokenName)) : ts;
-            const timeoutDiff = ts - timeoutStarted; // current timestamp minus timestamp when token first set
-            const timeoutMS = 297000 - timeoutDiff; // 5 minutes minus 3seconds or (5*60)*1000ms - 3000ms = 297000
-            sessionStorage.setItem(tokenName, timeoutStarted);
-            const tokenTimeout = setTimeout(() => {
-                $elsToRemove.forEach(z => z.remove()); // remove elements
-                $elsToShow.forEach(z => z.classList.remove('hidden')); // add elements
-            }, timeoutMS); // if timeoutMS is negative value the timeout will trigger immediately
-        </script>
-    <? } ?>
+
+    <script type="text/javascript">
+        document.cookie = "cookietest=1";
+        cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
+        document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
+        if (!cookieEnabled) {
+            const formParentElement = document.querySelector('.form');
+            const errorElement = document.createElement('p');
+            errorElement.classList.add('error');
+            errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
+
+            document.body.textContent = '';
+            document.body.appendChild(errorElement);
+        }
+    </script>
 </body>
 </html>
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php
index 9c5c9550ac..5c6b58b32d 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php
@@ -31,6 +31,24 @@
 exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
 
 function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
+
+function is_localhost() {
+  // Use the peer IP, not the Host header which can be spoofed
+  return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  if (session_status() === PHP_SESSION_ACTIVE) {
+    session_destroy();
+  }
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  my_logger("Unraid GUI-boot: created root session for localhost request.");
+}
 ?>
 <!DOCTYPE html>
 <html <?=$display['rtl']?>lang="<?=strtok($locale,'_')?:'en'?>" class="<?= $themeHtmlClass ?>">
@@ -54,7 +72,6 @@ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-color-palette.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-base.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-dynamix.css")?>">
-<link type="text/css" rel="stylesheet" href="<?autov("/plugins/dynamix/styles/dynamix-jquery-ui.css")?>">
 <link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/themes/{$display['theme']}.css")?>">
 
 <style>
@@ -1275,7 +1292,11 @@ function isValidURL(url) {
   if (document.hidden) {
     nchanFocusStop();
   } else {
-    nchanFocusStart();
+    <? if (isset($myPage['Load']) && $myPage['Load'] > 0):?>
+      window.location.reload();
+    <?else:?>
+      nchanFocusStart();
+    <?endif;?>
   }
 <?endif;?>
 });
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
index 7c9d93c984..a4193c8d91 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
 
 import { createPatch } from 'diff';
 
+import isGuiMode from '@app/core/utils/validation/is-gui-mode.js';
 import {
     FileModification,
     ShouldApplyWithReason,
@@ -32,19 +33,60 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
-    private applyToSource(fileContent: string): string {
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
+
+    private async patchGuiBootAuth(source: string): Promise<string> {
+        if (source.includes('if (is_localhost() && !is_good_session())')) {
+            return source;
+        }
+        // prettier-ignore
+        const newPhpCode =
+`
+function is_localhost() {
+  // Use the peer IP, not the Host header which can be spoofed
+  return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  if (session_status() === PHP_SESSION_ACTIVE) {
+    session_destroy();
+  }
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  my_logger("Unraid GUI-boot: created root session for localhost request.");
+}`;
+        // Add the PHP code before the DOCTYPE declaration
+        return this.prependDoctypeWithPhp(source, newPhpCode);
+    }
+
+    private async applyToSource(fileContent: string): Promise<string> {
         const transformers = [
             this.removeNotificationBell.bind(this),
             this.replaceToasts.bind(this),
             this.addToaster.bind(this),
+            this.patchGuiBootAuth.bind(this),
         ];
-        return transformers.reduce((content, fn) => fn(content), fileContent);
+
+        return transformers.reduce(async (contentPromise, transformer) => {
+            const content = await contentPromise;
+            return transformer(content);
+        }, Promise.resolve(fileContent));
     }
 
     protected async generatePatch(overridePath?: string): Promise<string> {
         const fileContent = await readFile(this.filePath, 'utf-8');
 
-        const newContent = this.applyToSource(fileContent);
+        const newContent = await this.applyToSource(fileContent);
 
         return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
     }
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
index e476a130b2..1953c61c09 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
@@ -2,7 +2,36 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
 ===================================================================
 --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	original
 +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	modified
-@@ -603,20 +603,11 @@
+@@ -29,10 +29,28 @@
+ // adjust the text color in docker log window
+ $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
+ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
+ 
+ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
++
++function is_localhost() {
++  // Use the peer IP, not the Host header which can be spoofed
++  return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1';
++}
++function is_good_session() {
++  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
++}
++if (is_localhost() && !is_good_session()) {
++  if (session_status() === PHP_SESSION_ACTIVE) {
++    session_destroy();
++  }
++  session_start();
++  $_SESSION['unraid_login'] = time();
++  $_SESSION['unraid_user'] = 'root';
++  session_write_close();
++  my_logger("Unraid GUI-boot: created root session for localhost request.");
++}
+ ?>
+ <!DOCTYPE html>
+ <html <?=$display['rtl']?>lang="<?=strtok($locale,'_')?:'en'?>" class="<?= $themeHtmlClass ?>">
+ <head>
+ <title><?=_var($var,'NAME')?>/<?=_var($myPage,'name')?></title>
+@@ -602,20 +620,11 @@
  }
  
  function openNotifier() {
@@ -24,7 +53,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
  }
  
  function closeNotifier() {
-@@ -749,12 +740,12 @@
+@@ -748,12 +757,12 @@
    }
    // create list of nchan scripts to be started
    if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']);
@@ -38,7 +67,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
  foreach ($buttons as $button) {
    annotate($button['file']);
    // include page specific stylesheets (if existing)
-@@ -938,26 +929,18 @@
+@@ -937,26 +946,18 @@
          case 'warning': bell2++; break;
          case 'normal' : bell3++; break;
        }
@@ -70,7 +99,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
  });
  
  <?if ($wlan0):?>
-@@ -1334,7 +1317,8 @@
+@@ -1337,7 +1338,8 @@
        }
      }
    }
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
index 841c19ffa4..31a743dfcf 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
@@ -58,30 +58,30 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
  // Included in login.php
  
  // Only start a session to check if they have a cookie that looks like our session
- $server_name = strtok($_SERVER['HTTP_HOST'],":");
+ $server_name = strtok($_SERVER['HTTP_HOST'], ":");
  if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
-@@ -202,11 +253,11 @@
-             if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+@@ -128,11 +179,11 @@
+             }
              throw new Exception(_('Too many invalid login attempts'));
          }
  
          // Bail if username + password combo doesn't work
--        if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
-+        if (!verifyUsernamePasswordAndSSO($username, $password)) throw new Exception(_('Invalid username or password'));
- 
-         // Bail if we need a token but it's invalid
-         if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
+-        if (!verifyUsernamePassword($username, $password)) {
++        if (!verifyUsernamePasswordAndSSO($username, $password)) {
+             throw new Exception(_('Invalid username or password'));
+         }
  
          // Successful login, start session
-@@ -536,10 +587,11 @@
-                             document.body.textContent = '';
-                             document.body.appendChild(errorElement);
-                         }
-                     </script>
+         @unlink($failFile);
+@@ -434,10 +485,11 @@
+                     </p>
+                     <?php if ($error) { ?>
+                         <p class="error"><?= $error ?></p>
+                     <?php } ?>
                  </form>
 +<?php include "$docroot/plugins/dynamix.my.servers/include/sso-login.php"; ?>
+             </div>
+ 
+             <a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a>
  
-                 <? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
-                     <div class="js-addTimeout hidden">
-                         <p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
-                         <a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
+         </div>
diff --git a/justfile b/justfile
index 34302797c3..39e6947657 100644
--- a/justfile
+++ b/justfile
@@ -14,7 +14,6 @@ setup:
     @just ignore api/dev/states/myservers.cfg api/dev/Unraid.net/myservers.cfg
     
     pnpm install
-    just unraid-ui/setup
 
 # restore notification files under api/dev
 restore-notifications:
diff --git a/unraid-ui/package.json b/unraid-ui/package.json
index 63b200cf05..e0bfdec1ae 100644
--- a/unraid-ui/package.json
+++ b/unraid-ui/package.json
@@ -13,6 +13,7 @@
     "tailwind.config.ts"
   ],
   "scripts": {
+    "prepare": "pnpm build",
     "// Development": "",
     "dev": "vite",
     "preview": "vite preview",


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (5)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (1)</summary><blockquote>

`9-57`: _⚠️ Potential issue_

**SSO helper has several edge-case bugs**

1. `strlen($password) > 800` – many JWTs are ~600 – 700 chars. Rejecting shorter valid tokens will confuse users.  
2. `preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', …)` – the dots are unescaped, so *any* character is accepted as a separator. Use `\.`.  
3. Splitting response with `explode('{', $output[0], 2)` depends on tool output format; prefer `json_decode(implode("\n",$output), true)`.

Suggested quick fixes:

```diff
- if (strlen($password) > 800) {
+ if (strlen($password) >= 500) {

- if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {
+ if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {

- $jsonParts = explode('{', $output[0], 2);
- ...
+ $response = json_decode(implode("\n", $output), true);
```

These tighten validation and reduce false negatives.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (3)</summary><blockquote>

`30-45`: _🛠️ Refactor suggestion_

**`openNotifier` now calls nothing → dead code / async noise**

`openNotifier()` still performs an AJAX request but all `$.jGrowl` lines were stripped, leaving a loop that does nothing. This produces needless network calls every 15 s and silently discards notifications.

Either:

1. Emit notifications with the new `<uui-toaster>`, or  
2. Remove the polling entirely.

Example draft:

```diff
-$.each($.parseJSON(msg), function(i, notify){ ... });
+uuiToaster = document.querySelector('uui-toaster');
+JSON.parse(msg).forEach(n => {
+  uuiToaster.show({ message: n.subject + '<br>' + n.description,
+                    color: n.importance });
+});
```

---

`54-57`: _⚠️ Potential issue_

**Left-over bell icon references break JS**

The `<div class='nav-user…` element was removed, but later code (lines ~81-85) still manipulates `$('#bell')`. In browsers this now throws “Cannot read properties of null (reading ‘removeClass’)”.

Purge those jQuery calls or feature-flag them with `if ($('#bell').length) …`.

---

`100-101`: _🛠️ Refactor suggestion_

**`<uui-toaster>` inserted but never used**

The markup is present, yet no script pushes messages to it. Users will perceive notifications as lost. Hook `openNotifier()` (or a new consumer) to `document.querySelector('uui-toaster').show(...)`.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (1)</summary><blockquote>

`15-18`: _⚠️ Potential issue_

**Regex matches anything – escape the dots**

`preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)`  
The `.` meta-character means “any char”. As written, the check accepts *any* character between segments, defeating format validation. Escape the dots:

```diff
-'/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/'
+'/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/'
```

(Note the backslashes).

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (1)</summary><blockquote>

`1291-1299`: **Reload-on-focus can create unnecessary refresh loops**

Every time the tab regains focus *and* `$myPage['Load'] > 0`, you force a full `window.location.reload()`.  

If the page is already configured to auto-reload through `timers.reload`, this extra reload can double-fire, consume bandwidth, and reset UI state for users who simply alt-tab quickly.

```diff
-      window.location.reload();
+      // Avoid double reload when timers.reload is already active
+      if (!timers?.focusReload) {
+        timers.focusReload = setTimeout(() => location.reload(), 100);
+      }
```

Consider debouncing or re-using the existing `timers.reload` logic.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)</summary><blockquote>

`1278-1292`: **Minor regex typo may stop focus-resume reload from working**

The block is fine functionally, but note that both branches repeat identical `document.hidden` checks.  You can simplify:

```js
if (document.hidden) {
  nchanFocusStop();
} else if (<?=(isset($myPage['Load']) && $myPage['Load'] > 0) ? 'true' : 'false'?>) {
  location.reload();
} else {
  nchanFocusStart();
}
```

This removes one level of nested `if`s.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (2)</summary><blockquote>

`43-58`: **Missing opening PHP tag is OK but fragile**

Because you insert before the final `?>`, the added snippet inherits the already-open PHP context. A future maintainer might move the closing tag and break execution.

Safer: wrap the injected block explicitly.

```diff
-const newPhpCode = `
-if (session_status() == PHP_SESSION_NONE) {
+const newPhpCode = `
+<?php
+if (session_status() == PHP_SESSION_NONE) {
...
-my_logger("Page accessed without session; created session for root user.");
-}`;
+my_logger("Page accessed without session; created session for root user.");
+?>`;
```

This makes the patch self-contained.

---

`65-66`: **Transformer order: move auth patch first**

`removeNotificationBell` & `replaceToasts` operate on HTML/JS, while `patchGuiBootAuth` changes PHP *before* the doctype. Running it first avoids re-parsing the string multiple times.

```diff
-const transformers = [removeBell, replaceToasts, addToaster, patchGuiBootAuth];
+const transformers = [patchGuiBootAuth, removeBell, replaceToasts, addToaster];
```

Tiny perf gain and clearer intent.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (1)</summary><blockquote>

`61-72`: **Side-effect of function rename: old verifier still present?**

You replaced calls to `verifyUsernamePassword` with `verifyUsernamePasswordAndSSO`, but the original function is still defined elsewhere. For clarity and to avoid accidental reuse, consider:

```php
// @deprecated – superseded by verifyUsernamePasswordAndSSO()
function verifyUsernamePassword(...) { /* call new fn */ }
```

This provides backward compatibility without silent divergence.

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (1)</summary><blockquote>

`427-438`: **Duplicate HTML & PHP blocks – factor into include**

The root login form markup is duplicated across snapshot and fixture. Extract to a shared `login-form.inc.php` to reduce maintenance overhead.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c5a394eddfa846850fcaa092fdbd2d5dc2426dce and dcd2c9a08e5b698d931d23d3ad0237403cb2f2fb.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php` (14 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php` (14 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php` (2 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch` (4 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch` (1 hunks)
* `justfile` (0 hunks)
* `unraid-ui/package.json` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* justfile

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (2)</summary>

```
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch is used to remove the old jGrowl notification system from Unraid pages, as notifications are handled by a new system implemented on a different page.
```

```
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch removes the old jGrowl notification system and is complemented by the unraid-toaster component implementation. The new system is added through the DefaultPageLayout modification which inserts the toaster component with proper position configuration based on user preferences.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (2)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (6)</summary>

* `readFromFile` (20-32)
* `appendToFile` (34-43)
* `writeToFile` (45-54)
* `cleanupFails` (64-88)
* `isValidTimeStamp` (57-62)
* `verifyUsernamePassword` (90-102)

</details>
<details>
<summary>plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)</summary>

* `_` (108-108)

</details>

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (2)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (6)</summary>

* `readFromFile` (71-83)
* `appendToFile` (85-94)
* `writeToFile` (96-105)
* `cleanupFails` (115-139)
* `isValidTimeStamp` (108-113)
* `verifyUsernamePassword` (141-153)

</details>
<details>
<summary>plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)</summary>

* `_` (108-108)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Test API
* GitHub Check: Build API
* GitHub Check: Build Web App
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time (1)</summary>

`1-1`: **Skip: Timestamp fixture update**  
This update simply refreshes the fixture’s last-download timestamp. No functional behavior has changed.

</details>
<details>
<summary>unraid-ui/package.json (1)</summary>

`16-16`: **Approve the new `prepare` script**  
Adding `"prepare": "pnpm build"` ensures the UI assets are automatically built after dependencies are installed, streamlining local development and deployment.

</details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time (1)</summary>

`1-1`: **Skip: Timestamp fixture update**  
This change only updates the test fixture timestamp; there are no code logic modifications.

</details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time (1)</summary>

`1-1`: **Skip: Timestamp fixture update**  
The timestamp was refreshed to keep test snapshots in sync. No functional changes to review.

</details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time (1)</summary>

`1-1`: **Skip: Timestamp fixture update**  
This fixture’s timestamp aligns with other snapshot updates. No code behavior impacted.

</details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (1)</summary>

`20-24`: **Shell-argument length risk**

`escapeshellarg($password)` guards against injection, but extremely long JWTs (>128 kB) will break the exec buffer. Consider piping via STDIN:

```php
proc_open('/etc/rc.d/rc.unraid-api sso validate-token', $descriptors, $pipes);
fwrite($pipes[0], $password);
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:57:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)</summary><blockquote>

`35-49`: _⚠️ Potential issue_

**Security concern: Automatic root session creation needs additional safeguards**

Creating a root session automatically for localhost requests poses a security risk. While the comment indicates this should "only be possible when booting into GUI mode," there's no code-level verification to enforce this constraint. An attacker could potentially trigger this code path outside of the boot process.

Consider adding a verifiable server-side condition before granting root privileges, such as checking for a GUI-boot specific flag or environment variable.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dcd2c9a08e5b698d931d23d3ad0237403cb2f2fb and db3dbe103a907822e57db3046e48e30cca0fea03.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php` (2 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Test API
* GitHub Check: Build Web App
* GitHub Check: Build API
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (2)</summary>

`35-41`: **Good regex pattern that accounts for whitespace**

The regex pattern now correctly includes `\s*` before the `?>` which makes it resilient against any potential leading whitespace in the source file.

---

`71-71`: **Verify the execution order of transformers**

The `patchGuiBootAuth` transformer is added as the last in the chain. Make sure this is the intended order, as the other transformers might modify the content in ways that could affect the injection point.

</details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)</summary>

`293-298`: **Improved page refresh logic**

The updated code now intelligently handles window visibility changes:
- If `myPage['Load']` is set and greater than 0, it reloads the entire page
- Otherwise, it only resumes live updates with `nchanFocusStart()`

This approach optimizes for both cases - full refresh when needed and efficient incremental updates otherwise.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:45:47Z
user: login: elibosley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T20:35:00Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T20:35:34Z
user: login: elibosley
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T20:36:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>api/src/core/utils/validation/is-gui-mode.ts (2)</summary><blockquote>

`9-19`: **Consider memoising `isGuiMode()` to avoid spawning `pgrep` every call**  

`execa('pgrep', …)` is inexpensive once, but this helper will likely be called repeatedly during a single API lifetime (e.g. once per request from `DefaultPageLayoutModification`).  
A tiny in-memory cache with a short TTL (even 1-2 seconds) eliminates dozens of extra forks without behaviour changes.

```diff
-let isGuiMode = async (): Promise<boolean> => {
+const GUI_TTL = 2_000;            // ms
+let cachedValue: boolean | null = null;
+let cachedAt   = 0;
+
+const isGuiMode = async (): Promise<boolean> => {
+    if (cachedValue !== null && Date.now() - cachedAt < GUI_TTL) {
+        return cachedValue;
+    }
     try {
         // Use pgrep to check if slim process is running
         const { exitCode } = await execa('pgrep', ['slim'], { reject: false });
-        // exitCode 0 means process was found, 1 means not found
-        return exitCode === 0;
+        cachedValue = exitCode === 0;   // 0 → GUI mode
+        cachedAt    = Date.now();
+        return cachedValue;
     } catch (error) {
         internalLogger.error('Error checking GUI mode: %s', error);
         return false;
     }
 };
```

Nice-to-have but improves performance noticeably on busy installations.

---

`16-17`: **Use structured logging for errors**  

`internalLogger.error('Error checking GUI mode: %s', error)` flattens the stack trace.  
Passing the error as metadata keeps the stack searchable:

```ts
internalLogger.error({ err: error }, 'Error checking GUI mode');
```

</blockquote></details>
<details>
<summary>api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1)</summary><blockquote>

`75-87`: **Minor: avoid double `isGuiMode()` calls**  

`patchGuiBootAuth` and `getPregeneratedPatch` each invoke `isGuiMode()`.  
With the memoisation suggested earlier this is less costly, but you could still compute once and pass the result down (`this.isUnraidGuiMode` or via a closure) to simplify flow.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a9f0ff6b9fba47634c2a700cbefb03648f9a3ff6 and a65e0f5836e608d7024400fc25d2f3af76981140.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `api/dev/Unraid.net/myservers.cfg` (1 hunks)
* `api/dev/states/myservers.cfg` (1 hunks)
* `api/src/core/utils/validation/is-gui-mode.ts` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php` (1 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts` (2 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch` (4 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* api/dev/Unraid.net/myservers.cfg
* api/dev/states/myservers.cfg

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
* api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>api/src/core/utils/validation/is-gui-mode.ts (1)</summary><blockquote>

<details>
<summary>api/src/core/log.ts (1)</summary>

* `internalLogger` (39-39)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Test API
* GitHub Check: Build API
* GitHub Check: Build Web App
* GitHub Check: Cloudflare Pages

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T02:02:34Z
user: login: elibosley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T03:30:57Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T03:31:14Z

Review comments:
diff_hunk: @@ -31,6 +31,16 @@
 exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
 
 function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
+
+if (session_status() == PHP_SESSION_NONE) {
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_regenerate_id(true);
+  session_write_close();
+  # This situation should only be possible when booting into GUI mode
+  my_logger("Page accessed without session; created session for root user.");
+}
path: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Automatic “root” session creation can bypass authentication**

Injecting a session for `root` whenever `session_status() == PHP_SESSION_NONE` means *any* direct request to this page (e.g. `curl http://server/`) is silently promoted to a full-privilege session:

```php
$_SESSION['unraid_user'] = 'root';
```

1. An attacker could avoid the login screen by requesting the page when the service restarts and sessions are cleared.  
2. The comment _“should only be possible when booting into GUI mode”_ is not an enforceable guard. Nothing in the code restricts execution to that scenario.

At minimum:

```diff
-if (session_status() == PHP_SESSION_NONE) {
+if (session_status() == PHP_SESSION_NONE && php_sapi_name() === 'cli-server' /* or another verifiable GUI-boot flag */) {
```

Add a **verifiable, server-side condition** (e.g. kernel cmd-line flag, presence of `/var/run/unraid-gui-boot`) before granting root.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit a65e0f5
created_at: 2025-04-24T15:57:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,11 +32,37 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Regex is brittle against leading whitespace**

`^\?>\s*<!DOCTYPE html>` only matches when `?>` is at column 0.  
If the upstream file ever introduces indentation (e.g. `  ?>`), the inject will fail.

```diff
-const targetRegex = /^\?>\s*<!DOCTYPE html>/m;
+const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
```

Adding `\s*` before `?>` hardens the patcher.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
-        const targetRegex = /^\?>\s*<!DOCTYPE html>/m;
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;

        // Prepend the phpToAdd before the matched string
        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit db3dbe1
created_at: 2025-04-24T15:57:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,7 +2,28 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
 ===================================================================
 --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	original
 +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	modified
-@@ -603,20 +603,11 @@
+@@ -29,10 +29,20 @@
+ // adjust the text color in docker log window
+ $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
+ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
+ 
+ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
++
++if (session_status() == PHP_SESSION_NONE) {
++  session_start();
++  $_SESSION['unraid_login'] = time();
++  $_SESSION['unraid_user'] = 'root';
++  session_regenerate_id(true);
++  session_write_close();
++  # This situation should only be possible when booting into GUI mode
++  my_logger("Page accessed without session; created session for root user.");
++}
path: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Unconditional “root” session may bypass login**

Starting a session with `$_SESSION['unraid_user'] = 'root'` whenever no session cookie is present effectively authenticates any visitor (local or remote) who hits a page rendered with `DefaultPageLayout.php` before logging in.  If nginx is reachable on the network while the server is in GUI-boot or if the browser clears cookies, the user is silently elevated to root.

Consider tightening the guard:

```diff
-if (session_status() == PHP_SESSION_NONE) {
+if (session_status() == PHP_SESSION_NONE && php_sapi_name() !== 'cli') {
     session_start();
-    $_SESSION['unraid_login'] = time();
-    $_SESSION['unraid_user'] = 'root';
+    // Only create a guest session – login remains required
+    $_SESSION['unraid_login'] = 0;
+    $_SESSION['unraid_user']  = null;
```

or limit execution to GUI-boot (detect `isolcpus=unraid-gui` kernel param, local console IP, etc.).  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
if (session_status() == PHP_SESSION_NONE && php_sapi_name() !== 'cli') {
  session_start();
  // Only create a guest session – login remains required
  $_SESSION['unraid_login'] = 0;
  $_SESSION['unraid_user']  = null;
  session_regenerate_id(true);
  session_write_close();
  # This situation should only be possible when booting into GUI mode
  my_logger("Page accessed without session; created session for root user.");
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-24T15:57:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,38 +111,34 @@ function isRemoteTwoFactorEnabled(): bool {
 $failFile = "/var/log/pwfail/{$remote_addr}";
 
 // Get the credentials
-$username = $_POST['username']??'';
-$password = $_POST['password']??'';
-$token = $_REQUEST['token']??'';
-
-// Check if we need 2fa
-$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled());
+$username = $_POST['username'] ?? '';
+$password = $_POST['password'] ?? '';
 
 // If we have a username + password combo attempt to login
 if (!empty($username) && !empty($password)) {
     try {
-        // Bail if we're missing the 2FA token and we expect one
-        if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected'));
-
         // Read existing fails, cleanup expired ones
         $time = time();
         $failCount = cleanupFails($failFile, $time);
 
         // Check if we're limited
         if ($failCount >= $maxFails) {
-            if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            if ($failCount == $maxFails) {
+                my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
+            }
             throw new Exception(_('Too many invalid login attempts'));
         }
 
         // Bail if username + password combo doesn't work
-        if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
-
-        // Bail if we need a token but it's invalid
-        if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
+        if (!verifyUsernamePassword($username, $password)) {
+            throw new Exception(_('Invalid username or password'));
+        }
 
path: api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Out-of-date login path – SSO validation missing**

This fixture still calls `verifyUsernamePassword()` while production code now uses `verifyUsernamePasswordAndSSO()`. Tests using the fixture will not cover SSO paths and could regress silently. Update the call or de-duplicate the login script to avoid divergence.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T15:57:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,11 +32,43 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
+
+    private patchGuiBootAuth(source: string): string {
+        // prettier-ignore
+        const newPhpCode =
+`
+function is_localhost() {
+  $server_name = strtok($_SERVER['HTTP_HOST'], ":");
+  return $server_name == 'localhost' || $server_name == '127.0.0.1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  # This situation should only be possible when booting into GUI mode
+  my_logger("Page accessed without session; created session for root user.");
+}`;
+        // Add the PHP code before the DOCTYPE declaration
+        return this.prependDoctypeWithPhp(source, newPhpCode);
+    }
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Security concern: Consider additional verification for localhost session creation**

The code automatically creates a session for the 'root' user when detecting a localhost request without a valid session. This approach could be vulnerable if:

1. An attacker can make requests that appear to be from localhost
2. The code runs during normal operations rather than just during GUI boot

Consider adding additional security checks beyond just `is_localhost()`, such as verifying a boot-specific flag or ensuring this only happens during system startup.



Run the following script to find other references to similar automatic authentication patterns that might provide insights on additional safeguards:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for similar patterns of automatic authentication
rg -i "session.*root|root.*session|localhost.*auth|auth.*localhost" --type php
```

Length of output: 429

---



</details>

**Restrict Root Session Auto-Creation to GUI Boot Only**

The `patchGuiBootAuth` method inlines PHP that unconditionally creates a root session for any localhost request without confirming it’s during the initial GUI boot. An attacker who spoofs localhost or triggers this outside of startup could gain root access.

• File: `api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts`  
  Lines: ~43–64 (`patchGuiBootAuth`)  
• Recommendations:  
  – Introduce a boot-phase check (e.g. inspect an environment flag, CLI argument, or the existence of a “boot-in-progress” marker file).  
  – Wrap the PHP prepend in a guard so it only runs during Unraid’s GUI startup, not on every localhost request.  
  – Optionally require a signed, single-use token or additional credential to further limit misuse.  

Example diff sketch:  
```diff
 private patchGuiBootAuth(source: string): string {
-   // prettier-ignore
-   const newPhpCode = `…`; 
+   // Only enable root auto-session during GUI boot
+   if (!this.isGuiBootPhase()) {
+     return source;
+   }
+   // prettier-ignore
+   const newPhpCode = `…`; 
    return this.prependDoctypeWithPhp(source, newPhpCode);
 }
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T20:45:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,7 +2,28 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
 ===================================================================
 --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	original
 +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	modified
-@@ -603,20 +603,11 @@
+@@ -29,10 +29,20 @@
+ // adjust the text color in docker log window
+ $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
+ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
+ 
+ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
++
++if (session_status() == PHP_SESSION_NONE) {
++  session_start();
++  $_SESSION['unraid_login'] = time();
++  $_SESSION['unraid_user'] = 'root';
++  session_regenerate_id(true);
++  session_write_close();
++  # This situation should only be possible when booting into GUI mode
++  my_logger("Page accessed without session; created session for root user.");
++}
path: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
user: login: elibosley
body: This is alright since only users who are authenticated can see this page. Yes it seems like a hack but it is needed for Unraid GUI mode. 
created_at: 2025-04-28T20:35:00Z
author_association: MEMBER
diff_hunk: @@ -2,7 +2,28 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
 ===================================================================
 --- /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	original
 +++ /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php	modified
-@@ -603,20 +603,11 @@
+@@ -29,10 +29,20 @@
+ // adjust the text color in docker log window
+ $fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
+ exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
+ 
+ function annotate($text) {echo "\n<!--\n",str_repeat("#",strlen($text)),"\n$text\n",str_repeat("#",strlen($text)),"\n-->\n";}
++
++if (session_status() == PHP_SESSION_NONE) {
++  session_start();
++  $_SESSION['unraid_login'] = time();
++  $_SESSION['unraid_user'] = 'root';
++  session_regenerate_id(true);
++  session_write_close();
++  # This situation should only be possible when booting into GUI mode
++  my_logger("Page accessed without session; created session for root user.");
++}
path: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
user: login: coderabbitai[bot]
body: @elibosley I understand now. Thanks for clarifying that this is an intentional approach needed specifically for Unraid GUI mode. I'll defer to your domain knowledge on this particular authentication pattern.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: elibosley
PR: unraid/api#1368
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:0-0
Timestamp: 2025-04-28T20:35:32.954Z
Learning: For Unraid GUI mode, automatic session creation with root privileges is an acceptable pattern since these pages are already protected and only visible to authenticated users.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch is used to remove the old jGrowl notification system from Unraid pages, as notifications are handled by a new system implemented on a different page.
```

---

```
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch removes the old jGrowl notification system and is complemented by the unraid-toaster component implementation. The new system is added through the DefaultPageLayout modification which inserts the toaster component with proper position configuration based on user preferences.
```

</details>



<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T20:35:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,19 +33,73 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
-    private applyToSource(fileContent: string): string {
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**No fallback if `?> … <!DOCTYPE html>` sequence is missing**  

If upstream code ever removes or re-orders the `?>` + `<!DOCTYPE html>` line, `prependDoctypeWithPhp` silently returns the original source, skipping the security patch without warning.

Consider logging a warning (or throwing) when `targetRegex` fails to match so that missing injections are surfaced early.

```diff
-return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+if (!targetRegex.test(source)) {
+    this.logger?.warn(
+        '[%s] Unable to locate DOCTYPE boundary – GUI-boot auth snippet not injected.',
+        this.id,
+    );
+    return source;
+}
+return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
```
  


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;

        // Prepend the phpToAdd before the matched string
-       return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+       if (!targetRegex.test(source)) {
+           this.logger?.warn(
+               '[%s] Unable to locate DOCTYPE boundary – GUI-boot auth snippet not injected.',
+               this.id,
+           );
+           return source;
+       }
+       return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T02:02:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,19 +33,73 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
-    private applyToSource(fileContent: string): string {
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
+
+    private async patchGuiBootAuth(source: string): Promise<string> {
+        const isUnraidGuiMode = await isGuiMode();
+        if (!isUnraidGuiMode) {
+            return source;
+        }
+
+        if (source.includes('if (is_localhost() && !is_good_session())')) {
+            return source;
+        }
+        // prettier-ignore
+        const newPhpCode =
+`
+function is_localhost() {
+  $server_name = strtok($_SERVER['HTTP_HOST'], ":");
+  return $server_name == 'localhost' || $server_name == '127.0.0.1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  # This situation should only be possible when booting into GUI mode
+  my_logger("Page accessed without session; created session for root user.");
+}`;
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Root-session snippet is spoofable via the `Host` header**  

`is_localhost()` trusts `$_SERVER['HTTP_HOST']`, which an external client can forge (`curl -H "Host: localhost" …`).  
The check should rely on `REMOTE_ADDR` **and** guard against active sessions to avoid “session already started” warnings.  
`my_logger()` may also be undefined in vanilla Unraid.

```diff
-function is_localhost() {
-  $server_name = strtok($_SERVER['HTTP_HOST'], ":");
-  return $server_name == 'localhost' || $server_name == '127.0.0.1';
-}
+function is_localhost() {
+  // Use the peer IP, not the Host header
+  return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1';
+}
@@
-if (is_localhost() && !is_good_session()) {
-  session_start();
+if (is_localhost() && !is_good_session()) {
+  if (session_status() !== PHP_SESSION_ACTIVE) {
+      session_start();
+  }
   $_SESSION['unraid_login'] = time();
   $_SESSION['unraid_user'] = 'root';
   session_write_close();
-  # This situation should only be possible when booting into GUI mode
-  my_logger("Page accessed without session; created session for root user.");
+  // Log via standard PHP error log to avoid undefined helpers
+  error_log("Unraid GUI-boot: created root session for localhost request.");
 }
```

Leaking root access is a **critical** risk if the Host header path isn’t fixed.  
  


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
`
function is_localhost() {
  // Use the peer IP, not the Host header
  return $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $_SERVER['REMOTE_ADDR'] === '::1';
}
function is_good_session() {
  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
}
if (is_localhost() && !is_good_session()) {
  if (session_status() !== PHP_SESSION_ACTIVE) {
      session_start();
  }
  $_SESSION['unraid_login'] = time();
  $_SESSION['unraid_user'] = 'root';
  session_write_close();
  // Log via standard PHP error log to avoid undefined helpers
  error_log("Unraid GUI-boot: created root session for localhost request.");
}`
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T02:02:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,19 +33,73 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
-    private applyToSource(fileContent: string): string {
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
+
+    private async patchGuiBootAuth(source: string): Promise<string> {
+        const isUnraidGuiMode = await isGuiMode();
+        if (!isUnraidGuiMode) {
+            return source;
+        }
+
+        if (source.includes('if (is_localhost() && !is_good_session())')) {
+            return source;
+        }
+        // prettier-ignore
+        const newPhpCode =
+`
+function is_localhost() {
+  $server_name = strtok($_SERVER['HTTP_HOST'], ":");
+  return $server_name == 'localhost' || $server_name == '127.0.0.1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  # This situation should only be possible when booting into GUI mode
+  my_logger("Page accessed without session; created session for root user.");
+}`;
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: elibosley
body: Great suggestion, thank you
created_at: 2025-05-01T03:30:57Z
author_association: MEMBER
diff_hunk: @@ -32,19 +33,73 @@ export default class DefaultPageLayoutModification extends FileModification {
         return source.replace(jGrowlPattern, '');
     }
 
-    private applyToSource(fileContent: string): string {
+    private prependDoctypeWithPhp(source: string, phpToAdd: string): string {
+        // The regex to find the target string `?>\s*<!DOCTYPE html>` at the beginning of a line
+        const targetRegex = /^\s*\?>\s*<!DOCTYPE html>/m;
+
+        // Prepend the phpToAdd before the matched string
+        return source.replace(targetRegex, (match) => `${phpToAdd}\n${match}`);
+    }
+
+    private async patchGuiBootAuth(source: string): Promise<string> {
+        const isUnraidGuiMode = await isGuiMode();
+        if (!isUnraidGuiMode) {
+            return source;
+        }
+
+        if (source.includes('if (is_localhost() && !is_good_session())')) {
+            return source;
+        }
+        // prettier-ignore
+        const newPhpCode =
+`
+function is_localhost() {
+  $server_name = strtok($_SERVER['HTTP_HOST'], ":");
+  return $server_name == 'localhost' || $server_name == '127.0.0.1';
+}
+function is_good_session() {
+  return isset($_SESSION) && isset($_SESSION['unraid_user']) && isset($_SESSION['unraid_login']);
+}
+if (is_localhost() && !is_good_session()) {
+  session_start();
+  $_SESSION['unraid_login'] = time();
+  $_SESSION['unraid_user'] = 'root';
+  session_write_close();
+  # This situation should only be possible when booting into GUI mode
+  my_logger("Page accessed without session; created session for root user.");
+}`;
path: api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
user: login: coderabbitai[bot]
body: @elibosley You're welcome! I'm glad I could help with the security improvement. Let me know if you need any other assistance with your authentication work.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T03:31:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-24T15:54:04Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update removes all two-factor authentication (2FA) logic from the PHP login flow, simplifying authentication to require only a username and password. The login UI and backend checks are adjusted accordingly, with related functions and conditional rendering eliminated. Additionally, session initialization logic is introduced in the default page layout for cases where no session exists, and notification display logic is updated to use a web component instead of JavaScript pop-ups. Minor formatting and timestamp updates are included, and a new npm "prepare" script is added to the UI package.

## Changes

| File(s)                                                                                  | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .../downloaded/.login.php, .../snapshots/.login.php.modified.snapshot.php                | Removed all 2FA logic and checks from the login script and UI. Eliminated related functions, variables, and conditional rendering. Simplified login to username and password only. Cleaned up formatting and whitespace.                                                                                                              |
| .../downloaded/DefaultPageLayout.php, .../snapshots/DefaultPageLayout.php.modified.snapshot.php | Removed a stylesheet link. Modified JavaScript to reload the page on visibility change if a server-side flag is set. Added PHP session initialization logic if no session exists.                                                                                                                 |
| .../downloaded/Notifications.page.last-download-time, .../downloaded/DefaultPageLayout.php.last-download-time, .../downloaded/.login.php.last-download-time, .../downloaded/auth-request.php.last-download-time | Updated timestamp values only; no functional changes.                                                                                                                                                                                                                                                                                                                                                                 |
| .../modifications/patches/default-page-layout.patch                                      | Removed JavaScript notification pop-ups and user navigation bell icon. Added a `<uui-toaster>` web component for notifications. Added PHP session initialization at the start of the page.                                                                                                                    |
| .../modifications/patches/sso.patch                                                      | Introduced `verifyUsernamePasswordAndSSO` for login, supporting both password and SSO token authentication. Updated login logic and UI accordingly.                                                                                                                                            |
| .../modifications/default-page-layout.modification.ts                                    | Added methods to prepend PHP session logic before the DOCTYPE in HTML. Ensured this logic is applied during file modification asynchronously.                                                                                                                           |
| api/src/core/utils/validation/is-gui-mode.ts                                            | Added an async utility function `isGuiMode` to detect if the system is running in GUI mode by checking for the "slim" process.                                                                                                                                                                                                                                         |
| justfile                                                                                 | Removed the `just unraid-ui/setup` invocation from the `setup` recipe.                                                                                                                                                                                                                                                                                                                                               |
| unraid-ui/package.json                                                                  | Added a "prepare" npm script that runs `pnpm build`.                                                                                                                                                                                                                                                                                                                                                                |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant LoginPage (PHP)
    participant System (Shadow/SSO)
    participant Session

    User->>LoginPage (PHP): Submit username & password
    LoginPage (PHP)->>System (Shadow/SSO): verifyUsernamePasswordAndSSO()
    alt Credentials valid
        LoginPage (PHP)->>Session: Start session, set variables
        LoginPage (PHP)-->>User: Redirect to dashboard
    else Invalid credentials
        LoginPage (PHP)-->>User: Show error message
    end
```

```mermaid
sequenceDiagram
    participant Browser
    participant DefaultPageLayout (PHP)

    Browser->>DefaultPageLayout (PHP): Load page
    DefaultPageLayout (PHP)->>DefaultPageLayout (PHP): Check if session exists
    alt No session
        DefaultPageLayout (PHP)->>DefaultPageLayout (PHP): Start session, set variables
    end
    DefaultPageLayout (PHP)-->>Browser: Render page
    Browser->>Browser: On visibilitychange event
    alt $myPage['Load'] > 0
        Browser->>Browser: Reload page
    else
        Browser->>Browser: nchanFocusStart()
    end
```

## Suggested reviewers

- pujitm  
- mdatelle

## Poem

> 🎉  
> The two-factor dance has left the floor,  
> Now login’s simple—nothing more!  
> Sessions start with just a glance,  
> Notifications toast and twirl, not prance.  
> SSO joins the password song,  
> And "prepare" builds as we go along.  
> Streamlined code, a cleaner day—  
> Let’s celebrate the Unraid way!  
>

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 753c6ed645dd530e8035a553fddbf27468bba962 and 173c4e29d8d4d1cc4c59cddd7aacda85de62571e.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php` (2 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts` (2 hunks)
* `api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts
* api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php
* api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Build Web App
* GitHub Check: Test API
* GitHub Check: Build API
* GitHub Check: Cloudflare Pages

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=unraid/api&utm_content=1368):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-01T03:27:31Z
body: This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:
```
https://preview.dl.unraid.net/unraid-api/tag/PR1368/dynamix.unraid.net.plg
```
<!-- thollander/actions-comment-pull-request "prlink" -->
author_association: CONTRIBUTOR