Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion server/frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ function App() {
return (
<div className="App">
<Header setTheme={setTheme} theme={theme} cookieExist={cookieExist} />

<Routes>
<Route path="/" element={<Navigate replace to="/home" />} />
<Route path="/home" element={<Home cookieExist={cookieExist} />}>
Expand Down
308 changes: 153 additions & 155 deletions server/frontend/src/Components/Plugins/Plugins.jsx

Large diffs are not rendered by default.

80 changes: 57 additions & 23 deletions server/src/apinext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})),
});
})
Expand Down Expand Up @@ -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,
}
Comment on lines 700 to 705
Comment on lines 699 to 705
: globalPluginSettings;
: {
params: globalPluginSettings?.params || {},
enabledEvents: globalPluginSettings?.enabledEvents ?? [],
enabled: globalPluginSettings?.enabled,
};

return res.status(200).json({
status: "success",
Expand Down Expand Up @@ -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) } : {}),
Comment on lines +773 to +778
};
Comment on lines +773 to 779
// 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;
Comment on lines +781 to +790
}
if (input.notify) {
const globalSettings = database.data.pluginSettings.find(
Expand Down Expand Up @@ -1257,7 +1279,6 @@ router.get(
data: (database.data.hostGroups || []).map((g) => ({
id: g.id,
name: g.name,
slackWebhook: g.slackWebhook,
createdAt: g.createdAt,
})),
});
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -1290,16 +1310,14 @@ 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
.status(404)
.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 });
})
Expand All @@ -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 });
})
Expand Down
30 changes: 30 additions & 0 deletions server/src/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Comment on lines +37 to +61
if (migrated) {
await db.write();
}
};

export default db;
64 changes: 37 additions & 27 deletions server/src/pluginManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
);
}

Expand Down Expand Up @@ -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);
Comment on lines 148 to 152
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;
});
Expand Down Expand Up @@ -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);
}
})
);
Expand Down
21 changes: 4 additions & 17 deletions server/src/plugins/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)}
},
Expand Down
12 changes: 11 additions & 1 deletion server/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down