594 lines
20 KiB
Markdown
594 lines
20 KiB
Markdown
# Notifications Module
|
|
|
|
**Last Verified:** December 28, 2025
|
|
**Status:** ✅ Active (v1.2.2 - Notifications Page Added)
|
|
**Backend Path:** `backend/igny8_core/business/notifications/`
|
|
**Frontend Path:** `frontend/src/store/notificationStore.ts`, `frontend/src/components/header/NotificationDropdown.tsx`, `frontend/src/pages/account/NotificationsPage.tsx`
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
| What | File | Key Items |
|
|
|------|------|-----------|
|
|
| Models | `business/notifications/models.py` | `Notification`, `NotificationPreference` |
|
|
| Views | `business/notifications/views.py` | `NotificationViewSet` |
|
|
| Serializers | `business/notifications/serializers.py` | `NotificationSerializer` |
|
|
| Services | `business/notifications/services.py` | `NotificationService` |
|
|
| Frontend Store | `store/notificationStore.ts` | Zustand notification store |
|
|
| Frontend Dropdown | `components/header/NotificationDropdown.tsx` | Notification UI (header bell) |
|
|
| Frontend Page | `pages/account/NotificationsPage.tsx` | Full notifications history page |
|
|
| API Client | `services/notifications.api.ts` | API methods |
|
|
|
|
---
|
|
|
|
## Purpose
|
|
|
|
The Notifications module provides real-time user notifications for AI operations, system events, and workflow milestones. Notifications appear in the header dropdown and persist in the database.
|
|
|
|
**Key Features:**
|
|
- Real-time notifications for AI task completion/failure
|
|
- Account-wide or user-specific notifications
|
|
- Read/unread state tracking
|
|
- Action buttons with navigation
|
|
- Auto-fetch and auto-sync with API
|
|
|
|
---
|
|
|
|
## Data Models
|
|
|
|
### Notification
|
|
|
|
| Field | Type | Currently Used? | Purpose |
|
|
|-------|------|----------------|---------|
|
|
| account | FK | ✅ **Yes** | Owner account (required for multi-tenancy) |
|
|
| user | FK | ❌ **No** | User-specific notifications (nullable - if null, visible to all account users). **Currently unused** - all notifications are account-wide |
|
|
| notification_type | CharField | ✅ **Yes** | Type from NotificationType choices (for filtering, icons) |
|
|
| title | CharField(200) | ✅ **Yes** | Notification title shown in dropdown |
|
|
| message | TextField | ✅ **Yes** | Notification body text shown in dropdown |
|
|
| severity | CharField | ✅ **Yes** | info/success/warning/error (affects icon color) |
|
|
| site | FK | ✅ **Partial** | Related site (nullable). Created but **not displayed** in current dropdown |
|
|
| content_type | FK | ❌ **No** | Generic relation to any object. **Future:** Link notification to specific Content/Task/etc. for direct access |
|
|
| object_id | PositiveInteger | ❌ **No** | Generic relation ID. **Future:** Used with content_type for object linking |
|
|
| action_url | CharField(500) | ✅ **Yes** | Frontend route for action button (e.g., `/planner/clusters`) |
|
|
| action_label | CharField(50) | ✅ **Yes** | Action button text (e.g., "View Clusters") |
|
|
| is_read | Boolean | ✅ **Yes** | Read status (default: False). Used for unread badge count |
|
|
| read_at | DateTime | ✅ **Partial** | When marked read (nullable). Created but **not displayed** in dropdown |
|
|
| metadata | JSON | ✅ **Partial** | Additional data (counts, details). Stored but **not displayed** in dropdown |
|
|
| created_at | DateTime | ✅ **Yes** | Creation timestamp. Shown as relative time ("2 minutes ago") |
|
|
| updated_at | DateTime | ❌ **No** | Last update timestamp. **Currently unused** |
|
|
|
|
**Indexes:**
|
|
- `(account, -created_at)` - List notifications by account ✅ **Used**
|
|
- `(account, is_read, -created_at)` - Filter unread ✅ **Used**
|
|
- `(user, -created_at)` - User-specific notifications ❌ **Unused** (all notifications are account-wide)
|
|
|
|
---
|
|
|
|
### Why So Many Unused Fields?
|
|
|
|
The model was designed with **future expansion** in mind, but the current "dropdown-only" implementation uses less than half of the fields. Here's why they exist:
|
|
|
|
**Over-Engineered for Current Use:**
|
|
- `content_type` + `object_id` → For direct object linking (planned: click notification, go to that exact content item)
|
|
- `user` → For user-specific notifications (planned: "@John, your content is ready")
|
|
- `metadata` → For rich data (planned: show counts, progress, details in dedicated page)
|
|
- `read_at` → For analytics (planned: "You read this 2 days ago")
|
|
- `updated_at` → For editing notifications (planned: update notification instead of creating duplicate)
|
|
|
|
**Currently Essential:**
|
|
- `account`, `title`, `message`, `severity`, `notification_type` → Core notification data
|
|
- `action_url`, `action_label` → Navigation buttons
|
|
- `is_read`, `created_at` → Dropdown functionality
|
|
|
|
**The Design Mismatch:**
|
|
You're right - this is a **database schema designed for a full-featured notification system** (with dedicated page, filtering, search, history) but the frontend only implements a **simple dropdown**. The backend is over-built for the current use case.
|
|
|
|
---
|
|
|
|
## Notification Types
|
|
|
|
### AI Operations
|
|
|
|
| Type | Severity | Trigger | Message Format |
|
|
|------|----------|---------|----------------|
|
|
| `ai_cluster_complete` | success | Clustering finishes | "Created X clusters from Y keywords" |
|
|
| `ai_cluster_failed` | error | Clustering fails | "Failed to cluster keywords: [error]" |
|
|
| `ai_ideas_complete` | success | Idea generation finishes | "Generated X content ideas from Y clusters" |
|
|
| `ai_ideas_failed` | error | Idea generation fails | "Failed to generate ideas: [error]" |
|
|
| `ai_content_complete` | success | Content generation finishes | "Generated X articles (Y words)" |
|
|
| `ai_content_failed` | error | Content generation fails | "Failed to generate content: [error]" |
|
|
| `ai_images_complete` | success | Image generation finishes | "Generated X images" |
|
|
| `ai_images_failed` | error | Image generation fails | "Failed to generate X images: [error]" |
|
|
| `ai_prompts_complete` | success | Image prompts created | "X image prompts ready (1 featured + Y in-article)" |
|
|
| `ai_prompts_failed` | error | Image prompt creation fails | "Failed to create image prompts: [error]" |
|
|
|
|
### Workflow Events
|
|
|
|
| Type | Severity | Trigger | Message Format |
|
|
|------|----------|---------|----------------|
|
|
| `content_ready_review` | info | Content moved to review | "[Title] ready for review" |
|
|
| `content_published` | success | Content published | '"[Title]" published to [site]' |
|
|
| `content_publish_failed` | error | Publishing fails | 'Failed to publish "[Title]": [error]' |
|
|
|
|
### WordPress Sync
|
|
|
|
| Type | Severity | Trigger | Message Format |
|
|
|------|----------|---------|----------------|
|
|
| `wordpress_sync_success` | success | Sync completes | "Synced X items with [site]" |
|
|
| `wordpress_sync_failed` | error | Sync fails | "WordPress sync failed for [site]: [error]" |
|
|
|
|
### Credits & Billing
|
|
|
|
| Type | Severity | Trigger | Message Format |
|
|
|------|----------|---------|----------------|
|
|
| `credits_low` | warning | Credits < 20% | "You've used X% of your credits. Y remaining." |
|
|
| `credits_depleted` | error | Credits exhausted | "Your credits are exhausted. Upgrade to continue." |
|
|
|
|
### Setup & System
|
|
|
|
| Type | Severity | Trigger | Message Format |
|
|
|------|----------|---------|----------------|
|
|
| `site_setup_complete` | success | All setup steps done | "[Site] is fully configured and ready!" |
|
|
| `keywords_imported` | info | Keywords added | "Added X keywords to [site]" |
|
|
| `system_info` | info | System messages | Custom message |
|
|
|
|
---
|
|
|
|
## Notification Triggers
|
|
|
|
### AI Task Completion (AIEngine)
|
|
|
|
**Location:** `backend/igny8_core/ai/engine.py`
|
|
**Methods:** `_create_success_notification()`, `_create_failure_notification()`
|
|
|
|
**Flow:**
|
|
1. AIEngine executes AI function (`auto_cluster`, `generate_ideas`, etc.)
|
|
2. On **success** (after DONE phase):
|
|
- Calls `_create_success_notification(function_name, save_result, payload)`
|
|
- Maps function to NotificationService method
|
|
- Creates notification with counts/metrics
|
|
3. On **failure** (in `_handle_error()`):
|
|
- Calls `_create_failure_notification(function_name, error)`
|
|
- Creates error notification with error message
|
|
|
|
**Mapping:**
|
|
```python
|
|
auto_cluster → NotificationService.notify_clustering_complete/failed
|
|
generate_ideas → NotificationService.notify_ideas_complete/failed
|
|
generate_content → NotificationService.notify_content_complete/failed
|
|
generate_image_prompts → NotificationService.notify_prompts_complete/failed
|
|
generate_images → NotificationService.notify_images_complete/failed
|
|
```
|
|
|
|
### WordPress Publishing
|
|
|
|
**Location:** `backend/igny8_core/tasks/wordpress_publishing.py`
|
|
**Trigger:** After publishing content to WordPress
|
|
|
|
```python
|
|
NotificationService.notify_content_published(
|
|
account=account,
|
|
site=site,
|
|
title=content.title,
|
|
content_object=content
|
|
)
|
|
```
|
|
|
|
### WordPress Sync
|
|
|
|
**Location:** `backend/igny8_core/tasks/wordpress_publishing.py`
|
|
**Trigger:** After syncing posts from WordPress
|
|
|
|
```python
|
|
NotificationService.notify_wordpress_sync_success(
|
|
account=account,
|
|
site=site,
|
|
count=synced_count
|
|
)
|
|
```
|
|
|
|
### Credit Alerts
|
|
|
|
**Location:** `backend/igny8_core/business/billing/services/credit_service.py`
|
|
**Trigger:** After deducting credits
|
|
|
|
```python
|
|
# When credits drop below threshold
|
|
NotificationService.notify_credits_low(
|
|
account=account,
|
|
percentage_used=80,
|
|
credits_remaining=remaining
|
|
)
|
|
|
|
# When credits exhausted
|
|
NotificationService.notify_credits_depleted(account=account)
|
|
```
|
|
|
|
### Manual Triggers (Optional)
|
|
|
|
Notifications can also be created manually from anywhere:
|
|
|
|
```python
|
|
from igny8_core.business.notifications.services import NotificationService
|
|
|
|
NotificationService.notify_keywords_imported(
|
|
account=account,
|
|
site=site,
|
|
count=keyword_count
|
|
)
|
|
```
|
|
|
|
**Note:** As of v1.2.1, the following actions **DO create notifications**:
|
|
- ✅ AI task completion/failure (clustering, ideas, content, images, prompts)
|
|
- ✅ Keyword import via "Add to Workflow" - **Fixed in v1.2.1**
|
|
|
|
**Actions that DON'T yet create notifications** (planned):
|
|
- ❌ WordPress publishing (needs integration in wordpress_publishing.py)
|
|
- ❌ WordPress sync (needs integration in wordpress_publishing.py)
|
|
- ❌ Credit alerts (needs integration in credit_service.py)
|
|
- ❌ Automation completion (planned for future)
|
|
|
|
---
|
|
|
|
## User Interface
|
|
|
|
### Notification Dropdown (Header)
|
|
|
|
**Location:** Header bell icon (top right)
|
|
**File:** `frontend/src/components/header/NotificationDropdown.tsx`
|
|
|
|
**Features:**
|
|
- Shows last 50 notifications
|
|
- Animated badge with unread count
|
|
- Click notification to mark read + navigate
|
|
- "Mark all read" button
|
|
- Auto-refreshes every 30 seconds
|
|
- Refreshes when opened (if stale > 1 minute)
|
|
- "View All Notifications" link to full page
|
|
|
|
### Notifications Page (Full History)
|
|
|
|
**Location:** `/account/notifications`
|
|
**File:** `frontend/src/pages/account/NotificationsPage.tsx`
|
|
**Access:** Sidebar → ACCOUNT → Notifications OR NotificationDropdown → "View All Notifications"
|
|
|
|
**Features:**
|
|
- **Filters:**
|
|
- Severity (info/success/warning/error)
|
|
- Notification type (AI operations, WordPress sync, credits, etc.)
|
|
- Read status (all/unread/read)
|
|
- Site (filter by specific site)
|
|
- Date range (from/to dates)
|
|
- **Actions:**
|
|
- Mark individual notifications as read
|
|
- Mark all as read (bulk action)
|
|
- Delete individual notifications
|
|
- Click notification to navigate to action URL
|
|
- **Display:**
|
|
- Full notification history with pagination
|
|
- Severity icons with color coding
|
|
- Relative timestamps ("2 hours ago")
|
|
- Site badge when applicable
|
|
- Action buttons for related pages
|
|
- Unread badge in sidebar menu
|
|
|
|
**v1.2.2 Implementation:**
|
|
- ✅ Full-page notifications view created
|
|
- ✅ Advanced filtering by severity, type, read status, site, date range
|
|
- ✅ Bulk actions (mark all read)
|
|
- ✅ Individual actions (mark read, delete)
|
|
- ✅ Added to sidebar under ACCOUNT section
|
|
- ✅ Unread count badge in sidebar
|
|
- ✅ Fixed broken link in NotificationDropdown (was `/notifications`, now `/account/notifications`)
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### List Notifications
|
|
|
|
```http
|
|
GET /api/v1/notifications/
|
|
```
|
|
|
|
**Query Parameters:**
|
|
- `page` - Page number (default: 1)
|
|
- `page_size` - Results per page (default: 20)
|
|
- `is_read` - Filter by read status (`true`/`false`)
|
|
- `notification_type` - Filter by type (e.g., `ai_cluster_complete`)
|
|
- `severity` - Filter by severity (`info`/`success`/`warning`/`error`)
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"count": 42,
|
|
"next": "/api/v1/notifications/?page=2",
|
|
"previous": null,
|
|
"results": [
|
|
{
|
|
"id": 123,
|
|
"notification_type": "ai_cluster_complete",
|
|
"severity": "success",
|
|
"title": "Clustering Complete",
|
|
"message": "Created 5 clusters from 50 keywords",
|
|
"is_read": false,
|
|
"created_at": "2025-12-28T10:30:00Z",
|
|
"read_at": null,
|
|
"action_label": "View Clusters",
|
|
"action_url": "/planner/clusters",
|
|
"site": {"id": 1, "name": "My Blog"},
|
|
"metadata": {
|
|
"cluster_count": 5,
|
|
"keyword_count": 50
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Get Unread Count
|
|
|
|
```http
|
|
GET /api/v1/notifications/unread-count/
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"unread_count": 7
|
|
}
|
|
```
|
|
|
|
### Mark as Read
|
|
|
|
```http
|
|
POST /api/v1/notifications/{id}/read/
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"id": 123,
|
|
"is_read": true,
|
|
"read_at": "2025-12-28T10:35:00Z"
|
|
}
|
|
```
|
|
|
|
### Mark All as Read
|
|
|
|
```http
|
|
POST /api/v1/notifications/read-all/
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"updated_count": 7,
|
|
"message": "Marked 7 notifications as read"
|
|
}
|
|
```
|
|
|
|
### Delete Notification
|
|
|
|
```http
|
|
DELETE /api/v1/notifications/{id}/
|
|
```
|
|
|
|
**Response:** `204 No Content`
|
|
|
|
---
|
|
|
|
## Frontend Implementation
|
|
|
|
### Notification Store
|
|
|
|
**File:** `frontend/src/store/notificationStore.ts`
|
|
|
|
**Features:**
|
|
- Zustand store for state management
|
|
- In-memory queue for optimistic UI updates
|
|
- API sync for persistent notifications
|
|
- Auto-fetch on mount and periodic sync (every 30 seconds)
|
|
- Auto-refresh when dropdown opens (if stale > 1 minute)
|
|
|
|
**Key Methods:**
|
|
```typescript
|
|
// Add local notification (optimistic)
|
|
addNotification(notification)
|
|
|
|
// Mark as read (optimistic + API sync)
|
|
markAsRead(id)
|
|
|
|
// Mark all as read
|
|
markAllAsRead()
|
|
|
|
// Fetch from API
|
|
fetchNotifications()
|
|
|
|
// Sync unread count
|
|
syncUnreadCount()
|
|
```
|
|
|
|
### Notification Dropdown
|
|
|
|
**File:** `frontend/src/components/header/NotificationDropdown.tsx`
|
|
|
|
**Features:**
|
|
- Badge with unread count (animated ping when > 0)
|
|
- Dropdown with notification list
|
|
- Click notification to mark read and navigate
|
|
- "Mark all read" action
|
|
- Empty state when no notifications
|
|
- Auto-fetch on open if stale
|
|
|
|
**Icon Mapping:**
|
|
```typescript
|
|
auto_cluster → GroupIcon
|
|
generate_ideas → BoltIcon
|
|
generate_content → FileTextIcon
|
|
generate_images → FileIcon
|
|
system → AlertIcon
|
|
success → CheckCircleIcon
|
|
```
|
|
|
|
---
|
|
|
|
## Business Logic
|
|
|
|
### Visibility Rules
|
|
|
|
1. **Account-wide notifications** (`user=NULL`):
|
|
- Visible to ALL users in the account
|
|
- Example: "Automation completed 10 tasks"
|
|
|
|
2. **User-specific notifications** (`user=User`):
|
|
- Only visible to that specific user
|
|
- Example: "Your content is ready for review"
|
|
|
|
3. **Site-filtered** (frontend):
|
|
- Frontend can filter by site in UI
|
|
- Backend always returns all account notifications
|
|
|
|
### Read Status
|
|
|
|
- Notifications start as `is_read=False`
|
|
- Clicking notification marks it read (API call + optimistic update)
|
|
- "Mark all read" bulk updates all unread notifications
|
|
- Read notifications stay in dropdown (can be filtered out in future)
|
|
|
|
### Retention Policy
|
|
|
|
- Notifications never auto-delete (future: add retention policy)
|
|
- Users can manually delete notifications
|
|
- Admin can clean up old notifications via management command (future)
|
|
|
|
---
|
|
|
|
## Common Issues
|
|
|
|
| Issue | Cause | Fix |
|
|
|-------|-------|-----|
|
|
| Notifications not appearing | AIEngine not calling NotificationService | Fixed in v1.2.1 |
|
|
| "Add to workflow" no notification | KeywordViewSet not calling NotificationService | Fixed in v1.2.1 |
|
|
| Can't see notification history | No dedicated notifications page | Fixed in v1.2.2 - Page created at /account/notifications |
|
|
| "View All" button → 404 | Link to `/notifications` but page doesn't exist | Fixed in v1.2.2 - Link updated to /account/notifications |
|
|
| Duplicate notifications | Multiple AI task retries | Check task retry logic |
|
|
| Missing notifications | Celery worker crashed | Check Celery logs |
|
|
| Unread count wrong | Race condition in state | Refresh page or wait for sync |
|
|
| Action URL not working | Incorrect route in action_url | Check NotificationService methods |
|
|
|
|
---
|
|
|
|
## Integration Points
|
|
|
|
| From | To | Trigger |
|
|
|------|----|---------|
|
|
| AIEngine | NotificationService | AI task success/failure |
|
|
| WordPress Publisher | NotificationService | Publishing/sync events |
|
|
| Credit Service | NotificationService | Low credits/depleted |
|
|
| Automation | NotificationService | Automation milestones (future) |
|
|
|
|
---
|
|
|
|
## Planned Changes
|
|
|
|
| Feature | Status | Description |
|
|
|---------|--------|-------------|
|
|
| Notification preferences | 🔜 Planned | User can toggle notification types |
|
|
| Email notifications | 🔜 Planned | Send email for critical notifications |
|
|
| Push notifications | 🔜 Planned | Browser push for real-time alerts |
|
|
| Notification retention | 🔜 Planned | Auto-delete after 30/60 days |
|
|
| Notification categories | 🔜 Planned | Group by module (Planner, Writer, etc.) |
|
|
| Notification sounds | 🔜 Planned | Audio alerts for important events |
|
|
| Webhook notifications | 🔜 Planned | POST to external webhook URLs |
|
|
|
|
---
|
|
|
|
## Version History
|
|
|
|
| Version | Date | Changes |
|
|
|---------|------|---------|
|
|
| v1.2.2 | Dec 28, 2025 | **NEW:** Full notifications page at /account/notifications with filtering, bulk actions, and sidebar integration |
|
|
| v1.2.1 | Dec 28, 2025 | **Fixed:** Notifications now created on AI task completion + keyword import |
|
|
| v1.2.0 | Dec 27, 2025 | Initial notifications system implementation |
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Test Notification Creation
|
|
|
|
```python
|
|
# In Django shell: docker exec -it igny8_backend python manage.py shell
|
|
from igny8_core.business.notifications.services import NotificationService
|
|
from igny8_core.auth.models import Account
|
|
|
|
account = Account.objects.first()
|
|
|
|
# Test clustering notification
|
|
NotificationService.notify_clustering_complete(
|
|
account=account,
|
|
cluster_count=5,
|
|
keyword_count=50
|
|
)
|
|
|
|
# Test error notification
|
|
NotificationService.notify_clustering_failed(
|
|
account=account,
|
|
error="Insufficient credits"
|
|
)
|
|
```
|
|
|
|
### Test Frontend
|
|
|
|
1. Run AI operation (clustering, idea generation, etc.)
|
|
2. Check notification dropdown (bell icon in header)
|
|
3. Verify unread badge appears
|
|
4. Click notification to mark read and navigate
|
|
5. Click "Mark all read" to clear badge
|
|
|
|
---
|
|
|
|
## Debug Commands
|
|
|
|
```bash
|
|
# Check Celery logs for notification creation
|
|
docker logs igny8_celery_worker -f | grep -i "notification"
|
|
|
|
# Check notifications in database
|
|
docker exec -it igny8_backend python manage.py shell
|
|
>>> from igny8_core.business.notifications.models import Notification
|
|
>>> Notification.objects.count()
|
|
>>> Notification.objects.filter(is_read=False).count()
|
|
>>> Notification.objects.last().title
|
|
|
|
# Test notification API
|
|
curl -H "Authorization: Bearer <token>" \
|
|
http://localhost:8011/api/v1/notifications/
|
|
|
|
# Check notification creation code
|
|
grep -r "notify_clustering_complete" backend/
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture Notes
|
|
|
|
**Design Pattern:** Service Layer + Repository Pattern
|
|
- `NotificationService` provides static methods for creating notifications
|
|
- Each notification type has dedicated method with validation
|
|
- `Notification.create_notification()` is class method for low-level creation
|
|
- Views use `AccountModelViewSet` for automatic account filtering
|
|
|
|
**Why Not Signals?**
|
|
- Explicit is better than implicit
|
|
- Clear call sites for debugging
|
|
- Easier to test and mock
|
|
- No hidden side effects
|
|
- Can pass context-specific data
|
|
|
|
**Lazy Imports:**
|
|
- NotificationService uses lazy imports in AIEngine to avoid Django app loading issues
|
|
- Import inside method, not at module level
|