django-cfg 1.2.15__py3-none-any.whl → 1.2.17__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/maintenance/README.md +305 -0
- django_cfg/apps/maintenance/__init__.py +27 -0
- django_cfg/apps/maintenance/admin/__init__.py +28 -0
- django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
- django_cfg/apps/maintenance/admin/events_admin.py +374 -0
- django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
- django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
- django_cfg/apps/maintenance/apps.py +105 -0
- django_cfg/apps/maintenance/management/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
- django_cfg/apps/maintenance/managers/__init__.py +20 -0
- django_cfg/apps/maintenance/managers/deployments.py +287 -0
- django_cfg/apps/maintenance/managers/events.py +374 -0
- django_cfg/apps/maintenance/managers/monitoring.py +301 -0
- django_cfg/apps/maintenance/managers/sites.py +335 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
- django_cfg/apps/maintenance/migrations/__init__.py +0 -0
- django_cfg/apps/maintenance/models/__init__.py +27 -0
- django_cfg/apps/maintenance/models/cloudflare.py +316 -0
- django_cfg/apps/maintenance/models/maintenance.py +334 -0
- django_cfg/apps/maintenance/models/monitoring.py +393 -0
- django_cfg/apps/maintenance/models/sites.py +419 -0
- django_cfg/apps/maintenance/serializers/__init__.py +60 -0
- django_cfg/apps/maintenance/serializers/actions.py +310 -0
- django_cfg/apps/maintenance/serializers/base.py +44 -0
- django_cfg/apps/maintenance/serializers/deployments.py +209 -0
- django_cfg/apps/maintenance/serializers/events.py +210 -0
- django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
- django_cfg/apps/maintenance/serializers/sites.py +213 -0
- django_cfg/apps/maintenance/services/README.md +168 -0
- django_cfg/apps/maintenance/services/__init__.py +21 -0
- django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
- django_cfg/apps/maintenance/services/dns_manager.py +497 -0
- django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
- django_cfg/apps/maintenance/services/site_sync.py +448 -0
- django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
- django_cfg/apps/maintenance/services/worker_manager.py +264 -0
- django_cfg/apps/maintenance/signals.py +38 -0
- django_cfg/apps/maintenance/urls.py +36 -0
- django_cfg/apps/maintenance/views/__init__.py +18 -0
- django_cfg/apps/maintenance/views/base.py +61 -0
- django_cfg/apps/maintenance/views/deployments.py +175 -0
- django_cfg/apps/maintenance/views/events.py +204 -0
- django_cfg/apps/maintenance/views/monitoring.py +213 -0
- django_cfg/apps/maintenance/views/sites.py +338 -0
- django_cfg/apps/urls.py +5 -1
- django_cfg/core/config.py +34 -3
- django_cfg/core/generation.py +15 -10
- django_cfg/models/cloudflare.py +316 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +1 -1
- django_cfg/modules/base.py +12 -5
- django_cfg/modules/django_unfold/dashboard.py +16 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.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/'))
|