Update to customFiles - Object instead of array (#7384)

* Updated to use Object instead of array, still supports old array

TheDevRyan committed Oct 27, 2025 at 16:09 UTC 3bcfd031ca6e747a57bba582ebb2b36c9a1a160e
3 files changed +92 -25
meshcentral-config-schema.json
+34 -9
@@ -1988,15 +1988,41 @@
1988 "type": "object"
1989 },
1990 "customFiles": {
1991 - "type": "array",
1992 - "description": "Advanced customization system allowing multiple CSS and JavaScript file configurations per domain. Each configuration can target specific templates using the 'scope' property, enabling granular control over where custom files are loaded. All custom files are loaded after the default custom.css and custom.js files.",
1993 - "items": {
1991 + "type": "object",
1992 + "description": "Advanced customization system allowing multiple CSS and JavaScript file configurations per domain. Each configuration is identified by a unique name and can target specific templates using the 'scope' property, enabling granular control over where custom files are loaded. All custom files are loaded after the default custom.css and custom.js files.",
1993 + "properties": {
1994 + "example-config": {
1995 + "type": "object",
1996 + "description": "Example configuration - replace 'example-config' with your desired name",
1997 + "properties": {
1998 + "css": {
1999 + "type": "array",
2000 + "items": {
2001 + "type": "string"
2002 + },
2003 + "description": "Ordered list of custom CSS files to load. Files are loaded in the specified order after the default custom.css. Place files in the meshcentral-data/public/styles/ directory. Example: [\"theme.css\", \"overrides.css\", \"mobile.css\"]"
2004 + },
2005 + "js": {
2006 + "type": "array",
2007 + "items": {
2008 + "type": "string"
2009 + },
2010 + "description": "Ordered list of custom JavaScript files to load. Files are loaded in the specified order after the default custom.js. Place files in the meshcentral-data/public/scripts/ directory. Example: [\"analytics.js\", \"custom-functions.js\", \"ui-enhancements.js\"]"
2011 + },
2012 + "scope": {
2013 + "type": "array",
2014 + "items": {
2015 + "type": "string"
2016 + },
2017 + "description": "List of template names where these custom files should be loaded. If not specified or empty, files will NOT be loaded anywhere. Use 'all' to load on all templates. Available template names: all, default, default3, default-mobile, login, login2, login-mobile, messenger, message, message2, invite, agentinvite, player, xterm, mstsc, ssh, sharing, sharing-mobile, download, download2, terms, terms-mobile, error404, error4042, error404-mobile. Example: [\"default3\", \"login2\", \"player\"] or [\"all\"]"
2018 + }
2019 + }
2020 + }
2021 + },
2022 + "additionalProperties": {
2023 "type": "object",
2024 + "description": "Custom file configuration identified by a unique name",
2025 "properties": {
1996 - "name": {
1997 - "type": "string",
1998 - "description": "Unique name for this custom files configuration. Used for identification and debugging purposes."
1999 - },
2026 "css": {
2027 "type": "array",
2028 "items": {
@@ -2018,8 +2044,7 @@
2044 },
2045 "description": "List of template names where these custom files should be loaded. If not specified or empty, files will NOT be loaded anywhere. Use 'all' to load on all templates. Available template names: all, default, default3, default-mobile, login, login2, login-mobile, messenger, message, message2, invite, agentinvite, player, xterm, mstsc, ssh, sharing, sharing-mobile, download, download2, terms, terms-mobile, error404, error4042, error404-mobile. Example: [\"default3\", \"login2\", \"player\"] or [\"all\"]"
2046 }
2021 - },
2022 - "required": ["name"]
2047 + }
2048 }
2049 },
2050 "consentMessages": {
sample-config-advanced.json
+12
@@ -347,6 +347,18 @@
347 "autoAcceptIfNoUser": false,
348 "oldStyle": true
349 },
350 + "_customFiles": {
351 + "desktop-theme": {
352 + "css": ["theme.css"],
353 + "js": ["theme.js"],
354 + "scope": ["all"]
355 + },
356 + "mobile-theme": {
357 + "css": ["theme.css", "test.css"],
358 + "js": ["analytics.js", "test.js"],
359 + "scope": ["login2", "login"]
360 + }
361 + },
362 "_notificationMessages": {
363 "title": "MeshCentral",
364 "desktop": "{0} started a remote desktop session.",
webserver.js
+46 -16
@@ -9462,16 +9462,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9462 return null;
9463 }
9464
9465 - function generateCustomCSSTags(customFilesArray, currentTemplate) {
9465 + function generateCustomCSSTags(customFilesObject, currentTemplate) {
9466 var cssTags = '';
9467
9468 cssTags += '<link keeplink=1 type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />\n ';
9469
9470
9471 - if (customFilesArray) {
9472 - if (Array.isArray(customFilesArray)) {
9473 - for (var i = 0; i < customFilesArray.length; i++) {
9474 - var customFileConfig = customFilesArray[i];
9471 + if (customFilesObject) {
9472 + if (Array.isArray(customFilesObject)) {
9473 + // Legacy array support - convert to object format
9474 + for (var i = 0; i < customFilesObject.length; i++) {
9475 + var customFileConfig = customFilesObject[i];
9476 if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
9477 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
9478 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
@@ -9481,9 +9482,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9482 }
9483 }
9484 }
9484 - } else if (customFilesArray.css && Array.isArray(customFilesArray.css)) {
9485 - for (var i = 0; i < customFilesArray.css.length; i++) {
9486 - cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFilesArray.css[i] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
9485 + } else if (customFilesObject.css && Array.isArray(customFilesObject.css)) {
9486 + // Legacy single object support
9487 + for (var i = 0; i < customFilesObject.css.length; i++) {
9488 + cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFilesObject.css[i] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
9489 + }
9490 + } else if (typeof customFilesObject === 'object') {
9491 + // New object format - iterate through each custom file configuration
9492 + for (var configName in customFilesObject) {
9493 + var customFileConfig = customFilesObject[configName];
9494 + if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
9495 + if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
9496 + (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
9497 + for (var j = 0; j < customFileConfig.css.length; j++) {
9498 + cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFileConfig.css[j] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
9499 + }
9500 + }
9501 + }
9502 }
9503 }
9504 }
@@ -9491,15 +9506,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9506 return cssTags.trim();
9507 }
9508
9494 - function generateCustomJSTags(customFilesArray, currentTemplate) {
9509 + function generateCustomJSTags(customFilesObject, currentTemplate) {
9510 var jsTags = '';
9511
9512 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/custom.js"></script>\n ';
9513
9499 - if (customFilesArray) {
9500 - if (Array.isArray(customFilesArray)) {
9501 - for (var i = 0; i < customFilesArray.length; i++) {
9502 - var customFileConfig = customFilesArray[i];
9514 + if (customFilesObject) {
9515 + if (Array.isArray(customFilesObject)) {
9516 + // Legacy array support - convert to object format
9517 + for (var i = 0; i < customFilesObject.length; i++) {
9518 + var customFileConfig = customFilesObject[i];
9519 if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
9520 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
9521 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
@@ -9509,9 +9525,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9525 }
9526 }
9527 }
9512 - } else if (customFilesArray.js && Array.isArray(customFilesArray.js)) {
9513 - for (var i = 0; i < customFilesArray.js.length; i++) {
9514 - jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFilesArray.js[i] + '"></script>\n ';
9528 + } else if (customFilesObject.js && Array.isArray(customFilesObject.js)) {
9529 + // Legacy single object support
9530 + for (var i = 0; i < customFilesObject.js.length; i++) {
9531 + jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFilesObject.js[i] + '"></script>\n ';
9532 + }
9533 + } else if (typeof customFilesObject === 'object') {
9534 + // New object format - iterate through each custom file configuration
9535 + for (var configName in customFilesObject) {
9536 + var customFileConfig = customFilesObject[configName];
9537 + if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
9538 + if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
9539 + (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
9540 + for (var j = 0; j < customFileConfig.js.length; j++) {
9541 + jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFileConfig.js[j] + '"></script>\n ';
9542 + }
9543 + }
9544 + }
9545 }
9546 }
9547 }