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
@@ -1,375 +1,261 @@
1
1
  """
2
- Multi-site maintenance management command.
2
+ Simplified maintenance management command.
3
3
 
4
- Provides CLI interface for managing maintenance mode across multiple sites
5
- with ORM-like syntax and bulk operations.
4
+ Single command instead of 2+ complex commands.
5
+ Usage: python manage.py maintenance enable/disable/status/sync domain.com
6
6
  """
7
7
 
8
- import asyncio
9
8
  from django.core.management.base import BaseCommand, CommandError
10
- from django.contrib.auth import get_user_model
11
- from django.utils import timezone
12
- from typing import List, Optional
9
+ from django.db import transaction
10
+ from typing import Any, Optional
13
11
 
14
- from django_cfg.apps.maintenance.services import MaintenanceManager
15
- from django_cfg.apps.maintenance.models import CloudflareSite
16
-
17
- User = get_user_model()
12
+ from ...models import CloudflareSite, MaintenanceLog
13
+ from ...services import MaintenanceService
18
14
 
19
15
 
20
16
  class Command(BaseCommand):
21
- """
22
- Multi-site maintenance management command.
23
-
24
- Examples:
25
- # Enable maintenance for all production sites
26
- python manage.py maintenance enable --environment production
27
-
28
- # Disable maintenance for specific project
29
- python manage.py maintenance disable --project myproject
30
-
31
- # Check status of all sites
32
- python manage.py maintenance status
33
-
34
- # Enable maintenance with custom message
35
- python manage.py maintenance enable --domain example.com --message "Upgrading database"
36
-
37
- # Bulk operations with filters
38
- python manage.py maintenance enable --tag critical --reason "Security patch"
39
- """
17
+ """Simple maintenance management command."""
40
18
 
41
- help = 'Manage maintenance mode for multiple Cloudflare sites'
19
+ help = 'Manage maintenance mode for Cloudflare sites'
42
20
 
43
- def add_arguments(self, parser):
21
+ def add_arguments(self, parser) -> None:
44
22
  """Add command arguments."""
45
- # Main action
46
23
  parser.add_argument(
47
24
  'action',
48
- choices=['enable', 'disable', 'status', 'list', 'discover'],
25
+ choices=['enable', 'disable', 'status', 'sync', 'list'],
49
26
  help='Action to perform'
50
27
  )
51
28
 
52
- # Site filters
53
- parser.add_argument(
54
- '--domain',
55
- help='Specific domain to target'
56
- )
57
- parser.add_argument(
58
- '--environment',
59
- choices=['production', 'staging', 'development', 'testing'],
60
- help='Filter by environment'
61
- )
62
29
  parser.add_argument(
63
- '--project',
64
- help='Filter by project name'
65
- )
66
- parser.add_argument(
67
- '--tag',
68
- help='Filter by tag'
69
- )
70
- parser.add_argument(
71
- '--owner',
72
- help='Filter by owner username'
30
+ 'domain',
31
+ nargs='?',
32
+ help='Domain to operate on (required for enable/disable/status/sync)'
73
33
  )
74
34
 
75
- # Maintenance options
76
35
  parser.add_argument(
77
36
  '--reason',
78
- default='Manual maintenance via CLI',
79
- help='Reason for maintenance'
80
- )
81
- parser.add_argument(
82
- '--message',
83
- help='Custom maintenance message'
37
+ default='Maintenance via CLI',
38
+ help='Reason for enabling maintenance (default: "Maintenance via CLI")'
84
39
  )
85
40
 
86
- # Operation options
87
- parser.add_argument(
88
- '--dry-run',
89
- action='store_true',
90
- help='Show what would be done without actually doing it'
91
- )
92
41
  parser.add_argument(
93
42
  '--force',
94
43
  action='store_true',
95
- help='Force operation without confirmation'
44
+ help='Skip confirmation prompts'
96
45
  )
97
46
 
