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

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.96"
35
+ __version__ = "1.4.99"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -4,8 +4,9 @@ JWT tokens are automatically injected into HTML responses for authenticated user
4
4
  This is specific to Next.js frontend apps only.
5
5
 
6
6
  Features:
7
- - Automatic extraction of ZIP archives with timestamp comparison
8
- - Auto-reextraction when ZIP is newer than extracted directory
7
+ - Automatic extraction of ZIP archives with metadata comparison (size + mtime)
8
+ - Auto-reextraction when ZIP content changes (size or timestamp)
9
+ - Marker file (.zip_meta) tracks ZIP metadata for reliable comparison
9
10
  - Cache busting (no-store headers for HTML)
10
11
  - SPA routing with fallback strategies
11
12
  - JWT token injection for authenticated users
@@ -29,12 +30,13 @@ logger = logging.getLogger(__name__)
29
30
 
30
31
  class ZipExtractionMixin:
31
32
  """
32
- Mixin for automatic ZIP extraction with timestamp-based refresh.
33
+ Mixin for automatic ZIP extraction with metadata-based refresh.
33
34
 
34
35
  Provides intelligent ZIP archive handling:
35
36
  - Auto-extraction when directory doesn't exist
36
- - Auto-reextraction when ZIP is newer than extracted directory
37
- - Timestamp comparison ensures fresh builds are always deployed
37
+ - Auto-reextraction when ZIP metadata changes (size or mtime)
38
+ - Marker file (.zip_meta) tracks ZIP state for reliable comparison
39
+ - Works correctly in Docker where timestamps can be misleading
38
40
 
39
41
  Usage:
40
42
  class MyView(ZipExtractionMixin, View):
@@ -43,12 +45,16 @@ class ZipExtractionMixin:
43
45
 
44
46
  def extract_zip_if_needed(self, base_dir: Path, zip_path: Path, app_name: str) -> bool:
45
47
  """
46
- Extract ZIP archive if needed based on ZIP modification time comparison.
48
+ Extract ZIP archive if needed based on ZIP metadata (size + mtime) comparison.
47
49
 
48
50
  Logic:
49
51
  1. If directory doesn't exist → extract
50
- 2. If ZIP is newer than directory remove and re-extract
51
- 3. If directory is newer than ZIPuse existing
52
+ 2. If marker file doesn't exist → extract
53
+ 3. If ZIP metadata changed (size or mtime) remove and re-extract
54
+ 4. If metadata matches → use existing
55
+
56
+ Uses marker file (.zip_meta) to track ZIP metadata. More reliable than
57
+ just mtime comparison, especially in Docker where timestamps can be misleading.
52
58
 
53
59
  Args:
54
60
  base_dir: Target directory for extraction
@@ -65,30 +71,40 @@ class ZipExtractionMixin:
65
71
  logger.error(f"[{app_name}] ZIP not found: {zip_path}")
66
72
  return False
67
73
 
68
- # Get ZIP modification time
69
- zip_mtime = datetime.fromtimestamp(zip_path.stat().st_mtime)
74
+ # Get ZIP metadata (size + mtime for reliable comparison)
75
+ zip_stat = zip_path.stat()
76
+ current_meta = f"{zip_stat.st_size}:{zip_stat.st_mtime}"
77
+
78
+ # Marker file stores ZIP metadata
79
+ marker_file = base_dir / '.zip_meta'
70
80
 
71
81
  # Priority 1: If directory doesn't exist at all - always extract
72
82
  if not base_dir.exists():
73
83
  should_extract = True
74
84
  logger.info(f"[{app_name}] Directory doesn't exist, will extract")
75
85
 
76
- # Priority 2: Directory exists - compare timestamps
86
+ # Priority 2: Marker file doesn't exist - extract (first run or corrupted)
87
+ elif not marker_file.exists():
88
+ should_extract = True
89
+ logger.info(f"[{app_name}] No marker file found, will extract")
90
+
91
+ # Priority 3: Compare stored metadata with current ZIP metadata
77
92
  else:
