django-cfg 1.4.88__py3-none-any.whl → 1.4.89__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 (146) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/centrifugo/views/__init__.py +0 -2
  3. django_cfg/apps/dashboard/services/__init__.py +2 -0
  4. django_cfg/apps/dashboard/services/overview_service.py +205 -0
  5. django_cfg/apps/frontend/test_routing.py +134 -0
  6. django_cfg/apps/frontend/views.py +69 -28
  7. django_cfg/apps/urls.py +0 -1
  8. django_cfg/core/builders/apps_builder.py +0 -58
  9. django_cfg/modules/django_unfold/__init__.py +5 -24
  10. django_cfg/modules/django_unfold/models/__init__.py +0 -23
  11. django_cfg/modules/django_unfold/models/config.py +11 -65
  12. django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
  13. django_cfg/modules/django_unfold/tailwind.py +2 -4
  14. django_cfg/pyproject.toml +1 -1
  15. django_cfg/registry/third_party.py +0 -9
  16. django_cfg/routing/callbacks.py +1 -43
  17. django_cfg/static/frontend/admin/404/index.html +1 -1
  18. django_cfg/static/frontend/admin/404.html +1 -1
  19. django_cfg/static/frontend/admin/500/index.html +1 -1
  20. django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
  21. django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
  22. django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-1c0fff0f59a6d683.js → _app-c7dcd3aa616fab68.js} +1 -1
  23. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-44a8313fa040e9ad.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
  24. django_cfg/static/frontend/admin/auth/index.html +1 -1
  25. django_cfg/static/frontend/admin/index.html +1 -1
  26. django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
  27. django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
  28. django_cfg/static/frontend/admin/legal/security/index.html +1 -1
  29. django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
  30. django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
  31. django_cfg/static/frontend/admin/private/index.html +1 -1
  32. django_cfg/static/frontend/admin/private/profile/index.html +1 -1
  33. django_cfg/static/frontend/admin/private/ui/index.html +2 -2
  34. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
  35. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/RECORD +39 -143
  36. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
  37. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
  38. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
  39. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
  40. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
  41. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
  42. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
  43. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
  44. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
  45. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
  46. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
  47. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
  48. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
  49. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
  50. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
  51. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
  52. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
  53. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
  54. django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
  55. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
  56. django_cfg/apps/centrifugo/urls_admin.py +0 -20
  57. django_cfg/apps/centrifugo/views/dashboard.py +0 -28
  58. django_cfg/modules/django_dashboard/__init__.py +0 -23
  59. django_cfg/modules/django_dashboard/components.py +0 -312
  60. django_cfg/modules/django_dashboard/debug.py +0 -174
  61. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  62. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  63. django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
  64. django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
  65. django_cfg/modules/django_dashboard/sections/base.py +0 -129
  66. django_cfg/modules/django_dashboard/sections/commands.py +0 -33
  67. django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
  68. django_cfg/modules/django_dashboard/sections/overview.py +0 -398
  69. django_cfg/modules/django_dashboard/sections/stats.py +0 -48
  70. django_cfg/modules/django_dashboard/sections/system.py +0 -74
  71. django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
  72. django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
  73. django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
  74. django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
  75. django_cfg/modules/django_unfold/callbacks/base.py +0 -290
  76. django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
  77. django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
  78. django_cfg/modules/django_unfold/callbacks/main.py +0 -322
  79. django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
  80. django_cfg/modules/django_unfold/callbacks/system.py +0 -180
  81. django_cfg/modules/django_unfold/callbacks/users.py +0 -65
  82. django_cfg/modules/django_unfold/models/dashboard.py +0 -207
  83. django_cfg/modules/django_unfold/models/tabs.py +0 -26
  84. django_cfg/modules/django_unfold/models.py +0 -98
  85. django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
  86. django_cfg/static/frontend/admin/_next/static/chunks/50314-5ec79b293c2283dd.js +0 -1
  87. django_cfg/templates/admin/sections/commands_section.html +0 -5
  88. django_cfg/templates/admin/sections/documentation_section.html +0 -5
  89. django_cfg/templates/admin/sections/overview_section.html +0 -5
  90. django_cfg/templates/admin/sections/stats_section.html +0 -5
  91. django_cfg/templates/admin/sections/system_section.html +0 -5
  92. django_cfg/templates/admin/sections/widgets_section.html +0 -11
  93. django_cfg/templates/admin_old/components/action_grid.html +0 -49
  94. django_cfg/templates/admin_old/components/card.html +0 -50
  95. django_cfg/templates/admin_old/components/data_table.html +0 -67
  96. django_cfg/templates/admin_old/components/metric_card.html +0 -39
  97. django_cfg/templates/admin_old/components/modal.html +0 -58
  98. django_cfg/templates/admin_old/components/progress_bar.html +0 -20
  99. django_cfg/templates/admin_old/components/section_header.html +0 -26
  100. django_cfg/templates/admin_old/components/stat_item.html +0 -32
  101. django_cfg/templates/admin_old/components/stats_grid.html +0 -72
  102. django_cfg/templates/admin_old/components/status_badge.html +0 -28
  103. django_cfg/templates/admin_old/components/user_avatar.html +0 -27
  104. django_cfg/templates/admin_old/constance/change_list.html +0 -74
  105. django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
  106. django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
  107. django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
  108. django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
  109. django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
  110. django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
  111. django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
  112. django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
  113. django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
  114. django_cfg/templates/admin_old/index.html +0 -80
  115. django_cfg/templates/admin_old/index_new.html +0 -119
  116. django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
  117. django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
  118. django_cfg/templates/admin_old/sections/commands_section.html +0 -549
  119. django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
  120. django_cfg/templates/admin_old/sections/overview_section.html +0 -112
  121. django_cfg/templates/admin_old/sections/stats_section.html +0 -35
  122. django_cfg/templates/admin_old/sections/system_section.html +0 -99
  123. django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
  124. django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
  125. django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
  126. django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
  127. django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
  128. django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
  129. django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
  130. django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
  131. django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
  132. django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
  133. django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
  134. django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
  135. django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
  136. django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
  137. django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
  138. django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
  139. django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
  140. django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
  141. django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
  142. django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
  143. /django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
  144. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
  145. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
  146. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.88"
