django-cfg 1.4.110__py3-none-any.whl → 1.4.113__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 (37) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/dashboard/serializers/__init__.py +10 -0
  3. django_cfg/apps/dashboard/serializers/crontab.py +84 -0
  4. django_cfg/apps/dashboard/serializers/overview.py +22 -11
  5. django_cfg/apps/dashboard/services/__init__.py +2 -0
  6. django_cfg/apps/dashboard/services/crontab_service.py +210 -0
  7. django_cfg/apps/dashboard/services/system_health_service.py +72 -0
  8. django_cfg/apps/dashboard/urls.py +2 -0
  9. django_cfg/apps/dashboard/views/__init__.py +2 -0
  10. django_cfg/apps/dashboard/views/crontab_views.py +72 -0
  11. django_cfg/apps/dashboard/views/overview_views.py +16 -2
  12. django_cfg/config.py +3 -4
  13. django_cfg/core/base/config_model.py +7 -0
  14. django_cfg/core/builders/apps_builder.py +4 -0
  15. django_cfg/core/generation/integration_generators/__init__.py +3 -0
  16. django_cfg/core/generation/integration_generators/crontab.py +64 -0
  17. django_cfg/core/generation/orchestrator.py +13 -0
  18. django_cfg/core/integration/display/startup.py +2 -2
  19. django_cfg/core/integration/url_integration.py +2 -2
  20. django_cfg/models/__init__.py +3 -0
  21. django_cfg/models/django/__init__.py +3 -0
  22. django_cfg/models/django/crontab.py +303 -0
  23. django_cfg/modules/django_admin/base/pydantic_admin.py +10 -0
  24. django_cfg/modules/django_admin/templates/django_admin/documentation_block.html +7 -1
  25. django_cfg/modules/django_admin/utils/html_builder.py +50 -2
  26. django_cfg/modules/django_admin/utils/markdown_renderer.py +19 -3
  27. django_cfg/modules/django_admin/utils/mermaid_plugin.py +288 -0
  28. django_cfg/pyproject.toml +2 -2
  29. django_cfg/registry/core.py +4 -0
  30. django_cfg/static/frontend/admin.zip +0 -0
  31. django_cfg/templates/admin/index.html +389 -166
  32. django_cfg/templatetags/django_cfg.py +8 -0
  33. {django_cfg-1.4.110.dist-info → django_cfg-1.4.113.dist-info}/METADATA +2 -1
  34. {django_cfg-1.4.110.dist-info → django_cfg-1.4.113.dist-info}/RECORD +37 -31
  35. {django_cfg-1.4.110.dist-info → django_cfg-1.4.113.dist-info}/WHEEL +0 -0
  36. {django_cfg-1.4.110.dist-info → django_cfg-1.4.113.dist-info}/entry_points.txt +0 -0
  37. {django_cfg-1.4.110.dist-info → django_cfg-1.4.113.dist-info}/licenses/LICENSE +0 -0
@@ -79,6 +79,14 @@
79
79
  border: 1px solid rgba(209, 213, 219, 0.2);
80
80
  border-radius: 0.375rem; /* rounded-md */
81
81
  overflow: hidden;
82
+ min-height: 400px;
83
+ }
84
+
85
+ /* On mobile, use more screen space */
86
+ @media (max-width: 768px) {
87
+ .iframe-container {
88
+ min-height: calc(100vh - 200px);
89
+ }
82
90
  }
83
91
 
84
92
  .dark .iframe-container {
@@ -147,7 +155,15 @@
147
155
  },