78
- # Get directory modification time
79
- dir_mtime = datetime.fromtimestamp(base_dir.stat().st_mtime)
80
-
81
- # If ZIP is newer than directory - re-extract
82
- if zip_mtime > dir_mtime:
83
- logger.info(f"[{app_name}] ZIP is newer (ZIP: {zip_mtime}, DIR: {dir_mtime}), re-extracting")
84
- try:
85
- shutil.rmtree(base_dir)
86
- should_extract = True
87
- except Exception as e:
88
- logger.error(f"[{app_name}] Failed to remove old directory: {e}")
89
- return False
90
- else:
91
- logger.debug(f"[{app_name}] Directory is up-to-date (DIR: {dir_mtime} >= ZIP: {zip_mtime})")
93
+ try:
94
+ stored_meta = marker_file.read_text().strip()
95
+ if stored_meta != current_meta:
96
+ logger.info(f"[{app_name}] ZIP metadata changed (stored: {stored_meta}, current: {current_meta}), re-extracting")
97
+ try:
98
+ shutil.rmtree(base_dir)
99
+ should_extract = True
100
+ except Exception as e:
101
+ logger.error(f"[{app_name}] Failed to remove old directory: {e}")
102
+ return False
103
+ else:
104
+ logger.info(f"[{app_name}] ZIP unchanged (meta: {current_meta}), using existing directory")
105
+ except Exception as e:
106
+ logger.warning(f"[{app_name}] Failed to read marker file: {e}, will re-extract")
107
+ should_extract = True
92
108
 
93
109
  # Extract ZIP if needed
94
110
  if should_extract:
@@ -97,7 +113,10 @@ class ZipExtractionMixin:
97
113
  base_dir.parent.mkdir(parents=True, exist_ok=True)
98
114
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
99
115
  zip_ref.extractall(base_dir)
100
- logger.info(f"[{app_name}] Successfully extracted {zip_path.name}")
116
+
117
+ # Write marker file with current metadata
118
+ marker_file.write_text(current_meta)
119
+ logger.info(f"[{app_name}] Successfully extracted {zip_path.name} and saved marker (meta: {current_meta})")
101
120
  return True
102
121
  except Exception as e:
103
122
  logger.error(f"[{app_name}] Failed to extract: {e}")
@@ -114,7 +133,7 @@ class NextJSStaticView(ZipExtractionMixin, View):
114
133
 
115
134
  Features:
116
135
  - Serves Next.js static export files like a static file server
117
- - Smart ZIP extraction: auto-refreshes when ZIP is newer than directory
136
+ - Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
118
137
  - Automatically injects JWT tokens for authenticated users
119
138
  - Tokens injected into HTML responses only
120
139
  - Handles Next.js client-side routing (.html fallback)
@@ -123,9 +142,10 @@ class NextJSStaticView(ZipExtractionMixin, View):
123
142
 
124
143
  ZIP Extraction Logic:
125
144
  - If directory doesn't exist: extract from ZIP
126
- - If ZIP is newer than directory: remove and re-extract
127
- - If directory is up-to-date: use existing files
128
- - This ensures fresh builds are always deployed automatically
145
+ - If marker file missing: extract from ZIP
146
+ - If ZIP metadata changed: remove and re-extract
147
+ - If metadata matches: use existing files
148
+ - Marker file (.zip_meta) ensures reliable comparison in Docker
129
149
 
130
150
  Path resolution examples:
131
151
  - /cfg/admin/ → /cfg/admin/index.html
@@ -63,7 +63,7 @@ class NavigationManager(BaseCfgModule):
63
63
  separator=True,
64
64
  collapsible=True,
65
65
  items=[
66
- NavigationItem(title="Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/admin/private/centrifugo"),
66
+ NavigationItem(title="Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/admin/admin/centrifugo"),
67
67
  NavigationItem(title="Logs", icon=Icons.LIST_ALT, link=str(reverse_lazy("admin:django_cfg_centrifugo_centrifugolog_changelist"))),
68
68
  ]
69
69
  )
@@ -4,19 +4,18 @@ Views for Next.js admin integration.
4
4
  Serves Next.js static files with SPA routing support and JWT injection.
5
5
 
6
6
  Features:
7
- - Priority-based ZIP resolution (solution project package fallback)
8
- - Automatic extraction with timestamp comparison (ZIP mtime vs directory mtime)
9
- - Cache busting (no-store headers + timestamp query params)
7
+ - Automatic extraction with metadata comparison (ZIP size + mtime vs marker file)
8
+ - Cache busting (no-store headers for HTML)
10
9
  - SPA routing with fallback strategies
11
10
  - JWT token injection for authenticated users
12
11
 
13
- ZIP Resolution Priority:
14
- 1. Solution project: {BASE_DIR}/static/nextjs_admin.zip → {BASE_DIR}/static/nextjs_admin/
15
- 2. Package fallback: django_cfg/static/frontend/nextjs_admin.zip → django_cfg/static/frontend/nextjs_admin/
12
+ ZIP Location:
13
+ - Solution project: {BASE_DIR}/static/nextjs_admin.zip → {BASE_DIR}/static/nextjs_admin/
16
14
 
