This commit is contained in:
dgtlmoon
2026-01-06 17:45:47 +01:00
parent 5fad56b548
commit 237f08b8ed
4 changed files with 86 additions and 31 deletions
@@ -154,6 +154,7 @@
</div>
</div>
<script src="{{ url_for('static_content', group='js', filename='notification-dashboard.js') }}"></script>
<script>
// Event selection and detail display
$(function() {
@@ -196,16 +197,16 @@ $(function() {
}
html += `<span class="detail-value"><span class="status-pill ${statusClass}">${statusText}</span></span></div>`;
// Timestamp
// Timestamp (converted to browser's local timezone)
if (event.timestamp) {
html += '<div class="detail-row"><span class="detail-label">Timestamp:</span>';
html += `<span class="detail-value">${event.timestamp_formatted || 'N/A'}</span></div>`;
html += `<span class="detail-value">${window.formatTimestampLocal(event.timestamp)}</span></div>`;
}
// Retry time (for retrying status)
if (event.status === 'retrying' && event.retry_at_formatted) {
// Retry time (for retrying status, converted to browser's local timezone)
if (event.status === 'retrying' && event.retry_at) {
html += '<div class="detail-row"><span class="detail-label">Next Retry:</span>';
html += `<span class="detail-value">${event.retry_at_formatted}</span></div>`;
html += `<span class="detail-value">${window.formatTimestampLocal(event.retry_at)}</span></div>`;
}
// Task ID
+3 -2
View File
@@ -12,6 +12,7 @@ from ..diff import HTML_REMOVED_STYLE, REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMA
import re
from ..notification_service import NotificationContextData
from .exceptions import AppriseNotificationException
newline_re = re.compile(r'\r\n|\r|\n')
@@ -437,10 +438,10 @@ def process_notification(n_object: NotificationContextData, datastore):
if log_value:
error_msg += f"\nApprise logs:\n{log_value}"
logger.critical(error_msg)
raise Exception(error_msg)
raise AppriseNotificationException(error_msg, sent_objs=sent_objs)
elif log_value and ('WARNING' in log_value or 'ERROR' in log_value):
logger.critical(f"Apprise warning/error detected:\n{log_value}")
raise Exception(log_value)
raise AppriseNotificationException(log_value, sent_objs=sent_objs)
# Return what was sent for better logging - after the for loop
return sent_objs
@@ -1436,6 +1436,12 @@ def send_notification_task(n_object: NotificationContextData):
from datetime import datetime
import json
from changedetectionio.notification.exceptions import (
AppriseNotificationException,
WatchNotFoundException,
NotificationConfigurationException
)
# Wrap dict in NotificationContextData if needed (for retried tasks from Huey)
if not isinstance(n_object, NotificationContextData):
n_object = NotificationContextData(n_object)
@@ -1443,7 +1449,7 @@ def send_notification_task(n_object: NotificationContextData):
# Load watch
watch = datastore.data['watching'].get(n_object.get('uuid'))
if not watch:
raise Exception(f"No watch found for uuid {n_object.get('uuid')}")
raise WatchNotFoundException(f"No watch found for uuid {n_object.get('uuid')}")
try:
# Reload notification config with cascading (Watch > Tag > System)
@@ -1481,31 +1487,49 @@ def send_notification_task(n_object: NotificationContextData):
logger.success(f"Notification sent successfully for {n_object.get('watch_url')}")
return sent_objs
except Exception as e:
# Log error
logger.error(f"Watch URL: {n_object.get('watch_url')} Error: {str(e)}")
except (WatchNotFoundException, NotificationConfigurationException) as e:
# Non-recoverable error - don't retry, immediately mark as failed
logger.error(f"Non-recoverable notification error: {str(e)}")
# Try to render the notification to show what was actually attempted
# This ensures RETRYING and FAILED notifications show rendered content, not templates
attempted_payload = None
# Store as failed (no retries) with error details
attempted_payload = {
'notification_urls': n_object.get('notification_urls'),
'notification_title': n_object.get('notification_title'),
'notification_body': n_object.get('notification_body'),
'notification_format': n_object.get('notification_format'),
}
# Store in dead-letter queue immediately (no retries)
try:
from changedetectionio.jinja2_custom import render as jinja_render
from changedetectionio.notification.handler import create_notification_parameters
_store_retry_attempt(n_object, e, payload=attempted_payload)
except Exception as store_error:
logger.debug(f"Unable to store failed notification: {store_error}")
# Render the notification the same way process_notification does
notification_parameters = create_notification_parameters(n_object, datastore)
rendered_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
rendered_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
# Handle error: update watch, log, signal
watch_uuid = n_object.get('uuid')
_handle_notification_error(watch_uuid, e, notification_debug_log, app, datastore)
# Re-raise to ensure Huey marks it as failed
# But since this is non-recoverable, Huey will exhaust retries and mark as failed
raise
except AppriseNotificationException as e:
# Recoverable Apprise error - retry with exponential backoff
logger.error(f"Apprise notification failed (will retry): {str(e)}")
# Get rendered notification payload from exception
attempted_payload = None
if e.sent_objs:
first_sent = e.sent_objs[0]
attempted_payload = {
'notification_urls': n_object.get('notification_urls'),
'notification_title': rendered_title,
'notification_body': rendered_body,
'notification_title': first_sent.get('title'),
'notification_body': first_sent.get('body'),
'notification_format': n_object.get('notification_format'),
}
except Exception as render_error:
# If rendering fails, fall back to raw template
logger.debug(f"Unable to render notification for retry attempt, using raw template: {render_error}")
logger.debug("Using fully rendered notification from AppriseNotificationException")
else:
# No sent_objs (shouldn't happen, but fallback)
attempted_payload = {
'notification_urls': n_object.get('notification_urls'),
'notification_title': n_object.get('notification_title'),
@@ -1526,6 +1550,30 @@ def send_notification_task(n_object: NotificationContextData):
# Re-raise to trigger Huey retry
raise
except Exception as e:
# Other unexpected errors - log and retry
logger.error(f"Unexpected error sending notification: {str(e)}", exc_info=True)
attempted_payload = {
'notification_urls': n_object.get('notification_urls'),
'notification_title': n_object.get('notification_title'),
'notification_body': n_object.get('notification_body'),
'notification_format': n_object.get('notification_format'),
}
# Store retry attempt
try:
_store_retry_attempt(n_object, e, payload=attempted_payload)
except Exception as store_error:
logger.debug(f"Unable to store retry attempt: {store_error}")
# Handle error: update watch, log, signal
watch_uuid = n_object.get('uuid')
_handle_notification_error(watch_uuid, e, notification_debug_log, app, datastore)
# Re-raise to trigger Huey retry
raise
# Decorator will be applied after huey is initialized
# This is set up in init_huey_task()
@@ -3,17 +3,22 @@
* Handles timezone conversion, AJAX log fetching, and user interactions
*/
// Global utility function to format Unix timestamp to local timezone
window.formatTimestampLocal = function(timestamp) {
if (!timestamp) return 'N/A';
return new Intl.DateTimeFormat(undefined, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
timeZoneName: 'short'
}).format(timestamp * 1000);
};
$(function() {
// Convert retry timestamps to local timezone
$('.retry-time[data-timestamp]').each(function() {
var timestamp = parseInt($(this).data('timestamp'));
if (timestamp) {
var formatted = new Intl.DateTimeFormat(undefined, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
timeZoneName: 'short'
}).format(timestamp * 1000);
$(this).text(formatted);
$(this).text(window.formatTimestampLocal(timestamp));
}
});