dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
core.js
Go to the documentation of this file.
1/**
2 * @file core.js
3 * Clientseitiger dbXapp-Kernel.
4 *
5 * Sinn:
6 * - stellt den globalen Namespace `window.dbx` bereit
7 * - stellt `dbx.feature.register()` fuer selbstregistrierende Feature-Libs bereit
8 * - scannt `data-dbx`-Marker im DOM
9 * - stellt Events, Logging, Runtime-Footer und Task-Phasen bereit
10 *
11 * Feature-Beispiel:
12 * ```js
13 * dbx.feature.register("beispiel", {
14 * selector: ".dbxBeispiel",
15 * scope: "element",
16 * init: function (el, cfg) {
17 * el.textContent = cfg.label || "bereit";
18 * }
19 * });
20 * ```
21 *
22 * Template-Beispiel:
23 * ```html
24 * <div data-dbx="lib=beispiel|label=Hallo"></div>
25 * ```
26 */
27
28(function (window, document, $) {
29 "use strict";
30
31
32 /* =====================================================
33 * Namespace
34 * ===================================================== */
35 if (!window.dbx) window.dbx = {};
36 const dbx = window.dbx;
37
38 dbx._tasks = [];
39 dbx.assetVersion = "";
40
41 /**
42 * Liefert einen Framework-Text in der aktiven Dokumentensprache.
43 *
44 * Fachliche Formular- und Reportmeldungen kommen weiterhin aus der
45 * sprachabhängigen FD. Diese kleine Funktion ist ausschließlich für
46 * generische JavaScript-Chrome wie Laufzeit, Grid oder Sortierstatus.
47 */
48 dbx.translate = function (translations, fallbackLanguage) {
49 const language = String(document.documentElement.lang || "de")
50 .toLowerCase()
51 .slice(0, 2);
52 const fallback = String(fallbackLanguage || "de")
53 .toLowerCase()
54 .slice(0, 2);
55 const values = translations && typeof translations === "object"
56 ? translations
57 : {};
58
59 return values[language] ?? values[fallback] ?? "";
60 };
61
62 try {
63 const currentScript = document.currentScript;
64 if (currentScript && currentScript.src) {
65 dbx.assetVersion = new URL(currentScript.src, location.href).searchParams.get("v") || "";
66 }
67 } catch (e) {}
68
69
70 /* =====================================================
71 * WINDOW HELPER (GLOBAL BRIDGE)
72 * ===================================================== */
73 dbx.win = {
74
75 getCurrentWindowEl() {
76 const script = document.currentScript;
77 if (script) {
78 return script.closest?.('.dbx-window') || null;
79 }
80
81 const el = document.activeElement;
82 return el?.closest?.('.dbx-window') || null;
83 },
84
85 getCurrentWindowId() {
86 const win = this.getCurrentWindowEl();
87 return win ? win.id : null;
88 },
89
90 close(target) {
91 if (target === 'all' || target === '*') {
92 if (dbx.openWin && dbx.openWin.closeAll) {
93 dbx.openWin.closeAll();
94 }
95 return;
96 }
97
98 const id = this.getCurrentWindowId();
99
100 if (id && dbx.openWin && dbx.openWin.close) {
101 dbx.openWin.close(id);
102 }
103 },
104
105 reload(target) {
106 if (target === 'all' || target === '*') {
107
108 if (!dbx.openWin || !dbx.openWin.getAll) return;
109
110 dbx.openWin.getAll().forEach(w => {
111 if (w && w.id) {
112 this._reloadSingle(w.id);
113 }
114 });
115
116 return;
117 }
118
119 const id = this.getCurrentWindowId();
120 if (id) {
121 this._reloadSingle(id);
122 }
123 },
124
125 _reloadSingle(windowId) {
126
127 if (!dbx.openWin || !dbx.openWin.get) return;
128
129 const w = dbx.openWin.get(windowId);
130 if (!w || !w.url) return;
131
132 let url = w.url;
133
134 const sep = url.includes("?") ? "&" : "?";
135 url = url + sep + "_=" + Date.now();
136
137 if (dbx.openWin && dbx.openWin.open) {
138 dbx.openWin.open(w.cfg);
139 }
140 }
141
142 };
143
144
145
146 /* =====================================================
147 * LOG SYSTEM
148 * ===================================================== */
149 dbx.LOG = false;
150
151 dbx.log = (...a) => {
152 if (dbx.LOG) console.log("[dbx " + dbx._ts() + "]", ...a);
153 };
154
155 dbx.warn = function (...a) {
156 console.warn("[dbx " + dbx._ts() + "]", ...a);
157 if (a.length >= 3 && typeof a[0] === "string" && typeof a[1] === "string") {
158 dbx.diag("warn", a[0], a[1], String(a[2] || ""), a[3] || {});
159 }
160 };
161
162 dbx.isIgnorableBrowserError = function (msg) {
163 const text = String(msg || "");
164
165 return (
166 text.includes("ResizeObserver loop completed with undelivered notifications") ||
167 text.includes("ResizeObserver loop limit exceeded")
168 );
169 };
170
171 dbx.error = (...a) => {
172
173 const msg = "[dbx " + dbx._ts() + "] " + a.join(" ");
174
175 // -------------------------------------------------
176 // 1. console (immer)
177 // -------------------------------------------------
178 console.error(msg);
179
180 // -------------------------------------------------
181 // 2. intern speichern
182 // -------------------------------------------------
183 dbx._errors = dbx._errors || [];
184 dbx._errors.push({
185 time: Date.now(),
186 message: msg
187 });
188
189 dbx._hasCriticalError = true;
190
191 if (dbx.diag && dbx.diag.queueReport) {
192 const diagEntry = {
193 time: Date.now(),
194 level: "error",
195 lib: "core",
196 code: "JS_ERROR",
197 message: msg,
198 ctx: { args: a },
199 element: ""
200 };
201 dbx._diagnostics.push(diagEntry);
202 dbx.diag.queueReport(diagEntry);
203 }
204
205 // -------------------------------------------------
206 // 3. DEV MODE → optional alert
207 // -------------------------------------------------
208 if (dbx.LOG) {
209 // ⚠️ kein Spam – nur erste Meldung als Alert
210 if (!dbx.__errorAlertShown) {
211 alert(
212 "[DBX ERROR]\n\n" +
213 msg
214 );
215 dbx.__errorAlertShown = true;
216 }
217 }
218
219 // -------------------------------------------------
220 // 4. LIVE MODE → visueller Indikator (minimal)
221 // -------------------------------------------------
222 if (!dbx.LOG) {
223
224 if (!dbx.__errorIndicator) {
225
226 const el = document.createElement("div");
227
228 el.className = "dbx-error-indicator";
229 el.style.position = "fixed";
230 el.style.top = "10px";
231 el.style.right = "10px";
232 el.style.width = "12px";
233 el.style.height = "12px";
234 el.style.background = "#dc3545";
235 el.style.borderRadius = "50%";
236 el.style.zIndex = "99999";
237 el.style.boxShadow = "0 0 6px rgba(0,0,0,0.3)";
238 el.title = dbx.translate({
239 de: "DBX-Fehler aufgetreten",
240 en: "A DBX error occurred",
241 es: "Se ha producido un error de DBX"
242 });
243
244 document.body.appendChild(el);
245
246 dbx.__errorIndicator = el;
247 }
248 }
249 };
250
251 dbx._ts = function () {
252 const d = new Date();
253 const t = d.toISOString().split("T")[1];
254 return t.replace("Z", "");
255 };
256
257
258 /* =====================================================
259 * DIAGNOSTICS + DECLARE (Defaults, Aliase, Autoload)
260 * ===================================================== */
261 dbx._diagnostics = dbx._diagnostics || [];
262
263 dbx.diag = function (level, lib, code, message, ctx) {
264
265 const entry = {
266 time: Date.now(),
267 level: String(level || "info"),
268 lib: String(lib || "core"),
269 code: String(code || "DIAG"),
270 message: String(message || ""),
271 ctx: ctx || {},
272 element: dbx.diag.elementHint(ctx && ctx.el)
273 };
274
275 dbx._diagnostics.push(entry);
276
277 if (entry.level === "info" && !dbx.LOG) {
278 return entry;
279 }
280
281 const line = dbx.diag.format(entry);
282
283 if (entry.level === "error") {
284 console.error(line);
285 } else if (entry.level === "warn") {
286 console.warn(line);
287 } else if (dbx.LOG) {
288 console.log(line);
289 }
290
291 if (entry.level === "warn" || entry.level === "error") {
292 dbx.diag.queueReport(entry);
293 }
294
295 return entry;
296 };
297
298 dbx.diag.elementHint = function (el) {
299
300 if (!el || el.nodeType !== 1) return "";
301
302 const parts = [];
303
304 if (el.id) {
305 parts.push("#" + el.id);
306 } else if (el.classList && el.classList.length) {
307 parts.push("." + Array.from(el.classList).slice(0, 3).join("."));
308 } else {
309 parts.push(el.tagName.toLowerCase());
310 }
311
312 if (el.getAttribute && el.getAttribute("data-dbx")) {
313 const raw = el.getAttribute("data-dbx");
314 const m = String(raw).match(/lib=([^|]+)/);
315 if (m) parts.push("lib=" + m[1]);
316 }
317
318 return parts.join("");
319 };
320
321 dbx.diag.format = function (entry) {
322
323 const el = entry.element ? " @ " + entry.element : "";
324 const field = entry.ctx && entry.ctx.field ? " field=" + entry.ctx.field : "";
325 const src = entry.ctx && entry.ctx.source ? " source=" + entry.ctx.source : "";
326 const val = entry.ctx && entry.ctx.value !== undefined ? " value=" + JSON.stringify(entry.ctx.value) : "";
327
328 return "[dbx][" + entry.lib + "][" + entry.code + "] " + entry.message + el + field + src + val;
329 };
330
331 dbx.diag.queueReport = function (entry) {
332
333 dbx.diag._reportQueue = dbx.diag._reportQueue || [];
334 dbx.diag._reportQueue.push(entry);
335
336 if (dbx.diag._reportTimer) return;
337
338 dbx.diag._reportTimer = setTimeout(function () {
339 dbx.diag._reportTimer = null;
340 dbx.diag.flushReports();
341 }, 1500);
342 };
343
344 dbx.diag.reportUrl = function () {
345 const root = (dbx.config && dbx.config.rootPath) ? dbx.config.rootPath : "/";
346 return root + "?dbx_modul=dbxAdmin&dbx_run1=sysmsg&dbx_run2=client_diag";
347 };
348
349 dbx.diag.flushReports = function () {
350
351 const queue = (dbx.diag._reportQueue || []).slice();
352 dbx.diag._reportQueue = [];
353
354 if (!queue.length) return;
355
356 const payload = {
357 entries: queue.map(function (e) {
358 return {
359 level: e.level,
360 lib: e.lib,
361 code: e.code,
362 message: e.message,
363 element: e.element,
364 ctx: e.ctx
365 };
366 })
367 };
368
369 if (!dbx.ajax || typeof dbx.ajax.request !== "function") return;
370
371 dbx.ajax.request({
372 url: dbx.diag.reportUrl(),
373 method: "POST",
374 mode: "json",
375 headers: { "Content-Type": "application/json" },
376 body: JSON.stringify(payload),
377 keepalive: true,
378 skipRuntime: true
379 }).catch(function (err) {
380 dbx.log("diag report failed:", err);
381 });
382 };
383
384 dbx.declare = dbx.declare || {};
385
386 dbx.declare.schemas = dbx.declare.schemas || {};
387 dbx.declare.transforms = dbx.declare.transforms || {};
388 dbx.declare.infer = dbx.declare.infer || {};
389
390 dbx.declare.registerSchema = function (lib, schema) {
391 if (!lib || !schema) return;
392 dbx.declare.schemas[lib] = schema;
393 };
394
395 dbx.declare.readAttr = function (el, name) {
396 if (!el || !el.getAttribute) return null;
397 const v = el.getAttribute(name);
398 if (v === null) return null;
399 return String(v);
400 };
401
402 dbx.declare.readField = function (el, lib, field, spec, ctx) {
403
404 const aliases = [spec.attr].concat(spec.aliases || []).filter(Boolean);
405 let source = "missing";
406 let value;
407
408 if (ctx && ctx.base && ctx.base[field] !== undefined && String(ctx.base[field]).trim() !== "") {
409 return {
410 value: String(ctx.base[field]).trim(),
411 source: "data-dbx",
412 field: field
413 };
414 }
415
416 for (let i = 0; i < aliases.length; i++) {
417 const raw = dbx.declare.readAttr(el, aliases[i]);
418 if (raw !== null && String(raw).trim() !== "") {
419 source = aliases[i];
420 value = String(raw).trim();
421 break;
422 }
423 }
424
425 if (value === undefined && typeof spec.infer === "function") {
426 const inferred = spec.infer(el, ctx || {});
427 if (inferred !== undefined && inferred !== null && String(inferred).trim() !== "") {
428 source = "infer";
429 value = String(inferred).trim();
430 }
431 }
432
433 if (value === undefined) {
434 if (spec.default !== undefined) {
435 value = spec.default;
436 source = "default";
437 dbx.diag("info", lib, field.toUpperCase() + "_DEFAULT",
438 "Attribut nicht gesetzt, Standard verwendet",
439 { el: el, field: field, source: source, value: value });
440 } else if (spec.required) {
441 dbx.diag("warn", lib, field.toUpperCase() + "_MISSING",
442 "Pflicht-Attribut fehlt",
443 { el: el, field: field, source: source });
444 value = "";
445 } else {
446 value = "";
447 }
448 }
449
450 if (typeof spec.coerce === "function") {
451 value = spec.coerce(value, el, ctx || {});
452 }
453
454 return { value: value, source: source, field: field };
455 };
456
457 dbx.declare.infer.ajaxTarget = function (root) {
458
459 if (!root || root.nodeType !== 1) return "";
460
461 const id = String(root.id || "").trim();
462 if (/^dbx_target_\d+$/i.test(id)) return id;
463
464 const form = root.tagName === "FORM"
465 ? root
466 : root.querySelector("form.dbxAjax, form[id^='dbx_form_']");
467
468 if (form && form.id) {
469 const m = form.id.match(/dbx_form_(\d+)/i);
470 if (m) return "dbx_target_" + m[1];
471 }
472
473 const target = root.querySelector("[id^='dbx_target_']");
474 if (target && target.id) return target.id;
475
476 return "";
477 };
478
479 dbx.declare.infer.confirmRoot = function (el) {
480
481 if (!el || el.nodeType !== 1) return null;
482
483 const form = el.closest("form.dbxAjax");
484 if (form) return form;
485
486 const panel = el.closest(".dbx-confirm-root, [data-dbx*='lib=confirm']");
487 if (panel) return panel;
488
489 return el.parentElement || document.body;
490 };
491
492 dbx.declare.buildFromSchema = function (lib, root, baseCfg, index) {
493
494 const schema = dbx.declare.schemas[lib];
495 if (!schema || !schema.fields) return null;
496
497 const ctx = { root: root, index: index, base: baseCfg || {} };
498 const raw = { _index: index, lib: lib };
499
500 Object.keys(schema.fields).forEach(function (field) {
501 const spec = schema.fields[field];
502 const res = dbx.declare.readField(root, lib, field, spec, ctx);
503 raw[field] = res.value;
504 raw["_" + field + "Source"] = res.source;
505 });
506
507 if (typeof dbx.declare.transforms[lib] === "function") {
508 return dbx.declare.transforms[lib](raw, root);
509 }
510
511 return raw;
512 };
513
514 dbx.declare.resolve = function (lib, root) {
515
516 if (!root) return [];
517
518 const cacheKey = "_dbx" + lib.charAt(0).toUpperCase() + lib.slice(1) + "Configs";
519
520 if (Array.isArray(root[cacheKey])) {
521 return root[cacheKey];
522 }
523
524 const attr = dbx.declare.readAttr(root, "data-dbx") || "";
525 const parsed = dbx.parseData(attr).filter(function (cfg) {
526 return cfg.lib === lib;
527 });
528
529 let out = [];
530
531 if (parsed.length) {
532 out = parsed.map(function (cfg, index) {
533 return dbx.declare.buildFromSchema(lib, root, cfg, index) || cfg;
534 });
535 } else if (dbx.declare.schemas[lib]) {
536 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
537 }
538
539 if (!out.length && dbx.declare.schemas[lib]) {
540 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
541 }
542
543 root[cacheKey] = out;
544
545 return out;
546 };
547
548 dbx.declare.hasLibInDataDbx = function (el, lib) {
549 const attr = dbx.declare.readAttr(el, "data-dbx") || "";
550 return dbx.parseData(attr).some(function (cfg) {
551 return cfg.lib === lib;
552 });
553 };
554
555 dbx.declare.markAutoload = function (el, lib) {
556 el.__dbxAutoload = el.__dbxAutoload || {};
557 if (el.__dbxAutoload[lib]) return false;
558 el.__dbxAutoload[lib] = true;
559 return true;
560 };
561
562 dbx.ensureElementScopeStores = function (el) {
563
564 if (!el || el.nodeType !== 1) return;
565
566 el.__dbxInitialized = el.__dbxInitialized || {};
567 el.__dbxState = el.__dbxState || {};
568 el.__dbxError = el.__dbxError || {};
569 };
570
571 dbx.declare.queueFeature = function (lib, el, scopeType, cfg) {
572
573 if (!dbx.declare.markAutoload(el, lib)) return;
574
575 dbx.ensureElementScopeStores(el);
576
577 const f = dbx.feature._features[lib];
578 let scope = scopeType || "element";
579
580 if (f && f.scope) {
581 scope = f.scope;
582 }
583
584 const id = (cfg && cfg.id) ? cfg.id : (el.id || "undef");
585 const keyScoped = scope + ":" + lib + ":" + id;
586
587 dbx._tasks.push({
588 lib: lib,
589 el: el,
590 cfg: Object.assign({ lib: lib, id: id }, cfg || {}),
591 _key: keyScoped,
592 _scopeType: scope
593 });
594 };
595
596 dbx.declare.scanAutoload = function (ctx) {
597
598 const root = ctx || document;
599
600 root.querySelectorAll("form.dbxAjax").forEach(function (form) {
601 if (!dbx.declare.hasLibInDataDbx(form, "ajax")) {
602 dbx.declare.queueFeature("ajax", form, "element", { id: form.id || "undef" });
603 }
604 if (!dbx.declare.hasLibInDataDbx(form, "form")) {
605 const wrap = form.closest(".dbxForm_wrapper") || form;
606 dbx.declare.queueFeature("form", wrap, "element", { id: wrap.id || form.id || "undef" });
607 }
608 if (form.querySelector(".dbxConfirm, [data-confirm]") && !dbx.declare.hasLibInDataDbx(form, "confirm")) {
609 dbx.declare.queueFeature("confirm", form, "element", { id: form.id || "undef" });
610 }
611 });
612
613 root.querySelectorAll(".dbx-ajax-root").forEach(function (el) {
614 if (!dbx.declare.hasLibInDataDbx(el, "ajax")) {
615 dbx.declare.queueFeature("ajax", el, "element", { id: el.id || "undef" });
616 }
617 });
618
619 root.querySelectorAll(".dbxConfirm:not(form), [data-confirm]:not(form)").forEach(function (el) {
620 const confirmRoot = dbx.declare.infer.confirmRoot(el);
621 if (confirmRoot && !dbx.declare.hasLibInDataDbx(confirmRoot, "confirm")) {
622 dbx.declare.queueFeature("confirm", confirmRoot, "element", { id: confirmRoot.id || "undef" });
623 }
624 });
625
626 root.querySelectorAll(".dbx-win, .dbx-win-preload").forEach(function (el) {
627 if (!dbx.declare.hasLibInDataDbx(el, "openWin")) {
628 dbx.declare.queueFeature("openWin", el, "global", { id: el.id || "undef" });
629 }
630 });
631
632 dbx.log("declare.scanAutoload → tasks:", dbx._tasks.length);
633 };
634
635
636 /* =====================================================
637 * GLOBAL ERROR SHIELD (FAIL-SAFE)
638 * ===================================================== */
639
640 if (!dbx.__errorShield) {
641
642 window.onerror = function (msg, src, line, col, err) {
643
644 if (dbx.isIgnorableBrowserError(msg)) {
645 dbx.log("IGNORED BROWSER ERROR:", msg);
646 return true;
647 }
648
649 dbx.error(
650 "GLOBAL ERROR:",
651 msg,
652 "at", src + ":" + line + ":" + col
653 );
654
655 // DEV → Fehler NICHT unterdrücken (Browser zeigt Stacktrace)
656 if (dbx.LOG) return false;
657
658 // LIVE → Fehler unterdrücken (silent)
659 return true;
660 };
661
662 window.onunhandledrejection = function (e) {
663
664 const reason = e && e.reason ? e.reason : "";
665 const msg = reason && reason.message ? reason.message : reason;
666 if (dbx.isIgnorableBrowserError(msg)) {
667 dbx.log("IGNORED PROMISE ERROR:", msg);
668 return true;
669 }
670
671 dbx.error(
672 "PROMISE ERROR:",
673 reason
674 );
675 };
676
677 dbx.__errorShield = true;
678 }
679
680
681
682
683 /* =====================================================
684 * CONFIG
685 * ===================================================== */
686 dbx.config = dbx.config || (function () {
687
688 let scriptPath = "/";
689 let design = "default";
690 let rootPath = "/";
691 let log = false;
692
693 const scripts = document.getElementsByTagName("script");
694
695 for (let s of scripts) {
696
697 if (!s.src || !s.src.includes("core.js")) continue;
698
699 const url = new URL(s.src, window.location.origin);
700
701 scriptPath = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
702
703 const d = url.searchParams.get("design");
704 if (d) design = d;
705
706 const l = url.searchParams.get("log");
707 if (l && l.toLowerCase() === "on") log = true;
708
709 if (scriptPath.includes("/js/lib/")) {
710 rootPath = scriptPath.split("/js/lib/")[0] + "/";
711 }
712
713 break;
714 }
715
716 return {
717 libPath: scriptPath,
718 rootPath,
719 design,
720 log
721 };
722
723 })();
724
725 dbx.LOG = dbx.config.log;
726 dbx.log("core init → config:", dbx.config);
727
728 /* =====================================================
729 * UI STATE
730 * ===================================================== */
731 dbx.uiSet = function (lib, id, key, val) {
732 if (!lib || !id || !key || id === "undef") return;
733 const k = `dbx.UI.${lib}.${id}.${key}`;
734 try {
735 localStorage.setItem(k, JSON.stringify(val));
736 dbx.log("uiSet:", k, val);
737 } catch (e) {
738 dbx.warn("uiSet failed:", k, e);
739 }
740 };
741
742 dbx.uiGet = function (lib, id, key, def) {
743 if (!lib || !id || !key || id === "undef") return def;
744 const k = `dbx.UI.${lib}.${id}.${key}`;
745 try {
746 const v = localStorage.getItem(k);
747 if (v === null) return def;
748 const parsed = JSON.parse(v);
749 dbx.log("uiGet:", k, parsed);
750 return parsed;
751 } catch (e) {
752 dbx.warn("uiGet failed:", k, e);
753 return def;
754 }
755 };
756
757 dbx.getDesign = () => dbx.config.design;
758 dbx.getLibId = cfg => (cfg && cfg.id) ? cfg.id : "undef";
759
760 /* =====================================================
761 * FOOTER STATUS
762 * ===================================================== */
763 dbx.footerStatus = dbx.footerStatus || (function () {
764
765 function formatSeconds(seconds) {
766 seconds = Math.max(0, Number(seconds) || 0);
767 return seconds.toFixed(3);
768 }
769
770 function formatPhpSeconds(seconds) {
771 seconds = Math.max(0, Number(seconds) || 0);
772 return Math.max(0.001, seconds).toFixed(3);
773 }
774
775 function runtimeElement() {
776 return document.querySelector("[data-dbx-runtime]");
777 }
778
779 function responseRuntimeSeconds() {
780 const perf = window.performance;
781 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
782 if (nav && nav.duration) {
783 return nav.duration / 1000;
784 }
785
786 return perf && perf.now ? perf.now() / 1000 : 0;
787 }
788
789 function navigationPhpRuntimeSeconds() {
790 const perf = window.performance;
791 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
792 const entries = nav && nav.serverTiming ? Array.from(nav.serverTiming) : [];
793 const timer = entries.find(item => item && item.name === "dbxphp");
794 const duration = timer ? Number(timer.duration) : NaN;
795
796 return Number.isFinite(duration) && duration >= 0 ? duration / 1000 : null;
797 }
798
799 function phpRuntimeSeconds(el) {
800 const headerRuntime = navigationPhpRuntimeSeconds();
801 if (headerRuntime !== null) {
802 return headerRuntime;
803 }
804
805 const raw = el ? el.getAttribute("data-dbx-php-runtime") : "";
806 const value = Number(raw);
807
808 return Number.isFinite(value) && value >= 0 ? value : null;
809 }
810
811 function write(responseSeconds, phpSeconds, label) {
812 const el = runtimeElement();
813 if (!el) return;
814
815 const responseLabel = formatSeconds(responseSeconds);
816 const hasPhp = phpSeconds != null && phpSeconds !== ""
817 && Number.isFinite(Number(phpSeconds)) && Number(phpSeconds) >= 0;
818 const phpValue = hasPhp ? Number(phpSeconds) : null;
819 const phpLabel = hasPhp ? "/" + formatPhpSeconds(phpValue) : "";
820 const text = responseLabel + phpLabel + " sec";
821
822 el.textContent = text;
823
824 if (hasPhp) {
825 el.setAttribute("data-dbx-php-runtime", formatPhpSeconds(phpValue));
826 }
827
828 const title = hasPhp
829 ? label + ": " + responseLabel + " / " + formatPhpSeconds(phpValue) + " sec"
830 : label + ": " + responseLabel + " sec";
831
832 el.setAttribute("title", title);
833 el.setAttribute("data-dbx-page-runtime-title", title);
834 }
835
836 function init() {
837 const el = runtimeElement();
838 if (!el) return;
839
840 const update = function () {
841 write(
842 responseRuntimeSeconds(),
843 phpRuntimeSeconds(el),
844 dbx.translate({
845 de: "Komplette Antwortlaufzeit / PHP-Laufzeit",
846 en: "Complete response time / PHP runtime",
847 es: "Tiempo total de respuesta / tiempo de ejecución de PHP"
848 })
849 );
850 };
851
852 if (document.readyState === "complete") {
853 update();
854 } else {
855 window.addEventListener("load", update, { once: true });
856 setTimeout(update, 0);
857 }
858 }
859
860 function requestHasAjaxFlag(url, body) {
861 const targetUrl = String(url || "");
862 if (/[?&]dbx_ajax=1(?:&|$)/.test(targetUrl)) {
863 return true;
864 }
865
866 if (body instanceof URLSearchParams && body.get("dbx_ajax") === "1") {
867 return true;
868 }
869
870 if (body instanceof FormData && body.get("dbx_ajax") === "1") {
871 return true;
872 }
873
874 if (typeof body === "string" && /(?:^|&)dbx_ajax=1(?:&|$)/.test(body)) {
875 return true;
876 }
877
878 return false;
879 }
880
881 function shouldTrackAjaxRuntime(url, body, options) {
882 if (options && (
883 options.skipRuntime === true ||
884 options.footerRuntime === "hidden" ||
885 options.footerRuntime === "skip" ||
886 options.footerRuntime === "0"
887 )) {
888 return false;
889 }
890
891 if (options && String(options.mode || "").toLowerCase() === "json") {
892 return false;
893 }
894
895 const targetUrl = String(url || "");
896 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
897 return false;
898 }
899
900 if (body instanceof URLSearchParams && body.get("dbx_sync") === "0") {
901 return false;
902 }
903
904 if (body instanceof FormData && body.get("dbx_sync") === "0") {
905 return false;
906 }
907
908 if (typeof body === "string" && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
909 return false;
910 }
911
912 return true;
913 }
914
915 return {
916 init,
917 formatSeconds,
918 shouldTrackAjaxRuntime,
919 updateAjax(responseSeconds, phpSeconds) {
920 write(
921 responseSeconds,
922 phpSeconds,
923 dbx.translate({
924 de: "Letzte AJAX-Anfrage / PHP-Laufzeit",
925 en: "Latest AJAX request / PHP runtime",
926 es: "Última solicitud AJAX / tiempo de ejecución de PHP"
927 })
928 );
929 }
930 };
931
932 })();
933
934 /* =====================================================
935 * LOADER
936 * ===================================================== */
937 dbx.loader = {
938
939 _loaded: {},
940 _callbacks: {},
941
942 _cacheBustUrl(url) {
943 const searchParams = new URLSearchParams(location.search);
944 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust") || dbx.assetVersion;
945
946 if (!cacheBust) return url;
947
948 try {
949 const parsed = new URL(url, location.origin);
950 if (parsed.searchParams.has("v")) return url;
951 } catch(e) {}
952
953 return url + (url.includes("?") ? "&" : "?") + "v=" + encodeURIComponent(cacheBust);
954 },
955
956 css(url, cb) {
957
958 if (!url) {
959 cb && cb({ ok: false });
960 return;
961 }
962
963 url = this._cacheBustUrl(url);
964
965 if (this._loaded[url] === "loaded") {
966 dbx.log("CSS already loaded:", url);
967 return cb && cb({ ok: true });
968 }
969
970 if (this._loaded[url] === "loading") {
971 dbx.log("CSS already loading:", url);
972 (this._callbacks[url] ||= []).push(cb);
973 return;
974 }
975
976 // 🔥 FIX: sauberes URL Matching
977 const requested = new URL(url, location.origin);
978 const target = requested.pathname;
979 const targetSearch = requested.search;
980
981 const links = document.querySelectorAll('link[rel="stylesheet"]');
982 for (let l of links) {
983 try {
984 const current = new URL(l.href);
985 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
986 this._loaded[url] = "loaded";
987 dbx.log("CSS already in DOM:", url);
988 return cb && cb({ ok: true });
989 }
990 } catch(e){}
991 }
992
993 dbx.log("CSS load start:", url);
994
995 this._loaded[url] = "loading";
996 this._callbacks[url] = cb ? [cb] : [];
997
998 const link = document.createElement("link");
999 link.rel = "stylesheet";
1000 link.href = url;
1001
1002 let finished = false;
1003
1004 function done(self, status) {
1005 if (finished) return;
1006 finished = true;
1007
1008 self._callbacks[url].forEach(f => f && f(status));
1009 self._callbacks[url] = [];
1010 }
1011
1012 link.onload = () => {
1013 this._loaded[url] = "loaded";
1014 dbx.log("CSS loaded:", url);
1015 done(this, { ok: true });
1016 };
1017
1018 link.onerror = () => {
1019 this._loaded[url] = "error";
1020 dbx.error("CSS load failed:", url);
1021 done(this, { ok: false });
1022 };
1023
1024 document.head.appendChild(link);
1025 },
1026
1027 js(url, cb) {
1028
1029 if (!url) {
1030 cb && cb({ ok: false });
1031 return;
1032 }
1033
1034 url = this._cacheBustUrl(url);
1035
1036 if (this._loaded[url] === "loaded") {
1037 dbx.log("JS already loaded:", url);
1038 return cb && cb({ ok: true });
1039 }
1040
1041 if (this._loaded[url] === "loading") {
1042 dbx.log("JS already loading:", url);
1043 (this._callbacks[url] ||= []).push(cb);
1044 return;
1045 }
1046
1047 // 🔥 FIX: sauberes URL Matching
1048 const requested = new URL(url, location.origin);
1049 const target = requested.pathname;
1050 const targetSearch = requested.search;
1051
1052 const scripts = document.querySelectorAll('script[src]');
1053 for (let s of scripts) {
1054 try {
1055 const current = new URL(s.src);
1056 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
1057 this._loaded[url] = "loaded";
1058 dbx.log("JS already in DOM:", url);
1059 return cb && cb({ ok: true });
1060 }
1061 } catch(e){}
1062 }
1063
1064 dbx.log("JS load start:", url);
1065
1066 this._loaded[url] = "loading";
1067 this._callbacks[url] = cb ? [cb] : [];
1068
1069 const s = document.createElement("script");
1070 s.src = url;
1071 s.async = false;
1072
1073 let finished = false;
1074
1075 function done(self, status) {
1076 if (finished) return;
1077 finished = true;
1078
1079 self._callbacks[url].forEach(f => f && f(status));
1080 self._callbacks[url] = [];
1081 }
1082
1083 s.onload = () => {
1084 this._loaded[url] = "loaded";
1085 dbx.log("JS loaded:", url);
1086 done(this, { ok: true });
1087 };
1088
1089 s.onerror = () => {
1090 this._loaded[url] = "error";
1091 dbx.error("JS load failed:", url);
1092 done(this, { ok: false });
1093 };
1094
1095 document.body.appendChild(s);
1096 }
1097
1098 };
1099
1100 dbx.add_css = function (type, file, cb) {
1101
1102 if (!type || !file) {
1103 cb && cb({ ok: false }); // 🔥 FIX
1104 return;
1105 }
1106
1107 let url = "";
1108
1109 if (type === "lib") {
1110 url = dbx.config.libPath + file;
1111 }
1112
1113 if (type === "design") {
1114 url = dbx.config.rootPath + "design/" + dbx.config.design + "/css/" + file;
1115 }
1116
1117 if (type === "root") {
1118 url = dbx.config.rootPath + file;
1119 }
1120
1121 const searchParams = new URLSearchParams(location.search);
1122 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1123 if (cacheBust) {
1124 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1125 }
1126
1127 if (!url) {
1128 dbx.warn("add_css invalid type:", type);
1129
1130 alert(
1131 "[DBX ERROR]\n" +
1132 "Invalid CSS scope\n\n" +
1133 "type=" + type + "\n" +
1134 "file=" + file
1135 );
1136
1137 cb && cb({ ok: false }); // 🔥 FIX
1138 return;
1139 }
1140
1141 dbx.log("add_css:", type, "→", url);
1142 dbx.loader.css(url, cb);
1143 };
1144
1145 dbx.add_js = function (type, file, cb) {
1146
1147 if (!type || !file) {
1148 cb && cb({ ok: false }); // 🔥 FIX
1149 return;
1150 }
1151
1152 let url = "";
1153
1154 if (type === "lib") {
1155 url = dbx.config.libPath + file;
1156 }
1157
1158 if (type === "design") {
1159 url = dbx.config.rootPath + "design/" + dbx.config.design + "/js/" + file;
1160 }
1161
1162 if (type === "root") {
1163 url = dbx.config.rootPath + file;
1164 }
1165
1166 const searchParams = new URLSearchParams(location.search);
1167 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1168 if (cacheBust) {
1169 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1170 }
1171
1172 if (!url) {
1173 dbx.warn("add_js invalid type:", type);
1174
1175 alert(
1176 "[DBX ERROR]\n" +
1177 "Invalid JS scope\n\n" +
1178 "type=" + type + "\n" +
1179 "file=" + file
1180 );
1181
1182 cb && cb({ ok: false }); // 🔥 FIX
1183 return;
1184 }
1185
1186 dbx.log("add_js:", type, "→", url);
1187 dbx.loader.js(url, cb);
1188 };
1189
1190 /* =====================================================
1191 * DEPENDENCY CHAIN
1192 * ===================================================== */
1193 dbx.load = function (list, done) {
1194
1195 let i = 0;
1196
1197 function next() {
1198
1199 if (i >= list.length) {
1200 dbx.log("dbx.load → complete");
1201 return done && done();
1202 }
1203
1204 const [type, scope, file] = list[i++];
1205
1206 dbx.log("dbx.load → step:", type, scope, file);
1207
1208 let finished = false;
1209
1210 function safeNext(res) {
1211
1212 if (finished) return;
1213 finished = true;
1214
1215 if (!res || res.ok !== true) {
1216 dbx.warn("dbx.load → asset failed:", type, file);
1217 }
1218
1219 next();
1220 }
1221
1222 // 🔥 FAILSAFE TIMEOUT (z.B. 5s)
1223 const timer = setTimeout(() => {
1224
1225 dbx.error("dbx.load timeout:", type, file);
1226
1227 safeNext({ ok: false });
1228
1229 }, 5000);
1230
1231 function wrappedNext(res) {
1232 clearTimeout(timer);
1233 safeNext(res);
1234 }
1235
1236 if (type === "js") {
1237 dbx.add_js(scope, file, wrappedNext);
1238 } else if (type === "css") {
1239 dbx.add_css(scope, file, wrappedNext);
1240 } else {
1241 wrappedNext({ ok: true });
1242 }
1243 }
1244
1245 next();
1246 };
1247
1248 /* =====================================================
1249 * FEATURE REGISTRY
1250 * ===================================================== */
1251 dbx.feature = {
1252
1253 _features: {},
1254
1255 register(name, obj) {
1256 this._features[name] = obj;
1257 dbx.log("feature registered:", name);
1258 },
1259
1260 has(name) {
1261 return Object.prototype.hasOwnProperty.call(this._features, name);
1262 },
1263
1264 init(name, el, cfg) {
1265
1266 dbx.log("init feature:", name, "id:", cfg.id);
1267
1268 if (this.has(name)) {
1269 try {
1270 this._features[name].init(el, cfg);
1271 } catch (e) {
1272 dbx.error("Feature init failed:", name, e);
1273 }
1274 return;
1275 }
1276
1277 dbx.error("Feature not registered:", name);
1278 }
1279 };
1280
1281 dbx.loadUtilities = function () {
1282 const existing = document.querySelector('script[src*="/utilities.js"],script[src$="utilities.js"]');
1283 if (existing) {
1284 return;
1285 }
1286
1287 dbx.add_js("lib", "utilities.js");
1288 };
1289
1290 if (document.body) {
1291 dbx.loadUtilities();
1292 } else {
1293 document.addEventListener("DOMContentLoaded", dbx.loadUtilities, { once: true });
1294 }
1295
1296 /* =====================================================
1297 * PARSER
1298 * ===================================================== */
1299 dbx.parseData = function (str) {
1300
1301 const result = [];
1302 if (!str) return result;
1303
1304 const blocks = str.split("||");
1305
1306 function normalize(val) {
1307
1308 if (val === undefined || val === null) return val;
1309
1310 const raw = val.toString().trim();
1311 const v = raw.toLowerCase();
1312
1313 if (v === "on" || v === "1") return 1;
1314 if (v === "off" || v === "0") return 0;
1315 if (v === "true" || v === "1") return 1;
1316 if (v === "false" || v === "0") return 0;
1317
1318 // 🔥 JSON AUTO PARSE
1319 if (
1320 (raw.startsWith("{") && raw.endsWith("}")) ||
1321 (raw.startsWith("[") && raw.endsWith("]"))
1322 ) {
1323 try {
1324 return JSON.parse(raw);
1325 } catch (e) {
1326 dbx.warn("parseData JSON failed:", raw);
1327 }
1328 }
1329
1330 return raw;
1331 }
1332
1333 blocks.forEach(block => {
1334
1335 const cfg = {};
1336 if (!block) return;
1337
1338 block.split("|").forEach(part => {
1339
1340 if (!part) return;
1341
1342 const idx = part.indexOf("=");
1343 if (idx === -1) return;
1344
1345 // 🔥 FIX 1: key lowercase
1346 const key = part.substring(0, idx).trim().toLowerCase();
1347
1348 // 🔥 FIX 2: kein trim hier (macht normalize)
1349 let val = part.substring(idx + 1);
1350
1351 val = normalize(val);
1352
1353 cfg[key] = val;
1354 });
1355
1356 if (cfg.lib) result.push(cfg);
1357 });
1358
1359 return result;
1360 };
1361
1362 /* =====================================================
1363 * RESOLVER (EXACT + FALLBACK BEHALTEN!)
1364 * ===================================================== */
1365 function waitForFeatureRegistration(libName, callback, attempts) {
1366
1367 if (dbx.feature.has(libName)) {
1368 callback(true);
1369 return;
1370 }
1371
1372 attempts = attempts === undefined ? 8 : attempts;
1373
1374 if (attempts <= 0) {
1375 callback(false);
1376 return;
1377 }
1378
1379 window.setTimeout(function () {
1380 waitForFeatureRegistration(libName, callback, attempts - 1);
1381 }, 25);
1382 }
1383
1384 function reloadFeatureScript(libName, callback) {
1385 const url = dbx.config.libPath + libName + ".js";
1386 const target = new URL(url, location.origin).pathname;
1387
1388 document.querySelectorAll("script[src]").forEach(script => {
1389 try {
1390 if (new URL(script.src).pathname === target) {
1391 script.parentNode && script.parentNode.removeChild(script);
1392 }
1393 } catch (e) {}
1394 });
1395
1396 const reloadUrl = url + (url.includes("?") ? "&" : "?") + "dbx_reload=" + Date.now();
1397 dbx.loader._loaded[url] = null;
1398 dbx.loader._loaded[reloadUrl] = null;
1399
1400 dbx.log("resolveFeature → reload script:", libName);
1401 dbx.loader.js(reloadUrl, function (res) {
1402 if (!res || res.ok !== true) {
1403 callback(false);
1404 return;
1405 }
1406 waitForFeatureRegistration(libName, callback, 8);
1407 });
1408 }
1409
1410 dbx.resolveFeature = function (libName, callback) {
1411
1412 if (!libName) {
1413 callback && callback(false);
1414 return;
1415 }
1416
1417 if (dbx.feature.has(libName)) {
1418 dbx.log("resolveFeature → already loaded:", libName);
1419 return callback && callback(true);
1420 }
1421
1422 const exactUrl = dbx.config.libPath + libName + ".js";
1423
1424 dbx.log("resolveFeature → try exact:", exactUrl);
1425
1426 dbx.loader.js(exactUrl, function (res) {
1427
1428 // 🔥 NEW: loader status prüfen
1429 if (!res || res.ok !== true) {
1430 dbx.error("resolveFeature → load failed:", libName);
1431 return callback && callback(false);
1432 }
1433
1434 waitForFeatureRegistration(libName, function (registered) {
1435
1436 if (registered) {
1437 dbx.log("resolveFeature → loaded exact:", libName);
1438 return callback && callback(true);
1439 }
1440
1441 dbx.warn("resolveFeature → script loaded but feature NOT registered:", libName);
1442
1443 // 🔥 FALLBACK bleibt
1444 if (libName.includes("-")) {
1445
1446 const base = libName.split("-")[0];
1447
1448 if (dbx.feature.has(base)) {
1449 dbx.log("resolveFeature → base already loaded:", base);
1450 return callback && callback(true);
1451 }
1452
1453 const baseUrl = dbx.config.libPath + base + ".js";
1454
1455 dbx.log("resolveFeature → fallback base:", baseUrl);
1456
1457 dbx.loader.js(baseUrl, function (res2) {
1458
1459 // 🔥 loader fail
1460 if (!res2 || res2.ok !== true) {
1461 dbx.error("resolveFeature → fallback load failed:", base);
1462 return callback && callback(false);
1463 }
1464
1465 waitForFeatureRegistration(base, function (baseRegistered) {
1466
1467 if (baseRegistered) {
1468 dbx.log("resolveFeature → loaded base:", base);
1469 return callback && callback(true);
1470 }
1471
1472 dbx.error("Feature not registered after fallback:", libName);
1473 return callback && callback(false);
1474 });
1475 });
1476
1477 } else {
1478
1479 reloadFeatureScript(libName, function (reloaded) {
1480 if (reloaded) {
1481 dbx.log("resolveFeature → loaded after reload:", libName);
1482 return callback && callback(true);
1483 }
1484 dbx.error("Feature not registered:", libName);
1485 return callback && callback(false);
1486 });
1487 }
1488 });
1489 });
1490 };
1491
1492 /* =====================================================
1493 * SCAN
1494 * ===================================================== */
1495 dbx.scan = function (root) {
1496
1497 const ctx = root || document;
1498
1499 if (dbx.declare && typeof dbx.declare.scanAutoload === "function") {
1500 dbx.declare.scanAutoload(ctx);
1501 }
1502
1503 // 🔥 FIX: root selbst + children
1504 const nodes = [];
1505
1506 if (ctx.nodeType === 1 && ctx.hasAttribute && ctx.hasAttribute("data-dbx")) {
1507 nodes.push(ctx);
1508 }
1509
1510 ctx.querySelectorAll("[data-dbx]").forEach(n => nodes.push(n));
1511
1512 // 🔥 NEW: global scoped state store
1513 dbx._scopeState = dbx._scopeState || {};
1514 dbx._scopeError = dbx._scopeError || {};
1515 dbx._scopeInit = dbx._scopeInit || {};
1516
1517 nodes.forEach(el => {
1518
1519 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
1520 if (!cfgList.length) return;
1521
1522 el.__dbxInitialized = el.__dbxInitialized || {};
1523 el.__dbxState = el.__dbxState || {};
1524 el.__dbxError = el.__dbxError || {};
1525
1526 cfgList.forEach(cfg => {
1527
1528 if (!cfg.id) cfg.id = "undef";
1529
1530 const f = dbx.feature._features[cfg.lib];
1531
1532 if (!f) {
1533 dbx.log("scan → feature not yet loaded:", cfg.lib);
1534 }
1535
1536 let scopeType = "element";
1537
1538 if (f && f.scope) {
1539
1540 if (!["element","group","global"].includes(f.scope)) {
1541 dbx.error("Invalid scope:", cfg.lib, f.scope);
1542 return;
1543 }
1544
1545 scopeType = f.scope;
1546
1547 } else {
1548 dbx.log("scan → fallback scope=element:", cfg.lib);
1549 }
1550
1551 let scopeKey;
1552
1553 if (scopeType === "global") {
1554
1555 scopeKey = "global";
1556
1557 } else if (scopeType === "group") {
1558
1559 scopeKey = cfg.group || cfg.id || "group";
1560
1561 } else {
1562
1563 if (!el.__dbxScopeId) {
1564 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1565 el.__dbxScopeId = dbx._scopeUid;
1566 }
1567
1568 scopeKey = el.__dbxScopeId;
1569 }
1570
1571 const keyScoped = cfg.lib + "::" + scopeKey;
1572
1573 const stateStore = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
1574 const errorStore = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
1575 const initStore = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
1576
1577 const state = stateStore[keyScoped];
1578
1579 if (state === "pending") {
1580 // 🔥 FIX: pending darf nicht blockieren wenn kein task mehr existiert
1581 if (!dbx._taskMap || !dbx._taskMap[keyScoped]) {
1582 stateStore[keyScoped] = undefined;
1583 } else {
1584 return;
1585 }
1586 }
1587 if (state === "done") return;
1588
1589 let allowRetry = false;
1590
1591 if (state === "error") {
1592
1593 const err = errorStore[keyScoped] || {};
1594
1595 const count = err.count || 1;
1596 const last = err.last || 0;
1597
1598 const delay = Math.min(10000, count * 2000);
1599
1600 if (count >= 5) {
1601 dbx.warn("retry limit reached:", keyScoped);
1602 return;
1603 }
1604
1605 if (Date.now() - last < delay) {
1606 return;
1607 }
1608
1609 dbx.log("retry allowed:", keyScoped, "attempt:", count + 1);
1610 allowRetry = true;
1611 }
1612
1613 if (initStore[keyScoped] && stateStore[keyScoped] === "done") return;
1614
1615 const key = keyScoped;
1616
1617 dbx._taskMap = dbx._taskMap || {};
1618
1619 //if (!allowRetry && dbx._taskMap[key]) return;
1620
1621 stateStore[keyScoped] = "pending";
1622
1623 dbx._taskMap[key] = true;
1624
1625 dbx._tasks.push({
1626 el: el,
1627 lib: cfg.lib,
1628 cfg: cfg,
1629 _key: keyScoped,
1630 _scopeType: scopeType
1631 });
1632
1633 });
1634 });
1635
1636 dbx.runTasks();
1637 };
1638
1639 /* =====================================================
1640 * runTasks
1641 * ===================================================== */
1642 dbx.runTasks = function () {
1643
1644 if (!dbx._tasks.length) return;
1645
1646 if (dbx._running) {
1647 dbx.log("runTasks → already running, skip");
1648 return;
1649 }
1650
1651 dbx._running = true;
1652
1653 const tasks = dbx._tasks.slice();
1654 dbx._tasks = [];
1655
1656 dbx.log("runTasks → start", tasks.length);
1657
1658 const phases = {
1659 veryfirst: [],
1660 first: [],
1661 mid: [],
1662 last: [],
1663 verylast: []
1664 };
1665
1666 // -------------------------------------------------
1667 // 🔥 HELPER: richtiger Store je Scope
1668 // -------------------------------------------------
1669 function getStores(t) {
1670
1671 let scopeType = t._scopeType;
1672
1673 if (!scopeType) {
1674 const f = dbx.feature._features[t.lib];
1675 scopeType = (f && f.scope) ? f.scope : "element";
1676 t._scopeType = scopeType;
1677 }
1678
1679 if (scopeType === "element") {
1680
1681 if (!t.el || t.el.nodeType !== 1) {
1682 dbx._scopeState = dbx._scopeState || {};
1683 dbx._scopeError = dbx._scopeError || {};
1684 dbx._scopeInit = dbx._scopeInit || {};
1685 return {
1686 state: dbx._scopeState,
1687 error: dbx._scopeError,
1688 init: dbx._scopeInit
1689 };
1690 }
1691
1692 dbx.ensureElementScopeStores(t.el);
1693
1694 return {
1695 state: t.el.__dbxState,
1696 error: t.el.__dbxError,
1697 init: t.el.__dbxInitialized
1698 };
1699 }
1700
1701 dbx._scopeState = dbx._scopeState || {};
1702 dbx._scopeError = dbx._scopeError || {};
1703 dbx._scopeInit = dbx._scopeInit || {};
1704
1705 return {
1706 state: dbx._scopeState,
1707 error: dbx._scopeError,
1708 init: dbx._scopeInit
1709 };
1710 }
1711
1712 function requeueTask(t, store, key, reason) {
1713
1714 const err = (store.error && store.error[key]) || { count: 0 };
1715 const attempt = err.count + 1;
1716 const maxAttempts = 3;
1717
1718 if (store.error) {
1719 store.error[key] = { count: attempt, last: Date.now() };
1720 }
1721
1722 if (attempt < maxAttempts) {
1723 dbx.log("runTasks → requeue (" + attempt + "/" + maxAttempts + "):", t.lib, reason || "");
1724 if (store.state) delete store.state[key];
1725 if (dbx._taskMap) delete dbx._taskMap[key];
1726 dbx._tasks.push(t);
1727 return true;
1728 }
1729
1730 return false;
1731 }
1732
1733 function getScopeType(t) {
1734 const f = dbx.feature._features[t.lib];
1735 const scopeType = (f && f.scope) ? f.scope : "element";
1736
1737 if (!["element","group","global"].includes(scopeType)) {
1738 dbx.error("Invalid scope:", t.lib, scopeType);
1739 return "element";
1740 }
1741
1742 return scopeType;
1743 }
1744
1745 function makeTaskKey(t, scopeType) {
1746 if (scopeType === "global") {
1747 return t.lib + "::global";
1748 }
1749
1750 if (scopeType === "group") {
1751 return t.lib + "::" + (t.cfg.group || t.cfg.id || "group");
1752 }
1753
1754 if (!t.el.__dbxScopeId) {
1755 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1756 t.el.__dbxScopeId = dbx._scopeUid;
1757 }
1758
1759 return t.lib + "::" + t.el.__dbxScopeId;
1760 }
1761
1762 function storeForScope(t, scopeType) {
1763 if (scopeType === "element") {
1764 if (t.el && t.el.nodeType === 1) {
1765 dbx.ensureElementScopeStores(t.el);
1766 }
1767 return {
1768 state: t.el.__dbxState,
1769 error: t.el.__dbxError,
1770 init: t.el.__dbxInitialized
1771 };
1772 }
1773
1774 dbx._scopeState = dbx._scopeState || {};
1775 dbx._scopeError = dbx._scopeError || {};
1776 dbx._scopeInit = dbx._scopeInit || {};
1777
1778 return {
1779 state: dbx._scopeState,
1780 error: dbx._scopeError,
1781 init: dbx._scopeInit
1782 };
1783 }
1784
1785 function normalizeTaskScope(t) {
1786 const oldKey = t._key;
1787 const oldScopeType = t._scopeType || "element";
1788 const newScopeType = getScopeType(t);
1789 const newKey = makeTaskKey(t, newScopeType);
1790
1791 if (oldKey && oldKey !== newKey) {
1792 const oldStore = storeForScope(t, oldScopeType);
1793
1794 if (oldStore.state && oldStore.state[oldKey] === "pending") {
1795 delete oldStore.state[oldKey];
1796 }
1797
1798 if (oldStore.error && oldStore.error[oldKey]) {
1799 delete oldStore.error[oldKey];
1800 }
1801
1802 if (dbx._taskMap) {
1803 delete dbx._taskMap[oldKey];
1804 }
1805 }
1806
1807 t._scopeType = newScopeType;
1808 t._key = newKey;
1809
1810 return t;
1811 }
1812
1813 function assignPhases() {
1814 const seen = {};
1815
1816 tasks.forEach(t => {
1817
1818 normalizeTaskScope(t);
1819
1820 const store = getStores(t);
1821
1822 if (store.init[t._key] || store.state[t._key] === "done") {
1823 if (dbx._taskMap) delete dbx._taskMap[t._key];
1824 return;
1825 }
1826
1827 if (seen[t._key]) {
1828 return;
1829 }
1830
1831 seen[t._key] = true;
1832 store.state[t._key] = "pending";
1833 dbx._taskMap = dbx._taskMap || {};
1834 dbx._taskMap[t._key] = true;
1835
1836 const f = dbx.feature._features[t.lib];
1837 const libPrio = f && f.priority ? f.priority : "mid";
1838 const cfgPrio = t.cfg.prio;
1839 const prio = cfgPrio || libPrio || "mid";
1840
1841 if (!phases[prio]) phases.mid.push(t);
1842 else phases[prio].push(t);
1843 });
1844 }
1845
1846 // =================================================
1847 // VERYFIRST
1848 // =================================================
1849 function runVeryFirstImmediate(done) {
1850
1851 const list = phases.veryfirst;
1852 if (!list.length) return done();
1853
1854 let i = 0;
1855
1856 function next() {
1857
1858 if (i >= list.length) return done();
1859
1860 const t = list[i++];
1861 const key = t._key;
1862
1863 const store = getStores(t);
1864
1865 if (store.state[key] !== "pending") {
1866 store.state[key] = "pending";
1867 }
1868
1869 dbx.resolveFeature(t.lib, function (ok) {
1870
1871 if (ok !== true) {
1872 if (requeueTask(t, store, key, "resolveFeature failed")) {
1873 return next();
1874 }
1875 dbx.error("runTasks → lib load failed:", t.lib);
1876 store.state[key] = "error";
1877 return next();
1878 }
1879
1880 const f = dbx.feature._features[t.lib];
1881
1882 const assets = [];
1883
1884 if (f && Array.isArray(f.css)) {
1885 assets.push(...f.css);
1886 }
1887
1888 if (f && Array.isArray(f.js)) {
1889 f.js.forEach(entry => {
1890 if (Array.isArray(entry)) {
1891 assets.push(entry);
1892 } else {
1893 assets.push(['js', 'lib', entry]);
1894 }
1895 });
1896 }
1897
1898 if (assets.length) {
1899 dbx.load(assets, runInit);
1900 } else {
1901 runInit();
1902 }
1903
1904 function runInit() {
1905
1906 try {
1907
1908 const feature = dbx.feature._features[t.lib];
1909
1910 if (!feature || !feature.init) {
1911 if (requeueTask(t, store, key, "init missing")) {
1912 return next();
1913 }
1914 dbx.error("runTasks → no init for lib:", t.lib);
1915 store.state[key] = "error";
1916 return next();
1917 }
1918
1919 feature.init.call(feature, t.el, t.cfg);
1920
1921 store.init[key] = true;
1922 store.state[key] = "done";
1923
1924 delete store.error[key];
1925
1926 if (dbx._taskMap) delete dbx._taskMap[key];
1927
1928 } catch (e) {
1929
1930 if (requeueTask(t, store, key, "init error: " + e.message)) {
1931 return next();
1932 }
1933
1934 dbx.error("runTasks → INIT ERROR:", t.lib, e);
1935 store.state[key] = "error";
1936
1937 if (dbx._taskMap) delete dbx._taskMap[key];
1938 }
1939
1940 next();
1941 }
1942 });
1943 }
1944
1945 next();
1946 }
1947
1948 // -------------------------------------------------
1949 // PREPARE (unverändert)
1950 // -------------------------------------------------
1951 function runPrepare(done) {
1952
1953 let i = 0;
1954 const assetSet = new Set();
1955
1956 function next() {
1957
1958 if (i >= tasks.length) {
1959
1960 const list = Array.from(assetSet).map(s => JSON.parse(s));
1961
1962 if (!list.length) return done();
1963
1964 return dbx.load(list, done);
1965 }
1966
1967 const t = tasks[i++];
1968
1969 dbx.resolveFeature(t.lib, function (ok) {
1970
1971 if (ok !== true) return next();
1972
1973 const f = dbx.feature._features[t.lib];
1974
1975 if (f && Array.isArray(f.css)) {
1976 f.css.forEach(entry => assetSet.add(JSON.stringify(entry)));
1977 }
1978
1979 if (f && Array.isArray(f.js)) {
1980 f.js.forEach(entry => {
1981 if (Array.isArray(entry)) {
1982 assetSet.add(JSON.stringify(entry));
1983 } else {
1984 assetSet.add(JSON.stringify(['js', 'lib', entry]));
1985 }
1986 });
1987 }
1988
1989 next();
1990 });
1991 }
1992
1993 next();
1994 }
1995
1996 // -------------------------------------------------
1997 // RUNNER (FIXED)
1998 // -------------------------------------------------
1999 function runPhase(name, list, done) {
2000
2001 dbx.log("runPhase →", name, "tasks:", list.length);
2002
2003 let i = 0;
2004
2005 function next() {
2006
2007 if (i >= list.length) {
2008 return done && done();
2009 }
2010
2011 const t = list[i++];
2012
2013 dbx.log("runPhase → task:", name, t.lib, t._key);
2014
2015 if (name === "veryfirst") return next();
2016
2017 const key = t._key;
2018 const store = getStores(t);
2019
2020 if (store.state[key] !== "pending") {
2021 store.state[key] = "pending";
2022 }
2023
2024 new Promise((resolve) => {
2025
2026 function runInit() {
2027
2028 try {
2029
2030 const f = dbx.feature._features[t.lib];
2031
2032 if (!f || !f.init) {
2033
2034 if (requeueTask(t, store, key, "init missing")) {
2035 return resolve();
2036 }
2037
2038 dbx.error("runPhase → NO INIT:", t.lib);
2039 store.state[key] = "error";
2040
2041 if (dbx._taskMap) delete dbx._taskMap[key];
2042
2043 return resolve();
2044 }
2045
2046 dbx.log("runPhase → INIT:", t.lib);
2047
2048 // 🔥 FIX: korrektes this wiederherstellen (einzige Änderung)
2049 const res = f.init.call(f, t.el, t.cfg);
2050
2051 if (res && typeof res.then === "function") {
2052 res.finally(() => {
2053 store.init[key] = true;
2054 store.state[key] = "done";
2055 delete store.error[key];
2056
2057 if (dbx._taskMap) delete dbx._taskMap[key];
2058
2059 resolve();
2060 });
2061 } else {
2062 store.init[key] = true;
2063 store.state[key] = "done";
2064 delete store.error[key];
2065
2066 if (dbx._taskMap) delete dbx._taskMap[key];
2067
2068 resolve();
2069 }
2070
2071 } catch (e) {
2072
2073 if (requeueTask(t, store, key, "init error: " + e.message)) {
2074 return resolve();
2075 }
2076
2077 dbx.error("runPhase → INIT ERROR:", t.lib, e);
2078 store.state[key] = "error";
2079
2080 if (dbx._taskMap) delete dbx._taskMap[key];
2081
2082 resolve();
2083 }
2084 }
2085
2086 const libName = t.lib || t.cfg?.lib;
2087
2088 dbx.resolveFeature(libName, (ok) => {
2089
2090 if (ok !== true) {
2091 if (requeueTask(t, store, key, "resolveFeature failed")) {
2092 return resolve();
2093 }
2094 dbx.error("runPhase → lib load failed:", libName);
2095 store.state[key] = "error";
2096 if (dbx._taskMap) delete dbx._taskMap[key];
2097 return resolve();
2098 }
2099
2100 const f = dbx.feature._features[libName];
2101
2102 const assets = [];
2103
2104 if (f && Array.isArray(f.css)) {
2105 assets.push(...f.css);
2106 }
2107
2108 if (f && Array.isArray(f.js)) {
2109 f.js.forEach(entry => {
2110 if (Array.isArray(entry)) {
2111 assets.push(entry);
2112 } else {
2113 assets.push(['js', 'lib', entry]);
2114 }
2115 });
2116 }
2117
2118 if (assets.length) {
2119 dbx.load(assets, runInit);
2120 } else {
2121 runInit();
2122 }
2123
2124 });
2125
2126 }).then(next);
2127 }
2128
2129 next();
2130 }
2131
2132 // =================================================
2133 // EXECUTION
2134 // =================================================
2135 runPrepare(function () {
2136
2137 assignPhases();
2138
2139 runVeryFirstImmediate(function () {
2140
2141 runPhase("first", phases.first, function () {
2142
2143 runPhase("mid", phases.mid, function () {
2144
2145 runPhase("last", phases.last, function () {
2146
2147 runPhase("verylast", phases.verylast, function () {
2148
2149 dbx._taskMap = {};
2150 dbx._running = false;
2151
2152 if (dbx._tasks.length) {
2153 setTimeout(dbx.runTasks, 0);
2154 }
2155
2156 });
2157 });
2158 });
2159 });
2160 });
2161 });
2162 };
2163
2164
2165 /* =====================================================
2166 * 🔥 MUTATION OBSERVER (ADD ONLY)
2167 * ===================================================== */
2168 if (!dbx.__observerInit) {
2169
2170 dbx.log("observer → init");
2171
2172 const observer = new MutationObserver(function (mutations) {
2173
2174 mutations.forEach(m => {
2175
2176 // =================================================
2177 // 🔥 ADD (bestehend)
2178 // =================================================
2179 m.addedNodes.forEach(node => {
2180
2181 if (node.nodeType !== 1) return;
2182
2183 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2184 dbx.log("observer → node");
2185 dbx.scan(node);
2186 }
2187
2188 if (node.querySelectorAll) {
2189 const found = node.querySelectorAll("[data-dbx]");
2190 if (found.length) {
2191 dbx.log("observer → subtree:", found.length);
2192 dbx.scan(node);
2193 }
2194 }
2195 });
2196
2197 // =================================================
2198 // 🔥 NEW: REMOVE → CLEANUP + DESTROY
2199 // =================================================
2200 m.removedNodes.forEach(node => {
2201
2202 if (node.nodeType !== 1) return;
2203
2204 const list = [];
2205
2206 // selbst prüfen
2207 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2208 list.push(node);
2209 }
2210
2211 // subtree prüfen
2212 if (node.querySelectorAll) {
2213 node.querySelectorAll("[data-dbx]").forEach(n => list.push(n));
2214 }
2215
2216 if (!list.length) return;
2217
2218 list.forEach(el => {
2219
2220 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
2221 if (!cfgList.length) return;
2222
2223 cfgList.forEach(cfg => {
2224
2225 if (!cfg.id) cfg.id = "undef";
2226
2227 const f = dbx.feature._features[cfg.lib];
2228 if (!f || !f.scope) return;
2229
2230 const scopeType = f.scope;
2231
2232 let scopeKey;
2233
2234 if (scopeType === "global") {
2235 // global → NICHT löschen
2236 return;
2237 }
2238
2239 if (scopeType === "group") {
2240 scopeKey = cfg.group || cfg.id || "group";
2241 } else {
2242 scopeKey = el.__dbxScopeId;
2243 }
2244
2245 if (!scopeKey) return;
2246
2247 const keyScoped = cfg.lib + "::" + scopeKey;
2248
2249 const storeState = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
2250 const storeError = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
2251 const storeInit = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
2252
2253 // -------------------------------------------------
2254 // 🔥 DESTROY (optional)
2255 // -------------------------------------------------
2256 try {
2257 if (f.destroy && storeInit && storeInit[keyScoped]) {
2258 f.destroy(el, cfg);
2259 }
2260 } catch (e) {
2261 dbx.error("destroy failed:", cfg.lib, e);
2262 }
2263
2264 // -------------------------------------------------
2265 // 🔥 CLEANUP
2266 // -------------------------------------------------
2267 if (storeState && keyScoped in storeState) {
2268 delete storeState[keyScoped];
2269 }
2270
2271 if (storeError && keyScoped in storeError) {
2272 delete storeError[keyScoped];
2273 }
2274
2275 if (storeInit && keyScoped in storeInit) {
2276 delete storeInit[keyScoped];
2277 }
2278
2279 dbx.log("GC cleanup:", keyScoped);
2280 });
2281 });
2282 });
2283 });
2284 });
2285
2286 observer.observe(document.body, {
2287 childList: true,
2288 subtree: true
2289 });
2290
2291 dbx.__observerInit = true;
2292 }
2293
2294 /* =====================================================
2295 * 🔥 EVENT DELEGATION HELPER
2296 * ===================================================== */
2297 dbx.on = function (event, selector, handler) {
2298
2299 document.addEventListener(event, function (e) {
2300
2301 const el = e.target.closest(selector);
2302 if (!el) return;
2303
2304 handler.call(el, e, el);
2305
2306 }, true); // 🔥 WICHTIG: CAPTURE PHASE
2307
2308 dbx.log("delegation registered:", event, selector);
2309 };
2310
2311
2312 /* =====================================================
2313 * 🔥 EVENT SYSTEM (CUSTOM)
2314 * ===================================================== */
2315
2316 dbx.event = dbx.event || {
2317
2318 _events: {},
2319
2320 on(name, handler) {
2321 if (!name || typeof handler !== "function") return;
2322
2323 this._events[name] = this._events[name] || [];
2324 this._events[name].push(handler);
2325
2326 dbx.log("event:on →", name);
2327 },
2328
2329 emit(name, data) {
2330 if (!name) return;
2331
2332 const list = this._events[name];
2333 if (!list || !list.length) return;
2334
2335 dbx.log("event:emit →", name, data);
2336
2337 list.forEach(fn => {
2338 try {
2339 fn(data);
2340 } catch (e) {
2341 dbx.error("event handler failed:", name, e);
2342 }
2343 });
2344
2345 // 🔥 NEU: scoped event
2346 if (data && data.id) {
2347 const scoped = name + ":" + data.id;
2348 const list2 = this._events[scoped];
2349
2350 if (list2) {
2351 list2.forEach(fn => {
2352 try {
2353 fn(data);
2354 } catch (e) {
2355 dbx.error("event handler failed:", scoped, e);
2356 }
2357 });
2358 }
2359 }
2360 }
2361
2362 };
2363
2364 /* =====================================================
2365 * DEVICE (STATE-BASED ABSTRACTION)
2366 * ===================================================== */
2367 dbx.device = (function(){
2368
2369 const state = {
2370 visible: true,
2371 online: true,
2372 active: true,
2373 lastActivity: Date.now()
2374 };
2375
2376 const IDLE_MS = 30000; // 30s
2377
2378 /* =====================================================
2379 * INTERNAL UPDATES
2380 * ===================================================== */
2381
2382 function updateVisibility(){
2383 state.visible = !document.hidden;
2384 }
2385
2386 function updateOnline(){
2387 state.online = navigator.onLine !== false;
2388 }
2389
2390 function markActive(){
2391 state.lastActivity = Date.now();
2392 state.active = true;
2393 }
2394
2395 function checkIdle(){
2396 const now = Date.now();
2397 state.active = (now - state.lastActivity) < IDLE_MS;
2398 }
2399
2400 /* =====================================================
2401 * BIND EVENTS (BROWSER)
2402 * ===================================================== */
2403
2404 document.addEventListener('visibilitychange', updateVisibility, true);
2405 window.addEventListener('online', updateOnline, true);
2406 window.addEventListener('offline', updateOnline, true);
2407
2408 document.addEventListener('mousemove', markActive, true);
2409 document.addEventListener('keydown', markActive, true);
2410 document.addEventListener('click', markActive, true);
2411
2412 // idle checker (leichtgewichtig)
2413 setInterval(checkIdle, 5000);
2414
2415 // init
2416 updateVisibility();
2417 updateOnline();
2418
2419 /* =====================================================
2420 * PUBLIC API
2421 * ===================================================== */
2422
2423 return {
2424
2425 /* ===== READ ===== */
2426
2427 isVisible(){
2428 return state.visible === true;
2429 },
2430
2431 isOnline(){
2432 return state.online === true;
2433 },
2434
2435 isActive(){
2436 return state.active === true;
2437 },
2438
2439 getState(){
2440 return Object.assign({}, state);
2441 },
2442
2443 /* =====================================================
2444 * OPTIONAL: EXTERNAL OVERRIDE (FUTURE: APP / PWA)
2445 * ===================================================== */
2446
2447 _set(partial){
2448
2449 if(!partial || typeof partial !== 'object') return;
2450
2451 if(typeof partial.visible === 'boolean'){
2452 state.visible = partial.visible;
2453 }
2454
2455 if(typeof partial.online === 'boolean'){
2456 state.online = partial.online;
2457 }
2458
2459 if(typeof partial.active === 'boolean'){
2460 state.active = partial.active;
2461 }
2462
2463 if(typeof partial.lastActivity === 'number'){
2464 state.lastActivity = partial.lastActivity;
2465 }
2466
2467 dbx.log('[device] override', partial);
2468 }
2469
2470 };
2471
2472 })();
2473
2474 /* =====================================================
2475 * LOOP (SMART POLLING CORE)
2476 * ===================================================== */
2477 dbx.loop = (function(){
2478
2479 const tasks = {};
2480
2481 function clamp(v, min, max){
2482 if(min != null && v < min) return min;
2483 if(max != null && v > max) return max;
2484 return v;
2485 }
2486
2487 function resolveInterval(task){
2488
2489 const t = task.timing || {};
2490
2491 let interval;
2492
2493 if(task.paused){
2494 return null;
2495 }
2496
2497 // hint
2498 if(task.hint){
2499
2500 switch(task.hint){
2501
2502 case 'fast': interval = t.min || t.base; break;
2503 case 'slow': interval = Math.max((t.base||2000)*2, t.idle||3000); break;
2504 case 'idle': interval = t.idle || t.base; break;
2505 case 'boost': interval = (t.base||2000) / 2; break;
2506 default: interval = t.base || 2000;
2507 }
2508
2509 } else {
2510
2511 if(!dbx.device.isVisible()){
2512 interval = t.hidden || (t.base||2000)*3;
2513 }
2514 else if(!dbx.device.isActive()){
2515 interval = t.idle || (t.base||2000)*2;
2516 }
2517 else{
2518 interval = t.base || 2000;
2519 }
2520 }
2521
2522 return clamp(
2523 interval,
2524 t.min || 500,
2525 t.max || 30000
2526 );
2527 }
2528
2529 function schedule(task){
2530
2531 if(task.paused) return;
2532
2533 if(task.timer) {
2534 clearTimeout(task.timer); // 🔥 FIX
2535 task.timer = null; // 🔥 FIX
2536 }
2537
2538 const interval = resolveInterval(task);
2539 if(interval == null) return;
2540
2541 task.timer = setTimeout(() => run(task), interval);
2542 }
2543
2544 function run(task){
2545
2546 if(task.running) return;
2547
2548 task.running = true;
2549
2550 let res;
2551
2552 try{
2553 res = task.onRun({
2554 id: task.id,
2555 hint: task.hint
2556 });
2557 }
2558 catch(e){
2559 dbx.error('[loop] run error', task.id, e);
2560 finish();
2561 return;
2562 }
2563
2564 Promise.resolve(res)
2565 .catch(err => {
2566 dbx.error('[loop] async error', task.id, err);
2567 })
2568 .finally(() => finish());
2569
2570 function finish(){
2571
2572 task.running = false;
2573 task.lastRun = Date.now();
2574
2575 task.timer = null; // 🔥 FIX
2576
2577 if(task.hintUntil && Date.now() > task.hintUntil){
2578 task.hint = null;
2579 task.hintUntil = 0;
2580 }
2581
2582 schedule(task);
2583 }
2584 }
2585
2586 return {
2587
2588 add(cfg){
2589
2590 if(!cfg || !cfg.id || !cfg.onRun){
2591 dbx.error('[loop] invalid task', cfg);
2592 return;
2593 }
2594
2595 if(tasks[cfg.id]){
2596 dbx.warn('[loop] already exists', cfg.id);
2597 return;
2598 }
2599
2600 tasks[cfg.id] = {
2601 id: cfg.id,
2602 onRun: cfg.onRun,
2603 timing: cfg.timing || {},
2604 running: false,
2605 paused: false,
2606 hint: null,
2607 hintUntil: 0,
2608 timer: null,
2609 lastRun: 0
2610 };
2611
2612 schedule(tasks[cfg.id]);
2613
2614 dbx.log('[loop] add', cfg.id);
2615 },
2616
2617 hint(id, mode, opts){
2618
2619 const t = tasks[id];
2620 if(!t) return;
2621
2622 if(mode === 'pause'){
2623 t.paused = true;
2624 if(t.timer){
2625 clearTimeout(t.timer);
2626 t.timer = null;
2627 }
2628 return;
2629 }
2630
2631 if(mode === 'resume'){
2632 t.paused = false;
2633 if(t.timer) clearTimeout(t.timer);
2634 schedule(t);
2635 return;
2636 }
2637
2638 if(mode === 'none'){
2639 t.hint = null;
2640 t.hintUntil = 0;
2641 return;
2642 }
2643
2644 t.hint = mode;
2645
2646 if(opts && opts.duration){
2647 t.hintUntil = Date.now() + opts.duration;
2648 }
2649
2650 if(mode === 'boost'){
2651
2652 // 🔥 FIX: niemals während running starten
2653 if(t.running){
2654 return; // einfach nächsten Zyklus beschleunigen
2655 }
2656
2657 if(t.timer){
2658 clearTimeout(t.timer);
2659 t.timer = null;
2660 }
2661
2662 run(t);
2663 }
2664 },
2665
2666 debug(){
2667 const out = [];
2668
2669 Object.values(tasks).forEach(t => {
2670 out.push({
2671 id: t.id,
2672 running: t.running,
2673 hint: t.hint,
2674 lastRun: t.lastRun,
2675 timer: !!t.timer
2676 });
2677 });
2678
2679 return out;
2680 }
2681
2682 };
2683
2684 })();
2685
2686
2687
2688 /* =====================================================
2689 * DEVICE CAPABILITIES (BRIDGE)
2690 * ===================================================== */
2691
2692 dbx.device.camera = {
2693
2694 async open(opts){
2695
2696 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
2697 dbx.error('[device] camera not supported');
2698 throw new Error('camera_not_supported');
2699 }
2700
2701 const constraints = opts?.constraints || { video: true };
2702
2703 try {
2704
2705 const stream = await navigator.mediaDevices.getUserMedia(constraints);
2706
2707 return {
2708 stream,
2709 stop(){
2710 stream.getTracks().forEach(t => t.stop());
2711 }
2712 };
2713
2714 } catch (e) {
2715
2716 dbx.error('[device] camera error', e);
2717 throw e;
2718 }
2719 }
2720
2721 };
2722
2723
2724 dbx.device.file = {
2725
2726 async pick(opts){
2727
2728 return new Promise((resolve, reject) => {
2729
2730 const input = document.createElement('input');
2731 input.type = 'file';
2732
2733 if (opts?.accept) input.accept = opts.accept;
2734 if (opts?.multiple) input.multiple = true;
2735
2736 input.onchange = () => {
2737
2738 if(!input.files || !input.files.length){
2739 resolve(null); // 🔥 FIX
2740 } else {
2741 resolve(input.files);
2742 }
2743 };
2744
2745 input.onerror = reject;
2746
2747 input.click();
2748 });
2749 }
2750
2751 };
2752
2753
2754 dbx.device.clipboard = {
2755
2756 async write(text){
2757
2758 if (!navigator.clipboard) {
2759 dbx.error('[device] clipboard not supported');
2760 throw new Error('clipboard_not_supported');
2761 }
2762
2763 return navigator.clipboard.writeText(text);
2764 },
2765
2766 async read(){
2767
2768 if (!navigator.clipboard) {
2769 dbx.error('[device] clipboard not supported');
2770 throw new Error('clipboard_not_supported');
2771 }
2772
2773 return navigator.clipboard.readText();
2774 }
2775
2776 };
2777
2778
2779 dbx.device.share = {
2780
2781 async open(data){
2782
2783 if (!navigator.share) {
2784 dbx.error('[device] share not supported');
2785 throw new Error('share_not_supported');
2786 }
2787
2788 return navigator.share(data);
2789 }
2790
2791 };
2792
2793
2794 /* =====================================================
2795 * DOM READY
2796 * ===================================================== */
2797 $(function () {
2798 dbx.log("DOM ready → scan");
2799 dbx.scan(document);
2800 dbx.footerStatus.init();
2801 });
2802
2803 /* =====================================================
2804 * API
2805 * ===================================================== */
2806 dbx.rescan = function (root) {
2807 dbx.log("rescan");
2808 const ctx = root || document;
2809 dbx.scan(ctx);
2810
2811 Object.keys(dbx.feature._features || {}).forEach(function (name) {
2812 const f = dbx.feature._features[name];
2813 if (!f || typeof f.rescan !== "function") return;
2814
2815 try {
2816 f.rescan(ctx);
2817 } catch (e) {
2818 dbx.error("rescan → feature error:", name, e);
2819 }
2820 });
2821 };
2822
2823 dbx.loadFeature = function (name, cb) {
2824 dbx.resolveFeature(name, cb || function () {});
2825 };
2826
2827})(window, document, jQuery);