django-cfg 1.4.99__py3-none-any.whl → 1.4.102__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.99"
35
+ __version__ = "1.4.102"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -61,7 +61,11 @@ class CryptoFieldsConfig(BaseModel):
61
61
  # === Django Revision Settings (for django-audit-fields dependency) ===
62
62
  ignore_git_dir: bool = Field(
63
63
  default=True,
64
- description="Ignore git directory for django-revision (set DJANGO_REVISION_IGNORE_WORKING_DIR=True)"
64
+ description=(
65
+ "Ignore git directory for django-revision. "
66
+ "When True, version is discovered from: package metadata → pyproject.toml → VERSION file. "
67
+ "Prevents 'not recommended' warning from settings.REVISION fallback."
68
+ )
65
69
  )
66
70
 
67
71
  def to_django_settings(self, base_dir: Path, is_production: bool, debug: bool) -> dict:
@@ -108,13 +112,52 @@ class CryptoFieldsConfig(BaseModel):
108
112
  }
109
113
 
110
114
  # Disable django-revision git integration (required by django-audit-fields)
111
- # RevisionField will use package metadata or pyproject.toml instead
115
+ # RevisionField will use package metadata, pyproject.toml, or VERSION file instead
112
116
  if self.ignore_git_dir:
113
117
  settings["DJANGO_REVISION_IGNORE_WORKING_DIR"] = True
114
- # Completely disable django-revision metadata checks to avoid errors
115
- settings["DJANGO_REVISION_IGNORE_METADATA"] = True
116
- # Set static revision to satisfy django-revision requirements
117
- settings["REVISION"] = "1.0.0"
118
+ # Allow django-revision to discover version from:
119
+ # 1. Package metadata (version())
120
+ # 2. pyproject.toml [project][version]
121
+ # 3. VERSION file (BASE_DIR/VERSION)
122
+ # Do NOT set REVISION here to avoid "not recommended" warning
123
+
124
+ # Create VERSION file if it doesn't exist (prevents fallback to settings.REVISION)
125
+ version_file = base_dir / "VERSION"
126
+ if not version_file.exists():
127
+ try:
128
+ # Try to get version from pyproject.toml or package metadata
129
+ version_to_write = "1.0.0" # Default fallback
130
+
131
+ # Try pyproject.toml first
132
+ pyproject_file = base_dir / "pyproject.toml"
133
+ if pyproject_file.exists():
134
+ try:
135
+ import tomllib
136
+ except ImportError:
137
+ # Python < 3.11
138
+ try:
139
+ import tomli as tomllib # type: ignore
140
+ except ImportError:
141
+ tomllib = None # type: ignore
142
+
143
+ if tomllib:
144
+ try:
145
+ with open(pyproject_file, "rb") as f:
146
+ pyproject_data = tomllib.load(f)
147
+ version_to_write = pyproject_data.get("project", {}).get("version", version_to_write)
148
+ except Exception:
149
+ pass # Use default version
150
+
151
+ # Write VERSION file
152
+ version_file.write_text(version_to_write + "\n")
153
+
154
+ import logging
155
+ logger = logging.getLogger(__name__)
156
+ logger.info(f"Created VERSION file at {version_file} with version {version_to_write}")
157
+ except Exception as e:
158
+ import logging
159
+ logger = logging.getLogger(__name__)
160
+ logger.warning(f"Could not create VERSION file: {e}")
118
161
 
119
162
  return settings
120
163
 
@@ -710,19 +710,11 @@ class Command(BaseCommand):
710
710
  self.stdout.write(self.style.SUCCESS(
711
711
  f" ✅ Created ZIP archive: {django_static_zip.relative_to(base_dir)} ({zip_size_mb:.1f}MB)"
712
712
  ))
713
-
714
- # Copy ZIP to django_cfg package static/frontend directory
715
- import django_cfg
716
- django_cfg_static_zip = Path(django_cfg.__file__).parent / 'static' / 'frontend' / 'nextjs_admin.zip'
717
- django_cfg_static_zip.parent.mkdir(parents=True, exist_ok=True)
718
-
719
- if django_cfg_static_zip.exists():
720
- django_cfg_static_zip.unlink()
721
-
722
- shutil.copy2(django_static_zip, django_cfg_static_zip)
723
-
724
713
  self.stdout.write(self.style.SUCCESS(
725
- f" Copied ZIP to django_cfg: {django_cfg_static_zip.relative_to(Path(django_cfg.__file__).parent)}"
714
+ f" 📍 ZIP location: {django_static_zip.relative_to(base_dir)}"
715
+ ))
716
+ self.stdout.write(self.style.SUCCESS(
717
+ " ℹ️ This ZIP is used by NextJsAdminView (Tab 2: External Admin)"
726
718
  ))
727
719
 
