diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py
index 310309a890..72e81f04d2 100644
--- a/InvenTree/InvenTree/config.py
+++ b/InvenTree/InvenTree/config.py
@@ -7,6 +7,7 @@ import os
 import random
 import shutil
 import string
+import warnings
 from pathlib import Path
 
 logger = logging.getLogger('inventree')
@@ -341,3 +342,58 @@ def get_custom_file(env_ref: str, conf_ref: str, log_ref: str, lookup_media: boo
         value = False
 
     return value
+
+
+def get_frontend_settings(debug=True):
+    """Return a dictionary of settings for the frontend interface.
+
+    Note that the new config settings use the 'FRONTEND' key,
+    whereas the legacy key was 'PUI' (platform UI) which is now deprecated
+    """
+
+    # Legacy settings
+    pui_settings = get_setting('INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict)
+
+    if len(pui_settings) > 0:
+        warnings.warn(
+            "The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead",
+            DeprecationWarning, stacklevel=2
+        )
+
+    # New settings
+    frontend_settings = get_setting('INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict)
+
+    # Merge settings
+    settings = {**pui_settings, **frontend_settings}
+
+    # Set the base URL
+    if 'base_url' not in settings:
+        base_url = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', '')
+
+        if base_url:
+            warnings.warn(
+                "The 'INVENTREE_PUI_URL_BASE' key is deprecated. Please use 'INVENTREE_FRONTEND_URL_BASE' instead",
+                DeprecationWarning, stacklevel=2
+            )
+        else:
+            base_url = get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform')
+
+        settings['base_url'] = base_url
+
+    # Set the server list
+    settings['server_list'] = settings.get('server_list', [])
+
+    # Set the debug flag
+    settings['debug'] = debug
+
+    if 'environment' not in settings:
+        settings['environment'] = 'development' if debug else 'production'
+
+    if debug and 'show_server_selector' not in settings:
+        # In debug mode, show server selector by default
+        settings['show_server_selector'] = True
+    elif len(settings['server_list']) == 0:
+        # If no servers are specified, show server selector
+        settings['show_server_selector'] = True
+
+    return settings
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
index c34c5416e8..7fac82e13f 100644
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object):
             elif request.path_info.startswith('/accounts/'):
                 authorized = True
 
-            elif request.path_info.startswith(f'/{settings.PUI_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.PUI_URL_BASE}':
+            elif request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.FRONTEND_URL_BASE}':
                 authorized = True
 
             elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 206b4a697b..a808f6d603 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -1044,9 +1044,9 @@ CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', '
 
 CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
 
-# Frontend settings
-PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform')
-PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {})
+# Load settings for the frontend interface
+FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
+FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
 
 if DEBUG:
     logger.info("InvenTree running with DEBUG enabled")
@@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS:
 
 # Magic login django-sesame
 SESAME_MAX_AGE = 300
-# LOGIN_REDIRECT_URL = f"/{PUI_URL_BASE}/logged-in/"
+# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
 LOGIN_REDIRECT_URL = "/index/"
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 5687d5f7a6..584f69a246 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase):
         self.assertEqual(resp.url, '/index/')
         # Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet
         # TODO: In the future, the URL comparison will need to be reverted
-        # self.assertEqual(resp.url, f'/{settings.PUI_URL_BASE}/logged-in/')
+        # self.assertEqual(resp.url, f'/{settings.FRONTEND_URL_BASE}/logged-in/')
         # And we should be logged in again
         self.assertEqual(resp.wsgi_request.user, self.user)
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 5755949cac..1e51863a68 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER
 #   logo: img/custom_logo.png
 #   splash: img/custom_splash.jpg
 
-# Platform UI options
-# pui_settings:
+# Frontend UI settings
+# frontend_settings:
+#   base_url: 'frontend'
 #   server_list:
 #     my_server1:
-#       host: https://demo.inventree.org/api/
+#       host: https://demo.inventree.org/
 #       name: InvenTree Demo
 #   default_server: my_server1
 #   show_server_selector: false
 #   sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
 #   environment: development
