django-cfg 1.2.16__py3-none-any.whl → 1.2.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/models/__init__.py +68 -0
  3. django_cfg/apps/accounts/models/activity.py +34 -0
  4. django_cfg/apps/accounts/models/auth.py +50 -0
  5. django_cfg/apps/accounts/models/base.py +8 -0
  6. django_cfg/apps/accounts/models/choices.py +32 -0
  7. django_cfg/apps/accounts/models/integrations.py +75 -0
  8. django_cfg/apps/accounts/models/registration.py +52 -0
  9. django_cfg/apps/accounts/models/user.py +80 -0
  10. django_cfg/apps/maintenance/__init__.py +53 -24
  11. django_cfg/apps/maintenance/admin/__init__.py +7 -18
  12. django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
  13. django_cfg/apps/maintenance/admin/log_admin.py +156 -0
  14. django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
  15. django_cfg/apps/maintenance/admin/site_admin.py +448 -0
  16. django_cfg/apps/maintenance/apps.py +9 -96
  17. django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
  18. django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
  19. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
  20. django_cfg/apps/maintenance/managers/__init__.py +7 -12
  21. django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
  22. django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
  23. django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
  24. django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
  25. django_cfg/apps/maintenance/models/__init__.py +23 -21
  26. django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
  27. django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
  28. django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
  29. django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
  30. django_cfg/apps/maintenance/services/__init__.py +37 -16
  31. django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
  32. django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
  33. django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
  34. django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
  35. django_cfg/apps/maintenance/utils/__init__.py +12 -0
  36. django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
  37. django_cfg/config.py +3 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/RECORD +45 -55
  42. django_cfg/apps/maintenance/README.md +0 -305
  43. django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
  44. django_cfg/apps/maintenance/admin/events_admin.py +0 -374
  45. django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
  46. django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
  47. django_cfg/apps/maintenance/managers/deployments.py +0 -287
  48. django_cfg/apps/maintenance/managers/events.py +0 -374
  49. django_cfg/apps/maintenance/managers/monitoring.py +0 -301
  50. django_cfg/apps/maintenance/managers/sites.py +0 -335
  51. django_cfg/apps/maintenance/models/cloudflare.py +0 -316
  52. django_cfg/apps/maintenance/models/maintenance.py +0 -334
  53. django_cfg/apps/maintenance/models/monitoring.py +0 -393
  54. django_cfg/apps/maintenance/models/sites.py +0 -419
  55. django_cfg/apps/maintenance/serializers/__init__.py +0 -60
  56. django_cfg/apps/maintenance/serializers/actions.py +0 -310
  57. django_cfg/apps/maintenance/serializers/base.py +0 -44
  58. django_cfg/apps/maintenance/serializers/deployments.py +0 -209
  59. django_cfg/apps/maintenance/serializers/events.py +0 -210
  60. django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
  61. django_cfg/apps/maintenance/serializers/sites.py +0 -213
  62. django_cfg/apps/maintenance/services/README.md +0 -168
  63. django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
  64. django_cfg/apps/maintenance/services/dns_manager.py +0 -497
  65. django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
  66. django_cfg/apps/maintenance/services/site_sync.py +0 -448
  67. django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
  68. django_cfg/apps/maintenance/services/worker_manager.py +0 -264
  69. django_cfg/apps/maintenance/signals.py +0 -38
  70. django_cfg/apps/maintenance/urls.py +0 -36
  71. django_cfg/apps/maintenance/views/__init__.py +0 -18
  72. django_cfg/apps/maintenance/views/base.py +0 -61
  73. django_cfg/apps/maintenance/views/deployments.py +0 -175
  74. django_cfg/apps/maintenance/views/events.py +0 -204
  75. django_cfg/apps/maintenance/views/monitoring.py +0 -213
  76. django_cfg/apps/maintenance/views/sites.py +0 -338
  77. django_cfg/models/cloudflare.py +0 -316
  78. /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
  79. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,390 @@
