many fixes of backeend and fronteend

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-06 16:41:35 +00:00
parent a0eee0df42
commit bfb07947ea
19 changed files with 638 additions and 19 deletions

View File

@@ -181,7 +181,26 @@ class AccountModelViewSet(viewsets.ModelViewSet):
"""
try:
instance = self.get_object()
self.perform_destroy(instance)
# Protect system account
if hasattr(instance, 'slug') and getattr(instance, 'slug', '') == 'aws-admin':
from django.core.exceptions import PermissionDenied
raise PermissionDenied("System account cannot be deleted.")
if hasattr(instance, 'soft_delete'):
user = getattr(request, 'user', None)
retention_days = None
account = getattr(instance, 'account', None)
if account and hasattr(account, 'deletion_retention_days'):
retention_days = account.deletion_retention_days
elif hasattr(instance, 'deletion_retention_days'):
retention_days = getattr(instance, 'deletion_retention_days', None)
instance.soft_delete(
user=user if getattr(user, 'is_authenticated', False) else None,
retention_days=retention_days,
reason='api_delete'
)
else:
self.perform_destroy(instance)
return success_response(
data=None,
message='Deleted successfully',

View File

@@ -56,6 +56,11 @@ class AccountAdmin(AccountAdminMixin, admin.ModelAdmin):
pass
return qs.none()
def has_delete_permission(self, request, obj=None):
if obj and getattr(obj, 'slug', '') == 'aws-admin':
return False
return super().has_delete_permission(request, obj)
@admin.register(Subscription)
class SubscriptionAdmin(AccountAdminMixin, admin.ModelAdmin):

View File

@@ -0,0 +1,42 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.business.planning.models import Clusters, Keywords, ContentIdeas
from igny8_core.business.content.models import Tasks, Content, Images
class Command(BaseCommand):
help = "Permanently delete soft-deleted records whose retention window has expired."
def handle(self, *args, **options):
now = timezone.now()
total_deleted = 0
models = [
Account,
Site,
Sector,
Clusters,
Keywords,
ContentIdeas,
Tasks,
Content,
Images,
]
for model in models:
qs = model.all_objects.filter(is_deleted=True, restore_until__lt=now)
if model is Account:
qs = qs.exclude(slug='aws-admin')
count = qs.count()
if count:
qs.delete()
total_deleted += count
self.stdout.write(self.style.SUCCESS(f"Purged {count} {model.__name__} record(s)."))
if total_deleted == 0:
self.stdout.write("No expired soft-deleted records to purge.")
else:
self.stdout.write(self.style.SUCCESS(f"Total purged: {total_deleted}"))

View File

@@ -0,0 +1,93 @@
from django.db import migrations, models
import django.db.models.deletion
from django.core.validators import MinValueValidator, MaxValueValidator
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0005_account_owner_nullable'),
]
operations = [
migrations.AddField(
model_name='account',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='account',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='account',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='account',
name='deletion_retention_days',
field=models.PositiveIntegerField(default=14, help_text='Retention window (days) before soft-deleted items are purged', validators=[MinValueValidator(1), MaxValueValidator(365)]),
),
migrations.AddField(
model_name='account',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='account',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='sector',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='sector',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='sector',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='sector',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='sector',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='site',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='site',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='site',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='site',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='site',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
]

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from igny8_core.common.soft_delete import SoftDeletableModel, SoftDeleteManager
class AccountBaseModel(models.Model):
@@ -52,7 +53,7 @@ class SiteSectorBaseModel(AccountBaseModel):
super().save(*args, **kwargs)
class Account(models.Model):
class Account(SoftDeletableModel):
"""
Account/Organization model for multi-account support.
"""
@@ -76,6 +77,11 @@ class Account(models.Model):
plan = models.ForeignKey('igny8_core_auth.Plan', on_delete=models.PROTECT, related_name='accounts')
credits = models.IntegerField(default=0, validators=[MinValueValidator(0)])
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='trial')
deletion_retention_days = models.PositiveIntegerField(
default=14,
validators=[MinValueValidator(1), MaxValueValidator(365)],
help_text="Retention window (days) before soft-deleted items are purged",
)
# Billing information
billing_email = models.EmailField(blank=True, null=True, help_text="Email for billing notifications")
@@ -99,6 +105,9 @@ class Account(models.Model):
models.Index(fields=['status']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def __str__(self):
return self.name
@@ -107,6 +116,15 @@ class Account(models.Model):
# System accounts bypass all filtering restrictions
return self.slug in ['aws-admin', 'default-account', 'default']
def soft_delete(self, user=None, reason=None, retention_days=None):
if self.is_system_account():
from django.core.exceptions import PermissionDenied
raise PermissionDenied("System account cannot be deleted.")
return super().soft_delete(user=user, reason=reason, retention_days=retention_days)
def delete(self, using=None, keep_parents=False):
return self.soft_delete()
class Plan(models.Model):
"""
@@ -202,7 +220,7 @@ class Subscription(models.Model):
class Site(AccountBaseModel):
class Site(SoftDeletableModel, AccountBaseModel):
"""
Site model - Each account can have multiple sites based on their plan.
Each site belongs to ONE industry and can have 1-5 sectors from that industry.
@@ -275,6 +293,9 @@ class Site(AccountBaseModel):
help_text="SEO metadata: meta tags, Open Graph, Schema.org"
)
objects = SoftDeleteManager()
all_objects = models.Manager()
class Meta:
db_table = 'igny8_sites'
unique_together = [['account', 'slug']] # Slug unique per account
@@ -409,7 +430,7 @@ class SeedKeyword(models.Model):
return f"{self.keyword} ({self.industry.name} - {self.sector.name})"
class Sector(AccountBaseModel):
class Sector(SoftDeletableModel, AccountBaseModel):
"""
Sector model - Each site can have 1-5 sectors.
Sectors are site-specific instances that reference an IndustrySector template.
@@ -437,6 +458,9 @@ class Sector(AccountBaseModel):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = SoftDeleteManager()
all_objects = models.Manager()
class Meta:
db_table = 'igny8_sectors'
unique_together = [['site', 'slug']] # Slug unique per site

