django-cfg 1.2.17__py3-none-any.whl → 1.2.19__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 +4 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.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.17.dist-info → django_cfg-1.2.19.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,241 @@
1
+ """
2
+ Management command for processing scheduled maintenance events.
3
+
4
+ Handles automatic start/stop of scheduled maintenance windows.
5
+ """
6
+
7
+ from django.core.management.base import BaseCommand
8
+ from django.utils import timezone
9
+ from datetime import timedelta
10
+
11
+ from ...services.scheduled_maintenance_service import scheduled_maintenance_service
12
+ from ...models import ScheduledMaintenance
13
+
14
+
15
+ class Command(BaseCommand):
16
+ """Process scheduled maintenance events."""
17
+
18
+ help = 'Process scheduled maintenance events (start due, complete overdue)'
19
+
20
+ def add_arguments(self, parser):
21
+ """Add command arguments."""
22
+ parser.add_argument(
23
+ '--dry-run',
24
+ action='store_true',
25
+ help='Show what would be processed without making changes'
26
+ )
27
+
28
+ parser.add_argument(
29
+ '--upcoming',
30
+ type=int,
31
+ default=24,
32
+ help='Show upcoming maintenances within N hours (default: 24)'
33
+ )
34
+
35
+ parser.add_argument(
36
+ '--verbose',
37
+ action='store_true',
38
+ help='Enable verbose output'
39
+ )
40
+
41
+ def handle(self, *args, **options):
42
+ """Handle the command execution."""
43
+ verbosity = 2 if options['verbose'] else 1
44
+
45
+ if options['dry_run']:
46
+ self.stdout.write(
47
+ self.style.WARNING('🔍 DRY RUN MODE - No changes will be made')
48
+ )
49
+
50
+ # Show current status
51
+ self._show_current_status(verbosity)
52
+
53
+ if not options['dry_run']:
54
+ # Process due maintenances
55
+ self._process_due_maintenances(verbosity)
56
+
57
+ # Process overdue maintenances
58
+ self._process_overdue_maintenances(verbosity)
59
+ else:
60
+ # Show what would be processed
61
+ self._show_dry_run_info()
62
+
63
+ # Show upcoming maintenances
64
+ self._show_upcoming_maintenances(options['upcoming'], verbosity)
65
+
66
+ def _show_current_status(self, verbosity: int):
67
+ """Show current maintenance status."""
68
+ self.stdout.write("\n" + "="*60)
69
+ self.stdout.write(self.style.SUCCESS("📊 SCHEDULED MAINTENANCE STATUS"))
70
+ self.stdout.write("="*60)
71
+
72
+ # Active maintenances
73
+ active = scheduled_maintenance_service.get_active_maintenances()
74
+ if active:
75
+ self.stdout.write(f"\n🔧 Active Maintenances ({len(active)}):")
76
+ for maintenance in active:
77
+ time_left = ""
78
+ if maintenance['time_until_end']:
79
+ hours = int(maintenance['time_until_end'] // 3600)
80
+ minutes = int((maintenance['time_until_end'] % 3600) // 60)
81
+ time_left = f" ({hours}h {minutes}m remaining)"
82
+
83
+ overdue = " ⚠️ OVERDUE" if maintenance['is_overdue'] else ""
84
+
85
+ self.stdout.write(
86
+ f" • {maintenance['title']}{time_left}{overdue}"
87
+ )
88
+ if verbosity >= 2:
89
+ self.stdout.write(f" Sites: {maintenance['sites_count']}, Priority: {maintenance['priority']}")
90
+ else:
91
+ self.stdout.write("\n✅ No active maintenances")
92
+
93
+ # Due maintenances
94
+ due = ScheduledMaintenance.get_due_maintenances()
95
+ if due:
96
+ self.stdout.write(f"\n⏰ Due to Start ({due.count()}):")
97
+ for maintenance in due:
98
+ auto_text = " (auto)" if maintenance.auto_enable else " (manual)"
99
+ self.stdout.write(f" • {maintenance.title}{auto_text}")
100
+ if verbosity >= 2:
101
+ self.stdout.write(f" Sites: {maintenance.affected_sites_count}, Scheduled: {maintenance.scheduled_start}")
102
+
103
+ # Overdue maintenances
104
+ overdue = ScheduledMaintenance.get_overdue_maintenances()
105
+ if overdue:
106
+ self.stdout.write(f"\n⚠️ Overdue to Complete ({overdue.count()}):")
107
+ for maintenance in overdue:
108
+ auto_text = " (auto)" if maintenance.auto_disable else " (manual)"
109
+ self.stdout.write(f" • {maintenance.title}{auto_text}")
110
+ if verbosity >= 2:
111
+ overdue_time = timezone.now() - maintenance.scheduled_end
112
+ hours = int(overdue_time.total_seconds() // 3600)
113
+ minutes = int((overdue_time.total_seconds() % 3600) // 60)
114
+ self.stdout.write(f" Overdue by: {hours}h {minutes}m")
115
+
116
+ def _process_due_maintenances(self, verbosity: int):
117
+ """Process maintenances that are due to start."""
118
+ self.stdout.write("\n" + "-"*40)
119
+ self.stdout.write("🚀 Processing Due Maintenances")
120
+ self.stdout.write("-"*40)
121
+
122
+ results = scheduled_maintenance_service.process_due_maintenances()
123
+
124
+ if results['processed'] == 0:
125
+ self.stdout.write("✅ No due maintenances to process")
126
+ return
127
+
128
+ self.stdout.write(f"📊 Processed: {results['processed']}")
129
+ self.stdout.write(f"✅ Successful: {results['successful']}")
130
+
131
+ if results['failed'] > 0:
132
+ self.stdout.write(f"❌ Failed: {results['failed']}")
133
+
134
+ # Show details
135
+ if verbosity >= 2 and results['details']:
136
+ self.stdout.write("\n📋 Details:")
137
+ for detail in results['details']:
138
+ status = "✅" if detail['success'] else "❌"
139
+ self.stdout.write(f" {status} {detail['title']}")
140
+ if detail.get('sites_affected'):
141
+ self.stdout.write(f" Sites affected: {detail['sites_affected']}")
142
+ if detail.get('error'):
143
+ self.stdout.write(f" Error: {detail['error']}")
144
+
145
+ def _process_overdue_maintenances(self, verbosity: int):
146
+ """Process maintenances that are overdue to complete."""
147
+ self.stdout.write("\n" + "-"*40)
148
+ self.stdout.write("🏁 Processing Overdue Maintenances")
149
+ self.stdout.write("-"*40)
150
+
151
+ results = scheduled_maintenance_service.process_overdue_maintenances()
152
+
153
+ if results['processed'] == 0:
154
+ self.stdout.write("✅ No overdue maintenances to process")
155
+ return
156
+
157
+ self.stdout.write(f"📊 Processed: {results['processed']}")
158
+ self.stdout.write(f"✅ Successful: {results['successful']}")
159
+
160
+ if results['failed'] > 0:
161
+ self.stdout.write(f"❌ Failed: {results['failed']}")
162
+
163
+ # Show details
164
+ if verbosity >= 2 and results['details']:
165
+ self.stdout.write("\n📋 Details:")
166
+ for detail in results['details']:
167
+ status = "✅" if detail['success'] else "❌"
168
+ self.stdout.write(f" {status} {detail['title']}")
169
+ if detail.get('sites_affected'):
170
+ self.stdout.write(f" Sites affected: {detail['sites_affected']}")
171
+ if detail.get('actual_duration'):
172
+ duration_hours = detail['actual_duration'] / 3600
173
+ self.stdout.write(f" Duration: {duration_hours:.1f}h")
174
+ if detail.get('error'):
175
+ self.stdout.write(f" Error: {detail['error']}")
176
+
177
+ def _show_dry_run_info(self):
178
+ """Show what would be processed in dry run mode."""
179
+ self.stdout.write("\n" + "-"*40)
180
+ self.stdout.write("🔍 Dry Run - What Would Be Processed")
181
+ self.stdout.write("-"*40)
182
+
183
+ # Due maintenances
184
+ due = ScheduledMaintenance.get_due_maintenances().filter(auto_enable=True)
185
+ if due:
186
+ self.stdout.write(f"\n🚀 Would start {due.count()} maintenances:")
187
+ for maintenance in due:
188
+ self.stdout.write(f" • {maintenance.title} ({maintenance.affected_sites_count} sites)")
189
+
190
+ # Overdue maintenances
191
+ overdue = ScheduledMaintenance.get_overdue_maintenances().filter(auto_disable=True)
192
+ if overdue:
193
+ self.stdout.write(f"\n🏁 Would complete {overdue.count()} maintenances:")
194
+ for maintenance in overdue:
195
+ self.stdout.write(f" • {maintenance.title} ({maintenance.affected_sites_count} sites)")
196
+
197
+ if not due and not overdue:
198
+ self.stdout.write("✅ Nothing to process")
199
+
200
+ def _show_upcoming_maintenances(self, hours: int, verbosity: int):
201
+ """Show upcoming maintenance events."""
202
+ upcoming = scheduled_maintenance_service.get_upcoming_maintenances(hours=hours)
203
+
204
+ if not upcoming:
205
+ self.stdout.write(f"\n📅 No maintenances scheduled in next {hours} hours")
206
+ return
207
+
208
+ self.stdout.write(f"\n📅 Upcoming Maintenances (next {hours}h):")
209
+
210
+ for maintenance in upcoming:
211
+ # Calculate time until start
212
+ time_until = maintenance['time_until_start']
213
+ if time_until:
214
+ hours_until = int(time_until // 3600)
215
+ minutes_until = int((time_until % 3600) // 60)
216
+ time_str = f"in {hours_until}h {minutes_until}m"
217
+ else:
218
+ time_str = "now"
219
+
220
+ priority_emoji = {
221
+ 'low': '🟢',
222
+ 'normal': '🟡',
223
+ 'high': '🟠',
224
+ 'critical': '🔴'
225
+ }.get(maintenance['priority'], '⚪')
226
+
227
+ auto_text = " (auto)" if maintenance['auto_enable'] else " (manual)"
228
+
229
+ self.stdout.write(
230
+ f" {priority_emoji} {maintenance['title']} - {time_str}{auto_text}"
231
+ )
232
+
233
+ if verbosity >= 2:
234
+ start_time = maintenance['scheduled_start'][:16].replace('T', ' ')
235
+ duration_hours = maintenance['estimated_duration'] / 3600
236
+ self.stdout.write(
237
+ f" Start: {start_time}, Duration: {duration_hours:.1f}h, Sites: {maintenance['sites_count']}"
238
+ )
239
+
240
+ self.stdout.write(f"\n💡 Run with --dry-run to see what would be processed")
241
+ self.stdout.write(f"⏰ Current time: {timezone.now().strftime('%Y-%m-%d %H:%M:%S %Z')}")
@@ -1,168 +1,209 @@
1
1
  """
2
- Django management command for synchronizing sites with Cloudflare.
2
+ Management command for syncing sites with Cloudflare zones.
3
3
 
4
- Fetches zones from Cloudflare API and creates/updates CloudflareSite records.
4
+ Automatically discovers and syncs Cloudflare zones with Django models.
5
5
  """
6
6
 
7
7
  from django.core.management.base import BaseCommand, CommandError
8
- from django.contrib.auth import get_user_model
9
- from django.db import transaction
10
8
  from django.utils import timezone
11
- from typing import Dict, List, Any, Optional
12
- import logging
9
+ from typing import Optional
13
10
 
14
- from ...models import CloudflareSite
15
- from ...services import SyncCommandService
16
-
17
- User = get_user_model()
18
- logger = logging.getLogger(__name__)
11
+ from ...models import CloudflareApiKey
12
+ from ...services.site_sync_service import SiteSyncService
19
13
 
20
14
 
21
15
  class Command(BaseCommand):
22
- """Synchronize sites with Cloudflare zones."""
16
+ """Sync sites with Cloudflare zones."""
23
17
 
24
- help = 'Synchronize CloudflareSite records with Cloudflare zones'
18
+ help = 'Sync CloudflareSite models with actual Cloudflare zones'
25
19
 
26
20
  def add_arguments(self, parser):
27
21
  """Add command arguments."""
28
22
  parser.add_argument(
29
- '--user',
23
+ '--api-key',
30
24
  type=str,
31
- help='Username or email of the site owner (required)',
32
- required=True
33
- )
34
-
35
- parser.add_argument(
36
- '--api-token',
37
- type=str,
38
- help='Cloudflare API token (if not provided, will try to get from user config)',
25
+ help='Name of specific API key to sync (default: all active keys)'
39
26
  )
40
27
 
41
28
  parser.add_argument(
42
29
  '--dry-run',
43
30
  action='store_true',
44
- help='Show what would be synchronized without making changes',
31
+ help='Show what would be changed without making changes'
45
32
  )
46
33
 
47
34
  parser.add_argument(
48
- '--force',
35
+ '--force-update',
49
36
  action='store_true',
50
- help='Force update existing sites (overwrite local changes)',
51
- )
52
-
53
- parser.add_argument(
54
- '--environment',
55
- type=str,
56
- choices=['production', 'staging', 'development', 'testing'],
57
- default='production',
58
- help='Default environment for new sites (default: production)',
59
- )
60
-
61
- parser.add_argument(
62
- '--project',
63
- type=str,
64
- help='Default project name for new sites',
65
- )
66
-
67
- parser.add_argument(
68
- '--tags',
69
- type=str,
70
- nargs='*',
71
- help='Default tags for new sites (space-separated)',
37
+ help='Update existing sites even if they haven\'t changed'
72
38
  )
73
39
 
74
40
  parser.add_argument(
75
41
  '--verbose',
76
42
  action='store_true',
77
- help='Enable verbose output',
43
+ help='Enable verbose output'
78
44
  )
79
45
 
80
46
  def handle(self, *args, **options):
81
- """Execute the command."""
82
- try:
83
- # Validate parameters
84
- validation = self._validate_parameters(options)
85
- if not validation['valid']:
86
- for error in validation['errors']:
87
- self.stdout.write(self.style.ERROR(f"❌ {error}"))
88
- raise CommandError("Invalid parameters")
89
-
90
- # Show warnings
91
- for warning in validation['warnings']:
92
- self.stdout.write(self.style.WARNING(f"⚠️ {warning}"))
93
-
94
- # Initialize service
95
- sync_service = SyncCommandService.create_from_params(
96
- api_token=validation['api_token']
47
+ """Handle the command execution."""
48
+ verbosity = 2 if options['verbose'] else 1
49
+
50
+ if options['dry_run']:
51
+ self.stdout.write(
52
+ self.style.WARNING('🔍 DRY RUN MODE - No changes will be made')
97
53
  )
98
-
99
- # Perform sync
100
- self.stdout.write("🔍 Syncing sites with Cloudflare...")
101
- result = sync_service.sync_user_sites_command(
102
- user=validation['user'],
103
- dry_run=options['dry_run'],
104
- force_update=options['force'],
105
- environment=options['environment'],
106
- project=options.get('project') or '',
107
- tags=options.get('tags') or [],
108
- verbose=options['verbose']
54
+
55
+ # Get API keys to sync
56
+ if options['api_key']:
57
+ try:
58
+ api_keys = [CloudflareApiKey.objects.get(
59
+ name=options['api_key'],
60
+ is_active=True
61
+ )]
62
+ self.stdout.write(f"📡 Syncing specific API key: {options['api_key']}")
63
+ except CloudflareApiKey.DoesNotExist:
64
+ raise CommandError(f"API key '{options['api_key']}' not found or inactive")
65
+ else:
66
+ api_keys = CloudflareApiKey.objects.filter(is_active=True)
67
+ self.stdout.write(f"📡 Syncing all {api_keys.count()} active API keys")
68
+
69
+ if not api_keys:
70
+ self.stdout.write(
71
+ self.style.WARNING('⚠️ No active API keys found')
109
72
  )
73
+ return
74
+
75
+ # Sync each API key
76
+ total_stats = {
77
+ 'discovered': 0,
78
+ 'created': 0,
79
+ 'updated': 0,
80
+ 'skipped': 0,
81
+ 'errors': 0
82
+ }
83
+
84
+ for api_key in api_keys:
85
+ self.stdout.write(f"\n🔑 Processing API key: {api_key.name}")
110
86
 
111
- if result['success']:
112
- # Display results
113
- self._display_results(result['stats'], options['dry_run'])
114
- else:
115
- self.stdout.write(self.style.ERROR(f"❌ Sync failed: {result['error']}"))
116
- raise CommandError(result['error'])
117
-
118
- except Exception as e:
119
- logger.exception("Command failed")
120
- raise CommandError(f"Synchronization failed: {e}")
121
-
122
- def _validate_parameters(self, options: Dict[str, Any]) -> Dict[str, Any]:
123
- """Validate command parameters using service."""
124
- # Create temporary service for validation
125
- temp_service = SyncCommandService.create_from_params("dummy_token")
126
-
127
- return temp_service.validate_sync_parameters(
128
- user_identifier=options['user'],
129
- api_token=options.get('api_token'),
130
- environment=options['environment']
131
- )
132
-
133
- # Old methods removed - logic moved to SyncCommandService
134
-
135
- def _display_results(self, stats: Dict[str, int], dry_run: bool) -> None:
136
- """Display synchronization results."""
137
- mode = "DRY RUN" if dry_run else "COMPLETED"
87
+ try:
88
+ sync_service = SiteSyncService(api_key)
89
+ stats = sync_service.sync_zones(
90
+ force_update=options['force_update'],
91
+ dry_run=options['dry_run']
92
+ )
93
+
94
+ # Update totals
95
+ for key in total_stats:
96
+ total_stats[key] += stats[key]
97
+
98
+ # Display results for this API key
99
+ self._display_api_key_results(api_key.name, stats, verbosity)
100
+
101
+ except Exception as e:
102
+ self.stdout.write(
103
+ self.style.ERROR(f'❌ Failed to sync {api_key.name}: {e}')
104
+ )
105
+ total_stats['errors'] += 1
138
106
 
139
- self.stdout.write(f"\n🎯 Synchronization {mode}")
140
- self.stdout.write("=" * 40)
107
+ # Display overall summary
108
+ self._display_summary(total_stats, options['dry_run'])
109
+
110
+ def _display_api_key_results(self, api_key_name: str, stats: dict, verbosity: int):
111
+ """Display results for a single API key."""
112
+ if stats['errors'] > 0:
113
+ self.stdout.write(
114
+ self.style.ERROR(
115
+ f" ❌ {stats['errors']} errors occurred"
116
+ )
117
+ )
141
118
 
142
119
  if stats['created'] > 0:
120
+ action = "Would create" if stats.get('dry_run') else "Created"
143
121
  self.stdout.write(
144
- self.style.SUCCESS(f"✨ Created: {stats['created']} sites")
122
+ self.style.SUCCESS(
123
+ f" ✅ {action} {stats['created']} new sites"
124
+ )
145
125
  )
146
126
 
147
127
  if stats['updated'] > 0:
128
+ action = "Would update" if stats.get('dry_run') else "Updated"
148
129
  self.stdout.write(
149
- self.style.SUCCESS(f"🔄 Updated: {stats['updated']} sites")
130
+ self.style.SUCCESS(
131
+ f" 🔄 {action} {stats['updated']} existing sites"
132
+ )
150
133
  )
151
134
 
152
135
  if stats['skipped'] > 0:
153
136
  self.stdout.write(
154
- self.style.WARNING(f"⏭️ Skipped: {stats['skipped']} sites")
137
+ self.style.WARNING(
138
+ f" ⏭️ Skipped {stats['skipped']} sites (no changes)"
139
+ )
155
140
  )
156
141
 
142
+ # Verbose output - show individual sites
143
+ if verbosity >= 2 and stats.get('sites'):
144
+ self.stdout.write(" 📋 Site details:")
145
+ for site_info in stats['sites']:
146
+ if site_info['action'] == 'created':
147
+ self.stdout.write(f" ➕ Created: {site_info['domain']}")
148
+ elif site_info['action'] == 'updated':
149
+ self.stdout.write(f" 🔄 Updated: {site_info['domain']}")
150
+ elif site_info['action'] == 'would_create':
151
+ self.stdout.write(f" ➕ Would create: {site_info['domain']}")
152
+ elif site_info['action'] == 'would_update':
153
+ self.stdout.write(f" 🔄 Would update: {site_info['domain']}")
154
+ if 'changes' in site_info:
155
+ for field, change in site_info['changes'].items():
156
+ self.stdout.write(
157
+ f" • {field}: {change['old']} → {change['new']}"
158
+ )
159
+ elif site_info['action'] == 'error':
160
+ self.stdout.write(
161
+ self.style.ERROR(
162
+ f" ❌ Error: {site_info['domain']} - {site_info['error']}"
163
+ )
164
+ )
165
+
166
+ def _display_summary(self, stats: dict, dry_run: bool):
167
+ """Display overall summary."""
168
+ self.stdout.write("\n" + "="*50)
169
+ self.stdout.write(
170
+ self.style.SUCCESS("📊 SYNC SUMMARY") if not dry_run
171
+ else self.style.WARNING("📊 DRY RUN SUMMARY")
172
+ )
173
+ self.stdout.write("="*50)
174
+
175
+ if stats['discovered'] > 0:
176
+ self.stdout.write(f"🔍 Zones discovered: {stats['discovered']}")
177
+
178
+ if stats['created'] > 0:
179
+ action = "Would be created" if dry_run else "Created"
180
+ self.stdout.write(
181
+ self.style.SUCCESS(f"✅ Sites {action.lower()}: {stats['created']}")
182
+ )
183
+
184
+ if stats['updated'] > 0:
185
+ action = "Would be updated" if dry_run else "Updated"
186
+ self.stdout.write(
187
+ self.style.SUCCESS(f"🔄 Sites {action.lower()}: {stats['updated']}")
188
+ )
189
+
190
+ if stats['skipped'] > 0:
191
+ self.stdout.write(f"⏭️ Sites skipped: {stats['skipped']}")
192
+
157
193
  if stats['errors'] > 0:
158
194
  self.stdout.write(
159
- self.style.ERROR(f"❌ Errors: {stats['errors']} sites")
195
+ self.style.ERROR(f"❌ Errors: {stats['errors']}")
160
196
  )
161
197
 
162
- total = sum(stats.values())
163
- self.stdout.write(f"\n📊 Total processed: {total} zones")
198
+ total_processed = stats['created'] + stats['updated'] + stats['skipped']
199
+ if total_processed > 0:
200
+ self.stdout.write(f"\n📈 Total sites processed: {total_processed}")
164
201
 
165
- if dry_run:
202
+ if dry_run and (stats['created'] > 0 or stats['updated'] > 0):
166
203
  self.stdout.write(
167
- self.style.NOTICE("\n💡 Run without --dry-run to apply changes")
204
+ self.style.WARNING(
205
+ "\n💡 Run without --dry-run to apply these changes"
206
+ )
168
207
  )
208
+
209
+ self.stdout.write(f"⏰ Completed at: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}")
@@ -1,20 +1,15 @@
1
1
  """
2
- Custom managers for maintenance app models.
2
+ Custom managers for maintenance models.
3
3
 
4
- Provides enhanced querying capabilities and business logic methods
5
- for CloudflareSite, MaintenanceEvent, and related models.
4
+ Simplified managers with useful query methods.
6
5
  """
7
6
 
8
- from .sites import CloudflareSiteManager, SiteGroupManager
9
- from .events import MaintenanceEventManager, MaintenanceLogManager
10
- from .monitoring import MonitoringTargetManager
11
- from .deployments import CloudflareDeploymentManager
7
+ from .cloudflare_site_manager import CloudflareSiteManager, CloudflareSiteQuerySet
8
+ from .maintenance_log_manager import MaintenanceLogManager, MaintenanceLogQuerySet
12
9
 
13
10
  __all__ = [
14
11
  'CloudflareSiteManager',
15
- 'SiteGroupManager',
16
- 'MaintenanceEventManager',
17
- 'MaintenanceLogManager',
18
- 'MonitoringTargetManager',
19
- 'CloudflareDeploymentManager',
12
+ 'CloudflareSiteQuerySet',
13
+ 'MaintenanceLogManager',
14
+ 'MaintenanceLogQuerySet',
20
15
  ]