django-cfg 1.2.7__py3-none-any.whl → 1.2.8__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 (45) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/urls.py +2 -2
  3. django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
  4. django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
  5. django_cfg/modules/django_unfold/callbacks/base.py +98 -0
  6. django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
  7. django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
  8. django_cfg/modules/django_unfold/callbacks/main.py +191 -0
  9. django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
  10. django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
  11. django_cfg/modules/django_unfold/callbacks/system.py +180 -0
  12. django_cfg/modules/django_unfold/callbacks/users.py +65 -0
  13. django_cfg/modules/django_unfold/models/config.py +10 -3
  14. django_cfg/modules/django_unfold/tailwind.py +68 -0
  15. django_cfg/templates/admin/components/action_grid.html +49 -0
  16. django_cfg/templates/admin/components/card.html +50 -0
  17. django_cfg/templates/admin/components/data_table.html +67 -0
  18. django_cfg/templates/admin/components/metric_card.html +39 -0
  19. django_cfg/templates/admin/components/modal.html +58 -0
  20. django_cfg/templates/admin/components/progress_bar.html +25 -0
  21. django_cfg/templates/admin/components/section_header.html +26 -0
  22. django_cfg/templates/admin/components/stat_item.html +32 -0
  23. django_cfg/templates/admin/components/stats_grid.html +72 -0
  24. django_cfg/templates/admin/components/status_badge.html +28 -0
  25. django_cfg/templates/admin/components/user_avatar.html +27 -0
  26. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
  27. django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
  28. django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
  29. django_cfg/templates/admin/snippets/components/django_commands.html +18 -18
  30. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
  31. django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
  32. django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
  33. django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
  34. django_cfg/templates/admin/snippets/components/system_health.html +13 -63
  35. django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
  36. django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
  37. django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
  38. django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
  39. django_cfg/templatetags/django_cfg.py +2 -1
  40. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
  41. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/RECORD +44 -24
  42. django_cfg/modules/django_unfold/callbacks.py +0 -795
  43. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
  44. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
  45. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,795 +0,0 @@
