PageSourceSearch

https://napoparte-cmms.hf.space/assets/modal_layout.js?m=1789806719.0

js napoparte-cmms.hf.space collected 2026-10-03 17:10:06 UTC 27,680 bytes, 580 lines download raw bytes

1/**
2 * modal_layout.js
3 *
4 * Fixes modal layout at all zoom levels.
5 *
6 * Problems solved:
7 * [4] Modal sits too low — Bootstrap sets .modal padding-top ~86px which pushes
8 *     the dialog below the navbar. We override with the actual navbar height.
9 * [5] Col3 readme content doesn't scroll — the flex chain from modal-body → row →
10 *     col → inner divs → readme-content fails.
11 * [6] At zoom >100%, same problem is worse.
12 * [7] Small modals (change-pwd, login, confirm...) bị full-height giống large modals
13 *     do fixModalPosition() inject height inline style cho tất cả.
14 * [8] admin-modal khi ở login state vẫn chiếm 95vw — cần thu nhỏ lại.
15 * [9] Click outside modal to close — for simple modals close immediately;
16 *     for admin-modal trigger the close-btn which checks unsaved changes.
17 */
18
19(function () {
20    'use strict';
21
22    // ── Config ──────────────────────────────────────────────────────────────
23    // Chỉ các modal này mới cần full-height (chiếm gần hết viewport)
24    var LARGE_MODAL_IDS = [
25        'about-modal',
26        'admin-modal',
27        'user-logs-modal',
28        'layout-editor-modal',
29        'batch-export-modal',
30        'maint-cl-detail-modal',
31        'incident-report-modal',
32        'rootcause-modal',
33        'maint-sp-mini-modal',
34        'incident-spare-modal',
35    ];
36
37    // Các modal AUTO-HEIGHT: shrink vừa nội dung khi ít, expand tới max-height khi nhiều
38    // Không bao giờ bị fixed height như LARGE_MODAL_IDS
39    // NOTE: drilldown-modal đã được xử lý bởi applyAutoHeightXlModalStyle() trong utils_js.py
40    // (detect bằng #drilldown-modal-body) — không đưa vào đây để tránh conflict.
41    var AUTO_HEIGHT_MODAL_IDS = [
42        // Các modal dưới đây được xử lý bởi utils_js.py applyAutoHeightXlModalStyle
43        // (detect bằng unique child ID, scale-aware) — KHÔNG đưa vào đây để tránh conflict:
44        // 'drilldown-modal', 'maint-settings-modal', 'planning-planned-status-modal', 'asset-detail-modal'
45        'sp-add-modal',              // Modal Thêm vật tư mới
46        'sp-in-modal',               // Modal Nhập kho
47        'sp-out-modal',              // Modal Xuất kho
48        'online-users-modal',        // Modal Người dùng Online
49        'batch-export-modal-auto',   // (alias, handled below)
50        // (Fix modal Shift Schedule bị top quá thấp / không co giãn theo
51        // % viewport / không scroll được nội dung dài trên các màn hình
52        // khác resolution — vd 14" vs 24") — áp dụng cùng cơ chế
53        // auto-height đang dùng ổn định cho sp-add-modal/sp-in-modal.
54        'shift-add-code-modal',      // Modal "Thêm mã ca hoặc mã nghỉ"
55        'shift-add-employee-modal',  // Modal "Thêm/Sửa nhân viên"
56        'shift-schedule-guide-modal', // Modal "Hướng dẫn xếp ca"
57        'role-edit-modal',           // Modal "Edit Role Permissions" (Admin Settings > Users/Roles)
58        // (FIX <wp-edit-modal-scroll-y>: modal Sửa Work Permit không có
59        // scroll Y — nội dung dài (nhiều field) bị tràn xuống dưới viewport
60        // và không đọc/thao tác được) — áp dụng cùng cơ chế auto-height
61        // đang dùng ổn định cho role-edit-modal (max-height theo viewport,
62        // modal-body tự scroll khi nội dung dài).
63        'wp-edit-modal',              // Modal "Sá»­a Work Permit" (tab Work Permit)
64    ];
65
66    // Các modal nhỏ — CHỈ fix margin-top (để clear navbar), KHÔNG set height
67    var SMALL_MODAL_IDS = [
68        'change-pwd-modal',
69        'login-modal',
70        'confirm-delete-modal',
71        'confirm-save-modal',
72        'admin-confirm-modal',
73        'admin-action-confirm-modal',
74        'fullscreen-modal',
75        'maint-settings-confirm-modal',
76    ];
77
78    var ALL_MODAL_IDS = LARGE_MODAL_IDS.concat(AUTO_HEIGHT_MODAL_IDS).concat(SMALL_MODAL_IDS);
79
80    // Fix #9: Modals that should close when clicking the backdrop
81    // For admin-modal: clicking outside triggers the close-btn (which checks unsaved changes)
82    // For layout-editor-modal: clicking outside triggers the layout close button
83    // Simple modals: click outside → click the close/X button automatically
84    var BACKDROP_CLOSE_CONFIG = {
85        // modal_id: close_btn_id or 'default' (use built-in X close button)
86        'session-details-modal': 'session-details-modal-close-btn',
87        'online-users-modal':  'online-users-modal-close-btn',
88        'drilldown-modal':     'drilldown-modal-close',
89        'sync-logs-modal': 'sync-logs-close-btn',
90        'about-modal': null, // Use built-in close button (close_button=True)
91        'admin-modal': 'admin-modal-close-btn',  // Triggers unsaved-changes check
92        'layout-editor-modal': 'layout-editor-close-btn', // Triggers unsaved-changes check
93        'user-logs-modal': 'user-logs-close-btn',
94        'batch-export-modal': null, // batch-export: keep static (complex state)
95    };
96
97    // ── Navbar height helper ─────────────────────────────────────────────────
98    function getNavbarHeight() {
99        var nav = document.querySelector(
100            'nav.navbar, .navbar, header[class*="navbar"], #navbar'
101        );
102        if (nav) return nav.getBoundingClientRect().height;
103        var v = getComputedStyle(document.documentElement)
104            .getPropertyValue('--navbar-height');
105        if (v) return parseFloat(v) || 66;
106        return 66;
107    }
108
109    // ── Kiểm tra admin-modal đang ở trạng thái login hay settings ────────────
110    function isAdminLoginState() {
111        var loginSection = document.getElementById('admin-login-section');
112        var settingsSection = document.getElementById('admin-settings-section');
113        if (!loginSection || !settingsSection) return false;
114        var loginVisible = (loginSection.style.display !== 'none') &&
115                           (getComputedStyle(loginSection).display !== 'none');
116        var settingsVisible = (settingsSection.style.display !== 'none') &&
117                              (getComputedStyle(settingsSection).display !== 'none');
118        return loginVisible && !settingsVisible;
119    }
120
121    // ── Toggle class trên admin-modal để CSS có thể target ───────────────────
122    function updateAdminModalState() {
123        var adminModal = document.getElementById('admin-modal');
124        if (!adminModal) return;
125        if (isAdminLoginState()) {
126            adminModal.classList.add('admin-modal--login-state');
127        } else {
128            adminModal.classList.remove('admin-modal--login-state');
129        }
130    }
131
132    // ── Fix large modal: full-height, just below navbar ──────────────────────
133    function fixLargeModalPosition(modalEl, modalId) {
134        var navH = getNavbarHeight();
135        // Dùng gap nhỏ hơn trên màn có viewport thấp (14" thường < 800px)
136        // để modal expand gần tới bottom hơn
137        var gap  = window.innerHeight < 800 ? 2 : 6;
138
139        modalEl.style.paddingTop    = '0px';
140        modalEl.style.paddingBottom = '0px';
141
142        var dialog = modalEl.querySelector('.modal-dialog');
143        if (!dialog) return;
144
145        // admin-modal khi ở login state → thu nhỏ như small modal
146        if (modalId === 'admin-modal' && isAdminLoginState()) {
147            fixSmallModalPosition(modalEl);
148            return;
149        }
150
151        var bottomMargin = window.innerHeight < 800 ? 2 : 4;
152        var availH = window.innerHeight - navH - gap - bottomMargin;
153        dialog.style.marginTop    = (navH + gap) + 'px';
154        dialog.style.marginBottom = bottomMargin + 'px';
155        dialog.style.height       = availH + 'px';
156        dialog.style.maxHeight    = availH + 'px';
157        dialog.style.minHeight    = availH + 'px';
158    }
159
160    // ── Fix small modal: chỉ đẩy xuống dưới navbar, KHÔNG set height ─────────
161    function fixSmallModalPosition(modalEl) {
162        // Small modals: SCALING_JS (applySmallModalStyle) đã xử lý đầy đủ
163        // transform:scale, margin-top, width => modal_layout.js chỉ cần
164        // reset paddingTop của wrapper để không bị Bootstrap padding đẩy thêm.
165        modalEl.style.paddingTop    = '0px';
166        modalEl.style.paddingBottom = '0px';
167
168        var dialog = modalEl.querySelector('.modal-dialog');
169        if (!dialog) return;
170
171        // Xóa inline height nếu đã từng bị set từ trước (ví dụ khi switch modal type)
172        dialog.style.height    = '';
173        dialog.style.minHeight = '';
174        // KHÔNG set marginTop hay width ở đây — applySmallMod
174alStyle lo việc đó.
175    }
176
177    // ── Fix auto-height modal: shrink-to-fit content, max = viewport - navbar ─
178    function fixAutoHeightModalPosition(modalEl) {
179        var navH = getNavbarHeight();
180        var gap  = window.innerHeight < 800 ? 4 : 8;
181        var bottomGap = window.innerHeight < 800 ? 4 : 8;
182        var maxH = window.innerHeight - navH - gap - bottomGap;
183
184        modalEl.style.paddingTop    = '0px';
185        modalEl.style.paddingBottom = '0px';
186
187        var dialog = modalEl.querySelector('.modal-dialog');
188        if (!dialog) return;
189
190        // marginTop clears navbar; no fixed height → shrinks to content
191        dialog.style.marginTop    = (navH + gap) + 'px';
192        dialog.style.marginBottom = bottomGap + 'px';
193        dialog.style.maxHeight    = maxH + 'px';
194        // Remove any fixed height previously set (e.g. if modal was in LARGE list before)
195        dialog.style.height    = '';
196        dialog.style.minHeight = '';
197
198        // Ensure modal-content also respects maxHeight
199        var content = dialog.querySelector('.modal-content');
200        if (content) {
201            content.style.maxHeight = maxH + 'px';
202        }
203    }
204
205    // ── Fix about-modal column layout (issues 5, 6) ──────────────────────────
206    function fixAboutModalCols() {
207        var modalBody = document.querySelector('#about-modal .modal-body');
208        var row       = document.querySelector('#about-modal .modal-body > .row');
209        var cols      = document.querySelectorAll(
210            '#about-modal .modal-body > .row > [class*="col-"]'
211        );
212        if (!modalBody || !row || !cols.length) return;
213
214        var bodyH = Math.round(modalBody.getBoundingClientRect().height);
215        if (bodyH < 50) return;
216
217        row.style.height    = bodyH + 'px';
218        row.style.maxHeight = bodyH + 'px';
219        row.style.minHeight = bodyH + 'px';
220        row.style.overflow  = 'hidden';
221        row.style.display   = 'flex';
222        row.style.flexWrap  = 'nowrap';
223        row.style.alignItems = 'stretch';
224
225        cols.forEach(function (col) {
226            col.style.height    = bodyH + 'px';
227            col.style.maxHeight = bodyH + 'px';
228            col.style.minHeight = bodyH + 'px';
229            col.style.overflow  = 'hidden';
230            col.style.display   = 'flex';
231            col.style.flexDirection = 'column';
232
233            var inner = col.firstElementChild;
234            if (inner) {
235                inner.style.flex      = '1 1 auto';
236                inner.style.minHeight = '0';
237                inner.style.overflowY = 'auto';
238                inner.style.overflowX = 'hidden';
239            }
240        });
241
242        var col1 = cols[0];
243        if (col1) {
244            var col1inner = col1.firstElementChild;
245            if (col1inner) {
246                col1inner.style.display       = 'flex';
247                col1inner.style.flexDirection = 'column';
248                var footer = col1inner.lastElementChild;
249                if (footer) footer.style.marginTop = 'auto';
250            }
251        }
252
253        var col3 = cols[cols.length - 1];
254        if (col3) {
255            var col3inner = col3.firstElementChild;
256            if (col3inner) {
257                col3inner.style.display       = 'flex';
258                col3inner.style.flexDirection = 'column';
259                col3inner.style.overflow      = 'hidden';
260
261                var readmeCard = col3inner.firstElementChild;
262                if (readmeCard) {
263                    readmeCard.style.flex          = '1 1 auto';
264                    readmeCard.style.minHeight     = '0';
265                    readmeCard.style.display       = 'flex';
266                    readmeCard.style.flexDirection = 'column';
267                    readmeCard.style.overflow      = 'hidden';
268                }
269            }
270
271            var readme = document.getElementById('about-readme-content');
272            if (readme) {
273                readme.style.flex      = '1 1 auto';
274                readme.style.minHeight = '0';
275                readme.style.maxHeight = '100%';
276                readme.style.overflowY = 'auto';
277                readme.style.overflowX = 'hidden';
278            }
279        }
280    }
281
282    // ── Fix #9: Click outside modal to close ─────────────────────────────────
283    function setupBackdropClose(modalId, closeBtnId) {
284        // Don't setup backdrop close for null entries (keep static)
285        if (closeBtnId === null && modalId !== 'about-modal') return;
286
287        var modalEl = document.getElementById(modalId);
288        if (!modalEl) return;
289
290        // Prevent double-binding
291        if (modalEl.__backdropCloseSetup) return;
292        modalEl.__backdropCloseSetup = true;
293
294        modalEl.addEventListener('mousedown', function(e) {
295            // Check if click was directly on the modal backdrop (the outermost .modal element)
296            if (e.target !== modalEl) return;
297            // Modal must be open
298            if (!modalEl.classList.contains('show')) return;
299
300            if (closeBtnId) {
301                // Click the designated close button — this triggers Dash callback
302                // which checks unsaved changes before closing
303                var closeBtn = document.getElementById(closeBtnId);
304                if (closeBtn) {
305                    closeBtn.click();
306                }
307            } else {
308                // Find built-in Bootstrap close button (for modals with close_button=True)
309                var builtInClose = modalEl.querySelector('.modal-header .btn-close, .modal-header [aria-label="Close"]');
310                if (builtInClose) {
311                    builtInClose.click();
312                }
313            }
314        });
315    }
316
317    // ── Observe modal khi mở/đóng ────────────────────────────────────────────
318    function observeModal(modalId, isLarge, isAutoHeight) {
319        var modalEl = document.getElementById(modalId);
320        if (!modalEl) return;
321
322        var observer = new MutationObserver(function () {
323            var isOpen = modalEl.classList.contains('show');
324            if (!isOpen) return;
325
326            if (isLarge) {
327                fixLargeModalPosition(modalEl, modalId);
328                if (modalId === 'about-modal') {
329                    setTimeout(fixAboutModalCols, 50);
330                    setTimeout(fixAboutModalCols, 300);
331                }
332            } else if (isAutoHeight) {
333                fixAutoHeightModalPosition(modalEl);
334            } else {
335                fixSmallModalPosition(modalEl);
336            }
337        });
338
339        observer.observe(modalEl, {
340            attributes: true,
341            attributeFilter: ['class', 'style'],
342        });
343
344        // admin-modal: theo dõi thêm login/settings section để phát hiện state change
345        if (modalId === 'admin-modal') {
346            var watchSection = function(sectionId) {
347                var section = document.getElementById(sectionId);
348                if (!section) return;
349                new MutationObserver(function () {
350                    updateAdminModalState();
351                    if (modalEl.classList.contains('show')) {
352                        fixLargeModalPosition(modalEl, 'admin-modal');
353                    }
354                }).observe(section, { attributes: true, attributeFilter: ['style'] });
355            };
356            // Delay vì Dash render section sau khi modal mount
357            setTimeout(function() {
358                watchSection('admin-login-section');
359                watchSection('admin-settings-section');
360            }, 1000);
361        }
362    }
363
364    // ── ResizeObserver: re-sync khi window resize hoặc zoom thay đổi ─────────
365    function watchResize() {
366        if (typeof ResizeObserver === 'undefined') return;
367
368        var ro = new ResizeObserver(function () {
369            LARGE_MODAL_IDS.forEach(function (id) {
370                var el = document.getElementById(id);
371                if (el && el.classList.contains('show')) {
372                    fixLargeModalPosition(el, id);
373                    if (id === 'about-modal') fixAboutModalCols();
374                }
375            });
376            AUTO_HEIGHT_MODAL_IDS.forEach(function (id) {
377                var el = document.getElementById(id);
378                if (el && el.classList.contains('show')) {
379                    fixAutoHeightModalPosition(el);
380                }
381            });
382            SMALL_MODAL_IDS.forEach(function (id) {
383                var el = document.getElementById(id);
384                if (el && el.classList.contains('show')) {
385                    fixSmallModalPosition(el);
386                }
387            });
388        });
389
390        ro.observe(document.documentElement);
391    }
392
393    // ── Init ─────────────────────────────────────────────────────────────────
394    function init() {
395        LARGE_MODAL_IDS.forEach(function (id) { observeModal(id, true, false); });
396        AUTO_HEIGHT_MODAL_IDS.forEach(function (id) { observeModal(id, false, true); });
397        SMALL_MODAL_IDS.forEach(function (id) { observeModal(id, false, false); });
398        watchResize();
399
400        // Fix #9: Setup backdrop click-to-close for configured modals
401        // Use setTimeout to ensure DOM is ready
402        setTimeout(function() {
403            Object.keys(BACKDROP_CLOSE_CONFIG).forEach(function(modalId) {
404                var closeBtnId = BACKDROP_CLOSE_CONFIG[modalId];
405                setupBackdropClose(modalId, closeBtnId);
406            });
407        }, 1500);
408    }
409
410    if (document.readyState === 'loading') {
411        document.addEventListener('DOMContentLoaded', init);
412    } else {
413        var retries = 0;
414        var interval = setInterval(function () {
415            var found = ALL_MODAL_IDS.some(function (id) {
416                return document.getElementById(id);
417            });
418            if (found || retries++ > 20) {
419                clearInterval(interval);
420                init();
421            }
422        }, 500);
423    }
424
425})();
426/* ══════════════════════════════════════════════════════════════════
427   FIX #4: Portal-style fix for dcc.Dropdown inside overflow:hidden
428   Uses MutationObserver to detect when role dropdown opens and
429   repositions the menu absolutely relative to viewport.
430   ══════════════════════════════════════════════════════════════════ */
431(function() {
432    function fixDropdownMenu(menuOuter) {
433        if (!menuOuter || menuOuter._asm_fixed) return;
434        const control = menuOuter.closest('.Select');
435        if (!control) return;
436        const rect = control.getBoundingClientRect();
437        // FIX (scale bug — màn hình nhỏ vd laptop 14"): applyScale() (xem
438        // utils_js.py) đặt transform:scale(X) lên #react-entry-point khi
439        // window.outerWidth < 1920. Theo spec CSS, MỌI ancestor có transform
440        // sẽ trở thành "containing block" cho các phần tử con position:
441        // fixed — nghĩa là "top/left" bên dưới không còn tính theo viewport
442        // THẬT nữa, mà tính theo hệ toạ độ LOCAL (chưa scale) của
443        // #react-entry-point. rect.* từ getBoundingClientRect() lại LUÔN trả
444        // về toạ độ THẬT (đã scale) — nên nếu gán thẳng rect.bottom/rect.left
445        // làm top/left px cho 1 phần tử VẪN NẰM LỒNG bên trong
446        // #react-entry-point (như menuOuter ở đây — nó không được portal ra
447        // <body>), kết quả sẽ bị lệch đúng bằng hệ số scale (menu bị "co"
448        // lại về phía góc trên-trái). Chia ngược lại cho scale để quy đổi về
449        // đúng hệ toạ độ LOCAL trước khi gán.
450        const scale = window._appScale || 1;
451        const rectBottom = rect.bottom / scale;
452        const rectLeft = rect.left / scale;
453        const rectWidth = rect.width / scale;
454        // Calculate available space below
455        const spaceBelow = window.innerHeight - rect.bottom - 10;
456        const menuH = Math.min(200, Math.max(100, spaceBelow));
457        // Use fixed positioning to escape overflow:hidden/auto ancestors
458        menuOuter.style.position = 'fixed';
459        menuOuter.style.top = rectBottom + 'px';
460        menuOuter.style.left = rectLeft + 'px';
461        menuOuter.style.width = Math.max(rectWidth, 130) + 'px';
462        menuOuter.style.minWidth = Math.max(rectWidth, 130) + 'px';
463        menuOuter.style.maxHeight = menuH + 'px';
464        menuOuter.style.zIndex = '999999';
465        menuOuter.style.overflow = 'auto';
466        menuOuter.style.boxShadow = '0 4px 20px rgba(0,0,0,0.35)';
467        menuOuter._asm_fixed = true;
468        // Re-position if control moves (e.g. on scroll)
469        const scrollParent = menuOuter.closest('#user-list-container, .modal-body');
470        if (scrollParent) {
471            const onScroll = function() {
472                const r2 = control.getBoundingClientRect();
473                const s2 = window._appScale || 1;
474                menuOuter.style.top = (r2.bottom / s2) + 'px';
475                menuOuter.style.left = (r2.left / s2) + 'px';
476            };
477            scrollParent.addEventListener('scroll', onScroll, { passive: true });
478            menuOuter._asm_scroll_cleanup = function() {
479                scrollParent.removeEventListener('scroll', onScroll);
480            };
481        }
482    }
483
484    function observeDropdowns() {
485        const adminModal = document.getElementById('admin-modal');
486        if (!adminModal) return;
487        const obs = new MutationObserver(function(mutations) {
488            mutations.forEach(function(m) {
489                // Clean up removed menus
490                m.removedNodes.forEach(function(node) {
491                    if (node._asm_scroll_cleanup) node._asm_scroll_cleanup();
492                });
493                m.addedNodes.forEach(function(node) {
494                    if (node.nodeType !== 1) return;
495                    // Check if it's a dropdown menu inside new-user-role or user list role dropdowns
496                    if (node.classList && (node.classList.contains('Select-menu-outer') || node.classList.contains('Select-menu'))) {
497                        const parent = node.closest('#new-user-role, .new-user-role-dropdown, [id^="user-role-"], .user-role-dropdown');
498                        if (parent) fixDropdownMenu(node);
499                    }
500                    // Also check children
501                    node.querySelectorAll && node.querySelectorAll('.Select-menu-outer').forEach(function(menu) {
502                        const parent = menu.closest('#new-user-role, .new-user-role-dropdown, [id^="user-role-"], .user-role-dropdown');
503                        if (parent) fixDropdownMenu(menu);
504                    });
505                });
506            });
507        });
508        obs.observe(adminModal, { childList: true, subtree: true });
509    }
510
511    // Start observing when DOM is ready
512    if (document.readyState === 'loading') {
513        document.addEventListener('DOMContentLoaded', observeDropdowns);
514    } else {
515        observeDropdowns();
516    }
517    // PERF FIX: Replaced 2000ms setInterval with MutationObserver.
518    // Watch for admin-modal being added to DOM instead of polling.
519    if (!window._asm_dropdown_obs_started) {
520        var _adminModalWatcher = new MutationObserver(function() {
521            if (!window._asm_dropdown_obs_started) {
522                var modal = document.getElementById('admin-modal');
523                if (modal) {
524                    observeDropdowns();
525                    window._asm_dropdown_obs_started = true;
526                    _adminModalWatcher.disconnect();
527                }
528            }
529        });
530        _adminModalWatcher.observe(document.body, { childList: true, subtree: true });
531    }
532})();
533
534/* ══════════════════════════════════════════════════════════════════════
535   FIX #2: Auto-clear session-details-msg after 4s
536   MutationObserver detects when message changes, resets CSS animation
537   so it works every time (not just first time).
538   ══════════════════════════════════════════════════════════════════════ */
539(function() {
540    var _msgTimer = null;
541    function watchSessionMsg() {
542        var msgEl = document.getElementById('session-details-msg');
543        if (!msgEl) return;
544        var obs = new MutationObserver(function() {
545            if (!msgEl.textContent.trim() && !msgEl.innerHTML.trim()) return;
546            // Reset animation by cloning
547            msgEl.style.animation = 'none';
548            msgEl.offsetHeight; // reflow
549            msgEl.style.animation = '';
550            // Also hard-clear after 4s to ensure it's gone
551            clearTimeout(_msgTimer);
552            _msgTimer = setTimeout(function() {
553                msgEl.style.opacity = '0';
554                setTimeout(function() {
555                    // Actually clear via Dash would require a callback.
556                    // Just hide it visually — Dash will set to "" on next force logout click
557                    msgEl.innerHTML = '';
558                    msgEl.style.opacity = '';
559                }, 300);
560            }, 4000);
561        });
562        obs.observe(msgEl, { childList: true, subtree: true, characterData: true });
563    }
564    // Start watching when DOM ready
565    if (document.readyState === 'loading') {
566        document.addEventListener('DOMContentLoaded', watchSessionMsg);
567    } else {
568        watchSessionMsg();
569    }
570    // PERF FIX: Replaced 2000ms setInterval with MutationObserver.
571    // Watch for session-details-msg being added to DOM instead of polling.
572    var _sessionMsgWatcher = new MutationObserver(function() {
573        var msgEl = document.getElementById('session-details-msg');
574        if (msgEl && !msgEl._asm_msg_watched) {
575            msgEl._asm_msg_watched = true;
576            watchSessionMsg();
577        }
578    });
579    _sessionMsgWatcher.observe(document.body, { childList: true, subtree: true });
580})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.