django-cfg 1.4.58__py3-none-any.whl → 1.4.60__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (59) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/ipc/RPC_LOGGING.md +321 -0
  3. django_cfg/apps/ipc/TESTING.md +539 -0
  4. django_cfg/apps/ipc/__init__.py +12 -3
  5. django_cfg/apps/ipc/admin.py +212 -0
  6. django_cfg/apps/ipc/migrations/0001_initial.py +137 -0
  7. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  8. django_cfg/apps/ipc/models.py +221 -0
  9. django_cfg/apps/ipc/serializers/__init__.py +10 -0
  10. django_cfg/apps/ipc/serializers/serializers.py +114 -0
  11. django_cfg/apps/ipc/services/client/client.py +83 -4
  12. django_cfg/apps/ipc/services/logging.py +239 -0
  13. django_cfg/apps/ipc/services/monitor.py +5 -3
  14. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +269 -0
  15. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +259 -0
  16. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +375 -0
  17. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +22 -0
  18. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +9 -0
  19. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +9 -0
  20. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +23 -0
  21. django_cfg/apps/ipc/templates/django_cfg_ipc/components/stat_cards.html +50 -0
  22. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +47 -0
  23. django_cfg/apps/ipc/templates/django_cfg_ipc/components/tab_navigation.html +29 -0
  24. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +184 -0
  25. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +56 -0
  26. django_cfg/apps/ipc/urls.py +4 -2
  27. django_cfg/apps/ipc/views/__init__.py +7 -2
  28. django_cfg/apps/ipc/views/dashboard.py +1 -1
  29. django_cfg/apps/ipc/views/{viewsets.py → monitoring.py} +17 -11
  30. django_cfg/apps/ipc/views/testing.py +285 -0
  31. django_cfg/core/backends/__init__.py +1 -0
  32. django_cfg/core/backends/smtp.py +69 -0
  33. django_cfg/middleware/authentication.py +157 -0
  34. django_cfg/models/api/drf/config.py +2 -2
  35. django_cfg/models/services/email.py +11 -1
  36. django_cfg/modules/django_client/system/generate_mjs_clients.py +1 -1
  37. django_cfg/modules/django_dashboard/sections/widgets.py +209 -0
  38. django_cfg/modules/django_unfold/callbacks/main.py +43 -18
  39. django_cfg/modules/django_unfold/dashboard.py +41 -4
  40. django_cfg/pyproject.toml +1 -1
  41. django_cfg/static/js/api/index.mjs +8 -3
  42. django_cfg/static/js/api/ipc/client.mjs +40 -0
  43. django_cfg/static/js/api/knowbase/client.mjs +309 -0
  44. django_cfg/static/js/api/knowbase/index.mjs +13 -0
  45. django_cfg/static/js/api/payments/client.mjs +46 -1215
  46. django_cfg/static/js/api/types.mjs +164 -337
  47. django_cfg/templates/admin/index.html +8 -0
  48. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +13 -1
  49. django_cfg/templates/admin/sections/widgets_section.html +129 -0
  50. django_cfg/templates/admin/snippets/tabs/widgets_tab.html +38 -0
  51. django_cfg/utils/smart_defaults.py +1 -1
  52. {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/METADATA +1 -1
  53. {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/RECORD +58 -31
  54. django_cfg/apps/ipc/templates/django_cfg_ipc/dashboard.html +0 -202
  55. /django_cfg/apps/ipc/static/django_cfg_ipc/js/{dashboard.mjs → dashboard.mjs.old} +0 -0
  56. /django_cfg/apps/ipc/templates/django_cfg_ipc/{base.html → layout/base.html} +0 -0
  57. {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/WHEEL +0 -0
  58. {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/entry_points.txt +0 -0
  59. {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ Custom SMTP Email Backend for Django CFG.
3
+
4
+ Supports SMTP with unverified SSL certificates for development environments.
5
+ """
6
+
7
+ import smtplib
8
+ import ssl
9
+
10
+ from django.core.mail.backends.smtp import EmailBackend as DjangoSMTPBackend
11
+
12
+
13
+ class UnverifiedSSLEmailBackend(DjangoSMTPBackend):
14
+ """
15
+ SMTP backend that accepts self-signed SSL certificates.
16
+
17
+ WARNING: Only use in development! In production, use proper SSL certificates
18
+ with the standard Django SMTP backend.
19
+
20
+ This backend disables SSL certificate verification, which makes it vulnerable
21
+ to man-in-the-middle attacks. It's designed for dev environments where you
22
+ control the mail server and use self-signed certificates.
23
+ """
24
+
25
+ def open(self):
26
+ """
27
+ Open connection with unverified SSL context.
28
+
29
+ Overrides parent to use unverified SSL context for self-signed certs.
30
+ """
31
+ if self.connection:
32
+ # Already have a connection
33
+ return False
34
+
35
+ connection_params = {'timeout': self.timeout}
36
+ if self.ssl_keyfile:
37
+ connection_params['keyfile'] = self.ssl_keyfile
38
+ if self.ssl_certfile:
39
+ connection_params['certfile'] = self.ssl_certfile
40
+
41
+ if self.use_ssl:
42
+ # Create unverified SSL context (accepts self-signed certs)
43
+ ssl_context = ssl.create_default_context()
44
+ ssl_context.check_hostname = False
45
+ ssl_context.verify_mode = ssl.CERT_NONE
46
+
47
+ connection_params['context'] = ssl_context
48
+ self.connection = smtplib.SMTP_SSL(
49
+ self.host, self.port, **connection_params
50
+ )
51
+ else:
52
+ self.connection = smtplib.SMTP(
53
+ self.host, self.port, **connection_params
54
+ )
55
+
56
+ if self.use_tls:
57
+ # Create unverified SSL context for STARTTLS too
58
+ ssl_context = ssl.create_default_context()
59
+ ssl_context.check_hostname = False
60
+ ssl_context.verify_mode = ssl.CERT_NONE
61
+
62
+ self.connection.ehlo()
63
+ self.connection.starttls(context=ssl_context)
64
+ self.connection.ehlo()
65
+
66
+ if self.username and self.password:
67
+ self.connection.login(self.username, self.password)
68
+
69
+ return True
@@ -0,0 +1,157 @@
1
+ """
2
+ Custom JWT Authentication for Django CFG.
3
+
4
+ Extends rest_framework_simplejwt.authentication.JWTAuthentication to automatically
5
+ update user's last_login field on successful authentication.
6
+ """
7
+
8
+ import logging
9
+
10
+ from django.contrib.auth import get_user_model
11
+ from django.utils import timezone
12
+ from rest_framework_simplejwt.authentication import JWTAuthentication
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ User = get_user_model()
17
+
18
+
19
+ class JWTAuthenticationWithLastLogin(JWTAuthentication):
20
+ """
21
+ JWT Authentication that updates last_login on successful authentication.
22
+
23
+ Updates last_login field with intelligent throttling to avoid database spam.
24
+ Only updates if last_login is None or older than the configured interval.
25
+
26
+ Features:
27
+ - Automatic last_login tracking for all JWT-authenticated requests
28
+ - Built-in throttling (default: 5 minutes) to minimize database writes
29
+ - In-memory cache for tracking last update times
30
+ - Automatic cache cleanup to prevent memory leaks
31
+ - Error handling to prevent authentication failures
32
+
33
+ Usage:
34
+ Add to REST_FRAMEWORK settings:
35
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
36
+ 'django_cfg.apps.accounts.authentication.JWTAuthenticationWithLastLogin',
37
+ ]
38
+ """
39
+
40
+ # Class-level cache to track last update times (shared across all instances)
41
+ _last_updates = {}
42
+
43
+ # Update interval in seconds (5 minutes by default, same as UserActivityMiddleware)
44
+ UPDATE_INTERVAL = 300
45
+
46
+ # Maximum cache size before cleanup (prevents memory leaks)
47
+ MAX_CACHE_SIZE = 1000
48
+ CLEANUP_CACHE_SIZE = 500
49
+
50
+ def authenticate(self, request):
51
+ """
52
+ Authenticate request and update last_login if needed.
53
+
54
+ Args:
55
+ request: Django HttpRequest object
56
+
57
+ Returns:
58
+ Tuple of (user, token) if authentication succeeds, None otherwise
59
+ """
60
+ # Perform standard JWT authentication
61
+ result = super().authenticate(request)
62
+
63
+ if result is not None:
64
+ user, token = result
65
+ # Update last_login with throttling
66
+ self._update_last_login(user)
67
+
68
+ return result
69
+
70
+ def _update_last_login(self, user):
71
+ """
72
+ Update user's last_login field with intelligent throttling.
73
+
74
+ Only updates if:
75
+ - last_login is None (never logged in)
76
+ - last_login is older than UPDATE_INTERVAL seconds
77
+
78
+ Uses UPDATE query to avoid triggering signals and save() overhead.
79
+
80
+ Args:
81
+ user: User instance to update
82
+ """
83
+ now = timezone.now()
84
+ user_id = user.pk
85
+
86
+ # Check if we should update (avoid database spam)
87
+ last_update = self._last_updates.get(user_id)
88
+ if last_update and (now - last_update).total_seconds() < self.UPDATE_INTERVAL:
89
+ # Skip update - too soon since last update
90
+ return
91
+
92
+ try:
93
+ # Use update() to avoid triggering signals and save() overhead
94
+ # This is more efficient than user.save(update_fields=['last_login'])
95
+ updated_count = User.objects.filter(pk=user_id).update(last_login=now)
96
+
97
+ if updated_count > 0:
98
+ # Cache the update time
99
+ self._last_updates[user_id] = now
100
+
101
+ logger.debug(
102
+ f"Updated last_login for user {user.email} (ID: {user_id}) "
103
+ f"via JWT authentication"
104
+ )
105
+
106
+ # Clean up old cache entries to prevent memory leaks
107
+ if len(self._last_updates) > self.MAX_CACHE_SIZE:
108
+ self._cleanup_cache()
109
+
110
+ except Exception as e:
111
+ # Log error but don't break authentication
112
+ # Authentication should succeed even if last_login update fails
113
+ logger.error(
114
+ f"Failed to update last_login for user {user_id}: {e}",
115
+ exc_info=True
116
+ )
117
+
118
+ def _cleanup_cache(self):
119
+ """
120
+ Clean up old cache entries to prevent memory leaks.
121
+
122
+ Keeps only the most recent CLEANUP_CACHE_SIZE entries.
123
+ Sorted by update time (newest first).
124
+ """
125
+ try:
126
+ # Sort by update time (value), keep newest entries
127
+ sorted_items = sorted(
128
+ self._last_updates.items(),
129
+ key=lambda x: x[1],
130
+ reverse=True
131
+ )
132
+
133
+ # Keep only the most recent entries
134
+ self._last_updates = dict(sorted_items[:self.CLEANUP_CACHE_SIZE])
135
+
136
+ logger.debug(
137
+ f"Cleaned up last_login cache: "
138
+ f"kept {len(self._last_updates)} most recent entries"
139
+ )
140
+
141
+ except Exception as e:
142
+ logger.error(f"Failed to cleanup last_login cache: {e}")
143
+
144
+ @classmethod
145
+ def get_cache_stats(cls):
146
+ """
147
+ Get cache statistics for monitoring/debugging.
148
+
149
+ Returns:
150
+ dict: Cache statistics including size and configuration
151
+ """
152
+ return {
153
+ 'cache_size': len(cls._last_updates),
154
+ 'max_cache_size': cls.MAX_CACHE_SIZE,
155
+ 'update_interval_seconds': cls.UPDATE_INTERVAL,
156
+ 'update_interval_minutes': cls.UPDATE_INTERVAL / 60,
157
+ }
@@ -17,12 +17,12 @@ class DRFConfig(BaseModel):
17
17
  # Authentication
18
18
  authentication_classes: List[str] = Field(
19
19
  default_factory=lambda: [
20
- 'rest_framework_simplejwt.authentication.JWTAuthentication',
20
+ 'django_cfg.middleware.authentication.JWTAuthenticationWithLastLogin',
21
21
  'rest_framework.authentication.TokenAuthentication',
22
22
  # SessionAuthentication removed from defaults (requires CSRF)
23
23
  # Add it manually in your config if you need Browsable API with session auth
24
24
  ],
25
- description="Default authentication classes"
25
+ description="Default authentication classes (JWT with auto last_login update)"
26
26
  )
27
27
 
28
28
  # Permissions
@@ -58,6 +58,11 @@ class EmailConfig(BaseConfig):
58
58
  description="Use SSL for SMTP"
59
59
  )
60
60
 
61
+ ssl_verify: bool = Field(
62
+ default=True,
63
+ description="Verify SSL certificates (set False for self-signed certs in dev)"
64
+ )
65
+
61
66
  timeout: int = Field(
62
67
  default=30,
63
68
  ge=1,
@@ -111,7 +116,12 @@ class EmailConfig(BaseConfig):
111
116
  def to_django_settings(self) -> Dict[str, Any]:
112
117
  """Convert to Django email settings."""
113
118
  if self.backend == 'smtp':
114
- backend_class = 'django.core.mail.backends.smtp.EmailBackend'
119
+ # Choose backend based on SSL verification setting
120
+ if self.ssl_verify:
121
+ backend_class = 'django.core.mail.backends.smtp.EmailBackend'
122
+ else:
123
+ # Use custom backend that accepts self-signed SSL certs
124
+ backend_class = 'django_cfg.core.backends.smtp.UnverifiedSSLEmailBackend'
115
125
 
116
126
  settings = {
117
127
  'EMAIL_BACKEND': backend_class,
@@ -54,7 +54,7 @@ def main():
54
54
  django_cfg_dir = modules_dir.parent # django_cfg
55
55
  src_dir = django_cfg_dir.parent # src
56
56
  dev_dir = src_dir.parent # django-cfg-dev
57
- example_django_dir = dev_dir.parent / "django-cfg-example" / "django"
57
+ example_django_dir = dev_dir.parent / "solution" / "projects" / "django"
58
58
 
59
59
  # Output directory for MJS clients
60
60
  if args.output:
@@ -0,0 +1,209 @@
1
+ """Widgets section for dashboard.
2
+
3
+ Automatically renders widgets from DashboardManager.get_widgets_config()
4
+ """
5
+
6
+ from datetime import timedelta
7
+ from typing import Any, Dict, List
8
+
9
+ import psutil
10
+ from django.db.models import Avg
11
+ from django.template import Context, Template
12
+ from django.utils import timezone
13
+
14
+ from .base import DataSection
15
+
16
+
17
+ class WidgetsSection(DataSection):
18
+ """
19
+ Widgets section showing automatically generated dashboard widgets.
20
+
21
+ Widgets are defined in DashboardManager.get_widgets_config() and
22
+ can include:
23
+ - System metrics (CPU, Memory, Disk)
24
+ - RPC monitoring stats
25
+ - Custom application widgets
26
+ """
27
+
28
+ template_name = "admin/sections/widgets_section.html"
29
+ title = "Dashboard Widgets"
30
+ icon = "widgets"
31
+
32
+ def get_data(self) -> Dict[str, Any]:
33
+ """Get widgets configuration from DashboardManager."""
34
+ from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
35
+
36
+ dashboard_manager = get_dashboard_manager()
37
+
38
+ # Get widgets from dashboard manager (base system widgets)
39
+ widgets = dashboard_manager.get_widgets_config()
40
+
41
+ return {
42
+ 'widgets': widgets,
43
+ 'widgets_count': len(widgets),
44
+ 'has_widgets': len(widgets) > 0,
45
+ }
46
+
47
+ def merge_custom_widgets(self, widgets: List[Dict], custom_widgets: List[Any]) -> List[Dict]:
48
+ """
49
+ Merge custom widgets from dashboard_callback.
50
+
51
+ Allows projects to add widgets via dashboard_callback:
52
+ context["custom_widgets"] = [
53
+ StatsCardsWidget(...),
54
+ ...
55
+ ]
56
+ """
57
+ if not custom_widgets:
58
+ return widgets
59
+
60
+ # Convert custom widgets to dicts if they are Pydantic models
61
+ for widget in custom_widgets:
62
+ if hasattr(widget, 'to_dict'):
63
+ widgets.append(widget.to_dict())
64
+ elif hasattr(widget, 'model_dump'):
65
+ widgets.append(widget.model_dump())
66
+ elif isinstance(widget, dict):
67
+ widgets.append(widget)
68
+
69
+ return widgets
70
+
71
+ def get_context_data(self, **kwargs) -> Dict[str, Any]:
72
+ """Add additional context for widget rendering."""
73
+ context = super().get_context_data(**kwargs)
74
+
75
+ # Get base widgets from DashboardManager
76
+ widgets = context['data']['widgets']
77
+
78
+ # Merge custom widgets from dashboard_callback if provided
79
+ custom_widgets_from_callback = kwargs.get('custom_widgets', [])
80
+ if custom_widgets_from_callback:
81
+ widgets = self.merge_custom_widgets(widgets, custom_widgets_from_callback)
82
+ # Update count
83
+ context['data']['widgets_count'] = len(widgets)
84
+ context['data']['has_widgets'] = len(widgets) > 0
85
+
86
+ # Get metrics data first
87
+ metrics_data = {}
88
+ metrics_data.update(self.get_system_metrics())
89
+
90
+ # Add RPC metrics if enabled
91
+ dashboard_manager = self._get_dashboard_manager()
92
+ if dashboard_manager.is_rpc_enabled():
93
+ metrics_data.update(self.get_rpc_metrics())
94
+
95
+ # Also merge any custom metrics from kwargs
96
+ custom_metrics = kwargs.get('custom_metrics', {})
97
+ if custom_metrics:
98
+ metrics_data.update(custom_metrics)
99
+
100
+ # Process widgets and resolve template variables
101
+ processed_stats_widgets = []
102
+ for widget in widgets:
103
+ if widget.get('type') == 'stats_cards':
104
+ processed_widget = self._process_stats_widget(widget, metrics_data)
105
+ processed_stats_widgets.append(processed_widget)
106
+
107
+ chart_widgets = [w for w in widgets if w.get('type') == 'chart']
108
+ custom_widgets = [w for w in widgets if w.get('type') not in ['stats_cards', 'chart']]
109
+
110
+ # Add processed widgets
111
+ context.update({
112
+ 'stats_widgets': processed_stats_widgets,
113
+ 'chart_widgets': chart_widgets,
114
+ 'custom_widgets': custom_widgets,
115
+ })
116
+
117
+ # Also add metrics for direct access
118
+ context.update(metrics_data)
119
+
120
+ return context
121
+
122
+ def _get_dashboard_manager(self):
123
+ """Get dashboard manager instance (lazy import to avoid circular dependencies)."""
124
+ from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
125
+ return get_dashboard_manager()
126
+
127
+ def _process_stats_widget(self, widget: Dict[str, Any], context_data: Dict[str, Any]) -> Dict[str, Any]:
128
+ """Process StatsCardsWidget and resolve template variables in cards."""
129
+ processed_widget = widget.copy()
130
+ processed_cards = []
131
+
132
+ for card in widget.get('cards', []):
133
+ processed_card = card.copy()
134
+
135
+ # Resolve value_template using Django template engine
136
+ value_template = card.get('value_template', '')
137
+ if '{{' in value_template:
138
+ try:
139
+ template = Template(value_template)
140
+ context = Context(context_data)
141
+ resolved_value = template.render(context)
142
+ processed_card['value_template'] = resolved_value
143
+ except Exception:
144
+ # Keep original if rendering fails
145
+ pass
146
+
147
+ # Also resolve change field if it has template variables
148
+ change_template = card.get('change', '')
149
+ if change_template and '{{' in change_template:
150
+ try:
151
+ template = Template(change_template)
152
+ context = Context(context_data)
153
+ resolved_change = template.render(context)
154
+ processed_card['change'] = resolved_change
155
+ except Exception:
156
+ # Keep original if rendering fails
157
+ pass
158
+
159
+ processed_cards.append(processed_card)
160
+
161
+ processed_widget['cards'] = processed_cards
162
+ return processed_widget
163
+
164
+ def get_system_metrics(self) -> Dict[str, Any]:
165
+ """Get system metrics for widgets."""
166
+ return {
167
+ 'cpu_percent': round(psutil.cpu_percent(interval=0.1), 1),
168
+ 'memory_percent': round(psutil.virtual_memory().percent, 1),
169
+ 'disk_percent': round(psutil.disk_usage('/').percent, 1),
170
+ }
171
+
172
+ def get_rpc_metrics(self) -> Dict[str, Any]:
173
+ """Get RPC metrics for widgets."""
174
+ try:
175
+ from django_cfg.apps.ipc.models import RPCLog
176
+
177
+ # Get stats for last 24 hours
178
+ since = timezone.now() - timedelta(hours=24)
179
+
180
+ logs = RPCLog.objects.filter(timestamp__gte=since)
181
+
182
+ total_calls = logs.count()
183
+ successful_calls = logs.filter(status='success').count()
184
+ failed_calls = logs.filter(status='error').count()
185
+
186
+ success_rate = round((successful_calls / total_calls * 100) if total_calls > 0 else 0, 1)
187
+
188
+ avg_duration = logs.filter(
189
+ duration__isnull=False
190
+ ).aggregate(
191
+ avg=Avg('duration')
192
+ )['avg']
193
+
194
+ avg_duration = round(avg_duration * 1000, 1) if avg_duration else 0 # Convert to ms
195
+
196
+ return {
197
+ 'rpc_total_calls': total_calls,
198
+ 'rpc_success_rate': success_rate,
199
+ 'rpc_avg_duration': avg_duration,
200
+ 'rpc_failed_calls': failed_calls,
201
+ }
202
+ except Exception as e:
203
+ # Return zeros if RPC models not available
204
+ return {
205
+ 'rpc_total_calls': 0,
206
+ 'rpc_success_rate': 0,
207
+ 'rpc_avg_duration': 0,
208
+ 'rpc_failed_calls': 0,
209
+ }
@@ -19,6 +19,7 @@ from django_cfg.modules.django_dashboard.sections.documentation import Documenta
19
19
  from django_cfg.modules.django_dashboard.sections.overview import OverviewSection
20
20
  from django_cfg.modules.django_dashboard.sections.stats import StatsSection
21
21
  from django_cfg.modules.django_dashboard.sections.system import SystemSection
22
+ from django_cfg.modules.django_dashboard.sections.widgets import WidgetsSection
22
23
 
23
24
  from ...base import BaseCfgModule
24
25
  from ..models.dashboard import DashboardData
@@ -155,6 +156,29 @@ class UnfoldCallbacks(
155
156
  logger.error(f"Failed to render documentation section: {e}", exc_info=True)
156
157
  documentation_section = None
157
158
 
159
+ # Extract custom widgets from context if provided by project's dashboard_callback
160
+ custom_widgets = context.get('custom_widgets', [])
161
+ custom_metrics = {}
162
+
163
+ # Extract all metric-like variables from context for widget template resolution
164
+ # This allows dashboard_callback to add metrics like: context['total_users'] = 123
165
+ for key, value in context.items():
166
+ if key not in ['request', 'cards', 'system_health', 'quick_actions'] and isinstance(value, (int, float, str)):
167
+ custom_metrics[key] = value
168
+
169
+ try:
170
+ # Render widgets section with custom widgets and metrics from callback
171
+ widgets_section = WidgetsSection(request).render(
172
+ custom_widgets=custom_widgets,
173
+ custom_metrics=custom_metrics
174
+ )
175
+ # Debug: save render (only in debug mode)
176
+ if config and config.debug:
177
+ save_section_render('widgets', widgets_section)
178
+ except Exception as e:
179
+ logger.error(f"Failed to render widgets section: {e}", exc_info=True)
180
+ widgets_section = None
181
+
158
182
  # Combine all stat cards (data already loaded above)
159
183
  all_stats = user_stats + support_stats
160
184
 
@@ -176,6 +200,7 @@ class UnfoldCallbacks(
176
200
  "system_section": system_section,
177
201
  "commands_section": commands_section,
178
202
  "documentation_section": documentation_section,
203
+ "widgets_section": widgets_section,
179
204
 
180
205
  # Statistics cards
181
206
  "cards": cards_data,
@@ -252,24 +277,24 @@ class UnfoldCallbacks(
252
277
  })
253
278
 
254
279
  # Log charts data for debugging
255
- charts_data = context.get('charts', {})
256
- logger.info(f"Charts data added to context: {list(charts_data.keys())}")
257
- if 'user_registrations' in charts_data:
258
- reg_data = charts_data['user_registrations']
259
- logger.info(f"Registration chart labels: {reg_data.get('labels', [])}")
260
- if 'user_activity' in charts_data:
261
- act_data = charts_data['user_activity']
262
- logger.info(f"Activity chart labels: {act_data.get('labels', [])}")
263
-
264
- # Log recent users data for debugging
265
- recent_users_data = context.get('recent_users', [])
266
- logger.info(f"Recent users data count: {len(recent_users_data)}")
267
- if recent_users_data:
268
- logger.info(f"First user: {recent_users_data[0].get('username', 'N/A')}")
269
-
270
- # Log activity tracker data for debugging
271
- activity_tracker_data = context.get('activity_tracker', [])
272
- logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
280
+ # charts_data = context.get('charts', {})
281
+ # # logger.info(f"Charts data added to context: {list(charts_data.keys())}")
282
+ # if 'user_registrations' in charts_data:
283
+ # reg_data = charts_data['user_registrations']
284
+ # logger.info(f"Registration chart labels: {reg_data.get('labels', [])}")
285
+ # if 'user_activity' in charts_data:
286
+ # act_data = charts_data['user_activity']
287
+ # logger.info(f"Activity chart labels: {act_data.get('labels', [])}")
288
+
289
+ # # Log recent users data for debugging
290
+ # recent_users_data = context.get('recent_users', [])
291
+ # logger.info(f"Recent users data count: {len(recent_users_data)}")
292
+ # if recent_users_data:
293
+ # logger.info(f"First user: {recent_users_data[0].get('username', 'N/A')}")
294
+
295
+ # # Log activity tracker data for debugging
296
+ # activity_tracker_data = context.get('activity_tracker', [])
297
+ # logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
273
298
 
274
299
  return context
275
300
 
@@ -73,9 +73,10 @@ class DashboardManager(BaseCfgModule):
73
73
 
74
74
  # RPC Dashboard (if enabled)
75
75
  if self.is_rpc_enabled():
76
- operations_items.append(
77
- NavigationItem(title="IPC/RPC Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/ipc/admin/")
78
- )
76
+ operations_items.extend([
77
+ NavigationItem(title="IPC/RPC Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/ipc/admin/"),
78
+ NavigationItem(title="RPC Logs", icon=Icons.LIST_ALT, link=str(reverse_lazy("admin:django_cfg_ipc_rpclog_changelist"))),
79
+ ])
79
80
 
80
81
  # Background Tasks (if enabled)
81
82
  if self.should_enable_tasks():
@@ -298,6 +299,8 @@ class DashboardManager(BaseCfgModule):
298
299
 
299
300
  def get_widgets_config(self) -> List[Dict[str, Any]]:
300
301
  """Get dashboard widgets configuration using Pydantic models."""
302
+ widgets = []
303
+
301
304
  # Create system overview widget with StatCard models
302
305
  system_overview_widget = StatsCardsWidget(
303
306
  title="System Overview",
@@ -322,9 +325,43 @@ class DashboardManager(BaseCfgModule):
322
325
  ),
323
326
  ]
324
327
  )
328
+ widgets.append(system_overview_widget.to_dict())
329
+
330
+ # Add RPC monitoring widget if IPC is enabled
331
+ if self.is_rpc_enabled():
332
+ rpc_monitoring_widget = StatsCardsWidget(
333
+ title="IPC/RPC Monitoring",
334
+ cards=[
335
+ StatCard(
336
+ title="Total Calls",
337
+ value="{{ rpc_total_calls }}",
338
+ icon=Icons.API,
339
+ color="blue",
340
+ ),
341
+ StatCard(
342
+ title="Success Rate",
343
+ value="{{ rpc_success_rate }}%",
344
+ icon=Icons.CHECK_CIRCLE,
345
+ color="green",
346
+ ),
347
+ StatCard(
348
+ title="Avg Response Time",
349
+ value="{{ rpc_avg_duration }}ms",
350
+ icon=Icons.SPEED,
351
+ color="purple",
352
+ ),
353
+ StatCard(
354
+ title="Failed Calls",
355
+ value="{{ rpc_failed_calls }}",
356
+ icon=Icons.ERROR,
357
+ color="red",
358
+ ),
359
+ ]
360
+ )
361
+ widgets.append(rpc_monitoring_widget.to_dict())
325
362
 
326
363
  # Convert to dictionaries for Unfold
327
- return [system_overview_widget.to_dict()]
364
+ return widgets
328
365
 
329
366
 
330
367
  # Lazy initialization to avoid circular imports
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.58"
7
+ version = "1.4.60"
8
8
  description = "Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "ai-agents", "enterprise-django", "django-settings", "type-safe-config",]