feat: add Usage Limits Panel component with usage tracking and visual indicators for limits

style: implement custom color schemes and gradients for account section, enhancing visual hierarchy
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-12 13:15:15 +00:00
parent 12956ec64a
commit 6e2101d019
29 changed files with 3622 additions and 85 deletions

677
docs/PLAN-LIMITS.md Normal file
View File

@@ -0,0 +1,677 @@
# Plan Limits System
## Overview
The Plan Limits System enforces subscription-based usage restrictions in IGNY8. It tracks both **hard limits** (persistent throughout subscription) and **monthly limits** (reset on billing cycle).
**File:** `/docs/PLAN-LIMITS.md`
**Version:** 1.0.0
**Last Updated:** December 12, 2025
---
## Architecture
### Limit Types
#### Hard Limits (Never Reset)
These limits persist for the lifetime of the subscription and represent total capacity:
| Limit Type | Field Name | Description | Example Value |
|------------|------------|-------------|---------------|
| Sites | `max_sites` | Maximum number of sites per account | Starter: 2, Growth: 5, Scale: Unlimited |
| Team Users | `max_users` | Maximum team members | Starter: 1, Growth: 3, Scale: 10 |
| Keywords | `max_keywords` | Total keywords allowed | Starter: 500, Growth: 1000, Scale: Unlimited |
| Clusters | `max_clusters` | Total clusters allowed | Starter: 50, Growth: 100, Scale: Unlimited |
#### Monthly Limits (Reset on Billing Cycle)
These limits reset automatically at the start of each billing period:
| Limit Type | Field Name | Description | Example Value |
|------------|------------|-------------|---------------|
| Content Ideas | `max_content_ideas` | New ideas generated per month | Starter: 100, Growth: 300, Scale: 600 |
| Content Words | `max_content_words` | Total words generated per month | Starter: 100K, Growth: 300K, Scale: 500K |
| Basic Images | `max_images_basic` | Basic AI images per month | Starter: 100, Growth: 300, Scale: 500 |
| Premium Images | `max_images_premium` | Premium AI images (DALL-E) per month | Starter: 20, Growth: 60, Scale: 100 |
| Image Prompts | `max_image_prompts` | AI-generated prompts per month | Starter: 100, Growth: 300, Scale: 500 |
---
## Database Schema
### Plan Model Extensions
**Location:** `backend/igny8_core/auth/models.py`
```python
class Plan(models.Model):
# ... existing fields ...
# Hard Limits
max_sites = IntegerField(default=2, validators=[MinValueValidator(1)])
max_users = IntegerField(default=1, validators=[MinValueValidator(1)])
max_keywords = IntegerField(default=500, validators=[MinValueValidator(1)])
max_clusters = IntegerField(default=50, validators=[MinValueValidator(1)])
# Monthly Limits
max_content_ideas = IntegerField(default=100, validators=[MinValueValidator(1)])
max_content_words = IntegerField(default=100000, validators=[MinValueValidator(1)])
max_images_basic = IntegerField(default=100, validators=[MinValueValidator(1)])
max_images_premium = IntegerField(default=20, validators=[MinValueValidator(1)])
max_image_prompts = IntegerField(default=100, validators=[MinValueValidator(1)])
```
### PlanLimitUsage Model
**Location:** `backend/igny8_core/business/billing/models.py`
Tracks monthly consumption for each limit type:
```python
class PlanLimitUsage(AccountBaseModel):
LIMIT_TYPE_CHOICES = [
('content_ideas', 'Content Ideas'),
('content_words', 'Content Words'),
('images_basic', 'Basic Images'),
('images_premium', 'Premium Images'),
('image_prompts', 'Image Prompts'),
]
limit_type = CharField(max_length=50, choices=LIMIT_TYPE_CHOICES, db_index=True)
amount_used = IntegerField(default=0, validators=[MinValueValidator(0)])
period_start = DateField()
period_end = DateField()
metadata = JSONField(default=dict) # Stores breakdown by site, content_id, etc.
class Meta:
unique_together = [['account', 'limit_type', 'period_start']]
indexes = [
Index(fields=['account', 'period_start']),
Index(fields=['period_end']),
]
```
**Migration:** `backend/igny8_core/modules/billing/migrations/0015_planlimitusage.py`
---
## Service Layer
### LimitService
**Location:** `backend/igny8_core/business/billing/services/limit_service.py`
Central service for all limit operations.
#### Key Methods
##### 1. Check Hard Limit
```python
LimitService.check_hard_limit(account, limit_type, additional_count=1)
```
**Purpose:** Validate if adding items would exceed hard limit
**Raises:** `HardLimitExceededError` if limit exceeded
**Example:**
```python
try:
LimitService.check_hard_limit(account, 'sites', additional_count=1)
# Proceed with site creation
except HardLimitExceededError as e:
raise PermissionDenied(str(e))
```
##### 2. Check Monthly Limit
```python
LimitService.check_monthly_limit(account, limit_type, amount)
```
**Purpose:** Validate if operation would exceed monthly allowance
**Raises:** `MonthlyLimitExceededError` if limit exceeded
**Example:**
```python
try:
LimitService.check_monthly_limit(account, 'content_words', amount=2500)
# Proceed with content generation
except MonthlyLimitExceededError as e:
raise InsufficientCreditsError(str(e))
```
##### 3. Increment Usage
```python
LimitService.increment_usage(account, limit_type, amount, metadata=None)
```
**Purpose:** Record usage after successful operation
**Returns:** New total usage
**Example:**
```python
LimitService.increment_usage(
account=account,
limit_type='content_words',
amount=2500,
metadata={
'content_id': 123,
'content_title': 'My Article',
'site_id': 456
}
)
```
##### 4. Get Usage Summary
```python
LimitService.get_usage_summary(account)
```
**Purpose:** Comprehensive usage report for all limits
**Returns:** Dictionary with hard_limits, monthly_limits, period info
**Example Response:**
```json
{
"account_id": 1,
"account_name": "Acme Corp",
"plan_name": "Growth Plan",
"period_start": "2025-12-01",
"period_end": "2025-12-31",
"days_until_reset": 19,
"hard_limits": {
"sites": {
"display_name": "Sites",
"current": 3,
"limit": 5,
"remaining": 2,
"percentage_used": 60
},
"keywords": {
"display_name": "Keywords",
"current": 750,
"limit": 1000,
"remaining": 250,
"percentage_used": 75
}
},
"monthly_limits": {
"content_words": {
"display_name": "Content Words",
"current": 245000,
"limit": 300000,
"remaining": 55000,
"percentage_used": 82
},
"images_basic": {
"display_name": "Basic Images",
"current": 120,
"limit": 300,
"remaining": 180,
"percentage_used": 40
}
}
}
```
##### 5. Reset Monthly Limits
```python
LimitService.reset_monthly_limits(account)
```
**Purpose:** Reset all monthly usage at period end (called by Celery task)
**Returns:** Dictionary with reset summary
**Note:** Called automatically by scheduled task, not manually
---
## Enforcement Points
### 1. Site Creation
**File:** `backend/igny8_core/auth/views.py` (SiteViewSet.perform_create)
```python
LimitService.check_hard_limit(account, 'sites', additional_count=1)
```
### 2. Content Generation
**File:** `backend/igny8_core/business/content/services/content_generation_service.py`
```python
# Check limit before generation
LimitService.check_monthly_limit(account, 'content_words', amount=total_word_count)
# Increment usage after successful generation
LimitService.increment_usage(account, 'content_words', amount=actual_word_count)
```
### 3. Content Save Hook
**File:** `backend/igny8_core/business/content/models.py` (Content.save)
Automatically increments `content_words` usage when content_html is saved:
```python
def save(self, *args, **kwargs):
# Auto-calculate word count
if self.content_html:
calculated_count = calculate_word_count(self.content_html)
self.word_count = calculated_count
super().save(*args, **kwargs)
# Increment usage for newly generated words
if new_words > 0:
LimitService.increment_usage(account, 'content_words', amount=new_words)
```
### 4. Additional Enforcement Points (To Be Implemented)
Following the same pattern, add checks to:
- **Keyword Import:** Check `max_keywords` before bulk import
- **Clustering:** Check `max_clusters` before creating new clusters
- **Idea Generation:** Check `max_content_ideas` before generating ideas
- **Image Generation:** Check `max_images_basic`/`max_images_premium` before AI call
---
## Word Counting Utility
**Location:** `backend/igny8_core/utils/word_counter.py`
Provides accurate word counting from HTML content.
### Functions
#### calculate_word_count(html_content)
```python
from igny8_core.utils.word_counter import calculate_word_count
word_count = calculate_word_count('<p>Hello <strong>world</strong>!</p>')
# Returns: 2
```
**Method:**
1. Strips HTML tags using BeautifulSoup
2. Fallback to regex if BeautifulSoup fails
3. Counts words (sequences of alphanumeric characters)
#### format_word_count(count)
```python
formatted = format_word_count(1500) # "1.5K"
formatted = format_word_count(125000) # "125K"
```
#### validate_word_count_limit(html_content, limit)
```python
result = validate_word_count_limit(html, limit=100000)
# Returns: {
# 'allowed': True,
# 'word_count': 2500,
# 'limit': 100000,
# 'remaining': 97500,
# 'would_exceed_by': 0
# }
```
---
## Scheduled Tasks
**Location:** `backend/igny8_core/tasks/plan_limits.py`
### 1. Reset Monthly Plan Limits
**Task Name:** `reset_monthly_plan_limits`
**Schedule:** Daily at 00:30 UTC
**Purpose:** Reset monthly usage for accounts at period end
**Process:**
1. Find all active accounts with subscriptions
2. Check if `current_period_end` <= today
3. Call `LimitService.reset_monthly_limits(account)`
4. Update subscription period dates
5. Log reset summary
### 2. Check Approaching Limits
**Task Name:** `check_approaching_limits`
**Schedule:** Daily at 09:00 UTC
**Purpose:** Warn users when usage exceeds 80% threshold
**Process:**
1. Find all active accounts
2. Get usage summary
3. Check if any limit >= 80%
4. Log warnings (future: send email notifications)
**Celery Beat Configuration:**
`backend/igny8_core/celery.py`
```python
app.conf.beat_schedule = {
'reset-monthly-plan-limits': {
'task': 'reset_monthly_plan_limits',
'schedule': crontab(hour=0, minute=30),
},
'check-approaching-limits': {
'task': 'check_approaching_limits',
'schedule': crontab(hour=9, minute=0),
},
}
```
---
## API Endpoints
### Get Usage Summary
**Endpoint:** `GET /api/v1/billing/usage-summary/`
**Authentication:** Required (IsAuthenticatedAndActive)
**Response:** Usage summary for current account
**Example Request:**
```bash
curl -H "Authorization: Bearer <token>" \
/api/v1/billing/usage-summary/
```
**Example Response:**
```json
{
"success": true,
"message": "Usage summary retrieved successfully.",
"data": {
"account_id": 1,
"account_name": "Acme Corp",
"plan_name": "Growth Plan",
"period_start": "2025-12-01",
"period_end": "2025-12-31",
"days_until_reset": 19,
"hard_limits": { ... },
"monthly_limits": { ... }
}
}
```
---
## Error Handling
### HardLimitExceededError
```python
raise HardLimitExceededError(
f"Sites limit exceeded. Current: 5, Limit: 5. "
f"Upgrade your plan to increase this limit."
)
```
**HTTP Status:** 403 Forbidden
**User Action:** Upgrade plan or delete unused resources
### MonthlyLimitExceededError
```python
raise MonthlyLimitExceededError(
f"Content Words limit exceeded. Used: 295000, Requested: 8000, Limit: 300000. "
f"Resets on December 31, 2025. Upgrade your plan or wait for reset."
)
```
**HTTP Status:** 403 Forbidden
**User Action:** Wait for reset, upgrade plan, or reduce request size
---
## Frontend Integration Guide
### TypeScript Types
```typescript
interface Plan {
id: number;
name: string;
// Hard limits
max_sites: number;
max_users: number;
max_keywords: number;
max_clusters: number;
// Monthly limits
max_content_ideas: number;
max_content_words: number;
max_images_basic: number;
max_images_premium: number;
max_image_prompts: number;
}
interface UsageSummary {
account_id: number;
account_name: string;
plan_name: string;
period_start: string;
period_end: string;
days_until_reset: number;
hard_limits: {
[key: string]: {
display_name: string;
current: number;
limit: number;
remaining: number;
percentage_used: number;
};
};
monthly_limits: {
[key: string]: {
display_name: string;
current: number;
limit: number;
remaining: number;
percentage_used: number;
};
};
}
```
### API Hook Example
```typescript
// src/services/api/billing.ts
export const getUsageSummary = async (): Promise<UsageSummary> => {
const response = await apiClient.get('/billing/usage-summary/');
return response.data.data;
};
// src/pages/Dashboard.tsx
const { data: usage } = useQuery('usage-summary', getUsageSummary);
```
### UI Components
#### Usage Widget
```tsx
<Card>
<CardHeader>
<h3>Usage This Month</h3>
<span>{usage.days_until_reset} days until reset</span>
</CardHeader>
<CardBody>
{Object.entries(usage.monthly_limits).map(([key, data]) => (
<div key={key}>
<div>{data.display_name}</div>
<ProgressBar
value={data.percentage_used}
variant={data.percentage_used >= 80 ? 'warning' : 'primary'}
/>
<span>{data.current.toLocaleString()} / {data.limit.toLocaleString()}</span>
</div>
))}
</CardBody>
</Card>
```
#### Limit Warning Alert
```tsx
{usage.monthly_limits.content_words.percentage_used >= 80 && (
<Alert variant="warning">
You've used {usage.monthly_limits.content_words.percentage_used}% of your
monthly word limit. Resets in {usage.days_until_reset} days.
<Link to="/billing/plans">Upgrade Plan</Link>
</Alert>
)}
```
---
## Testing
### Manual Testing Checklist
1. **Hard Limit - Sites:**
- Set plan `max_sites = 2`
- Create 2 sites successfully
- Attempt to create 3rd site → should fail with error
2. **Monthly Limit - Words:**
- Set plan `max_content_words = 5000`
- Generate content with 3000 words
- Generate content with 2500 words → should fail
- Check usage API shows 3000/5000
3. **Usage Increment:**
- Generate content
- Verify `PlanLimitUsage.amount_used` increments correctly
- Check metadata contains content_id
4. **Monthly Reset:**
- Manually run: `docker exec igny8_backend python manage.py shell`
- Execute:
```python
from igny8_core.tasks.plan_limits import reset_monthly_plan_limits
reset_monthly_plan_limits()
```
- Verify usage resets to 0
- Verify new period records created
5. **Usage Summary API:**
- Call GET `/api/v1/billing/usage-summary/`
- Verify all limits present
- Verify percentages calculated correctly
### Unit Test Example
```python
# tests/test_limit_service.py
def test_check_hard_limit_exceeded():
account = create_test_account(plan_max_sites=2)
create_test_sites(account, count=2)
with pytest.raises(HardLimitExceededError):
LimitService.check_hard_limit(account, 'sites', additional_count=1)
def test_increment_monthly_usage():
account = create_test_account()
LimitService.increment_usage(account, 'content_words', amount=1000)
usage = PlanLimitUsage.objects.get(account=account, limit_type='content_words')
assert usage.amount_used == 1000
```
---
## Monitoring & Logs
### Key Log Messages
**Successful limit check:**
```
INFO Hard limit check: sites - Current: 2, Requested: 1, Limit: 5
INFO Monthly limit check: content_words - Current: 50000, Requested: 2500, Limit: 100000
```
**Limit exceeded:**
```
WARNING Hard limit exceeded: sites - Current: 5, Requested: 1, Limit: 5
WARNING Monthly limit exceeded: content_words - Used: 98000, Requested: 5000, Limit: 100000
```
**Usage increment:**
```
INFO Incremented content_words usage by 2500. New total: 52500
```
**Monthly reset:**
```
INFO Resetting limits for account 123 (Acme Corp) - period ended 2025-12-31
INFO Reset complete for account 123: New period 2026-01-01 to 2026-01-31
INFO Monthly plan limits reset task complete: 45 accounts reset, 0 errors
```
---
## Troubleshooting
### Issue: Limits not enforcing
**Check:**
1. Verify Plan has non-zero limit values: `Plan.objects.get(id=X)`
2. Check if service calling LimitService methods
3. Review logs for exceptions being caught
### Issue: Usage not incrementing
**Check:**
1. Verify Content.save() executing successfully
2. Check for exceptions in logs during increment_usage
3. Query `PlanLimitUsage` table directly
### Issue: Reset task not running
**Check:**
1. Celery Beat is running: `docker exec igny8_backend celery -A igny8_core inspect active`
2. Check Celery Beat schedule: `docker exec igny8_backend celery -A igny8_core inspect scheduled`
3. Review Celery logs: `docker logs igny8_celery_beat`
---
## Future Enhancements
1. **Email Notifications:**
- Send warning emails at 80%, 90%, 100% thresholds
- Weekly usage summary reports
- Monthly reset confirmations
2. **Additional Enforcement:**
- Keyword bulk import limit check
- Cluster creation limit check
- Idea generation limit check
- Image generation limit checks
3. **Usage Analytics:**
- Historical usage trends
- Projection of limit exhaustion date
- Recommendations for plan upgrades
4. **Soft Limits:**
- Allow slight overages with warnings
- Grace period before hard enforcement
5. **Admin Tools:**
- Override limits for specific accounts
- One-time usage bonuses
- Custom limit adjustments
---
## Related Files
**Models:**
- `backend/igny8_core/auth/models.py` - Plan model
- `backend/igny8_core/business/billing/models.py` - PlanLimitUsage model
**Services:**
- `backend/igny8_core/business/billing/services/limit_service.py` - LimitService
- `backend/igny8_core/utils/word_counter.py` - Word counting utility
**Views:**
- `backend/igny8_core/auth/views.py` - Site creation enforcement
- `backend/igny8_core/business/billing/views.py` - Usage summary API
- `backend/igny8_core/business/content/services/content_generation_service.py` - Content generation enforcement
**Tasks:**
- `backend/igny8_core/tasks/plan_limits.py` - Reset and warning tasks
- `backend/igny8_core/celery.py` - Celery Beat schedule
**Migrations:**
- `backend/igny8_core/auth/migrations/0013_plan_max_clusters_plan_max_content_ideas_and_more.py`
- `backend/igny8_core/modules/billing/migrations/0015_planlimitusage.py`
**Documentation:**
- `CHANGELOG.md` - Version history with plan limits feature
- `.cursorrules` - Development standards and versioning rules
---
**End of Document**