View File

@@ -1,9 +1,10 @@
from django.db import models
from django.core.validators import MinValueValidator
from igny8_core.auth.models import SiteSectorBaseModel
from igny8_core.common.soft_delete import SoftDeletableModel, SoftDeleteManager
class Tasks(SiteSectorBaseModel):
class Tasks(SoftDeletableModel, SiteSectorBaseModel):
"""Tasks model for content generation queue"""
STATUS_CHOICES = [
@@ -116,6 +117,9 @@ class Tasks(SiteSectorBaseModel):
models.Index(fields=['site', 'sector']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def __str__(self):
return self.title
@@ -133,7 +137,7 @@ class ContentTaxonomyRelation(models.Model):
unique_together = [['content', 'taxonomy']]
class Content(SiteSectorBaseModel):
class Content(SoftDeletableModel, SiteSectorBaseModel):
"""
Content model for AI-generated or WordPress-imported content.
Final architecture: simplified content management.
@@ -267,6 +271,9 @@ class Content(SiteSectorBaseModel):
models.Index(fields=['site', 'sector']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def __str__(self):
return self.title or f"Content {self.id}"
@@ -350,7 +357,7 @@ class ContentTaxonomy(SiteSectorBaseModel):
return f"{self.name} ({self.get_taxonomy_type_display()})"
class Images(SiteSectorBaseModel):
class Images(SoftDeletableModel, SiteSectorBaseModel):
"""Images model for content-related images (featured, desktop, mobile, in-article)"""
IMAGE_TYPE_CHOICES = [
@@ -399,6 +406,9 @@ class Images(SiteSectorBaseModel):
models.Index(fields=['task', 'position']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def save(self, *args, **kwargs):
"""Automatically set account, site, and sector from content or task"""
# Prefer content over task

View File

@@ -1,8 +1,9 @@
from django.db import models
from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword
from igny8_core.common.soft_delete import SoftDeletableModel, SoftDeleteManager
class Clusters(SiteSectorBaseModel):
class Clusters(SoftDeletableModel, SiteSectorBaseModel):
"""Clusters model for keyword grouping - pure topic clusters"""
STATUS_CHOICES = [
@@ -33,11 +34,14 @@ class Clusters(SiteSectorBaseModel):
models.Index(fields=['site', 'sector']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def __str__(self):
return self.name
class Keywords(SiteSectorBaseModel):
class Keywords(SoftDeletableModel, SiteSectorBaseModel):
"""
Keywords model for SEO keyword management.
Site-specific instances that reference global SeedKeywords.
@@ -101,6 +105,9 @@ class Keywords(SiteSectorBaseModel):
models.Index(fields=['seed_keyword', 'site', 'sector']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
@property
def keyword(self):
"""Get keyword text from seed_keyword"""
@@ -144,7 +151,7 @@ class Keywords(SiteSectorBaseModel):
return self.keyword
class ContentIdeas(SiteSectorBaseModel):
class ContentIdeas(SoftDeletableModel, SiteSectorBaseModel):
"""Content Ideas model for planning content based on keyword clusters"""
STATUS_CHOICES = [
@@ -232,6 +239,9 @@ class ContentIdeas(SiteSectorBaseModel):
models.Index(fields=['site', 'sector']),
]
objects = SoftDeleteManager()
all_objects = models.Manager()
def __str__(self):
return self.idea_title

View File

@@ -43,6 +43,11 @@ app.conf.beat_schedule = {
'task': 'automation.check_scheduled_automations',
'schedule': crontab(minute=0), # Every hour at :00
},
# Maintenance: purge expired soft-deleted records daily at 3:15 AM
'purge-soft-deleted-records': {
'task': 'igny8_core.purge_soft_deleted',
'schedule': crontab(hour=3, minute=15),
},
}
@app.task(bind=True, ignore_result=True)

View File

@@ -0,0 +1,95 @@
from django.db import models
from django.utils import timezone
from django.conf import settings
class SoftDeleteQuerySet(models.QuerySet):
"""QuerySet that filters out soft-deleted rows by default."""
def delete(self):
# Prevent accidental hard deletes through queryset.delete()
for obj in self:
obj.delete()
def hard_delete(self):
return super().delete()
def with_deleted(self):
"""Return all rows, including soft-deleted."""
return super().all()
class SoftDeleteManager(models.Manager):
"""Manager that hides soft-deleted rows by default."""
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=False)
def with_deleted(self):
return SoftDeleteQuerySet(self.model, using=self._db)
class SoftDeletableModel(models.Model):
"""
Abstract mixin for soft-deletion with retention window.
Objects are hidden by default via SoftDeleteManager.
"""
is_deleted = models.BooleanField(default=False, db_index=True)
deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)
restore_until = models.DateTimeField(null=True, blank=True, db_index=True)
delete_reason = models.CharField(max_length=255, null=True, blank=True)
deleted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+',
)
objects = SoftDeleteManager()
all_objects = models.Manager()
DEFAULT_RETENTION_DAYS = 14
class Meta:
abstract = True
def soft_delete(self, user=None, reason=None, retention_days=None):
"""Mark the instance as deleted with a retention window."""
if self.is_deleted:
return
now = timezone.now()
if retention_days is None:
retention_days = getattr(
getattr(self, 'account', None),
'deletion_retention_days',
self.DEFAULT_RETENTION_DAYS,
)
self.is_deleted = True
self.deleted_at = now
self.restore_until = now + timezone.timedelta(days=retention_days)
self.deleted_by = user
if reason:
self.delete_reason = reason
self.save(update_fields=['is_deleted', 'deleted_at', 'restore_until', 'deleted_by', 'delete_reason'])
def restore(self):
"""Restore a soft-deleted instance if within the retention window."""
if not self.is_deleted:
return
self.is_deleted = False
self.deleted_at = None
self.restore_until = None
self.delete_reason = None
self.deleted_by = None
self.save(update_fields=['is_deleted', 'deleted_at', 'restore_until', 'delete_reason', 'deleted_by'])
def delete(self, using=None, keep_parents=False):
"""Override delete to perform a soft delete."""
self.soft_delete()
def hard_delete(self, using=None, keep_parents=False):
"""Irrevocably delete the row."""
return super().delete(using=using, keep_parents=keep_parents)