1
+ """
2
+ Site synchronization service for automatic Cloudflare zone discovery.
3
+
4
+ Automatically discovers and syncs Cloudflare zones with Django models.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, List, Any, Optional
9
+ from datetime import datetime
10
+ from django.utils import timezone
11
+ from django.db import transaction
12
+
13
+ from ..models import CloudflareApiKey, CloudflareSite, MaintenanceLog
14
+ from .maintenance_service import MaintenanceService
15
+ from ..utils.retry_utils import retry_on_failure, CloudflareRetryError
16
+
17
+ try:
18
+ from cloudflare import Cloudflare
19
+ except ImportError:
20
+ raise ImportError("cloudflare library is required. Install with: pip install cloudflare>=4.3.0")
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class SiteSyncService:
26
+ """
27
+ Service for synchronizing CloudflareSite models with actual Cloudflare zones.
28
+
29
+ Provides automatic discovery and sync of Cloudflare zones.
30
+ """
31
+
32
+ def __init__(self, api_key: CloudflareApiKey):
33
+ """Initialize sync service with API key."""
34
+ self.api_key = api_key
35
+ self.client = Cloudflare(api_token=api_key.api_token)
36
+
37
+ @retry_on_failure(max_retries=3)
38
+ def discover_zones(self) -> List[Dict[str, Any]]:
39
+ """
40
+ Discover all zones in the Cloudflare account.
41
+
42
+ Returns:
43
+ List of zone data dictionaries
44
+ """
45
+ logger.info(f"Discovering zones for API key: {self.api_key.name}")
46
+
47
+ try:
48
+ zones = []
49
+ for zone in self.client.zones.list():
50
+ zone_data = {
51
+ 'id': zone.id,
52
+ 'name': zone.name,
53
+ 'account_id': zone.account.id if zone.account else None,
54
+ 'status': zone.status,
55
+ 'paused': zone.paused,
56
+ 'type': zone.type,
57
+ 'development_mode': zone.development_mode,
58
+ 'name_servers': zone.name_servers,
59
+ 'created_on': zone.created_on,
60
+ 'modified_on': zone.modified_on,
61
+ }
62
+ zones.append(zone_data)
63
+
64
+ logger.info(f"Discovered {len(zones)} zones")
65
+ return zones
66
+
67
+ except Exception as e:
68
+ logger.error(f"Failed to discover zones: {e}")
69
+ raise CloudflareRetryError(f"Zone discovery failed: {e}")
70
+
71
+ def sync_zones(self,
72
+ force_update: bool = False,
73
+ dry_run: bool = False) -> Dict[str, Any]:
74
+ """
75
+ Sync discovered zones with Django models.
76
+
77
+ Args:
78
+ force_update: Update existing sites even if they haven't changed
79
+ dry_run: Only show what would be changed without making changes
80
+
81
+ Returns:
82
+ Dict with sync statistics and results
83
+ """
84
+ logger.info(f"Starting zone sync (dry_run={dry_run}, force_update={force_update})")
85
+
86
+ stats = {
87
+ 'discovered': 0,
88
+ 'created': 0,
89
+ 'updated': 0,
90
+ 'skipped': 0,
91
+ 'errors': 0,
92
+ 'sites': []
93
+ }
94
+
95
+ try:
96
+ # Discover zones from Cloudflare
97
+ cf_zones = self.discover_zones()
98
+ stats['discovered'] = len(cf_zones)
99
+
100
+ # Get existing sites for this API key
101
+ existing_sites = {
102
+ site.zone_id: site
103
+ for site in CloudflareSite.objects.filter(api_key=self.api_key)
104
+ }
105
+
106
+ # Process each discovered zone
107
+ for zone_data in cf_zones:
108
+ try:
109
+ result = self._sync_single_zone(
110
+ zone_data=zone_data,
111
+ existing_sites=existing_sites,
112
+ force_update=force_update,
113
+ dry_run=dry_run
114
+ )
115
+
116
+ stats[result['action']] += 1
117
+ stats['sites'].append(result)
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error syncing zone {zone_data['name']}: {e}")
121
+ stats['errors'] += 1
122
+ stats['sites'].append({
123
+ 'domain': zone_data['name'],
124
+ 'action': 'error',
125
+ 'error': str(e)
126
+ })
127
+
128
+ # Mark API key as used
129
+ self.api_key.mark_used()
130
+
131
+ logger.info(f"Zone sync completed: {stats}")
132
+ return stats
133
+
134
+ except Exception as e:
135
+ logger.error(f"Zone sync failed: {e}")
136
+ stats['errors'] += 1
137
+ return stats
138
+
139
+ def _sync_single_zone(self,
140
+ zone_data: Dict[str, Any],
141
+ existing_sites: Dict[str, CloudflareSite],
142
+ force_update: bool,
143
+ dry_run: bool) -> Dict[str, Any]:
144
+ """Sync a single zone with Django model."""
145
+ zone_id = zone_data['id']
146
+ domain = zone_data['name']
147
+
148
+ if zone_id in existing_sites:
149
+ # Site exists - check if update needed
150
+ site = existing_sites[zone_id]
151
+
152
+ needs_update = (
153
+ force_update or
154
+ site.domain != domain or
155
+ site.account_id != zone_data.get('account_id')
156
+ )
157
+
158
+ if needs_update:
159
+ if dry_run:
160
+ return {
161
+ 'domain': domain,
162
+ 'action': 'would_update',
163
+ 'changes': self._get_site_changes(site, zone_data)
164
+ }
165
+ else:
166
+ # Update existing site
167
+ site.domain = domain
168
+ site.account_id = zone_data.get('account_id', site.account_id)
169
+ site.save()
170
+
171
+ # Log the sync
172
+ MaintenanceLog.log_success(
173
+ site=site,
174
+ action=MaintenanceLog.Action.SYNC,
175
+ reason="Automatic zone sync - updated existing site"
176
+ )
177
+
178
+ return {
179
+ 'domain': domain,
180
+ 'action': 'updated',
181
+ 'site_id': site.id
182
+ }
183
+ else:
184
+ return {
185
+ 'domain': domain,
186
+ 'action': 'skipped',
187
+ 'reason': 'No changes needed'
188
+ }
189
+ else:
190
+ # New site - create it
191
+ if dry_run:
192
+ return {
193
+ 'domain': domain,
194
+ 'action': 'would_create',
195
+ 'zone_data': zone_data
196
+ }
197
+ else:
198
+ # Create new site
199
+ site = CloudflareSite.objects.create(
200
+ name=self._generate_site_name(domain),
201
+ domain=domain,
202
+ zone_id=zone_id,
203
+ account_id=zone_data.get('account_id', ''),
204
+ api_key=self.api_key,
205
+ is_active=not zone_data.get('paused', False)
206
+ )
207
+
208
+ # Log the creation
209
+ MaintenanceLog.log_success(
210
+ site=site,
211
+ action=MaintenanceLog.Action.SYNC,
212
+ reason="Automatic zone sync - created new site"
213
+ )
214
+
215
+ return {
216
+ 'domain': domain,
217
+ 'action': 'created',
218
+ 'site_id': site.id
219
+ }
220
+
221
+ def _get_site_changes(self, site: CloudflareSite, zone_data: Dict[str, Any]) -> Dict[str, Any]:
222
+ """Get changes that would be made to a site."""
223
+ changes = {}
224
+
225
+ if site.domain != zone_data['name']:
226
+ changes['domain'] = {
227
+ 'old': site.domain,
228
+ 'new': zone_data['name']
229
+ }
230
+
231
+ if site.account_id != zone_data.get('account_id'):
232
+ changes['account_id'] = {
233
+ 'old': site.account_id,
234
+ 'new': zone_data.get('account_id')
235
+ }
236
+
237
+ return changes
238
+
239
+ def _generate_site_name(self, domain: str) -> str:
240
+ """Generate a friendly site name from domain."""
241
+ # Remove common prefixes and TLD for cleaner name
242
+ name = domain.replace('www.', '').replace('api.', '').replace('app.', '')
243
+
244
+ # Capitalize first letter
245
+ if '.' in name:
246
+ name = name.split('.')[0].capitalize()
247
+ else:
248
+ name = name.capitalize()
249
+
250
+ return name
251
+
252
+ def check_site_status(self, site: CloudflareSite) -> Dict[str, Any]:
253
+ """
254
+ Check current status of a site in Cloudflare.
255
+
256
+ Args:
257
+ site: CloudflareSite to check
258
+
259
+ Returns:
260
+ Dict with status information
261
+ """
262
+ try:
263
+ # Get zone info
264
+ zone = self.client.zones.get(zone_id=site.zone_id)
265
+
266
+ # Check if maintenance worker is active
267
+ maintenance_active = self._check_maintenance_worker(site)
268
+
269
+ status_info = {
270
+ 'zone_status': zone.status,
271
+ 'zone_paused': zone.paused,
272
+ 'development_mode': zone.development_mode,
273
+ 'maintenance_active': maintenance_active,
274
+ 'last_checked': timezone.now().isoformat()
275
+ }
276
+
277
+ # Update site status
278
+ site.maintenance_active = maintenance_active
279
+ site.is_active = not zone.paused
280
+ site.save()
281
+
282
+ return status_info
283
+
284
+ except Exception as e:
285
+ logger.error(f"Failed to check status for {site.domain}: {e}")
286
+ return {
287
+ 'error': str(e),
288
+ 'last_checked': timezone.now().isoformat()
289
+ }
290
+
291
+ @retry_on_failure(max_retries=2)
292
+ def _check_maintenance_worker(self, site: CloudflareSite) -> bool:
293
+ """Check if maintenance worker is active for site."""
294
+ try:
295
+ # List worker routes for the zone
296
+ routes = self.client.workers.routes.list(zone_id=site.zone_id)
297
+
298
+ # Check if any route matches our maintenance worker pattern
299
+ maintenance_service = MaintenanceService(site)
300
+ worker_name = maintenance_service.worker_name
301
+
302
+ for route in routes.result:
303
+ if hasattr(route, 'script') and route.script == worker_name:
304
+ return True
305
+
306
+ return False
307
+
308
+ except Exception as e:
309
+ logger.warning(f"Could not check maintenance worker for {site.domain}: {e}")
310
+ return False
311
+
312
+ def bulk_sync_all_api_keys(self) -> Dict[str, Any]:
313
+ """
314
+ Sync zones for all active API keys.
315
+
316
+ Returns:
317
+ Dict with overall sync results
318
+ """
319
+ logger.info("Starting bulk sync for all API keys")
320
+
321
+ overall_stats = {
322
+ 'api_keys_processed': 0,
323
+ 'total_discovered': 0,
324
+ 'total_created': 0,
325
+ 'total_updated': 0,
326
+ 'total_skipped': 0,
327
+ 'total_errors': 0,
328
+ 'api_key_results': []
329
+ }
330
+
331
+ active_api_keys = CloudflareApiKey.objects.filter(is_active=True)
332
+
333
+ for api_key in active_api_keys:
334
+ try:
335
+ sync_service = SiteSyncService(api_key)
336
+ result = sync_service.sync_zones()
337
+
338
+ overall_stats['api_keys_processed'] += 1
339
+ overall_stats['total_discovered'] += result['discovered']
340
+ overall_stats['total_created'] += result['created']
341
+ overall_stats['total_updated'] += result['updated']
342
+ overall_stats['total_skipped'] += result['skipped']
343
+ overall_stats['total_errors'] += result['errors']
344
+
345
+ overall_stats['api_key_results'].append({
346
+ 'api_key_name': api_key.name,
347
+ 'success': True,
348
+ 'stats': result
349
+ })
350
+
351
+ except Exception as e:
352
+ logger.error(f"Failed to sync API key {api_key.name}: {e}")
353
+ overall_stats['total_errors'] += 1
354
+ overall_stats['api_key_results'].append({
355
+ 'api_key_name': api_key.name,
356
+ 'success': False,
357
+ 'error': str(e)
358
+ })
359
+
360
+ logger.info(f"Bulk sync completed: {overall_stats}")
361
+ return overall_stats
362
+
363
+
364
+ def sync_site_from_cloudflare(site: CloudflareSite) -> MaintenanceLog:
365
+ """
366
+ Convenience function to sync a single site from Cloudflare.
367
+
368
+ Args:
369
+ site: CloudflareSite to sync
370
+
371
+ Returns:
372
+ MaintenanceLog entry for the sync operation
373
+ """
374
+ try:
375
+ sync_service = SiteSyncService(site.api_key)
376
+ status_info = sync_service.check_site_status(site)
377
+
378
+ return MaintenanceLog.log_success(
379
+ site=site,
380
+ action=MaintenanceLog.Action.SYNC,
381
+ reason="Manual site sync",
382
+ cloudflare_response=status_info
383
+ )
384
+
385
+ except Exception as e:
386
+ return MaintenanceLog.log_failure(
387
+ site=site,
388
+ action=MaintenanceLog.Action.SYNC,
389
+ error_message=str(e)
390
+ )
@@ -0,0 +1,12 @@
1
+ """
2
+ Maintenance utilities.
3
+
4
+ Helper functions and classes for maintenance operations.
5
+ """
6
+
7
+ from .retry_utils import retry_on_failure, CloudflareRetryError
8
+
9
+ __all__ = [
10
+ 'retry_on_failure',
11
+ 'CloudflareRetryError',
12
+ ]
@@ -0,0 +1,109 @@
1
+ """
2
+ Retry utilities for Cloudflare operations.
3
+
4
+ Simple retry logic extracted from the complex old system.
5
+ """
6
+
7
+ import time
8
+ import random
9
+ import logging
10
+ from typing import Callable, Any, List
11
+ from functools import wraps
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CloudflareRetryError(Exception):
17
+ """Exception raised when all retry attempts fail."""
18
+ pass
19
+
20
+
21
+ def retry_on_failure(
22
+ max_retries: int = 3,
23
+ base_delay: float = 1.0,
24
+ max_delay: float = 30.0,
25
+ backoff_factor: float = 2.0,
26
+ retry_on_status: List[int] = None,
27
+ jitter: bool = True
28
+ ):
29
+ """
30
+ Decorator for retrying Cloudflare operations with exponential backoff.
31
+
32
+ Args:
33
+ max_retries: Maximum number of retry attempts
34
+ base_delay: Initial delay between retries in seconds
35
+ max_delay: Maximum delay between retries in seconds
36
+ backoff_factor: Factor to multiply delay by after each retry
37
+ retry_on_status: HTTP status codes to retry on (deprecated, kept for compatibility)
38
+ jitter: Whether to add random jitter to delays
39
+ """
40
+ if retry_on_status is None:
41
+ retry_on_status = [429, 502, 503, 504]
42
+
43
+ def decorator(func: Callable) -> Callable:
44
+ @wraps(func)
45
+ def wrapper(*args, **kwargs) -> Any:
46
+ last_exception = None
47
+
48
+ for attempt in range(max_retries + 1):
49
+ try:
50
+ return func(*args, **kwargs)
51
+
52
+ except Exception as e:
53
+ last_exception = e
54
+
55
+ # Don't retry on the last attempt
56
+ if attempt == max_retries:
57
+ break
58
+
59
+ # Check if we should retry based on the error
60
+ should_retry = True # Default to retry
61
+
62
+ # Check for HTTP status codes in the error message
63
+ error_str = str(e).lower()
64
+
65
+ # Check if any custom retry status codes are in the error
66
+ has_custom_status = False
67
+ for status in retry_on_status:
68
+ if str(status) in error_str:
69
+ has_custom_status = True
70
+ break
71
+
72
+ # If we have custom status codes and found one, always retry
73
+ if has_custom_status:
74
+ should_retry = True
75
+ else:
76
+ # Don't retry on certain non-retryable errors (only if no custom status found)
77
+ if any(keyword in error_str for keyword in [
78
+ 'authentication', 'unauthorized', 'forbidden', 'not found',
79
+ 'invalid', 'malformed'
80
+ ]):
81
+ should_retry = False
82
+
83
+ if not should_retry:
84
+ # Don't retry on non-retryable errors
85
+ break
86
+
87
+ # Calculate delay with exponential backoff and optional jitter
88
+ delay = min(base_delay * (backoff_factor ** attempt), max_delay)
89
+ if jitter:
90
+ jitter_amount = random.uniform(0.1, 0.3) * delay
91
+ total_delay = delay + jitter_amount
92
+ else:
93
+ total_delay = delay
94
+
95
+ logger.warning(
96
+ f"Attempt {attempt + 1}/{max_retries + 1} failed for {func.__name__}: {e}. "
97
+ f"Retrying in {total_delay:.2f}s..."
98
+ )
99
+
100
+ time.sleep(total_delay)
101
+
102
+ # All retries failed
103
+ raise CloudflareRetryError(
104
+ f"All {max_retries + 1} attempts failed for {func.__name__}. "
105
+ f"Last error: {last_exception}"
106
+ ) from last_exception
107
+
108
+ return wrapper
109
+ return decorator
django_cfg/config.py CHANGED
@@ -17,6 +17,9 @@ LIB_GITHUB_URL = "https://github.com/django-cfg/django-cfg"
17
17
  LIB_SUPPORT_URL = "https://djangocfg.com/support"
18
18
  LIB_HEALTH_URL = "/cfg/health/"
19
19
 
20
+ def get_maintenance_url(domain: str) -> str:
21
+ """Get the maintenance URL for the current site."""
22
+ return f"{LIB_SITE_URL}/maintenance/{domain}/"
20
23
 
21
24
  def get_default_dropdown_items() -> List[SiteDropdownItem]:
22
25
  """Get default dropdown menu items for Unfold admin."""
django_cfg/core/config.py CHANGED
@@ -154,6 +154,10 @@ class DjangoConfig(BaseModel):
154
154
  default=False,
155
155
  description="Enable django-cfg AI Agents application (agent definitions, executions, workflows, tools)",
156
156
  )
157
+ enable_maintenance: bool = Field(
158
+ default=False,
159
+ description="Enable django-cfg Maintenance application (multi-site maintenance mode with Cloudflare)",
160
+ )
157
161
 
158
162
  # === URLs ===
159
163
  site_url: str = Field(default="http://localhost:3000", description="Frontend site URL")
@@ -270,12 +274,6 @@ class DjangoConfig(BaseModel):
270
274
  description="Application limits configuration (file uploads, requests, etc.)",
271
275
  )
272
276
 
273
- # === Maintenance Mode Configuration ===
274
- enable_maintenance: bool = Field(
275
- default=False,
276
- description="Enable django-cfg Maintenance application (multi-site maintenance mode with Cloudflare)",
277
- )
278
-
279
277
  # === Middleware Configuration ===
280
278
  custom_middleware: List[str] = Field(
281
279
  default_factory=list,
@@ -156,11 +156,10 @@ class DashboardManager(BaseCfgModule):
156
156
  separator=True,
157
157
  collapsible=True,
158
158
  items=[
159
- NavigationItem(title="Cloudflare Sites", icon=Icons.CLOUD, link="/admin/django_cfg_maintenance/cloudflaresite/"),
160
- NavigationItem(title="Site Groups", icon=Icons.GROUP, link="/admin/django_cfg_maintenance/sitegroup/"),
161
- NavigationItem(title="Maintenance Events", icon=Icons.BUILD, link="/admin/django_cfg_maintenance/maintenanceevent/"),
162
- NavigationItem(title="Monitoring Targets", icon=Icons.MONITOR, link="/admin/django_cfg_maintenance/monitoringtarget/"),
163
- NavigationItem(title="Deployments", icon=Icons.ROCKET_LAUNCH, link="/admin/django_cfg_maintenance/cloudflaredeployment/"),
159
+ NavigationItem(title="Cloudflare Sites", icon=Icons.CLOUD, link="/admin/maintenance/cloudflaresite/"),
160
+ NavigationItem(title="API Keys", icon=Icons.KEY, link="/admin/maintenance/cloudflareapikey/"),
161
+ NavigationItem(title="Maintenance Logs", icon=Icons.HISTORY, link="/admin/maintenance/maintenancelog/"),
162
+ NavigationItem(title="Scheduled Maintenance", icon=Icons.SCHEDULE, link="/admin/maintenance/scheduledmaintenance/"),
164
163
  ]
165
164
  ))
166
165
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.2.16
3
+ Version: 1.2.18
4
4
  Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://docs.djangocfg.com
@@ -264,6 +264,7 @@ class EnterpriseConfig(DjangoConfig):
264
264
  enable_leads: bool = True # CRM integration
265
265
  enable_agents: bool = True # AI workflow automation
266
266
  enable_knowbase: bool = True # AI knowledge management
267
+ enable_maintenance: bool = True # Multi-site Cloudflare maintenance
267
268
 
268
269
  config = EnterpriseConfig()
269
270
  ```
@@ -315,6 +316,34 @@ workflow = Workflow([
315
316
  result = workflow.run({"document_id": "doc_123"})
316
317
  ```
317
318
 
319
+ ### 🌐 **Multi-Site Cloudflare Maintenance**
320
+ **Zero-configuration maintenance mode** for enterprise applications with automated monitoring.
321
+
322
+ ```python
323
+ from django_cfg.apps.maintenance.services import MaintenanceManager
324
+
325
+ # Enable maintenance for all production sites
326
+ manager = MaintenanceManager(user)
327
+ manager.bulk_enable_maintenance(
328
+ sites=CloudflareSite.objects.filter(environment='production'),
329
+ reason="Database migration",
330
+ message="🚀 Upgrading our systems. Back online in 30 minutes!"
331
+ )
332
+
333
+ # CLI management
334
+ # python manage.py maintenance enable --environment production
335
+ # python manage.py sync_cloudflare --api-token your_token
336
+ ```
337
+
338
+ **Features:**
339
+ - ✅ **Zero-config setup** - Just provide API token and domain
340
+ - ✅ **Multi-site management** - Handle hundreds of sites with ORM queries
341
+ - ✅ **Automated monitoring** - Health checks with auto-triggers
342
+ - ✅ **Rich admin interface** - Bulk operations and real-time status
343
+ - ✅ **CLI automation** - Perfect for CI/CD pipelines
344
+
345
+ [**📚 View Maintenance Documentation →**](https://docs.djangocfg.com/features/built-in-apps/maintenance)
346
+
318
347
  ### 🏢 **Enterprise User Management**
319
348
  **Multi-channel authentication** with OTP, SMS, email verification, and audit trails.
320
349
 
@@ -531,6 +560,7 @@ docker-compose up -d
531
560
 
532
561
  ### **🚀 Enterprise Features**
533
562
  - [**Built-in Applications**](https://docs.djangocfg.com/features/built-in-apps/accounts) - User management, support, CRM
563
+ - [**Maintenance Management**](https://docs.djangocfg.com/features/built-in-apps/maintenance) - Multi-site Cloudflare maintenance
534
564
  - [**Modular System**](https://docs.djangocfg.com/features/modules/overview) - Email, SMS, LLM, currency modules
535
565
  - [**Third-party Integrations**](https://docs.djangocfg.com/features/integrations/patterns) - Dramatiq, Twilio, ngrok
536
566
 
@@ -749,6 +779,27 @@ python manage.py generate_deployment --platform=docker --environment=production
749
779
  curl http://localhost:8000/health/
750
780
  ```
751
781
 
782
+ ### **🌐 Multi-Site Maintenance Management**
783
+ ```bash
784
+ # Enable maintenance for all production sites
785
+ python manage.py maintenance enable --environment production --reason "Database upgrade"
786
+
787
+ # Disable maintenance for specific project
788
+ python manage.py maintenance disable --project ecommerce
789
+
790
+ # Check status of all sites
791
+ python manage.py maintenance status --format json
792
+
793
+ # Auto-discover sites from Cloudflare
794
+ python manage.py sync_cloudflare --api-token your_token
795
+
796
+ # Bulk operations with filters
797
+ python manage.py maintenance enable --tag critical --reason "Security patch"
798
+
799
+ # Dry run to preview changes
800
+ python manage.py maintenance enable --environment staging --dry-run
801
+ ```
802
+
752
803
  ---
753
804
 
754
805
  ## 🔒 Enterprise Security & Compliance