17
15
  Extraction Logic:
18
- - Compares ZIP mtime with directory mtime
19
- - Re-extracts only when ZIP is newer
16
+ - Marker file (.zip_meta) tracks ZIP metadata (size:mtime)
17
+ - Re-extracts when metadata changes (size or timestamp)
18
+ - Reliable in Docker where timestamps can be misleading
20
19
  - Ensures fresh builds are deployed automatically
21
20
  """
22
21
 
@@ -40,22 +39,21 @@ class NextJsAdminView(ZipExtractionMixin, LoginRequiredMixin, View):
40
39
  Serve Next.js admin panel with JWT injection and SPA routing.
41
40
 
42
41
  Features:
43
- - Serves Next.js static build files
44
- - Priority-based ZIP resolution (solution first, package fallback)
45
- - Smart ZIP extraction: compares ZIP mtime vs directory mtime
42
+ - Serves Next.js static build files from solution project
43
+ - Smart ZIP extraction: metadata comparison (size + mtime) with marker file
46
44
  - Cache busting: no-store headers for HTML files
47
45
  - Automatic JWT token injection for authenticated users
48
46
  - SPA routing support (path/to/route → path/to/route/index.html)
49
47
 
50
- ZIP Resolution Priority:
51
- 1. Solution: {BASE_DIR}/static/nextjs_admin.zip → {BASE_DIR}/static/nextjs_admin/
52
- 2. Package: django_cfg/static/frontend/nextjs_admin.zip → django_cfg/static/frontend/nextjs_admin/
48
+ ZIP Location:
49
+ - {BASE_DIR}/static/nextjs_admin.zip → {BASE_DIR}/static/nextjs_admin/
53
50
 
54
51
  ZIP Extraction Logic:
55
52
  - If directory doesn't exist: extract from ZIP
56
- - If ZIP is newer than directory: remove and re-extract
57
- - If directory is up-to-date: use existing files
58
- - Ensures fresh builds are deployed automatically
53
+ - If marker file missing: extract from ZIP
54
+ - If ZIP metadata changed: remove and re-extract
55
+ - If metadata matches: use existing files
56
+ - Marker file (.zip_meta) ensures reliable comparison in Docker
59
57
 
60
58
  URL Examples:
61
59
  /cfg/nextjs-admin/admin/ → admin/index.html
@@ -74,28 +72,18 @@ class NextJsAdminView(ZipExtractionMixin, LoginRequiredMixin, View):
74
72
 
75
73
  nextjs_config = config.nextjs_admin
76
74
 
77
- # Priority 1: Try solution project static directory first
75
+ # Use solution project static directory
78
76
  from django.conf import settings
79
- solution_zip = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin.zip'
80
- solution_base_dir = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin'
81
-
82
- # Priority 2: Fallback to django_cfg package
83
- default_zip = Path(django_cfg.__file__).parent / 'static' / 'frontend' / 'nextjs_admin.zip'
84
- default_base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / 'nextjs_admin'
85
-
86
- # Choose which ZIP to use
87
- if solution_zip.exists():
88
- zip_path = solution_zip
89
- base_dir = solution_base_dir
90
- logger.info(f"[nextjs_admin] Using ZIP from solution project: {solution_zip}")
91
- elif default_zip.exists():
92
- zip_path = default_zip
93
- base_dir = default_base_dir
94
- logger.info(f"[nextjs_admin] Using ZIP from django_cfg package: {default_zip}")
95
- else:
96
- logger.error(f"[nextjs_admin] No ZIP found in solution ({solution_zip}) or package ({default_zip})")
77
+ zip_path = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin.zip'
78
+ base_dir = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin'
79
+
80
+ # Check if ZIP exists
81
+ if not zip_path.exists():
82
+ logger.error(f"[nextjs_admin] ZIP not found: {zip_path}")
97
83
  return render(request, 'frontend/404.html', status=404)
98
84
 
85
+ logger.info(f"[nextjs_admin] Using ZIP from solution project: {zip_path}")
86
+
99
87
  # Extract ZIP if needed using mixin
100
88
  if not self.extract_zip_if_needed(base_dir, zip_path, 'nextjs_admin'):
101
89
  return render(request, 'frontend/404.html', status=404)
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.96"
7
+ version = "1.4.99"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
Binary file
@@ -9,28 +9,80 @@
9
9
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
10
  <link rel="stylesheet" href="{% static 'admin/css/dashboard.css' %}">
11
11
  <style>
12
+ /* Make content container full height and full width */
13
+ #content {
14
+ display: flex;
15
+ flex-direction: column;
16
+ height: 100%;
17
+ }
18
+
19
+ /* Remove container centering and width limits */
20
+ #content.container,
21
+ #content.mx-auto,
22
+ #content .mx-auto {
23
+ margin-left: 0 !important;
24
+ margin-right: 0 !important;
25
+ max-width: none !important;
26
+ }
27
+
28
+ /* Also remove from unfold container component and make it full height */
29
+ #content > .mx-auto {
30
+ margin-left: 0 !important;
31
+ margin-right: 0 !important;
32
+ max-width: none !important;
33
+ height: 100%;
34
+ }
35
+
36
+ /* Remove horizontal padding on mobile for @container */
37
+ @media (max-width: 768px) {
38
+ div[class*="@container"].px-4 {
39
+ padding-left: 0 !important;
40
+ padding-right: 0 !important;
41
+ }
42
+ }
43
+
44
+ /* Make Alpine tabs wrapper full height */
45
+ #content [x-data] {
46
+ height: 100%;
47
+ display: flex;
48
+ flex-direction: column;
49
+ }
50
+
51
+ /* Make tab content blocks full height */
52
+ #content [x-data] > div[x-show] {
53
+ flex: 1;
54
+ display: flex;
55
+ flex-direction: column;
56
+ }
57
+
12
58
  .nextjs-dashboard-iframe {
13
59
  width: 100%;
14
- min-height: 500px;
15
- height: 500px;
60
+ height: 100%;
16
61
  border: none;
17
62
  display: block;
18
63
  visibility: hidden;
19
64
  opacity: 0;
20
- transition: height 0.2s ease-in-out, opacity 0.3s ease-in-out;
65
+ transition: opacity 0.3s ease-in-out;
21
66
  }
22
67
 
23
68
  .nextjs-dashboard-iframe.loaded {
24
69
  visibility: visible;
25
70
  opacity: 1;
26
- height: auto;
27
- min-height: 600px;
28
71
  }
29
72
 
30
73
  .iframe-container {
31
74
  position: relative;
32
75
  background: transparent;
33
- min-height: 500px;
76
+ flex: 1;
77
+ display: flex;
78
+ flex-direction: column;
79
+ border: 1px solid rgba(209, 213, 219, 0.2);
80
+ border-radius: 0.375rem; /* rounded-md */
81
+ overflow: hidden;
82
+ }
83
+
84
+ .dark .iframe-container {
85
+ border-color: rgba(75, 85, 99, 0.2);
34
86
  }
35
87
 
36
88
  .iframe-loading {
@@ -67,6 +119,8 @@
67
119
 
68
120
  {% block breadcrumbs %}{% endblock %}
69
121
 
122
+ {% block coltype %}{% endblock %}
123
+
70
124
  {% block title %}
71
125
  {% if subtitle %}{{ subtitle }} | {% endif %}
72
126
  {{ title }} | {{ site_title|default:'Django site admin' }}
@@ -102,7 +156,7 @@
102
156
  }">
103
157
  {% if is_frontend_dev_mode %}
104
158
  <!-- Development Mode Badge -->
105
- <div class="bg-yellow-100 dark:bg-yellow-900 border-l-4 border-yellow-500 text-yellow-700 dark:text-yellow-300 p-4 mb-4" role="alert">
159
+ <div class="bg-yellow-100 dark:bg-yellow-900 border-l-4 border-yellow-500 text-yellow-700 dark:text-yellow-300" role="alert">
106
160
  <div class="flex items-center">
107
161
  <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
108
162
  <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
@@ -118,14 +172,15 @@
118
172
  <!-- Tab Navigation -->
119
173
  {% has_nextjs_external_admin as is_external_enabled %}
120
174
  {% if is_external_enabled %}
121
- <div class="mb-6">
175
+ <div>
122
176
  <div class="border-b border-gray-300 dark:border-gray-600" style="border-bottom-color: rgba(209, 213, 219, 0.2);">
123
177
  <style>
124
178
  .dark [style*="border-bottom-color"] {
125
179
  border-bottom-color: rgba(75, 85, 99, 0.2) !important;
126
180
  }
127
181
  </style>
128
- <nav class="-mb-px flex space-x-8 px-4" aria-label="Dashboard Tabs">
182
+ <div class="flex items-center justify-between px-4">
183
+ <nav class="-mb-px flex space-x-8" aria-label="Dashboard Tabs">
129
184
  <button @click="switchTab('builtin')"
130
185
  class="whitespace-nowrap py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-200"
131
186
  :class="activeTab === 'builtin'
@@ -144,6 +199,15 @@
144
199
  <span>{% nextjs_external_admin_title %}</span>
145
200
  </button>
146
201
  </nav>
202
+
203
+ <!-- Version info -->
204
+ <div class="py-4 text-xs text-gray-400 dark:text-gray-500">
205
+ {% load django_cfg %}
206
+ <a href="{% lib_site_url %}" class="text-blue-600 hover:text-blue-700">
207
+ {% lib_name %}
208
+ </a>
209
+ </div>
210
+ </div>
147
211
  </div>
148
212
  </div>
149
213
  {% endif %}
@@ -163,7 +227,6 @@
163
227
  data-original-src="{% nextjs_admin_url %}"
164
228
  title="Next.js Dashboard"
165
229
  sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation"
166
- scrolling="no"
167
230
  ></iframe>
168
231
  </div>
169
232
  </div>
@@ -179,31 +242,16 @@
179
242
 
180
243
  <iframe
181
244
  id="nextjs-dashboard-iframe-nextjs"
182
- class="nextjs-dashboard-iframe nextjs-external-iframe"
245
+ class="nextjs-dashboard-iframe"
183
246
  src="{% nextjs_external_admin_url %}"
184
247
  data-original-src="{% nextjs_external_admin_url %}"
185
248
  title="{% nextjs_external_admin_title %}"
186
249
  sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation"
187
- scrolling="no"
188
250
  ></iframe>
189
251
  </div>
190
252
  </div>
191
253
  {% endif %}
192
254
  </div>
193
-
194
- <!-- Footer Section -->
195
- <div class="text-center py-8 mt-5">
196
- <p class="text-sm text-gray-500 dark:text-gray-400">
197
- {% load django_cfg %}
198
- <a href="{% lib_site_url %}" class="text-blue-600 hover:text-blue-700 font-medium">
199
- {% lib_name %}
200
- </a>
201
- <span class="mx-2">•</span>
202
- <a href="{% lib_health_url %}" class="text-blue-600 hover:text-blue-700">
203
- System Health
204
- </a>
205
- </p>
206
- </div>
207
255
  {% endcomponent %}
208
256
  {% endblock %}
209
257
 
@@ -392,23 +440,31 @@
392
440
  break;
393
441
 
394
442
  case 'iframe-resize':
395
- if (data?.height && typeof data.height === 'number') {
396
- // First resize - show iframe immediately
397
- if (!iframe.classList.contains('loaded')) {
398
- console.log('[Django-CFG] First resize received - showing iframe');
399
- if (loading) loading.classList.add('hidden');
400
- iframe.classList.add('loaded');
401
- }
402
-
403
- // Debounce resize updates (300ms delay)
404
- if (resizeTimer) {
405
- clearTimeout(resizeTimer);
406
- }
407
-
408
- resizeTimer = setTimeout(() => {
409
- const newHeight = Math.max(600, data.height + 50);
410
- iframe.style.height = newHeight + 'px';
411
- }, 300);
443
+ // DISABLED: Fixed height 80vh instead of dynamic resize
444
+ // if (data?.height && typeof data.height === 'number') {
445
+ // // First resize - show iframe immediately
446
+ // if (!iframe.classList.contains('loaded')) {
447
+ // console.log('[Django-CFG] First resize received - showing iframe');
448
+ // if (loading) loading.classList.add('hidden');
449
+ // iframe.classList.add('loaded');
450
+ // }
451
+ //
452
+ // // Debounce resize updates (300ms delay)
453
+ // if (resizeTimer) {
454
+ // clearTimeout(resizeTimer);
455
+ // }
456
+ //
457
+ // resizeTimer = setTimeout(() => {
458
+ // const newHeight = Math.max(600, data.height + 50);
459
+ // iframe.style.height = newHeight + 'px';
460
+ // }, 300);
461
+ // }
462
+
463
+ // Show iframe on first resize event
464
+ if (!iframe.classList.contains('loaded')) {
465
+ console.log('[Django-CFG] First resize received - showing iframe');
466
+ if (loading) loading.classList.add('hidden');
467
+ iframe.classList.add('loaded');
412
468
  }
413
469
  break;
414
470
 
@@ -6,7 +6,6 @@ Provides template tags for accessing django-cfg configuration constants.
6
6
 
7
7
  import os
8
8
  import socket
9
- import time
10
9
  from django import template
11
10
  from django.conf import settings
12
11
  from django.utils.safestring import mark_safe
@@ -175,36 +174,30 @@ def nextjs_admin_url(path=''):
175
174
  2. Otherwise → /cfg/admin/admin/{path} (static files)
176
175
 
177
176
  Note: Port 3000 is reserved for external Next.js admin (Tab 2).
177
+ Both tabs use /admin route for consistency.
178
178
 
179
179
  Usage in template:
180
180
  {% load django_cfg %}
181
181
  <iframe src="{% nextjs_admin_url %}"></iframe>
182
- <iframe src="{% nextjs_admin_url 'centrifugo' %}"></iframe>
182
+ <iframe src="{% nextjs_admin_url 'crypto' %}"></iframe>
183
183
  """
