diff --git a/client/src/main.js b/client/src/main.js
index c3e1d153..091e524d 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -13,6 +13,7 @@ import router from './router';
import setupHttpInterceptor from './js/http-interceptor';
import { getWebSocketURL, isElectron } from '@/js/platform';
import { initRemoteLogging } from '@/js/logger';
+import log from 'loglevel';
import './assets/styles/dark.scss';
import 'vue-toast-notification/dist/theme-sugar.css';
@@ -22,6 +23,8 @@ import 'splitpanes/dist/splitpanes.css';
setupHttpInterceptor();
initRemoteLogging();
+log.info(`Running in ${import.meta.env.MODE} mode.`);
+
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);
Vue.component('MultiSelect', Multiselect);
diff --git a/client/src/store/modules/system.js b/client/src/store/modules/system.js
index b0c7bec6..2cb19096 100644
--- a/client/src/store/modules/system.js
+++ b/client/src/store/modules/system.js
@@ -9,6 +9,7 @@ export default {
availableShows: [],
rawSettings: {},
rbacRoles: [],
+ settingsCategories: {},
},
mutations: {
UPDATE_SETTINGS(state, settings) {
@@ -23,6 +24,9 @@ export default {
UPDATE_RBAC_ROLES(state, rbac) {
state.rbacRoles = rbac;
},
+ UPDATE_SETTINGS_CATEGORIES(state, categories) {
+ state.settingsCategories = categories;
+ },
},
actions: {
async GET_AVAILABLE_SHOWS(context) {
@@ -88,6 +92,15 @@ export default {
log.error('Unable to fetch RBAC roles');
}
},
+ async GET_SETTINGS_CATEGORIES(context) {
+ const response = await fetch(makeURL('/api/v1/settings/categories'));
+ if (response.ok) {
+ const categories = await response.json();
+ await context.commit('UPDATE_SETTINGS_CATEGORIES', categories.categories);
+ } else {
+ log.error('Unable to fetch settings categories');
+ }
+ },
},
getters: {
AVAILABLE_SHOWS(state) {
@@ -102,5 +115,8 @@ export default {
RBAC_ROLES(state) {
return state.rbacRoles;
},
+ SETTINGS_CATEGORIES(state) {
+ return state.settingsCategories;
+ },
},
};
diff --git a/client/src/vue_components/config/ConfigSettings.vue b/client/src/vue_components/config/ConfigSettings.vue
index fbf7f9af..b8b917e7 100644
--- a/client/src/vue_components/config/ConfigSettings.vue
+++ b/client/src/vue_components/config/ConfigSettings.vue
@@ -9,62 +9,96 @@
@reset.stop.prevent="resetForm(true)"
>
-
+
+
+
+
+
+
+
+ {{ setting.display_name }}
+
+
+ {{ key }}
+
+
+
+
+ {{ setting.help_text }}
+
+
+
+
+
+
+
+
+
+ Unknown setting type {{ setting.type }} for setting {{ key }}.
+
+
+
+
+
+
-
-
-
- {{ setting.display_name }}
-
-
- {{ key }}
-
-
-
-
- {{ setting.help_text }}
-
-
-
-
-
-
-
-
-
- Unknown setting type {{ setting.type }} for setting {{ key }}.
-
-
-
Reset
Submit
@@ -80,7 +114,7 @@
diff --git a/server/controllers/api/settings.py b/server/controllers/api/settings.py
index 4a0fefc3..4a203eeb 100644
--- a/server/controllers/api/settings.py
+++ b/server/controllers/api/settings.py
@@ -41,6 +41,13 @@ async def patch(self):
self.write({"message": "Settings updated"})
+@ApiRoute("settings/categories", ApiVersion.V1)
+class SettingsCategoriesController(BaseAPIController):
+ @allow_when_password_required
+ async def get(self):
+ await self.finish({"categories": self.application.digi_settings.categories})
+
+
@ApiRoute("settings/raw", ApiVersion.V1)
class RawSettingsController(BaseAPIController):
async def get(self):
diff --git a/server/digi_server/settings.py b/server/digi_server/settings.py
index 2f582c50..8d1274f8 100644
--- a/server/digi_server/settings.py
+++ b/server/digi_server/settings.py
@@ -4,7 +4,7 @@
import os
import tomllib
from pathlib import Path
-from typing import TYPE_CHECKING, Dict, Optional
+from typing import TYPE_CHECKING, Dict, List, Optional
from tornado.locks import Lock
@@ -167,8 +167,17 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
)
os.makedirs(os.path.dirname(self.settings_path))
+ self.categories: Dict[str : List[str]] = {"General": []}
self.settings: Dict[str, SettingsObject] = {}
+ self.init_settings()
+ self._load(spawn_callbacks=False)
+ self._file_watcher = IOLoopFileWatcher(
+ self.settings_path, self.auto_reload_changes, 100
+ )
+ self._file_watcher.add_error_callback(self.file_deleted)
+ self._file_watcher.watch()
+ def init_settings(self):
db_default = f"sqlite:///{os.path.join(os.path.dirname(__file__), '../conf/digiscript.sqlite')}"
self.define(
"has_admin_user",
@@ -205,6 +214,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
self._application.regen_logging,
display_name="Log Level",
choice_options=get_level_names_by_order(),
+ category="Logging",
)
self.define(
"log_path",
@@ -213,6 +223,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Application Log Path",
+ category="Logging",
)
self.define(
"max_log_mb",
@@ -221,6 +232,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Max Log Size (MB)",
+ category="Logging",
)
self.define(
"log_backups",
@@ -229,6 +241,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Log Backups",
+ category="Logging",
)
self.define(
"log_redaction",
@@ -237,6 +250,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
display_name="Enable Log Redaction",
help_text="When enabled, potentially sensitive information will be redacted from logs.",
+ category="Logging",
)
self.define(
"db_log_enabled",
@@ -245,6 +259,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Enable Database Log",
+ category="DB Logging",
)
self.define(
"db_log_path",
@@ -253,6 +268,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Database Log Path",
+ category="DB Logging",
)
self.define(
"db_max_log_mb",
@@ -261,6 +277,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Max Database Log Size (MB)",
+ category="DB Logging",
)
self.define(
"db_log_backups",
@@ -269,6 +286,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
self._application.regen_logging,
display_name="Database Log Backups",
+ category="DB Logging",
)
self.define(
"compiled_script_path",
@@ -294,6 +312,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
True,
display_name="Enable Client Log Forwarding",
help_text="When enabled, client browsers will forward their logs to the server.",
+ category="Client Logging",
)
self.define(
"client_log_level",
@@ -304,6 +323,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
display_name="Client Log Level",
help_text="Minimum log level that clients will forward to the server.",
choice_options=["TRACE", "DEBUG", "INFO", "WARN", "ERROR"],
+ category="Client Logging",
)
self.define(
"client_log_path",
@@ -313,6 +333,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
self._application.regen_logging,
display_name="Client Log Path",
help_text="Path to the log file for client-side log messages.",
+ category="Client Logging",
)
self.define(
"client_max_log_mb",
@@ -322,6 +343,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
self._application.regen_logging,
display_name="Max Client Log Size (MB)",
help_text="Maximum size in MB of the client log file before it is rotated.",
+ category="Client Logging",
)
self.define(
"client_log_backups",
@@ -331,6 +353,7 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
self._application.regen_logging,
display_name="Client Log Backups",
help_text="Number of rotated client log file backups to retain.",
+ category="Client Logging",
)
self.define(
"log_buffer_size",
@@ -341,16 +364,9 @@ def __init__(self, application: DigiScriptServer, settings_path=None):
display_name="Log Buffer Size",
help_text="Number of recent log entries to keep in memory for the log viewer. "
"Larger values use more memory. Changes take effect after restart.",
+ category="Client Logging",
)
- self._load(spawn_callbacks=False)
-
- self._file_watcher = IOLoopFileWatcher(
- self.settings_path, self.auto_reload_changes, 100
- )
- self._file_watcher.add_error_callback(self.file_deleted)
- self._file_watcher.watch()
-
def define(
self,
key,
@@ -363,7 +379,11 @@ def define(
help_text: str = "",
hide_from_ui: bool = False,
choice_options: Optional[list] = None,
+ category: str = "General",
):
+ if key in self.settings:
+ raise KeyError(f"Setting {key} is already defined")
+
self.settings[key] = SettingsObject(
key,
val_type,
@@ -376,6 +396,10 @@ def define(
hide_from_ui,
choice_options,
)
+ if category not in self.categories:
+ self.categories[category] = [key]
+ else:
+ self.categories[category].append(key)
def file_deleted(self):
get_logger().info("Settings file deleted; recreating from in memory settings")
diff --git a/server/main.py b/server/main.py
index 3da787f7..80e8994d 100755
--- a/server/main.py
+++ b/server/main.py
@@ -81,6 +81,8 @@ def patched_define(
display_name="",
help_text="",
hide_from_ui=False,
+ choice_options=None,
+ category="General",
):
# For database path, adjust to a writable location if needed
if key == "db_path" and default and isinstance(default, str):
@@ -106,6 +108,8 @@ def patched_define(
display_name,
help_text,
hide_from_ui,
+ choice_options,
+ category,
)
# Apply the patch