PageSourceSearch

http://voronezhfm.ru/js/plugins/Scanner/scanner.js

js voronezhfm.ru collected 2026-10-08 01:00:53 UTC 76,537 bytes, 1,604 lines download raw bytes

1(() => {
2///////////////////////////////////////////////////////////////
3///                                                         ///
4///  SCANNER CLIENT SCRIPT FOR FM-DX-WEBSERVER (V4.3)      ///
5///                                                         ///
6///  by Highpoint               last update: 20.04.2026     ///
7///  powered by PE5PVB                                      ///
8///                                                         ///
9///  https://github.com/Highpoint2000/webserver-scanner     ///
10///                                                         ///
11///////////////////////////////////////////////////////////////
12
13    const pluginSetupOnlyNotify = true;
14    const CHECK_FOR_UPDATES = true;
15
16///////////////////////////////////////////////////////////////
17
18    const pluginVersion = '4.3';
19    const pluginName         = "Scanner";
20    const pluginHomepageUrl  = "https://github.com/Highpoint2000/webserver-scanner/releases";
21    const pluginUpdateUrl    = "https://raw.githubusercontent.com/Highpoint2000/webserver-scanner/refs/heads/main/plugins/Scanner/scanner.js";
22
23    let AvailableScannerConfigs = ["default"];
24    let ActiveScannerConfig = 'default';
25    const EnableBlacklist = true; // auto from config
26    const EnableWhitelist = true; // auto from config
27    const EnableSpectrumScan = true; // auto from config
28    const EnableSpectrumScanBL = true; // auto from config
29    const EnableDifferenceScan = true; // auto from config
30    const EnableDifferenceScanBL = true; // auto from config
31
32    let SignalStrengthUnit = 'dBµV'; // initial, will be updated from UI
33
34    const currentURL   = new URL(window.location.href);
35    const WebserverURL = currentURL.hostname;
36    const WebserverPath = currentURL.pathname.replace(/setup/g, '');
37    const WebserverPORT = currentURL.port || (currentURL.protocol === 'https:' ? '443' : '80');
38    const protocol     = currentURL.protocol === 'https:' ? 'wss:' : 'ws:';
39    const WEBSOCKET_URL = `${protocol}//${WebserverURL}:${WebserverPORT}${WebserverPath}data_plugins`;
40    const target       = '127.0.0.1';
41
42    let wsSendSocket;
43    let clientIp               = '';
44    let isTuneAuthenticated    = false;
45    let isTunerLocked          = false;
46    let isTuningAllowed        = true;
47    let scannerButtonsExecuted = false;
48    let Scan = 'off';
49    let ScanPE5PVBstatus       = '';
50    let SpectrumLimiterValueStatus;
51    let ScannerModeStatus;
52
53    // Remember last internal values from server
54    let lastInternalSensitivity = null;
55    let lastScannerModeValue    = null;
56    let lastScanHoldTimeValue   = null;
57
58    ///////////////////////////////////////////////////////////
59    // Helper: map UI label to internal SignalStrengthUnit
60    ///////////////////////////////////////////////////////////
61    function mapUnitLabelToInternal(label) {
62        const txt = (label || '').toLowerCase();
63        if (txt.includes('dbf'))  return 'dBf';
64        if (txt.includes('dbuv')) return 'dBµV';  // canonical
65        if (txt.includes('dbm'))  return 'dBm';
66        return SignalStrengthUnit; // fallback: keep previous
67    }
68
69    ///////////////////////////////////////////////////////////
70    // Create a status message object
71    ///////////////////////////////////////////////////////////
72    function createMessage(status, Scan = '', Search = '', Sensitivity = '', ScannerMode = '', ScanHoldTime = '') {
73        return {
74            type: 'Scanner',
75            value: {
76                status,
77                Scan,
78                Search,
79                Sensitivity,
80                ScannerMode,
81                ScanHoldTime,
82            },
83            source: clientIp,
84            target: target
85        };
86    }
87
88    ///////////////////////////////////////////////////////////
89    // Update-check for /setup
90    ///////////////////////////////////////////////////////////
91    function checkUpdate(setupOnly, pluginName, urlUpdateLink, urlFetchLink) {
92        if (setupOnly && window.location.pathname !== '/setup') return;
93
94        let pluginVersionCheck =
95            typeof pluginVersion     !== 'undefined' ? pluginVersion :
96            typeof plugin_version    !== 'undefined' ? plugin_version :
97            typeof PLUGIN_VERSION    !== 'undefined' ? PLUGIN_VERSION :
98            'Unknown';
99
100        async function fetchFirstLine() {
101            const urlCheckForUpdate = urlFetchLink;
102
103            try {
104                const response = await fetch(urlCheckForUpdate);
105                if (!response.ok) {
106                    throw new Error(`[${pluginName}] update check HTTP error! status: ${response.status}`);
107                }
108
109                const text  = await response.text();
110                const lines = text.split('\n');
111
112                let version;
113
114                // Robuster: RegEx mit \s* statt exaktem " ="
115                const versionRegex = /const\s+(?:pluginVersion|plugin_version|PLUGIN_VERSION)\s*=\s*['"]([^'"]+)['"]/;
116
117                const versionLine = lines.find(line => versionRegex.test(line));
118                if (versionLine) {
119                    const match = versionLine.match(versionRegex);
120                    if (match) {
121                        version = match[1];
122                    }
123                }
124
125                if (!version) {
126                    const firstLine = lines[0].trim();
127                    version = /^\d/.test(firstLine) ? firstLine : "Unknown";
128                }
129
130                return version;
131            } catch (error) {
132                console.error(`[${pluginName}] error fetching file:`, error);
133                return null;
134            }
135        }
136
137
138        fetchFirstLine().then(newVersion => {
139            if (newVersion && newVersion !== pluginVersionCheck) {
140                const updateConsoleText = "There is a new version of this plugin available";
141                console.log(`[${pluginName}] ${updateConsoleText}`);
142                setupNotify(pluginVersionCheck, newVersion, pluginName, urlUpdateLink);
143            }
144        });
145
146        function setupNotify(pluginVersionCheck, newVersion, pluginName, urlUpdateLink) {
147            if (window.location.pathname === '/setup') {
148                const pluginSettings = document.getElementById('plugin-settings');
149                if (pluginSettings) {
150                    const currentText = pluginSettings.textContent.trim();
151                    const newText = `<a href="${urlUpdateLink}" target="_blank">[${pluginName}] Update available: ${pluginVersionCheck} --> ${newVersion}</a><br>`;
152
153                    if (currentText === 'No plugin settings are available.') {
154                        pluginSettings.innerHTML = newText;
155                    } else {
156                        pluginSettings.innerHTML += ' ' + newText;
157                    }
158                }
159
160                const updateIcon =
161                    document.querySelector('.wrapper-outer #navigation .sidenav-content .fa-puzzle-piece') ||
162                    document.querySelector('.wrapper-outer .sidenav-content') ||
163                    document.querySelector('.sidenav-content');
164
165                const redDot = document.createElement('span');
166                redDot.style.display = 'block';
167                redDot.style.width = '12px';
168                redDot.style.height = '12px';
169                redDot.style.borderRadius = '50%';
170                redDot.style.backgroundColor = '#FE0830' || 'var(--color-main-bright)';
171                redDot.style.marginLeft = '82px';
172                redDot.style.marginTop  = '-12px';
173
174                if (updateIcon) {
175                    updateIcon.appendChild(redDot);
176                }
177            }
178        }
179    }
180
181    if (CHECK_FOR_UPDATES) {
182        checkUpdate(pluginSetupOnlyNotify, pluginName, pluginHomepageUrl, pluginUpdateUrl);
183    }
184
185    ///////////////////////////////////////////////////////////
186    // Initial WebSocket request
187    ///////////////////////////////////////////////////////////
188    async function sendInitialWebSocketMessage() {
189        try {
190            const response = await fetch('https://icanhazip.com');
191            clientIp = (await response.text()).trim();
192            console.log("Public IP address:", clientIp);
193
194            const initialMessage = createMessage('request');
195
196            if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) {
197                wsSendSocket.send(JSON.stringify(initialMessage));
198                console.log("Scanner sent initial message:", initialMessage);
199            } else {
200                console.error("Scanner Error! WebSocket is not open. Cannot send initial message.");
201                sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send initial message.', false, false);
202            }
203        } catch (error) {
204            console.error("Error fetching public IP address:", error);
205            sendToast('error important', 'Scanner', 'Error fetching public IP address.', false, false);
206        }
207    }
208
209    ///////////////////////////////////////////////////////////
210    // Commands over WebSocket
211    ///////////////////////////////////////////////////////////
212    async function sendSearch(SearchFunction) {
213        try {
214            const searchMessage = createMessage('command', '', SearchFunction);
215            if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) {
216                wsSendSocket.send(JSON.stringify(searchMessage));
217                console.log("Search message sent:", searchMessage);
218            } else {
219                console.error("Scanner Error! WebSocket is not open. Cannot send search message.");
220                sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false);
221            }
222        } catch (error) {
223            console.error(error);
224        }
225    }
226
227    async function sendScan(ScanFunction) {
228        try {
229            const scanMessage = createMessage('command', ScanFunction);
230            if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) {
231                wsSendSocket.send(JSON.stringify(scanMessage));
232                console.log("Scanner sent message:", scanMessage);
233            } else {
234                console.error("Scanner Error! WebSocket is not open. Cannot send scan message.");
235                sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false);
236            }
237        } catch (error) {
238            console.error(error);
239        }
240    }
241
242    async function SendValue(Sensitivity, ScannerMode, ScanHoldTime) {
243        const ssu = (SignalStrengthUnit || '').toLowerCase();
244        let resultSensitivity;
245
246        if (ssu === 'dbuv' || ssu === 'dbµv' || ssu === 'dbμv') {
247            resultSensitivity = Math.round(parseFloat(Sensitivity) + 10.875);
248        } else if (ssu === 'dbm') {
249            resultSensitivity = Math.round(parseFloat(Sensitivity) + 119.75);
250        } else if (ssu === 'dbf') {
251            resultSensitivity = Math.round(parseFloat(Sensitivity));
252        } else {
253            resultSensitivity = Math.round(parseFloat(Sensitivity));
254        }
255
256        try {
257            const valueMessage = createMessage('command', '', '', resultSensitivity, ScannerMode, ScanHoldTime);
258            if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) {
259                wsSendSocket.send(JSON.stringify(valueMessage));
260                console.log("Value message sent:", valueMessage);
261            } else {
262                console.error("Scanner Error! WebSocket is not open. Cannot send value message.");
263                sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false);
264            }
265        } catch (error) {
266            console.error(error);
267        }
268    }
269
270    ///////////////////////////////////////////////////////////
271    // WebSocket connection setup
272    ///////////////////////////////////////////////////////////
273    async function setupSendSocket() {
274        if (!wsSendSocket || wsSendSocket.readyState === WebSocket.CLOSED) {
275            try {
276                wsSendSocket = new WebSocket(WEBSOCKET_URL);
277
278                wsSendSocket.onopen = () => {
279                    console.log("Scanner connected WebSocket");
280                    sendInitialWebSocketMessage();
281                };
282
283                wsSendSocket.onmessage = handleWebSocketMessage;
284
285                wsSendSocket.onerror = (error) => {
286                    console.error("Scanner Websocket Error:", error);
287                    sendToast('error important', 'Scanner', 'Websocket Error', false, false);
288                };
289
290                wsSendSocket.onclose = (event) => {
291                    console.log("Scanner Error! Websocket closed or not open:", event);
292                    setTimeout(setupSendSocket, 5000);
293                };
294            } catch (error) {
295                console.error("Failed to setup Send WebSocket:", error);
296                sendToast('error important', 'Scanner', 'Failed to setup Send WebSocket', false, false);
297                setTimeout(setupSendSocket, 5000);
298            }
299        }
300    }
301
302    ///////////////////////////////////////////////////////////
303    // Sensitivity normalization (internal → UI)
304    ///////////////////////////////////////////////////////////
305    function normalizeSensitivity(Sensitivity, unitStr) {
306        const ssu = (unitStr || '').toLowerCase();
307        let result;
308
309        if (ssu === 'dbuv' || ssu === 'dbµv' || ssu === 'dbμv') {
310            result = parseFloat(Sensitivity) - 10.875;
311        } else if (ssu === 'dbm') {
312            result = parseFloat(Sensitivity) - 119.75;
313        } else if (ssu === 'dbf') {
314            result = parseFloat(Sensitivity);
315        } else {
316            result = parseFloat(Sensitivity);
317        }
318
319        return Math.round(result);
320    }
321
322    ///////////////////////////////////////////////////////////
323    // Toast with cooldown
324    ///////////////////////////////////////////////////////////
325    let lastToastTime = 0;
326
327    function sendToastWithCooldown(type, title, message, autoClose = true, closeOnClick = true) {
328        const currentTime = Date.now();
329        if (currentTime - lastToastTime < 150) {
330            return;
331        }
332        lastToastTime = currentTime;
333        sendToast(type, title, message, autoClose, closeOnClick);
334    }
335
336    ///////////////////////////////////////////////////////////
337    // WebSocket message handler
338    ///////////////////////////////////////////////////////////
339    function handleWebSocketMessage(event) {
340        try {
341            const eventData = JSON.parse(event.data);
342
343            if (eventData.source !== clientIp) {
344                // console.log(eventData);
345            }
346
347            if (eventData.type === 'Scanner') {
348                const {
349                    status,
350                    ScanPE5PVB,
351                    SearchPE5PVB,
352                    Scan,
353                    Sensitivity,
354                    ScannerMode,
355                    ScanHoldTime,
356                    SpectrumLimiterValue,
357                    StatusFMLIST,
358                    InfoFMLIST,
359                    AvailableConfigs,
360                    ActiveConfig
361                } = eventData.value;
362
363                console.log(eventData.value);
364
365                if (typeof ScanPE5PVB !== 'undefined') {
366                    ScanPE5PVBstatus = ScanPE5PVB;
367                }
368
369                // Update Dynamic Configuration lists if provided by WebSocket
370                let configsChanged = false;
371                if (AvailableConfigs !== undefined) {
372                    if (JSON.stringify(AvailableConfigs) !== JSON.stringify(AvailableScannerConfigs)) {
373                        AvailableScannerConfigs = AvailableConfigs;
374                        configsChanged = true;
375                    }
376                }
377                if (ActiveConfig !== undefined) {
378                    if (ActiveConfig !== ActiveScannerConfig) {
379                        ActiveScannerConfig = ActiveConfig;
380                        configsChanged = true;
381                    }
382                }
383
384                if (configsChanged && isTuneAuthenticated) {
385                    updateAdminScannerDropdownUI();
386                }
387
388                // IMPORTANT: Only store valid (non-empty) values for later re-render to prevent NaN issues
389                if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') {
390                    lastInternalSensitivity = Sensitivity;
391                }
392                if (ScannerMode !== undefined && ScannerMode !== null && ScannerMode !== '') {
393                    lastScannerModeValue = ScannerMode;
394                }
395                if (ScanHoldTime !== undefined && ScanHoldTime !== null && ScanHoldTime !== '') {
396                    lastScanHoldTimeValue = ScanHoldTime;
397                }
398
399                if (status === 'response') {
400                    if (!scannerButtonsExecuted) {
401                        ScannerButtons(Sensitivity, ScannerMode, ScanHoldTime);
402                        SearchButtons();
403                        scannerButtonsExecuted = true;
404
405                        if (isTuneAuthenticated) {
406                            if (
407                                ScannerMode === 'spectrum'   ||
408                                ScannerMode === 'spectrumBL' ||
409                                ScannerMode === 'difference' ||
410                                ScannerMode === 'differenceBL'
411                            ) {
412                                sendToastWithCooldown(
413                                    'info',
414                                    'Scanner',
415                                    `Settings activated! PE5PVB Scan: ${ScanPE5PVB} | PE5PVB Search: ${SearchPE5PVB} | Autoscan: ${Scan} | Sensitivity: ${Sensitivity} | Limit: ${SpectrumLimiterValue} | Scanmode: ${ScannerMode} | Scanholdtime: ${ScanHoldTime}`,
416                                    false,
417                                    false
418                                );
419                                if (SpectrumLimiterValue !== 'undefined' && SpectrumLimiterValue !== '') {
420                                    SpectrumLimiterValueStatus = SpectrumLimiterValue;
421                                    ScannerModeStatus = `${ScannerMode}`;
422                                }
423                            } else {
424                                sendToastWithCooldown(
425                                    'info',
426                                    'Scanner',
427                                    `Settings activated! PE5PVB Scan: ${ScanPE5PVB} | PE5PVB Search: ${SearchPE5PVB} | Autoscan: ${Scan} | Sensitivity: ${Sensitivity} | Scanmode: ${ScannerMode} | Scanholdtime: ${ScanHoldTime}`,
428                                    false,
429                                    false
430                                );
431                            }
432                        }
433                    }
434
435                    // Protect normalizeSensitivity from processing undefined or empty strings
436                    if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') {
437                        const normSens = normalizeSensitivity(Sensitivity, SignalStrengthUnit);
438                        updateDropdownValues(normSens, ScannerMode, ScanHoldTime);
439                    }
440
441                } else if (
442                    status === 'broadcast' &&
443                    InfoFMLIST &&
444                    InfoFMLIST.includes("successful")
445                ) {
446                    sendToastWithCooldown('success important', 'Scanner', `${InfoFMLIST}`, false, false);
447                    sendInitialWebSocketMessage();
448
449                } else if (
450                    status === 'broadcast' &&
451                    InfoFMLIST &&
452                    InfoFMLIST.includes("failed")
453                ) {
454                    sendToastWithCooldown('error important', 'Scanner', `${InfoFMLIST}`, false, false);
455                    sendInitialWebSocketMessage();
456
457                } else if (status === 'broadcast' || status === 'send') {
458                    if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') {
459                        const normSens = normalizeSensitivity(Sensitivity, SignalStrengthUnit);
460                        updateDropdownValues(normSens, ScannerMode, ScanHoldTime);
461                    }
462                }
463
464                // Scan button behaviour (UI only)
465                const ScanButton        = document.getElementById('Scan-on-off');
466                const blinkTextElement  = document.querySelector('#tune-buttons .autoscan-blink');
467                const scannerControls   = document.getElementById('scanner-control
467s');
468                const HideElement       = document.querySelector('.panel-33.hide-phone.no-bg');
469                const volumeSlider      = document.getElementById('volumeSlider');
470                const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null;
471
472                if (ScanButton) {
473                    if (Scan === 'off') {
474                        const element = document.getElementById('log-fmlist');
475                        if (element) element.style.display = 'block';
476
477                        ScanButton.setAttribute('data-scan-status', 'off');
478                        ScanButton.classList.add('bg-color-3');
479                        ScanButton.classList.remove('bg-color-4');
480
481                        const freqDownElement = document.getElementById('freq-down');
482                        if (freqDownElement) freqDownElement.style.display = 'block';
483
484                        const freqUpElement = document.getElementById('freq-up');
485                        if (freqUpElement) freqUpElement.style.display = 'block';
486
487                        const searchDownElement = document.getElementById('search-down');
488                        if (searchDownElement) searchDownElement.style.display = 'block';
489
490                        const searchUpElement = document.getElementById('search-up');
491                        if (searchUpElement) searchUpElement.style.display = 'block';
492
493                        const commandInputElement = document.getElementById('commandinput');
494                        if (commandInputElement) commandInputElement.style.display = 'block';
495
496                        if (blinkTextElement) blinkTextElement.style.display = 'none';
497
498                        if (window.innerWidth < 769) {
499                            if (volumeSliderParent) volumeSliderParent.style.display = 'none';
500                            if (scannerControls)    scannerControls.style.display    = 'none';
501                            if (HideElement)        HideElement.classList.add('hide-phone');
502                        }
503
504                    } else if (Scan === 'on') {
505                        const element = document.getElementById('log-fmlist');
506                        if (element) element.style.display = 'none';
507
508                        ScanButton.setAttribute('data-scan-status', 'on');
509                        ScanButton.classList.add('bg-color-4');
510                        ScanButton.classList.remove('bg-color-3');
511
512                        const freqDownElement = document.getElementById('freq-down');
513                        if (freqDownElement) freqDownElement.style.display = 'none';
514
515                        const freqUpElement = document.getElementById('freq-up');
516                        if (freqUpElement) freqUpElement.style.display = 'none';
517
518                        const searchDownElement = document.getElementById('search-down');
519                        if (searchDownElement) searchDownElement.style.display = 'none';
520
521                        const searchUpElement = document.getElementById('search-up');
522                        if (searchUpElement) searchUpElement.style.display = 'none';
523
524                        const commandInputElement = document.getElementById('commandinput');
525                        if (commandInputElement) commandInputElement.style.display = 'none';
526
527                        if (blinkTextElement) blinkTextElement.style.display = 'block';
528
529                        if (window.innerWidth < 769) {
530                            if (volumeSliderParent) volumeSliderParent.style.display = 'none';
531                            if (scannerControls)    scannerControls.style.display    = 'flex';
532                            if (HideElement)        HideElement.classList.remove('hide-phone');
533                        }
534                    }
535                }
536            }
537        } catch (error) {
538            console.error("Error handling WebSocket message:", error);
539        }
540    }
541
542    ///////////////////////////////////////////////////////////
543    // Update dropdown values (display)
544    ///////////////////////////////////////////////////////////
545    function updateDropdownValues(Sensitivity, ScannerMode, ScanHoldTime) {
546        if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '' && !Number.isNaN(Sensitivity)) {
547            const sensitivityInput = document.querySelector('input[title="Scanner Sensitivity"]');
548            if (sensitivityInput) {
549                if (ScanPE5PVBstatus) {
550                    sensitivityInput.value = `${Sensitivity}`;
551                } else {
552                    sensitivityInput.value = `${Sensitivity} ${SignalStrengthUnit}`;
553                }
554                sensitivityInput.setAttribute('data-value', Sensitivity);
555            }
556        }
557
558        if (ScannerMode !== undefined && ScannerMode !== null && ScannerMode !== '') {
559            const modeInput = document.querySelector('input[title="Scanner Mode"]');
560            if (modeInput) {
561                modeInput.value = `${ScannerMode}`;
562                modeInput.setAttribute('data-value', ScannerMode);
563            }
564        }
565
566        if (ScanHoldTime !== undefined && ScanHoldTime !== null && ScanHoldTime !== '') {
567            const holdTimeInput = document.querySelector('input[title="Scanhold Time"]');
568            if (holdTimeInput) {
569                holdTimeInput.value = `${ScanHoldTime} sec.`;
570                holdTimeInput.setAttribute('data-value', ScanHoldTime);
571            }
572        }
573    }
574
575    ///////////////////////////////////////////////////////////
576    // Search buttons
577    ///////////////////////////////////////////////////////////
578    function SearchButtons() {
579        const searchDownButton = document.createElement('button');
580        searchDownButton.id = 'search-down';
581        searchDownButton.setAttribute('aria-label', 'Scan Down');
582        searchDownButton.classList.add('rectangular-downbutton');
583        searchDownButton.innerHTML = '<i class="fa-solid fa-chevron-left"></i><i class="fa-solid fa-chevron-left"></i>';
584
585        const searchUpButton = document.createElement('button');
586        searchUpButton.id = 'search-up';
587        searchUpButton.setAttribute('aria-label', 'Scan Up');
588        searchUpButton.classList.add('rectangular-upbutton');
589        searchUpButton.innerHTML = '<i class="fa-solid fa-chevron-right"></i><i class="fa-solid fa-chevron-right"></i>';
590
591        const rectangularButtonStyle = `
592            .rectangular-downbutton {
593                border: 3px solid #ccc;
594                border-radius: 0px;
595                padding: 5px 10px;
596                background-color: #fff;
597                color: #333;
598                cursor: pointer;
599                transition: background-color 0.3s, color 0.3s, border-color 0.3s;
600                margin-left: 1px;
601            }
602
603            .rectangular-upbutton {
604                border: 3px solid #ccc;
605                border-radius: 0px;
606                padding: 5px 10px;
607                background-color: #fff;
608                color: #333;
609                cursor: pointer;
610                transition: background-color 0.3s, color 0.3s, border-color 0.3s;
611                margin-right: 1px;
612            }
613
614            .rectangular-button:hover {
615                background-color: #f0f0f0;
616                border-color: #aaa;
617            }
618        `;
619
620        const styleElement = document.createElement('style');
621        styleElement.innerHTML = rectangularButtonStyle;
622        document.head.appendChild(styleElement);
623
624        const freqDownButton = document.getElementById('freq-down');
625        if (freqDownButton && freqDownButton.parentNode) {
626            freqDownButton.parentNode.insertBefore(searchDownButton, freqDownButton.nextSibling);
627        }
628
629        const freqUpButton = document.getElementById('freq-up');
630        if (freqUpButton && freqUpButton.parentNode) {
631            freqUpButton.parentNode.insertBefore(searchUpButton, freqUpButton);
632        }
633
634        searchDownButton.addEventListener('click', function () {
635            if (!isTuningAllowed) {
636                sendToast('warning', 'Scanner', 'Tuner is currently locked to admin!', false, false);
637                return;
638            }
639            sendSearch('down');
640        });
641
642        searchUpButton.addEventListener('click', function () {
643            if (!isTuningAllowed) {
644                sendToast('warning', 'Scanner', 'Tuner is currently locked to admin!', false, false);
645                return;
646            }
647            sendSearch('up');
648        });
649    }
650
651    ///////////////////////////////////////////////////////////
652    // Autoscan blinking label
653    ///////////////////////////////////////////////////////////
654    function BlinkAutoScan() {
655        console.log('BlinkAutoScan started');
656
657        const parentElement = document.getElementById('tune-buttons');
658        if (parentElement) {
659            parentElement.classList.remove('no-bg');
660            parentElement.style.position = 'relative';
661
662            let blinkContainer = parentElement.querySelector('.autoscan-blink-container');
663            if (!blinkContainer) {
664                blinkContainer = document.createElement('div');
665                blinkContainer.className = 'autoscan-blink-container';
666                blinkContainer.style.position = 'absolute';
667                blinkContainer.style.top = '0';
668                blinkContainer.style.left = '0';
669                blinkContainer.style.width = '100%';
670                blinkContainer.style.height = '100%';
671                blinkContainer.style.pointerEvents = 'none';
672                blinkContainer.style.display = 'none';
673                parentElement.appendChild(blinkContainer);
674            }
675
676            const blinkText = document.createElement('span');
677            blinkText.textContent = 'Autoscan active!';
678            blinkText.classList.add('autoscan-blink');
679            blinkText.style.display = 'none';
680            blinkContainer.appendChild(blinkText);
681
682            const style = document.createElement('style');
683            style.textContent = `
684                .autoscan-blink {
685                    font-size: 32px;
686                    font-weight: bold;
687                    font-family: "Titillium Web", Calibri, sans-serif;
688                    color: red;
689                    animation: autoscan-blink-animation 1s step-start infinite;
690                }
691                @keyframes autoscan-blink-animation {
692                    50% { opacity: 0; }
693                }
694            `;
695            document.head.appendChild(style);
696
697            setTimeout(() => {
698                blinkContainer.style.display = 'block';
699            }, 100);
700        } else {
701            console.warn('Element with the ID "tune-buttons" not found.');
702        }
703    }
704
705    ///////////////////////////////////////////////////////////
706    // Cookie helpers
707    ///////////////////////////////////////////////////////////
708    function setCookie(name, value, days) {
709        const expires = new Date(Date.now() + days * 864e5).toUTCString();
710        document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/';
711    }
712
713    function getCookie(name) {
714        return document.cookie.split('; ').reduce((r, v) => {
715            const parts = v.split('=');
716            return parts[0] === name ? decodeURIComponent(parts[1]) : r;
717        }, '');
718    }
719
720    function eraseCookie(name) {
721        setCookie(name, '', -1);
722    }
723
724    ///////////////////////////////////////////////////////////
725    // Scanner main button
726    ///////////////////////////////////////////////////////////
727    function ScannerButtons(Sensitivity, ScannerMode, ScanHoldTime) {
728        const ScannerButton = document.createElement('button');
729        ScannerButton.id = 'Scan-on-off';
730        ScannerButton.setAttribute('aria-label', 'Scan');
731        ScannerButton.setAttribute('data-scan-status', 'off');
732        ScannerButton.style.borderRadius = '0px';
733        ScannerButton.style.position = 'relative';
734        ScannerButton.style.top = '0px';
735
736        if (Scan === 'off') {
737            ScannerButton.classList.add('bg-color-3');
738        }
739
740        ScannerButton.title = `Plugin Version ${pluginVersion}`;
741
742        const spans = document.querySelectorAll('span.text-small.color-4');
743        let hasSDR = false;
744        spans.forEach(span => {
745            if (span.textContent.trim() === 'SDR') {
746                hasSDR = true;
747            }
748        });
749
750        if (hasSDR) {
751            ScannerButton.style.borderRadius = '15px';
752            ScannerButton.style.width = '300%';
753            ScannerButton.style.right = '100%';
754        } else {
755            ScannerButton.style.marginLeft = '1px';
756            ScannerButton.style.marginRight = '1px';
757
758            if (window.innerWidth < 769) {
759                ScannerButton.style.width  = '100%';
760                ScannerButton.style.height = '48px';
761                ScannerButton.innerHTML = '<strong>Autoscan</strong>';
762                ScannerButton.style.borderRadius = '15px';
763            } else if (window.innerWidth < 890) {
764                ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
765                ScannerButton.style.borderRadius = '0px';
766                ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.0%' : '97.5%';
767            } else if (window.innerWidth < 990) {
768                ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
769                ScannerButton.style.borderRadius = '0px';
770                ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.4%' : '97.7%';
771            } else if (window.innerWidth < 1180) {
772                ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
773                ScannerButton.style.borderRadius = '0px';
774                ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '98.5%';
775            } else {
776                ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
777                ScannerButton.style.borderRadius = '0px';
778                ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '98.5%';
779            }
780
781            const el = document.getElementById('data-ant');
782            if (el) {
783                if (window.innerWidth < 769) {
784                    ScannerButton.style.width  = '100%';
785                    ScannerButton.style.height = '48px';
786                    ScannerButton.innerHTML = '<strong>Autoscan</strong>';
787                    ScannerButton.style.borderRadius = '15px';
788                } else if (window.innerWidth < 890) {
789                    ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
790                    ScannerButton.style.borderRadius = '0px';
791                    ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '96.5%' : '95.0%';
792                } else if (window.innerWidth < 990) {
793                    ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
794                    ScannerButton.style.borderRadius = '0px';
795                    ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.4%' : '97.0%';
796                } else if (window.innerWidth < 1180) {
797                    ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
798                    ScannerButton.style.borderRadius = '0px';
799                    ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.
7990%' : '97.4%';
800                } else {
801                    ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>';
802                    ScannerButton.style.borderRadius = '0px';
803                    ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '97.5%';
804                }
805            }
806        }
807
808        let pressTimer;
809        let isLongPress = false;
810
811        ScannerButton.addEventListener('click', function () {
812            const isActive = ScannerButton.getAttribute('data-scan-status') === 'on';
813            if (!isLongPress) {
814                if (isActive) {
815                    ScannerButton.setAttribute('data-scan-status', 'off');
816                    ScannerButton.style.backgroundColor = 'var(--color-3)';
817                } else {
818                    ScannerButton.setAttribute('data-scan-status', 'on');
819                    ScannerButton.style.backgroundColor = 'var(--color-4)';
820                }
821            }
822        });
823
824        if (window.innerWidth < 769 && ScannerButton) {
825            const popupContent = document.querySelector('.popup-content');
826            if (popupContent) {
827                const allParagraphs = popupContent.querySelectorAll('p.flex-phone.flex-center');
828                let targetP = null;
829
830                allParagraphs.forEach(p => {
831                    const text = p.textContent.trim();
832                    if (text === 'Bandwidth & Antennas' && !targetP) {
833                        targetP = p;
834                    } else if (text === 'Filters' && !targetP) {
835                        targetP = p;
836                    }
837                });
838
839                if (targetP && !document.getElementById('scanner-button-wrapper')) {
840                    const scannerLabel = document.createElement('p');
841                    scannerLabel.className = 'flex-phone flex-center';
842                    scannerLabel.textContent = 'Scanner';
843
844                    const scannerContainer = document.createElement('div');
845                    scannerContainer.className = 'flex-container flex-phone flex-center';
846                    scannerContainer.id = 'scanner-button-wrapper';
847                    scannerContainer.style.marginBottom = '10px';
848
849                    scannerContainer.appendChild(ScannerButton);
850
851                    popupContent.insertBefore(scannerLabel, targetP);
852                    popupContent.insertBefore(scannerContainer, scannerLabel.nextSibling);
853                }
854            }
855        } else {
856            const buttonIms = document.querySelector('.button-ims');
857            if (buttonIms && ScannerButton) {
858                const newDiv = document.createElement('div');
859                newDiv.className = 'panel-50 no-bg br-0 h-100 m-0';
860                newDiv.id = 'ScannerButtonWrapper';
861                newDiv.appendChild(ScannerButton);
862                buttonIms.parentNode.insertBefore(newDiv, buttonIms);
863            }
864        }
865
866        function toggleScan(isLongPressAction) {
867            const ScanButton = document.getElementById('Scan-on-off');
868            const isScanOn  = ScanButton.getAttribute('data-scan-status') === 'on';
869
870            const scannerControls = document.getElementById('scanner-controls');
871            const volumeSlider    = document.getElementById('volumeSlider');
872            const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null;
873
874            if (isLongPressAction) {
875                if (isTuneAuthenticated) {
876                    const currentScanStatus = isScanOn;
877
878                    if (scannerControls) {
879                        scannerControls.parentNode.removeChild(scannerControls);
880                        if (volumeSliderParent) {
881                            volumeSliderParent.style.display = 'block';
882                        }
883                        setCookie('scannerControlsStatus', 'off', 7);
884                    } else {
885                        // Priority to the remembered state over closure variables
886                        if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') {
887                            const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit);
888                            createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue);
889                        } else {
890                            const norm = normalizeSensitivity(Sensitivity, SignalStrengthUnit);
891                            createScannerControls(norm, ScannerMode, ScanHoldTime);
892                        }
893                        setCookie('scannerControlsStatus', 'on', 7);
894                    }
895
896                    if (currentScanStatus) {
897                        ScanButton.setAttribute('data-scan-status', 'on');
898                    } else {
899                        ScanButton.setAttribute('data-scan-status', 'off');
900                    }
901
902                } else {
903                    sendToast('warning', 'Scanner', 'Admin must be logged in to use the autoscan mode!', false, false);
904                }
905            } else {
906                if (isTuneAuthenticated) {
907                    if (isScanOn) {
908                        sendScan('off');
909                        setCookie('scannerControlsStatus', 'off', 7);
910                    } else {
911                        sendScan('on');
912                        setCookie('scannerControlsStatus', 'on', 7);
913                    }
914                } else {
915                    sendToast('warning', 'Scanner', 'Admin must be logged in to use the autoscan mode!', false, false);
916                }
917            }
918        }
919
920        function startPressTimer() {
921            isLongPress = false;
922            pressTimer = setTimeout(() => {
923                isLongPress = true;
924                toggleScan(true);
925            }, 1000);
926        }
927
928        function cancelPressTimer() {
929            clearTimeout(pressTimer);
930            if (!isLongPress) {
931                toggleScan(false);
932            }
933        }
934
935        const ScanButton = document.getElementById('Scan-on-off');
936        ScanButton.addEventListener('mousedown', startPressTimer);
937        ScanButton.addEventListener('mouseup',   cancelPressTimer);
938
939        const scannerControlsStatus = getCookie('scannerControlsStatus');
940        if (scannerControlsStatus === 'on' && isTuneAuthenticated) {
941            if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') {
942                const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit);
943                createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue);
944            } else {
945                const norm = normalizeSensitivity(Sensitivity, SignalStrengthUnit);
946                createScannerControls(norm, ScannerMode, ScanHoldTime);
947            }
948        } else {
949            const scannerControls = document.getElementById('scanner-controls');
950            if (scannerControls) {
951                scannerControls.parentNode.removeChild(scannerControls);
952            }
953        }
954    }
955
956    ///////////////////////////////////////////////////////////
957    // Scanner controls
958    ///////////////////////////////////////////////////////////
959    function createScannerControls(Sensitivity, ScannerMode, ScanHoldTime) {
960        const scannerControls = document.createElement('div');
961        scannerControls.className = "no-bg h-100";
962        scannerControls.id = "scanner-controls";
963        scannerControls.style.width = '100%';
964        scannerControls.style.display = 'flex';
965        scannerControls.style.justifyContent = 'space-between';
966        scannerControls.style.marginTop = "0px";
967        scannerControls.style.marginRight = "0px";
968        scannerControls.style.position = 'relative';
969
970        const sensitivityContainer = document.createElement('div');
971        sensitivityContainer.className = "dropdown";
972        sensitivityContainer.style.marginRight = "1px";
973        sensitivityContainer.style.marginLeft = "0px";
974        sensitivityContainer.style.width = "100%";
975        sensitivityContainer.style.height = "99%";
976        sensitivityContainer.style.position = 'relative';
977        sensitivityContainer.style.borderTopLeftRadius     = '15px';
978        sensitivityContainer.style.borderTopRightRadius    = '0px';
979        sensitivityContainer.style.borderBottomLeftRadius  = '15px';
980        sensitivityContainer.style.borderBottomRightRadius = '0px';
981
982        const modeContainer = document.createElement('div');
983        modeContainer.className = "dropdown";
984        modeContainer.style.marginRight = "1px";
985        modeContainer.style.marginLeft  = "0px";
986        modeContainer.style.width = "100%";
987        modeContainer.style.height = "99%";
988        modeContainer.style.position = 'relative';
989        modeContainer.style.borderRadius = '0px';
990
991        let optionsHTML = `
992            <input type="text" placeholder="${ScannerMode}" title="Scanner Mode" readonly>
993            <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
994                <li data-value="normal" class="option">normal</li>
995        `;
996        if (EnableBlacklist)      optionsHTML += `<li data-value="blackl
996ist" class="option">blacklist</li>`;
997        if (EnableWhitelist)      optionsHTML += `<li data-value="whitelist" class="option">whitelist</li>`;
998        if (EnableSpectrumScan)   optionsHTML += `<li data-value="spectrum" class="option">spectrum</li>`;
999        if (EnableSpectrumScanBL) optionsHTML += `<li data-value="spectrumBL" class="option">spectrumBL</li>`;
1000        if (EnableDifferenceScan) optionsHTML += `<li data-value="difference" class="option">difference</li>`;
1001        if (EnableDifferenceScanBL) optionsHTML += `<li data-value="differenceBL" class="option">differenceBL</li>`;
1002        optionsHTML += `</ul>`;
1003        modeContainer.innerHTML = optionsHTML;
1004
1005        const delayContainer = document.createElement('div');
1006        delayContainer.className = "dropdown";
1007        delayContainer.style.marginLeft = "0px";
1008        delayContainer.style.width  = "100%";
1009        delayContainer.style.height = "99%";
1010        delayContainer.style.position = 'relative';
1011        delayContainer.style.borderTopLeftRadius     = '0px';
1012        delayContainer.style.borderTopRightRadius    = '15px';
1013        delayContainer.style.borderBottomLeftRadius  = '0px';
1014        delayContainer.style.borderBottomRightRadius = '15px';
1015
1016        const VolumeSlider = document.getElementById('volumeSlider');
1017        const VolumeSliderWidth = VolumeSlider ? VolumeSlider.clientWidth : 0;
1018
1019        delayContainer.style.marginRight = VolumeSliderWidth > 300 ? "0px" : "5px";
1020
1021        const unit = (SignalStrengthUnit || '').toLowerCase();
1022
1023        if (ScanPE5PVBstatus) {
1024            sensitivityContainer.innerHTML = `
1025                <input type="text" placeholder="Sensitivity" title="Scanner Sensitivity" readonly>
1026                <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
1027                    <li data-value="1"  class="option">1</li>
1028                    <li data-value="2"  class="option">2</li>
1029                    <li data-value="3"  class="option">3</li>
1030                    <li data-value="4"  class="option">4</li>
1031                    <li data-value="5"  class="option">5</li>
1032                    <li data-value="10" class="option">10</li>
1033                    <li data-value="15" class="option">15</li>
1034                    <li data-value="20" class="option">20</li>
1035                    <li data-value="25" class="option">25</li>
1036                    <li data-value="30" class="option">30</li>
1037                </ul>
1038            `;
1039        } else if (unit === 'dbf') {
1040            sensitivityContainer.innerHTML = `
1041                <input type="text" placeholder="${Sensitivity} dBf" title="Scanner Sensitivity" readonly>
1042                <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
1043                    <li data-value="1"  class="option">1 dBf</li>
1044                    <li data-value="2"  class="option">2 dBf</li>
1045                    <li data-value="3"  class="option">3 dBf</li>
1046                    <li data-value="4"  class="option">4 dBf</li>
1047                    <li data-value="5"  class="option">5 dBf</li>
1048                    <li data-value="10" class="option">10 dBf</li>
1049                    <li data-value="15" class="option">15 dBf</li>
1050                    <li data-value="20" class="option">20 dBf</li>
1051                    <li data-value="25" class="option">25 dBf</li>
1052                    <li data-value="30" class="option">30 dBf</li>
1053                    <li data-value="35" class="option">35 dBf</li>
1054                    <li data-value="40" class="option">
105440 dBf</li>
1055                    <li data-value="45" class="option">45 dBf</li>
1056                    <li data-value="50" class="option">50 dBf</li>
1057                    <li data-value="55" class="option">55 dBf</li>
1058                    <li data-value="60" class="option">60 dBf</li>
1059                    <li data-value="65" class="option">65 dBf</li>
1060                    <li data-value="70" class="option">70 dBf</li>
1061                    <li data-value="75" class="option">75 dBf</li>
1062                    <li data-value="80" class="option">80 dBf</li>
1063                </ul>
1064            `;
1065        } else if (unit === 'dbuv' || unit === 'dbµv' || unit === 'dbμv') {
1066            sensitivityContainer.innerHTML = `
1067                <input type="text" placeholder="${Sensitivity} dBuV" title="Scanner Sensitivity" readonly>
1068                <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
1069                    <li data-value="1"  class="option">1 dBuV</li>
1070                    <li data-value="2"  class="option">2 dBuV</li>
1071                    <li data-value="3"  class="option">3 dBuV</li>
1072                    <li data-value="4"  class="option">4 dBuV</li>
1073                    <li data-value="5"  class="option">5 dBuV</li>
1074                    <li data-value="10" class="option">10 dBuV</li>
1075                    <li data-value="15" class="option">15 dBuV</li>
1076                    <li data-value="20" class="option">20 dBuV</li>
1077                    <li data-value="25" class="option">25 dBuV</li>
1078                    <li data-value="30" class="option">30 dBuV</li>
1079                    <li data-value="35" class="option">35 dBuV</li>
1080                    <li data-value="40" class="option">40 dBuV</li>
1081                    <li data-value="45" class="option">45 dBuV</li>
1082                    <li data-value="50" class="option">50 dBuV</li>
1083                    <li data-value="55" class="option">55 dBuV</li>
1084                    <li data-value="60" class="option">60 dBuV</li>
1085                    <li data-value="65" class="option">65 dBuV</li>
1086                    <li data-value="70" class="option">70 dBuV</li>
1087                    <li data-value="75" class="option">75 dBuV</li>
1088                    <li data-value="80" class="option">80 dBuV</li>
1089                </ul>
1090            `;
1091        } else if (unit === 'dbm') {
1092            sensitivityContainer.innerHTML = `
1093                <input type="text" placeholder="${Sensitivity} dBm" title="Scanner Sensitivity" readonly>
1094                <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
1095                    <li data-value="-115" class="option">-115 dBm</li>
1096                    <li data-value="-112" class="option">-112 dBm</li>
1097                    <li data-value="-113" class="option">-113 dBm</li>
1098                    <li data-value="-114" class="option">-114 dBm</li>
1099                    <li data-value="-110" class="option">-110 dBm</li>
1100                    <li data-value="-105" class="option">-105 dBm</li>
1101                    <li data-value="-100" class="option">-100 dBm</li>
1102                    <li data-value="-95"  class="option">-95 dBm</li>
1103                    <li data-value="-90"  class="option">-90 dBm</li>
1104                    <li data-value="-85"  class="option">-85 dBm</li>
1105                    <li data-value="-80"  class="option">-80 dBm</li>
1106                    <li data-value="-75"  class="option">-75 dBm</li>
1107                    <li data-value="-70"  class="option">-70 dBm</li>
1108                    <li data-value="-65"  class="option">-65 dBm</li>
1109                    <li data-value="-60"  class="option">-60 dBm</li>
1110                    <li data-value="-55"  class="option">-55 dBm</li>
1111                    <li data-value="-50"  class="option">-50 dBm</li>
1112                    <li data-value="-45"  class="option">-45 dBm</li>
1113                    <li data-value="-40"  class="option">-40 dBm</li>
1114                </ul>
1115            `;
1116        } else {
1117            sensitivityContainer.innerHTML = `
1118                <input type="text" placeholder="${Sensitivity}" title="Scanner Sensitivity" readonly>
1119            `;
1120        }
1121
1122        delayContainer.innerHTML = `
1123            <input type="text" placeholder="${ScanHoldTime} sec." title="Scanhold Time" readonly>
1124            <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;">
1125                <li data-value="1"  class="option">1 sec.</li>
1126                <li data-value="2"  class="option">2 sec.</li>
1127                <li data-value="3"  class="option">3 sec.</li>
1128                <li data-value="4"  class="option">4 sec.</li>
1129                <li data-value="5"  class="option">5 sec.</li>
1130                <li data-value="7"  class="option">7 sec.</li>
1131                <li data-value="10" class="option">10 sec.</li>
1132                <li data-value="15" class="option">15 sec.</li>
1133                <li data-value="20" class="option">20 sec.</li>
1134                <li data-value="30" class="option">30 sec.</li>
1135            </ul>
1136        `;
1137
1138        scannerControls.appendChild(sensitivityContainer);
1139        initializeDropdown(sensitivityContainer, 'Selected Sensitivity:', 'I', Sensitivity, ScannerMode, ScanHoldTime);
1140
1141        if (!ScanPE5PVBstatus) {
1142            modeContainer.style.display = 'block';
1143            scannerControls.appendChild(modeContainer);
1144            initializeDropdown(modeContainer, 'Selected Mode:', 'M', Sensitivity, ScannerMode, ScanHoldTime);
1145        }
1146
1147        scannerControls.appendChild(delayContainer);
1148        initializeDropdown(delayContainer, 'Selected Delay:', 'K', Sensitivity, ScannerMode, ScanHoldTime);
1149
1150        const volumeSlider = document.getElementById('volumeSlider');
1151        const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null;
1152        if (volumeSliderParent) {
1153            volumeSliderParent.style.display = 'none';
1154            volumeSliderParent.parentNode.insertBefore(scannerControls, volumeSliderParent.
1154nextSibling);
1155        }
1156    }
1157
1158    ///////////////////////////////////////////////////////////
1159    // Dropdown logic
1160    ///////////////////////////////////////////////////////////
1161    function initializeDropdown(container, logPrefix, commandPrefix, Sensitivity, ScannerMode, ScanHoldTime) {
1162        const input    = container.querySelector('input');
1163        const options  = container.querySelectorAll('.option');
1164        const dropdown = container.querySelector('.options');
1165
1166        input.addEventListener('click', () => {
1167            const isOpen = dropdown.style.display === 'block';
1168            closeAllDropdowns();
1169            dropdown.style.display = isOpen ? 'none' : 'block';
1170        });
1171
1172        options.forEach(option => {
1173            option.addEventListener('click', () => {
1174                const value = option.getAttribute('data-value');
1175                input.value = option.textContent.trim();
1176                input.setAttribute('data-value', value);
1177                dropdown.style.display = 'none';
1178
1179                // Hole aktuelle Werte direkt aus dem DOM, um veraltete Closure-Werte (wie den Default) zu vermeiden!
1180                const currentSensInput = document.querySelector('input[title="Scanner Sensitivity"]');
1181                const currentModeInput = document.querySelector('input[title="Scanner Mode"]');
1182                const currentHoldInput = document.querySelector('input[title="Scanhold Time"]');
1183
1184                let currentSens = currentSensInput ? currentSensInput.getAttribute('data-value') : Sensitivity;
1185                let currentMode = currentModeInput ? currentModeInput.getAttribute('data-value') : ScannerMode;
1186                let currentHold = currentHoldInput ? currentHoldInput.getAttribute('data-value') : ScanHoldTime;
1187
1188                if (commandPrefix === 'I') {
1189                    SendValue(value, currentMode, currentHold);
1190                    if (
1191                        SpectrumLimiterValueStatus &&
1192                        value >= SpectrumLimiterValueStatus &&
1193                        (ScannerModeStatus === 'spectrum'   ||
1194                         ScannerModeStatus === 'spectrumBL' ||
1195                         ScannerModeStatus === 'difference' ||
1196                         ScannerModeStatus === 'differenceBL')
1197                    ) {
1198                        sendToast('error important', 'Scanner', `Sensitivity must be smaller than SpectrumLimiter (${SpectrumLimiterValueStatus} ${SignalStrengthUnit})!`, false, false);
1199                    }
1200                }
1201                if (commandPrefix === 'M') {
1202                    SendValue(currentSens, value, currentHold);
1203                    ScannerModeStatus = value;
1204                }
1205                if (commandPrefix === 'K') {
1206                    SendValue(currentSens, currentMode, value);
1207                }
1208            });
1209        });
1210
1211        document.addEventListener('click', (event) => {
1212            if (!container.contains(event.target)) {
1213                dropdown.style.display = 'none';
1214            }
1215        });
1216    }
1217
1218    function closeAllDropdowns() {
1219        const allDropdowns = document.querySelectorAll('.dropdown .options');
1220        allDropdowns.forEach(dropdown => {
1221            dropdown.style.display = 'none';
1222        });
1223    }
1224
1225    ///////////////////////////////////////////////////////////
1226    // Admin / Tune authentication check
1227    ///////////////////////////////////////////////////////////
1228    function checkAdminMode() {
1229        const bodyText = document.body.textContent || document.body.innerText;
1230        const isAdminLoggedIn =
1231            bodyText.includes("You are logged in as an administrator.") ||
1232            bodyText.includes("You are logged in as an adminstrator.");
1233        const canControlReceiver =
1234            bodyText.includes("You are logged in and can control the receiver.");
1235
1236        // Check if tuner is currently locked to admin
1237        isTunerLocked = !!document.querySelector('.fa-solid.fa-key.pointer.tooltip') || !!document.querySelector('.fa-solid.fa-lock.pointer.tooltip');
1238
1239        if (isAdminLoggedIn || canControlReceiver) {
1240            console.log("Admin or Tune mode found. Scanner Plugin Authentication successful.");
1241            isTuneAuthenticated = true;
1242        } else {
1243            console.log("No special authentication message found. Authentication failed.");
1244            isTuneAuthenticated = false;
1245        }
1246
1247        if (!isTunerLocked || isTuneAuthenticated) {
1248            isTuningAllowed = true;
1249        } else {
1250            isTuningAllowed = false;
1251        }
1252
1253        // Initialize the Scanner Option dynamic dropdown if logged in as admin
1254        if (isAdminLoggedIn) {
1255            createAdminScannerDropdown();
1256        }
1257    }
1258
1259    ///////////////////////////////////////////////////////////
1260    // Admin Dynamic Scanner Config Dropdown
1261    ///////////////////////////////////////////////////////////
1262    function createAdminScannerDropdown() {
1263        // Look for the modal content area
1264        const modalContent = document.querySelector('.modal-panel-content');
1265        if (!modalContent) return;
1266
1267        // Search for the specific paragraph confirming admin login
1268        const paragraphs = Array.from(modalContent.querySelectorAll('p.color-3'));
1269        const adminPara = paragraphs.find(p => 
1270            p.textContent.includes("You are logged in as an administrator.") || 
1271            p.textContent.includes("You are logged in as an adminstrator.")
1272        );
1273
1274        if (!adminPara) return; // Admin is not logged in
1275
1276        // If it doesn't exist, create it once
1277        let container = document.getElementById('admin-scanner-dropdown-container');
1278        if (!container) {
1279            container = document.createElement('div');
1280            container.id = 'admin-scanner-dropdown-container';
1281            container.className = 'panel-full flex-center no-bg m-0';
1282            container.style.flexDirection = 'column';
1283            container.style.marginTop = '15px';
1284            container.style.marginBottom = '15px';
1285
1286            container.innerHTML = `
1287                <span class="text-bold" style="color: var(--color-4); text-transform: uppercase; font-size: 13px; margin-bottom: 5px;">Scanner Setting</span>
1288                <div class="dropdown" style="width: 200px; position: relative;">
1289                    <input type="text" id="admin-scanner-dropdown-input" placeholder="Default" readonly tabindex="0" style="cursor: pointer; text-align: left; padding-left: 15px;">
1290                    <ul class="options open-bottom" tabindex="-1" style="display: none; position: absolute; z-index: 100; width: 100%; text-align: left;">
1291                        <!-- Options generated dynamically via updateAdminScannerDropdownUI -->
1292                    </ul>
1293                </div>
1294            `;
1295            // Insert directly above the admin logged-in text
1296            adminPara.parentNode.insertBefore(container, adminPara);
1297
1298            // Add standard Dropdown Toggle behavior
1299            const dropdownEl = container.querySelector('.dropdown');
1300            const inputEl = dropdownEl.querySelector('input');
1301            const optionsList = dropdownEl.querySelector('ul.options');
1302
1303            inputEl.addEventListener('click', (e) => {
1304                e.stopPropagation();
1305                const isOpen = optionsList.style.display === 'block';
1306                
1307                // Close all other dropdowns globally to prevent overlap
1308                document.querySelectorAll('.dropdown .options').forEach(ul => {
1309                    ul.style.display = 'none';
1310                });
1311                
1312                optionsList.style.display = isOpen ? 'none' : 'block';
1313            });
1314
1315            // Close dropdown when clicking outside
1316            document.addEventListener('click', (e) => {
1317                if (!dropdownEl.contains(e.target)) {
1318                    optionsList.style.display = 'none';
1319                }
1320            });
1321        }
1322
1323        // Render the options
1324        updateAdminScannerDropdownUI();
1325    }
1326
1327    function updateAdminScannerDropdownUI() {
1328        const container = document.getElementById('admin-scanner-dropdown-container');
1329        if (!container) return;
1330
1331        // Hide if there are no additional configs besides default
1332        if (!AvailableScannerConfigs || AvailableScannerConfigs.length <= 1) {
1333            container.style.display = 'none';
1334            return;
1335        }
1336
1337        container.style.display = 'flex';
1338
1339        const optionsList = container.querySelector('ul.options');
1340        const inputEl = container.querySelector('input');
1341
1342        if (optionsList && inputEl) {
1343            // Set the active displayed value
1344            const displayValue = ActiveScannerConfig === 'default' ? 'Default' : ActiveScannerConfig;
1345            inputEl.value = displayValue;
1346            inputEl.setAttribute('data-value', ActiveScannerConfig);
1347
1348            // Rebuild the list items
1349            optionsList.innerHTML = '';
1350            AvailableScannerConfigs.forEach(conf => {
1351                const li = document.createElement('li');
1352                li.className = 'option';
1353                li.tabIndex = 0;
1354                li.setAttribute('data-value', conf);
1355                li.textContent = conf === 'default' ? 'Default' : conf;
1356                
1357                li.addEventListener('click', (e) => {
1358                    e.stopPropagation();
1359                    inputEl.value = li.textContent;
1360                    inputEl.setAttribute('data-value', conf);
1361                    optionsList.style.display = 'none';
1362                    
1363                    console.log(`[Scanner] User selected config: ${conf}`);
1364                    
1365                    // Send command to switch configuration on the server
1366                    if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) {
1367                        wsSendSocket.send(JSON.stringify({
1368                            type: 'Scanner',
1369                            value: { status: 'command', LoadScannerConfig: conf },
1370                            source: clientIp,
1371                            target: target
1372                        }));
1373                    }
1374                });
1375                optionsList.appendChild(li);
1376            });
1377        }
1378    }
1379
1380
1381    ///////////////////////////////////////////////////////////
1382    // Mapviewer button (CSV check)
1383    ///////////////////////////////////////////////////////////
1384    window.initializeMapViewerButton = function () {
1385        const buttonId = "Mapviewer";
1386
1387        const currentDomain = window.location.hostname;
1388        const currentPort   = window.location.port ? ':' + window.location.port : '';
1389        const baseUrl       = `${window.location.protocol}//${currentDomain}${currentPort}`;
1390        const csvFileUrl    = `${baseUrl}/logs/CSVfilename`;
1391
1392        fetch(csvFileUrl, { cache: "no-cache" })
1393            .then(response => {
1394                if (!response.ok) {
1395                    throw new Error("File does not exist");
1396                }
1397                return response.text();
1398            })
1399            .then(initialContent => {
1400                const checkInterval = setInterval(() => {
1401                    if (typeof addIconToPluginPanel === 'function') {
1402                        clearInterval(checkInterval);
1403                        console.log("addIconToPluginPanel found, adding Map Viewer button...");
1404
1405                        addIconToPluginPanel(buttonId, "Mapviewer", "solid", "globe", "Open URDS Map Viewer");
1406
1407                        setTimeout(() => {
1408                            const button = document.getElementById(buttonId);
1409                            if (button) {
1410                                button.addEventListener('click', () => {
1411                                    fetch(csvFileUrl, { cache: "no-cache" })
1412                                        .then(response => response.text())
1413                                        .then(fileContent => {
1414                                            const trimmedContent = fileContent.trim();
1415                                            if (trimmedContent === "NoFileName") {
1416                                                sendToast(
1417                                                    'warning',
1418                                                    'Scanner',
1419                                                    'No CSV Logfile currently available!',
1420                                                    false,
1421                                                    false
1422                                                );
1423                                            } else {
1424                                                const url = `https://tef.noobish.eu/logos/URDSMapViewer.html?file=https://cors-proxy.de:13128/${baseUrl}/logs/${trimmedContent}`;
1425                                                console.log("Opening MapViewer URL:", url);
1426                                                window.open(url, '_blank');
1427                                            }
1428                                        })
1429                                        .catch(error => {
1430                                            console.error("Error reading CSV file:", error);
1431                                            sendToast(
1432                                                'warning',
1433                                                'Scanner',
1434                                                'Error reading CSV Logfile!',
1435                                                false,
1436                                                false
1437                                            );
1438                                        });
1439                                });
1440
1441                                console.log("✅ MapViewer button added successfully!");
1442                            } else {
1443                                console.error("❌ MapViewer button was not created. Check if addIconToPluginPanel appended it correctly.");
1444                            }
1445                        }, 1000);
1446                    }
1447                }, 500);
1448            })
1449            .catch(error => {
1450                if (error.message === "File does not exist") {
1451                    return; // silently ignore missing CSV
1452                }
1453                console.error("CSV file fetch error:", error);
1454            });
1455    };
1456
1457    if (!window.matchMedia("(pointer: coarse)").matches) {
1458        initializeMapViewerButton();
1459        initializeValidatorButton();
1460    }
1461	
1462	
1463	///////////////////////////////////////////////////////////
1464    // URDS Log Validator button
1465    ///////////////////////////////////////////////////////////
1466            function initializeValidatorButton() {
1467        const buttonId = "URDSValidator";
1468
1469        const currentDomain = window.location.hostname;
1470        const currentPort   = window.location.port ? ':' + window.location.port : '';
1471        const baseUrl       = `${window.location.protocol}//${currentDomain}${currentPort}`;
1472        const csvFileUrl    = `${baseUrl}/logs/CSVfilename`;
1473
1474        fetch(csvFileUrl, { cache: "no-cache" })
1475            .then(response => {
1476                if (!response.ok) {
1477                    throw new Error("File does not exist");
1478                }
1479                return response.text();
1480            })
1481            .then(() => {
1482                const checkInterval = setInterval(() => {
1483                    if (typeof addIconToPluginPanel === 'function') {
1484                        clearInterval(checkInterval);
1485                        console.log("addIconToPluginPanel found, adding URDS Log Validator button...");
1486
1487                        addIconToPluginPanel(buttonId, "Validator", "solid", "file-circle-check", "Open URDS Log Validator");
1488
1489                        setTimeout(() => {
1490                            const button = document.getElementById(buttonId);
1491                            if (button) {
1492                                 button.addEventListener('click', () => {
1493                                    fetch(csvFileUrl, { cache: "no-cache" })
1494                                        .then(response => response.text())
1495                                        .then(fileContent => {
1496                                            const trimmedContent = fileContent.trim();
1497                                            if (trimmedContent === "NoFileName") {
1498                                                sendToast(
1499                                                    'warning',
1500                                                    'Scanner',
1501                                                    'No CSV Logfile currently available!',
1502                                                    false,
1503                                                    false
1504                                                );
1505                                            } else {
1506                                                const fileUrl = `${baseUrl}/logs/${trimmedContent}`;
1507                                                const url = `https://highpoint.fmdx.org/webtools/urds-log-validator.html?file=${encodeURIComponent('https://cors-proxy.de:13128/' + fileUrl)}`;
1508                                                console.log("Opening URDS Log Validator URL:", url);
1509                                                window.open(url, '_blank');
1510                                            }
1511                                        })
1512                                        .catch(error => {
1513                                            console.error("Error reading CSV file:", error);
1514                                            sendToast(
1515                                                'warning',
1516                                                'Scanner',
1517                                                'Error reading CSV Logfile!',
1518                                                false,
1519                                                false
1520                                            );
1521                                        });
1522                                });
1523                                console.log("✅ URDS Log Validator button added successfully!");
1524                            } else {
1525                                console.error("❌ URDS Log Validator button was not created.");
1526                            }
1527                        }, 1000);
1528                    }
1529                }, 500);
1530            })
1531            .catch(error => {
1532                if (error.message === "File does not exist") {
1533                    return;
1534                }
1535                console.error("CSV file fetch error:", error);
1536            });
1537    }
1538	
1539    ///////////////////////////////////////////////////////////
1540    // NEW: Watcher for Signal Units dropdown
1541    ///////////////////////////////////////////////////////////
1542    function initSignalUnitWatcher() {
1543        const input = document.getElementById('signal-selector-input');
1544        if (!input) {
1545            console.log('[Scanner] Signal unit selector not found, skipping unit watcher.');
1546            return;
1547        }
1548
1549        // Initial sync
1550        const initialLabel = input.value || input.placeholder || '';
1551        SignalStrengthUnit = mapUnitLabelToInternal(initialLabel);
1552        console.log('[Scanner] Initial SignalStrengthUnit from UI:', SignalStrengthUnit);
1553
1554        // Standard JS assignments to input.value do NOT trigger MutationObserver.
1555        // Therefore, we use a lightweight interval to check for changes instantly.
1556        setInterval(() => {
1557            const label = input.value || input.placeholder || '';
1558            const newUnit = mapUnitLabelToInternal(label);
1559            
1560            if (newUnit !== SignalStrengthUnit) {
1561                console.log('[Scanner] Signal unit changed in UI →', SignalStrengthUnit, '→', newUnit);
1562                SignalStrengthUnit = newUnit;
1563
1564                if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') {
1565                    const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit);
1566
1567                    // Check if scanner controls are currently open
1568                    const existing = document.getElementById('scanner-controls');
1569                    if (existing && isTuneAuthenticated) {
1570                        existing.parentNode.removeChild(existing);
1571                        // Recreate with new units
1572                        createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue);
1573                        // Update the input field text to reflect the new unit
1574                        updateDropdownValues(norm, lastScannerModeValue, lastScanHoldTimeValue);
1575                    }
1576                }
1577            }
1578        }, 250);
1579    }
1580
1581    ///////////////////////////////////////////////////////////
1582    // DOM ready
1583    ///////////////////////////////////////////////////////////
1584    document.addEventListener('DOMContentLoaded', () => {
1585        setTimeout(() => {
1586            initSignalUnitWatcher();   // <--- wichtig: Einheit aus UI übernehmen + beobachten
1587            BlinkAutoScan();
1588            checkAdminMode();
1589            setupSendSocket();
1590        }, 1000);
1591    });
1592
1593    ///////////////////////////////////////////////////////////
1594    // Toast wrapper (global safe)
1595    ///////////////////////////////////////////////////////////
1596    function sendToast(type, title, message, autoClose = true, closeOnClick = true) {
1597        if (typeof window.sendToast === "function") {
1598            window.sendToast(type, title, message, autoClose, closeOnClick);
1599        } else {
1600            console.log(`[${title}] ${message}`);
1601        }
1602    }
1603
1604})();

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.