184
184
  # Normalize path - remove leading/trailing slashes
185
185
  path = path.strip('/')
186
186
 
187
- # Add cache busting parameter (timestamp in milliseconds)
188
- cache_buster = f'_={int(time.time() * 1000)}'
189
-
190
187
  if not settings.DEBUG:
191
- # Production mode: always use static files with cache buster
192
- base_url = f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
193
- return f'{base_url}?{cache_buster}'
188
+ # Production mode: always use static files with /admin route
189
+ return f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
194
190
 
195
191
  # Check if port 3001 is available for Tab 1 (built-in admin)
196
192
  port_3001_available = _is_port_available('localhost', 3001)
197
193
 
198
194
  if port_3001_available:
199
- # Dev server is running on 3001 - use it (builtin admin has no /admin prefix)
200
- base_url = 'http://localhost:3001'
201
- url = f'{base_url}/{path}' if path else base_url
202
- return f'{url}?{cache_buster}'
195
+ # Dev server is running on 3001 - use /admin route for consistency
196
+ base_url = 'http://localhost:3001/admin'
197
+ return f'{base_url}/{path}' if path else base_url
203
198
  else:
204
- # No dev server or dev server stopped - use static files with cache buster
205
- # Static files are served from /cfg/admin/ but ZIP contains files in root (no /admin prefix)
206
- base_url = f'/cfg/admin/{path}' if path else '/cfg/admin/'
207
- return f'{base_url}?{cache_buster}'
199
+ # No dev server - use static files with /admin route
200
+ return f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
208
201
 
209
202
 
210
203
  @register.simple_tag
@@ -280,20 +273,15 @@ def nextjs_external_admin_url(route=''):
280
273
 
281
274
  route = route.strip('/')
282
275
 
283
- # Add cache busting parameter (timestamp in milliseconds)
284
- cache_buster = f'_={int(time.time() * 1000)}'
285
-
286
276
  # Auto-detect development mode: DEBUG=True + port 3000 available
287
277
  if settings.DEBUG and _is_port_available('localhost', 3000):
288
278
  # Development mode: solution project on port 3000
289
279
  # Routes start with /admin in Next.js (e.g., /admin, /admin/crypto)
290
280
  base_url = 'http://localhost:3000/admin'
291
- url = f'{base_url}/{route}' if route else base_url
292
- return f'{url}?{cache_buster}'
281
+ return f'{base_url}/{route}' if route else base_url
293
282
  else:
294
283
  # Production mode: use relative URL - Django serves from extracted ZIP with /admin prefix
295
- base_url = f"/cfg/nextjs-admin/admin/{route}" if route else "/cfg/nextjs-admin/admin/"
296
- return f'{base_url}?{cache_buster}'
284
+ return f"/cfg/nextjs-admin/admin/{route}" if route else "/cfg/nextjs-admin/admin/"
297
285
  except Exception:
298
286
  return ''
299
287
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.96
3
+ Version: 1.4.99
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=UbaqH6yGdQLEgNwn0Hmp7oKhbtKyZTVYU4UIvyIPn_c,1620
2
+ django_cfg/__init__.py,sha256=2_6He9GXwm8Mx_NZieYsriBE-ZI6kzIIoeAoLHEJgEs,1620
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=y4Z3rnYsHBE0TehpwAIPaxr---mkvyKrZGGsNwYso74,1398
5
5
  django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
@@ -228,7 +228,7 @@ django_cfg/apps/frontend/apps.py,sha256=9eEK0Tq2nOsol7xSK5aobdwTDJTJrWx6Iy1I1DQb
228
228
  django_cfg/apps/frontend/setup.py,sha256=TxKQwQw4xTF6VSyhrQBzbUsdsVQR9JHdjc36LZKeQh4,2444
229
229
  django_cfg/apps/frontend/test_routing.py,sha256=fshJOR9ln7m3gXY9EI1_ix_6E5xua6DR264b16RIF-w,4832
230
230
  django_cfg/apps/frontend/urls.py,sha256=Vz22_2i2w1J0KQYDCxHnTF5rUf32kUUSBDJZrP07XgY,284
231
- django_cfg/apps/frontend/views.py,sha256=SOYOgZdJWnNQLOMLY1Lx7xkGnI_pS4OKV5UNydQsh48,13922
231
+ django_cfg/apps/frontend/views.py,sha256=JPTJRPCGY3zLLiiz1F9CsZiLkXbQ5RfnViJZzBUpV7o,15023
232
232
  django_cfg/apps/frontend/templates/frontend/404.html,sha256=LCFig_dcgDDmYKhgOLu8R2KDs_aQS6Es6rAxLTAEXWs,2175