728
720
  except Exception as zip_error:
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.99"
7
+ version = "1.4.102"
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
@@ -0,0 +1,504 @@
1
+ # Dual-Tab Admin Architecture
2
+
3
+ This document explains the architecture and logic behind Django-CFG's dual-tab admin interface, which allows seamless integration of both built-in and external Next.js admin panels.
4
+
5
+ ## Overview
6
+
7
+ Django-CFG admin interface supports **two independent Next.js admin panels** running side-by-side in separate tabs:
8
+
9
+ 1. **Tab 1: Built-in Dashboard** - The default Next.js admin panel shipped with Django-CFG
10
+ 2. **Tab 2: External Next.js Admin** - A custom Next.js admin panel from your solution project
11
+
12
+ Each tab operates independently with its own:
13
+ - Development server port
14
+ - Static file serving path
15
+ - Fallback mechanism
16
+ - iframe communication channel
17
+
18
+ ## Architecture Diagram
19
+
20
+ ```
21
+ ┌─────────────────────────────────────────────────────────────────┐
22
+ │ Django Admin Interface │
23
+ │ ┌───────────────────────────────┬───────────────────────────┐ │
24
+ │ │ Tab 1: Built-in Dashboard │ Tab 2: External Admin │ │
25
+ │ └───────────────────────────────┴───────────────────────────┘ │
26
+ │ │
27
+ │ Dev Mode: │ Dev Mode: │
28
+ │ ↓ http://localhost:3777/admin │ ↓ http://localhost:3000/admin│
29
+ │ │
30
+ │ Fallback (Static): │ Fallback (Static): │
31
+ │ ↓ /cfg/admin/admin/ │ ↓ /cfg/nextjs-admin/admin/ │
32
+ └─────────────────────────────────────────────────────────────────┘
33
+ ```
34
+
35
+ ## Port Allocation
36
+
37
+ | Tab | Purpose | Dev Port | Static Path |
38
+ |-----|---------|----------|-------------|
39
+ | Tab 1 | Built-in Dashboard | `3777` | `/cfg/admin/admin/` |
40
+ | Tab 2 | External Next.js Admin | `3000` | `/cfg/nextjs-admin/admin/` |
41
+
42
+ ### Why Different Ports?
43
+
44
+ Using separate ports prevents conflicts and allows both dev servers to run simultaneously:
45
+ - **Port 3777**: Reserved for Django-CFG's built-in admin panel
46
+ - **Port 3000**: Reserved for solution project's custom admin panel
47
+
48
+ ## URL Resolution Logic
49
+
50
+ ### Development Mode (DEBUG=True)
51
+
52
+ **Server-Side (Python)**: Always returns dev server URLs in DEBUG mode:
53
+
54
+ ```python
55
+ def nextjs_admin_url(path=''):
56
+ """Built-in Dashboard (Tab 1)"""
57
+ if settings.DEBUG:
58
+ return f'http://localhost:3777/admin/{path}'
59
+ else:
60
+ return f'/cfg/admin/admin/{path}'
61
+
62
+ def nextjs_external_admin_url(route=''):
63
+ """External Next.js Admin (Tab 2)"""
64
+ if settings.DEBUG:
65
+ return f'http://localhost:3000/admin/{route}'
66
+ else:
67
+ return f'/cfg/nextjs-admin/admin/{route}'
68
+ ```
69
+
70
+ **Client-Side (JavaScript)**: Verifies dev server availability before loading iframe:
71
+
72
+ ```javascript
73
+ async function checkDevServerAvailable(url, retries = 3, timeout = 1000) {
74
+ // Tries to fetch dev server with exponential backoff
75
+ // - Attempt 1: immediate
76
+ // - Attempt 2: +500ms delay
77
+ // - Attempt 3: +1000ms delay
78
+ // Total max: ~3.5s
79
+ }
80
+ ```
81
+
82
+ **Why client-side checking?**
83
+ - Dev servers may be compiling on first request (cold start)
84
+ - Browser can handle CORS/network errors better than server-side sockets
85
+ - Allows retry with exponential backoff for slow Next.js compilation
86
+ - No server-side blocking during HTML rendering
87
+
88
+ ### Production Mode (DEBUG=False)
89
+
90
+ In production, both tabs serve static files:
91
+ - Tab 1: Serves from `/cfg/admin/admin/` (built-in static files)
92
+ - Tab 2: Serves from `/cfg/nextjs-admin/admin/` (extracted from ZIP)
93
+
94
+ ## Fallback Mechanism
95
+
96
+ Each iframe implements a smart fallback strategy when dev servers are unavailable:
97
+
98
+ ```javascript
99
+ function handleLoadFailure() {
100
+ if (!isDevMode) return; // Already using static files
101
+
102
+ const originalSrc = iframe.getAttribute('data-original-src');
103
+ const devUrl = new URL(originalSrc);
104
+ const pathPart = devUrl.pathname.replace('/admin', '');
105
+
106
+ // Determine correct fallback based on iframe ID
107
+ let staticUrl;
108
+ if (iframe.id === 'nextjs-dashboard-iframe-builtin') {
109
+ staticUrl = `/cfg/admin/admin/${pathPart}`;
110
+ } else if (iframe.id === 'nextjs-dashboard-iframe-nextjs') {
111
+ staticUrl = `/cfg/nextjs-admin/admin/${pathPart}`;
112
+ }
113
+
114
+ iframe.src = staticUrl;
115
+ }
116
+ ```
117
+
118
+ ### Fallback Triggers
119
+
120
+ 1. **Timeout**: 3 seconds without successful load
121
+ 2. **Error Event**: iframe fails to load (network error, server down)
122
+ 3. **Manual**: Dev server stopped during development
123
+
124
+ ## iframe Communication
125
+
126
+ Each iframe maintains its own communication channel with the parent window using `postMessage`.
127
+
128
+ ### Origin Detection
129
+
130
+ ```javascript
131
+ // Get origin from data-original-src (not iframe.src, which may change)
132
+ const iframeSrc = iframe.getAttribute('data-original-src');
133
+ const iframeUrl = new URL(iframeSrc, window.location.origin);
134
+ const iframeOrigin = iframeUrl.origin;
135
+ ```
136
+
137
+ **Why `data-original-src`?**
138
+ The `iframe.src` attribute may be changed by the fallback mechanism, so we use `data-original-src` to maintain the original intended origin for `postMessage` communication.
139
+
140
+ ### Message Types
141
+
142
+ | Message Type | Direction | Purpose |
143
+ |--------------|-----------|---------|
144
+ | `parent-theme` | Parent → iframe | Sync theme (dark/light mode) |
145
+ | `parent-auth` | Parent → iframe | Inject JWT tokens |
146
+ | `iframe-ready` | iframe → Parent | iframe loaded and ready |
147
+ | `iframe-resize` | iframe → Parent | Update iframe height |
148
+ | `iframe-navigation` | iframe → Parent | Route change notification |
149
+ | `iframe-auth-status` | iframe → Parent | Authentication status |
150
+
151
+ ## Tab Switching & State Reset
152
+
153
+ ### Reset Logic
154
+
155
+ When switching tabs, the **previous** tab's iframe is reset to its initial URL:
156
+
157
+ ```javascript
158
+ switchTab(tab) {
159
+ if (this.previousTab !== tab) {
160
+ // Reset iframe that was just hidden
161
+ this.resetIframe(this.previousTab);
162
+ this.previousTab = tab;
163
+ }
164
+ this.activeTab = tab;
165
+ }
166
+
167
+ resetIframe(tab) {
168
+ const iframeId = tab === 'builtin'
169
+ ? 'nextjs-dashboard-iframe-builtin'
170
+ : 'nextjs-dashboard-iframe-nextjs';
171
+ const iframe = document.getElementById(iframeId);
172
+
173
+ if (iframe) {
174
+ const originalSrc = iframe.getAttribute('data-original-src');
175
+ iframe.src = originalSrc; // Reset to initial URL
176
+ }
177
+ }
178
+ ```
179
+
180
+ **Why Reset?**
181
+ This ensures that when users switch back to a tab, it starts from the home page rather than whatever route they navigated to previously.
182
+
183
+ ## Static File Serving
184
+
185
+ ### Built-in Admin (Tab 1)
186
+
187
+ **Location**: `django_cfg/static/frontend/admin/`
188
+
189
+ **Serving**:
190
+ - Development: `http://localhost:3777/admin`
191
+ - Production: `/cfg/admin/admin/` (Django `staticfiles`)
192
+
193
+ ### External Admin (Tab 2)
194
+
195
+ **Source**: `{solution_project}/static/nextjs_admin.zip`
196
+
197
+ **Extraction**:
198
+ ```python
199
+ # Target directory
200
+ base_dir = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin'
201
+
202
+ # ZIP with metadata tracking
203
+ zip_path = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin.zip'
204
+
205
+ # Automatic extraction with metadata comparison
206
+ extract_zip_if_needed(base_dir, zip_path, 'nextjs_admin')
207
+ ```
208
+
209
+ **Serving**:
210
+ - Development: `http://localhost:3000/admin`
211
+ - Production: `/cfg/nextjs-admin/admin/` (Django serves extracted files)
212
+
213
+ ### ZIP Extraction Logic
214
+
215
+ The system uses a **metadata marker file** (`.zip_meta`) to track ZIP state:
216
+
217
+ ```python
218
+ # Metadata format: {size}:{mtime}
219
+ current_meta = f"{zip_stat.st_size}:{zip_stat.st_mtime}"
220
+
221
+ # Extraction triggers:
222
+ # 1. Directory doesn't exist → Extract
223
+ # 2. Marker file missing → Extract
224
+ # 3. ZIP metadata changed → Re-extract
225
+ # 4. Metadata matches → Use existing
226
+ ```
227
+
228
+ **Why Metadata Comparison?**
229
+ More reliable than timestamp-only comparison, especially in Docker environments where file timestamps can be misleading.
230
+
231
+ ## JWT Token Injection
232
+
233
+ Both tabs receive JWT tokens automatically for authenticated users:
234
+
235
+ ```javascript
236
+ // Inject tokens into localStorage
237
+ localStorage.setItem('auth_token', '{access_token}');
238
+ localStorage.setItem('refresh_token', '{refresh_token}');
239
+ ```
240
+
241
+ ### Injection Strategy
242
+
243
+ 1. **Server-Side**: Tokens injected into HTML response (see `views.py`)
244
+ 2. **Client-Side**: Tokens sent via `postMessage` after iframe loads
245
+ 3. **Storage**: Tokens stored in `localStorage` for Next.js apps
246
+
247
+ ### Cache Control
248
+
249
+ HTML responses have aggressive cache-busting headers to ensure fresh token injection:
250
+
251
+ ```python
252
+ response['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
253
+ response['Pragma'] = 'no-cache'
254
+ response['Expires'] = '0'
255
+ ```
256
+
257
+ ## Configuration
258
+
259
+ ### Django Config (solution project)
260
+
261
+ ```python
262
+ from django_cfg import NextJsAdminConfig
263
+
264
+ config = DjangoConfig(
265
+ nextjs_admin=NextJsAdminConfig(
266
+ project_path="../django_admin/apps/admin",
267
+ api_output_path="src/api/generated",
268
+ auto_build=True,
269
+ # Optional overrides:
270
+ # static_url="/cfg/nextjs-admin/",
271
+ # dev_url="http://localhost:3000",
272
+ # tab_title="Custom Admin",
273
+ )
274
+ )
275
+ ```
276
+
277
+ ### Next.js Dev Servers
278
+
279
+ **Built-in Admin** (`django_admin/apps/admin/package.json`):
280
+ ```json
281
+ {
282
+ "scripts": {
283
+ "dev": "next dev -p 3777"
284
+ }
285
+ }
286
+ ```
287
+
288
+ **External Admin** (solution project):
289
+ ```json
290
+ {
291
+ "scripts": {
292
+ "dev": "next dev -p 3000"
293
+ }
294
+ }
295
+ ```
296
+
297
+ ## Development Workflow
298
+
299
+ ### Starting Both Dev Servers
300
+
301
+ ```bash
302
+ # Terminal 1: Built-in admin
303
+ cd projects/django-cfg-dev/src/django_admin/apps/admin
304
+ pnpm dev # Runs on port 3777
305
+
306
+ # Terminal 2: External admin
307
+ cd solution/projects/django_admin/apps/admin
308
+ pnpm dev # Runs on port 3000
309
+
310
+ # Terminal 3: Django
311
+ cd solution/projects/django
312
+ python manage.py runserver
313
+ ```
314
+
315
+ ### Running Single Tab Only
316
+
317
+ You can run only one dev server - the other will fallback to static files:
318
+
319
+ ```bash
320
+ # Only external admin in dev mode
321
+ cd solution/projects/django_admin/apps/admin
322
+ pnpm dev
323
+
324
+ # Built-in admin will fallback to /cfg/admin/admin/
325
+ ```
326
+
327
+ ## Template Tags Reference
328
+
329
+ ### `{% nextjs_admin_url %}`
330
+ Returns URL for built-in dashboard (Tab 1)
331
+ - Dev: `http://localhost:3777/admin`
332
+ - Prod: `/cfg/admin/admin/`
333
+
334
+ ### `{% nextjs_external_admin_url %}`
335
+ Returns URL for external admin (Tab 2)
336
+ - Dev: `http://localhost:3000/admin`
337
+ - Prod: `/cfg/nextjs-admin/admin/`
338
+
339
+ ### `{% nextjs_external_admin_title %}`
340
+ Returns custom tab title from config (default: "Next.js Admin")
341
+
342
+ ### `{% is_frontend_dev_mode %}`
343
+ Returns `True` if any dev server (port 3000 or 3777) is running
344
+
345
+ ### `{% has_nextjs_external_admin %}`
346
+ Returns `True` if `NextJsAdminConfig` is configured
347
+
348
+ ## Troubleshooting
349
+
350
+ ### Tab shows wrong content
351
+
352
+ **Symptom**: Second tab shows content from first tab
353
+
354
+ **Cause**: `handleLoadFailure()` using wrong static URL
355
+
356
+ **Solution**: Check iframe ID in fallback logic (see `index.html:342-353`)
357
+
358
+ ### postMessage origin mismatch
359
+
360
+ **Symptom**: Console error about mismatched origins
361
+
362
+ **Cause**: `iframeOrigin` determined from `iframe.src` after fallback changed it
363
+
364
+ **Solution**: Use `data-original-src` attribute instead (see `index.html:287`)
365
+
366
+ ### Dev server not detected
367
+
368
+ **Symptom**: Tabs use static files despite dev server running
369
+
370
+ **Possible Causes**:
371
+ 1. **Dev server still compiling**: Next.js cold start can take 1-2s
372
+ 2. **Wrong port**: Dev server on different port than expected
373
+ 3. **IPv6 vs IPv4**: Dev server listening only on IPv6 (`::1`)
374
+ 4. **Firewall**: Connection blocked by firewall/antivirus
375
+ 5. **Retry exhausted**: All 3 retry attempts failed
376
+
377
+ **Diagnostic Steps**:
378
+
379
+ 1. **Check dev server is actually running**:
380
+ ```bash
381
+ lsof -i :3000 -i :3777 | grep LISTEN
382
+ ```
383
+
384
+ 2. **Test connection manually**:
385
+ ```bash
386
+ curl -I http://localhost:3000/admin
387
+ curl -I http://localhost:3777/admin
388
+ ```
389
+
390
+ 3. **Check Django template tag logs** (add temporary logging):
391
+ ```python
392
+ # In django_cfg.py
393
+ result = _is_port_available('localhost', 3000)
394
+ print(f"[DEBUG] Port 3000 check: {result}")
395
+ ```
396
+
397
+ 4. **Increase retry attempts** (if dev server is very slow):
398
+ ```python
399
+ # Temporary test - increase retries
400
+ _is_port_available('localhost', 3000, timeout=0.5, retries=5, retry_delay=0.1)
401
+ ```
402
+
403
+ **Solutions**:
404
+ - **Wait for compilation**: Refresh page after 5-10 seconds
405
+ - **Verify port**: Check `package.json` scripts (`"dev": "next dev -p 3000"`)
406
+ - **Force IPv4**: Use `127.0.0.1` instead of `localhost`
407
+ - **Disable firewall**: Temporarily disable to test
408
+ - **Hard refresh**: Cmd+Shift+R to bypass browser cache
409
+
410
+ ### Tokens not injected
411
+
412
+ **Symptom**: Next.js app shows "Not authenticated"
413
+
414
+ **Cause**:
415
+ 1. User not logged in to Django
416
+ 2. Cache returning 304 Not Modified
417
+ 3. `postMessage` origin mismatch
418
+
419
+ **Solution**:
420
+ - Check Django session authentication
421
+ - Verify cache-control headers
422
+ - Check browser console for `postMessage` errors
423
+
424
+ ## Performance Considerations
425
+
426
+ ### Client-Side Dev Server Check
427
+
428
+ Port availability check happens in browser with exponential backoff:
429
+ ```javascript
430
+ checkDevServerAvailable('http://localhost:3000', retries=3, timeout=1000)
431
+ ```
432
+
433
+ **Impact per check**:
434
+ - **Best case**: ~0.1s (first fetch succeeds immediately)
435
+ - **Typical case**: ~0.5-1.5s (dev server compiling, succeeds on retry)
436
+ - **Worst case**: ~3.5s (all 3 attempts fail with delays: 1s + 0.5s + 1s + 1s)
437
+
438
+ **Retry schedule** (exponential backoff):
439
+ 1. Attempt 1: immediate (0ms delay)
440
+ 2. Attempt 2: +500ms delay
441
+ 3. Attempt 3: +1000ms delay
442
+
443
+ **Why client-side checking?**
444
+ - ✅ Non-blocking: doesn't slow down server-side HTML rendering
445
+ - ✅ Better error handling: browser handles CORS/network errors gracefully
446
+ - ✅ User feedback: loading spinner shows progress
447
+ - ✅ Parallel loading: both iframe checks can run concurrently
448
+
449
+ ### iframe Load Timeout
450
+
451
+ 3-second timeout before fallback:
452
+ ```javascript
453
+ loadTimeout = setTimeout(() => {
454
+ handleLoadFailure();
455
+ }, 3000);
456
+ ```
457
+
458
+ **Impact**: Users may see loading spinner for up to 3s if dev server is down
459
+
460
+ ### Static File Caching
461
+
462
+ - **HTML**: No caching (`no-store, no-cache`)
463
+ - **Assets**: Standard browser caching (JS, CSS, images)
464
+
465
+ ## Security Considerations
466
+
467
+ ### iframe Sandbox
468
+
469
+ Both iframes use restrictive sandbox attributes:
470
+ ```html
471
+ sandbox="allow-same-origin allow-scripts allow-forms
472
+ allow-popups allow-modals
473
+ allow-storage-access-by-user-activation"
474
+ ```
475
+
476
+ **Warning**: Console will show "iframe can escape sandboxing" - this is expected because `allow-same-origin` + `allow-scripts` together allow sandbox escape.
477
+
478
+ ### JWT Token Security
479
+
480
+ - Tokens injected via `postMessage` with origin validation
481
+ - Tokens stored in `localStorage` (accessible to Next.js apps)
482
+ - New tokens generated on each HTML page load
483
+ - Tokens cleared when Django session expires
484
+
485
+ ### CORS & Origins
486
+
487
+ - Dev mode: Cross-origin iframes (`localhost:3777`, `localhost:3000`)
488
+ - Prod mode: Same-origin iframes (both served from Django domain)
489
+
490
+ ## Future Enhancements
491
+
492
+ - [ ] Configurable port allocation per project
493
+ - [ ] Hot-reload coordination between Django and Next.js
494
+ - [ ] Shared state synchronization between tabs
495
+ - [ ] Tab-specific URL routing (preserve state across refreshes)
496
+ - [ ] WebSocket support for real-time updates
497
+ - [ ] SSO integration for external admin panels
498
+ - [ ] Health check dashboard for dev servers
499
+
500
+ ---
501
+
502
+ **Last Updated**: 2025-10-29
503
+ **Django-CFG Version**: 2.x
504
+ **Next.js Version**: 15.x
@@ -291,67 +291,17 @@
291
291
 