98
- # Discovery options
99
- parser.add_argument(
100
- '--api-token',
101
- help='Cloudflare API token for site discovery'
102
- )
103
-
104
- # Output options
105
- parser.add_argument(
106
- '--format',
107
- choices=['table', 'json', 'csv'],
108
- default='table',
109
- help='Output format'
110
- )
111
47
  parser.add_argument(
112
48
  '--verbose',
113
49
  action='store_true',
114
- help='Verbose output'
50
+ help='Show detailed output'
115
51
  )
116
52
 
117
- def handle(self, *args, **options):
118
- """Handle command execution."""
119
- self.options = options
120
- self.verbosity = options.get('verbosity', 1)
53
+ def handle(self, *args: Any, **options: Any) -> None:
54
+ """Handle the command."""
55
+ action = options['action']
56
+ domain = options['domain']
57
+ reason = options['reason']
58
+ force = options['force']
59
+ verbose = options['verbose']
60
+
61
+ # Validate arguments
62
+ if action in ['enable', 'disable', 'status', 'sync'] and not domain:
63
+ raise CommandError(f"Domain is required for '{action}' action")
121
64
 
122
65
  try:
123
- # Get user for operations
124
- user = self._get_user(options.get('owner'))
125
-
126
- # Execute action
127
- if options['action'] == 'enable':
128
- asyncio.run(self._handle_enable(user))
129
- elif options['action'] == 'disable':
130
- asyncio.run(self._handle_disable(user))
131
- elif options['action'] == 'status':
132
- asyncio.run(self._handle_status(user))
133
- elif options['action'] == 'list':
134
- self._handle_list(user)
135
- elif options['action'] == 'discover':
136
- asyncio.run(self._handle_discover(user))
137
-
66
+ if action == 'list':
67
+ self._handle_list(verbose)
68
+ elif action == 'status':
69
+ self._handle_status(domain, verbose)
70
+ elif action == 'enable':
71
+ self._handle_enable(domain, reason, force, verbose)
72
+ elif action == 'disable':
73
+ self._handle_disable(domain, force, verbose)
74
+ elif action == 'sync':
75
+ self._handle_sync(domain, verbose)
76
+
77
+ except CloudflareSite.DoesNotExist:
78
+ raise CommandError(f"Site '{domain}' not found. Use 'list' action to see available sites.")
138
79
  except Exception as e:
139
- raise CommandError(f"Command failed: {str(e)}")
80
+ raise CommandError(f"Operation failed: {str(e)}")
140
81
 
141
- def _get_user(self, username: Optional[str] = None) -> User:
142
- """Get user for operations."""
143
- if username:
144
- try:
145
- return User.objects.get(username=username)
146
- except User.DoesNotExist:
147
- raise CommandError(f"User '{username}' not found")
148
- else:
149
- # Use first superuser if no user specified
150
- superuser = User.objects.filter(is_superuser=True).first()
151
- if not superuser:
152
- raise CommandError("No superuser found. Please specify --owner or create a superuser.")
153
- return superuser
154
-
155
- def _get_sites_queryset(self, user: User):
156
- """Get filtered sites queryset based on command options."""
157
- sites = multi_site_manager.sites(user)
158
-
159
- # Apply filters
160
- if self.options.get('domain'):
161
- sites = sites.filter(domain=self.options['domain'])
82
+ def _handle_list(self, verbose: bool) -> None:
83
+ """List all sites."""
84
+ sites = CloudflareSite.objects.all().order_by('name')
162
85
 
163
- if self.options.get('environment'):
164
- sites = sites.filter(environment=self.options['environment'])
165
-
166
- if self.options.get('project'):
167
- sites = sites.filter(project=self.options['project'])
168
-
169
- if self.options.get('tag'):
170
- sites = sites.with_tag(self.options['tag'])
171
-
172
- return sites
173
-
174
- async def _handle_enable(self, user: User):
175
- """Handle enable maintenance action."""
176
- sites = self._get_sites_queryset(user)
177
-
178
- if sites.count() == 0:
179
- self.stdout.write(self.style.WARNING("No sites match the specified filters."))
86
+ if not sites:
87
+ self.stdout.write(self.style.WARNING("No sites configured"))
180
88
  return
