diff --git a/docker-compose.yml b/docker-compose.yml index c258192b..e6e116a5 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +version: "3" services: netalertx: privileged: true @@ -63,7 +64,9 @@ services: # DELETE END anyone trying to use this file: comment out / delete ABOVE lines, they are only for development purposes # --------------------------------------------------------------------------- environment: + # - APP_CONF_OVERRIDE={"SCAN_SUBNETS":"['192.168.1.0/24 --interface=eth1']","UI_dark_mode":"True"} - TZ=${TZ} - - PORT=${PORT} + - PORT=${PORT} # ❗ DANGER ZONE BELOW - Setting ALWAYS_FRESH_INSTALL=true will delete the content of the /db & /config folders - ALWAYS_FRESH_INSTALL=${ALWAYS_FRESH_INSTALL} + diff --git a/dockerfiles/setup.sh b/dockerfiles/setup.sh index 1b3e50f5..63b0897b 100755 --- a/dockerfiles/setup.sh +++ b/dockerfiles/setup.sh @@ -41,6 +41,16 @@ if [ "$ALWAYS_FRESH_INSTALL" = true ]; then rm -rf "$INSTALL_DIR_OLD/db/"* fi +# OVERRIDE settings: Handling APP_CONF_OVERRIDE +# Check if APP_CONF_OVERRIDE is set +if [ -z "$APP_CONF_OVERRIDE" ]; then + echo "APP_CONF_OVERRIDE is not set. Skipping config file creation." +else + # Save the APP_CONF_OVERRIDE env variable as a JSON file + echo "$APP_CONF_OVERRIDE" > "${INSTALL_DIR}/config/app_conf_override.json" + echo "Config file saved to ${INSTALL_DIR}/config/app_conf_override.json" +fi + # 🔻 FOR BACKWARD COMPATIBILITY - REMOVE AFTER 12/12/2024 # Check if pialert.db exists, then create a symbolic link to app.db diff --git a/docs/DATABASE.md b/docs/DATABASE.md index fedef963..60665c0f 100755 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -17,7 +17,7 @@ | Pholus_Scan | Scan results of the Pholus python network penetration script. | ![Screen8][screen8] | | Plugins_Events | For capturing events exposed by a plugin via the `last_result.log` file. If unique then saved into the `Plugins_Objects` table. Entries are deleted once processed and stored in the `Plugins_History` and/or `Plugins_Objects` tables. | ![Screen10][screen10] | | Plugins_History | History of all entries from the `Plugins_Events` table | ![Screen11][screen11] | - | Plugins_Language_Strings | Language strings colelcted from the plugin `config.json` files used for string resolution in the frontend. | ![Screen12][screen12] | + | Plugins_Language_Strings | Language strings collected from the plugin `config.json` files used for string resolution in the frontend. | ![Screen12][screen12] | | Plugins_Objects | Unique objects detected by individual plugins. | ![Screen13][screen13] | | Sessions | Used to display sessions in the charts | ![Screen15][screen15] | | Settings | Database representation of the sum of all settings from `app.conf` and plugins coming from `config.json` files. | ![Screen16][screen16] | diff --git a/front/deviceDetails.php b/front/deviceDetails.php index 2847d15f..0669e778 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -693,7 +693,6 @@ if ($ENABLED_DARKMODE === True) { var pos = -1; var parPeriod = 'Front_Details_Period'; - var parTab = 'Front_Details_Tab'; var parSessionsRows = 'Front_Details_Sessions_Rows'; var parEventsRows = 'Front_Details_Events_Rows'; var parEventsHide = 'Front_Details_Events_Hide'; @@ -736,7 +735,7 @@ function main () { $('#chkHideConnectionEvents')[0].checked = eval(eventsHide == 'true'); // Initialize components with parameters - initializeTabs(); + initializeTabsNew(); initializeiCheck(); initializeCombos(); initializeDatatables(); @@ -763,17 +762,6 @@ function main () { } -// ----------------------------------------------------------------------------- -function initializeTabs () { - // Activate panel - $('.nav-tabs a[id='+ tab +']').tab('show'); - - // When changed save new current tab - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - setParameter (parTab, $(e.target).attr('id')); - }); -} - // ----------------------------------------------------------------------------- function initializeiCheck () { // Blue diff --git a/front/plugins/ui_settings/config.json b/front/plugins/ui_settings/config.json index 499778b3..def2e5c4 100755 --- a/front/plugins/ui_settings/config.json +++ b/front/plugins/ui_settings/config.json @@ -26,6 +26,275 @@ ], "params": [], "settings": [ + { + "function": "NOT_RANDOM_MAC", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { "placeholder": "Enter value" }, + { "suffix": "_in" }, + { "cssClasses": "col-sm-10" }, + { "prefillValue": "null" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": ["_in"] }, + { "separator": "" }, + { "cssClasses": "col-xs-12" }, + { "onClick": "addList(this,false)" }, + { "getStringKey": "Gen_Add" } + ], + "transformers": [] + }, + { + "elementType": "select", + "elementHasInputValue": 1, + "elementOptions": [ + { "multiple": "true" }, + { "readonly": "true" }, + { "editable": "true" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeAllOptions(this)" }, + { "getStringKey": "Gen_Remove_All" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeFromList(this)" }, + { "getStringKey": "Gen_Remove_Last" } + ], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": [], + "options": [], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, + { + "function": "ICONS", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { "placeholder": "Enter value" }, + { "suffix": "_in" }, + { "cssClasses": "col-sm-10" }, + { "prefillValue": "null" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": ["_in"] }, + { "separator": "" }, + { "cssClasses": "col-xs-12" }, + { "onClick": "addList(this,false)" }, + { "getStringKey": "Gen_Add" } + ], + "transformers": [] + }, + { + "elementType": "select", + "elementHasInputValue": 1, + "elementOptions": [ + { "multiple": "true" }, + { "readonly": "true" }, + { "editable": "true" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeAllOptions(this)" }, + { "getStringKey": "Gen_Remove_All" } + ], + "transformers": [] + }, + { + "elementType": "button", + "elementOptions": [ + { "sourceSuffixes": [] }, + { "separator": "" }, + { "cssClasses": "col-xs-6" }, + { "onClick": "removeFromList(this)" }, + { "getStringKey": "Gen_Remove_Last" } + ], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": [ + "PGkgY2xhc3M9J2ZhIGZhLXdpZmknPjwvaT4=", + "PGkgY2xhc3M9ImZhIGZhLWNvbXB1dGVyIj48L2k+", + "PGkgY2xhc3M9ImZhIGZhLWV0aGVybmV0Ij48L2k+", + "PGkgY2xhc3M9ImZhIGZhLWdhbWVwYWQiPjwvaT4", + "PGkgY2xhc3M9ImZhIGZhLWdsb2JlIj48L2k+", + "PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==", + "PGkgY2xhc3M9ImZhIGZhLWxpZ2h0YnVsYiI+PC9pPg==", + "PGkgY2xhc3M9ImZhIGZhLXNoaWVsZCI+PC9pPg==", + "PGkgY2xhc3M9ImZhIGZhLXdpZmkiPjwvaT4", + "PGkgY2xhc3M9J2ZhIGZhLWdhbWVwYWQnPjwvaT4" + ], + "options": [], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, + { + "function": "REFRESH", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "number" }], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": 0, + "options": [], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, + { + "function": "DEV_SECTIONS", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "select", + "elementOptions": [{ "multiple": "true" }], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": [], + "options": ["Tile Cards", "Device Presence"], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, + { + "function": "PRESENCE", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "select", + "elementOptions": [{ "multiple": "true" }], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": ["online", "offline", "archived"], + "options": ["online", "offline", "archived"], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, + { + "function": "MY_DEVICES", + "type": { + "dataType": "array", + "elements": [ + { + "elementType": "select", + "elementOptions": [{ "multiple": "true" }], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": ["online", "offline", "archived", "new", "down"], + "options": ["online", "offline", "archived", "new", "down"], + "localized": [], + "name": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ], + "description": [ + { + "string": "_GLOBAL_LANG_FILES_" + } + ] + }, { "function": "device_columns", "type": { @@ -83,7 +352,7 @@ "options": [ "Device_TableHead_Name", "Device_TableHead_Owner", - "Device_TableHead_Type", + "Device_TableHead_Type", "Device_TableHead_Icon", "Device_TableHead_Favorite", "Device_TableHead_Group", diff --git a/server/const.py b/server/const.py index aa100a3a..4b0e043e 100755 --- a/server/const.py +++ b/server/const.py @@ -15,6 +15,7 @@ pluginsPath = applicationPath + '/front/plugins' logPath = applicationPath + '/front/log' apiPath = applicationPath + '/front/api/' reportTemplatesPath = applicationPath + '/front/report_templates/' +fullConfFolder = applicationPath + '/config' fullConfPath = applicationPath + confPath fullDbPath = applicationPath + dbPath vendorsPath = '/usr/share/arp-scan/ieee-oui.txt' diff --git a/server/initialise.py b/server/initialise.py index 72be014b..cb02366b 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -11,8 +11,8 @@ import re import conf -from const import fullConfPath, applicationPath -from helper import collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject, updateState, setting_value_to_python_type, timeNowTZ +from const import fullConfPath, applicationPath, fullConfFolder +from helper import collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject, updateState, setting_value_to_python_type, timeNowTZ, get_setting_value from logger import mylog from api import update_api from scheduler import schedule_class @@ -29,7 +29,8 @@ from notification import write_notification # Check config dictionary #------------------------------------------------------------------------------- -def ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", regex="", setJsonMetadata=None, overrideTemplate=None): +# managing application settings, ensuring SQL safety for user input, and updating internal configuration lists +def ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", regex="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False): if events is None: events = [] if setJsonMetadata is None: @@ -41,7 +42,7 @@ def ccd(key, default, config_dir, name, inputtype, options, group, events=None, result = default # Use existing value if already supplied, otherwise default value is used - if key in config_dir: + if forceDefault == False and key in config_dir: result = config_dir[key] # Single quotes might break SQL queries, replacing them @@ -70,16 +71,20 @@ def update_or_append(settings_list, item_tuple, key): if settings_list is None: settings_list = [] - # mylog('debug', ['[Import Config] update_or_append debug ']) - # mylog('debug', ['[Import Config] update_or_append ', settings_list]) - # mylog('debug', ['[Import Config] update_or_append item_tuple ' , item_tuple]) - for index, item in enumerate(settings_list): if item[0] == key: - settings_list[index] = item_tuple - mylog('debug', ['[Import Config] FOUND key : ', key]) - return settings_list + mylog('trace', ['[Import Config] OLD TUPLE : ', item]) + # Replace only non-empty values in the tuple + updated_tuple = tuple( + new_val if new_val != "_KEEP_" else old_val + for old_val, new_val in zip(item, item_tuple) + ) + mylog('trace', ['[Import Config] NEW TUPLE : ', updated_tuple]) + settings_list[index] = updated_tuple + mylog('trace', ['[Import Config] FOUND key : ', key]) + return settings_list + settings_list.append(item_tuple) return settings_list @@ -145,13 +150,7 @@ def importConfigs (db, all_plugins): conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', '[]', 'General') # UI - conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'French', 'German', 'Norwegian', 'Russian', 'Spanish', 'Italian (it_it)', 'Portuguese (pt_br)', 'Polish (pl_pl)', 'Turkish (tr_tr)', 'Chinese (zh_cn)', 'Czech (cs_cz)' ]", 'UI') - conf.UI_NOT_RANDOM_MAC = ccd('UI_NOT_RANDOM_MAC', [] , c_d, 'Exlude from Random Prefix', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', "[]", 'UI') - conf.UI_ICONS = ccd('UI_ICONS', ['PGkgY2xhc3M9J2ZhIGZhLXdpZmknPjwvaT4=', 'PGkgY2xhc3M9ImZhIGZhLWNvbXB1dGVyIj48L2k+', 'PGkgY2xhc3M9ImZhIGZhLWV0aGVybmV0Ij48L2k+', 'PGkgY2xhc3M9ImZhIGZhLWdhbWVwYWQiPjwvaT4', 'PGkgY2xhc3M9ImZhIGZhLWdsb2JlIj48L2k+', 'PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==', 'PGkgY2xhc3M9ImZhIGZhLWxpZ2h0YnVsYiI+PC9pPg==', 'PGkgY2xhc3M9ImZhIGZhLXNoaWVsZCI+PC9pPg==', 'PGkgY2xhc3M9ImZhIGZhLXdpZmkiPjwvaT4', 'PGkgY2xhc3M9J2ZhIGZhLWdhbWVwYWQnPjwvaT4'] , c_d, 'Icons', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', "[]", 'UI') - conf.UI_REFRESH = ccd('UI_REFRESH', 0 , c_d, 'Refresh interval', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', "[]", 'UI') - conf.UI_DEV_SECTIONS = ccd('UI_DEV_SECTIONS', [] , c_d, 'Show sections', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true"}] ,"transformers": []}]}', "['Tile Cards', 'Device Presence']", 'UI') - conf.UI_PRESENCE = ccd('UI_PRESENCE', ['online', 'offline', 'archived'] , c_d, 'Include in presence', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true"}] ,"transformers": []}]}', "['online', 'offline', 'archived']", 'UI') - conf.UI_MY_DEVICES = ccd('UI_MY_DEVICES', ['online', 'offline', 'archived', 'new', 'down'] , c_d, 'Include in My Devices', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true"}] ,"transformers": []}]}', "['online', 'offline', 'archived', 'new', 'down']", 'UI') + conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'French', 'German', 'Norwegian', 'Russian', 'Spanish', 'Italian (it_it)', 'Portuguese (pt_br)', 'Polish (pl_pl)', 'Turkish (tr_tr)', 'Chinese (zh_cn)', 'Czech (cs_cz)' ]", 'UI') # Init timezone in case it changed conf.tz = timezone(conf.TIMEZONE) @@ -297,18 +296,53 @@ def importConfigs (db, all_plugins): conf.plugins_once_run = False # ----------------- # Plugins END + + # TODO check app_conf_override.json + # Assuming fullConfFolder is defined elsewhere + app_conf_override_path = fullConfFolder + '/app_conf_override.json' + + if os.path.exists(app_conf_override_path): + with open(app_conf_override_path, 'r') as f: + try: + # Load settings_override from the JSON file + settings_override = json.load(f) + + # Loop through settings_override dictionary + for setting_name, value in settings_override.items(): + # Ensure the value is treated as a string and passed directly + if isinstance(value, str): + # Log the value being passed + # ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", regex="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False) + mylog('debug', [f"[Config] Setting override {setting_name} with value: {value}"]) + ccd(setting_name, value, c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", "", None, None, True) + else: + # Convert to string and log + # ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", regex="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False) + mylog('debug', [f"[Config] Setting override {setting_name} with value: {str(value)}"]) + ccd(setting_name, str(value), c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", "", None, None, True) + + except json.JSONDecodeError: + mylog('none', [f"[Config] [ERROR] Setting override decoding JSON from {app_conf_override_path}"]) + else: + mylog('debug', [f"[Config] File {app_conf_override_path} does not exist."]) # Check if app was upgraded with open(applicationPath + '/front/buildtimestamp.txt', 'r') as f: - buildTimestamp = int(f.read().strip()) - if str(conf.VERSION) != str(buildTimestamp): + buildTimestamp = int(f.read().strip()) + cur_version = conf.VERSION + + mylog('debug', [f"[Config] buildTimestamp: '{buildTimestamp}'"]) + mylog('debug', [f"[Config] conf.VERSION : '{cur_version}'"]) + + if str(cur_version) != str(buildTimestamp): - mylog('none', ['[Config] App upgraded 🚀']) + mylog('none', ['[Config] App upgraded 🚀']) + + # ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", regex="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False) + ccd('VERSION', buildTimestamp , c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", "", None, None, True) - conf.VERSION = ccd('VERSION', buildTimestamp , c_d, 'Version', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [{ "readonly": "true" }] ,"transformers": []}]}', '', 'General') - - write_notification(f'[Upgrade] : App upgraded 🚀 Please clear the caches: