feat(notifications): add per-user notification system with inbox, email, and webhook targets

- Add UserNotificationTarget, UserNotificationPreference, InAppNotification models
- Add migration 025_add_user_notifications (tables + indexes)
- Add app/utils/user_notification.py dispatch service
- Add app/api/notifications.py REST endpoints (inbox, targets, preferences)
- Add app/views/notifications.py view route
- Add frontend/templates/notifications_dashboard.html Alpine.js dashboard
- Add bell icon with unread badge in base.html nav (desktop + mobile)
- Register routers in app/api/__init__.py and app/views/__init__.py
- Add 32 unit tests in tests/test_notifications_api.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-08 21:34:25 +00:00
parent af44af98b8
commit fcefd0978f
10 changed files with 2719 additions and 0 deletions
+43
View File
@@ -194,6 +194,20 @@
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>Help
</a>
<!-- Bell notification icon -->
<a href="/notifications"
id="notificationBell"
class="relative p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
aria-label="Notifications"
title="Notifications"
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
<i class="fas fa-bell" aria-hidden="true"></i>
<span id="notificationBadge"
class="hidden absolute -top-1 -right-1 h-4 w-4 rounded-full bg-red-500 text-white text-xs flex items-center justify-center font-bold"
aria-live="polite"
aria-label="unread notifications count"></span>
</a>
<!-- Dark mode toggle -->
<button
id="darkModeToggle"
@@ -283,6 +297,11 @@
{% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}>
<i class="fas fa-plug mr-2 text-gray-400" aria-hidden="true"></i>Integrations
</a>
<a href="/notifications"
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
<i class="fas fa-bell mr-2 text-gray-400" aria-hidden="true"></i>Notifications
</a>
<!-- Admin section in mobile menu shown only for admin users via JS -->
<div id="mobileAdminSection" class="hidden">
@@ -427,6 +446,30 @@
<!-- Common JS (shared) -->
<script src="/static/js/common.js"></script>
<!-- Notification badge updater -->
<script>
(function() {
async function updateNotificationBadge() {
try {
const resp = await fetch('/api/user-notifications/inbox/unread-count');
if (!resp.ok) return;
const data = await resp.json();
const badge = document.getElementById('notificationBadge');
if (!badge) return;
if (data.count > 0) {
badge.textContent = data.count > 99 ? '99+' : data.count;
badge.classList.remove('hidden');
badge.setAttribute('aria-label', data.count + ' unread notifications');
} else {
badge.classList.add('hidden');
}
} catch (_) {}
}
document.addEventListener('DOMContentLoaded', updateNotificationBadge);
setInterval(updateNotificationBadge, 60000);
})();
</script>
<!-- Let child pages define extra scripts if needed -->
{% block scripts %}{% endblock %}
</body>