181
89
 
182
- # Show what will be affected
183
- self.stdout.write(f"Will enable maintenance for {sites.count()} sites:")
184
- for site in sites.all()[:10]: # Show first 10
185
- self.stdout.write(f" - {site.domain} ({site.environment})")
90
+ self.stdout.write(self.style.SUCCESS(f"Found {sites.count()} sites:"))
91
+ self.stdout.write("")
186
92
 
187
- if sites.count() > 10:
188
- self.stdout.write(f" ... and {sites.count() - 10} more sites")
189
-
190
- # Confirm unless forced
191
- if not self.options.get('force') and not self.options.get('dry_run'):
192
- confirm = input("\nProceed? (y/N): ")
193
- if confirm.lower() != 'y':
194
- self.stdout.write("Operation cancelled.")
195
- return
196
-
197
- # Execute operation
198
- result = await sites.enable_maintenance(
199
- reason=self.options['reason'],
200
- message=self.options.get('message'),
201
- user=user,
202
- dry_run=self.options.get('dry_run', False)
203
- )
204
-
205
- # Display results
206
- self._display_bulk_result("Enable Maintenance", result)
93
+ for site in sites:
94
+ status_style = self.style.WARNING if site.maintenance_active else self.style.SUCCESS
95
+ status_text = "🔧 MAINTENANCE" if site.maintenance_active else "🟢 ACTIVE"
96
+
97
+ self.stdout.write(f" {status_style(status_text)} {site.name} ({site.domain})")
98
+
99
+ if verbose:
100
+ self.stdout.write(f" Zone ID: {site.zone_id}")
101
+ self.stdout.write(f" Account ID: {site.account_id}")
102
+ self.stdout.write(f" Created: {site.created_at.strftime('%Y-%m-%d %H:%M')}")
103
+ if site.last_maintenance_at:
104
+ self.stdout.write(f" Last Maintenance: {site.last_maintenance_at.strftime('%Y-%m-%d %H:%M')}")
105
+
106
+ # Show recent logs
107
+ recent_logs = site.logs.all()[:3]
108
+ if recent_logs:
109
+ self.stdout.write(" Recent logs:")
110
+ for log in recent_logs:
111
+ status_emoji = {
112
+ MaintenanceLog.Status.SUCCESS: "✅",
113
+ MaintenanceLog.Status.FAILED: "❌",
114
+ MaintenanceLog.Status.PENDING: "⏳"
115
+ }.get(log.status, "❓")
116
+
117
+ self.stdout.write(f" {status_emoji} {log.get_action_display()} - {log.created_at.strftime('%m-%d %H:%M')}")
118
+
119
+ self.stdout.write("")
207
120
 
208
- async def _handle_disable(self, user: User):
209
- """Handle disable maintenance action."""
210
- sites = self._get_sites_queryset(user).in_maintenance()
121
+ def _handle_status(self, domain: str, verbose: bool) -> None:
122
+ """Show status for specific site."""
123
+ site = CloudflareSite.objects.get(domain=domain)
211
124
 
212
- if sites.count() == 0:
213
- self.stdout.write(self.style.WARNING("No sites in maintenance match the specified filters."))
214
- return
215
-
216
- # Show what will be affected
217
- self.stdout.write(f"Will disable maintenance for {sites.count()} sites:")
218
- for site in sites.all()[:10]:
219
- duration = site.maintenance_duration
220
- duration_str = f" ({duration})" if duration else ""
221
- self.stdout.write(f" - {site.domain}{duration_str}")
125
+ status_style = self.style.WARNING if site.maintenance_active else self.style.SUCCESS
126
+ status_text = "🔧 MAINTENANCE ACTIVE" if site.maintenance_active else "🟢 ACTIVE"
222
127
 
223
- if sites.count() > 10:
224
- self.stdout.write(f" ... and {sites.count() - 10} more sites")
128
+ self.stdout.write(f"Status for {site.name} ({domain}):")
129
+ self.stdout.write(f" {status_style(status_text)}")
225
130
 