292
292
  const iframeUrl = new URL(iframeSrc, window.location.origin);
293
293
  const iframeOrigin = iframeUrl.origin;
294
- const isDevMode = iframeOrigin !== window.location.origin;
295
294
 
296
295
  // Debounce timer for resize events (prevents jittery iframe height changes)
297
296
  let resizeTimer = null;
298
- let loadTimeout = null;
299
297
 
300
298
  // iframe load event
301
299
  iframe.addEventListener('load', function() {
302
- // Clear the fallback timeout
303
- if (loadTimeout) {
304
- clearTimeout(loadTimeout);
305
- loadTimeout = null;
306
- }
307
-
308
300
  setTimeout(() => {
309
301
  sendDataToIframe();
310
302
  }, 100);
311
303
  });
312
304
 
313
- // iframe error event - fallback to static files
314
- iframe.addEventListener('error', function() {
315
- console.warn('[Django-CFG] iframe failed to load, falling back to static files');
316
- handleLoadFailure();
317
- });
318
-
319
- // Timeout fallback - if iframe doesn't load in 3 seconds
320
- loadTimeout = setTimeout(() => {
321
- if (!iframe.classList.contains('loaded') && isDevMode) {
322
- console.warn('[Django-CFG] Dev server timeout, falling back to static files');
323
- handleLoadFailure();
324
- } else if (!iframe.classList.contains('loaded')) {
325
- console.log('[Django-CFG] Timeout - showing iframe anyway');
326
- if (loading) loading.classList.add('hidden');
327
- iframe.classList.add('loaded');
328
- }
329
- }, 3000);
330
-
331
- // Handle load failure - switch to static files
332
- function handleLoadFailure() {
333
- if (!isDevMode) return; // Already using static files
334
-
335
- const originalSrc = iframe.getAttribute('data-original-src');
336
- if (!originalSrc) return;
337
-
338
- // Extract path from dev server URL
339
- const devUrl = new URL(originalSrc);
340
- const pathPart = devUrl.pathname.replace('/admin', '').replace(/^\//, '');
341
-
342
- // Build static files URL
343
- const staticUrl = `/cfg/admin/admin/${pathPart}`;
344
-
345
- console.log('[Django-CFG] Switching to static files:', staticUrl);
346
- iframe.src = staticUrl;
347
-
348
- // Show iframe after switching
349
- setTimeout(() => {
350
- if (loading) loading.classList.add('hidden');
351
- iframe.classList.add('loaded');
352
- }, 500);
353
- }
354
-
355
305
  // Send theme and auth data to iframe
356
306
  function sendDataToIframe() {
357
307
  if (!iframe || !iframe.contentWindow) {
@@ -14,29 +14,53 @@ from rest_framework_simplejwt.tokens import RefreshToken
14
14
  register = template.Library()
15
15
 
16
16
 
17
- def _is_port_available(host: str, port: int, timeout: float = 0.1) -> bool:
17
+ def _is_port_available(host: str, port: int, timeout: float = 0.1, retries: int = 3, retry_delay: float = 0.05) -> bool:
18
18
  """
19
- Check if a port is available (listening) on the specified host.
19
+ Check if a port is available (listening) on the specified host with retry logic.
20
20
 
21
- Uses a quick socket connection test with minimal timeout.
22
- Returns True if the port is open and accepting connections.
21
+ Performs multiple connection attempts with delays to handle dev servers
22
+ that may be compiling or starting up (e.g., Next.js first request).
23
23
 
24
24
  Args:
25
25
  host: Host to check (e.g., 'localhost', '127.0.0.1')
26
26
  port: Port number to check
27
- timeout: Connection timeout in seconds (default: 0.1s)
27
+ timeout: Connection timeout in seconds per attempt (default: 0.1s)
28
+ retries: Number of retry attempts (default: 3)
29
+ retry_delay: Delay between retries in seconds (default: 0.05s)
28
30
 
29
31
  Returns:
30
32
  bool: True if port is available, False otherwise
33
+
34
+ Example:
35
+ # Quick check (default): ~0.4s max (3 attempts × 0.1s + 2 × 0.05s delay)
36
+ _is_port_available('localhost', 3000)
37
+
38
+ # Patient check for slow servers
39
+ _is_port_available('localhost', 3000, timeout=0.5, retries=5)
31
40
  """
32
- try:
33
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34
- sock.settimeout(timeout)
35
- result = sock.connect_ex((host, port))
36
- sock.close()
37
- return result == 0
38
- except Exception:
39
- return False
41
+ import time
42
+
43
+ for attempt in range(retries):
44
+ try:
45
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
46
+ sock.settimeout(timeout)
47
+ result = sock.connect_ex((host, port))
48
+ sock.close()
49
+
50
+ if result == 0:
51
+ return True # Success - port is available
52
+
53
+ # Port not available, retry if not last attempt
54
+ if attempt < retries - 1:
55
+ time.sleep(retry_delay)
56
+
57
+ except Exception:
58
+ # Connection error, retry if not last attempt
59
+ if attempt < retries - 1:
60
+ time.sleep(retry_delay)
61
+ continue
62
+
63
+ return False # All retries failed
40
64
 
41
65
 
42
66
  @register.simple_tag
@@ -169,9 +193,12 @@ def nextjs_admin_url(path=''):
169
193
  """
170
194
  Get the URL for Next.js Admin Panel (Built-in Dashboard - Tab 1).
171
195
 
172
- Auto-detects development mode with priority:
173
- 1. If port 3001 is available http://localhost:3001/admin/{path} (dev server)
174
- 2. Otherwise → /cfg/admin/admin/{path} (static files)
196
+ In DEBUG mode, always returns dev server URL. Client-side JavaScript
197
+ will handle fallback to static files if dev server is unavailable.
198
+
199
+ Returns:
200
+ - DEBUG=True: http://localhost:3777/admin/{path}
201
+ - DEBUG=False: /cfg/admin/admin/{path}
175
202
 
176
203
  Note: Port 3000 is reserved for external Next.js admin (Tab 2).
177
204
  Both tabs use /admin route for consistency.
@@ -188,12 +215,12 @@ def nextjs_admin_url(path=''):
188
215
  # Production mode: always use static files with /admin route
189
216
  return f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
190
217
 
191
- # Check if port 3001 is available for Tab 1 (built-in admin)
192
- port_3001_available = _is_port_available('localhost', 3001)
218
+ # Check if port 3777 is available for Tab 1 (built-in admin)
219
+ port_3777_available = _is_port_available('localhost', 3777)
193
220
 
194
- if port_3001_available:
195
- # Dev server is running on 3001 - use /admin route for consistency
196
- base_url = 'http://localhost:3001/admin'
221
+ if port_3777_available:
222
+ # Dev server is running on 3777 - use /admin route for consistency
223
+ base_url = 'http://localhost:3777/admin'
197
224
  return f'{base_url}/{path}' if path else base_url
198
225
  else:
199
226
  # No dev server - use static files with /admin route
@@ -207,7 +234,7 @@ def is_frontend_dev_mode():
207
234
 
208
235
  Auto-detects by checking:
209
236
  - DEBUG=True
210
- - AND (port 3000 OR port 3001 is available)
237
+ - AND (port 3000 OR port 3777 is available)
211
238
 
212
239
  Returns True if any Next.js dev server is detected.
213
240
 
@@ -222,7 +249,7 @@ def is_frontend_dev_mode():
222
249
 
223
250
  # Check if either dev server is running
224
251
  return (_is_port_available('localhost', 3000) or
225
- _is_port_available('localhost', 3001))
252
+ _is_port_available('localhost', 3777))
226
253
 
227
254
 
228
255
  @register.simple_tag
@@ -252,12 +279,14 @@ def nextjs_external_admin_url(route=''):
252
279
  """
253
280
  Get URL for external Next.js admin (Tab 2 - solution project).
254
281
 
255
- Auto-detects development mode:
256
- - If DEBUG=True AND localhost:3000 is available http://localhost:3000/admin/{route}
257
- - Otherwise → /cfg/nextjs-admin/admin/{route} (static files)
282
+ In DEBUG mode, always returns dev server URL. Client-side JavaScript
283
+ will handle fallback to static files if dev server is unavailable.
284
+
285
+ Returns:
286
+ - DEBUG=True: http://localhost:3000/admin/{route}
287
+ - DEBUG=False: /cfg/nextjs-admin/admin/{route}
258
288
 
259
289
  This is for the external admin panel (solution project).
260
- No env variable needed - automatically detects running dev server!
261
290
 
262
291
  Usage in template:
263
292
  {% load django_cfg %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.99
3
+ Version: 1.4.102
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=2_6He9GXwm8Mx_NZieYsriBE-ZI6kzIIoeAoLHEJgEs,1620
2
+ django_cfg/__init__.py,sha256=cOoOOBDNJqLqEa69QzfvXJ5NAJqhLoE8KMekqdTLWWI,1621
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
@@ -664,7 +664,7 @@ django_cfg/models/base/module.py,sha256=nxN1Y9J4l94kOfSXLQJ2eGgIGWTq8kyh7hUGvCQN
664
664
  django_cfg/models/django/__init__.py,sha256=xY_ts1oGmMROXfKHLuERqwG35dQf3EpsZxqj9DcRYwI,397
665
665
  django_cfg/models/django/axes.py,sha256=-4nk2gSfpj7lNY5vnm_2jHVLz8VAKoEd9yF2TuCR8O8,5624
666
666
  django_cfg/models/django/constance.py,sha256=E4U3HS_gFq5_q2nhI1R8inONGtD7IgvSwxOEGNGGrEI,9127
667
- django_cfg/models/django/crypto_fields.py,sha256=frqLVcDC0zymUyZ6u7tqakojjBNEziHyjbnu1PPvjY0,4187
667
+ django_cfg/models/django/crypto_fields.py,sha256=EritHX2V8q5zHoVznGIO0sX78GPYif6NQDS_CiXvSKo,6238
668
668
  django_cfg/models/django/environment.py,sha256=lBCHBs1lphv9tlu1BCTfLZeH_kUame0p66A_BIjBY7M,9440
669
669
  django_cfg/models/django/openapi.py,sha256=avE3iapaCj8eyOqVUum_v2EExR3V-hwHrexqtXMHtTQ,3739
670
670
  django_cfg/models/django/revolution_legacy.py,sha256=Z4SPUS7QSv62EuPAeFFoXGEgqLmdXnVEr7Ofk1IDtVc,8918
@@ -859,7 +859,7 @@ django_cfg/modules/django_client/core/validation/rules/base.py,sha256=xVJli0eSEz
859
859
  django_cfg/modules/django_client/core/validation/rules/type_hints.py,sha256=hwjTMADillsTPruDvXZQeZMj4LVV443zxY9o0Gqgg6k,10200
860
860
  django_cfg/modules/django_client/management/__init__.py,sha256=mCTPP_bIOmqNnn0WAG2n4BuF6zwc9PTgdZr_dORfNDk,54
861
861
  django_cfg/modules/django_client/management/commands/__init__.py,sha256=CJ55pHUNYQ5h-QHUe3axeTtxzlUJv7wbEuZmGN21iCM,36
862
- django_cfg/modules/django_client/management/commands/generate_client.py,sha256=Mzc0QyxCtL814MevY7oM9wg4DtqK0rTu2MKmBpBBj08,30360
862
+ django_cfg/modules/django_client/management/commands/generate_client.py,sha256=uPoHIU2-rqq0733H7Pj5fl-vNu7Oj5aZYyFqVNKlTVU,29955
863
863
  django_cfg/modules/django_client/management/commands/validate_openapi.py,sha256=IBKk7oRP3tMQzXjvZNIgQtMPk3k_mB2diNS7bkaSLz4,11011
864
864
  django_cfg/modules/django_client/spectacular/__init__.py,sha256=M8fG-odu2ltkG36aMMr0KDkCKGX676TwdrJO8vky2cI,345
865
865
  django_cfg/modules/django_client/spectacular/async_detection.py,sha256=S_pwGR7_2SIWHjZJyiu7SCfySF3Nr3P8eqjDyBSkkLs,5731
@@ -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=tJMXJynfHO7oj85nhrx3iGg-stYsF4Imtuwnf2Vs0k8,7631452
1053
+ django_cfg/static/frontend/admin.zip,sha256=w-E-vlsWyPlx3PpmwTxPenibUWXNSlhN7Hu0oPlhFP8,15272375
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,12 @@ 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=bKpeJFRJltoe3iWmFcR5YUzTwPgnogwAsQXjUlELQEE,20072
1075
+ django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md,sha256=kDUOgLjEv6OdtZNadtQuPhNQmeEZ_TVNt6PKGF6abw4,15607
1076
+ django_cfg/templates/admin/index.html,sha256=ygX2GklrPx9yEzk507Kk7Z56zDfaxjMHpq-TqQCN_1M,17743
1076
1077
  django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
1077
1078
  django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
1078
1079
  django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1079
- django_cfg/templatetags/django_cfg.py,sha256=pAUZQhq3k_JJtQXzabQnXyiHxkDoEGt0iUceeVHz6os,9498
1080
+ django_cfg/templatetags/django_cfg.py,sha256=bJPLZ9Uwlr499-umGN7bcaUibMgZs_XQHvoee93fMfk,10486
1080
1081
  django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
1081
1082
  django_cfg/utils/path_resolution.py,sha256=2n0I04lQkSssFaELu3A93YyMAl1K10KPdpxMt5k4Iy0,13341
1082
1083
  django_cfg/utils/smart_defaults.py,sha256=ZUj6K_Deq-fp5O0Dy_Emt257UWFn0f9bkgFv9YCR58U,9239
@@ -1084,9 +1085,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1084
1085
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1085
1086
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1086
1087
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
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,,
1088
+ django_cfg/pyproject.toml,sha256=CAd9yDwAO6AKftMS1vPChOxU9JdGt-Wrx-RbfONN4KM,8573
1089
+ django_cfg-1.4.102.dist-info/METADATA,sha256=zeGwi6Nm5lQaPgZtGI3YDSICPQ4r3rzefmfAwvECwH8,23734
1090
+ django_cfg-1.4.102.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1091
+ django_cfg-1.4.102.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1092
+ django_cfg-1.4.102.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1093
+ django_cfg-1.4.102.dist-info/RECORD,,