35
+ __version__ = "1.4.89"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -3,7 +3,6 @@ Views for Centrifugo module.
3
3
  """
4
4
 
5
5
  from .admin_api import CentrifugoAdminAPIViewSet
6
- from .dashboard import dashboard_view
7
6
  from .monitoring import CentrifugoMonitorViewSet
8
7
  from .testing_api import CentrifugoTestingAPIViewSet
9
8
 
@@ -11,5 +10,4 @@ __all__ = [
11
10
  'CentrifugoMonitorViewSet',
12
11
  'CentrifugoAdminAPIViewSet',
13
12
  'CentrifugoTestingAPIViewSet',
14
- 'dashboard_view',
15
13
  ]
@@ -10,6 +10,7 @@ from .system_health_service import SystemHealthService
10
10
  from .charts_service import ChartsService
11
11
  from .commands_service import CommandsService
12
12
  from .apizones_service import APIZonesService
13
+ from .overview_service import OverviewService
13
14
 
14
15
  __all__ = [
15
16
  'StatisticsService',
@@ -17,4 +18,5 @@ __all__ = [
17
18
  'ChartsService',
18
19
  'CommandsService',
19
20
  'APIZonesService',
21
+ 'OverviewService',
20
22
  ]
@@ -0,0 +1,205 @@
1
+ """
2
+ Overview Service
3
+
4
+ Provides overview data including recent users and activity tracking.
5
+ Extracted from django_dashboard for API-based dashboard.
6
+ """
7
+
8
+ import logging
9
+ from datetime import timedelta
10
+ from typing import Any, Dict, List
11
+
12
+ from django.contrib.auth import get_user_model
13
+ from django.utils import timezone
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class OverviewService:
19
+ """
20
+ Service for dashboard overview data.
21
+
22
+ %%PRIORITY:HIGH%%
23
+ %%AI_HINT: Provides recent activity and user registration tracking%%
24
+
25
+ TAGS: overview, dashboard, activity, service
26
+ DEPENDS_ON: [django.contrib.auth]
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize overview service."""
31
+ self.logger = logger
32
+
33
+ def _get_user_model(self):
34
+ """Get the user model safely."""
35
+ return get_user_model()
36
+
37
+ def get_recent_users(self, limit: int = 10) -> List[Dict[str, Any]]:
38
+ """
39
+ Get recent users for activity section.
40
+
41
+ Args:
42
+ limit: Maximum number of users to return (default: 10)
43
+
44
+ Returns:
45
+ List of user dictionaries with id, username, email, is_active, date_joined
46
+
47
+ %%AI_HINT: Real Django ORM query for recent users%%
48
+ """
49
+ try:
50
+ User = self._get_user_model()
51
+ recent_users = User.objects.order_by('-date_joined')[:limit]
52
+
53
+ return [
54
+ {
55
+ 'id': user.id,
56
+ 'username': user.username,
57
+ 'email': user.email or '',
58
+ 'is_active': user.is_active,
59
+ 'is_staff': user.is_staff,
60
+ 'date_joined': user.date_joined.isoformat() if user.date_joined else None,
61
+ }
62
+ for user in recent_users
63
+ ]
64
+ except Exception as e:
65
+ self.logger.error(f"Error getting recent users: {e}")
66
+ return []
67
+
68
+ def get_activity_tracker(self, days: int = 365) -> List[Dict[str, Any]]:
69
+ """
70
+ Get activity tracker data for GitHub-style heatmap.
71
+
72
+ Returns list of dicts with date and count for last N days.
73
+
74
+ Args:
75
+ days: Number of days to track (default: 365)
76
+
77
+ Returns:
78
+ List of activity data: [{'date': 'YYYY-MM-DD', 'count': int, 'level': int}, ...]
79
+ level: 0-4 (0=no activity, 4=very high activity)
80
+
81
+ %%AI_HINT: Generates daily activity counts from user registrations and logins%%
82
+ """
83
+ try:
84
+ User = self._get_user_model()
85
+ today = timezone.now().date()
86
+ activity_data = []
87
+
88
+ for days_ago in range(days - 1, -1, -1): # Newest last
89
+ date = today - timedelta(days=days_ago)
90
+
91
+ # Count user registrations on this day
92
+ registrations = User.objects.filter(
93
+ date_joined__date=date
94
+ ).count()
95
+
96
+ # Count logins on this day (if last_login exists)
97
+ logins = 0
98
+ if hasattr(User, 'last_login'):
99
+ logins = User.objects.filter(
100
+ last_login__date=date
101
+ ).count()
102
+
103
+ # Total activity for the day
104
+ total_activity = registrations + logins
105
+
106
+ activity_data.append({
107
+ 'date': date.isoformat(),
108
+ 'count': total_activity,
109
+ 'level': self._get_activity_level(total_activity),
110
+ })
111
+
112
+ return activity_data
113
+
114
+ except Exception as e:
115
+ self.logger.error(f"Error getting activity tracker: {e}")
116
+ return []
117
+
118
+ def _get_activity_level(self, count: int) -> int:
119
+ """
120
+ Convert activity count to level (0-4) for heatmap colors.
121
+
122
+ Args:
123
+ count: Number of activities
124
+
125
+ Returns:
126
+ Activity level (0-4):
127
+ - 0 = no activity (gray)
128
+ - 1 = low (light green)
129
+ - 2 = medium (green)
130
+ - 3 = high (dark green)
131
+ - 4 = very high (darkest green)
132
+ """
133
+ if count == 0:
134
+ return 0
135
+ elif count <= 2:
136
+ return 1
137
+ elif count <= 5:
138
+ return 2
139
+ elif count <= 10:
140
+ return 3
141
+ else:
142
+ return 4
143
+
144
+ def get_key_stats(self) -> Dict[str, Any]:
145
+ """
146
+ Get key statistics for overview.
147
+
148
+ Returns:
149
+ Dictionary with users, databases, apps counts
150
+
151
+ %%AI_HINT: Provides high-level system statistics%%
152
+ """
153
+ try:
154
+ User = self._get_user_model()
155
+
156
+ # Get database count
157
+ from django.db import connection
158
+ db_count = len(connection.settings_dict.get('DATABASES', {})) if hasattr(connection, 'settings_dict') else 1
159
+
160
+ # Get app count
161
+ from django.apps import apps
162
+ app_count = len(apps.get_app_configs())
163
+
164
+ return {
165
+ 'users': User.objects.count(),
166
+ 'databases': db_count,
167
+ 'apps': app_count,
168
+ }
169
+ except Exception as e:
170
+ self.logger.error(f"Error getting key stats: {e}")
171
+ return {
172
+ 'users': 0,
173
+ 'databases': 0,
174
+ 'apps': 0,
175
+ 'error': str(e)
176
+ }
177
+
178
+ def get_system_info(self) -> Dict[str, Any]:
179
+ """
180
+ Get system information.
181
+
182
+ Returns:
183
+ Dictionary with Python version and system metrics
184
+
185
+ %%AI_HINT: System-level information using psutil%%
186
+ """
187
+ import sys
188
+ import psutil
189
+
190
+ try:
191
+ return {
192
+ 'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
193
+ 'cpu_percent': round(psutil.cpu_percent(interval=0.1), 1),
194
+ 'memory_percent': round(psutil.virtual_memory().percent, 1),
195
+ 'disk_percent': round(psutil.disk_usage('/').percent, 1),
196
+ }
197
+ except Exception as e:
198
+ self.logger.error(f"Error getting system info: {e}")
199
+ return {
200
+ 'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
201
+ 'cpu_percent': 0,
202
+ 'memory_percent': 0,
203
+ 'disk_percent': 0,
204
+ 'error': str(e)
205
+ }
@@ -0,0 +1,134 @@
1
+ """
2
+ Test script for SPA routing logic.
3
+
4
+ Run this to verify that URL paths resolve correctly to static files.
5
+ """
6
+
7
+ from pathlib import Path
8
+ import django_cfg
9
+
10
+
11
+ def test_resolve_spa_path():
12
+ """Test the SPA path resolution logic."""
13
+
14
+ # Simulate the base directory
15
+ base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / 'admin'
16
+
17
+ print(f"Base directory: {base_dir}")
18
+ print(f"Base directory exists: {base_dir.exists()}\n")
19
+
20
+ # Test cases
21
+ test_cases = [
22
+ # (input_path, expected_output_path, description)
23
+ ('', 'index.html', 'Root path'),
24
+ ('/', 'index.html', 'Root path with slash'),
25
+ ('private/centrifugo', 'private/centrifugo/index.html', 'Nested route without trailing slash'),
26
+ ('private/centrifugo/', 'private/centrifugo/index.html', 'Nested route with trailing slash'),
27
+ ('private', 'private.html', 'Single segment route'),
28
+ ('private/', 'private/index.html', 'Single segment with trailing slash'),
29
+ ('_next/static/chunks/app.js', '_next/static/chunks/app.js', 'Static asset (exact match)'),
30
+ ('favicon.ico', 'favicon.ico', 'Favicon (exact match)'),
31
+ ('unknown/route', 'index.html', 'Unknown route (SPA fallback)'),
32
+ ]
33
+
34
+ def resolve_spa_path(base_dir, path):
35
+ """
36
+ Exact copy of the view's _resolve_spa_path method.
37
+
38
+ This should match the logic in views.py exactly.
39
+ """
40
+ # Handle empty path (done in view before calling this method)
41
+ if not path or path == '/':
42
+ path = 'index.html'
43
+ return path
44
+
45
+ file_path = base_dir / path
46
+ path_normalized = path.rstrip('/')
47
+
48
+ # Strategy 1: Exact file match (for static assets like JS, CSS, images)
49
+ if file_path.exists() and file_path.is_file():
50
+ return path
51
+
52
+ # Strategy 2: Try path/index.html (most common for SPA routes)
53
+ index_in_dir = base_dir / path_normalized / 'index.html'
54
+ if index_in_dir.exists():
55
+ resolved_path = f"{path_normalized}/index.html"
56
+ return resolved_path
57
+
58
+ # Strategy 3: Try with trailing slash + index.html
59
+ if path.endswith('/'):
60
+ index_path = path + 'index.html'
61
+ if (base_dir / index_path).exists():
62
+ return index_path
63
+
64
+ # Strategy 4: Try path.html (Next.js static export behavior)
65
+ html_file = base_dir / (path_normalized + '.html')
66
+ if html_file.exists():
67
+ resolved_path = path_normalized + '.html'
68
+ return resolved_path
69
+
70
+ # Strategy 5: Check if it's a directory without index.html
71
+ if file_path.exists() and file_path.is_dir():
72
+ # Try index.html in that directory
73
+ index_in_existing_dir = file_path / 'index.html'
74
+ if index_in_existing_dir.exists():
75
+ resolved_path = f"{path_normalized}/index.html"
76
+ return resolved_path
77
+
78
+ # Strategy 6: SPA fallback - serve root index.html
79
+ # This allows client-side routing to handle unknown routes
80
+ root_index = base_dir / 'index.html'
81
+ if root_index.exists():
82
+ return 'index.html'
83
+
84
+ # Strategy 7: Nothing found - return original path (will 404)
85
+ return path
86
+
87
+ print("=" * 80)
88
+ print("SPA ROUTING TEST RESULTS")
89
+ print("=" * 80)
90
+
91
+ passed = 0
92
+ failed = 0
93
+
94
+ for input_path, expected, description in test_cases:
95
+ result = resolve_spa_path(base_dir, input_path)
96
+
97
+ # Check if file exists
98
+ resolved_file = base_dir / result
99
+ file_exists = resolved_file.exists() and resolved_file.is_file()
100
+
101
+ # Determine test status
102
+ if result == expected:
103
+ status = "✅ PASS"
104
+ passed += 1
105
+ else:
106
+ status = "❌ FAIL"
107
+ failed += 1
108
+
109
+ print(f"\n{status} - {description}")
110
+ print(f" Input: '{input_path}'")
111
+ print(f" Expected: '{expected}'")
112
+ print(f" Got: '{result}'")
113
+ print(f" File exists: {file_exists}")
114
+
115
+ if result != expected:
116
+ print(f" ⚠️ Mismatch detected!")
117
+
118
+ print("\n" + "=" * 80)
119
+ print(f"TEST SUMMARY: {passed} passed, {failed} failed out of {len(test_cases)} tests")
120
+ print("=" * 80)
121
+
122
+ # Additional info
123
+ print("\n📁 File structure check:")
124
+ if base_dir.exists():
125
+ print(f" - index.html: {(base_dir / 'index.html').exists()}")
126
+ print(f" - private.html: {(base_dir / 'private.html').exists()}")
127
+ print(f" - private/index.html: {(base_dir / 'private' / 'index.html').exists()}")
128
+ print(f" - private/centrifugo/index.html: {(base_dir / 'private' / 'centrifugo' / 'index.html').exists()}")
129
+ else:
130
+ print(f" ⚠️ Base directory does not exist!")
131
+
132
+
133
+ if __name__ == '__main__':
134
+ test_resolve_spa_path()
@@ -55,34 +55,8 @@ class NextJSStaticView(View):
55
55
  path = 'index.html'
56
56
  logger.debug(f"Root path requested, serving: {path}")
57
57
 
58
- # Handle trailing slash - try multiple strategies like a static file server
59
- if path.endswith('/') and path != '/':
60
- path_without_slash = path.rstrip('/')
61
-
62
- # Strategy 1: Try directory/index.html (most common for static servers)
63
- index_path = path + 'index.html'
64
- if (base_dir / index_path).exists():
65
- path = index_path
66
- # Strategy 2: Try path.html (Next.js static export behavior)
67
- elif (base_dir / (path_without_slash + '.html')).exists():
68
- path = path_without_slash + '.html'
69
- # Strategy 3: Keep as directory path (will fail later if directory doesn't exist)
70
- else:
71
- path = index_path # Default to index.html in directory
72
-
73
- # For routes without extension, try .html (Next.js static export behavior)
74
- file_path = base_dir / path
75
- if not file_path.exists() and not path.endswith('.html') and '.' not in Path(path).name:
76
- html_path = path + '.html'
77
- html_file = base_dir / html_path
78
- if html_file.exists():
79
- path = html_path
80
- else:
81
- # Try path/index.html as fallback
82
- index_path = path + '/index.html'
83
- index_file = base_dir / index_path
84
- if index_file.exists():
85
- path = index_path
58
+ # Resolve file path with SPA routing fallback strategy
59
+ path = self._resolve_spa_path(base_dir, path)
86
60
 
87
61
  # For HTML files, remove conditional GET headers to force full response
88
62
  # This allows JWT token injection (can't inject into 304 Not Modified responses)
@@ -116,6 +90,73 @@ class NextJSStaticView(View):
116
90
 
117
91
  return response
118
92
 
93
+ def _resolve_spa_path(self, base_dir, path):
94
+ """
95
+ Resolve SPA path with multiple fallback strategies.
96
+
97
+ Resolution order:
98
+ 1. Exact file match (e.g., script.js, style.css)
99
+ 2. path/index.html (e.g., private/centrifugo/index.html)
100
+ 3. path.html (e.g., private.html for /private)
101
+ 4. Fallback to root index.html for SPA routing
102
+
103
+ Examples:
104
+ /private/centrifugo → private/centrifugo/index.html
105
+ /private → private.html OR private/index.html
106
+ /_next/static/... → _next/static/... (exact match)
107
+ /unknown/route → index.html (SPA fallback)
108
+ """
109
+ file_path = base_dir / path
110
+
111
+ # Remove trailing slash for processing
112
+ path_normalized = path.rstrip('/')
113
+
114
+ # Strategy 1: Exact file match (for static assets like JS, CSS, images)
115
+ if file_path.exists() and file_path.is_file():
116
+ logger.debug(f"[SPA Router] Exact match: {path}")
117
+ return path
118
+
119
+ # Strategy 2: Try path/index.html (most common for SPA routes)
120
+ index_in_dir = base_dir / path_normalized / 'index.html'
121
+ if index_in_dir.exists():
122
+ resolved_path = f"{path_normalized}/index.html"
123
+ logger.debug(f"[SPA Router] Resolved {path} → {resolved_path}")
124
+ return resolved_path
125
+
126
+ # Strategy 3: Try with trailing slash + index.html
127
+ if path.endswith('/'):
128
+ index_path = path + 'index.html'
129
+ if (base_dir / index_path).exists():
130
+ logger.debug(f"[SPA Router] Trailing slash resolved: {index_path}")
131
+ return index_path
132
+
133
+ # Strategy 4: Try path.html (Next.js static export behavior)
134
+ html_file = base_dir / (path_normalized + '.html')
135
+ if html_file.exists():
136
+ resolved_path = path_normalized + '.html'
137
+ logger.debug(f"[SPA Router] HTML file match: {resolved_path}")
138
+ return resolved_path
139
+
140
+ # Strategy 5: Check if it's a directory without index.html
141
+ if file_path.exists() and file_path.is_dir():
142
+ # Try index.html in that directory
143
+ index_in_existing_dir = file_path / 'index.html'
144
+ if index_in_existing_dir.exists():
145
+ resolved_path = f"{path_normalized}/index.html"
146
+ logger.debug(f"[SPA Router] Directory with index: {resolved_path}")
147
+ return resolved_path
148
+
149
+ # Strategy 6: SPA fallback - serve root index.html
150
+ # This allows client-side routing to handle unknown routes
151
+ root_index = base_dir / 'index.html'
152
+ if root_index.exists():
153
+ logger.debug(f"[SPA Router] Fallback to index.html for route: {path}")
154
+ return 'index.html'
155
+
156
+ # Strategy 7: Nothing found - return original path (will 404)
157
+ logger.warning(f"[SPA Router] No match found for: {path}")
158
+ return path
159
+
119
160
  def _should_inject_jwt(self, request, response):
120
161
  """Check if JWT tokens should be injected."""
121
162
  # Only for authenticated users
django_cfg/apps/urls.py CHANGED
@@ -170,7 +170,6 @@ APP_URL_MAP = {
170
170
  ],
171
171
  "django_cfg.apps.centrifugo": [
172
172
  ("cfg/centrifugo/", "django_cfg.apps.centrifugo.urls"),
173
- ("cfg/centrifugo/admin/", "django_cfg.apps.centrifugo.urls_admin"),
174
173
  ],
175
174
  }
176
175
 
@@ -183,66 +183,8 @@ class InstalledAppsBuilder:
183
183
  # django-browser-reload not installed, skip it
184
184
  pass
185
185
 
186
- # Auto-detect dashboard apps from Unfold callback
187
- dashboard_apps = self._get_dashboard_apps_from_callback()
188
- apps.extend(dashboard_apps)
189
-
190
186
  return apps
191
187
 
192
- def _get_dashboard_apps_from_callback(self) -> List[str]:
193
- """
194
- Auto-detect dashboard apps from Unfold dashboard_callback setting.
195
-
196
- Extracts app names from callback paths like:
197
- - "api.dashboard.callbacks.main_dashboard_callback" → ["api.dashboard"]
198
- - "myproject.admin.callbacks.dashboard" → ["myproject.admin"]
199
-
200
- This allows django-cfg to automatically add dashboard apps to INSTALLED_APPS
201
- without requiring manual configuration.
202
-
203
- Returns:
204
- List of dashboard app names to add to INSTALLED_APPS
205
- """
206
- dashboard_apps = []
207
-
208
- # Check if Unfold is configured with a theme
209
- if not self.config.unfold or not self.config.unfold.theme:
210
- return dashboard_apps
211
-
212
- # Get dashboard callback path from theme
213
- callback_path = getattr(self.config.unfold.theme, "dashboard_callback", None)
214
- if not callback_path:
215
- return dashboard_apps
216
-
217
- try:
218
- # Parse callback path: "api.dashboard.callbacks.main_dashboard_callback"
219
- # Extract app part: "api.dashboard"
220
- parts = callback_path.split(".")
221
-
222
- # Look for common callback patterns
223
- callback_indicators = ["callbacks", "views", "handlers"]
224
-
225
- # Find the callback indicator and extract app path before it
226
- app_parts = []
227
- for i, part in enumerate(parts):
228
- if part in callback_indicators:
229
- app_parts = parts[:i] # Everything before the callback indicator
230
- break
231
-
232
- # If no callback indicator found, assume last part is function name
233
- if not app_parts and len(parts) > 1:
234
- app_parts = parts[:-1] # Everything except the last part
235
-
236
- if app_parts:
237
- app_name = ".".join(app_parts)
238
- dashboard_apps.append(app_name)
239
-
240
- except Exception:
241
- # If parsing fails, silently continue - dashboard callback is optional
242
- pass
243
-
244
- return dashboard_apps
245
-
246
188
  def _deduplicate(self, apps: List[str]) -> List[str]:
247
189
  """