-# Base URL for serving Platform UI
-# pui_url_base: 'platform'
 
 # Custom flags
 # InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py
index 7554158b72..95d59a6e82 100644
--- a/InvenTree/users/admin.py
+++ b/InvenTree/users/admin.py
@@ -6,7 +6,6 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.contrib.auth import get_user_model
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import Group
-from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
 
 from users.models import ApiToken, Owner, RuleSet
@@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin):  # pragma: no cover
         users = form.cleaned_data['users']
 
         # Check for users who are members of multiple groups
-        warning_message = ''
+        multiple_group_users = []
+
         for user in users:
             if user.groups.all().count() > 1:
-                warning_message += f'<br>- <b>{user.username}</b> is member of: '
-                for idx, group in enumerate(user.groups.all()):
-                    warning_message += f'<b>{group.name}</b>'
-                    if idx < len(user.groups.all()) - 1:
-                        warning_message += ', '
+                multiple_group_users.append(user.username)
 
         # If any, display warning message when group is saved
-        if warning_message:
-            warning_message = mark_safe(_(f'The following users are members of multiple groups:'
-                                          f'{warning_message}'))
-            messages.add_message(request, messages.WARNING, warning_message)
+        if len(multiple_group_users) > 0:
+
+            msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users)
+
+            messages.add_message(
+                request,
+                messages.WARNING,
+                msg
+            )
 
     def save_formset(self, request, form, formset, change):
         """Save the inline formset"""
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index cce30ecc91..803ed1c614 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -422,11 +422,7 @@ class RuleSet(models.Model):
         """Construct the correctly formatted permission string, given the app_model name, and the permission type."""
         model, app = split_model(model)
 
-        return "{app}.{perm}_{model}".format(
-            app=app,
-            perm=permission,
-            model=model
-        )
+        return f"{app}.{permission}_{model}"
 
     def __str__(self, debug=False):  # pragma: no cover
         """Ruleset string representation."""
@@ -504,12 +500,7 @@ def update_group_roles(group, debug=False):
     # and create a simplified permission key string
     for p in group.permissions.all().prefetch_related('content_type'):
         (permission, app, model) = p.natural_key()
-
-        permission_string = '{app}.{perm}'.format(
-            app=app,
-            perm=permission
-        )
-
+        permission_string = f"{app}.{permission}"
         group_permissions.add(permission_string)
 
     # List of permissions which must be added to the group
