- {slackPlugin && (
-

+
+
+ {/* Installed plugins */}
+ {g.installedPlugins && g.installedPlugins.length > 0 && (
+
+ {g.installedPlugins.map((pluginId) => {
+ const pl = pluginsArr.find((p) => p.id === pluginId);
+ return (
+
+
+ {pl &&

}
+
{pluginId}
+
+
+
+ Settings
+
+
+
+
+ );
+ })}
+
)}
-
-
{g.name}
- {g.channelName && (
-
{g.channelName}
+
+ {/* Add plugin button */}
+
+ {addPluginGroupId === g.id ? (
+
+
+
+
+ ) : (
+
+
+
+
)}
-
))}
{/* Add group card */}
-
+
{!addGroupOpen ? (
) : (
@@ -182,6 +220,7 @@ const Plugins = () => {
className="w-full mb-3 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
placeholder="Group name"
onKeyDown={(e) => e.key === "Enter" && createGroup()}
+ autoFocus
/>
-
-
-
-
Copied to clipboard!
-
-
);
};
diff --git a/server/src/apinext.js b/server/src/apinext.js
index 996de31..d01d0fd 100644
--- a/server/src/apinext.js
+++ b/server/src/apinext.js
@@ -660,8 +660,9 @@ router.get(
hostGroups: (database.data.hostGroups || []).map((g) => ({
id: g.id,
name: g.name,
- channelName: g.channelName || null,
- hasSettings: !!g.slackSettings,
+ installedPlugins: (database.data.groupPluginSettings || [])
+ .filter((s) => s.groupId === g.id)
+ .map((s) => s.pluginId),
})),
});
})
@@ -689,15 +690,24 @@ router.get(
(ps) => ps.id === plugin.id
);
+ const groupPluginEntry = group
+ ? (database.data.groupPluginSettings || []).find(
+ (s) => s.groupId === groupId && s.pluginId === plugin.id
+ )
+ : null;
+
// For group-specific settings, use group's own params (not the global ones)
const pluginSettings = group
? {
- ...globalPluginSettings,
- params: group.slackSettings?.params || {},
- enabledEvents: group.slackSettings?.enabledEvents || globalPluginSettings?.enabledEvents || [],
+ params: groupPluginEntry?.params || {},
+ enabledEvents: groupPluginEntry?.enabledEvents ?? globalPluginSettings?.enabledEvents ?? [],
enabled: globalPluginSettings?.enabled,
}
- : globalPluginSettings;
+ : {
+ params: globalPluginSettings?.params || {},
+ enabledEvents: globalPluginSettings?.enabledEvents ?? [],
+ enabled: globalPluginSettings?.enabled,
+ };
return res.status(200).json({
status: "success",
@@ -750,22 +760,34 @@ router.post(
const input = parseNestedForm(req.body);
const groupId = req.query.groupId;
- // If saving group-specific settings for slack
- if (groupId && plugin.id === "slack-notifications") {
+ // If saving group-specific plugin settings
+ if (groupId) {
const group = (database.data.hostGroups || []).find((g) => g.id === groupId);
if (!group) {
return res.status(404).json({ error: "Group not found" });
}
- group.slackSettings = {
- params: input.params,
- enabledEvents: input.events ? Object.keys(input.events) : [],
+ database.data.groupPluginSettings ||= [];
+ const idx = database.data.groupPluginSettings.findIndex(
+ (s) => s.groupId === groupId && s.pluginId === plugin.id
+ );
+ const entry = {
+ groupId,
+ pluginId: plugin.id,
+ params: input.params || {},
+ // undefined = inherit global enabledEvents; explicit array = group override
+ ...(input.events ? { enabledEvents: Object.keys(input.events) } : {}),
};
- // Use group webhook param as the slackWebhook for notification routing
- if (input.params?.webhook) {
- group.slackWebhook = input.params.webhook;
- }
- if (input.params?.channel_name !== undefined) {
- group.channelName = input.params.channel_name;
+ const isNew = idx === -1;
+ if (isNew) {
+ database.data.groupPluginSettings.push(entry);
+ // init plugin if not globally enabled and not already initialized
+ const globallyEnabled = database.data.pluginSettings.find((ps) => ps.id === plugin.id)?.enabled;
+ if (!globallyEnabled && !PluginManagerSingleton()._initializedPlugins?.has(plugin.id)) {
+ await plugin.onPluginEnabled?.();
+ PluginManagerSingleton()._initializedPlugins?.add(plugin.id);
+ }
+ } else {
+ database.data.groupPluginSettings[idx] = entry;
}
if (input.notify) {
const globalSettings = database.data.pluginSettings.find(
@@ -1257,7 +1279,6 @@ router.get(
data: (database.data.hostGroups || []).map((g) => ({
id: g.id,
name: g.name,
- slackWebhook: g.slackWebhook,
createdAt: g.createdAt,
})),
});
@@ -1267,7 +1288,7 @@ router.get(
router.post(
"/host_groups",
mustBeAuthorizedView(async (req, res) => {
- const { name, slackWebhook } = req.body || {};
+ const { name } = req.body || {};
if (!name || !String(name).trim()) {
return res
.status(400)
@@ -1277,7 +1298,6 @@ router.post(
const group = {
id: uuidv4(),
name: String(name).trim(),
- slackWebhook: (slackWebhook || "").trim(),
createdAt: new Date().getTime(),
};
database.data.hostGroups.push(group);
@@ -1290,7 +1310,7 @@ router.post(
"/host_groups/:id",
mustBeAuthorizedView(async (req, res) => {
const { id } = req.params;
- const { name, slackWebhook } = req.body || {};
+ const { name } = req.body || {};
const group = (database.data.hostGroups || []).find((g) => g.id === id);
if (!group) {
return res
@@ -1298,8 +1318,6 @@ router.post(
.json({ status: "rejected", code: 404, error: "group not found" });
}
if (typeof name === "string" && name.trim()) group.name = name.trim();
- if (typeof slackWebhook === "string")
- group.slackWebhook = slackWebhook.trim();
await database.write();
return res.status(200).json({ status: "success", code: 200, data: group });
})
@@ -1323,6 +1341,22 @@ router.post(
(database.data.httpMonitoringData || []).forEach((h) => {
if (h.groupId === id) h.groupId = null;
});
+ // remove all group plugin settings for this group
+ database.data.groupPluginSettings = (database.data.groupPluginSettings || []).filter(
+ (s) => s.groupId !== id
+ );
+ await database.write();
+ return res.status(200).json({ status: "success", code: 200 });
+ })
+);
+
+router.post(
+ "/host_groups/:groupId/plugin/:pluginId/remove",
+ mustBeAuthorizedView(async (req, res) => {
+ const { groupId, pluginId } = req.params;
+ database.data.groupPluginSettings = (database.data.groupPluginSettings || []).filter(
+ (s) => !(s.groupId === groupId && s.pluginId === pluginId)
+ );
await database.write();
return res.status(200).json({ status: "success", code: 200 });
})
diff --git a/server/src/database.js b/server/src/database.js
index ef9f99a..5c23651 100644
--- a/server/src/database.js
+++ b/server/src/database.js
@@ -32,6 +32,36 @@ db.read = async function () {
};
db.data.pluginSettings ||= [];
db.data.hostGroups ||= [];
+ db.data.groupPluginSettings ||= [];
+
+ // Migrate legacy slackSettings/slackWebhook from group objects to groupPluginSettings
+ let migrated = false;
+ for (const group of db.data.hostGroups) {
+ if (group.slackSettings || group.slackWebhook) {
+ const alreadyMigrated = db.data.groupPluginSettings.some(
+ (s) => s.groupId === group.id && s.pluginId === 'slack-notifications'
+ );
+ if (!alreadyMigrated) {
+ const entry = {
+ groupId: group.id,
+ pluginId: 'slack-notifications',
+ params: group.slackSettings?.params || (group.slackWebhook ? { webhook: group.slackWebhook } : {}),
+ };
+ // Only set enabledEvents if explicitly configured; omit the property to inherit global settings
+ if (group.slackSettings?.enabledEvents) {
+ entry.enabledEvents = group.slackSettings.enabledEvents;
+ }
+ db.data.groupPluginSettings.push(entry);
+ }
+ delete group.slackSettings;
+ delete group.slackWebhook;
+ delete group.channelName;
+ migrated = true;
+ }
+ }
+ if (migrated) {
+ await db.write();
+ }
};
export default db;
\ No newline at end of file
diff --git a/server/src/pluginManager.js b/server/src/pluginManager.js
index 1448154..88994e4 100644
--- a/server/src/pluginManager.js
+++ b/server/src/pluginManager.js
@@ -48,15 +48,20 @@ class PluginManager {
return p.default;
})
);
+ const pluginsUsedInGroups = new Set(
+ (database.data.groupPluginSettings || []).map((s) => s.pluginId)
+ );
+ this._initializedPlugins = new Set();
await Promise.all(
this.plugins
.filter((p) => {
- return (
- database.data.pluginSettings.find((ps) => ps.id === p.id)
- ?.enabled ?? false
- );
+ const globallyEnabled = database.data.pluginSettings.find((ps) => ps.id === p.id)?.enabled ?? false;
+ return globallyEnabled || pluginsUsedInGroups.has(p.id);
+ })
+ .map((p) => {
+ this._initializedPlugins.add(p.id);
+ return p.onPluginEnabled && p.onPluginEnabled();
})
- .map((p) => p.onPluginEnabled && p.onPluginEnabled())
);
}
@@ -119,25 +124,34 @@ class PluginManager {
const plugins = this.plugins
.map((p) => {
- const settings =
+ const globalSettings =
database.data.pluginSettings.find((ps) => ps.id === p.id) || {};
- return {
- plugin: p,
- settings,
- };
+ // Merge group-specific plugin settings on top of global settings
+ const groupEntry = newData.HOST_GROUP?.pluginSettings?.[p.id];
+ const settings = groupEntry
+ ? {
+ ...globalSettings,
+ params: { ...globalSettings.params, ...groupEntry.params },
+ // Only override enabledEvents when explicitly set in group entry
+ ...('enabledEvents' in groupEntry ? { enabledEvents: groupEntry.enabledEvents } : {}),
+ }
+ : globalSettings;
+ return { plugin: p, settings };
})
.filter((p) => {
const effectiveEnabledEvents = p.plugin.getEffectiveEnabledEvents
? p.plugin.getEffectiveEnabledEvents({ data: newData, settings: p.settings })
: p.settings.enabledEvents;
+ const hasGroupOverride = !!newData.HOST_GROUP?.pluginSettings?.[p.plugin.id];
+ const effectiveEnabled = hasGroupOverride || p.settings.enabled;
const pass =
- p.settings.enabled &&
+ effectiveEnabled &&
effectiveEnabledEvents?.includes(eventType) &&
!hostEvents.includes(eventType) &&
enabledPlugins.includes(p.plugin.id);
if (!pass) {
- console.log(`[PluginManager] Skip plugin ${p.plugin.id}: enabled=${p.settings.enabled} hasEvent=${effectiveEnabledEvents?.includes(eventType)} notSuppressed=${!hostEvents.includes(eventType)} inEnabledList=${enabledPlugins.includes(p.plugin.id)}`);
+ console.log(`[PluginManager] Skip plugin ${p.plugin.id}: enabled=${effectiveEnabled}(group=${hasGroupOverride}) hasEvent=${effectiveEnabledEvents?.includes(eventType)} notSuppressed=${!hostEvents.includes(eventType)} inEnabledList=${enabledPlugins.includes(p.plugin.id)}`);
}
return pass;
});
@@ -201,30 +215,26 @@ class PluginManager {
}
const enabledPluginsArr = getEnabledPluginsForHost();
- // plugins var is only enabled plugin for this event based on enabledPlugins
const plugins = this.plugins
.map((p) => {
- const settings =
- database.data.pluginSettings.find((ps) => ps.id === p.id) || {};
- return {
- plugin: p,
- settings,
- };
+ const globalSettings = database.data.pluginSettings.find((ps) => ps.id === p.id) || {};
+ const groupEntry = hostGroup?.pluginSettings?.[p.id];
+ const settings = groupEntry
+ ? { ...globalSettings, params: { ...globalSettings.params, ...groupEntry.params } }
+ : globalSettings;
+ return { plugin: p, settings };
})
-
.filter((p) => {
- return p.settings.enabled && enabledPluginsArr.includes(p.plugin.id);
+ const hasGroupOverride = !!hostGroup?.pluginSettings?.[p.plugin.id];
+ return (hasGroupOverride || p.settings.enabled) && enabledPluginsArr.includes(p.plugin.id);
});
+
await Promise.all(
plugins.map(async (p) => {
try {
- const webhookOverride =
- p.plugin.id === "slack-notifications" && hostGroup?.slackWebhook
- ? hostGroup.slackWebhook
- : undefined;
- await p.plugin.sendMessage(p.settings, rssFormatedMessage, webhookOverride);
+ await p.plugin.sendMessage(p.settings, rssFormatedMessage);
} catch (e) {
- console.error("Error in plugin", p.id, e, "stack:", e.stack);
+ console.error("Error in plugin", p.plugin.id, e, "stack:", e.stack);
}
})
);
diff --git a/server/src/plugins/slack.js b/server/src/plugins/slack.js
index 2dd1263..ceab141 100644
--- a/server/src/plugins/slack.js
+++ b/server/src/plugins/slack.js
@@ -119,13 +119,7 @@ Webhook is URL which look like this:
},
],
- getEffectiveEnabledEvents({ data, settings }) {
- const groupSlackSettings = data?.HOST_GROUP?.slackSettings;
- if (groupSlackSettings) {
- return groupSlackSettings.enabledEvents || [];
- }
- return settings.enabledEvents;
- },
+ // getEffectiveEnabledEvents not needed — pluginManager merges group settings into `settings` automatically
async sendMessage(settings, text, webhookOverride) {
if(!text) {
@@ -158,19 +152,12 @@ Webhook is URL which look like this:
},
// main event handling is done here
+ // `settings` already has group overrides merged in by pluginManager
async handleEvent({ eventType, data, settings }) {
- // Use group-specific params (message templates, webhook) when available
- const groupParams = data?.HOST_GROUP?.slackSettings?.params;
- const effectiveParams = groupParams
- ? { ...settings.params, ...groupParams }
- : settings.params;
-
- const template = this.hbs.compile(effectiveParams[`${eventType}_message`], {noEscape: true});
+ const template = this.hbs.compile(settings.params[`${eventType}_message`], {noEscape: true});
const text = template(data);
-
- const webhookOverride = data?.HOST_GROUP?.slackWebhook;
try {
- this.sendMessage(settings, text, webhookOverride);
+ await this.sendMessage(settings, text);
}
catch (e) {console.log(e)}
},
diff --git a/server/src/utils.js b/server/src/utils.js
index 482e4b6..ae3caf9 100644
--- a/server/src/utils.js
+++ b/server/src/utils.js
@@ -790,7 +790,17 @@ export const getGroupForHost = (host) => {
const groups = database.data.hostGroups || [];
const g = groups.find((gr) => gr.id === host.groupId);
if (!g) return null;
- return { id: g.id, name: g.name, slackWebhook: g.slackWebhook, slackSettings: g.slackSettings || null };
+ const pluginSettings = {};
+ (database.data.groupPluginSettings || [])
+ .filter((s) => s.groupId === g.id)
+ .forEach((s) => {
+ const entry = { params: s.params };
+ if (Object.prototype.hasOwnProperty.call(s, 'enabledEvents')) {
+ entry.enabledEvents = s.enabledEvents;
+ }
+ pluginSettings[s.pluginId] = entry;
+ });
+ return { id: g.id, name: g.name, pluginSettings };
};
export const getEffectiveSettingsForHost = (host) => {