248
190
  Remove duplicate apps while preserving order.
@@ -1,13 +1,12 @@
1
1
  """
2
2
  Django CFG Unfold Module
3
3
 
4
- Provides complete Unfold admin interface integration with dashboard,
5
- navigation, theming, and callback support.
4
+ Provides complete Unfold admin interface integration with
5
+ navigation and theming support.
6
6
  """
7
7
 
8
- from .callbacks import UnfoldCallbacks
9
- from .dashboard import DashboardManager, get_dashboard_manager
10
8
  from .models import *
9
+ from .navigation import NavigationManager, get_navigation_manager
11
10
  from .system_monitor import SystemMonitor
12
11
  from .tailwind import get_css_variables, get_unfold_colors
13
12
 
@@ -20,19 +19,10 @@ def get_system_monitor() -> SystemMonitor:
20
19
  globals()['_system_monitor'] = SystemMonitor()
21
20
  return globals()['_system_monitor']
22
21
 
23
- def get_unfold_callbacks() -> UnfoldCallbacks:
24
- """Get the global unfold callbacks instance."""
25
- global _unfold_callbacks
26
- if '_unfold_callbacks' not in globals():
27
- globals()['_unfold_callbacks'] = UnfoldCallbacks()
28
- return globals()['_unfold_callbacks']
29
-
30
22
  # Export main components