226
- # Confirm unless forced
227
- if not self.options.get('force') and not self.options.get('dry_run'):
228
- confirm = input("\nProceed? (y/N): ")
229
- if confirm.lower() != 'y':
230
- self.stdout.write("Operation cancelled.")
231
- return
232
-
233
- # Execute operation
234
- result = await sites.disable_maintenance(
235
- user=user,
236
- dry_run=self.options.get('dry_run', False)
237
- )
238
-
239
- # Display results
240
- self._display_bulk_result("Disable Maintenance", result)
131
+ if verbose:
132
+ self.stdout.write(f" Zone ID: {site.zone_id}")
133
+ self.stdout.write(f" Account ID: {site.account_id}")
134
+ self.stdout.write(f" Created: {site.created_at.strftime('%Y-%m-%d %H:%M')}")
135
+ self.stdout.write(f" Updated: {site.updated_at.strftime('%Y-%m-%d %H:%M')}")
136
+
137
+ if site.last_maintenance_at:
138
+ self.stdout.write(f" Last Maintenance: {site.last_maintenance_at.strftime('%Y-%m-%d %H:%M')}")
139
+
140
+ # Show recent logs
141
+ recent_logs = site.logs.all()[:5]
142
+ if recent_logs:
143
+ self.stdout.write(" Recent activity:")
144
+ for log in recent_logs:
145
+ status_emoji = {
146
+ MaintenanceLog.Status.SUCCESS: "✅",
147
+ MaintenanceLog.Status.FAILED: "❌",
148
+ MaintenanceLog.Status.PENDING: "⏳"
149
+ }.get(log.status, "❓")
150
+
151
+ duration_text = f" ({log.duration_seconds}s)" if log.duration_seconds else ""
152
+ self.stdout.write(f" {status_emoji} {log.get_action_display()}{duration_text} - {log.created_at.strftime('%Y-%m-%d %H:%M')}")
153
+
154
+ if log.error_message and verbose:
155
+ self.stdout.write(f" Error: {log.error_message[:100]}")
241
156
 
242
- async def _handle_status(self, user: User):
243
- """Handle status check action."""
244
- sites = self._get_sites_queryset(user)
157
+ def _handle_enable(self, domain: str, reason: str, force: bool, verbose: bool) -> None:
158
+ """Enable maintenance for site."""
159
+ site = CloudflareSite.objects.get(domain=domain)
245
160
 
246
- if sites.count() == 0:
247
- self.stdout.write(self.style.WARNING("No sites match the specified filters."))
161
+ if site.maintenance_active:
162
+ self.stdout.write(self.style.WARNING(f"Maintenance is already active for {domain}"))
248
163
  return
249
164
 
250
- self.stdout.write(f"Checking status of {sites.count()} sites...")
251
-
252
- result = await sites.check_status()
253
-
254
- # Display status summary
255
- self.stdout.write(self.style.SUCCESS(f"\n✅ Status check completed for {result['total']} sites"))
165
+ # Confirmation
166
+ if not force:
167
+ confirm = input(f"Enable maintenance for {site.name} ({domain})? [y/N]: ")
168
+ if confirm.lower() not in ['y', 'yes']:
169
+ self.stdout.write("Cancelled")
170
+ return
256
171
 
257
- status_summary = result.get('status_summary', {})
258
- if status_summary:
259
- self.stdout.write("\nStatus Summary:")
260
- for status, count in status_summary.items():
261
- self.stdout.write(f" {status.title()}: {count} sites")
172
+ self.stdout.write(f"Enabling maintenance for {domain}...")
173
+ if verbose:
174
+ self.stdout.write(f"Reason: {reason}")
262
175
 
263
- # Display individual site status if verbose
264
- if self.options.get('verbose'):
265
- self.stdout.write("\nIndividual Site Status:")
266
- for site_info in result.get('sites', []):
267
- status_icon = {
268
- 'active': '🟢',
269
- 'maintenance': '🔧',
270
- 'offline': '🔴',
271
- 'unknown': '❓',
272
- 'error': '❌'
273
- }.get(site_info['status'], '❓')
176
+ try:
177
+ with transaction.atomic():
178
+ service = MaintenanceService(site)
179
+ log_entry = service.enable_maintenance(reason)
180
+
181
+ if log_entry.status == MaintenanceLog.Status.SUCCESS:
182
+ duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
183
+ self.stdout.write(self.style.SUCCESS(f"✅ Maintenance enabled successfully{duration_text}"))
274
184
 
