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