3 * Clientseitiger dbXapp-Kernel.
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
13 * dbx.feature.register("beispiel", {
14 * selector: ".dbxBeispiel",
16 * init: function (el, cfg) {
17 * el.textContent = cfg.label || "bereit";
24 * <div data-dbx="lib=beispiel|label=Hallo"></div>
28(function (window, document, $) {
32 /* =====================================================
34 * ===================================================== */
35 if (!window.dbx) window.dbx = {};
36 const dbx = window.dbx;
39 dbx.assetVersion = "";
42 const currentScript = document.currentScript;
43 if (currentScript && currentScript.src) {
44 dbx.assetVersion = new URL(currentScript.src, location.href).searchParams.get("v") || "";
49 /* =====================================================
50 * WINDOW HELPER (GLOBAL BRIDGE)
51 * ===================================================== */
54 getCurrentWindowEl() {
55 const script = document.currentScript;
57 return script.closest?.('.dbx-window') || null;
60 const el = document.activeElement;
61 return el?.closest?.('.dbx-window') || null;
64 getCurrentWindowId() {
65 const win = this.getCurrentWindowEl();
66 return win ? win.id : null;
70 if (target === 'all' || target === '*') {
71 if (dbx.openWin && dbx.openWin.closeAll) {
72 dbx.openWin.closeAll();
77 const id = this.getCurrentWindowId();
79 if (id && dbx.openWin && dbx.openWin.close) {
80 dbx.openWin.close(id);
85 if (target === 'all' || target === '*') {
87 if (!dbx.openWin || !dbx.openWin.getAll) return;
89 dbx.openWin.getAll().forEach(w => {
91 this._reloadSingle(w.id);
98 const id = this.getCurrentWindowId();
100 this._reloadSingle(id);
104 _reloadSingle(windowId) {
106 if (!dbx.openWin || !dbx.openWin.get) return;
108 const w = dbx.openWin.get(windowId);
109 if (!w || !w.url) return;
113 const sep = url.includes("?") ? "&" : "?";
114 url = url + sep + "_=" + Date.now();
116 if (dbx.openWin && dbx.openWin.open) {
117 dbx.openWin.open(w.cfg);
125 /* =====================================================
127 * ===================================================== */
130 dbx.log = (...a) => {
131 if (dbx.LOG) console.log("[dbx " + dbx._ts() + "]", ...a);
134 dbx.warn = function (...a) {
135 console.warn("[dbx " + dbx._ts() + "]", ...a);
136 if (a.length >= 3 && typeof a[0] === "string" && typeof a[1] === "string") {
137 dbx.diag("warn", a[0], a[1], String(a[2] || ""), a[3] || {});
141 dbx.isIgnorableBrowserError = function (msg) {
142 const text = String(msg || "");
145 text.includes("ResizeObserver loop completed with undelivered notifications") ||
146 text.includes("ResizeObserver loop limit exceeded")
150 dbx.error = (...a) => {
152 const msg = "[dbx " + dbx._ts() + "] " + a.join(" ");
154 // -------------------------------------------------
155 // 1. console (immer)
156 // -------------------------------------------------
159 // -------------------------------------------------
160 // 2. intern speichern
161 // -------------------------------------------------
162 dbx._errors = dbx._errors || [];
168 dbx._hasCriticalError = true;
170 if (dbx.diag && dbx.diag.queueReport) {
180 dbx._diagnostics.push(diagEntry);
181 dbx.diag.queueReport(diagEntry);
184 // -------------------------------------------------
185 // 3. DEV MODE → optional alert
186 // -------------------------------------------------
188 // ⚠️ kein Spam – nur erste Meldung als Alert
189 if (!dbx.__errorAlertShown) {
194 dbx.__errorAlertShown = true;
198 // -------------------------------------------------
199 // 4. LIVE MODE → visueller Indikator (minimal)
200 // -------------------------------------------------
203 if (!dbx.__errorIndicator) {
205 const el = document.createElement("div");
207 el.className = "dbx-error-indicator";
208 el.style.position = "fixed";
209 el.style.top = "10px";
210 el.style.right = "10px";
211 el.style.width = "12px";
212 el.style.height = "12px";
213 el.style.background = "#dc3545";
214 el.style.borderRadius = "50%";
215 el.style.zIndex = "99999";
216 el.style.boxShadow = "0 0 6px rgba(0,0,0,0.3)";
217 el.title = "DBX Fehler aufgetreten";
219 document.body.appendChild(el);
221 dbx.__errorIndicator = el;
226 dbx._ts = function () {
227 const d = new Date();
228 const t = d.toISOString().split("T")[1];
229 return t.replace("Z", "");
233 /* =====================================================
234 * DIAGNOSTICS + DECLARE (Defaults, Aliase, Autoload)
235 * ===================================================== */
236 dbx._diagnostics = dbx._diagnostics || [];
238 dbx.diag = function (level, lib, code, message, ctx) {
242 level: String(level || "info"),
243 lib: String(lib || "core"),
244 code: String(code || "DIAG"),
245 message: String(message || ""),
247 element: dbx.diag.elementHint(ctx && ctx.el)
250 dbx._diagnostics.push(entry);
252 if (entry.level === "info" && !dbx.LOG) {
256 const line = dbx.diag.format(entry);
258 if (entry.level === "error") {
260 } else if (entry.level === "warn") {
262 } else if (dbx.LOG) {
266 if (entry.level === "warn" || entry.level === "error") {
267 dbx.diag.queueReport(entry);
273 dbx.diag.elementHint = function (el) {
275 if (!el || el.nodeType !== 1) return "";
280 parts.push("#" + el.id);
281 } else if (el.classList && el.classList.length) {
282 parts.push("." + Array.from(el.classList).slice(0, 3).join("."));
284 parts.push(el.tagName.toLowerCase());
287 if (el.getAttribute && el.getAttribute("data-dbx")) {
288 const raw = el.getAttribute("data-dbx");
289 const m = String(raw).match(/lib=([^|]+)/);
290 if (m) parts.push("lib=" + m[1]);
293 return parts.join("");
296 dbx.diag.format = function (entry) {
298 const el = entry.element ? " @ " + entry.element : "";
299 const field = entry.ctx && entry.ctx.field ? " field=" + entry.ctx.field : "";
300 const src = entry.ctx && entry.ctx.source ? " source=" + entry.ctx.source : "";
301 const val = entry.ctx && entry.ctx.value !== undefined ? " value=" + JSON.stringify(entry.ctx.value) : "";
303 return "[dbx][" + entry.lib + "][" + entry.code + "] " + entry.message + el + field + src + val;
306 dbx.diag.queueReport = function (entry) {
308 dbx.diag._reportQueue = dbx.diag._reportQueue || [];
309 dbx.diag._reportQueue.push(entry);
311 if (dbx.diag._reportTimer) return;
313 dbx.diag._reportTimer = setTimeout(function () {
314 dbx.diag._reportTimer = null;
315 dbx.diag.flushReports();
319 dbx.diag.reportUrl = function () {
320 const root = (dbx.config && dbx.config.rootPath) ? dbx.config.rootPath : "/";
321 return root + "?dbx_modul=dbxAdmin&dbx_run1=sysmsg&dbx_run2=client_diag&dbx_ajax=1";
324 dbx.diag.flushReports = function () {
326 const queue = (dbx.diag._reportQueue || []).slice();
327 dbx.diag._reportQueue = [];
329 if (!queue.length) return;
332 entries: queue.map(function (e) {
344 if (!dbx.ajax || typeof dbx.ajax.request !== "function") return;
347 url: dbx.diag.reportUrl(),
350 headers: { "Content-Type": "application/json" },
351 body: JSON.stringify(payload),
354 }).catch(function (err) {
355 dbx.log("diag report failed:", err);
359 dbx.declare = dbx.declare || {};
361 dbx.declare.schemas = dbx.declare.schemas || {};
362 dbx.declare.transforms = dbx.declare.transforms || {};
363 dbx.declare.infer = dbx.declare.infer || {};
365 dbx.declare.registerSchema = function (lib, schema) {
366 if (!lib || !schema) return;
367 dbx.declare.schemas[lib] = schema;
370 dbx.declare.readAttr = function (el, name) {
371 if (!el || !el.getAttribute) return null;
372 const v = el.getAttribute(name);
373 if (v === null) return null;
377 dbx.declare.readField = function (el, lib, field, spec, ctx) {
379 const aliases = [spec.attr].concat(spec.aliases || []).filter(Boolean);
380 let source = "missing";
383 if (ctx && ctx.base && ctx.base[field] !== undefined && String(ctx.base[field]).trim() !== "") {
385 value: String(ctx.base[field]).trim(),
391 for (let i = 0; i < aliases.length; i++) {
392 const raw = dbx.declare.readAttr(el, aliases[i]);
393 if (raw !== null && String(raw).trim() !== "") {
395 value = String(raw).trim();
400 if (value === undefined && typeof spec.infer === "function") {
401 const inferred = spec.infer(el, ctx || {});
402 if (inferred !== undefined && inferred !== null && String(inferred).trim() !== "") {
404 value = String(inferred).trim();
408 if (value === undefined) {
409 if (spec.default !== undefined) {
410 value = spec.default;
412 dbx.diag("info", lib, field.toUpperCase() + "_DEFAULT",
413 "Attribut nicht gesetzt, Standard verwendet",
414 { el: el, field: field, source: source, value: value });
415 } else if (spec.required) {
416 dbx.diag("warn", lib, field.toUpperCase() + "_MISSING",
417 "Pflicht-Attribut fehlt",
418 { el: el, field: field, source: source });
425 if (typeof spec.coerce === "function") {
426 value = spec.coerce(value, el, ctx || {});
429 return { value: value, source: source, field: field };
432 dbx.declare.infer.ajaxTarget = function (root) {
434 if (!root || root.nodeType !== 1) return "";
436 const id = String(root.id || "").trim();
437 if (/^dbx_target_\d+$/i.test(id)) return id;
439 const form = root.tagName === "FORM"
441 : root.querySelector("form.dbxAjax, form[id^='dbx_form_']");
443 if (form && form.id) {
444 const m = form.id.match(/dbx_form_(\d+)/i);
445 if (m) return "dbx_target_" + m[1];
448 const target = root.querySelector("[id^='dbx_target_']");
449 if (target && target.id) return target.id;
454 dbx.declare.infer.confirmRoot = function (el) {
456 if (!el || el.nodeType !== 1) return null;
458 const form = el.closest("form.dbxAjax");
459 if (form) return form;
461 const panel = el.closest(".dbx-confirm-root, [data-dbx*='lib=confirm']");
462 if (panel) return panel;
464 return el.parentElement || document.body;
467 dbx.declare.buildFromSchema = function (lib, root, baseCfg, index) {
469 const schema = dbx.declare.schemas[lib];
470 if (!schema || !schema.fields) return null;
472 const ctx = { root: root, index: index, base: baseCfg || {} };
473 const raw = { _index: index, lib: lib };
475 Object.keys(schema.fields).forEach(function (field) {
476 const spec = schema.fields[field];
477 const res = dbx.declare.readField(root, lib, field, spec, ctx);
478 raw[field] = res.value;
479 raw["_" + field + "Source"] = res.source;
482 if (typeof dbx.declare.transforms[lib] === "function") {
483 return dbx.declare.transforms[lib](raw, root);
489 dbx.declare.resolve = function (lib, root) {
491 if (!root) return [];
493 const cacheKey = "_dbx" + lib.charAt(0).toUpperCase() + lib.slice(1) + "Configs";
495 if (Array.isArray(root[cacheKey])) {
496 return root[cacheKey];
499 const attr = dbx.declare.readAttr(root, "data-dbx") || "";
500 const parsed = dbx.parseData(attr).filter(function (cfg) {
501 return cfg.lib === lib;
507 out = parsed.map(function (cfg, index) {
508 return dbx.declare.buildFromSchema(lib, root, cfg, index) || cfg;
510 } else if (dbx.declare.schemas[lib]) {
511 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
514 if (!out.length && dbx.declare.schemas[lib]) {
515 out = [dbx.declare.buildFromSchema(lib, root, {}, 0)];
518 root[cacheKey] = out;
523 dbx.declare.hasLibInDataDbx = function (el, lib) {
524 const attr = dbx.declare.readAttr(el, "data-dbx") || "";
525 return dbx.parseData(attr).some(function (cfg) {
526 return cfg.lib === lib;
530 dbx.declare.markAutoload = function (el, lib) {
531 el.__dbxAutoload = el.__dbxAutoload || {};
532 if (el.__dbxAutoload[lib]) return false;
533 el.__dbxAutoload[lib] = true;
537 dbx.ensureElementScopeStores = function (el) {
539 if (!el || el.nodeType !== 1) return;
541 el.__dbxInitialized = el.__dbxInitialized || {};
542 el.__dbxState = el.__dbxState || {};
543 el.__dbxError = el.__dbxError || {};
546 dbx.declare.queueFeature = function (lib, el, scopeType, cfg) {
548 if (!dbx.declare.markAutoload(el, lib)) return;
550 dbx.ensureElementScopeStores(el);
552 const f = dbx.feature._features[lib];
553 let scope = scopeType || "element";
559 const id = (cfg && cfg.id) ? cfg.id : (el.id || "undef");
560 const keyScoped = scope + ":" + lib + ":" + id;
565 cfg: Object.assign({ lib: lib, id: id }, cfg || {}),
571 dbx.declare.scanAutoload = function (ctx) {
573 const root = ctx || document;
575 root.querySelectorAll("form.dbxAjax").forEach(function (form) {
576 if (!dbx.declare.hasLibInDataDbx(form, "ajax")) {
577 dbx.declare.queueFeature("ajax", form, "element", { id: form.id || "undef" });
579 if (!dbx.declare.hasLibInDataDbx(form, "form")) {
580 const wrap = form.closest(".dbxForm_wrapper") || form;
581 dbx.declare.queueFeature("form", wrap, "element", { id: wrap.id || form.id || "undef" });
583 if (form.querySelector(".dbxConfirm, [data-confirm]") && !dbx.declare.hasLibInDataDbx(form, "confirm")) {
584 dbx.declare.queueFeature("confirm", form, "element", { id: form.id || "undef" });
588 root.querySelectorAll(".dbx-ajax-root").forEach(function (el) {
589 if (!dbx.declare.hasLibInDataDbx(el, "ajax")) {
590 dbx.declare.queueFeature("ajax", el, "element", { id: el.id || "undef" });
594 root.querySelectorAll(".dbxConfirm:not(form), [data-confirm]:not(form)").forEach(function (el) {
595 const confirmRoot = dbx.declare.infer.confirmRoot(el);
596 if (confirmRoot && !dbx.declare.hasLibInDataDbx(confirmRoot, "confirm")) {
597 dbx.declare.queueFeature("confirm", confirmRoot, "element", { id: confirmRoot.id || "undef" });
601 root.querySelectorAll(".dbx-win, .dbx-win-preload").forEach(function (el) {
602 if (!dbx.declare.hasLibInDataDbx(el, "openWin")) {
603 dbx.declare.queueFeature("openWin", el, "global", { id: el.id || "undef" });
607 dbx.log("declare.scanAutoload → tasks:", dbx._tasks.length);
611 /* =====================================================
612 * GLOBAL ERROR SHIELD (FAIL-SAFE)
613 * ===================================================== */
615 if (!dbx.__errorShield) {
617 window.onerror = function (msg, src, line, col, err) {
619 if (dbx.isIgnorableBrowserError(msg)) {
620 dbx.log("IGNORED BROWSER ERROR:", msg);
627 "at", src + ":" + line + ":" + col
630 // DEV → Fehler NICHT unterdrücken (Browser zeigt Stacktrace)
631 if (dbx.LOG) return false;
633 // LIVE → Fehler unterdrücken (silent)
637 window.onunhandledrejection = function (e) {
639 const reason = e && e.reason ? e.reason : "";
640 const msg = reason && reason.message ? reason.message : reason;
641 if (dbx.isIgnorableBrowserError(msg)) {
642 dbx.log("IGNORED PROMISE ERROR:", msg);
652 dbx.__errorShield = true;
658 /* =====================================================
660 * ===================================================== */
661 dbx.config = dbx.config || (function () {
663 let scriptPath = "/";
664 let design = "default";
668 const scripts = document.getElementsByTagName("script");
670 for (let s of scripts) {
672 if (!s.src || !s.src.includes("core.js")) continue;
674 const url = new URL(s.src, window.location.origin);
676 scriptPath = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
678 const d = url.searchParams.get("design");
681 const l = url.searchParams.get("log");
682 if (l && l.toLowerCase() === "on") log = true;
684 if (scriptPath.includes("/js/lib/")) {
685 rootPath = scriptPath.split("/js/lib/")[0] + "/";
700 dbx.LOG = dbx.config.log;
701 dbx.log("core init → config:", dbx.config);
703 /* =====================================================
705 * ===================================================== */
706 dbx.uiSet = function (lib, id, key, val) {
707 if (!lib || !id || !key || id === "undef") return;
708 const k = `dbx.UI.${lib}.${id}.${key}`;
710 localStorage.setItem(k, JSON.stringify(val));
711 dbx.log("uiSet:", k, val);
713 dbx.warn("uiSet failed:", k, e);
717 dbx.uiGet = function (lib, id, key, def) {
718 if (!lib || !id || !key || id === "undef") return def;
719 const k = `dbx.UI.${lib}.${id}.${key}`;
721 const v = localStorage.getItem(k);
722 if (v === null) return def;
723 const parsed = JSON.parse(v);
724 dbx.log("uiGet:", k, parsed);
727 dbx.warn("uiGet failed:", k, e);
732 dbx.getDesign = () => dbx.config.design;
733 dbx.getLibId = cfg => (cfg && cfg.id) ? cfg.id : "undef";
735 /* =====================================================
737 * ===================================================== */
738 dbx.footerStatus = dbx.footerStatus || (function () {
740 function formatSeconds(seconds) {
741 seconds = Math.max(0, Number(seconds) || 0);
742 return seconds.toFixed(3);
745 function runtimeElement() {
746 return document.querySelector("[data-dbx-runtime]");
749 function responseRuntimeSeconds() {
750 const perf = window.performance;
751 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
752 if (nav && nav.duration) {
753 return nav.duration / 1000;
756 return perf && perf.now ? perf.now() / 1000 : 0;
759 function navigationPhpRuntimeSeconds() {
760 const perf = window.performance;
761 const nav = perf && perf.getEntriesByType && perf.getEntriesByType("navigation")[0];
762 const entries = nav && nav.serverTiming ? Array.from(nav.serverTiming) : [];
763 const timer = entries.find(item => item && item.name === "dbxphp");
764 const duration = timer ? Number(timer.duration) : NaN;
766 return Number.isFinite(duration) && duration >= 0 ? duration / 1000 : null;
769 function phpRuntimeSeconds(el) {
770 const headerRuntime = navigationPhpRuntimeSeconds();
771 if (headerRuntime !== null) {
772 return headerRuntime;
775 const raw = el ? el.getAttribute("data-dbx-php-runtime") : "";
776 const value = Number(raw);
778 return Number.isFinite(value) && value >= 0 ? value : null;
781 function write(responseSeconds, phpSeconds, label) {
782 const el = runtimeElement();
785 const responseLabel = formatSeconds(responseSeconds);
786 const hasPhp = phpSeconds != null && phpSeconds !== ""
787 && Number.isFinite(Number(phpSeconds)) && Number(phpSeconds) >= 0;
788 const phpValue = hasPhp ? Number(phpSeconds) : null;
789 const phpLabel = hasPhp ? "/" + formatSeconds(phpValue) : "";
790 const text = responseLabel + phpLabel + " sec";
792 el.textContent = text;
795 el.setAttribute("data-dbx-php-runtime", formatSeconds(phpValue));
799 ? label + ": " + responseLabel + " / " + formatSeconds(phpValue) + " sec"
800 : label + ": " + responseLabel + " sec";
802 el.setAttribute("title", title);
803 el.setAttribute("data-dbx-page-runtime-title", title);
807 const el = runtimeElement();
810 const update = function () {
812 responseRuntimeSeconds(),
813 phpRuntimeSeconds(el),
814 "Komplette Response-Laufzeit / PHP-Laufzeit"
818 if (document.readyState === "complete") {
821 window.addEventListener("load", update, { once: true });
822 setTimeout(update, 0);
826 function requestHasAjaxFlag(url, body) {
827 const targetUrl = String(url || "");
828 if (/[?&]dbx_ajax=1(?:&|$)/.test(targetUrl)) {
832 if (body instanceof URLSearchParams && body.get("dbx_ajax") === "1") {
836 if (body instanceof FormData && body.get("dbx_ajax") === "1") {
840 if (typeof body === "string" && /(?:^|&)dbx_ajax=1(?:&|$)/.test(body)) {
847 function shouldTrackAjaxRuntime(url, body, options) {
849 options.skipRuntime === true ||
850 options.footerRuntime === "hidden" ||
851 options.footerRuntime === "skip" ||
852 options.footerRuntime === "0"
857 if (options && String(options.mode || "").toLowerCase() === "json") {
861 const targetUrl = String(url || "");
862 if (/[?&]dbx_sync=0(?:&|$)/.test(targetUrl)) {
866 if (body instanceof URLSearchParams && body.get("dbx_sync") === "0") {
870 if (body instanceof FormData && body.get("dbx_sync") === "0") {
874 if (typeof body === "string" && /(?:^|&)dbx_sync=0(?:&|$)/.test(body)) {
884 shouldTrackAjaxRuntime,
885 updateAjax(responseSeconds, phpSeconds) {
886 write(responseSeconds, phpSeconds, "Letzter AJAX-Request / PHP-Laufzeit");
892 /* =====================================================
894 * ===================================================== */
901 const searchParams = new URLSearchParams(location.search);
902 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust") || dbx.assetVersion;
904 if (!cacheBust) return url;
907 const parsed = new URL(url, location.origin);
908 if (parsed.searchParams.has("v")) return url;
911 return url + (url.includes("?") ? "&" : "?") + "v=" + encodeURIComponent(cacheBust);
917 cb && cb({ ok: false });
921 url = this._cacheBustUrl(url);
923 if (this._loaded[url] === "loaded") {
924 dbx.log("CSS already loaded:", url);
925 return cb && cb({ ok: true });
928 if (this._loaded[url] === "loading") {
929 dbx.log("CSS already loading:", url);
930 (this._callbacks[url] ||= []).push(cb);
934 // 🔥 FIX: sauberes URL Matching
935 const requested = new URL(url, location.origin);
936 const target = requested.pathname;
937 const targetSearch = requested.search;
939 const links = document.querySelectorAll('link[rel="stylesheet"]');
940 for (let l of links) {
942 const current = new URL(l.href);
943 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
944 this._loaded[url] = "loaded";
945 dbx.log("CSS already in DOM:", url);
946 return cb && cb({ ok: true });
951 dbx.log("CSS load start:", url);
953 this._loaded[url] = "loading";
954 this._callbacks[url] = cb ? [cb] : [];
956 const link = document.createElement("link");
957 link.rel = "stylesheet";
960 let finished = false;
962 function done(self, status) {
963 if (finished) return;
966 self._callbacks[url].forEach(f => f && f(status));
967 self._callbacks[url] = [];
970 link.onload = () => {
971 this._loaded[url] = "loaded";
972 dbx.log("CSS loaded:", url);
973 done(this, { ok: true });
976 link.onerror = () => {
977 this._loaded[url] = "error";
978 dbx.error("CSS load failed:", url);
979 done(this, { ok: false });
982 document.head.appendChild(link);
988 cb && cb({ ok: false });
992 url = this._cacheBustUrl(url);
994 if (this._loaded[url] === "loaded") {
995 dbx.log("JS already loaded:", url);
996 return cb && cb({ ok: true });
999 if (this._loaded[url] === "loading") {
1000 dbx.log("JS already loading:", url);
1001 (this._callbacks[url] ||= []).push(cb);
1005 // 🔥 FIX: sauberes URL Matching
1006 const requested = new URL(url, location.origin);
1007 const target = requested.pathname;
1008 const targetSearch = requested.search;
1010 const scripts = document.querySelectorAll('script[src]');
1011 for (let s of scripts) {
1013 const current = new URL(s.src);
1014 if (current.pathname === target && (!targetSearch || current.search === targetSearch)) {
1015 this._loaded[url] = "loaded";
1016 dbx.log("JS already in DOM:", url);
1017 return cb && cb({ ok: true });
1022 dbx.log("JS load start:", url);
1024 this._loaded[url] = "loading";
1025 this._callbacks[url] = cb ? [cb] : [];
1027 const s = document.createElement("script");
1031 let finished = false;
1033 function done(self, status) {
1034 if (finished) return;
1037 self._callbacks[url].forEach(f => f && f(status));
1038 self._callbacks[url] = [];
1042 this._loaded[url] = "loaded";
1043 dbx.log("JS loaded:", url);
1044 done(this, { ok: true });
1048 this._loaded[url] = "error";
1049 dbx.error("JS load failed:", url);
1050 done(this, { ok: false });
1053 document.body.appendChild(s);
1058 dbx.add_css = function (type, file, cb) {
1060 if (!type || !file) {
1061 cb && cb({ ok: false }); // 🔥 FIX
1067 if (type === "lib") {
1068 url = dbx.config.libPath + file;
1071 if (type === "design") {
1072 url = dbx.config.rootPath + "design/" + dbx.config.design + "/css/" + file;
1075 if (type === "root") {
1076 url = dbx.config.rootPath + file;
1079 const searchParams = new URLSearchParams(location.search);
1080 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1082 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1086 dbx.warn("add_css invalid type:", type);
1090 "Invalid CSS scope\n\n" +
1091 "type=" + type + "\n" +
1095 cb && cb({ ok: false }); // 🔥 FIX
1099 dbx.log("add_css:", type, "→", url);
1100 dbx.loader.css(url, cb);
1103 dbx.add_js = function (type, file, cb) {
1105 if (!type || !file) {
1106 cb && cb({ ok: false }); // 🔥 FIX
1112 if (type === "lib") {
1113 url = dbx.config.libPath + file;
1116 if (type === "design") {
1117 url = dbx.config.rootPath + "design/" + dbx.config.design + "/js/" + file;
1120 if (type === "root") {
1121 url = dbx.config.rootPath + file;
1124 const searchParams = new URLSearchParams(location.search);
1125 const cacheBust = searchParams.get("dbx_nocache") || searchParams.get("cachebust");
1127 url += (url.includes("?") ? "&" : "?") + "dbx_nocache=" + encodeURIComponent(cacheBust);
1131 dbx.warn("add_js invalid type:", type);
1135 "Invalid JS scope\n\n" +
1136 "type=" + type + "\n" +
1140 cb && cb({ ok: false }); // 🔥 FIX
1144 dbx.log("add_js:", type, "→", url);
1145 dbx.loader.js(url, cb);
1148 /* =====================================================
1150 * ===================================================== */
1151 dbx.load = function (list, done) {
1157 if (i >= list.length) {
1158 dbx.log("dbx.load → complete");
1159 return done && done();
1162 const [type, scope, file] = list[i++];
1164 dbx.log("dbx.load → step:", type, scope, file);
1166 let finished = false;
1168 function safeNext(res) {
1170 if (finished) return;
1173 if (!res || res.ok !== true) {
1174 dbx.warn("dbx.load → asset failed:", type, file);
1180 // 🔥 FAILSAFE TIMEOUT (z.B. 5s)
1181 const timer = setTimeout(() => {
1183 dbx.error("dbx.load timeout:", type, file);
1185 safeNext({ ok: false });
1189 function wrappedNext(res) {
1190 clearTimeout(timer);
1194 if (type === "js") {
1195 dbx.add_js(scope, file, wrappedNext);
1196 } else if (type === "css") {
1197 dbx.add_css(scope, file, wrappedNext);
1199 wrappedNext({ ok: true });
1206 /* =====================================================
1208 * ===================================================== */
1213 register(name, obj) {
1214 this._features[name] = obj;
1215 dbx.log("feature registered:", name);
1219 return Object.prototype.hasOwnProperty.call(this._features, name);
1222 init(name, el, cfg) {
1224 dbx.log("init feature:", name, "id:", cfg.id);
1226 if (this.has(name)) {
1228 this._features[name].init(el, cfg);
1230 dbx.error("Feature init failed:", name, e);
1235 dbx.error("Feature not registered:", name);
1239 dbx.loadUtilities = function () {
1240 const existing = document.querySelector('script[src*="/utilities.js"],script[src$="utilities.js"]');
1245 dbx.add_js("lib", "utilities.js");
1248 if (document.body) {
1249 dbx.loadUtilities();
1251 document.addEventListener("DOMContentLoaded", dbx.loadUtilities, { once: true });
1254 /* =====================================================
1256 * ===================================================== */
1257 dbx.parseData = function (str) {
1260 if (!str) return result;
1262 const blocks = str.split("||");
1264 function normalize(val) {
1266 if (val === undefined || val === null) return val;
1268 const raw = val.toString().trim();
1269 const v = raw.toLowerCase();
1271 if (v === "on" || v === "1") return 1;
1272 if (v === "off" || v === "0") return 0;
1273 if (v === "true" || v === "1") return 1;
1274 if (v === "false" || v === "0") return 0;
1276 // 🔥 JSON AUTO PARSE
1278 (raw.startsWith("{") && raw.endsWith("}")) ||
1279 (raw.startsWith("[") && raw.endsWith("]"))
1282 return JSON.parse(raw);
1284 dbx.warn("parseData JSON failed:", raw);
1291 blocks.forEach(block => {
1296 block.split("|").forEach(part => {
1300 const idx = part.indexOf("=");
1301 if (idx === -1) return;
1303 // 🔥 FIX 1: key lowercase
1304 const key = part.substring(0, idx).trim().toLowerCase();
1306 // 🔥 FIX 2: kein trim hier (macht normalize)
1307 let val = part.substring(idx + 1);
1309 val = normalize(val);
1314 if (cfg.lib) result.push(cfg);
1320 /* =====================================================
1321 * RESOLVER (EXACT + FALLBACK BEHALTEN!)
1322 * ===================================================== */
1323 function waitForFeatureRegistration(libName, callback, attempts) {
1325 if (dbx.feature.has(libName)) {
1330 attempts = attempts === undefined ? 8 : attempts;
1332 if (attempts <= 0) {
1337 window.setTimeout(function () {
1338 waitForFeatureRegistration(libName, callback, attempts - 1);
1342 function reloadFeatureScript(libName, callback) {
1343 const url = dbx.config.libPath + libName + ".js";
1344 const target = new URL(url, location.origin).pathname;
1346 document.querySelectorAll("script[src]").forEach(script => {
1348 if (new URL(script.src).pathname === target) {
1349 script.parentNode && script.parentNode.removeChild(script);
1354 const reloadUrl = url + (url.includes("?") ? "&" : "?") + "dbx_reload=" + Date.now();
1355 dbx.loader._loaded[url] = null;
1356 dbx.loader._loaded[reloadUrl] = null;
1358 dbx.log("resolveFeature → reload script:", libName);
1359 dbx.loader.js(reloadUrl, function (res) {
1360 if (!res || res.ok !== true) {
1364 waitForFeatureRegistration(libName, callback, 8);
1368 dbx.resolveFeature = function (libName, callback) {
1371 callback && callback(false);
1375 if (dbx.feature.has(libName)) {
1376 dbx.log("resolveFeature → already loaded:", libName);
1377 return callback && callback(true);
1380 const exactUrl = dbx.config.libPath + libName + ".js";
1382 dbx.log("resolveFeature → try exact:", exactUrl);
1384 dbx.loader.js(exactUrl, function (res) {
1386 // 🔥 NEW: loader status prüfen
1387 if (!res || res.ok !== true) {
1388 dbx.error("resolveFeature → load failed:", libName);
1389 return callback && callback(false);
1392 waitForFeatureRegistration(libName, function (registered) {
1395 dbx.log("resolveFeature → loaded exact:", libName);
1396 return callback && callback(true);
1399 dbx.warn("resolveFeature → script loaded but feature NOT registered:", libName);
1401 // 🔥 FALLBACK bleibt
1402 if (libName.includes("-")) {
1404 const base = libName.split("-")[0];
1406 if (dbx.feature.has(base)) {
1407 dbx.log("resolveFeature → base already loaded:", base);
1408 return callback && callback(true);
1411 const baseUrl = dbx.config.libPath + base + ".js";
1413 dbx.log("resolveFeature → fallback base:", baseUrl);
1415 dbx.loader.js(baseUrl, function (res2) {
1418 if (!res2 || res2.ok !== true) {
1419 dbx.error("resolveFeature → fallback load failed:", base);
1420 return callback && callback(false);
1423 waitForFeatureRegistration(base, function (baseRegistered) {
1425 if (baseRegistered) {
1426 dbx.log("resolveFeature → loaded base:", base);
1427 return callback && callback(true);
1430 dbx.error("Feature not registered after fallback:", libName);
1431 return callback && callback(false);
1437 reloadFeatureScript(libName, function (reloaded) {
1439 dbx.log("resolveFeature → loaded after reload:", libName);
1440 return callback && callback(true);
1442 dbx.error("Feature not registered:", libName);
1443 return callback && callback(false);
1450 /* =====================================================
1452 * ===================================================== */
1453 dbx.scan = function (root) {
1455 const ctx = root || document;
1457 if (dbx.declare && typeof dbx.declare.scanAutoload === "function") {
1458 dbx.declare.scanAutoload(ctx);
1461 // 🔥 FIX: root selbst + children
1464 if (ctx.nodeType === 1 && ctx.hasAttribute && ctx.hasAttribute("data-dbx")) {
1468 ctx.querySelectorAll("[data-dbx]").forEach(n => nodes.push(n));
1470 // 🔥 NEW: global scoped state store
1471 dbx._scopeState = dbx._scopeState || {};
1472 dbx._scopeError = dbx._scopeError || {};
1473 dbx._scopeInit = dbx._scopeInit || {};
1475 nodes.forEach(el => {
1477 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
1478 if (!cfgList.length) return;
1480 el.__dbxInitialized = el.__dbxInitialized || {};
1481 el.__dbxState = el.__dbxState || {};
1482 el.__dbxError = el.__dbxError || {};
1484 cfgList.forEach(cfg => {
1486 if (!cfg.id) cfg.id = "undef";
1488 const f = dbx.feature._features[cfg.lib];
1491 dbx.log("scan → feature not yet loaded:", cfg.lib);
1494 let scopeType = "element";
1498 if (!["element","group","global"].includes(f.scope)) {
1499 dbx.error("Invalid scope:", cfg.lib, f.scope);
1503 scopeType = f.scope;
1506 dbx.log("scan → fallback scope=element:", cfg.lib);
1511 if (scopeType === "global") {
1513 scopeKey = "global";
1515 } else if (scopeType === "group") {
1517 scopeKey = cfg.group || cfg.id || "group";
1521 if (!el.__dbxScopeId) {
1522 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1523 el.__dbxScopeId = dbx._scopeUid;
1526 scopeKey = el.__dbxScopeId;
1529 const keyScoped = cfg.lib + "::" + scopeKey;
1531 const stateStore = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
1532 const errorStore = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
1533 const initStore = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
1535 const state = stateStore[keyScoped];
1537 if (state === "pending") {
1538 // 🔥 FIX: pending darf nicht blockieren wenn kein task mehr existiert
1539 if (!dbx._taskMap || !dbx._taskMap[keyScoped]) {
1540 stateStore[keyScoped] = undefined;
1545 if (state === "done") return;
1547 let allowRetry = false;
1549 if (state === "error") {
1551 const err = errorStore[keyScoped] || {};
1553 const count = err.count || 1;
1554 const last = err.last || 0;
1556 const delay = Math.min(10000, count * 2000);
1559 dbx.warn("retry limit reached:", keyScoped);
1563 if (Date.now() - last < delay) {
1567 dbx.log("retry allowed:", keyScoped, "attempt:", count + 1);
1571 if (initStore[keyScoped] && stateStore[keyScoped] === "done") return;
1573 const key = keyScoped;
1575 dbx._taskMap = dbx._taskMap || {};
1577 //if (!allowRetry && dbx._taskMap[key]) return;
1579 stateStore[keyScoped] = "pending";
1581 dbx._taskMap[key] = true;
1588 _scopeType: scopeType
1597 /* =====================================================
1599 * ===================================================== */
1600 dbx.runTasks = function () {
1602 if (!dbx._tasks.length) return;
1605 dbx.log("runTasks → already running, skip");
1609 dbx._running = true;
1611 const tasks = dbx._tasks.slice();
1614 dbx.log("runTasks → start", tasks.length);
1624 // -------------------------------------------------
1625 // 🔥 HELPER: richtiger Store je Scope
1626 // -------------------------------------------------
1627 function getStores(t) {
1629 let scopeType = t._scopeType;
1632 const f = dbx.feature._features[t.lib];
1633 scopeType = (f && f.scope) ? f.scope : "element";
1634 t._scopeType = scopeType;
1637 if (scopeType === "element") {
1639 if (!t.el || t.el.nodeType !== 1) {
1640 dbx._scopeState = dbx._scopeState || {};
1641 dbx._scopeError = dbx._scopeError || {};
1642 dbx._scopeInit = dbx._scopeInit || {};
1644 state: dbx._scopeState,
1645 error: dbx._scopeError,
1646 init: dbx._scopeInit
1650 dbx.ensureElementScopeStores(t.el);
1653 state: t.el.__dbxState,
1654 error: t.el.__dbxError,
1655 init: t.el.__dbxInitialized
1659 dbx._scopeState = dbx._scopeState || {};
1660 dbx._scopeError = dbx._scopeError || {};
1661 dbx._scopeInit = dbx._scopeInit || {};
1664 state: dbx._scopeState,
1665 error: dbx._scopeError,
1666 init: dbx._scopeInit
1670 function requeueTask(t, store, key, reason) {
1672 const err = (store.error && store.error[key]) || { count: 0 };
1673 const attempt = err.count + 1;
1674 const maxAttempts = 3;
1677 store.error[key] = { count: attempt, last: Date.now() };
1680 if (attempt < maxAttempts) {
1681 dbx.log("runTasks → requeue (" + attempt + "/" + maxAttempts + "):", t.lib, reason || "");
1682 if (store.state) delete store.state[key];
1683 if (dbx._taskMap) delete dbx._taskMap[key];
1691 function getScopeType(t) {
1692 const f = dbx.feature._features[t.lib];
1693 const scopeType = (f && f.scope) ? f.scope : "element";
1695 if (!["element","group","global"].includes(scopeType)) {
1696 dbx.error("Invalid scope:", t.lib, scopeType);
1703 function makeTaskKey(t, scopeType) {
1704 if (scopeType === "global") {
1705 return t.lib + "::global";
1708 if (scopeType === "group") {
1709 return t.lib + "::" + (t.cfg.group || t.cfg.id || "group");
1712 if (!t.el.__dbxScopeId) {
1713 dbx._scopeUid = (dbx._scopeUid || 0) + 1;
1714 t.el.__dbxScopeId = dbx._scopeUid;
1717 return t.lib + "::" + t.el.__dbxScopeId;
1720 function storeForScope(t, scopeType) {
1721 if (scopeType === "element") {
1722 if (t.el && t.el.nodeType === 1) {
1723 dbx.ensureElementScopeStores(t.el);
1726 state: t.el.__dbxState,
1727 error: t.el.__dbxError,
1728 init: t.el.__dbxInitialized
1732 dbx._scopeState = dbx._scopeState || {};
1733 dbx._scopeError = dbx._scopeError || {};
1734 dbx._scopeInit = dbx._scopeInit || {};
1737 state: dbx._scopeState,
1738 error: dbx._scopeError,
1739 init: dbx._scopeInit
1743 function normalizeTaskScope(t) {
1744 const oldKey = t._key;
1745 const oldScopeType = t._scopeType || "element";
1746 const newScopeType = getScopeType(t);
1747 const newKey = makeTaskKey(t, newScopeType);
1749 if (oldKey && oldKey !== newKey) {
1750 const oldStore = storeForScope(t, oldScopeType);
1752 if (oldStore.state && oldStore.state[oldKey] === "pending") {
1753 delete oldStore.state[oldKey];
1756 if (oldStore.error && oldStore.error[oldKey]) {
1757 delete oldStore.error[oldKey];
1761 delete dbx._taskMap[oldKey];
1765 t._scopeType = newScopeType;
1771 function assignPhases() {
1774 tasks.forEach(t => {
1776 normalizeTaskScope(t);
1778 const store = getStores(t);
1780 if (store.init[t._key] || store.state[t._key] === "done") {
1781 if (dbx._taskMap) delete dbx._taskMap[t._key];
1789 seen[t._key] = true;
1790 store.state[t._key] = "pending";
1791 dbx._taskMap = dbx._taskMap || {};
1792 dbx._taskMap[t._key] = true;
1794 const f = dbx.feature._features[t.lib];
1795 const libPrio = f && f.priority ? f.priority : "mid";
1796 const cfgPrio = t.cfg.prio;
1797 const prio = cfgPrio || libPrio || "mid";
1799 if (!phases[prio]) phases.mid.push(t);
1800 else phases[prio].push(t);
1804 // =================================================
1806 // =================================================
1807 function runVeryFirstImmediate(done) {
1809 const list = phases.veryfirst;
1810 if (!list.length) return done();
1816 if (i >= list.length) return done();
1818 const t = list[i++];
1821 const store = getStores(t);
1823 if (store.state[key] !== "pending") {
1824 store.state[key] = "pending";
1827 dbx.resolveFeature(t.lib, function (ok) {
1830 if (requeueTask(t, store, key, "resolveFeature failed")) {
1833 dbx.error("runTasks → lib load failed:", t.lib);
1834 store.state[key] = "error";
1838 const f = dbx.feature._features[t.lib];
1842 if (f && Array.isArray(f.css)) {
1843 assets.push(...f.css);
1846 if (f && Array.isArray(f.js)) {
1847 f.js.forEach(entry => {
1848 if (Array.isArray(entry)) {
1851 assets.push(['js', 'lib', entry]);
1856 if (assets.length) {
1857 dbx.load(assets, runInit);
1862 function runInit() {
1866 const feature = dbx.feature._features[t.lib];
1868 if (!feature || !feature.init) {
1869 if (requeueTask(t, store, key, "init missing")) {
1872 dbx.error("runTasks → no init for lib:", t.lib);
1873 store.state[key] = "error";
1877 feature.init.call(feature, t.el, t.cfg);
1879 store.init[key] = true;
1880 store.state[key] = "done";
1882 delete store.error[key];
1884 if (dbx._taskMap) delete dbx._taskMap[key];
1888 if (requeueTask(t, store, key, "init error: " + e.message)) {
1892 dbx.error("runTasks → INIT ERROR:", t.lib, e);
1893 store.state[key] = "error";
1895 if (dbx._taskMap) delete dbx._taskMap[key];
1906 // -------------------------------------------------
1907 // PREPARE (unverändert)
1908 // -------------------------------------------------
1909 function runPrepare(done) {
1912 const assetSet = new Set();
1916 if (i >= tasks.length) {
1918 const list = Array.from(assetSet).map(s => JSON.parse(s));
1920 if (!list.length) return done();
1922 return dbx.load(list, done);
1925 const t = tasks[i++];
1927 dbx.resolveFeature(t.lib, function (ok) {
1929 if (ok !== true) return next();
1931 const f = dbx.feature._features[t.lib];
1933 if (f && Array.isArray(f.css)) {
1934 f.css.forEach(entry => assetSet.add(JSON.stringify(entry)));
1937 if (f && Array.isArray(f.js)) {
1938 f.js.forEach(entry => {
1939 if (Array.isArray(entry)) {
1940 assetSet.add(JSON.stringify(entry));
1942 assetSet.add(JSON.stringify(['js', 'lib', entry]));
1954 // -------------------------------------------------
1956 // -------------------------------------------------
1957 function runPhase(name, list, done) {
1959 dbx.log("runPhase →", name, "tasks:", list.length);
1965 if (i >= list.length) {
1966 return done && done();
1969 const t = list[i++];
1971 dbx.log("runPhase → task:", name, t.lib, t._key);
1973 if (name === "veryfirst") return next();
1976 const store = getStores(t);
1978 if (store.state[key] !== "pending") {
1979 store.state[key] = "pending";
1982 new Promise((resolve) => {
1984 function runInit() {
1988 const f = dbx.feature._features[t.lib];
1990 if (!f || !f.init) {
1992 if (requeueTask(t, store, key, "init missing")) {
1996 dbx.error("runPhase → NO INIT:", t.lib);
1997 store.state[key] = "error";
1999 if (dbx._taskMap) delete dbx._taskMap[key];
2004 dbx.log("runPhase → INIT:", t.lib);
2006 // 🔥 FIX: korrektes this wiederherstellen (einzige Änderung)
2007 const res = f.init.call(f, t.el, t.cfg);
2009 if (res && typeof res.then === "function") {
2011 store.init[key] = true;
2012 store.state[key] = "done";
2013 delete store.error[key];
2015 if (dbx._taskMap) delete dbx._taskMap[key];
2020 store.init[key] = true;
2021 store.state[key] = "done";
2022 delete store.error[key];
2024 if (dbx._taskMap) delete dbx._taskMap[key];
2031 if (requeueTask(t, store, key, "init error: " + e.message)) {
2035 dbx.error("runPhase → INIT ERROR:", t.lib, e);
2036 store.state[key] = "error";
2038 if (dbx._taskMap) delete dbx._taskMap[key];
2044 const libName = t.lib || t.cfg?.lib;
2046 dbx.resolveFeature(libName, (ok) => {
2049 if (requeueTask(t, store, key, "resolveFeature failed")) {
2052 dbx.error("runPhase → lib load failed:", libName);
2053 store.state[key] = "error";
2054 if (dbx._taskMap) delete dbx._taskMap[key];
2058 const f = dbx.feature._features[libName];
2062 if (f && Array.isArray(f.css)) {
2063 assets.push(...f.css);
2066 if (f && Array.isArray(f.js)) {
2067 f.js.forEach(entry => {
2068 if (Array.isArray(entry)) {
2071 assets.push(['js', 'lib', entry]);
2076 if (assets.length) {
2077 dbx.load(assets, runInit);
2090 // =================================================
2092 // =================================================
2093 runPrepare(function () {
2097 runVeryFirstImmediate(function () {
2099 runPhase("first", phases.first, function () {
2101 runPhase("mid", phases.mid, function () {
2103 runPhase("last", phases.last, function () {
2105 runPhase("verylast", phases.verylast, function () {
2108 dbx._running = false;
2110 if (dbx._tasks.length) {
2111 setTimeout(dbx.runTasks, 0);
2123 /* =====================================================
2124 * 🔥 MUTATION OBSERVER (ADD ONLY)
2125 * ===================================================== */
2126 if (!dbx.__observerInit) {
2128 dbx.log("observer → init");
2130 const observer = new MutationObserver(function (mutations) {
2132 mutations.forEach(m => {
2134 // =================================================
2135 // 🔥 ADD (bestehend)
2136 // =================================================
2137 m.addedNodes.forEach(node => {
2139 if (node.nodeType !== 1) return;
2141 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2142 dbx.log("observer → node");
2146 if (node.querySelectorAll) {
2147 const found = node.querySelectorAll("[data-dbx]");
2149 dbx.log("observer → subtree:", found.length);
2155 // =================================================
2156 // 🔥 NEW: REMOVE → CLEANUP + DESTROY
2157 // =================================================
2158 m.removedNodes.forEach(node => {
2160 if (node.nodeType !== 1) return;
2165 if (node.hasAttribute && node.hasAttribute("data-dbx")) {
2170 if (node.querySelectorAll) {
2171 node.querySelectorAll("[data-dbx]").forEach(n => list.push(n));
2174 if (!list.length) return;
2176 list.forEach(el => {
2178 const cfgList = dbx.parseData(el.getAttribute("data-dbx"));
2179 if (!cfgList.length) return;
2181 cfgList.forEach(cfg => {
2183 if (!cfg.id) cfg.id = "undef";
2185 const f = dbx.feature._features[cfg.lib];
2186 if (!f || !f.scope) return;
2188 const scopeType = f.scope;
2192 if (scopeType === "global") {
2193 // global → NICHT löschen
2197 if (scopeType === "group") {
2198 scopeKey = cfg.group || cfg.id || "group";
2200 scopeKey = el.__dbxScopeId;
2203 if (!scopeKey) return;
2205 const keyScoped = cfg.lib + "::" + scopeKey;
2207 const storeState = (scopeType === "element") ? el.__dbxState : dbx._scopeState;
2208 const storeError = (scopeType === "element") ? el.__dbxError : dbx._scopeError;
2209 const storeInit = (scopeType === "element") ? el.__dbxInitialized : dbx._scopeInit;
2211 // -------------------------------------------------
2212 // 🔥 DESTROY (optional)
2213 // -------------------------------------------------
2215 if (f.destroy && storeInit && storeInit[keyScoped]) {
2219 dbx.error("destroy failed:", cfg.lib, e);
2222 // -------------------------------------------------
2224 // -------------------------------------------------
2225 if (storeState && keyScoped in storeState) {
2226 delete storeState[keyScoped];
2229 if (storeError && keyScoped in storeError) {
2230 delete storeError[keyScoped];
2233 if (storeInit && keyScoped in storeInit) {
2234 delete storeInit[keyScoped];
2237 dbx.log("GC cleanup:", keyScoped);
2244 observer.observe(document.body, {
2249 dbx.__observerInit = true;
2252 /* =====================================================
2253 * 🔥 EVENT DELEGATION HELPER
2254 * ===================================================== */
2255 dbx.on = function (event, selector, handler) {
2257 document.addEventListener(event, function (e) {
2259 const el = e.target.closest(selector);
2262 handler.call(el, e, el);
2264 }, true); // 🔥 WICHTIG: CAPTURE PHASE
2266 dbx.log("delegation registered:", event, selector);
2270 /* =====================================================
2271 * 🔥 EVENT SYSTEM (CUSTOM)
2272 * ===================================================== */
2274 dbx.event = dbx.event || {
2279 if (!name || typeof handler !== "function") return;
2281 this._events[name] = this._events[name] || [];
2282 this._events[name].push(handler);
2284 dbx.log("event:on →", name);
2290 const list = this._events[name];
2291 if (!list || !list.length) return;
2293 dbx.log("event:emit →", name, data);
2295 list.forEach(fn => {
2299 dbx.error("event handler failed:", name, e);
2303 // 🔥 NEU: scoped event
2304 if (data && data.id) {
2305 const scoped = name + ":" + data.id;
2306 const list2 = this._events[scoped];
2309 list2.forEach(fn => {
2313 dbx.error("event handler failed:", scoped, e);
2322 /* =====================================================
2323 * DEVICE (STATE-BASED ABSTRACTION)
2324 * ===================================================== */
2325 dbx.device = (function(){
2331 lastActivity: Date.now()
2334 const IDLE_MS = 30000; // 30s
2336 /* =====================================================
2338 * ===================================================== */
2340 function updateVisibility(){
2341 state.visible = !document.hidden;
2344 function updateOnline(){
2345 state.online = navigator.onLine !== false;
2348 function markActive(){
2349 state.lastActivity = Date.now();
2350 state.active = true;
2353 function checkIdle(){
2354 const now = Date.now();
2355 state.active = (now - state.lastActivity) < IDLE_MS;
2358 /* =====================================================
2359 * BIND EVENTS (BROWSER)
2360 * ===================================================== */
2362 document.addEventListener('visibilitychange', updateVisibility, true);
2363 window.addEventListener('online', updateOnline, true);
2364 window.addEventListener('offline', updateOnline, true);
2366 document.addEventListener('mousemove', markActive, true);
2367 document.addEventListener('keydown', markActive, true);
2368 document.addEventListener('click', markActive, true);
2370 // idle checker (leichtgewichtig)
2371 setInterval(checkIdle, 5000);
2377 /* =====================================================
2379 * ===================================================== */
2383 /* ===== READ ===== */
2386 return state.visible === true;
2390 return state.online === true;
2394 return state.active === true;
2398 return Object.assign({}, state);
2401 /* =====================================================
2402 * OPTIONAL: EXTERNAL OVERRIDE (FUTURE: APP / PWA)
2403 * ===================================================== */
2407 if(!partial || typeof partial !== 'object') return;
2409 if(typeof partial.visible === 'boolean'){
2410 state.visible = partial.visible;
2413 if(typeof partial.online === 'boolean'){
2414 state.online = partial.online;
2417 if(typeof partial.active === 'boolean'){
2418 state.active = partial.active;
2421 if(typeof partial.lastActivity === 'number'){
2422 state.lastActivity = partial.lastActivity;
2425 dbx.log('[device] override', partial);
2432 /* =====================================================
2433 * LOOP (SMART POLLING CORE)
2434 * ===================================================== */
2435 dbx.loop = (function(){
2439 function clamp(v, min, max){
2440 if(min != null && v < min) return min;
2441 if(max != null && v > max) return max;
2445 function resolveInterval(task){
2447 const t = task.timing || {};
2460 case 'fast': interval = t.min || t.base; break;
2461 case 'slow': interval = Math.max((t.base||2000)*2, t.idle||3000); break;
2462 case 'idle': interval = t.idle || t.base; break;
2463 case 'boost': interval = (t.base||2000) / 2; break;
2464 default: interval = t.base || 2000;
2469 if(!dbx.device.isVisible()){
2470 interval = t.hidden || (t.base||2000)*3;
2472 else if(!dbx.device.isActive()){
2473 interval = t.idle || (t.base||2000)*2;
2476 interval = t.base || 2000;
2487 function schedule(task){
2489 if(task.paused) return;
2492 clearTimeout(task.timer); // 🔥 FIX
2493 task.timer = null; // 🔥 FIX
2496 const interval = resolveInterval(task);
2497 if(interval == null) return;
2499 task.timer = setTimeout(() => run(task), interval);
2504 if(task.running) return;
2506 task.running = true;
2517 dbx.error('[loop] run error', task.id, e);
2522 Promise.resolve(res)
2524 dbx.error('[loop] async error', task.id, err);
2526 .finally(() => finish());
2530 task.running = false;
2531 task.lastRun = Date.now();
2533 task.timer = null; // 🔥 FIX
2535 if(task.hintUntil && Date.now() > task.hintUntil){
2548 if(!cfg || !cfg.id || !cfg.onRun){
2549 dbx.error('[loop] invalid task', cfg);
2554 dbx.warn('[loop] already exists', cfg.id);
2561 timing: cfg.timing || {},
2570 schedule(tasks[cfg.id]);
2572 dbx.log('[loop] add', cfg.id);
2575 hint(id, mode, opts){
2577 const t = tasks[id];
2580 if(mode === 'pause'){
2583 clearTimeout(t.timer);
2589 if(mode === 'resume'){
2591 if(t.timer) clearTimeout(t.timer);
2596 if(mode === 'none'){
2604 if(opts && opts.duration){
2605 t.hintUntil = Date.now() + opts.duration;
2608 if(mode === 'boost'){
2610 // 🔥 FIX: niemals während running starten
2612 return; // einfach nächsten Zyklus beschleunigen
2616 clearTimeout(t.timer);
2627 Object.values(tasks).forEach(t => {
2646 /* =====================================================
2647 * DEVICE CAPABILITIES (BRIDGE)
2648 * ===================================================== */
2650 dbx.device.camera = {
2654 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
2655 dbx.error('[device] camera not supported');
2656 throw new Error('camera_not_supported');
2659 const constraints = opts?.constraints || { video: true };
2663 const stream = await navigator.mediaDevices.getUserMedia(constraints);
2668 stream.getTracks().forEach(t => t.stop());
2674 dbx.error('[device] camera error', e);
2686 return new Promise((resolve, reject) => {
2688 const input = document.createElement('input');
2689 input.type = 'file';
2691 if (opts?.accept) input.accept = opts.accept;
2692 if (opts?.multiple) input.multiple = true;
2694 input.onchange = () => {
2696 if(!input.files || !input.files.length){
2697 resolve(null); // 🔥 FIX
2699 resolve(input.files);
2703 input.onerror = reject;
2712 dbx.device.clipboard = {
2716 if (!navigator.clipboard) {
2717 dbx.error('[device] clipboard not supported');
2718 throw new Error('clipboard_not_supported');
2721 return navigator.clipboard.writeText(text);
2726 if (!navigator.clipboard) {
2727 dbx.error('[device] clipboard not supported');
2728 throw new Error('clipboard_not_supported');
2731 return navigator.clipboard.readText();
2737 dbx.device.share = {
2741 if (!navigator.share) {
2742 dbx.error('[device] share not supported');
2743 throw new Error('share_not_supported');
2746 return navigator.share(data);
2752 /* =====================================================
2754 * ===================================================== */
2756 dbx.log("DOM ready → scan");
2758 dbx.footerStatus.init();
2761 /* =====================================================
2763 * ===================================================== */
2764 dbx.rescan = function (root) {
2766 const ctx = root || document;
2769 Object.keys(dbx.feature._features || {}).forEach(function (name) {
2770 const f = dbx.feature._features[name];
2771 if (!f || typeof f.rescan !== "function") return;
2776 dbx.error("rescan → feature error:", name, e);
2781 dbx.loadFeature = function (name, cb) {
2782 dbx.resolveFeature(name, cb || function () {});
2785})(window, document, jQuery);