148
156
  resetIframe(tab) {
149
157
  // Reset the iframe that was just hidden
150
- const iframeId = tab === 'builtin' ? 'nextjs-dashboard-iframe-builtin' : 'nextjs-dashboard-iframe-nextjs';
158
+ let iframeId;
159
+ if (tab === 'builtin') {
160
+ iframeId = 'nextjs-dashboard-iframe-builtin';
161
+ } else if (tab === 'nextjs') {
162
+ iframeId = 'nextjs-dashboard-iframe-nextjs';
163
+ } else if (tab === 'docs') {
164
+ iframeId = 'nextjs-dashboard-iframe-docs';
165
+ }
166
+
151
167
  const iframe = document.getElementById(iframeId);
152
168
  if (iframe) {
153
169
  const originalSrc = iframe.getAttribute('data-original-src') || iframe.src;
@@ -159,15 +175,24 @@
159
175
  }
160
176
  },
161
177
  openInNewWindow() {
162
- // Get the current iframe URL for the External Admin tab
163
- const iframe = document.getElementById('nextjs-dashboard-iframe-nextjs');
178
+ // Get iframe ID based on active tab
179
+ let iframeId;
180
+ if (this.activeTab === 'builtin') {
181
+ iframeId = 'nextjs-dashboard-iframe-builtin';
182
+ } else if (this.activeTab === 'nextjs') {
183
+ iframeId = 'nextjs-dashboard-iframe-nextjs';
184
+ } else if (this.activeTab === 'docs') {
185
+ iframeId = 'nextjs-dashboard-iframe-docs';
186
+ }
187
+
188
+ const iframe = document.getElementById(iframeId);
164
189
  if (!iframe) return;
165
190
 
166
191
  // Get base URL from iframe src or data-original-src
167
192
  let baseUrl = iframe.src || iframe.getAttribute('data-original-src');
168
193
 
169
- // If we have a tracked path from postMessage, use it
170
- if (this.currentNextjsPath) {
194
+ // If we have a tracked path from postMessage (only for nextjs tab), use it
195
+ if (this.activeTab === 'nextjs' && this.currentNextjsPath) {
171
196
  try {
172
197
  const url = new URL(baseUrl);
173
198
  // Replace pathname with tracked path
@@ -207,16 +232,43 @@
207
232
  .dark [style*="border-bottom-color"] {
208
233
  border-bottom-color: rgba(75, 85, 99, 0.2) !important;
209
234
  }
235
+
236
+ /* Mobile: Scrollable tabs */
237
+ .tabs-container {
238
+ overflow-x: auto;
239
+ -webkit-overflow-scrolling: touch;
240
+ scrollbar-width: thin;
241
+ scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
242
+ }
243
+
244
+ .tabs-container::-webkit-scrollbar {
245
+ height: 4px;
246
+ }
247
+
248
+ .tabs-container::-webkit-scrollbar-track {
249
+ background: transparent;
250
+ }
251
+
252
+ .tabs-container::-webkit-scrollbar-thumb {
253
+ background-color: rgba(156, 163, 175, 0.3);
254
+ border-radius: 2px;
255
+ }
256
+
257
+ .tabs-container::-webkit-scrollbar-thumb:hover {
258
+ background-color: rgba(156, 163, 175, 0.5);
259
+ }
210
260
  </style>
211
- <div class="flex items-center justify-between px-4">
212
- <nav class="-mb-px flex space-x-8" aria-label="Dashboard Tabs">
261
+ <div class="tabs-container">
262
+ <div class="flex items-center justify-between px-4 min-w-max">
263
+ <nav class="-mb-px flex space-x-2 sm:space-x-4 md:space-x-8" aria-label="Dashboard Tabs">
213
264
  <button @click="switchTab('builtin')"
214
265
  class="whitespace-nowrap py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-200"
215
266
  :class="activeTab === 'builtin'
216
267
  ? 'border-primary-600 text-primary-600 dark:border-primary-500 dark:text-primary-500'
217
268
  : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-700'">
218
269
  <span class="material-icons text-base">dashboard</span>
219
- <span>Built-in Dashboard</span>
270
+ <span class="hidden sm:inline">Built-in Dashboard</span>
271
+ <span class="sm:hidden">Built-in</span>
220
272
  </button>
221
273
 
222
274
  <button @click="switchTab('nextjs')"
@@ -225,32 +277,41 @@
225
277
  ? 'border-primary-600 text-primary-600 dark:border-primary-500 dark:text-primary-500'
226
278
  : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-700'">
227
279
  <span class="material-icons text-base">web</span>
228
- <span>{% nextjs_external_admin_title %}</span>
229
- </button>
230
- </nav>
231
-
232
- <!-- Actions & Version info -->
233
- <div class="flex items-center gap-4 py-4">
234
- <!-- Open in new window button (only for External Admin tab) -->
235
- <button @click="openInNewWindow()"
236
- x-show="activeTab === 'nextjs'"
237
- title="Open in new window"
238
- class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-700 transition-all duration-150"
239
- style="display: none;">
240
- <span class="material-icons" style="font-size: 16px;">open_in_new</span>
241
- <span>Open in New Window</span>
280
+ <span class="hidden sm:inline">{% nextjs_external_admin_title %}</span>
281
+ <span class="sm:hidden">Admin</span>
242
282
  </button>
243
283
 
244
- <!-- Version info -->
245
- <div class="text-xs text-gray-400 dark:text-gray-500">
246
- {% load django_cfg %}
247
- <a href="{% lib_site_url %}" class="text-blue-600 hover:text-blue-700">
248
- {% lib_name %}
249
- </a>
284
+ <button @click="switchTab('docs')"
285
+ class="whitespace-nowrap py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-200"
286
+ :class="activeTab === 'docs'
287
+ ? 'border-primary-600 text-primary-600 dark:border-primary-500 dark:text-primary-500'
288
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:border-gray-700'">
289
+ <span class="material-icons text-base">description</span>
290
+ <span>Docs</span>
291
+ </button>
292
+ </nav>
293
+
294
+ <!-- Actions & Version info -->
295
+ <div class="flex items-center gap-2 md:gap-4 py-4">
296
+ <!-- Open in new window button (available for all tabs) -->
297
+ <button @click="openInNewWindow()"
298
+ title="Open in new window"
299
+ class="flex items-center gap-1.5 px-2 md:px-3 py-1.5 text-xs font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-700 transition-all duration-150 whitespace-nowrap">
300
+ <span class="material-icons" style="font-size: 16px;">open_in_new</span>
301
+ <span class="hidden sm:inline">Open in New Window</span>
302
+ </button>
303
+
304
+ <!-- Version info -->
305
+ <div class="text-xs text-gray-400 dark:text-gray-500 hidden md:block">
306
+ {% load django_cfg %}
307
+ <a href="{% lib_site_url %}" class="text-blue-600 hover:text-blue-700">
308
+ {% lib_name %}
309
+ </a>
310
+ </div>
311
+ </div>
250
312
  </div>
251
313
  </div>
252
314
  </div>
253
- </div>
254
315
  </div>
255
316
  {% endif %}
256
317
 
@@ -293,6 +354,25 @@
293
354
  </div>
294
355
  </div>
295
356
  {% endif %}
357
+
358
+ <!-- Docs Tab Content -->
359
+ <div x-show="activeTab === 'docs'" style="display: none;">
360
+ <div class="iframe-container">
361
+ <div class="iframe-loading" id="iframe-loading-docs">
362
+ <div class="spinner"></div>
363
+ <p id="loading-text-docs">Loading documentation...</p>
364
+ </div>
365
+
366
+ <iframe
367
+ id="nextjs-dashboard-iframe-docs"
368
+ class="nextjs-dashboard-iframe"
369
+ src="{% lib_docs_url %}"
370
+ data-original-src="{% lib_docs_url %}"
371
+ title="Documentation"
372
+ sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation"
373
+ ></iframe>
374
+ </div>
375
+ </div>
296
376
  </div>
297
377
  {% endcomponent %}
298
378
  {% endblock %}
@@ -301,185 +381,328 @@
301
381
  {{ block.super }}
302
382
  <script>
303
383
  (function() {
304
- // Built-in dashboard iframe
305
- const iframeBuiltin = document.getElementById('nextjs-dashboard-iframe-builtin');
306
- const loadingBuiltin = document.getElementById('iframe-loading-builtin');
307
-
308
- // External Next.js admin iframe (if exists)
309
- const iframeNextjs = document.getElementById('nextjs-dashboard-iframe-nextjs');
310
- const loadingNextjs = document.getElementById('iframe-loading-nextjs');
311
-
312
- // Setup both iframes immediately (always loaded, not lazy-loaded)
313
- if (iframeBuiltin) {
314
- setupIframe(iframeBuiltin, loadingBuiltin);
315
- }
316
-
317
- if (iframeNextjs) {
318
- setupIframe(iframeNextjs, loadingNextjs);
319
- }
384
+ 'use strict';
320
385
 
321
386
  /**
322
- * Setup iframe communication and JWT injection
387
+ * MessageBridge - Handles postMessage communication with internal Next.js iframes
323
388
  */
324
- function setupIframe(iframe, loading) {
325
- if (!iframe) return;
326
-
327
- // Get iframe origin from src attribute
328
- const iframeSrc = iframe.src || iframe.getAttribute('src');
329
- if (!iframeSrc) {
330
- console.warn('[Django-CFG] iframe has no src attribute');
331
- return;
389
+ class MessageBridge {
390
+ constructor(iframe, origin) {
391
+ this.iframe = iframe;
392
+ this.origin = origin;
393
+ this.themeObserver = null;
332
394
  }
333
395
 
334
- const iframeUrl = new URL(iframeSrc, window.location.origin);
335
- const iframeOrigin = iframeUrl.origin;
336
-
337
- // Debounce timer for resize events (prevents jittery iframe height changes)
338
- let resizeTimer = null;
339
-
340
- // iframe load event
341
- iframe.addEventListener('load', function() {
342
- setTimeout(() => {
343
- sendDataToIframe();
344
- }, 100);
345
- });
346
-
347
- // Send theme and auth data to iframe
348
- function sendDataToIframe() {
349
- if (!iframe || !iframe.contentWindow) {
350
- console.error('[Django-CFG] iframe or contentWindow not available');
351
- return;
352
- }
396
+ /**
397
+ * Send theme data to iframe
398
+ */
399
+ sendTheme() {
400
+ if (!this.iframe?.contentWindow) return;
353
401
 
354
402
  const htmlElement = document.documentElement;
355
403
  const isDark = htmlElement.classList.contains('dark');
404
+ const themeMode = this._getThemeMode();
356
405
 
357
- // Get theme mode from Alpine.js
358
- let themeMode = 'auto';
359
- if (window.Alpine && window.Alpine.$data) {
360
- try {
361
- const alpineData = window.Alpine.$data(htmlElement);
362
- if (alpineData && alpineData.adminTheme) {
363
- themeMode = alpineData.adminTheme;
364
- }
365
- } catch (e) {
366
- console.warn('[Django-CFG] Failed to get Alpine data:', e);
367
- }
368
- }
369
-
370
- // Get JWT tokens from localStorage
371
- const authToken = localStorage.getItem('auth_token');
372
- const refreshToken = localStorage.getItem('refresh_token');
373
-
374
- // Send theme
375
406
  try {
376
- iframe.contentWindow.postMessage({
407
+ this.iframe.contentWindow.postMessage({
377
408
  type: 'parent-theme',
378
409
  data: {
379
410
  theme: isDark ? 'dark' : 'light',
380
411
  themeMode: themeMode
381
412
  }
382
- }, iframeOrigin);
413
+ }, this.origin);
383
414
  } catch (e) {
384
- console.error('[Django-CFG] Failed to send theme message:', e);
415
+ console.error('[Django-CFG] Failed to send theme:', e);
385
416
  }
417
+ }
386
418
 
387
- // Send auth tokens
388
- if (authToken) {
389
- try {
390
- iframe.contentWindow.postMessage({
391
- type: 'parent-auth',
392
- data: {
393
- authToken: authToken,
394
- refreshToken: refreshToken
395
- }
396
- }, iframeOrigin);
397
- } catch (e) {
398
- console.error('[Django-CFG] Failed to send auth message:', e);
399
- }
419
+ /**
420
+ * Send auth tokens to iframe
421
+ */
422
+ sendAuth() {
423
+ if (!this.iframe?.contentWindow) return;
424
+
425
+ const authToken = localStorage.getItem('auth_token');
426
+ const refreshToken = localStorage.getItem('refresh_token');
427
+
428
+ if (!authToken) return;
429
+
430
+ try {
431
+ this.iframe.contentWindow.postMessage({
432
+ type: 'parent-auth',
433
+ data: { authToken, refreshToken }
434
+ }, this.origin);
435
+ } catch (e) {
436
+ console.error('[Django-CFG] Failed to send auth:', e);
400
437
  }
401
438
  }
402
439
 
403
- // Watch for theme changes
404
- const observer = new MutationObserver((mutations) => {
405
- mutations.forEach((mutation) => {
406
- if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
407
- sendDataToIframe();
408
- }
440
+ /**
441
+ * Send all data (theme + auth)
442
+ */
443
+ sendAllData() {
444
+ this.sendTheme();
445
+ this.sendAuth();
446
+ }
447
+
448
+ /**
449
+ * Start watching for theme changes
450
+ */
451
+ watchThemeChanges() {
452
+ this.themeObserver = new MutationObserver(() => {
453
+ this.sendTheme();
409
454
  });
410
- });
411
455
 
412
- observer.observe(document.documentElement, {
413
- attributes: true,
414
- attributeFilter: ['class']
415
- });
456
+ this.themeObserver.observe(document.documentElement, {
457
+ attributes: true,
458
+ attributeFilter: ['class']
459
+ });
460
+ }
416
461
 
417
- // Listen for messages from iframe
418
- window.addEventListener('message', (event) => {
419
- if (event.origin !== iframeOrigin) {
420
- return;
462
+ /**
463
+ * Stop watching
464
+ */
465
+ destroy() {
466
+ if (this.themeObserver) {
467
+ this.themeObserver.disconnect();
468
+ this.themeObserver = null;
421
469
  }
470
+ }
471
+
472
+ /**
473
+ * Get theme mode from Alpine.js
474
+ */
475
+ _getThemeMode() {
476
+ if (!window.Alpine?.$$data) return 'auto';
477
+
478
+ try {
479
+ const alpineData = window.Alpine.$data(document.documentElement);
480
+ return alpineData?.adminTheme || 'auto';
481
+ } catch (e) {
482
+ return 'auto';
483
+ }
484
+ }
485
+ }
422
486
 
423
- const { type, data } = event.data || {};
487
+ /**
488
+ * InternalIframeHandler - Handles Next.js internal iframes with postMessage
489
+ */
490
+ class InternalIframeHandler {
491
+ constructor(iframe, loading, origin) {
492
+ this.iframe = iframe;
493
+ this.loading = loading;
494
+ this.origin = origin;
495
+ this.messageBridge = new MessageBridge(iframe, origin);
496
+ this.messageListener = null;
497
+ }
498
+
499
+ init() {
500
+ this._setupLoadHandler();
501
+ this._setupMessageListener();
502
+ this.messageBridge.watchThemeChanges();
503
+ }
504
+
505
+ _setupLoadHandler() {
506
+ this.iframe.addEventListener('load', () => {
507
+ setTimeout(() => {
508
+ this.messageBridge.sendAllData();
509
+ }, 100);
510
+ });
511
+ }
512
+
513
+ _setupMessageListener() {
514
+ this.messageListener = (event) => {
515
+ if (event.origin !== this.origin) return;
516
+
517
+ const { type, data } = event.data || {};
518
+ this._handleMessage(type, data);
519
+ };
424
520
 
521
+ window.addEventListener('message', this.messageListener);
522
+ }
523
+
524
+ _handleMessage(type, data) {
425
525
  switch (type) {
426
526
  case 'iframe-ready':
427
- sendDataToIframe();
527
+ this.messageBridge.sendAllData();
428
528
  break;
429
529
 
430
530
  case 'iframe-auth-status':
431
- console.log('[Django-CFG] Auth status received:', data);
531
+ console.log('[Django-CFG] Auth status:', data);
432
532
  break;
433
533
 
434
534
  case 'iframe-resize':
435
- // DISABLED: Fixed height 80vh instead of dynamic resize
436
- // if (data?.height && typeof data.height === 'number') {
437
- // // First resize - show iframe immediately
438
- // if (!iframe.classList.contains('loaded')) {
439
- // console.log('[Django-CFG] First resize received - showing iframe');
440
- // if (loading) loading.classList.add('hidden');
441
- // iframe.classList.add('loaded');
442
- // }
443
- //
444
- // // Debounce resize updates (300ms delay)
445
- // if (resizeTimer) {
446
- // clearTimeout(resizeTimer);
447
- // }
448
- //
449
- // resizeTimer = setTimeout(() => {
450
- // const newHeight = Math.max(600, data.height + 50);
451
- // iframe.style.height = newHeight + 'px';
452
- // }, 300);
453
- // }
454
-
455
- // Show iframe on first resize event
456
- if (!iframe.classList.contains('loaded')) {
457
- console.log('[Django-CFG] First resize received - showing iframe');
458
- if (loading) loading.classList.add('hidden');
459
- iframe.classList.add('loaded');
460
- }
535
+ this._handleResize();
461
536
  break;
462
537
 
463
538
  case 'iframe-navigation':
464
- console.log('[Django-CFG] iframe navigated to:', data?.path);
465
- // Track current path for "Open in New Window" button
466
- if (iframe.id === 'nextjs-dashboard-iframe-nextjs' && data?.path) {
467
- // Update Alpine.js data
468
- const alpineEl = document.querySelector('[x-data]');
469
- if (alpineEl && window.Alpine) {
470
- const alpineData = window.Alpine.$data(alpineEl);
471
- if (alpineData) {
472
- alpineData.currentNextjsPath = data.path;
473
- }
474
- }
475
- }
539
+ this._handleNavigation(data?.path);
476
540
  break;
541
+ }
542
+ }
477
543
 
478
- default:
479
- break;
544
+ _handleResize() {
545
+ if (this.iframe.classList.contains('loaded')) return;
546
+
547
+ console.log('[Django-CFG] First resize - showing iframe');
548
+ this.loading?.classList.add('hidden');
549
+ this.iframe.classList.add('loaded');
550
+ }
551
+
552
+ _handleNavigation(path) {
553
+ if (!path) return;
554
+
555
+ console.log('[Django-CFG] Navigation:', path);
556
+
557
+ // Track path for "Open in New Window" (only for nextjs tab)
558
+ if (this.iframe.id === 'nextjs-dashboard-iframe-nextjs') {
559
+ this._updateAlpinePath(path);
560
+ }
561
+ }
562
+
563
+ _updateAlpinePath(path) {
564
+ const alpineEl = document.querySelector('[x-data]');
565
+ if (!alpineEl || !window.Alpine) return;
566
+
567
+ const alpineData = window.Alpine.$data(alpineEl);
568
+ if (alpineData) {
569
+ alpineData.currentNextjsPath = path;
570
+ }
571
+ }
572
+
573
+ destroy() {
574
+ this.messageBridge.destroy();
575
+ if (this.messageListener) {
576
+ window.removeEventListener('message', this.messageListener);
577
+ }
578
+ }
579
+ }
580
+
581
+ /**
582
+ * ExternalIframeHandler - Handles external iframes (docs) without postMessage
583
+ */
584
+ class ExternalIframeHandler {
585
+ constructor(iframe, loading) {
586
+ this.iframe = iframe;
587
+ this.loading = loading;
588
+ this.LOAD_TIMEOUT = 5000;
589
+ }
590
+
591
+ init() {
592
+ this._setupLoadHandler();
593
+ this._setupErrorHandler();
594
+ this._setupTimeout();
595
+ }
596
+
597
+ _setupLoadHandler() {
598
+ this.iframe.addEventListener('load', () => {
599
+ console.log('[Django-CFG] External iframe loaded');
600
+ this._showIframe();
601
+ });
602
+ }
603
+
604
+ _setupErrorHandler() {
605
+ this.iframe.addEventListener('error', () => {
606
+ console.error('[Django-CFG] External iframe failed to load');
607
+ this._showError();
608
+ });
609
+ }
610
+
611
+ _setupTimeout() {
612
+ setTimeout(() => {
613
+ if (!this.iframe.classList.contains('loaded')) {
614
+ console.log('[Django-CFG] Timeout - showing iframe anyway');
615
+ this._showIframe();
616
+ }
617
+ }, this.LOAD_TIMEOUT);
618
+ }
619
+
620
+ _showIframe() {
621
+ this.loading?.classList.add('hidden');
622
+ this.iframe.classList.add('loaded');
623
+ }
624
+
625
+ _showError() {
626
+ const loadingText = this.loading?.querySelector('p');
627
+ if (loadingText) {
628
+ loadingText.textContent = 'Failed to load. Please check your connection.';
480
629
  }
481
- });
630
+ }
631
+
632
+ destroy() {
633
+ // No cleanup needed for external iframes
634
+ }
482
635
  }
636
+
637
+ /**
638
+ * IframeManager - Factory for creating appropriate iframe handlers
639
+ */
640
+ class IframeManager {
641
+ constructor() {
642
+ this.handlers = [];
643
+ }
644
+
645
+ /**
646
+ * Register an iframe for management
647
+ */
648
+ register(iframeId, loadingId, options = {}) {
649
+ const iframe = document.getElementById(iframeId);
650
+ const loading = document.getElementById(loadingId);
651
+
652
+ if (!iframe) {
653
+ console.warn(`[Django-CFG] Iframe not found: ${iframeId}`);
654
+ return;
655
+ }
656
+
657
+ const handler = this._createHandler(iframe, loading, options);
658
+ if (handler) {
659
+ handler.init();
660
+ this.handlers.push(handler);
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Create appropriate handler based on iframe type
666
+ */
667
+ _createHandler(iframe, loading, options) {
668
+ const src = iframe.src || iframe.getAttribute('src');
669
+ if (!src) {
670
+ console.warn('[Django-CFG] Iframe has no src');
671
+ return null;
672
+ }
673
+
674
+ const url = new URL(src, window.location.origin);
675
+ const isExternal = options.external || url.origin !== window.location.origin;
676
+
677
+ if (isExternal) {
678
+ return new ExternalIframeHandler(iframe, loading);
679
+ } else {
680
+ return new InternalIframeHandler(iframe, loading, url.origin);
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Cleanup all handlers
686
+ */
687
+ destroy() {
688
+ this.handlers.forEach(handler => handler.destroy());
689
+ this.handlers = [];
690
+ }
691
+ }
692
+
693
+ // Initialize iframe manager
694
+ const manager = new IframeManager();
695
+
696
+ // Register all iframes
697
+ manager.register('nextjs-dashboard-iframe-builtin', 'iframe-loading-builtin');
698
+ manager.register('nextjs-dashboard-iframe-nextjs', 'iframe-loading-nextjs');
699
+ manager.register('nextjs-dashboard-iframe-docs', 'iframe-loading-docs', { external: true });
700
+
701
+ // Cleanup on page unload
702
+ window.addEventListener('beforeunload', () => {
703
+ manager.destroy();
704
+ });
705
+
483
706
  })();
484
707
  </script>
485
708
  {% endblock %}