View File

@@ -0,0 +1,2 @@
# Package marker for Django management commands

View File

@@ -0,0 +1,2 @@
# Package marker for Django management commands

View File

@@ -0,0 +1,42 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.business.planning.models import Clusters, Keywords, ContentIdeas
from igny8_core.business.content.models import Tasks, Content, Images
class Command(BaseCommand):
help = "Permanently delete soft-deleted records whose retention window has expired."
def handle(self, *args, **options):
now = timezone.now()
total_deleted = 0
models = [
Account,
Site,
Sector,
Clusters,
Keywords,
ContentIdeas,
Tasks,
Content,
Images,
]
for model in models:
qs = model.all_objects.filter(is_deleted=True, restore_until__lt=now)
if model is Account:
qs = qs.exclude(slug='aws-admin')
count = qs.count()
if count:
qs.delete()
total_deleted += count
self.stdout.write(self.style.SUCCESS(f"Purged {count} {model.__name__} record(s)."))
if total_deleted == 0:
self.stdout.write("No expired soft-deleted records to purge.")
else:
self.stdout.write(self.style.SUCCESS(f"Total purged: {total_deleted}"))

View File

@@ -2,7 +2,9 @@
Billing Module Admin
"""
from django.contrib import admin
from django.utils.html import format_html
from igny8_core.admin.base import AccountAdminMixin
from igny8_core.business.billing.models import CreditCostConfig
from .models import CreditTransaction, CreditUsageLog, AccountPaymentMethod
@@ -70,3 +72,78 @@ class AccountPaymentMethodAdmin(AccountAdminMixin, admin.ModelAdmin):
}),
)
@admin.register(CreditCostConfig)
class CreditCostConfigAdmin(admin.ModelAdmin):
list_display = [
'operation_type',
'display_name',
'credits_cost_display',
'unit',
'is_active',
'cost_change_indicator',
'updated_at',
'updated_by'
]
list_filter = ['is_active', 'unit', 'updated_at']
search_fields = ['operation_type', 'display_name', 'description']
fieldsets = (
('Operation', {
'fields': ('operation_type', 'display_name', 'description')
}),
('Cost Configuration', {
'fields': ('credits_cost', 'unit', 'is_active')
}),
('Audit Trail', {
'fields': ('previous_cost', 'updated_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at', 'previous_cost']
def credits_cost_display(self, obj):
"""Show cost with color coding"""
if obj.credits_cost >= 20:
color = 'red'
elif obj.credits_cost >= 10:
color = 'orange'
else:
color = 'green'
return format_html(
'<span style="color: {}; font-weight: bold;">{} credits</span>',
color,
obj.credits_cost
)
credits_cost_display.short_description = 'Cost'
def cost_change_indicator(self, obj):
"""Show if cost changed recently"""
if obj.previous_cost is not None:
if obj.credits_cost > obj.previous_cost:
icon = '📈' # Increased
color = 'red'
elif obj.credits_cost < obj.previous_cost:
icon = '📉' # Decreased
color = 'green'
else:
icon = '➡️' # Same
color = 'gray'
return format_html(
'{} <span style="color: {};">({}{})</span>',
icon,
color,
obj.previous_cost,
obj.credits_cost
)
return ''
cost_change_indicator.short_description = 'Recent Change'
def save_model(self, request, obj, form, change):
"""Track who made the change"""
obj.updated_by = request.user
super().save_model(request, obj, form, change)

View File

@@ -4,6 +4,6 @@ from django.apps import AppConfig
class BillingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'igny8_core.modules.billing'
verbose_name = 'Billing'
verbose_name = 'Billing & Tenancy'

View File

@@ -0,0 +1,88 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('planner', '0007_fix_cluster_unique_constraint'),
('igny8_core_auth', '0006_soft_delete_and_retention'),
]
operations = [
migrations.AddField(
model_name='clusters',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='clusters',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='clusters',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='clusters',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='clusters',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='contentideas',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='contentideas',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='contentideas',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='contentideas',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='contentideas',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='keywords',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='keywords',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='keywords',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='keywords',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='keywords',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
]

View File

@@ -0,0 +1,88 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('writer', '0011_content_external_metadata_content_external_type_and_more'),
('igny8_core_auth', '0006_soft_delete_and_retention'),
]
operations = [
migrations.AddField(
model_name='content',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='content',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='content',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='content',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='content',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='images',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='images',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='images',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='images',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='images',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='tasks',
name='delete_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='tasks',
name='deleted_at',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='tasks',
name='deleted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='igny8_core_auth.user'),
),
migrations.AddField(
model_name='tasks',
name='is_deleted',
field=models.BooleanField(db_index=True, default=False),
),
migrations.AddField(
model_name='tasks',
name='restore_until',
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
import logging
from celery import shared_task
from django.core.management import call_command
logger = logging.getLogger(__name__)
@shared_task(name='igny8_core.purge_soft_deleted')
def purge_soft_deleted_task():
"""Periodic task to purge expired soft-deleted records."""
try:
call_command('purge_soft_deleted')
except Exception as exc:
logger.exception("purge_soft_deleted task failed: %s", exc)
raise

View File

@@ -1,5 +1,5 @@
import { Suspense, lazy, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import { Routes, Route, Navigate } from "react-router-dom";
import { HelmetProvider } from "react-helmet-async";
import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop";
@@ -176,10 +176,9 @@ export default function App() {
<>
<GlobalErrorDisplay />
<LoadingStateMonitor />
<Router>
<HelmetProvider>
<ScrollToTop />
<Routes>
<HelmetProvider>
<ScrollToTop />
<Routes>
{/* Auth Routes - Public */}
<Route path="/signin" element={<SignIn />} />
<Route path="/signup" element={<SignUp />} />
@@ -832,8 +831,7 @@ export default function App() {
{/* Fallback Route */}
<Route path="*" element={<NotFound />} />
</Routes>
</HelmetProvider>
</Router>
</HelmetProvider>
</>
);
}

View File

@@ -9,6 +9,7 @@ import { ThemeProvider } from "./context/ThemeContext";
import { ToastProvider } from "./components/ui/toast/ToastContainer";
import { HeaderMetricsProvider } from "./context/HeaderMetricsContext";
import { ErrorBoundary } from "./components/common/ErrorBoundary";
import { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -16,7 +17,9 @@ createRoot(document.getElementById("root")!).render(
<ThemeProvider>
<HeaderMetricsProvider>
<ToastProvider>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</ToastProvider>
</HeaderMetricsProvider>
</ThemeProvider>