iamge genration credits fixing - not fixed
This commit is contained in:
78
CHANGELOG.md
78
CHANGELOG.md
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
| Version | Date | Summary |
|
| Version | Date | Summary |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
|
| 1.7.1 | Jan 10, 2026 | **Image Generation Credits** - Complete credit deduction, verification, and logging for all image generation models (DALL-E 3, Runware, Google); Pre-generation credit check; AITaskLog logging; Notification integration |
|
||||||
| 1.7.0 | Jan 10, 2026 | **Major** - Pre-Launch Cleanup Complete (Phases 1, 5, 6): Code cleanup, UX improvements, data backup tools; WordPress plugin distribution system; Template design improvements; AI model fixes |
|
| 1.7.0 | Jan 10, 2026 | **Major** - Pre-Launch Cleanup Complete (Phases 1, 5, 6): Code cleanup, UX improvements, data backup tools; WordPress plugin distribution system; Template design improvements; AI model fixes |
|
||||||
| 1.6.2 | Jan 8, 2026 | **Design Refinements** - Updated marketing site gradients to brand colors (primary + success), reduced shadow weights, simplified automation icons, added Upcoming Features page |
|
| 1.6.2 | Jan 8, 2026 | **Design Refinements** - Updated marketing site gradients to brand colors (primary + success), reduced shadow weights, simplified automation icons, added Upcoming Features page |
|
||||||
| 1.6.1 | Jan 8, 2026 | **Email System** - SMTP configuration, email templates, password reset flow, email verification, unsubscribe functionality |
|
| 1.6.1 | Jan 8, 2026 | **Email System** - SMTP configuration, email templates, password reset flow, email verification, unsubscribe functionality |
|
||||||
@@ -40,6 +41,83 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.7.1 - January 10, 2026
|
||||||
|
|
||||||
|
### Image Generation Credit System
|
||||||
|
|
||||||
|
This release implements complete credit deduction, verification, and logging for all 3 image generation models, ensuring consistency with other AI functions in the automation pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💰 Credit System for Image Generation
|
||||||
|
|
||||||
|
**New Credit Check Method:**
|
||||||
|
- Added `CreditService.check_credits_for_image()` in [credit_service.py:307-335](backend/igny8_core/business/billing/services/credit_service.py#L307-L335)
|
||||||
|
- Pre-checks if account has sufficient credits before image generation starts
|
||||||
|
- Uses `credits_per_image` from AIModelConfig (as configured in admin)
|
||||||
|
- Returns required credits or raises `InsufficientCreditsError`
|
||||||
|
|
||||||
|
**Pre-Generation Credit Verification:**
|
||||||
|
- Added credit check before processing images in [tasks.py:290-319](backend/igny8_core/ai/tasks.py#L290-L319)
|
||||||
|
- Checks total credits needed for all images BEFORE starting generation
|
||||||
|
- Returns error with `InsufficientCreditsError` if not enough credits
|
||||||
|
- Gracefully handles missing model configurations for backward compatibility
|
||||||
|
|
||||||
|
**Credit Deduction (Already Existed - Now Verified):**
|
||||||
|
- `deduct_credits_for_image()` called after each successful image in [tasks.py:745-770](backend/igny8_core/ai/tasks.py#L745-L770)
|
||||||
|
- Fixed cost retrieval to use `result.get('cost')` (was incorrectly using `cost_usd`)
|
||||||
|
- Credits per model (from AIModelConfig):
|
||||||
|
- `runware:97@1`: 1 credit/image (basic tier)
|
||||||
|
- `dall-e-3`: 5 credits/image (quality tier)
|
||||||
|
- `google:4@2`: 15 credits/image (premium tier)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 Logging & Notifications
|
||||||
|
|
||||||
|
**AITaskLog Integration:**
|
||||||
|
- Added AITaskLog logging in [tasks.py:838-875](backend/igny8_core/ai/tasks.py#L838-L875)
|
||||||
|
- Records function_name (`generate_images`), account, phase, status, cost
|
||||||
|
- Consistent with other AI function logging (clustering, content generation, etc.)
|
||||||
|
|
||||||
|
**Notification Integration:**
|
||||||
|
- Added notification creation in [tasks.py:877-895](backend/igny8_core/ai/tasks.py#L877-L895)
|
||||||
|
- Creates success notifications via `NotificationService.notify_images_complete()`
|
||||||
|
- Creates failure notifications via `NotificationService.notify_images_failed()`
|
||||||
|
- Same pattern as used in AIEngine for other AI functions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔄 Automation Compatibility
|
||||||
|
|
||||||
|
**Verified automation flow for image generation:**
|
||||||
|
- Stage 6 in automation service calls `process_image_generation_queue` correctly
|
||||||
|
- Credit deduction happens inside the task (same as manual image generation)
|
||||||
|
- Credits tracked via `_get_credits_used()` and stored in `stage_6_result`
|
||||||
|
- Consistent with Stages 1, 2, 4, 5 credit tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📁 Files Changed
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `backend/igny8_core/business/billing/services/credit_service.py` | Added `check_credits_for_image()` method |
|
||||||
|
| `backend/igny8_core/ai/tasks.py` | Added credit check, AITaskLog logging, notifications |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📋 Credit Usage Logging Locations
|
||||||
|
|
||||||
|
| Table | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `CreditTransaction` | Financial ledger entry (via `deduct_credits_for_image`) |
|
||||||
|
| `CreditUsageLog` | Detailed usage log with model, cost, credits |
|
||||||
|
| `AITaskLog` | AI task execution tracking |
|
||||||
|
| `Notification` | User notifications for completion/failure |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.7.0 - January 10, 2026
|
## v1.7.0 - January 10, 2026
|
||||||
|
|
||||||
### Pre-Launch Cleanup & Plugin Distribution System
|
### Pre-Launch Cleanup & Plugin Distribution System
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
from igny8_core.ai.ai_core import AICore
|
from igny8_core.ai.ai_core import AICore
|
||||||
from igny8_core.ai.prompts import PromptRegistry
|
from igny8_core.ai.prompts import PromptRegistry
|
||||||
from igny8_core.business.billing.services.credit_service import CreditService
|
from igny8_core.business.billing.services.credit_service import CreditService
|
||||||
|
from igny8_core.business.billing.exceptions import InsufficientCreditsError, CreditCalculationError
|
||||||
|
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info(f"process_image_generation_queue STARTED")
|
logger.info(f"process_image_generation_queue STARTED")
|
||||||
@@ -286,6 +287,37 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
# Initialize AICore
|
# Initialize AICore
|
||||||
ai_core = AICore(account=account)
|
ai_core = AICore(account=account)
|
||||||
|
|
||||||
|
# Credit check before processing images
|
||||||
|
if account:
|
||||||
|
logger.info(f"[process_image_generation_queue] Step 3: Checking credits for {total_images} images")
|
||||||
|
try:
|
||||||
|
required_credits = CreditService.check_credits_for_image(
|
||||||
|
account=account,
|
||||||
|
model_name=model,
|
||||||
|
num_images=total_images
|
||||||
|
)
|
||||||
|
logger.info(f"[process_image_generation_queue] Credit check passed: {required_credits} credits required, {account.credits} available")
|
||||||
|
except InsufficientCreditsError as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
logger.error(f"[process_image_generation_queue] Insufficient credits: {error_msg}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': error_msg,
|
||||||
|
'error_type': 'InsufficientCreditsError',
|
||||||
|
'total_images': total_images,
|
||||||
|
'completed': 0,
|
||||||
|
'failed': total_images,
|
||||||
|
'results': []
|
||||||
|
}
|
||||||
|
except CreditCalculationError as e:
|
||||||
|
# Model not found or no credits_per_image configured - log warning but continue
|
||||||
|
# This allows backward compatibility if model not configured
|
||||||
|
logger.warning(f"[process_image_generation_queue] Credit calculation warning: {e}")
|
||||||
|
logger.warning(f"[process_image_generation_queue] Proceeding without credit check (model may not be configured)")
|
||||||
|
except Exception as e:
|
||||||
|
# Don't fail for unexpected credit check errors - log and continue
|
||||||
|
logger.warning(f"[process_image_generation_queue] Unexpected credit check error: {e}")
|
||||||
|
|
||||||
# Process each image sequentially
|
# Process each image sequentially
|
||||||
for index, image_id in enumerate(image_ids, 1):
|
for index, image_id in enumerate(image_ids, 1):
|
||||||
try:
|
try:
|
||||||
@@ -712,7 +744,7 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
else:
|
else:
|
||||||
# Deduct credits for successful image generation
|
# Deduct credits for successful image generation
|
||||||
credits_deducted = 0
|
credits_deducted = 0
|
||||||
cost_usd = result.get('cost_usd', 0)
|
cost_usd = result.get('cost', 0) or result.get('cost_usd', 0) # generate_image returns 'cost'
|
||||||
if account:
|
if account:
|
||||||
try:
|
try:
|
||||||
credits_deducted = CreditService.deduct_credits_for_image(
|
credits_deducted = CreditService.deduct_credits_for_image(
|
||||||
@@ -803,6 +835,65 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
logger.info(f" - Failed: {failed}")
|
logger.info(f" - Failed: {failed}")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
# Log to AITaskLog for consistency with other AI functions
|
||||||
|
if account:
|
||||||
|
try:
|
||||||
|
from igny8_core.ai.models import AITaskLog
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Calculate total cost from results
|
||||||
|
total_cost = sum(r.get('cost', 0) for r in results if r.get('status') == 'completed')
|
||||||
|
|
||||||
|
AITaskLog.objects.create(
|
||||||
|
task_id=self.request.id,
|
||||||
|
function_name='generate_images',
|
||||||
|
account=account,
|
||||||
|
phase='DONE' if completed > 0 else 'ERROR',
|
||||||
|
message=f'Generated {completed} images ({failed} failed)' if completed > 0 else f'All {total_images} images failed',
|
||||||
|
status='success' if completed > 0 else 'error',
|
||||||
|
duration=0, # Could track actual duration if needed
|
||||||
|
cost=total_cost,
|
||||||
|
tokens=0, # Image generation doesn't use tokens
|
||||||
|
request_steps=[{
|
||||||
|
'phase': 'IMAGE_GENERATION',
|
||||||
|
'status': 'success' if completed > 0 else 'error',
|
||||||
|
'message': f'Processed {total_images} images: {completed} completed, {failed} failed'
|
||||||
|
}],
|
||||||
|
response_steps=[],
|
||||||
|
error=None if completed > 0 else f'All {total_images} images failed',
|
||||||
|
payload={'image_ids': image_ids, 'content_id': content_id},
|
||||||
|
result={
|
||||||
|
'total_images': total_images,
|
||||||
|
'completed': completed,
|
||||||
|
'failed': failed,
|
||||||
|
'model': model,
|
||||||
|
'provider': provider
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(f"[process_image_generation_queue] AITaskLog entry created")
|
||||||
|
except Exception as log_error:
|
||||||
|
logger.warning(f"[process_image_generation_queue] Failed to create AITaskLog: {log_error}")
|
||||||
|
|
||||||
|
# Create notification for image generation completion
|
||||||
|
if account:
|
||||||
|
try:
|
||||||
|
from igny8_core.business.notifications.services import NotificationService
|
||||||
|
|
||||||
|
if completed > 0:
|
||||||
|
NotificationService.notify_images_complete(
|
||||||
|
account=account,
|
||||||
|
image_count=completed
|
||||||
|
)
|
||||||
|
elif failed > 0:
|
||||||
|
NotificationService.notify_images_failed(
|
||||||
|
account=account,
|
||||||
|
error=f'{failed} images failed to generate',
|
||||||
|
image_count=failed
|
||||||
|
)
|
||||||
|
logger.info(f"[process_image_generation_queue] Notification created")
|
||||||
|
except Exception as notif_error:
|
||||||
|
logger.warning(f"[process_image_generation_queue] Failed to create notification: {notif_error}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'total_images': total_images,
|
'total_images': total_images,
|
||||||
|
|||||||
@@ -304,6 +304,36 @@ class CreditService:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_credits_for_image(account, model_name: str, num_images: int = 1):
|
||||||
|
"""
|
||||||
|
Check if account has sufficient credits for image generation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account: Account instance
|
||||||
|
model_name: AI model name (e.g., 'dall-e-3', 'runware:97@1')
|
||||||
|
num_images: Number of images to generate
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InsufficientCreditsError: If account doesn't have enough credits
|
||||||
|
CreditCalculationError: If model not found or has no credits_per_image
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Required credits for the operation
|
||||||
|
"""
|
||||||
|
required = CreditService.calculate_credits_for_image(model_name, num_images)
|
||||||
|
|
||||||
|
if account.credits < required:
|
||||||
|
raise InsufficientCreditsError(
|
||||||
|
f"Insufficient credits for image generation. Required: {required}, Available: {account.credits}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Credit check passed for image generation: "
|
||||||
|
f"{num_images} images with {model_name} = {required} credits (available: {account.credits})"
|
||||||
|
)
|
||||||
|
return required
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def deduct_credits(account, amount, operation_type, description, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
|
def deduct_credits(account, amount, operation_type, description, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Billing Module
|
# Billing Module
|
||||||
|
|
||||||
**Last Verified:** January 8, 2026
|
**Last Verified:** January 10, 2026
|
||||||
**Status:** ✅ Active (Payment System Refactored January 2026)
|
**Status:** ✅ Active (Image Generation Credit System Complete January 2026)
|
||||||
**Backend Path:** `backend/igny8_core/modules/billing/` + `backend/igny8_core/business/billing/`
|
**Backend Path:** `backend/igny8_core/modules/billing/` + `backend/igny8_core/business/billing/`
|
||||||
**Frontend Path:** `frontend/src/pages/Billing/` + `frontend/src/pages/Account/`
|
**Frontend Path:** `frontend/src/pages/Billing/` + `frontend/src/pages/Account/`
|
||||||
|
|
||||||
@@ -208,6 +208,36 @@ credits = CreditService.calculate_credits_for_image(
|
|||||||
# Returns: 15 (3 images × 5 credits)
|
# Returns: 15 (3 images × 5 credits)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Check Credits for Image Generation (NEW v1.7.1)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Pre-check credits before image generation starts
|
||||||
|
# Raises InsufficientCreditsError if not enough credits
|
||||||
|
required = CreditService.check_credits_for_image(
|
||||||
|
account=account,
|
||||||
|
model_name='dall-e-3', # Model with credits_per_image = 5
|
||||||
|
num_images=3
|
||||||
|
)
|
||||||
|
# Returns: 15 (required credits) if sufficient, raises exception if not
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deduct Credits for Image Generation (v1.7.1 Verified)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Called after each successful image generation
|
||||||
|
credits_deducted = CreditService.deduct_credits_for_image(
|
||||||
|
account=account,
|
||||||
|
model_name='dall-e-3',
|
||||||
|
num_images=1,
|
||||||
|
description='Image generation: Article Title',
|
||||||
|
metadata={'image_id': 123, 'content_id': 456},
|
||||||
|
cost_usd=0.04,
|
||||||
|
related_object_type='image',
|
||||||
|
related_object_id=123
|
||||||
|
)
|
||||||
|
# Logs to CreditTransaction and CreditUsageLog
|
||||||
|
```
|
||||||
|
|
||||||
### Calculate Credits from Tokens by Model (NEW v1.4.0)
|
### Calculate Credits from Tokens by Model (NEW v1.4.0)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -327,5 +357,7 @@ Displays:
|
|||||||
| Feature | Status | Description |
|
| Feature | Status | Description |
|
||||||
|---------|--------|-------------|
|
|---------|--------|-------------|
|
||||||
| ~~AI Model Config database~~ | ✅ v1.4.0 | Model pricing moved to AIModelConfig |
|
| ~~AI Model Config database~~ | ✅ v1.4.0 | Model pricing moved to AIModelConfig |
|
||||||
| Image model quality tiers | ✅ v1.4.0 | credits_per_image per quality tier |
|
| ~~Image model quality tiers~~ | ✅ v1.4.0 | credits_per_image per quality tier |
|
||||||
| Credit service enhancements | 🔄 v1.5.0 | Model-specific calculation methods |
|
| ~~Credit service enhancements~~ | ✅ v1.7.1 | Model-specific calculation methods |
|
||||||
|
| ~~Image generation credit check~~ | ✅ v1.7.1 | Pre-generation credit verification |
|
||||||
|
| ~~Image generation logging~~ | ✅ v1.7.1 | AITaskLog + notifications for images |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Credit System
|
# Credit System
|
||||||
|
|
||||||
**Last Verified:** January 5, 2026
|
**Last Verified:** January 10, 2026
|
||||||
**Status:** ✅ Simplified (v1.5.0)
|
**Status:** ✅ Complete (v1.7.1 - Image Generation Credits)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ Credits calculated from actual token usage:
|
|||||||
| Content Generation | gpt-4o | 1,000 |
|
| Content Generation | gpt-4o | 1,000 |
|
||||||
| Content Optimization | gpt-4o-mini | 10,000 |
|
| Content Optimization | gpt-4o-mini | 10,000 |
|
||||||
|
|
||||||
### Fixed-Cost Operations (Image AI)
|
### Fixed-Cost Operations (Image AI) - v1.7.1 Complete
|
||||||
|
|
||||||
Credits per image based on quality tier:
|
Credits per image based on quality tier:
|
||||||
|
|
||||||
@@ -103,6 +103,13 @@ Credits per image based on quality tier:
|
|||||||
| Quality | dall-e-3 | 5 |
|
| Quality | dall-e-3 | 5 |
|
||||||
| Premium | google:4@2 | 15 |
|
| Premium | google:4@2 | 15 |
|
||||||
|
|
||||||
|
**Image Generation Credit Flow (v1.7.1):**
|
||||||
|
1. Pre-check: `CreditService.check_credits_for_image()` verifies sufficient credits
|
||||||
|
2. Generation: Images generated via `process_image_generation_queue` Celery task
|
||||||
|
3. Post-deduct: `CreditService.deduct_credits_for_image()` called per successful image
|
||||||
|
4. Logging: `CreditUsageLog` + `CreditTransaction` + `AITaskLog` entries created
|
||||||
|
5. Notifications: `NotificationService.notify_images_complete/failed()` called
|
||||||
|
|
||||||
### Free Operations
|
### Free Operations
|
||||||
|
|
||||||
| Operation | Cost |
|
| Operation | Cost |
|
||||||
@@ -452,3 +459,58 @@ All AI operations logged in `CreditUsageLog` with:
|
|||||||
- Model used
|
- Model used
|
||||||
- Token counts
|
- Token counts
|
||||||
- Related object metadata
|
- Related object metadata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Generation Credit System (v1.7.1)
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `CreditService.check_credits_for_image()` - [credit_service.py:307-335](../backend/igny8_core/business/billing/services/credit_service.py#L307-L335)
|
||||||
|
- `process_image_generation_queue` credit check - [tasks.py:290-319](../backend/igny8_core/ai/tasks.py#L290-L319)
|
||||||
|
- `deduct_credits_for_image()` - [tasks.py:745-770](../backend/igny8_core/ai/tasks.py#L745-L770)
|
||||||
|
- AITaskLog logging - [tasks.py:838-875](../backend/igny8_core/ai/tasks.py#L838-L875)
|
||||||
|
- Notifications - [tasks.py:877-895](../backend/igny8_core/ai/tasks.py#L877-L895)
|
||||||
|
|
||||||
|
### Credit Flow for Image Generation
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User triggers image generation
|
||||||
|
↓
|
||||||
|
2. CreditService.check_credits_for_image(account, model, num_images)
|
||||||
|
- Calculates: credits_per_image × num_images
|
||||||
|
- Raises InsufficientCreditsError if balance < required
|
||||||
|
↓
|
||||||
|
3. process_image_generation_queue() processes each image
|
||||||
|
↓
|
||||||
|
4. For each successful image:
|
||||||
|
CreditService.deduct_credits_for_image()
|
||||||
|
- Creates CreditUsageLog entry
|
||||||
|
- Creates CreditTransaction entry
|
||||||
|
- Updates account.credits balance
|
||||||
|
↓
|
||||||
|
5. After all images processed:
|
||||||
|
- AITaskLog entry created
|
||||||
|
- Notification created (success or failure)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging Locations
|
||||||
|
|
||||||
|
| Table | What's Logged | When |
|
||||||
|
|-------|---------------|------|
|
||||||
|
| CreditTransaction | Credit deduction (financial ledger) | Per image |
|
||||||
|
| CreditUsageLog | Usage details (model, cost, credits) | Per image |
|
||||||
|
| AITaskLog | Task execution summary | After batch |
|
||||||
|
| Notification | User notification | After batch |
|
||||||
|
|
||||||
|
### Automation Compatibility
|
||||||
|
|
||||||
|
Image generation credits work identically for:
|
||||||
|
- Manual image generation (from UI)
|
||||||
|
- Automation Stage 6 (scheduled/manual automation runs)
|
||||||
|
|
||||||
|
Both call `process_image_generation_queue` which handles:
|
||||||
|
- Credit checking before generation
|
||||||
|
- Credit deduction after each successful image
|
||||||
|
- Proper logging to all tables
|
||||||
|
|||||||
Reference in New Issue
Block a user