1
- """
2
- Base Unfold Dashboard Callbacks for Django CFG Toolkit
3
-
4
- Provides comprehensive system monitoring and dashboard functionality.
5
- Following CRITICAL_REQUIREMENTS.md - NO raw dicts, ALL type-safe.
6
- """
7
-
8
- import json
9
- import logging
10
- import os
11
- import shutil
12
- from typing import Dict, Any, List
13
- from datetime import timedelta
14
-
15
- from django.db.models import Count
16
- from django.db.models.functions import TruncDate
17
- from django.utils import timezone
18
- from django.conf import settings
19
- from django.urls import get_resolver
20
- from django.db import connection
21
- from django.core.cache import cache
22
- from django.apps import apps
23
-
24
- from ..base import BaseCfgModule
25
- from .models.dashboard import DashboardData, StatCard, SystemHealthItem, QuickAction
26
- from .icons import Icons
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
-
31
- def get_available_commands():
32
- """Get all available Django management commands."""
33
- from django.core.management import get_commands
34
- from django.core.management.base import BaseCommand
35
- import importlib
36
-
37
- commands_dict = get_commands()
38
- commands_list = []
39
-
40
- for command_name, app_name in commands_dict.items():
41
- try:
42
- # Try to get command description
43
- if app_name == 'django_cfg':
44
- module_path = f'django_cfg.management.commands.{command_name}'
45
- else:
46
- module_path = f'{app_name}.management.commands.{command_name}'
47
-
48
- try:
49
- command_module = importlib.import_module(module_path)
50
- if hasattr(command_module, 'Command'):
51
- command_class = command_module.Command
52
- description = getattr(command_class, 'help', f'{command_name} command')
53
- else:
54
- description = f'{command_name} command'
55
- except ImportError:
56
- description = f'{command_name} command'
57
-
58
- commands_list.append({
59
- 'name': command_name,
60
- 'app': app_name,
61
- 'description': description,
62
- 'is_core': app_name.startswith('django.'),
63
- 'is_custom': app_name == 'django_cfg',
64
- })
65
- except Exception:
66
- # Skip problematic commands
67
- continue
68
-
69
- return commands_list
70
-
71
-
72
- def get_commands_by_category():
73
- """Get commands categorized by type."""
74
- commands = get_available_commands()
75
-
76
- categorized = {
77
- 'django_cfg': [],
78
- 'django_core': [],
79
- 'third_party': [],
80
- 'project': [],
81
- }
82
-
83
- for cmd in commands:
84
- if cmd['app'] == 'django_cfg':
85
- categorized['django_cfg'].append(cmd)
86
- elif cmd['app'].startswith('django.'):
87
- categorized['django_core'].append(cmd)
88
- elif cmd['app'].startswith(('src.', 'api.', 'accounts.')):
89
- categorized['project'].append(cmd)
90
- else:
91
- categorized['third_party'].append(cmd)
92
-
93
- return categorized
94
-
95
-
96
- def get_user_admin_urls():
97
- """Get admin URLs for user model."""
98
- try:
99
- from django.contrib.auth import get_user_model
100
- User = get_user_model()
101
-
102
- app_label = User._meta.app_label
103
- model_name = User._meta.model_name
104
-
105
- return {
106
- 'changelist': f'admin:{app_label}_{model_name}_changelist',
107
- 'add': f'admin:{app_label}_{model_name}_add',
108
- 'change': f'admin:{app_label}_{model_name}_change/{{id}}/',
109
- 'delete': f'admin:{app_label}_{model_name}_delete/{{id}}/',
110
- 'view': f'admin:{app_label}_{model_name}_view/{{id}}/',
111
- }
112
- except Exception:
113
- # Universal fallback - return admin index for all actions
114
- return {
115
- 'changelist': 'admin:index',
116
- 'add': 'admin:index',
117
- 'change': 'admin:index',
118
- 'delete': 'admin:index',
119
- 'view': 'admin:index',
120
- }
121
-
122
-
123
- class UnfoldCallbacks(BaseCfgModule):
124
- """
125
- Base Unfold dashboard callbacks with full system monitoring.
126
-
127
- Provides comprehensive dashboard functionality using Pydantic models
128
- for type safety and data validation.
129
- """
130
-
131
- def _get_user_model(self):
132
- """Get the user model safely."""
133
- from django.contrib.auth import get_user_model
134
- return get_user_model()
135
-
136
- def get_user_statistics(self) -> List[StatCard]:
137
- """Get user-related statistics as Pydantic models."""
138
- try:
139
- User = self._get_user_model()
140
-
141
- total_users = User.objects.count()
142
- active_users = User.objects.filter(is_active=True).count()
143
- new_users_7d = User.objects.filter(
144
- date_joined__gte=timezone.now() - timedelta(days=7)
145
- ).count()
146
- staff_users = User.objects.filter(is_staff=True).count()
147
-
148
- return [
149
- StatCard(
150
- title="Total Users",
151
- value=f"{total_users:,}",
152
- icon=Icons.PEOPLE,
153
- change=f"+{new_users_7d}" if new_users_7d > 0 else None,
154
- change_type="positive" if new_users_7d > 0 else "neutral",
155
- description="Registered users",
156
- ),
157
- StatCard(
158
- title="Active Users",
159
- value=f"{active_users:,}",
160
- icon=Icons.PERSON,
161
- change=(
162
- f"{(active_users/total_users*100):.1f}%"
163
- if total_users > 0
164
- else "0%"
165
- ),
166
- change_type=(
167
- "positive" if active_users > total_users * 0.7 else "neutral"
168
- ),
169
- description="Currently active",
170
- ),
171
- StatCard(
172
- title="New This Week",
173
- value=f"{new_users_7d:,}",
174
- icon=Icons.PERSON_ADD,
175
- change_type="positive" if new_users_7d > 0 else "neutral",
176
- description="Last 7 days",
177
- ),
178
- StatCard(
179
- title="Staff Members",
180
- value=f"{staff_users:,}",
181
- icon=Icons.ADMIN_PANEL_SETTINGS,
182
- change=(
183
- f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
184
- ),
185
- change_type="neutral",
186
- description="Administrative access",
187
- ),
188
- ]
189
- except Exception as e:
190
- logger.error(f"Error getting user statistics: {e}")
191
- return [
192
- StatCard(
193
- title="Users",
194
- value="N/A",
195
- icon=Icons.PEOPLE,
196
- description="Data unavailable",
197
- )
198
- ]
199
-
200
- def get_system_health(self) -> List[SystemHealthItem]:
201
- """Get system health status as Pydantic models."""
202
- health_items = []
203
-
204
- # Database health
205
- try:
206
- with connection.cursor() as cursor:
207
- cursor.execute("SELECT 1")
208
- health_items.append(
209
- SystemHealthItem(
210
- component="database",
211
- status="healthy",
212
- description="Connection successful",
213
- last_check=timezone.now().strftime("%H:%M:%S"),
214
- health_percentage=95,
215
- )
216
- )
217
- except Exception as e:
218
- health_items.append(
219
- SystemHealthItem(
220
- component="database",
221
- status="error",
222
- description=f"Connection failed: {str(e)[:50]}",
223
- last_check=timezone.now().strftime("%H:%M:%S"),
224
- health_percentage=0,
225
- )
226
- )
227
-
228
- # Cache health
229
- try:
230
- cache.set("health_check", "ok", 10)
231
- if cache.get("health_check") == "ok":
232
- health_items.append(
233
- SystemHealthItem(
234
- component="cache",
235
- status="healthy",
236
- description="Cache operational",
237
- last_check=timezone.now().strftime("%H:%M:%S"),
238
- health_percentage=90,
239
- )
240
- )
241
- else:
242
- health_items.append(
243
- SystemHealthItem(
244
- component="cache",
245
- status="warning",
246
- description="Cache not responding",
247
- last_check=timezone.now().strftime("%H:%M:%S"),
248
- health_percentage=50,
249
- )
250
- )
251
- except Exception:
252
- health_items.append(
253
- SystemHealthItem(
254
- component="cache",
255
- status="unknown",
256
- description="Cache not configured",
257
- last_check=timezone.now().strftime("%H:%M:%S"),
258
- health_percentage=0,
259
- )
260
- )
261
-
262
- # Storage health
263
- try:
264
- total, used, free = shutil.disk_usage("/")
265
- usage_percentage = (used / total) * 100
266
- free_percentage = 100 - usage_percentage
267
-
268
- if free_percentage > 20:
269
- status = "healthy"
270
- desc = f"Disk space: {free_percentage:.1f}% free"
271
- elif free_percentage > 10:
272
- status = "warning"
273
- desc = f"Low disk space: {free_percentage:.1f}% free"
274
- else:
275
- status = "error"
276
- desc = f"Critical disk space: {free_percentage:.1f}% free"
277
-
278
- health_items.append(
279
- SystemHealthItem(
280
- component="storage",
281
- status=status,
282
- description=desc,
283
- last_check=timezone.now().strftime("%H:%M:%S"),
284
- health_percentage=int(free_percentage),
285
- )
286
- )
287
- except Exception as e:
288
- health_items.append(
289
- SystemHealthItem(
290
- component="storage",
291
- status="error",
292
- description="Storage check failed",
293
- last_check=timezone.now().strftime("%H:%M:%S"),
294
- health_percentage=0,
295
- )
296
- )
297
-
298
- # API health
299
- health_items.append(
300
- SystemHealthItem(
301
- component="api",
302
- status="healthy",
303
- description="API server running",
304
- last_check=timezone.now().strftime("%H:%M:%S"),
305
- health_percentage=100,
306
- )
307
- )
308
-
309
- return health_items
310
-
311
- def get_quick_actions(self) -> List[QuickAction]:
312
- """Get quick action buttons as Pydantic models."""
313
- # Get user admin URLs dynamically based on AUTH_USER_MODEL
314
- user_admin_urls = get_user_admin_urls()
315
-
316
- actions = [
317
- QuickAction(
318
- title="Add User",
319
- description="Create new user account",
320
- icon=Icons.PERSON_ADD,
321
- link=user_admin_urls["add"],
322
- color="primary",
323
- category="admin",
324
- ),
325
- QuickAction(
326
- title="Support Tickets",
327
- description="Manage support tickets",
328
- icon=Icons.SUPPORT_AGENT,
329
- link="admin:django_cfg_support_ticket_changelist",
330
- color="primary",
331
- category="support",
332
- ),
333
- QuickAction(
334
- title="Health Check",
335
- description="System health status",
336
- icon=Icons.HEALTH_AND_SAFETY,
337
- link="/cfg/health/",
338
- color="success",
339
- category="system",
340
- ),
341
- ]
342
-
343
- # # Automatically add Constance settings if configured
344
- # if self._is_constance_configured():
345
- # actions.append(
346
- # QuickAction(
347
- # title="System Settings",
348
- # description="Configure dynamic settings",
349
- # icon="settings",
350
- # link="/admin/constance/config/",
351
- # color="warning",
352
- # category="admin",
353
- # )
354
- # )
355
-
356
- return actions
357
-
358
- # def _is_constance_configured(self) -> bool:
359
- # """Check if Constance is configured."""
360
- # try:
361
- # from django.conf import settings
362
- # return bool(getattr(settings, 'CONSTANCE_CONFIG', {}))
363
- # except Exception:
364
- # return False
365
-
366
- def get_support_statistics(self) -> List[StatCard]:
367
- """Get support ticket statistics as Pydantic models."""
368
- try:
369
- # Check if support is enabled
370
- if not self.is_support_enabled():
371
- return []
372
-
373
- from django_cfg.apps.support.models import Ticket, Message
374
-
375
- total_tickets = Ticket.objects.count()
376
- open_tickets = Ticket.objects.filter(status='open').count()
377
- resolved_tickets = Ticket.objects.filter(status='resolved').count()
378
- new_tickets_7d = Ticket.objects.filter(
379
- created_at__gte=timezone.now() - timedelta(days=7)
380
- ).count()
381
-
382
- return [
383
- StatCard(
384
- title="Total Tickets",
385
- value=f"{total_tickets:,}",
386
- icon=Icons.SUPPORT_AGENT,
387
- change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
388
- change_type="positive" if new_tickets_7d > 0 else "neutral",
389
- description="All support tickets",
390
- ),
391
- StatCard(
392
- title="Open Tickets",
393
- value=f"{open_tickets:,}",
394
- icon=Icons.PENDING,
395
- change=(
396
- f"{(open_tickets/total_tickets*100):.1f}%"
397
- if total_tickets > 0
398
- else "0%"
399
- ),
400
- change_type=(
401
- "negative" if open_tickets > total_tickets * 0.3
402
- else "positive" if open_tickets == 0
403
- else "neutral"
404
- ),
405
- description="Awaiting response",
406
- ),
407
- StatCard(
408
- title="Resolved",
409
- value=f"{resolved_tickets:,}",
410
- icon=Icons.CHECK_CIRCLE,
411
- change=(
412
- f"{(resolved_tickets/total_tickets*100):.1f}%"
413
- if total_tickets > 0
414
- else "0%"
415
- ),
416
- change_type="positive",
417
- description="Successfully resolved",
418
- ),
419
- StatCard(
420
- title="New This Week",
421
- value=f"{new_tickets_7d:,}",
422
- icon=Icons.NEW_RELEASES,
423
- change_type="positive" if new_tickets_7d > 0 else "neutral",
424
- description="Last 7 days",
425
- ),
426
- ]
427
- except Exception as e:
428
- logger.error(f"Error getting support statistics: {e}")
429
- return [
430
- StatCard(
431
- title="Support",
432
- value="N/A",
433
- icon=Icons.SUPPORT_AGENT,
434
- description="Data unavailable",
435
- )
436
- ]
437
-
438
- def get_django_commands(self) -> Dict[str, Any]:
439
- """Get Django management commands information."""
440
- try:
441
- commands = get_available_commands()
442
- categorized = get_commands_by_category()
443
-
444
- return {
445
- "commands": commands,
446
- "categorized": categorized,
447
- "total_commands": len(commands),
448
- "categories": list(categorized.keys()),
449
- "core_commands": len([cmd for cmd in commands if cmd['is_core']]),
450
- "custom_commands": len([cmd for cmd in commands if cmd['is_custom']]),
451
- }
452
- except Exception as e:
453
- logger.error(f"Error getting Django commands: {e}")
454
- # Return safe fallback to prevent dashboard from breaking
455
- return {
456
- "commands": [],
457
- "categorized": {},
458
- "total_commands": 0,
459
- "categories": [],
460
- "core_commands": 0,
461
- "custom_commands": 0,
462
- }
463
-
464
- def get_revolution_zones_data(self) -> tuple[list, dict]:
465
- """Get Django Revolution zones data."""
466
- try:
467
- # Try to get revolution config from Django settings
468
- revolution_config = getattr(settings, "DJANGO_REVOLUTION", {})
469
- zones = revolution_config.get("zones", {})
470
- api_prefix = revolution_config.get("api_prefix", "apix")
471
-
472
- zones_data = []
473
- total_apps = 0
474
- total_endpoints = 0
475
-
476
- for zone_name, zone_config in zones.items():
477
- # Handle both dict and object access
478
- if isinstance(zone_config, dict):
479
- title = zone_config.get("title", zone_name.title())
480
- description = zone_config.get("description", f"{zone_name} zone")
481
- apps = zone_config.get("apps", [])
482
- public = zone_config.get("public", False)
483
- auth_required = zone_config.get("auth_required", True)
484
- else:
485
- # Handle object access (for ZoneConfig instances)
486
- title = getattr(zone_config, "title", zone_name.title())
487
- description = getattr(zone_config, "description", f"{zone_name} zone")
488
- apps = getattr(zone_config, "apps", [])
489
- public = getattr(zone_config, "public", False)
490
- auth_required = getattr(zone_config, "auth_required", True)
491
-
492
- # Count actual endpoints by checking URL patterns (simplified estimate)
493
- endpoint_count = len(apps) * 3 # Conservative estimate
494
-
495
- zones_data.append({
496
- "name": zone_name,
497
- "title": title,
498
- "description": description,
499
- "app_count": len(apps),
500
- "endpoint_count": endpoint_count,
501
- "status": "active",
502
- "public": public,
503
- "auth_required": auth_required,
504
- "schema_url": f"/schema/{zone_name}/schema/",
505
- "swagger_url": f"/schema/{zone_name}/schema/swagger/",
506
- "redoc_url": f"/schema/{zone_name}/redoc/",
507
- "api_url": f"/{api_prefix}/{zone_name}/",
508
- })
509
-
510
- total_apps += len(apps)
511
- total_endpoints += endpoint_count
512
-
513
- return zones_data, {
514
- "total_apps": total_apps,
515
- "total_endpoints": total_endpoints,
516
- "total_zones": len(zones),
517
- }
518
- except Exception as e:
519
- logger.error(f"Error getting revolution zones: {e}")
520
- return [], {
521
- "total_apps": 0,
522
- "total_endpoints": 0,
523
- "total_zones": 0,
524
- }
525
-
526
- def get_recent_users(self) -> List[Dict[str, Any]]:
527
- """Get recent users data for template."""
528
- try:
529
- User = self._get_user_model()
530
- recent_users = User.objects.select_related().order_by("-date_joined")[:10]
531
-
532
- # Get admin URLs for user model
533
- user_admin_urls = get_user_admin_urls()
534
-
535
- return [
536
- {
537
- "id": user.id,
538
- "username": user.username,
539
- "email": user.email or "No email",
540
- "date_joined": (
541
- user.date_joined.strftime("%Y-%m-%d")
542
- if user.date_joined
543
- else "Unknown"
544
- ),
545
- "is_active": user.is_active,
546
- "is_staff": user.is_staff,
547
- "is_superuser": user.is_superuser,
548
- "last_login": user.last_login,
549
- "admin_urls": {
550
- "change": (
551
- user_admin_urls["change"].format(id=user.id)
552
- if user.id
553
- else None
554
- ),
555
- "view": (
556
- user_admin_urls["view"].format(id=user.id) if user.id else None
557
- ),
558
- },
559
- }
560
- for user in recent_users
561
- ]
562
- except Exception as e:
563
- logger.error(f"Error getting recent users: {e}")
564
- return []
565
-
566
- def get_app_statistics(self) -> Dict[str, Any]:
567
- """Get statistics for all apps and their models."""
568
- stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
569
-
570
- # Get all installed apps
571
- for app_config in apps.get_app_configs():
572
- app_label = app_config.label
573
-
574
- # Skip system apps
575
- if app_label in ["admin", "contenttypes", "sessions", "auth"]:
576
- continue
577
-
578
- app_stats = self._get_app_stats(app_label)
579
- if app_stats:
580
- stats["apps"][app_label] = app_stats
581
- stats["total_records"] += app_stats.get("total_records", 0)
582
- stats["total_models"] += app_stats.get("model_count", 0)
583
- stats["total_apps"] += 1
584
-
585
- return stats
586
-
587
- def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
588
- """Get statistics for a specific app."""
589
- try:
590
- app_config = apps.get_app_config(app_label)
591
- # Convert generator to list to avoid len() error
592
- models_list = list(app_config.get_models())
593
-
594
- if not models_list:
595
- return None
596
-
597
- app_stats = {
598
- "name": app_config.verbose_name or app_label.title(),
599
- "models": {},
600
- "total_records": 0,
601
- "model_count": len(models_list),
602
- }
603
-
604
- for model in models_list:
605
- try:
606
- # Get model statistics
607
- model_stats = self._get_model_stats(model)
608
- if model_stats:
609
- app_stats["models"][model._meta.model_name] = model_stats
610
- app_stats["total_records"] += model_stats.get("count", 0)
611
- except Exception:
612
- continue
613
-
614
- return app_stats
615
-
616
- except Exception:
617
- return None
618
-
619
- def _get_model_stats(self, model) -> Dict[str, Any]:
620
- """Get statistics for a specific model."""
621
- try:
622
- # Get basic model info
623
- model_stats = {
624
- "name": model._meta.verbose_name_plural
625
- or model._meta.verbose_name
626
- or model._meta.model_name,
627
- "count": model.objects.count(),
628
- "fields_count": len(model._meta.fields),
629
- "admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
630
- }
631
-
632
- return model_stats
633
-
634
- except Exception:
635
- return None
636
-
637
- def get_system_metrics(self) -> Dict[str, Any]:
638
- """Get system metrics for dashboard."""
639
- metrics = {}
640
-
641
- # Database metrics
642
- try:
643
- with connection.cursor() as cursor:
644
- cursor.execute("SELECT 1")
645
- metrics["database"] = {
646
- "status": "healthy",
647
- "type": "PostgreSQL",
648
- "health_percentage": 95,
649
- "description": "Connection successful",
650
- }
651
- except Exception as e:
652
- metrics["database"] = {
653
- "status": "error",
654
- "type": "PostgreSQL",
655
- "health_percentage": 0,
656
- "description": f"Connection failed: {str(e)}",
657
- }
658
-
659
- # Cache metrics
660
- try:
661
- cache.set("health_check", "ok", 10)
662
- cache_result = cache.get("health_check")
663
- if cache_result == "ok":
664
- metrics["cache"] = {
665
- "status": "healthy",
666
- "type": "Memory Cache",
667
- "health_percentage": 90,
668
- "description": "Cache working properly",
669
- }
670
- else:
671
- metrics["cache"] = {
672
- "status": "warning",
673
- "type": "Memory Cache",
674
- "health_percentage": 50,
675
- "description": "Cache response delayed",
676
- }
677
- except Exception as e:
678
- metrics["cache"] = {
679
- "status": "error",
680
- "type": "Memory Cache",
681
- "health_percentage": 0,
682
- "description": f"Cache error: {str(e)}",
683
- }
684
-
685
- return metrics
686
-
687
- def main_dashboard_callback(self, request, context: Dict[str, Any]) -> Dict[str, Any]:
688
- """
689
- Main dashboard callback function with comprehensive system data.
690
-
691
- Returns all dashboard data as Pydantic models for type safety.
692
- """
693
- try:
694
- # Get dashboard data using Pydantic models
695
- user_stats = self.get_user_statistics()
696
- support_stats = self.get_support_statistics()
697
- system_health = self.get_system_health()
698
- quick_actions = self.get_quick_actions()
699
-
700
- # Combine all stat cards
701
- all_stats = user_stats + support_stats
702
-
703
- dashboard_data = DashboardData(
704
- stat_cards=all_stats,
705
- system_health=system_health,
706
- quick_actions=quick_actions,
707
- last_updated=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
708
- environment=getattr(settings, "ENVIRONMENT", "development"),
709
- )
710
-
711
- # Convert to template context (using to_dict for Unfold compatibility)
712
- cards_data = [card.to_dict() for card in dashboard_data.stat_cards]
713
-
714
- context.update({
715
- # Statistics cards
716
- "cards": cards_data,
717
- "user_stats": [card.to_dict() for card in user_stats],
718
- "support_stats": [card.to_dict() for card in support_stats],
719
- # System health (convert to dict for template)
720
- "system_health": {
721
- item.component + "_status": item.status
722
- for item in dashboard_data.system_health
723
- },
724
- # System metrics
725
- "system_metrics": self.get_system_metrics(),
726
- # Quick actions
727
- "quick_actions": [
728
- action.model_dump() for action in dashboard_data.quick_actions
729
- ],
730
- # Additional categorized actions
731
- "admin_actions": [
732
- action.model_dump()
733
- for action in dashboard_data.quick_actions
734
- if action.category == "admin"
735
- ],
736
- "support_actions": [
737
- action.model_dump()
738
- for action in dashboard_data.quick_actions
739
- if action.category == "support"
740
- ],
741
- "system_actions": [
742
- action.model_dump()
743
- for action in dashboard_data.quick_actions
744
- if action.category == "system"
745
- ],
746
- # Revolution zones
747
- "zones_table": {
748
- "headers": [
749
- {"label": "Zone"},
750
- {"label": "Title"},
751
- {"label": "Apps"},
752
- {"label": "Endpoints"},
753
- {"label": "Status"},
754
- {"label": "Actions"},
755
- ],
756
- "rows": self.get_revolution_zones_data()[0],
757
- },
758
- # Recent users
759
- "recent_users": self.get_recent_users(),
760
- "user_admin_urls": get_user_admin_urls(),
761
- # App statistics
762
- "app_statistics": self.get_app_statistics(),
763
- # Django commands
764
- "django_commands": self.get_django_commands(),
765
- # Meta information
766
- "last_updated": dashboard_data.last_updated,
767
- "environment": dashboard_data.environment,
768
- "dashboard_title": "Django CFG Dashboard",
769
- })
770
-
771
- # logger.info(f"Final context keys: {list(context.keys())}")
772
- # logger.info(f"Cards in context: {len(context.get('cards', []))}")
773
- # logger.info("=== DJANGO_CFG DASHBOARD CALLBACK COMPLETED ===")
774
-
775
- return context
776
-
777
- except Exception as e:
778
- logger.error(f"Dashboard callback error: {e}")
779
- # Return minimal safe defaults
780
- context.update({
781
- "cards": [
782
- {
783
- "title": "System Error",
784
- "value": "N/A",
785
- "icon": "error",
786
- "color": "danger",
787
- "description": "Dashboard data unavailable"
788
- }
789
- ],
790
- "system_health": {},
791
- "quick_actions": [],
792
- "last_updated": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
793
- "error": f"Dashboard error: {str(e)}",
794
- })
795
- return context