django-cfg 1.2.14__py3-none-any.whl → 1.2.16__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 (62) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +42 -3
  51. django_cfg/core/generation.py +16 -5
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +55 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_tasks.py +41 -3
  57. django_cfg/modules/django_unfold/dashboard.py +16 -1
  58. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/METADATA +2 -1
  59. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/RECORD +62 -14
  60. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/WHEEL +0 -0
  61. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/entry_points.txt +0 -0
  62. {django_cfg-1.2.14.dist-info → django_cfg-1.2.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,374 @@
1
+ """
2
+ Events admin interfaces with Unfold optimization.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.utils.html import format_html
7
+ from django.urls import reverse
8
+ from django.utils.safestring import mark_safe
9
+ from django.db import models
10
+ from django.db.models import Count, Q
11
+ from django.contrib import messages
12
+ from django.shortcuts import redirect
13
+ from unfold.admin import ModelAdmin, TabularInline
14
+ from unfold.decorators import display, action
15
+ from unfold.enums import ActionVariant
16
+ from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
17
+ from django_cfg import ImportExportModelAdmin, ExportMixin
18
+
19
+ from ..models import MaintenanceEvent, MaintenanceLog, CloudflareSite
20
+
21
+
22
+ class MaintenanceLogInline(TabularInline):
23
+ """Inline for maintenance logs with Unfold styling."""
24
+
25
+ model = MaintenanceLog
26
+ verbose_name = "Log Entry"
27
+ verbose_name_plural = "📋 Maintenance Logs"
28
+ extra = 0
29
+ max_num = 0
30
+ can_delete = False
31
+ show_change_link = True
32
+
33
+ def has_add_permission(self, request, obj=None):
34
+ return False
35
+
36
+ def has_change_permission(self, request, obj=None):
37
+ return False
38
+
39
+ fields = [
40
+ 'timestamp_display', 'level_badge', 'component',
41
+ 'operation', 'message_preview', 'user'
42
+ ]
43
+ readonly_fields = [
44
+ 'timestamp_display', 'level_badge', 'component',
45
+ 'operation', 'message_preview', 'user'
46
+ ]
47
+
48
+ @display(description="Time", ordering="timestamp")
49
+ def timestamp_display(self, obj):
50
+ """Display timestamp."""
51
+ return obj.timestamp.strftime("%H:%M:%S")
52
+
53
+ @display(description="Level", ordering="level")
54
+ def level_badge(self, obj):
55
+ """Display log level with colored badge."""
56
+ colors = {
57
+ 'DEBUG': 'secondary',
58
+ 'INFO': 'info',
59
+ 'WARNING': 'warning',
60
+ 'ERROR': 'danger',
61
+ 'CRITICAL': 'danger'
62
+ }
63
+ color = colors.get(obj.level, 'secondary')
64
+ return format_html(
65
+ '<span class="badge badge-{}">{}</span>',
66
+ color, obj.level
67
+ )
68
+
69
+ @display(description="Message")
70
+ def message_preview(self, obj):
71
+ """Display truncated message."""
72
+ if len(obj.message) > 50:
73
+ return obj.message[:50] + "..."
74
+ return obj.message
75
+
76
+
77
+ @admin.register(MaintenanceEvent)
78
+ class MaintenanceEventAdmin(ModelAdmin, ImportExportModelAdmin):
79
+ """Admin for MaintenanceEvent with Unfold styling."""
80
+
81
+ list_display = [
82
+ "title_with_icon",
83
+ "status_badge",
84
+ "sites_count",
85
+ "duration_display",
86
+ "initiated_by",
87
+ "created_at_display"
88
+ ]
89
+ list_display_links = ["title_with_icon"]
90
+ search_fields = ["title", "description", "initiated_by__email"]
91
+ list_filter = [
92
+ "status",
93
+ "created_at",
94
+ ("initiated_by", AutocompleteSelectFilter),
95
+ ("sites", AutocompleteSelectMultipleFilter),
96
+ ]
97
+ ordering = ["-created_at"]
98
+ readonly_fields = [
99
+ "created_at", "updated_at"
100
+ ]
101
+
102
+ fieldsets = (
103
+ ("Event Details", {
104
+ "fields": ("title", "description", "reason", "initiated_by")
105
+ }),
106
+ ("Scheduling", {
107
+ "fields": ("started_at", "ended_at", "estimated_duration", "status")
108
+ }),
109
+ ("Sites", {
110
+ "fields": ("sites",)
111
+ }),
112
+ ("Timestamps", {
113
+ "fields": ("created_at", "updated_at"),
114
+ "classes": ("collapse",)
115
+ })
116
+ )
117
+
118
+ filter_horizontal = ["sites"]
119
+ inlines = [MaintenanceLogInline]
120
+
121
+ # Unfold actions
122
+ actions_detail = ["start_maintenance", "complete_maintenance", "cancel_maintenance"]
123
+
124
+ @display(description="Event", ordering="title")
125
+ def title_with_icon(self, obj):
126
+ """Display event title with status icon."""
127
+ icons = {
128
+ 'scheduled': '📅',
129
+ 'in_progress': '🔧',
130
+ 'completed': '✅',
131
+ 'failed': '❌',
132
+ 'cancelled': '🚫'
133
+ }
134
+ icon = icons.get(obj.status, '📋')
135
+ return format_html('{} {}', icon, obj.title)
136
+
137
+ @display(description="Status", ordering="status")
138
+ def status_badge(self, obj):
139
+ """Display status with colored badge."""
140
+ colors = {
141
+ 'scheduled': 'info',
142
+ 'in_progress': 'warning',
143
+ 'completed': 'success',
144
+ 'failed': 'danger',
145
+ 'cancelled': 'secondary'
146
+ }
147
+ color = colors.get(obj.status, 'secondary')
148
+ return format_html(
149
+ '<span class="badge badge-{}">{}</span>',
150
+ color, obj.get_status_display()
151
+ )
152
+
153
+ @display(description="Sites", ordering="sites_count")
154
+ def sites_count(self, obj):
155
+ """Display affected sites count."""
156
+ count = obj.sites.count()
157
+ if count > 0:
158
+ return format_html(
159
+ '<a href="{}?maintenance_events__id__exact={}" class="text-decoration-none">{} sites</a>',
160
+ reverse('admin:django_cfg_maintenance_cloudflaresite_changelist'),
161
+ obj.id, count
162
+ )
163
+ return "No sites"
164
+
165
+ @display(description="Duration")
166
+ def duration_display(self, obj):
167
+ """Display event duration."""
168
+ if obj.actual_start and obj.actual_end:
169
+ duration = obj.actual_end - obj.actual_start
170
+ hours = duration.total_seconds() / 3600
171
+ if hours < 1:
172
+ return f"{int(duration.total_seconds() / 60)}m"
173
+ return f"{hours:.1f}h"
174
+ elif obj.scheduled_start and obj.scheduled_end:
175
+ duration = obj.scheduled_end - obj.scheduled_start
176
+ hours = duration.total_seconds() / 3600
177
+ if hours < 1:
178
+ return f"{int(duration.total_seconds() / 60)}m (planned)"
179
+ return f"{hours:.1f}h (planned)"
180
+ return "-"
181
+
182
+ @display(description="Scheduled Start", ordering="scheduled_start")
183
+ def scheduled_start_display(self, obj):
184
+ """Display scheduled start time."""
185
+ if not obj.scheduled_start:
186
+ return "-"
187
+ return obj.scheduled_start.strftime("%Y-%m-%d %H:%M")
188
+
189
+ @display(description="Created", ordering="created_at")
190
+ def created_at_display(self, obj):
191
+ """Display creation time."""
192
+ return obj.created_at.strftime("%Y-%m-%d %H:%M")
193
+
194
+ def get_queryset(self, request):
195
+ """Optimize queryset with annotations."""
196
+ return super().get_queryset(request).select_related(
197
+ 'initiated_by', 'completed_by'
198
+ ).prefetch_related(
199
+ 'sites'
200
+ ).annotate(
201
+ sites_count=Count('sites')
202
+ )
203
+
204
+ @action(
205
+ description="🚀 Start Maintenance",
206
+ icon="play_arrow",
207
+ variant=ActionVariant.WARNING
208
+ )
209
+ def start_maintenance(self, request, object_id):
210
+ """Start maintenance event."""
211
+ try:
212
+ event = MaintenanceEvent.objects.get(id=object_id)
213
+
214
+ if event.status != 'scheduled':
215
+ messages.error(request, "Only scheduled events can be started")
216
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
217
+
218
+ # Start maintenance logic here
219
+ event.status = 'in_progress'
220
+ event.save()
221
+
222
+ messages.success(
223
+ request,
224
+ f"Maintenance event '{event.title}' has been started"
225
+ )
226
+
227
+ except Exception as e:
228
+ messages.error(request, f"Failed to start maintenance: {str(e)}")
229
+
230
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
231
+
232
+ @action(
233
+ description="✅ Complete Maintenance",
234
+ icon="check_circle",
235
+ variant=ActionVariant.SUCCESS
236
+ )
237
+ def complete_maintenance(self, request, object_id):
238
+ """Complete maintenance event."""
239
+ try:
240
+ event = MaintenanceEvent.objects.get(id=object_id)
241
+
242
+ if event.status != 'in_progress':
243
+ messages.error(request, "Only in-progress events can be completed")
244
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
245
+
246
+ # Complete maintenance logic here
247
+ event.status = 'completed'
248
+ event.save()
249
+
250
+ messages.success(
251
+ request,
252
+ f"Maintenance event '{event.title}' has been completed"
253
+ )
254
+
255
+ except Exception as e:
256
+ messages.error(request, f"Failed to complete maintenance: {str(e)}")
257
+
258
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
259
+
260
+ @action(
261
+ description="🚫 Cancel Maintenance",
262
+ icon="cancel",
263
+ variant=ActionVariant.DANGER
264
+ )
265
+ def cancel_maintenance(self, request, object_id):
266
+ """Cancel maintenance event."""
267
+ try:
268
+ event = MaintenanceEvent.objects.get(id=object_id)
269
+
270
+ if event.status in ['completed', 'failed']:
271
+ messages.error(request, "Cannot cancel completed or failed events")
272
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
273
+
274
+ # Cancel maintenance logic here
275
+ event.status = 'cancelled'
276
+ event.save()
277
+
278
+ messages.success(
279
+ request,
280
+ f"Maintenance event '{event.title}' has been cancelled"
281
+ )
282
+
283
+ except Exception as e:
284
+ messages.error(request, f"Failed to cancel maintenance: {str(e)}")
285
+
286
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
287
+
288
+ def save_model(self, request, obj, form, change):
289
+ """Set initiated_by to current user if not set."""
290
+ if not change:
291
+ obj.initiated_by = request.user
292
+ super().save_model(request, obj, form, change)
293
+
294
+
295
+ @admin.register(MaintenanceLog)
296
+ class MaintenanceLogAdmin(ModelAdmin):
297
+ """Admin for MaintenanceLog with Unfold styling (read-only)."""
298
+
299
+ list_display = [
300
+ "timestamp_display",
301
+ "level_badge",
302
+ "component",
303
+ "operation",
304
+ "message_preview",
305
+ "user",
306
+ "maintenance_event_link"
307
+ ]
308
+ list_display_links = ["timestamp_display"]
309
+ search_fields = ["message", "component", "operation", "user__email"]
310
+ list_filter = [
311
+ "level",
312
+ "component",
313
+ "operation",
314
+ "timestamp",
315
+ ("user", AutocompleteSelectFilter),
316
+ ("maintenance_event", AutocompleteSelectFilter),
317
+ ]
318
+ ordering = ["-timestamp"]
319
+ readonly_fields = [
320
+ "timestamp", "level", "message", "component",
321
+ "operation", "user"
322
+ ]
323
+
324
+ def has_add_permission(self, request):
325
+ """Disable manual log creation."""
326
+ return False
327
+
328
+ def has_change_permission(self, request, obj=None):
329
+ """Make logs read-only."""
330
+ return False
331
+
332
+ def has_delete_permission(self, request, obj=None):
333
+ """Disable log deletion."""
334
+ return False
335
+
336
+ @display(description="Time", ordering="timestamp")
337
+ def timestamp_display(self, obj):
338
+ """Display timestamp."""
339
+ return obj.timestamp.strftime("%Y-%m-%d %H:%M:%S")
340
+
341
+ @display(description="Level", ordering="level")
342
+ def level_badge(self, obj):
343
+ """Display log level with colored badge."""
344
+ colors = {
345
+ 'DEBUG': 'secondary',
346
+ 'INFO': 'info',
347
+ 'WARNING': 'warning',
348
+ 'ERROR': 'danger',
349
+ 'CRITICAL': 'danger'
350
+ }
351
+ color = colors.get(obj.level, 'secondary')
352
+ return format_html(
353
+ '<span class="badge badge-{}">{}</span>',
354
+ color, obj.level
355
+ )
356
+
357
+ @display(description="Message")
358
+ def message_preview(self, obj):
359
+ """Display truncated message."""
360
+ if len(obj.message) > 100:
361
+ return obj.message[:100] + "..."
362
+ return obj.message
363
+
364
+ @display(description="Event")
365
+ def maintenance_event_link(self, obj):
366
+ """Display link to maintenance event."""
367
+ if obj.maintenance_event:
368
+ return format_html(
369
+ '<a href="{}" class="text-decoration-none">{}</a>',
370
+ reverse('admin:django_cfg_maintenance_maintenanceevent_change',
371
+ args=[obj.maintenance_event.id]),
372
+ obj.maintenance_event.title
373
+ )
374
+ return "-"
@@ -0,0 +1,215 @@
1
+ """
2
+ Admin interface for MonitoringTarget model with Unfold features.
3
+ """
4
+
5
+ from django.contrib import admin, messages
6
+ from django.utils.html import format_html
7
+ from django.urls import reverse
8
+ from django.shortcuts import redirect
9
+ from unfold.admin import ModelAdmin
10
+ from unfold.decorators import display, action
11
+ from unfold.enums import ActionVariant
12
+ from unfold.contrib.filters.admin import AutocompleteSelectFilter
13
+ from import_export.admin import ImportExportModelAdmin
14
+
15
+ from ..models import MonitoringTarget
16
+ from ..services import MaintenanceManager
17
+ from django.contrib.auth import get_user_model
18
+
19
+ User = get_user_model()
20
+
21
+
22
+ @admin.register(MonitoringTarget)
23
+ class MonitoringTargetAdmin(ModelAdmin, ImportExportModelAdmin):
24
+ """Admin for MonitoringTarget with Unfold styling."""
25
+
26
+ list_display = [
27
+ "site_with_icon",
28
+ "url_display",
29
+ "status_badge",
30
+ "last_check_display",
31
+ "check_interval_display",
32
+ "failure_count_display"
33
+ ]
34
+ list_display_links = ["site_with_icon", "url_display"]
35
+ search_fields = ["site__name", "site__domain", "check_url"]
36
+ list_filter = [
37
+ "status",
38
+ "check_interval",
39
+ "failure_threshold",
40
+ "last_check_at",
41
+ ("site", AutocompleteSelectFilter),
42
+ ]
43
+ ordering = ["-last_check_at"]
44
+ readonly_fields = [
45
+ "last_check_at", "status", "last_check_success",
46
+ "created_at", "updated_at"
47
+ ]
48
+
49
+ fieldsets = (
50
+ ("Target Configuration", {
51
+ "fields": ("site", "check_url", "status")
52
+ }),
53
+ ("Monitoring Settings", {
54
+ "fields": ("check_interval", "timeout", "expected_status_codes", "failure_threshold", "recovery_threshold")
55
+ }),
56
+ ("Response Validation", {
57
+ "fields": ("expected_content", "expected_response_time_ms"),
58
+ "classes": ("collapse",)
59
+ }),
60
+ ("Status Information", {
61
+ "fields": ("consecutive_failures", "consecutive_successes", "last_check_at", "last_check_success"),
62
+ "classes": ("collapse",)
63
+ }),
64
+ ("Timestamps", {
65
+ "fields": ("created_at", "updated_at"),
66
+ "classes": ("collapse",)
67
+ })
68
+ )
69
+
70
+ # Unfold actions
71
+ actions_detail = ["run_health_check", "enable_monitoring", "disable_monitoring"]
72
+
73
+ @display(description="Site", ordering="site__name")
74
+ def site_with_icon(self, obj):
75
+ """Display site name with icon."""
76
+ return format_html('🌐 {}', obj.site.name)
77
+
78
+ @display(description="URL", ordering="check_url")
79
+ def url_display(self, obj):
80
+ """Display monitoring URL."""
81
+ url = obj.check_url
82
+ if len(url) > 50:
83
+ url = url[:47] + "..."
84
+ return format_html('<code>{}</code>', url)
85
+
86
+ @display(
87
+ description="Status",
88
+ ordering="status",
89
+ label={
90
+ 'active': 'success',
91
+ 'paused': 'warning',
92
+ 'disabled': 'secondary',
93
+ 'error': 'danger'
94
+ }
95
+ )
96
+ def status_badge(self, obj):
97
+ """Display monitoring status."""
98
+ return obj.status, obj.get_status_display()
99
+
100
+ @display(description="Last Check", ordering="last_check_at")
101
+ def last_check_display(self, obj):
102
+ """Display last check time."""
103
+ if obj.last_check_at:
104
+ return obj.last_check_at.strftime("%Y-%m-%d %H:%M")
105
+ return "Never"
106
+
107
+ @display(description="Check Interval", ordering="check_interval")
108
+ def check_interval_display(self, obj):
109
+ """Display check interval in human readable format."""
110
+ seconds = obj.check_interval
111
+ if seconds >= 3600:
112
+ return f"{seconds // 3600}h {(seconds % 3600) // 60}m"
113
+ elif seconds >= 60:
114
+ return f"{seconds // 60}m"
115
+ return f"{seconds}s"
116
+
117
+ @display(description="Failures", ordering="consecutive_failures")
118
+ def failure_count_display(self, obj):
119
+ """Display failure count with threshold."""
120
+ return f"{obj.consecutive_failures}/{obj.failure_threshold}"
121
+
122
+ def get_queryset(self, request):
123
+ """Filter queryset based on user permissions."""
124
+ qs = super().get_queryset(request)
125
+ if request.user.is_superuser:
126
+ return qs
127
+ return qs.filter(site__owner=request.user)
128
+
129
+ def save_model(self, request, obj, form, change):
130
+ """Set owner to current user if not set."""
131
+ if not change and not obj.site.owner:
132
+ obj.site.owner = request.user
133
+ super().save_model(request, obj, form, change)
134
+
135
+ @action(
136
+ description="🩺 Run Health Check",
137
+ icon="health_and_safety",
138
+ variant=ActionVariant.INFO
139
+ )
140
+ def run_health_check(self, request, object_id):
141
+ """Run immediate health check."""
142
+ try:
143
+ target = self.get_object(request, object_id)
144
+ if not target:
145
+ messages.error(request, "Monitoring target not found.")
146
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
147
+
148
+ # TODO: Implement actual health check logic
149
+ messages.success(
150
+ request,
151
+ f"Health check queued for {target.check_url}."
152
+ )
153
+
154
+ except Exception as e:
155
+ import logging
156
+ logger = logging.getLogger(__name__)
157
+ logger.exception(f"Health check failed for target {object_id}")
158
+ messages.error(
159
+ request,
160
+ f"Health check failed: {e}"
161
+ )
162
+
163
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
164
+
165
+ @action(
166
+ description="▶️ Enable Monitoring",
167
+ icon="play_arrow",
168
+ variant=ActionVariant.SUCCESS
169
+ )
170
+ def enable_monitoring(self, request, object_id):
171
+ """Enable monitoring for target."""
172
+ try:
173
+ target = self.get_object(request, object_id)
174
+ if not target:
175
+ messages.error(request, "Monitoring target not found.")
176
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
177
+
178
+ target.status = MonitoringTarget.Status.ACTIVE
179
+ target.save(update_fields=['status'])
180
+
181
+ messages.success(
182
+ request,
183
+ f"Monitoring enabled for {target.check_url}."
184
+ )
185
+
186
+ except Exception as e:
187
+ messages.error(request, f"Failed to enable monitoring: {e}")
188
+
189
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
190
+
191
+ @action(
192
+ description="⏸️ Disable Monitoring",
193
+ icon="pause",
194
+ variant=ActionVariant.WARNING
195
+ )
196
+ def disable_monitoring(self, request, object_id):
197
+ """Disable monitoring for target."""
198
+ try:
199
+ target = self.get_object(request, object_id)
200
+ if not target:
201
+ messages.error(request, "Monitoring target not found.")
202
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))
203
+
204
+ target.status = MonitoringTarget.Status.DISABLED
205
+ target.save(update_fields=['status'])
206
+
207
+ messages.success(
208
+ request,
209
+ f"Monitoring disabled for {target.check_url}."
210
+ )
211
+
212
+ except Exception as e:
213
+ messages.error(request, f"Failed to disable monitoring: {e}")
214
+
215
+ return redirect(request.META.get('HTTP_REFERER', '/admin/'))