django phase 3 and 4
This commit is contained in:
@@ -1,137 +0,0 @@
|
||||
# Admin Sidebar Fix - Debug Log
|
||||
|
||||
## ATTEMPT #1 - FAILED
|
||||
**What:** Added `context['sidebar_navigation'] = custom_apps` in each_context
|
||||
**Result:** Main pages broke (no sidebar), subpages showed default
|
||||
|
||||
## ATTEMPT #2 - FAILED
|
||||
**What:** Changed `show_all_applications: True` in settings
|
||||
**Result:** Subpages still showed default Django sidebar
|
||||
|
||||
## ATTEMPT #3 - FAILED
|
||||
**What:** Overrode `get_sidebar_list()` to return `get_app_list()` directly
|
||||
**Result:** Empty sidebar on all pages (wrong format)
|
||||
|
||||
## ATTEMPT #4 - FAILED
|
||||
**What:** Converted Django format to Unfold format in `get_sidebar_list()`:
|
||||
- Changed `name` → `title`
|
||||
- Changed `models` → `items`
|
||||
- Changed `admin_url` → `link`
|
||||
- Set `show_all_applications: True`
|
||||
**Result:** Sidebar still showing default on subpages
|
||||
|
||||
## ATTEMPT #5 - FAILED
|
||||
**What:** Changed `show_all_applications: False` (data is correct, format is correct)
|
||||
**Data verified:**
|
||||
- sidebar_navigation has 15 groups with correct Unfold format
|
||||
- Structure: `{title, collapsible, items: [{title, link, icon}]}`
|
||||
**Result:** STILL NOT WORKING - sidebar not showing properly
|
||||
|
||||
## ATTEMPT #6 - PARTIAL SUCCESS! ✓
|
||||
**What:** Added `has_permission: True` to each item in sidebar_navigation
|
||||
**Result:**
|
||||
- Group pages (e.g., /admin/igny8_core_auth/) now show custom sidebar ✓
|
||||
- Subpages (e.g., /admin/igny8_core_auth/account/) still show DEFAULT sidebar ❌
|
||||
|
||||
## NEW DISCOVERY
|
||||
Different pages behave differently:
|
||||
- Homepage (/admin/): Custom sidebar ✓
|
||||
- Group pages (/admin/app/): Custom sidebar ✓
|
||||
- Model list pages (/admin/app/model/): DEFAULT sidebar ❌
|
||||
- Model detail pages (/admin/app/model/1/change/): DEFAULT sidebar ❌
|
||||
|
||||
## INVESTIGATION RESULTS
|
||||
1. Context is IDENTICAL on all pages (verified via shell) ✓
|
||||
2. sidebar_navigation has 15 custom groups on ALL pages ✓
|
||||
3. But HTML shows DEFAULT "Igny8_Core_Auth" on model pages ❌
|
||||
4. Both custom AND default groups appear in HTML (conflict!)
|
||||
|
||||
## ATTEMPT #7 - CRITICAL DISCOVERY!
|
||||
**What:** Traced each_context calls during actual HTTP request
|
||||
**DISCOVERY:** each_context() is NOT being called AT ALL on model pages! ❌
|
||||
**This explains everything:** If each_context isn't called, our sidebar_navigation never gets set!
|
||||
|
||||
## ROOT CAUSE FOUND
|
||||
- Homepage/group pages: Call each_context() → Custom sidebar works ✓
|
||||
- Model list/detail pages: DON'T call each_context() → Default sidebar shows ❌
|
||||
- Django/Unfold is using cached or pre-built context for model pages
|
||||
- Our overridden each_context() is being bypassed completely!
|
||||
|
||||
## NEXT STEPS
|
||||
Need to find where model admin views build their context and ensure each_context is called.
|
||||
|
||||
## ATTEMPT #8 - FORCING SIDEBAR_NAVIGATION
|
||||
**What:** Added explicit check in each_context - if sidebar_navigation is empty, force set it
|
||||
**Why:** Parent's each_context should call get_sidebar_list(), but maybe it's not or it's empty
|
||||
**Testing:** Backend restarted with additional safety check
|
||||
**Result:** NO CHANGE - subpages still show default sidebar ❌
|
||||
|
||||
## VERIFIED FACTS
|
||||
1. get_app_list() correctly ignores app_label parameter ✓
|
||||
2. Returns 16 custom groups regardless of app_label ✓
|
||||
3. Context should be correct in each_context() ✓
|
||||
4. But HTML shows DEFAULT sidebar on model pages ❌
|
||||
|
||||
## ANALYZING NOW
|
||||
Checking if different templates are used on different page types, or if sidebar is being rendered twice.
|
||||
|
||||
## ATTEMPT #9 - DIRECT PYTHON DEBUGGING
|
||||
**What:** Added print statements directly in each_context() to see if it's called and what it returns
|
||||
**Method:** Console debugging to trace actual execution flow
|
||||
**RESULT:** NO OUTPUT - each_context() is DEFINITELY NOT being called on model pages! ❌
|
||||
|
||||
## CRITICAL FINDING
|
||||
each_context() is NOT called on ModelAdmin changelist/change views!
|
||||
Django admin must be rendering model pages with a different context building method that bypasses AdminSite.each_context().
|
||||
|
||||
This explains EVERYTHING:
|
||||
- Homepage calls each_context() → custom sidebar ✓
|
||||
- Group pages call each_context() → custom sidebar ✓
|
||||
- Model pages DON'T call each_context() → default sidebar ❌
|
||||
|
||||
## SOLUTION NEEDED
|
||||
Must find how Django/Unfold builds context for model views and inject our sidebar there.
|
||||
Possible approaches:
|
||||
1. Override ModelAdmin.changelist_view() and .change_view()
|
||||
2. Override ChangeList class
|
||||
3. Find middleware/context processor that runs for model pages
|
||||
|
||||
## ATTEMPT #10 - OVERRIDE MODELADMIN VIEWS ✅ SUCCESSFUL!
|
||||
**What:** Created Igny8ModelAdmin base class that overrides all view methods to inject sidebar_navigation
|
||||
**Method:** Override changelist_view, change_view, add_view to inject extra_context with sidebar
|
||||
**Implementation:** Added to /data/app/igny8/backend/igny8_core/admin/base.py
|
||||
**Testing:** Changed AccountAdmin to inherit from Igny8ModelAdmin
|
||||
**Result:** ✅ SUCCESS! Sidebar shows correctly on Account pages
|
||||
**Applied to:** ALL 46+ admin classes across all modules (auth, ai, business, modules)
|
||||
|
||||
**Final Solution:**
|
||||
1. Created custom Igny8ModelAdmin class that inherits from UnfoldModelAdmin
|
||||
2. Overrides all view methods (changelist_view, change_view, add_view, delete_view, history_view)
|
||||
3. Each override calls _inject_sidebar_context() helper that:
|
||||
- Gets custom sidebar from admin_site.get_sidebar_list()
|
||||
- Forces sidebar_navigation, available_apps, app_list into extra_context
|
||||
- Adds branding: site_title, site_header, site_url, has_permission
|
||||
- Detects active group and marks it for expanded dropdown
|
||||
4. All admin classes now inherit from Igny8ModelAdmin instead of ModelAdmin
|
||||
5. Sidebar now appears consistently on ALL pages (homepage, group pages, model list, detail, add, edit)
|
||||
|
||||
**Root Cause:** Django's ModelAdmin views bypass AdminSite.each_context(), so custom sidebar was never injected
|
||||
**Fix:** Direct injection via extra_context parameter in overridden view methods
|
||||
|
||||
## HYPOTHESIS - BROWSER CACHING
|
||||
The context is correct, but user's browser might be showing cached HTML.
|
||||
Added logging to trace each_context calls. Need user to hard refresh (Ctrl+Shift+R).
|
||||
|
||||
## STATUS
|
||||
- Data is CORRECT (verified via shell)
|
||||
- Format is CORRECT (matches Unfold structure)
|
||||
- Setting is CORRECT (show_all_applications=False)
|
||||
- BUT TEMPLATE IS NOT USING IT
|
||||
|
||||
## POSSIBLE REMAINING ISSUES
|
||||
1. Template caching
|
||||
2. Unfold sidebar template has additional requirements we're missing
|
||||
3. Different template being used on subpages vs homepage
|
||||
|
||||
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
# ADMIN SIDEBAR FIX - COMPLETE ✅
|
||||
|
||||
**Date:** December 14, 2025
|
||||
**Status:** RESOLVED
|
||||
|
||||
---
|
||||
|
||||
## THE PROBLEM
|
||||
|
||||
Custom sidebar with 16 organized groups was only showing on:
|
||||
- ✅ Admin homepage (`/admin/`)
|
||||
- ✅ App index pages (`/admin/igny8_core_auth/`)
|
||||
|
||||
But NOT showing on:
|
||||
- ❌ Model list pages (`/admin/igny8_core_auth/account/`)
|
||||
- ❌ Model detail/edit pages (`/admin/igny8_core_auth/account/123/change/`)
|
||||
- ❌ Model add pages (`/admin/igny8_core_auth/account/add/`)
|
||||
|
||||
Model pages showed **DEFAULT Django sidebar** instead of custom 16-group sidebar.
|
||||
|
||||
---
|
||||
|
||||
## ROOT CAUSE
|
||||
|
||||
Django's `ModelAdmin` view methods (`changelist_view()`, `change_view()`, etc.) **DO NOT** call `AdminSite.each_context()`.
|
||||
|
||||
Our custom sidebar logic was in `site.py` `each_context()`, which was only called by:
|
||||
- `AdminSite.index()` (homepage)
|
||||
- `AdminSite.app_index()` (app level pages)
|
||||
|
||||
ModelAdmin views built their context independently, bypassing our custom sidebar entirely.
|
||||
|
||||
**Proof:** Added print debugging to `each_context()` - NO OUTPUT when visiting model pages.
|
||||
|
||||
---
|
||||
|
||||
## THE SOLUTION
|
||||
|
||||
Created **`Igny8ModelAdmin`** base class that overrides all ModelAdmin view methods to inject custom sidebar via `extra_context` parameter.
|
||||
|
||||
### Implementation
|
||||
|
||||
**File:** `/data/app/igny8/backend/igny8_core/admin/base.py`
|
||||
|
||||
```python
|
||||
class Igny8ModelAdmin(UnfoldModelAdmin):
|
||||
"""
|
||||
Custom ModelAdmin that ensures sidebar_navigation is set correctly on ALL pages
|
||||
|
||||
Django's ModelAdmin views don't call AdminSite.each_context(),
|
||||
so we override them to inject our custom sidebar.
|
||||
"""
|
||||
|
||||
def _inject_sidebar_context(self, request, extra_context=None):
|
||||
"""Helper to inject custom sidebar into context"""
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
|
||||
from igny8_core.admin.site import admin_site
|
||||
from django.conf import settings
|
||||
|
||||
# Get custom sidebar
|
||||
sidebar_navigation = admin_site.get_sidebar_list(request)
|
||||
|
||||
# Inject sidebar and branding
|
||||
extra_context['sidebar_navigation'] = sidebar_navigation
|
||||
extra_context['available_apps'] = admin_site.get_app_list(request, app_label=None)
|
||||
extra_context['app_list'] = extra_context['available_apps']
|
||||
extra_context['site_title'] = admin_site.site_title
|
||||
extra_context['site_header'] = admin_site.site_header
|
||||
extra_context['site_url'] = admin_site.site_url
|
||||
extra_context['has_permission'] = admin_site.has_permission(request)
|
||||
|
||||
# Detect active group for expanded dropdown
|
||||
if hasattr(request, 'resolver_match') and request.resolver_match:
|
||||
url_name = request.resolver_match.url_name
|
||||
app_label = request.resolver_match.app_name
|
||||
|
||||
for group in sidebar_navigation:
|
||||
for item in group.get('items', []):
|
||||
if item.get('link') and (url_name in item['link'] or app_label in item['link']):
|
||||
group['is_active'] = True
|
||||
item['is_active'] = True
|
||||
break
|
||||
|
||||
return extra_context
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
extra_context = self._inject_sidebar_context(request, extra_context)
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||
extra_context = self._inject_sidebar_context(request, extra_context)
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def add_view(self, request, form_url='', extra_context=None):
|
||||
extra_context = self._inject_sidebar_context(request, extra_context)
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
extra_context = self._inject_sidebar_context(request, extra_context)
|
||||
return super().delete_view(request, object_id, extra_context)
|
||||
|
||||
def history_view(self, request, object_id, extra_context=None):
|
||||
extra_context = self._inject_sidebar_context(request, extra_context)
|
||||
return super().history_view(request, object_id, extra_context)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DEPLOYMENT
|
||||
|
||||
### Applied to ALL 46+ Admin Classes
|
||||
|
||||
Changed all admin classes from:
|
||||
```python
|
||||
class MyModelAdmin(ModelAdmin):
|
||||
```
|
||||
|
||||
To:
|
||||
```python
|
||||
class MyModelAdmin(Igny8ModelAdmin):
|
||||
```
|
||||
|
||||
**Modified Files:**
|
||||
1. `igny8_core/auth/admin.py` - 11 admin classes
|
||||
2. `igny8_core/ai/admin.py` - 1 admin class
|
||||
3. `igny8_core/business/automation/admin.py` - 2 admin classes
|
||||
4. `igny8_core/business/integration/admin.py` - 2 admin classes
|
||||
5. `igny8_core/business/publishing/admin.py` - 2 admin classes
|
||||
6. `igny8_core/business/optimization/admin.py` - 1 admin class
|
||||
7. `igny8_core/business/billing/admin.py` - 1 admin class
|
||||
8. `igny8_core/modules/writer/admin.py` - 6 admin classes
|
||||
9. `igny8_core/modules/planner/admin.py` - 3 admin classes
|
||||
10. `igny8_core/modules/billing/admin.py` - 8 admin classes
|
||||
11. `igny8_core/modules/system/admin.py` - 5 admin classes
|
||||
|
||||
**Total:** 46+ admin classes updated
|
||||
|
||||
---
|
||||
|
||||
## FEATURES DELIVERED
|
||||
|
||||
### ✅ 1. Custom Sidebar Everywhere
|
||||
Custom 16-group sidebar now appears on:
|
||||
- Admin homepage
|
||||
- App index pages
|
||||
- Model changelist (list view)
|
||||
- Model change (edit view)
|
||||
- Model add (create view)
|
||||
- Model delete (confirm view)
|
||||
- Model history view
|
||||
|
||||
### ✅ 2. 16 Organized Groups
|
||||
1. **Dashboard** - Custom admin dashboard
|
||||
2. **Accounts & Users** - Account, User, Site, Sector, Industry
|
||||
3. **Billing & Tenancy** - Plans, Subscriptions, Payments, Invoices
|
||||
4. **Writer Module** - Content, Tasks, Images
|
||||
5. **Planner** - Keywords, Clusters, Content Ideas
|
||||
6. **Publishing** - Publishing Records, Deployments
|
||||
7. **Optimization** - Optimization Tasks
|
||||
8. **Automation** - Automation Config, Runs
|
||||
9. **Integration** - Site Integrations, Sync Events
|
||||
10. **AI Framework** - AI Task Logs
|
||||
11. **System Configuration** - Prompts, Settings, Strategies
|
||||
12. **Celery Results** - Task results, groups, chords
|
||||
13. **Content Types** - Content taxonomy, attributes
|
||||
14. **Administration** - Credit costs, payment methods
|
||||
15. **Authentication and Authorization** - Password resets
|
||||
16. **Sessions** - Active sessions
|
||||
|
||||
### ✅ 3. Consistent Branding
|
||||
All pages now show:
|
||||
- Site title: "IGNY8 Admin"
|
||||
- Site header: "IGNY8"
|
||||
- Logo and branding
|
||||
- Consistent navigation
|
||||
|
||||
### ✅ 4. Active Group Detection
|
||||
- Automatically detects current page's app/model
|
||||
- Marks relevant sidebar group as `is_active: true`
|
||||
- Keeps active group dropdown **expanded**
|
||||
- Highlights current navigation item
|
||||
|
||||
---
|
||||
|
||||
## TESTING
|
||||
|
||||
Verified sidebar appears correctly on:
|
||||
```
|
||||
✓ /admin/igny8_core_auth/account/
|
||||
✓ /admin/igny8_core_auth/site/
|
||||
✓ /admin/igny8_modules_writer/content/
|
||||
✓ /admin/igny8_modules_planner/keywords/
|
||||
```
|
||||
|
||||
All pages show:
|
||||
- ✅ 15 custom sidebar groups (16 including Dashboard)
|
||||
- ✅ Proper branding/logo
|
||||
- ✅ Active group expanded
|
||||
- ✅ Consistent navigation
|
||||
|
||||
---
|
||||
|
||||
## FUTURE MAINTENANCE
|
||||
|
||||
### Adding New Admin Classes
|
||||
|
||||
When creating new admin classes, use `Igny8ModelAdmin`:
|
||||
|
||||
```python
|
||||
from igny8_core.admin.base import Igny8ModelAdmin
|
||||
|
||||
@admin.register(MyModel)
|
||||
class MyModelAdmin(Igny8ModelAdmin):
|
||||
list_display = ['field1', 'field2']
|
||||
# ... rest of configuration
|
||||
```
|
||||
|
||||
### Benefits
|
||||
- Custom sidebar automatically available
|
||||
- Branding consistency maintained
|
||||
- Active state detection works
|
||||
- No additional configuration needed
|
||||
|
||||
---
|
||||
|
||||
## DEBUGGING HISTORY
|
||||
|
||||
See `ADMIN-SIDEBAR-DEBUG.md` for complete debugging journey (10 attempts).
|
||||
|
||||
**Key Discoveries:**
|
||||
1. Template debugging showed context was correct but HTML was wrong
|
||||
2. Print debugging proved `each_context()` not called on model pages
|
||||
3. Django source inspection confirmed ModelAdmin views bypass `each_context()`
|
||||
4. Solution required overriding view methods directly
|
||||
|
||||
**Time Investment:** ~4 hours debugging, 30 minutes implementation
|
||||
|
||||
---
|
||||
|
||||
## RELATED FILES
|
||||
|
||||
- `/data/app/igny8/backend/igny8_core/admin/base.py` - Igny8ModelAdmin implementation
|
||||
- `/data/app/igny8/backend/igny8_core/admin/site.py` - Custom sidebar definition
|
||||
- `/data/app/igny8/ADMIN-SIDEBAR-DEBUG.md` - Full debugging log
|
||||
- `/data/app/igny8/ADMIN-IMPLEMENTATION-STATUS.md` - Overall admin progress
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ COMPLETE - All subpages now show custom sidebar with active group expanded
|
||||
@@ -1,465 +0,0 @@
|
||||
# Plan Limits Implementation Checklist
|
||||
|
||||
**Version:** v1.1.0 (Minor version - new feature)
|
||||
**Started:** December 12, 2025
|
||||
**Status:** In Progress
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Implementing comprehensive plan limit system to enforce pricing tier limits for:
|
||||
- Hard limits (persistent): sites, users, keywords, clusters
|
||||
- Monthly limits (reset on billing cycle): content ideas, words, images, prompts
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Backend Setup ✅ / ❌
|
||||
|
||||
### 1.1 Plan Model Updates
|
||||
- [ ] Add `max_keywords` field to Plan model
|
||||
- [ ] Add `max_clusters` field to Plan model
|
||||
- [ ] Add `max_content_ideas` field to Plan model
|
||||
- [ ] Add `max_content_words` field to Plan model
|
||||
- [ ] Add `max_images_basic` field to Plan model
|
||||
- [ ] Add `max_images_premium` field to Plan model
|
||||
- [ ] Add `max_image_prompts` field to Plan model
|
||||
- [ ] Add help text and validators for all fields
|
||||
- [ ] Update `__str__` method if needed
|
||||
|
||||
**File:** `backend/igny8_core/auth/models.py`
|
||||
|
||||
### 1.2 PlanLimitUsage Model Creation
|
||||
- [ ] Create new `PlanLimitUsage` model
|
||||
- [ ] Add fields: account, limit_type, amount_used, period_start, period_end
|
||||
- [ ] Add Meta class with db_table, indexes, unique_together
|
||||
- [ ] Add `__str__` method
|
||||
- [ ] Add helper methods: `is_current_period()`, `remaining_allowance()`
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/models.py`
|
||||
|
||||
### 1.3 Database Migration
|
||||
- [ ] Run `makemigrations` command
|
||||
- [ ] Review generated migration
|
||||
- [ ] Test migration with `migrate --plan`
|
||||
- [ ] Run migration `migrate`
|
||||
- [ ] Verify schema in database
|
||||
|
||||
**Command:** `python manage.py makemigrations && python manage.py migrate`
|
||||
|
||||
### 1.4 Data Seeding
|
||||
- [ ] Create data migration for existing plans
|
||||
- [ ] Populate limit fields with default values (Starter: 2/1/100K, Growth: 5/3/300K, Scale: Unlimited/5/500K)
|
||||
- [ ] Create initial PlanLimitUsage records for existing accounts
|
||||
- [ ] Calculate current usage from existing data
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/migrations/00XX_seed_plan_limits.py`
|
||||
|
||||
### 1.5 Word Counter Utility
|
||||
- [ ] Create `word_counter.py` utility
|
||||
- [ ] Implement `calculate_word_count(html_content)` function
|
||||
- [ ] Strip HTML tags using BeautifulSoup or regex
|
||||
- [ ] Handle edge cases (empty content, None, malformed HTML)
|
||||
- [ ] Add unit tests
|
||||
|
||||
**File:** `backend/igny8_core/utils/word_counter.py`
|
||||
|
||||
### 1.6 Content Model Auto-Calculation
|
||||
- [ ] Update Content model `save()` method
|
||||
- [ ] Auto-calculate `word_count` when `content_html` changes
|
||||
- [ ] Use `word_counter.calculate_word_count()`
|
||||
- [ ] Test with sample content
|
||||
|
||||
**File:** `backend/igny8_core/business/content/models.py`
|
||||
|
||||
### 1.7 LimitService Creation
|
||||
- [ ] Create `limit_service.py`
|
||||
- [ ] Implement `check_hard_limit(account, limit_type)` - sites, users, keywords, clusters
|
||||
- [ ] Implement `check_monthly_limit(account, limit_type, amount)` - ideas, words, images, prompts
|
||||
- [ ] Implement `increment_usage(account, limit_type, amount, metadata)`
|
||||
- [ ] Implement `get_usage_summary(account)` - current usage stats
|
||||
- [ ] Implement `get_current_period(account)` - get billing period
|
||||
- [ ] Create custom exceptions: `HardLimitExceededError`, `MonthlyLimitExceededError`
|
||||
- [ ] Add logging for all operations
|
||||
- [ ] Add unit tests
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/services/limit_service.py`
|
||||
|
||||
### 1.8 Update PlanSerializer
|
||||
- [ ] Add all new limit fields to `PlanSerializer.Meta.fields`
|
||||
- [ ] Test serialization
|
||||
- [ ] Verify API response includes new fields
|
||||
|
||||
**File:** `backend/igny8_core/auth/serializers.py`
|
||||
|
||||
### 1.9 Create LimitUsageSerializer
|
||||
- [ ] Create `PlanLimitUsageSerializer`
|
||||
- [ ] Include all fields
|
||||
- [ ] Add computed fields: remaining, percentage_used
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/serializers.py`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Backend Enforcement ✅ / ❌
|
||||
|
||||
### 2.1 Hard Limit Enforcement - Sites
|
||||
- [ ] Update `SiteViewSet.create()` to check `max_sites`
|
||||
- [ ] Use `LimitService.check_hard_limit(account, 'sites')`
|
||||
- [ ] Return proper error message if limit exceeded
|
||||
- [ ] Test with different plan limits
|
||||
|
||||
**File:** `backend/igny8_core/auth/views.py`
|
||||
|
||||
### 2.2 Hard Limit Enforcement - Users
|
||||
- [ ] Update user invitation logic to check `max_users`
|
||||
- [ ] Use `LimitService.check_hard_limit(account, 'users')`
|
||||
- [ ] Return proper error message if limit exceeded
|
||||
|
||||
**File:** `backend/igny8_core/auth/views.py` (UserViewSet or invitation endpoint)
|
||||
|
||||
### 2.3 Hard Limit Enforcement - Keywords
|
||||
- [ ] Update keyword creation/import to check `max_keywords`
|
||||
- [ ] Check before bulk import
|
||||
- [ ] Check before individual creation
|
||||
- [ ] Use `LimitService.check_hard_limit(account, 'keywords')`
|
||||
|
||||
**File:** `backend/igny8_core/business/planning/views.py`
|
||||
|
||||
### 2.4 Hard Limit Enforcement - Clusters
|
||||
- [ ] Update clustering service to check `max_clusters`
|
||||
- [ ] Check before AI clustering operation
|
||||
- [ ] Use `LimitService.check_hard_limit(account, 'clusters')`
|
||||
|
||||
**File:** `backend/igny8_core/business/planning/services/clustering_service.py`
|
||||
|
||||
### 2.5 Monthly Limit Enforcement - Content Ideas
|
||||
- [ ] Update idea generation service
|
||||
- [ ] Check `max_content_ideas` before generation
|
||||
- [ ] Increment usage after successful generation
|
||||
- [ ] Use `LimitService.check_monthly_limit()` and `increment_usage()`
|
||||
|
||||
**File:** `backend/igny8_core/business/planning/services/idea_service.py` or similar
|
||||
|
||||
### 2.6 Monthly Limit Enforcement - Content Words
|
||||
- [ ] Update content generation service
|
||||
- [ ] Check `max_content_words` before generation
|
||||
- [ ] Use `Content.word_count` for actual usage
|
||||
- [ ] Increment usage after content created
|
||||
- [ ] Sum word counts for batch operations
|
||||
|
||||
**File:** `backend/igny8_core/business/content/services/content_generation_service.py`
|
||||
|
||||
### 2.7 Monthly Limit Enforcement - Images
|
||||
- [ ] Update image generation service
|
||||
- [ ] Check `max_images_basic` or `max_images_premium` based on model
|
||||
- [ ] Increment usage after image created
|
||||
- [ ] Track basic vs premium separately
|
||||
|
||||
**File:** `backend/igny8_core/business/content/services/image_service.py` or similar
|
||||
|
||||
### 2.8 Monthly Limit Enforcement - Image Prompts
|
||||
- [ ] Update image prompt extraction
|
||||
- [ ] Check `max_image_prompts` before extraction
|
||||
- [ ] Increment usage after prompts extracted
|
||||
|
||||
**File:** `backend/igny8_core/business/content/services/` (wherever prompts are extracted)
|
||||
|
||||
### 2.9 Automation Pipeline Integration
|
||||
- [ ] Update automation to check limits before each stage
|
||||
- [ ] Show limit warnings in pre-run estimation
|
||||
- [ ] Stop automation if limit would be exceeded
|
||||
- [ ] Log limit checks in activity log
|
||||
|
||||
**File:** `backend/igny8_core/business/automation/services/`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Monthly Reset Task ✅ / ❌
|
||||
|
||||
### 3.1 Celery Task Creation
|
||||
- [ ] Create `reset_monthly_plan_limits()` task
|
||||
- [ ] Find accounts with billing period ending today
|
||||
- [ ] Reset PlanLimitUsage records
|
||||
- [ ] Create new records for new period
|
||||
- [ ] Log reset operations
|
||||
- [ ] Handle errors gracefully
|
||||
|
||||
**File:** `backend/igny8_core/tasks/billing.py`
|
||||
|
||||
### 3.2 Celery Beat Schedule
|
||||
- [ ] Add task to `CELERY_BEAT_SCHEDULE`
|
||||
- [ ] Set to run daily at midnight UTC
|
||||
- [ ] Test task execution
|
||||
|
||||
**File:** `backend/igny8_core/celery.py`
|
||||
|
||||
### 3.3 Manual Reset Capability
|
||||
- [ ] Create admin action to manually reset limits
|
||||
- [ ] Add to `PlanLimitUsageAdmin`
|
||||
- [ ] Test manual reset
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/admin.py`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: API Endpoints ✅ / ❌
|
||||
|
||||
### 4.1 Limits Usage Endpoint
|
||||
- [ ] Create `/api/v1/billing/limits/usage/` endpoint
|
||||
- [ ] Return current usage for all limit types
|
||||
- [ ] Return remaining allowance
|
||||
- [ ] Return days until reset
|
||||
- [ ] Include plan limits for reference
|
||||
- [ ] Test endpoint
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/views.py`
|
||||
|
||||
### 4.2 Limits History Endpoint (Optional)
|
||||
- [ ] Create `/api/v1/billing/limits/history/` endpoint
|
||||
- [ ] Return historical usage data
|
||||
- [ ] Support date range filtering
|
||||
- [ ] Test endpoint
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/views.py`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Frontend Updates ✅ / ❌
|
||||
|
||||
### 5.1 Update Plan Interface
|
||||
- [ ] Add limit fields to Plan interface in `billing.api.ts`
|
||||
- [ ] Add limit fields to Plan interface in `Settings/Plans.tsx`
|
||||
- [ ] Add limit fields to Plan interface in `SignUpFormUnified.tsx`
|
||||
- [ ] Add limit fields to Plan interface in `AuthPages/SignUp.tsx`
|
||||
- [ ] Ensure consistency across all 4 locations
|
||||
|
||||
**Files:** Multiple interface definitions
|
||||
|
||||
### 5.2 Create PlanLimitsWidget Component
|
||||
- [ ] Create `PlanLimitsWidget.tsx` component
|
||||
- [ ] Display hard limits (Sites, Users, Keywords, Clusters) with counts
|
||||
- [ ] Display monthly limits with progress bars
|
||||
- [ ] Show days until reset
|
||||
- [ ] Fetch from `/api/v1/billing/limits/usage/`
|
||||
- [ ] Add refresh capability
|
||||
- [ ] Style with existing design system
|
||||
|
||||
**File:** `frontend/src/components/dashboard/PlanLimitsWidget.tsx`
|
||||
|
||||
### 5.3 Update Dashboard
|
||||
- [ ] Add `PlanLimitsWidget` to dashboard
|
||||
- [ ] Position alongside CreditBalanceWidget
|
||||
- [ ] Test responsive layout
|
||||
|
||||
**File:** `frontend/src/pages/Dashboard/Dashboard.tsx` or similar
|
||||
|
||||
### 5.4 Update Settings/Plans Page
|
||||
- [ ] Display all limit fields in plan cards
|
||||
- [ ] Format numbers (1,000 / 100K / Unlimited)
|
||||
- [ ] Update `extractFeatures()` function
|
||||
- [ ] Test plan display
|
||||
|
||||
**File:** `frontend/src/pages/Settings/Plans.tsx`
|
||||
|
||||
### 5.5 Update Usage Page
|
||||
- [ ] Add limits section to Usage page
|
||||
- [ ] Display current usage vs limits
|
||||
- [ ] Show limit types (hard vs monthly)
|
||||
- [ ] Add progress bars for monthly limits
|
||||
- [ ] Test with real data
|
||||
|
||||
**File:** `frontend/src/pages/Billing/Usage.tsx`
|
||||
|
||||
### 5.6 Create Limit Exceeded Modal
|
||||
- [ ] Create `LimitExceededModal.tsx` component
|
||||
- [ ] Show when limit is reached
|
||||
- [ ] Display current usage and limit
|
||||
- [ ] Show upgrade options
|
||||
- [ ] Link to billing page
|
||||
- [ ] Style with existing modal pattern
|
||||
|
||||
**File:** `frontend/src/components/billing/LimitExceededModal.tsx`
|
||||
|
||||
### 5.7 Add Limit Guards
|
||||
- [ ] Check limits before operations (optional - server-side is primary)
|
||||
- [ ] Show limit exceeded modal on API error
|
||||
- [ ] Handle `HardLimitExceededError` and `MonthlyLimitExceededError`
|
||||
- [ ] Display user-friendly messages
|
||||
|
||||
**Files:** Various page components
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Admin & Testing ✅ / ❌
|
||||
|
||||
### 6.1 Django Admin for PlanLimitUsage
|
||||
- [ ] Create `PlanLimitUsageAdmin` class
|
||||
- [ ] Add list display fields
|
||||
- [ ] Add filters (account, limit_type, period)
|
||||
- [ ] Add search fields
|
||||
- [ ] Add readonly fields (created_at, updated_at)
|
||||
- [ ] Add custom actions (reset, export)
|
||||
- [ ] Register admin
|
||||
|
||||
**File:** `backend/igny8_core/business/billing/admin.py`
|
||||
|
||||
### 6.2 Update Plan Admin
|
||||
- [ ] Add new limit fields to `PlanAdmin`
|
||||
- [ ] Group fields logically (Hard Limits, Monthly Limits)
|
||||
- [ ] Add inline for related limit usage (optional)
|
||||
- [ ] Test admin interface
|
||||
|
||||
**File:** `backend/igny8_core/auth/admin.py`
|
||||
|
||||
### 6.3 Backend Testing
|
||||
- [ ] Test hard limit checks
|
||||
- [ ] Test monthly limit checks
|
||||
- [ ] Test limit increment
|
||||
- [ ] Test monthly reset
|
||||
- [ ] Test word count calculation
|
||||
- [ ] Test API endpoints
|
||||
- [ ] Test error handling
|
||||
|
||||
### 6.4 Frontend Testing
|
||||
- [ ] Test plan display with limits
|
||||
- [ ] Test limits widget
|
||||
- [ ] Test usage page
|
||||
- [ ] Test limit exceeded modal
|
||||
- [ ] Test responsive design
|
||||
- [ ] Test with different plan tiers
|
||||
|
||||
### 6.5 Integration Testing
|
||||
- [ ] Test complete workflow: create content → check limits → increment usage
|
||||
- [ ] Test monthly reset → verify limits reset
|
||||
- [ ] Test upgrade plan → verify new limits apply
|
||||
- [ ] Test limit exceeded → verify operation blocked
|
||||
- [ ] Test across different accounts and sites
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Documentation ✅ / ❌
|
||||
|
||||
### 7.1 Update CHANGELOG
|
||||
- [ ] Document all changes
|
||||
- [ ] List new fields added
|
||||
- [ ] List new services created
|
||||
- [ ] List API endpoints added
|
||||
- [ ] List frontend components added
|
||||
- [ ] Update to v1.1.0
|
||||
|
||||
**File:** `CHANGELOG.md`
|
||||
|
||||
### 7.2 Update API Documentation
|
||||
- [ ] Document `/api/v1/billing/limits/usage/` endpoint
|
||||
- [ ] Document error responses
|
||||
- [ ] Add examples
|
||||
|
||||
**File:** `docs/20-API/BILLING-ENDPOINTS.md`
|
||||
|
||||
### 7.3 Update Feature Guide
|
||||
- [ ] Document plan limits feature
|
||||
- [ ] Document limit types
|
||||
- [ ] Document monthly reset
|
||||
- [ ] Add to features list
|
||||
|
||||
**File:** `IGNY8-COMPLETE-FEATURES-GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Deployment ✅ / ❌
|
||||
|
||||
### 8.1 Pre-Deployment Checklist
|
||||
- [ ] All migrations created and tested
|
||||
- [ ] All tests passing
|
||||
- [ ] No console errors in frontend
|
||||
- [ ] CHANGELOG updated
|
||||
- [ ] Version bumped to v1.1.0
|
||||
- [ ] Code reviewed
|
||||
|
||||
### 8.2 Deployment Steps
|
||||
- [ ] Backup database
|
||||
- [ ] Run migrations
|
||||
- [ ] Deploy backend
|
||||
- [ ] Deploy frontend
|
||||
- [ ] Verify limits working
|
||||
- [ ] Monitor for errors
|
||||
|
||||
### 8.3 Post-Deployment Validation
|
||||
- [ ] Test limit checks work
|
||||
- [ ] Test usage tracking works
|
||||
- [ ] Test monthly reset (wait or trigger manually)
|
||||
- [ ] Verify no existing functionality broken
|
||||
- [ ] Monitor error logs
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progress Summary
|
||||
|
||||
**Total Tasks:** ~80
|
||||
**Completed:** 2
|
||||
**In Progress:** 1
|
||||
**Not Started:** 77
|
||||
|
||||
**Estimated Time:** 8-12 hours
|
||||
**Started:** December 12, 2025
|
||||
**Target Completion:** December 13, 2025
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Testing Scenarios
|
||||
|
||||
### Scenario 1: Hard Limit - Sites
|
||||
1. Account with Starter plan (max 2 sites)
|
||||
2. Create 2 sites successfully
|
||||
3. Try to create 3rd site → should fail with limit error
|
||||
|
||||
### Scenario 2: Monthly Limit - Content Words
|
||||
1. Account with Starter plan (100K words/month)
|
||||
2. Generate content totaling 99K words
|
||||
3. Try to generate 2K words → should fail
|
||||
4. Wait for monthly reset
|
||||
5. Generate 2K words → should succeed
|
||||
|
||||
### Scenario 3: Monthly Reset
|
||||
1. Account at end of billing period
|
||||
2. Has used 90% of limits
|
||||
3. Run reset task
|
||||
4. Verify usage reset to 0
|
||||
5. Verify new period created
|
||||
|
||||
### Scenario 4: Plan Upgrade
|
||||
1. Account on Starter plan
|
||||
2. Reached 100% of limit
|
||||
3. Upgrade to Growth plan
|
||||
4. Verify new limits apply
|
||||
5. Perform operation → should succeed
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Known Risks & Mitigation
|
||||
|
||||
**Risk 1:** Breaking existing content generation
|
||||
**Mitigation:** Test thoroughly, use feature flags if needed
|
||||
|
||||
**Risk 2:** Word count inconsistency
|
||||
**Mitigation:** Use single source (Content.word_count), standardize calculation
|
||||
|
||||
**Risk 3:** Monthly reset errors
|
||||
**Mitigation:** Add error handling, logging, manual reset capability
|
||||
|
||||
**Risk 4:** Performance impact of limit checks
|
||||
**Mitigation:** Optimize queries, add database indexes, cache plan data
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Use `Content.word_count` as single source of truth for word counting
|
||||
- Ignore `estimated_word_count` in Ideas and `word_count` in Tasks for limit tracking
|
||||
- Hard limits check COUNT from database
|
||||
- Monthly limits track usage in PlanLimitUsage table
|
||||
- Reset task must be idempotent (safe to run multiple times)
|
||||
- All limit checks happen server-side (frontend is informational only)
|
||||
- Use proper error classes for different limit types
|
||||
- Log all limit operations for debugging
|
||||
@@ -66,8 +66,8 @@ class Igny8AdminConfig(AdminConfig):
|
||||
def _setup_celery_admin(self):
|
||||
"""Setup enhanced Celery admin with proper unregister/register"""
|
||||
try:
|
||||
from django_celery_results.models import TaskResult
|
||||
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin
|
||||
from django_celery_results.models import TaskResult, GroupResult
|
||||
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin, CeleryGroupResultAdmin
|
||||
|
||||
# Unregister the default TaskResult admin
|
||||
try:
|
||||
@@ -75,8 +75,15 @@ class Igny8AdminConfig(AdminConfig):
|
||||
except admin.sites.NotRegistered:
|
||||
pass
|
||||
|
||||
# Register our enhanced version
|
||||
# Unregister the default GroupResult admin
|
||||
try:
|
||||
admin.site.unregister(GroupResult)
|
||||
except admin.sites.NotRegistered:
|
||||
pass
|
||||
|
||||
# Register our enhanced versions
|
||||
admin.site.register(TaskResult, CeleryTaskResultAdmin)
|
||||
admin.site.register(GroupResult, CeleryGroupResultAdmin)
|
||||
except Exception as e:
|
||||
# Log the error but don't crash the app
|
||||
import logging
|
||||
|
||||
@@ -4,9 +4,10 @@ Celery Task Monitoring Admin - Unfold Style
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.contrib import messages
|
||||
from django_celery_results.models import TaskResult
|
||||
from django_celery_results.models import TaskResult, GroupResult
|
||||
from unfold.admin import ModelAdmin
|
||||
from unfold.contrib.filters.admin import RangeDateFilter
|
||||
from celery import current_app
|
||||
|
||||
|
||||
class CeleryTaskResultAdmin(ModelAdmin):
|
||||
@@ -79,12 +80,15 @@ class CeleryTaskResultAdmin(ModelAdmin):
|
||||
seconds = duration.total_seconds()
|
||||
|
||||
if seconds < 1:
|
||||
return format_html('<span style="color: #0bbf87;">{:.2f}ms</span>', seconds * 1000)
|
||||
time_str = f'{seconds * 1000:.2f}ms'
|
||||
return format_html('<span style="color: #0bbf87;">{}</span>', time_str)
|
||||
elif seconds < 60:
|
||||
return format_html('<span style="color: #0693e3;">{:.2f}s</span>', seconds)
|
||||
time_str = f'{seconds:.2f}s'
|
||||
return format_html('<span style="color: #0693e3;">{}</span>', time_str)
|
||||
else:
|
||||
minutes = seconds / 60
|
||||
return format_html('<span style="color: #ff7a00;">{:.1f}m</span>', minutes)
|
||||
time_str = f'{minutes:.1f}m'
|
||||
return format_html('<span style="color: #ff7a00;">{}</span>', time_str)
|
||||
return '-'
|
||||
execution_time.short_description = 'Duration'
|
||||
|
||||
@@ -143,9 +147,9 @@ class CeleryTaskResultAdmin(ModelAdmin):
|
||||
count = old_tasks.count()
|
||||
old_tasks.delete()
|
||||
|
||||
self.message_user(request, f'🗑️ Cleared {count} old task(s)', messages.SUCCESS)
|
||||
self.message_user(request, f'Cleared {count} old task(s)', messages.SUCCESS)
|
||||
|
||||
clear_old_tasks.short_description = '🗑️ Clear Old Tasks (30+ days)'
|
||||
clear_old_tasks.short_description = 'Clear Old Tasks (30+ days)'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable manual task creation"""
|
||||
@@ -154,3 +158,56 @@ class CeleryTaskResultAdmin(ModelAdmin):
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Make read-only"""
|
||||
return False
|
||||
|
||||
|
||||
class CeleryGroupResultAdmin(ModelAdmin):
|
||||
"""Admin interface for monitoring Celery group results with Unfold styling"""
|
||||
|
||||
list_display = [
|
||||
'group_id',
|
||||
'date_created',
|
||||
'date_done',
|
||||
'result_count',
|
||||
]
|
||||
list_filter = [
|
||||
('date_created', RangeDateFilter),
|
||||
('date_done', RangeDateFilter),
|
||||
]
|
||||
search_fields = ['group_id', 'result']
|
||||
readonly_fields = [
|
||||
'group_id', 'date_created', 'date_done', 'content_type',
|
||||
'content_encoding', 'result'
|
||||
]
|
||||
date_hierarchy = 'date_created'
|
||||
ordering = ['-date_created']
|
||||
|
||||
fieldsets = (
|
||||
('Group Information', {
|
||||
'fields': ('group_id', 'date_created', 'date_done')
|
||||
}),
|
||||
('Result Details', {
|
||||
'fields': ('content_type', 'content_encoding', 'result'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def result_count(self, obj):
|
||||
"""Count tasks in the group"""
|
||||
if obj.result:
|
||||
try:
|
||||
import json
|
||||
result_data = json.loads(obj.result) if isinstance(obj.result, str) else obj.result
|
||||
if isinstance(result_data, list):
|
||||
return len(result_data)
|
||||
except:
|
||||
pass
|
||||
return '-'
|
||||
result_count.short_description = 'Task Count'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable manual group result creation"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Make read-only"""
|
||||
return False
|
||||
|
||||
253
backend/igny8_core/admin/reports.py
Normal file
253
backend/igny8_core/admin/reports.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Analytics & Reporting Views for IGNY8 Admin
|
||||
"""
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.shortcuts import render
|
||||
from django.db.models import Count, Sum, Avg, Q
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
import json
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def revenue_report(request):
|
||||
"""Revenue and billing analytics"""
|
||||
from igny8_core.business.billing.models import Payment
|
||||
from igny8_core.auth.models import Plan
|
||||
|
||||
# Date ranges
|
||||
today = timezone.now()
|
||||
months = []
|
||||
monthly_revenue = []
|
||||
|
||||
for i in range(6):
|
||||
month_start = today.replace(day=1) - timedelta(days=30*i)
|
||||
month_end = month_start.replace(day=28) + timedelta(days=4)
|
||||
|
||||
revenue = Payment.objects.filter(
|
||||
status='succeeded',
|
||||
processed_at__gte=month_start,
|
||||
processed_at__lt=month_end
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
months.insert(0, month_start.strftime('%b %Y'))
|
||||
monthly_revenue.insert(0, float(revenue))
|
||||
|
||||
# Plan distribution
|
||||
plan_distribution = Plan.objects.annotate(
|
||||
account_count=Count('account')
|
||||
).values('name', 'account_count')
|
||||
|
||||
# Payment method breakdown
|
||||
payment_methods = Payment.objects.filter(
|
||||
status='succeeded'
|
||||
).values('payment_method').annotate(
|
||||
count=Count('id'),
|
||||
total=Sum('amount')
|
||||
).order_by('-total')
|
||||
|
||||
# Total revenue all time
|
||||
total_revenue = Payment.objects.filter(
|
||||
status='succeeded'
|
||||
).aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
context = {
|
||||
'title': 'Revenue Report',
|
||||
'months': json.dumps(months),
|
||||
'monthly_revenue': json.dumps(monthly_revenue),
|
||||
'plan_distribution': list(plan_distribution),
|
||||
'payment_methods': list(payment_methods),
|
||||
'total_revenue': float(total_revenue),
|
||||
}
|
||||
|
||||
# Merge with admin context
|
||||
from igny8_core.admin.site import admin_site
|
||||
admin_context = admin_site.each_context(request)
|
||||
context.update(admin_context)
|
||||
|
||||
return render(request, 'admin/reports/revenue.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def usage_report(request):
|
||||
"""Credit usage and AI operations analytics"""
|
||||
from igny8_core.business.billing.models import CreditUsageLog
|
||||
|
||||
# Usage by operation type
|
||||
usage_by_operation = CreditUsageLog.objects.values(
|
||||
'operation_type'
|
||||
).annotate(
|
||||
total_credits=Sum('credits_used'),
|
||||
total_cost=Sum('cost_usd'),
|
||||
operation_count=Count('id')
|
||||
).order_by('-total_credits')
|
||||
|
||||
# Top credit consumers
|
||||
top_consumers = CreditUsageLog.objects.values(
|
||||
'account__name'
|
||||
).annotate(
|
||||
total_credits=Sum('credits_used'),
|
||||
operation_count=Count('id')
|
||||
).order_by('-total_credits')[:10]
|
||||
|
||||
# Model usage distribution
|
||||
model_usage = CreditUsageLog.objects.values(
|
||||
'model_used'
|
||||
).annotate(
|
||||
usage_count=Count('id')
|
||||
).order_by('-usage_count')
|
||||
|
||||
# Total credits used
|
||||
total_credits = CreditUsageLog.objects.aggregate(
|
||||
total=Sum('credits_used')
|
||||
)['total'] or 0
|
||||
|
||||
context = {
|
||||
'title': 'Usage Report',
|
||||
'usage_by_operation': list(usage_by_operation),
|
||||
'top_consumers': list(top_consumers),
|
||||
'model_usage': list(model_usage),
|
||||
'total_credits': int(total_credits),
|
||||
}
|
||||
|
||||
# Merge with admin context
|
||||
from igny8_core.admin.site import admin_site
|
||||
admin_context = admin_site.each_context(request)
|
||||
context.update(admin_context)
|
||||
|
||||
return render(request, 'admin/reports/usage.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def content_report(request):
|
||||
"""Content production analytics"""
|
||||
from igny8_core.modules.writer.models import Content, Tasks
|
||||
|
||||
# Content by type
|
||||
content_by_type = Content.objects.values(
|
||||
'content_type'
|
||||
).annotate(count=Count('id')).order_by('-count')
|
||||
|
||||
# Production timeline (last 30 days)
|
||||
days = []
|
||||
daily_counts = []
|
||||
for i in range(30):
|
||||
day = timezone.now().date() - timedelta(days=i)
|
||||
count = Content.objects.filter(created_at__date=day).count()
|
||||
days.insert(0, day.strftime('%m/%d'))
|
||||
daily_counts.insert(0, count)
|
||||
|
||||
# Average word count by content type
|
||||
avg_words = Content.objects.values('content_type').annotate(
|
||||
avg_words=Avg('word_count')
|
||||
).order_by('-avg_words')
|
||||
|
||||
# Task completion rate
|
||||
total_tasks = Tasks.objects.count()
|
||||
completed_tasks = Tasks.objects.filter(status='completed').count()
|
||||
completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
||||
|
||||
# Total content produced
|
||||
total_content = Content.objects.count()
|
||||
|
||||
context = {
|
||||
'title': 'Content Production Report',
|
||||
'content_by_type': list(content_by_type),
|
||||
'days': json.dumps(days),
|
||||
'daily_counts': json.dumps(daily_counts),
|
||||
'avg_words': list(avg_words),
|
||||
'completion_rate': round(completion_rate, 1),
|
||||
'total_content': total_content,
|
||||
'total_tasks': total_tasks,
|
||||
'completed_tasks': completed_tasks,
|
||||
}
|
||||
|
||||
# Merge with admin context
|
||||
from igny8_core.admin.site import admin_site
|
||||
admin_context = admin_site.each_context(request)
|
||||
context.update(admin_context)
|
||||
|
||||
return render(request, 'admin/reports/content.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def data_quality_report(request):
|
||||
"""Check data quality and integrity"""
|
||||
issues = []
|
||||
|
||||
# Orphaned content (no site)
|
||||
from igny8_core.modules.writer.models import Content
|
||||
orphaned_content = Content.objects.filter(site__isnull=True).count()
|
||||
if orphaned_content > 0:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'type': 'Orphaned Records',
|
||||
'count': orphaned_content,
|
||||
'description': 'Content items without assigned site',
|
||||
'action_url': '/admin/writer/content/?site__isnull=True'
|
||||
})
|
||||
|
||||
# Tasks without clusters
|
||||
from igny8_core.modules.writer.models import Tasks
|
||||
tasks_no_cluster = Tasks.objects.filter(cluster__isnull=True).count()
|
||||
if tasks_no_cluster > 0:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'type': 'Missing Relationships',
|
||||
'count': tasks_no_cluster,
|
||||
'description': 'Tasks without assigned cluster',
|
||||
'action_url': '/admin/writer/tasks/?cluster__isnull=True'
|
||||
})
|
||||
|
||||
# Accounts with negative credits
|
||||
from igny8_core.auth.models import Account
|
||||
negative_credits = Account.objects.filter(credits__lt=0).count()
|
||||
if negative_credits > 0:
|
||||
issues.append({
|
||||
'severity': 'error',
|
||||
'type': 'Data Integrity',
|
||||
'count': negative_credits,
|
||||
'description': 'Accounts with negative credit balance',
|
||||
'action_url': '/admin/igny8_core_auth/account/?credits__lt=0'
|
||||
})
|
||||
|
||||
# Duplicate keywords
|
||||
from igny8_core.modules.planner.models import Keywords
|
||||
duplicates = Keywords.objects.values('keyword', 'site', 'sector').annotate(
|
||||
count=Count('id')
|
||||
).filter(count__gt=1).count()
|
||||
if duplicates > 0:
|
||||
issues.append({
|
||||
'severity': 'warning',
|
||||
'type': 'Duplicates',
|
||||
'count': duplicates,
|
||||
'description': 'Duplicate keywords for same site/sector',
|
||||
'action_url': '/admin/planner/keywords/'
|
||||
})
|
||||
|
||||
# Content without SEO data
|
||||
no_seo = Content.objects.filter(
|
||||
Q(meta_title__isnull=True) | Q(meta_title='') |
|
||||
Q(meta_description__isnull=True) | Q(meta_description='')
|
||||
).count()
|
||||
if no_seo > 0:
|
||||
issues.append({
|
||||
'severity': 'info',
|
||||
'type': 'Incomplete Data',
|
||||
'count': no_seo,
|
||||
'description': 'Content missing SEO metadata',
|
||||
'action_url': '/admin/writer/content/'
|
||||
})
|
||||
|
||||
context = {
|
||||
'title': 'Data Quality Report',
|
||||
'issues': issues,
|
||||
'total_issues': len(issues),
|
||||
}
|
||||
|
||||
# Merge with admin context
|
||||
from igny8_core.admin.site import admin_site
|
||||
admin_context = admin_site.each_context(request)
|
||||
context.update(admin_context)
|
||||
|
||||
return render(request, 'admin/reports/data_quality.html', context)
|
||||
@@ -21,16 +21,26 @@ class Igny8AdminSite(UnfoldAdminSite):
|
||||
index_title = 'IGNY8 Administration'
|
||||
|
||||
def get_urls(self):
|
||||
"""Get admin URLs with dashboard available at /admin/dashboard/"""
|
||||
"""Get admin URLs with dashboard and reports available"""
|
||||
from django.urls import path
|
||||
from .dashboard import admin_dashboard
|
||||
from .reports import revenue_report, usage_report, content_report, data_quality_report
|
||||
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path('dashboard/', self.admin_view(admin_dashboard), name='dashboard'),
|
||||
path('reports/revenue/', self.admin_view(revenue_report), name='report_revenue'),
|
||||
path('reports/usage/', self.admin_view(usage_report), name='report_usage'),
|
||||
path('reports/content/', self.admin_view(content_report), name='report_content'),
|
||||
path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def index(self, request, extra_context=None):
|
||||
"""Redirect to custom dashboard"""
|
||||
from django.shortcuts import redirect
|
||||
return redirect('admin:dashboard')
|
||||
|
||||
def get_sidebar_list(self, request):
|
||||
"""
|
||||
Override Unfold's get_sidebar_list to return our custom app groups
|
||||
|
||||
117
backend/igny8_core/templates/admin/reports/content.html
Normal file
117
backend/igny8_core/templates/admin/reports/content.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-6 py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Content Production Report</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Content creation metrics and task completion analytics</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Content</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_content }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Tasks</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_tasks }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Completed</h3>
|
||||
<p class="text-3xl font-bold text-green-600">{{ completed_tasks }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Completion Rate</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ completion_rate }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Production Timeline -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Daily Production (Last 30 Days)</h2>
|
||||
<canvas id="productionChart" height="80"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Content by Type -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Content by Type</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Content Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for content in content_by_type %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ content.content_type|default:"Unknown" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ content.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Word Count -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Average Word Count by Type</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Content Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Avg Words</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for avg in avg_words %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ avg.content_type|default:"Unknown" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ avg.avg_words|floatformat:0 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('productionChart');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {{ days|safe }},
|
||||
datasets: [{
|
||||
label: 'Content Produced',
|
||||
data: {{ daily_counts|safe }},
|
||||
backgroundColor: '#0bbf87',
|
||||
borderColor: '#0bbf87',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
91
backend/igny8_core/templates/admin/reports/data_quality.html
Normal file
91
backend/igny8_core/templates/admin/reports/data_quality.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-6 py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Data Quality Report</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">System integrity and data quality checks</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Card -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Total Issues Found</h3>
|
||||
<p class="text-4xl font-bold {% if total_issues == 0 %}text-green-600{% elif total_issues < 5 %}text-yellow-600{% else %}text-red-600{% endif %}">
|
||||
{{ total_issues }}
|
||||
</p>
|
||||
</div>
|
||||
{% if total_issues == 0 %}
|
||||
<div class="text-green-600">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Issues List -->
|
||||
{% if issues %}
|
||||
<div class="space-y-4">
|
||||
{% for issue in issues %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
{% if issue.severity == 'error' %}
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif issue.severity == 'warning' %}
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ issue.type }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ issue.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {% if issue.severity == 'error' %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% elif issue.severity == 'warning' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200{% endif %}">
|
||||
{{ issue.count }} issue{{ issue.count|pluralize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ issue.action_url }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Review & Fix
|
||||
<svg class="ml-2 -mr-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-8 text-center">
|
||||
<svg class="mx-auto w-16 h-16 text-green-600 dark:text-green-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-green-900 dark:text-green-100 mb-2">All Clear!</h3>
|
||||
<p class="text-green-700 dark:text-green-300">No data quality issues found. Your system is healthy.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
115
backend/igny8_core/templates/admin/reports/revenue.html
Normal file
115
backend/igny8_core/templates/admin/reports/revenue.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-6 py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Revenue Report</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Financial performance and billing analytics</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Revenue</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">${{ total_revenue|floatformat:2 }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Payment Methods</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ payment_methods|length }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Plans</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ plan_distribution|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Chart -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Monthly Revenue (Last 6 Months)</h2>
|
||||
<canvas id="revenueChart" height="80"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Plan Distribution -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Plan Distribution</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Plan Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Accounts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for plan in plan_distribution %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ plan.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ plan.account_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Methods -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Payment Methods</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Method</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for method in payment_methods %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ method.payment_method|default:"Unknown" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ method.count }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${{ method.total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('revenueChart');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {{ months|safe }},
|
||||
datasets: [{
|
||||
label: 'Revenue ($)',
|
||||
data: {{ monthly_revenue|safe }},
|
||||
borderColor: '#0693e3',
|
||||
backgroundColor: 'rgba(6, 147, 227, 0.1)',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
103
backend/igny8_core/templates/admin/reports/usage.html
Normal file
103
backend/igny8_core/templates/admin/reports/usage.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-6 py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Usage Report</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Credit usage and AI operations analytics</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Credits Used</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_credits|floatformat:0 }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Operation Types</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ usage_by_operation|length }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Accounts</h3>
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ top_consumers|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage by Operation Type -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Usage by Operation Type</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operation</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Credits Used</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost (USD)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for usage in usage_by_operation %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ usage.operation_type }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ usage.total_credits|floatformat:0 }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${{ usage.total_cost|floatformat:2 }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ usage.operation_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Credit Consumers -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Top Credit Consumers</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Account</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Credits</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Operations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for consumer in top_consumers %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ consumer.account__name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ consumer.total_credits|floatformat:0 }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ consumer.operation_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Usage Distribution -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Model Usage Distribution</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Usage Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for model in model_usage %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ model.model_used }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ model.usage_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1273,33 +1273,38 @@ class Command(BaseCommand):
|
||||
- [ ] Test all bulk operations
|
||||
- [ ] Test export functionality
|
||||
|
||||
### Phase 3: Monitoring & Dashboards (Week 4-5) - NOT STARTED
|
||||
### ✅ Phase 3: Monitoring & Dashboards (COMPLETED - Dec 14, 2025)
|
||||
|
||||
- [ ] Install django-celery-results
|
||||
- [ ] Configure Celery to use django-db backend
|
||||
- [ ] Create CeleryTaskResultAdmin with colored status
|
||||
- [ ] Add retry_failed_tasks action
|
||||
- [ ] Create admin_dashboard view function
|
||||
- [ ] Create dashboard.html template with metrics
|
||||
- [ ] Add dashboard route to admin site URLs
|
||||
- [ ] Redirect admin index to dashboard
|
||||
- [ ] Add health_indicator to Account admin
|
||||
- [ ] Create AdminAlerts utility class
|
||||
- [ ] Add alerts section to dashboard template
|
||||
- [ ] Add alert styling CSS
|
||||
- [ ] Test dashboard metrics accuracy
|
||||
- [ ] Test alert system functionality
|
||||
- [x] Install django-celery-results
|
||||
- [x] Configure Celery to use django-db backend
|
||||
- [x] Create CeleryTaskResultAdmin with colored status
|
||||
- [x] Create CeleryGroupResultAdmin with colored status
|
||||
- [x] Add retry_failed_tasks action
|
||||
- [x] Add clear_old_tasks action
|
||||
- [x] Create admin_dashboard view function
|
||||
- [x] Create dashboard.html template with metrics
|
||||
- [x] Add dashboard route to admin site URLs
|
||||
- [x] Redirect admin index to dashboard
|
||||
- [x] Add health_indicator to Account admin
|
||||
- [x] Add health_details to Account admin
|
||||
- [x] Create AdminAlerts utility class
|
||||
- [x] Add alerts section to dashboard template
|
||||
- [x] Fix execution_time format_html issue
|
||||
- [x] Test dashboard metrics accuracy
|
||||
- [x] Test alert system functionality
|
||||
- [x] Verify all Celery admin pages work (200 status)
|
||||
|
||||
### Phase 4: Analytics & Reporting (Week 6-7) - NOT STARTED
|
||||
### Phase 4: Analytics & Reporting (Week 6-7) - IN PROGRESS
|
||||
|
||||
- [ ] Create reports.py module
|
||||
- [ ] Implement revenue_report view
|
||||
- [ ] Implement usage_report view
|
||||
- [ ] Implement content_report view
|
||||
- [ ] Implement data_quality_report view
|
||||
- [ ] Create report templates (revenue, usage, content, data_quality)
|
||||
- [ ] Add chart.js or similar for visualizations
|
||||
- [ ] Add report links to admin navigation
|
||||
- [x] Create reports.py module
|
||||
- [x] Implement revenue_report view
|
||||
- [x] Implement usage_report view
|
||||
- [x] Implement content_report view
|
||||
- [x] Implement data_quality_report view
|
||||
- [x] Create report templates (revenue, usage, content, data_quality)
|
||||
- [x] Add chart.js for visualizations
|
||||
- [x] Add report routes to admin site URLs
|
||||
- [ ] Add report links to admin sidebar navigation
|
||||
- [ ] Create report permission checks
|
||||
- [ ] Test all reports with real data
|
||||
- [ ] Optimize report queries for performance
|
||||
Reference in New Issue
Block a user