31
23
  __all__ = [
32
- 'DashboardManager',
33
- 'get_dashboard_manager',
34
- 'UnfoldCallbacks',
35
- 'get_unfold_callbacks',
24
+ 'NavigationManager',
25
+ 'get_navigation_manager',
36
26
  'SystemMonitor',
37
27
  'get_system_monitor',
38
28
  'get_unfold_colors',
@@ -48,15 +38,6 @@ __all__ = [
48
38
  'NavigationSection',
49
39
  'NavigationItemType',
50
40
  'SiteDropdownItem',
51
- 'StatCard',
52
- 'SystemHealthItem',
53
- 'QuickAction',
54
- 'DashboardWidget',
55
- 'DashboardData',
56
- 'ChartDataset',
57
- 'ChartData',
58
- 'TabConfiguration',
59
- 'TabItem',
60
41
  ]
61
42
 
62
43
  # Version info
@@ -12,18 +12,8 @@ from .config import (
12
12
  UnfoldTheme,
13
13
  UnfoldThemeConfig,
14
14
  )
15
- from .dashboard import (
16
- ChartData,
17
- ChartDataset,
18
- DashboardData,
19
- DashboardWidget,
20
- QuickAction,
21
- StatCard,
22
- SystemHealthItem,
23
- )
24
15
  from .dropdown import SiteDropdownItem
25
16
  from .navigation import NavigationItem, NavigationItemType, NavigationSection
26
- from .tabs import TabConfiguration, TabItem
27
17
 
28
18
  __all__ = [
29
19
  # Config models
@@ -41,17 +31,4 @@ __all__ = [
41
31
 
42
32
  # Dropdown models
43
33
  'SiteDropdownItem',
44
-
45
- # Dashboard models
46
- 'StatCard',
47
- 'SystemHealthItem',
48
- 'QuickAction',
49
- 'DashboardWidget',
50
- 'DashboardData',
51
- 'ChartDataset',
52
- 'ChartData',
53
-
54
- # Tab models
55
- 'TabConfiguration',
56
- 'TabItem',
57
34
  ]