233
233
  django_cfg/apps/knowbase/README.md,sha256=HXt_J6WCN-LsMhA7p9mdvih07_vp_r_hkPdmqHhNEeo,3965
234
234
  django_cfg/apps/knowbase/__init__.py,sha256=cfGnxDQwjajPhUoleKkgvdabJcB0LdXEglnsBojKkPo,1045
@@ -1010,7 +1010,7 @@ django_cfg/modules/django_twilio/templates/guide.md,sha256=nZfwx-sgWyK5NApm93zOe
1010
1010
  django_cfg/modules/django_twilio/templates/sendgrid_otp_email.html,sha256=sXR6_D9hmOFfk9CrfPizpLddVhkRirBWpZd_ioEsxVk,6671
1011
1011
  django_cfg/modules/django_twilio/templates/sendgrid_test_data.json,sha256=fh1VyuSiDELHsS_CIz9gp7tlsMAEjaDOoqbAPSZ3yyo,339
1012
1012
  django_cfg/modules/django_unfold/__init__.py,sha256=Uquez6xgPUIc8FBMP7qif-adRYQSjQ2dBHxnJPom3eQ,1337
1013
- django_cfg/modules/django_unfold/navigation.py,sha256=BSJk4NxTpkTh2Pf_NTkLf77EJSm54ta3l1-SMzYKz2U,11521
1013
+ django_cfg/modules/django_unfold/navigation.py,sha256=4lEFyL-qB0aYMYDsqSU24XOIuMGaBDHci6f6adbQjTU,11519
1014
1014
  django_cfg/modules/django_unfold/system_monitor.py,sha256=KcrTa5irstdB1pDKe3sC0zl4tz9LVjhp7xvbqUM4YVk,6781
1015
1015
  django_cfg/modules/django_unfold/tailwind.py,sha256=NY8nWcUdQw61oNmjPoPl7agcTb-r5IqcpFYy35MNsTM,9107
1016
1016
  django_cfg/modules/django_unfold/utils.py,sha256=5aFaceRc9B3eXfpOVyKRD2wWqFt8KcHxjQg54oD7Oyg,4482
@@ -1021,7 +1021,7 @@ django_cfg/modules/django_unfold/models/navigation.py,sha256=PPEeqA2HBaA1-VjADiX
1021
1021
  django_cfg/modules/nextjs_admin/__init__.py,sha256=lfrZYyNRExH3Z5De8G4hQBIZoFlW5Ejze3couNrztbY,312
1022
1022
  django_cfg/modules/nextjs_admin/apps.py,sha256=HxVUMmWTKdYpwJ00iIfWVFsBzsawsOVhEPZqjk_izjI,347
1023
1023
  django_cfg/modules/nextjs_admin/urls.py,sha256=7n0yStm0WNchw14Rtu_mgsIA3WKQsYP9WZt3-YOUWjU,603
1024
- django_cfg/modules/nextjs_admin/views.py,sha256=4gzdo2gxY2zSs9MxxZc2Z7FfJHGsg5ZcGXWOo4i3sLA,11246
1024
+ django_cfg/modules/nextjs_admin/views.py,sha256=SELkdq6ioNqFLInDKfYyRSZRaXRtuGAVjQfE8sACQ_Q,10382
1025
1025
  django_cfg/modules/nextjs_admin/models/__init__.py,sha256=WGw9KXcYd1O9AoA_bpMoz2gLZUlRzjGmUBjjbObcUi0,100
1026
1026
  django_cfg/modules/nextjs_admin/models/config.py,sha256=0ADqLuiywSCQfx_z9dkwjFCca3lr3F2uQffIjTr_QXw,5864
1027
1027
  django_cfg/modules/nextjs_admin/templatetags/__init__.py,sha256=ChVBnJggCIY8rMhfyJFoA8k0qKo-8FtJknrk54Vx4wM,51
@@ -1050,7 +1050,7 @@ django_cfg/static/admin/js/alpine/commands-section.js,sha256=8z2MQNwZF9Tx_2EK1AY
1050
1050
  django_cfg/static/admin/js/alpine/dashboard-tabs.js,sha256=ob8Q_I9lFLDv_hFERXgTyvqMDBspAGfzCxI_7slRur4,1354