275
- self.stdout.write(
276
- f" {status_icon} {site_info['domain']}: {site_info['status']}"
277
- )
185
+ if verbose and log_entry.cloudflare_response:
186
+ self.stdout.write("Cloudflare response:")
187
+ import json
188
+ self.stdout.write(json.dumps(log_entry.cloudflare_response, indent=2))
189
+ else:
190
+ self.stdout.write(self.style.ERROR(f"❌ Failed to enable maintenance: {log_entry.error_message}"))
191
+
192
+ except Exception as e:
193
+ self.stdout.write(self.style.ERROR(f"❌ Error: {str(e)}"))
278
194
 
279
- def _handle_list(self, user: User):
280
- """Handle list sites action."""
281
- sites = self._get_sites_queryset(user)
195
+ def _handle_disable(self, domain: str, force: bool, verbose: bool) -> None:
196
+ """Disable maintenance for site."""
197
+ site = CloudflareSite.objects.get(domain=domain)
282
198
 
283
- if sites.count() == 0:
284
- self.stdout.write(self.style.WARNING("No sites match the specified filters."))
199
+ if not site.maintenance_active:
200
+ self.stdout.write(self.style.WARNING(f"Maintenance is not active for {domain}"))
285
201
  return
286
202
 
287
- # Display sites in table format
288
- self.stdout.write(f"\nFound {sites.count()} sites:\n")
203
+ # Confirmation
204
+ if not force:
205
+ confirm = input(f"Disable maintenance for {site.name} ({domain})? [y/N]: ")
206
+ if confirm.lower() not in ['y', 'yes']:
207
+ self.stdout.write("Cancelled")
208
+ return
289
209
 
290
- # Header
291
- self.stdout.write(
292
- f"{'Domain':<30} {'Environment':<12} {'Status':<12} {'Project':<20} {'Maintenance':<12}"
293
- )
294
- self.stdout.write("-" * 86)
210
+ self.stdout.write(f"Disabling maintenance for {domain}...")
295
211
 
296
- # Sites
297
- for site in sites.all():
298
- maintenance_status = "Active" if site.maintenance_active else "Inactive"
212
+ try:
213
+ with transaction.atomic():
214
+ service = MaintenanceService(site)
215
+ log_entry = service.disable_maintenance()
299
216
 
300
- self.stdout.write(
301
- f"{site.domain:<30} {site.environment:<12} {site.current_status:<12} "
302
- f"{(site.project or 'None'):<20} {maintenance_status:<12}"
303
- )
217
+ if log_entry.status == MaintenanceLog.Status.SUCCESS:
218
+ duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
219
+ self.stdout.write(self.style.SUCCESS(f" Maintenance disabled successfully{duration_text}"))
220
+
221
+ if verbose and log_entry.cloudflare_response:
222
+ self.stdout.write("Cloudflare response:")
223
+ import json
224
+ self.stdout.write(json.dumps(log_entry.cloudflare_response, indent=2))
225
+ else:
226
+ self.stdout.write(self.style.ERROR(f"❌ Failed to disable maintenance: {log_entry.error_message}"))
227
+
228
+ except Exception as e:
229
+ self.stdout.write(self.style.ERROR(f"❌ Error: {str(e)}"))
304
230
 
305
- async def _handle_discover(self, user: User):
306
- """Handle site discovery action."""
307
- api_token = self.options.get('api_token')
308
- if not api_token:
309
- raise CommandError("--api-token is required for site discovery")
231
+ def _handle_sync(self, domain: str, verbose: bool) -> None:
232
+ """Sync site from Cloudflare."""
233
+ site = CloudflareSite.objects.get(domain=domain)
310
234
 