@@ -527,7 +518,7 @@ def update_group_roles(group, debug=False):
             allowed: Whether or not the action is allowed
         """
         if action not in ['view', 'add', 'change', 'delete']:  # pragma: no cover
-            raise ValueError("Action {a} is invalid".format(a=action))
+            raise ValueError(f"Action {action} is invalid")
 
         permission_string = RuleSet.get_model_permission_string(model, action)
 
diff --git a/InvenTree/web/templatetags/spa_helper.py b/InvenTree/web/templatetags/spa_helper.py
index 4d8ebd9ae7..edea064d89 100644
--- a/InvenTree/web/templatetags/spa_helper.py
+++ b/InvenTree/web/templatetags/spa_helper.py
@@ -1,4 +1,5 @@
 """Template tag to render SPA imports."""
+
 import json
 from logging import getLogger
 from pathlib import Path
@@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe
 logger = getLogger("InvenTree")
 register = template.Library()
 
-PUI_DEFAULTS = {
-    'url_base': settings.PUI_URL_BASE,
-}
-PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {}))
-PUI_SETTINGS = json.dumps(PUI_DEFAULTS)
+FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
 
 
 @register.simple_tag
@@ -30,11 +27,11 @@ def spa_bundle():
     index = manifest_data.get("index.html")
     css_index = manifest_data.get("index.css")
 
-    dynmanic_files = index.get("dynamicImports", [])
+    dynamic_files = index.get("dynamicImports", [])
     imports_files = "".join(
         [
             f'<script type="module" src="{settings.STATIC_URL}web/{manifest_data[file]["file"]}"></script>'
-            for file in dynmanic_files
+            for file in dynamic_files
         ]
     )
 
@@ -47,4 +44,4 @@ def spa_bundle():
 @register.simple_tag
 def spa_settings():
     """Render settings for spa."""
-    return mark_safe(f"""<script>window.INVENTREE_SETTINGS={PUI_SETTINGS}</script>""")
+    return mark_safe(f"""<script>window.INVENTREE_SETTINGS={FRONTEND_SETTINGS}</script>""")
diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py
index d1e409eeab..35726815ac 100644
--- a/InvenTree/web/urls.py
+++ b/InvenTree/web/urls.py
@@ -20,12 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html
 
 
 urlpatterns = [
-    path(f'{settings.PUI_URL_BASE}/', include([
+    path(f'{settings.FRONTEND_URL_BASE}/', include([
         path("assets/<path:path>", RedirectAssetView.as_view()),
         re_path(r"^(?P<path>.*)/$", spa_view),
         path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
         path("", spa_view),]
     )),
-    path(settings.PUI_URL_BASE, spa_view, name='platform'),
+    path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
     path("assets/<path:path>", RedirectAssetView.as_view()),
 ]
diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx
index 9f7a3657f8..c9477de5e6 100644
--- a/src/frontend/src/functions/auth.tsx
+++ b/src/frontend/src/functions/auth.tsx
@@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => {
   return true;
 };
 
+/**
+ * Logout the user (invalidate auth token)
+ */
 export const doClassicLogout = async () => {
   // TODO @matmair - logout from the server session
   // Set token in context
diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx
index 48afa14def..e0e7fd44bb 100644
--- a/src/frontend/src/main.tsx
+++ b/src/frontend/src/main.tsx
@@ -14,7 +14,7 @@ declare global {
       server_list: HostList;
       default_server: string;
       show_server_selector: boolean;
-      url_base: string;
+      base_url: string;
       sentry_dsn?: string;
       environment?: string;
     };
@@ -27,20 +27,20 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
 
 window.INVENTREE_SETTINGS = {
   server_list: {
-    'mantine-cqj63coxn': {
+    localhost: {
       host: `${window.location.origin}/`,
       name: 'Current Server'
     },
     ...(IS_DEV_OR_DEMO
       ? {
-          'mantine-u56l5jt85': {
+          demo: {
             host: 'https://demo.inventree.org/',
             name: 'InvenTree Demo'
           }
         }
       : {})
   },
-  default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', // use demo server for demo mode
+  default_server: IS_DEMO ? 'demo' : 'localhost',
   show_server_selector: IS_DEV_OR_DEMO,
 
   // merge in settings that are already set via django's spa_view or for development
@@ -56,7 +56,7 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) {
   });
 }
 
-export const url_base = window.INVENTREE_SETTINGS.url_base || 'platform';
+export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform';
 
 ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
   <React.StrictMode>
@@ -66,5 +66,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
 
 // Redirect to base url if on /
 if (window.location.pathname === '/') {
-  window.location.replace(`/${url_base}`);
+  window.location.replace(`/${base_url}`);
 }
diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx
index 379343bc61..d704f4bf2b 100644
--- a/src/frontend/src/views/DesktopAppView.tsx
+++ b/src/frontend/src/views/DesktopAppView.tsx
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
 import { queryClient, setApiDefaults } from '../App';
 import { BaseContext } from '../contexts/BaseContext';
 import { defaultHostList } from '../defaults/defaultHostList';
-import { url_base } from '../main';
+import { base_url } from '../main';
 import { routes } from '../router';
 import { useLocalState } from '../states/LocalState';
 import { useSessionState } from '../states/SessionState';
@@ -49,7 +49,7 @@ export default function DesktopAppView() {
   return (
     <BaseContext>
       <QueryClientProvider client={queryClient}>
-        <BrowserRouter basename={url_base}>{routes}</BrowserRouter>
+        <BrowserRouter basename={base_url}>{routes}</BrowserRouter>
       </QueryClientProvider>
     </BaseContext>
   );