mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-22 02:16:23 +00:00
Update notification service
This commit is contained in:
@@ -240,11 +240,25 @@ export function NotificationSettings() {
|
|||||||
flat[`events.${cat}`] = String(enabled)
|
flat[`events.${cat}`] = String(enabled)
|
||||||
}
|
}
|
||||||
// Flatten event_toggles: { vm_start: true, vm_stop: false } -> event.vm_start, event.vm_stop
|
// Flatten event_toggles: { vm_start: true, vm_stop: false } -> event.vm_start, event.vm_stop
|
||||||
|
// Always write ALL toggles to DB so the backend has an explicit record.
|
||||||
|
// This ensures default_enabled changes in templates don't get overridden by stale DB values.
|
||||||
if (cfg.event_toggles) {
|
if (cfg.event_toggles) {
|
||||||
for (const [evt, enabled] of Object.entries(cfg.event_toggles)) {
|
for (const [evt, enabled] of Object.entries(cfg.event_toggles)) {
|
||||||
flat[`event.${evt}`] = String(enabled)
|
flat[`event.${evt}`] = String(enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Also write any events NOT in event_toggles using their template defaults.
|
||||||
|
// This covers newly added templates whose default_enabled may be false.
|
||||||
|
if (cfg.event_types_by_group) {
|
||||||
|
for (const events of Object.values(cfg.event_types_by_group)) {
|
||||||
|
for (const evt of (events as Array<{type: string, default_enabled: boolean}>)) {
|
||||||
|
const key = `event.${evt.type}`
|
||||||
|
if (!(key in flat)) {
|
||||||
|
flat[key] = String(evt.default_enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return flat
|
return flat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def get_notification_status():
|
|||||||
def get_notification_history():
|
def get_notification_history():
|
||||||
"""Get notification history with optional filters."""
|
"""Get notification history with optional filters."""
|
||||||
try:
|
try:
|
||||||
limit = request.args.get('limit', 50, type=int)
|
limit = request.args.get('limit', 100, type=int)
|
||||||
offset = request.args.get('offset', 0, type=int)
|
offset = request.args.get('offset', 0, type=int)
|
||||||
severity = request.args.get('severity', '')
|
severity = request.args.get('severity', '')
|
||||||
channel = request.args.get('channel', '')
|
channel = request.args.get('channel', '')
|
||||||
|
|||||||
@@ -962,9 +962,25 @@ class ProxmoxHookWatcher:
|
|||||||
|
|
||||||
severity = self._map_severity(severity_raw)
|
severity = self._map_severity(severity_raw)
|
||||||
|
|
||||||
|
# Extract "reason" as extra detail, NOT the full body.
|
||||||
|
# Templates already have their own intro text (e.g. "{vmname} has failed.").
|
||||||
|
# The body from the webhook often starts with the same intro, so using the
|
||||||
|
# full body as {reason} causes duplication. We strip the first line (which
|
||||||
|
# is typically the title/summary) and keep only the extra detail lines.
|
||||||
|
reason = ''
|
||||||
|
if body:
|
||||||
|
body_lines = body.strip().split('\n')
|
||||||
|
# If more than 1 line, skip the first (summary) and use the rest
|
||||||
|
if len(body_lines) > 1:
|
||||||
|
reason = '\n'.join(body_lines[1:]).strip()[:500]
|
||||||
|
else:
|
||||||
|
# Single-line body: only use it as reason if it differs from title
|
||||||
|
if body.strip().lower() != (title or '').strip().lower():
|
||||||
|
reason = body.strip()[:500]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'hostname': self._hostname,
|
'hostname': self._hostname,
|
||||||
'reason': body[:500] if body else title,
|
'reason': reason,
|
||||||
'title': title,
|
'title': title,
|
||||||
'source_component': source_component,
|
'source_component': source_component,
|
||||||
'notification_type': notification_type,
|
'notification_type': notification_type,
|
||||||
|
|||||||
@@ -311,6 +311,24 @@ class NotificationManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[NotificationManager] Failed to load config: {e}")
|
print(f"[NotificationManager] Failed to load config: {e}")
|
||||||
|
|
||||||
|
# Reconcile per-event toggles with current template defaults.
|
||||||
|
# If a template's default_enabled was changed (e.g. state_change False),
|
||||||
|
# but the DB has a stale 'true' from a previous default, fix it now.
|
||||||
|
# Only override if the user hasn't explicitly set it (we track this with
|
||||||
|
# a sentinel: if the value came from auto-save of defaults, it may be stale).
|
||||||
|
for event_type, tmpl in TEMPLATES.items():
|
||||||
|
key = f'event.{event_type}'
|
||||||
|
if key in self._config:
|
||||||
|
db_val = self._config[key] == 'true'
|
||||||
|
tmpl_default = tmpl.get('default_enabled', True)
|
||||||
|
# If template says disabled but DB says enabled, AND there's no
|
||||||
|
# explicit user marker, enforce the template default.
|
||||||
|
if not tmpl_default and db_val:
|
||||||
|
# Check if user explicitly enabled it (look for a marker)
|
||||||
|
marker = f'event_explicit.{event_type}'
|
||||||
|
if marker not in self._config:
|
||||||
|
self._config[key] = 'false'
|
||||||
|
|
||||||
self._enabled = self._config.get('enabled', 'false') == 'true'
|
self._enabled = self._config.get('enabled', 'false') == 'true'
|
||||||
self._rebuild_channels()
|
self._rebuild_channels()
|
||||||
|
|
||||||
@@ -533,6 +551,13 @@ class NotificationManager:
|
|||||||
if not self._group_limiter.allow(group):
|
if not self._group_limiter.allow(group):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Use the properly mapped severity from the event, not from template defaults.
|
||||||
|
# event.severity was set by _map_severity which normalises to CRITICAL/WARNING/INFO.
|
||||||
|
severity = event.severity
|
||||||
|
|
||||||
|
# Inject the canonical severity into data so templates see it too.
|
||||||
|
event.data['severity'] = severity
|
||||||
|
|
||||||
# Render message from template (structured output)
|
# Render message from template (structured output)
|
||||||
rendered = render_template(event.event_type, event.data)
|
rendered = render_template(event.event_type, event.data)
|
||||||
|
|
||||||
@@ -544,7 +569,7 @@ class NotificationManager:
|
|||||||
'model': self._config.get('ai_model', ''),
|
'model': self._config.get('ai_model', ''),
|
||||||
}
|
}
|
||||||
body = format_with_ai(
|
body = format_with_ai(
|
||||||
rendered['title'], rendered['body'], rendered['severity'], ai_config
|
rendered['title'], rendered['body'], severity, ai_config
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enrich data with structured fields for channels that support them
|
# Enrich data with structured fields for channels that support them
|
||||||
@@ -554,7 +579,7 @@ class NotificationManager:
|
|||||||
|
|
||||||
# Send through all active channels
|
# Send through all active channels
|
||||||
self._dispatch_to_channels(
|
self._dispatch_to_channels(
|
||||||
rendered['title'], body, rendered['severity'],
|
rendered['title'], body, severity,
|
||||||
event.event_type, enriched_data, event.source
|
event.event_type, enriched_data, event.source
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1048,6 +1073,19 @@ class NotificationManager:
|
|||||||
''', (full_key, str(value), now))
|
''', (full_key, str(value), now))
|
||||||
|
|
||||||
self._config[short_key] = str(value)
|
self._config[short_key] = str(value)
|
||||||
|
|
||||||
|
# If user is explicitly enabling an event that defaults to disabled,
|
||||||
|
# mark it so _load_config reconciliation won't override it later.
|
||||||
|
if short_key.startswith('event.') and str(value) == 'true':
|
||||||
|
event_type = short_key[6:] # strip 'event.'
|
||||||
|
tmpl = TEMPLATES.get(event_type, {})
|
||||||
|
if not tmpl.get('default_enabled', True):
|
||||||
|
marker_key = f'{SETTINGS_PREFIX}event_explicit.{event_type}'
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT OR REPLACE INTO user_settings (setting_key, setting_value, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (marker_key, 'true', now))
|
||||||
|
self._config[f'event_explicit.{event_type}'] = 'true'
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -472,8 +472,16 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
body_text = template['body']
|
body_text = template['body']
|
||||||
|
|
||||||
# Clean up empty lines from missing optional variables
|
# Clean up: remove empty lines and consecutive duplicate lines
|
||||||
body_text = '\n'.join(line for line in body_text.split('\n') if line.strip())
|
cleaned_lines = []
|
||||||
|
for line in body_text.split('\n'):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
if cleaned_lines and stripped == cleaned_lines[-1]:
|
||||||
|
continue # skip consecutive duplicate
|
||||||
|
cleaned_lines.append(stripped)
|
||||||
|
body_text = '\n'.join(cleaned_lines)
|
||||||
|
|
||||||
severity = variables.get('severity', 'INFO')
|
severity = variables.get('severity', 'INFO')
|
||||||
group = template.get('group', 'system')
|
group = template.get('group', 'system')
|
||||||
|
|||||||
Reference in New Issue
Block a user