311
- self.stdout.write("Discovering Cloudflare sites...")
235
+ self.stdout.write(f"Syncing {domain} from Cloudflare...")
312
236
 
313
237
  try:
314
- discovered_sites = await multi_site_manager.discover_sites(api_token, user)
238
+ service = MaintenanceService(site)
239
+ log_entry = service.sync_site_from_cloudflare()
315
240
 
316
- if discovered_sites:
317
- self.stdout.write(
318
- self.style.SUCCESS(f"✅ Discovered {len(discovered_sites)} new sites:")
319
- )
320
- for site in discovered_sites:
321
- self.stdout.write(f" - {site.domain} ({site.environment})")
241
+ if log_entry.status == MaintenanceLog.Status.SUCCESS:
242
+ duration_text = f" ({log_entry.duration_seconds}s)" if log_entry.duration_seconds else ""
243
+ self.stdout.write(self.style.SUCCESS(f"✅ Sync completed successfully{duration_text}"))
244
+
245
+ if verbose and log_entry.cloudflare_response:
246
+ response = log_entry.cloudflare_response
247
+ if 'updated_fields' in response and response['updated_fields']:
248
+ self.stdout.write(f"Updated fields: {', '.join(response['updated_fields'])}")
249
+
250
+ maintenance_status = response.get('maintenance_active', 'unknown')
251
+ self.stdout.write(f"Current maintenance status: {maintenance_status}")
252
+
253
+ if verbose:
254
+ import json
255
+ self.stdout.write("Full Cloudflare response:")
256
+ self.stdout.write(json.dumps(response, indent=2))
322
257
  else:
323
- self.stdout.write(self.style.WARNING("No new sites discovered."))
258
+ self.stdout.write(self.style.ERROR(f" Sync failed: {log_entry.error_message}"))
324
259
 
325
260
  except Exception as e:
326
- raise CommandError(f"Site discovery failed: {str(e)}")
327
-
328
- def _display_bulk_result(self, operation: str, result):
329
- """Display bulk operation results."""
330
- if result.dry_run:
331
- self.stdout.write(self.style.WARNING(f"\n🔍 DRY RUN - {operation}"))
332
- self.stdout.write(f"Would affect {len(result.would_affect)} sites:")
333
- for domain in result.would_affect[:10]:
334
- self.stdout.write(f" - {domain}")
335
- if len(result.would_affect) > 10:
336
- self.stdout.write(f" ... and {len(result.would_affect) - 10} more")
337
- return
338
-
339
- # Real operation results
340
- self.stdout.write(f"\n{operation} Results:")
341
- self.stdout.write(f"Total sites: {result.total}")
342
-
343
- if result.successful:
344
- self.stdout.write(
345
- self.style.SUCCESS(f"✅ Successful: {len(result.successful)} sites")
346
- )
347
- if self.options.get('verbose'):
348
- for domain in result.successful:
349
- self.stdout.write(f" ✅ {domain}")
350
-
351
- if result.failed:
352
- self.stdout.write(
353
- self.style.ERROR(f"❌ Failed: {len(result.failed)} sites")
354
- )
355
- for failure in result.failed:
356
- self.stdout.write(f" ❌ {failure['site']}: {failure['reason']}")
357
-
358
- if result.skipped:
359
- self.stdout.write(
360
- self.style.WARNING(f"⏭️ Skipped: {len(result.skipped)} sites")
361
- )
362
- if self.options.get('verbose'):
363
- for skip in result.skipped:
364
- self.stdout.write(f" ⏭️ {skip['site']}: {skip['reason']}")
365
-
366
- # Success rate
367
- success_rate = result.success_rate
368
- if success_rate == 100.0:
369
- style = self.style.SUCCESS
370
- elif success_rate >= 80.0:
371
- style = self.style.WARNING
372
- else:
373
- style = self.style.ERROR
374
-
375
- self.stdout.write(style(f"\nSuccess rate: {success_rate:.1f}%"))
261
+ self.stdout.write(self.style.ERROR(f" Error: {str(e)}"))