1051
1051
  django_cfg/static/admin/js/alpine/system-metrics.js,sha256=m-Fg55K_vpHXToD46PXL9twl4OBF_V9MONvbSWbQqDw,440
1052
1052
  django_cfg/static/admin/js/alpine/toggle-section.js,sha256=T141NFmy0fRJyGGuuaCJRjJXwPam-xxtQNW1hi8BJbc,672
1053
- django_cfg/static/frontend/admin.zip,sha256=n2G8ajruLqojcjqvw2pMEPjgpSTTwcmJxkeIQxJVw9U,7626768
1053
+ django_cfg/static/frontend/admin.zip,sha256=tJMXJynfHO7oj85nhrx3iGg-stYsF4Imtuwnf2Vs0k8,7631452
1054
1054
  django_cfg/static/js/api-loader.mjs,sha256=boGqqRGnFR-Mzo_RQOjhAzNvsb7QxZddSwMKROzkk9Q,5163
1055
1055
  django_cfg/static/js/api/base.mjs,sha256=KUxZHHdELAV8mNnACpwJRvaQhdJxp-n5LFEQ4oUZxBo,4707
1056
1056
  django_cfg/static/js/api/index.mjs,sha256=_-Q04jjHcgwi4CGfiaLyiOR6NW7Yu1HBhJWp2J1cjpc,2538
@@ -1072,11 +1072,11 @@ django_cfg/static/js/api/support/index.mjs,sha256=oPA3iGkUWYyKQuJlI5-tSxD3AOhwlA
1072
1072
  django_cfg/static/js/api/tasks/client.mjs,sha256=tIy8K-finXzTUL9kOo_L4Q1kchDaHyuzjwS4VymiWPM,3579
1073
1073
  django_cfg/static/js/api/tasks/index.mjs,sha256=yCY1GzdD-RtFZ3pAfk1l0msgO1epyo0lsGCjH0g1Afc,294
1074
1074
  django_cfg/templates/__init__.py,sha256=IzLjt-a7VIJ0OutmAE1_-w0_LpL2u0MgGpnIabjZuW8,19
1075
- django_cfg/templates/admin/index.html,sha256=RidRvZwc6LFzRi8l6vHBgyM_CD0yvhPWvr40uVKCClY,18138
1075
+ django_cfg/templates/admin/index.html,sha256=bKpeJFRJltoe3iWmFcR5YUzTwPgnogwAsQXjUlELQEE,20072
1076
1076
  django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
1077
1077
  django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
1078
1078
  django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1079
- django_cfg/templatetags/django_cfg.py,sha256=BLkdH6Pl32itRafOpPxXGg1QhZ9huacAAqFv-W8LTkM,10044
1079
+ django_cfg/templatetags/django_cfg.py,sha256=pAUZQhq3k_JJtQXzabQnXyiHxkDoEGt0iUceeVHz6os,9498
1080
1080
  django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
1081
1081
  django_cfg/utils/path_resolution.py,sha256=2n0I04lQkSssFaELu3A93YyMAl1K10KPdpxMt5k4Iy0,13341
1082
1082
  django_cfg/utils/smart_defaults.py,sha256=ZUj6K_Deq-fp5O0Dy_Emt257UWFn0f9bkgFv9YCR58U,9239
@@ -1084,9 +1084,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1084
1084
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1085
1085
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1086
1086
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1087
- django_cfg/pyproject.toml,sha256=Npc1mjOUxl_Dvp1uFVjLCbrQo1tlTMMxj4Oemc6kLvo,8572
1088
- django_cfg-1.4.96.dist-info/METADATA,sha256=6LS9GzmiwHVvee547cEZjnwrdkib9Kw9Zja8LowXnk0,23733
1089
- django_cfg-1.4.96.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1090
- django_cfg-1.4.96.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1091
- django_cfg-1.4.96.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1092
- django_cfg-1.4.96.dist-info/RECORD,,
1087
+ django_cfg/pyproject.toml,sha256=OR5Tftp5DeqgABTw9E9_Dc6qIuaMUO98Q7rTmg1ZXT0,8572
1088
+ django_cfg-1.4.99.dist-info/METADATA,sha256=hax4GWcL1VKcaR4GmP6Tx_2gyt3gMoTWaDWwKna4PIU,23733
1089
+ django_cfg-1.4.99.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1090
+ django_cfg-1.4.99.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1091
+ django_cfg-1.4.99.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1092
+ django_cfg-1.4.99.dist-info/RECORD,,