diff --git a/package-lock.json b/package-lock.json index fe42fd1..42f4dfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fm-dx-webserver", - "version": "1.3.4", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fm-dx-webserver", - "version": "1.3.4", + "version": "1.3.5", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "1.0.11", diff --git a/package.json b/package.json index ecb234c..3354eae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.3.4", + "version": "1.3.5", "description": "FM DX Webserver", "main": "index.js", "scripts": { diff --git a/server/datahandler.js b/server/datahandler.js index 82c9e8a..7467ab6 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -417,7 +417,7 @@ function handleData(wss, receivedData, rdsWss) { } }) .catch((error) => { - logError("Error fetching Tx info:", error); + console.log("Error fetching Tx info:", error); }); // Send the updated data to the client diff --git a/server/helpers.js b/server/helpers.js index 4c955dd..23368c6 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -1,4 +1,5 @@ const http = require('http'); +const https = require('https'); const net = require('net'); const crypto = require('crypto'); const dataHandler = require('./datahandler'); @@ -56,44 +57,86 @@ function authenticateWithXdrd(client, salt, password) { client.write('x\n'); } -function handleConnect(clientIp, currentUsers, ws) { - http.get(`http://ip-api.com/json/${clientIp}`, (response) => { - let data = ''; +const ipCache = new Map(); - response.on('data', (chunk) => { +function handleConnect(clientIp, currentUsers, ws, callback) { + if (ipCache.has(clientIp)) { + // Use cached location info + processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback); + return; + } + + http.get(`http://ip-api.com/json/${clientIp}`, (response) => { + let data = ""; + + response.on("data", (chunk) => { data += chunk; }); - response.on('end', () => { + response.on("end", () => { try { const locationInfo = JSON.parse(data); - const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }; - const connectionTime = new Date().toLocaleString([], options); - - if (locationInfo.as?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block - return; - } - - if(locationInfo.country === undefined) { - const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws }; - storage.connectedUsers.push(userData); - consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); - } else { - const userLocation = `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`; - const userData = { ip: clientIp, location: userLocation, time: connectionTime, instance: ws }; - storage.connectedUsers.push(userData); - consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.country}`); - } + ipCache.set(clientIp, locationInfo); // Store in cache + processConnection(clientIp, locationInfo, currentUsers, ws, callback); } catch (error) { - console.log(error); - consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); + console.error("Error parsing location data:", error); + callback("User allowed"); } }); - }).on('error', (err) => { - consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); + }).on("error", (err) => { + console.error("Error fetching location data:", err); + callback("User allowed"); }); } +function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { + const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }; + const connectionTime = new Date().toLocaleString([], options); + + https.get("https://fmdx.org/banned_as.json", (banResponse) => { + let banData = ""; + + banResponse.on("data", (chunk) => { + banData += chunk; + }); + + banResponse.on("end", () => { + try { + const bannedAS = JSON.parse(banData).banned_as || []; + + if (bannedAS.some((as) => locationInfo.as?.includes(as))) { + return callback("User banned"); + } + + const userLocation = + locationInfo.country === undefined + ? "Unknown" + : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`; + + storage.connectedUsers.push({ + ip: clientIp, + location: userLocation, + time: connectionTime, + instance: ws, + }); + + consoleCmd.logInfo( + `Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` + ); + + callback("User allowed"); + } catch (error) { + console.error("Error parsing banned AS list:", error); + callback("User allowed"); + } + }); + }).on("error", (err) => { + console.error("Error fetching banned AS list:", err); + callback("User allowed"); + }); +} + + function formatUptime(uptimeInSeconds) { const secondsInMinute = 60; const secondsInHour = secondsInMinute * 60; diff --git a/server/index.js b/server/index.js index ed74388..501c2b3 100644 --- a/server/index.js +++ b/server/index.js @@ -326,174 +326,144 @@ app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '../web')); app.use('/', endpoints); -// Anti-spam function -function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) { - const command = message.toString(); - const now = Date.now(); - if (endpointName === 'text') logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); - - // Initialize user command history if not present - if (!userCommandHistory[clientIp]) { - userCommandHistory[clientIp] = []; - } - - // Record the current timestamp for the user - userCommandHistory[clientIp].push(now); - - // Remove timestamps older than 20 ms from the history - userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20); - - // Check if there are 8 or more commands in the last 20 ms - if (userCommandHistory[clientIp].length >= 8) { - logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`); - - // Add to banlist if not already banned - if (!serverConfig.webserver.banlist.includes(clientIp)) { - serverConfig.webserver.banlist.push(clientIp); - logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`); - console.log(serverConfig.webserver.banlist); - configSave(); - } - - ws.close(1008, 'Bot-like behavior detected'); - return command; // Return command value before closing connection - } - - // Update the last message time for general spam detection - lastMessageTime = now; - - // Initialize command history for rate-limiting checks - if (!userCommands[command]) { - userCommands[command] = []; - } - - // Record the current timestamp for this command - userCommands[command].push(now); - - // Remove timestamps older than 1 second - userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000); - - // If command count exceeds limit, close connection - if (userCommands[command].length > lengthCommands) { - if (now - lastWarn.time > 1000) { // Check if 1 second has passed - logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`); - lastWarn.time = now; // Update the last warning time - } - ws.close(1008, 'Spamming detected'); - return command; // Return command value before closing connection - } - - return command; // Return command value for normal execution -} - /** * WEBSOCKET BLOCK */ +const tunerLockTracker = new WeakMap(); + wss.on('connection', (ws, request) => { - const output = serverConfig.xdrd.wirelessConnection ? client : serialport; - let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - const userCommandHistory = {}; - if (serverConfig.webserver.banlist?.includes(clientIp)) { - ws.close(1008, 'Banned IP'); - return; - } + const output = serverConfig.xdrd.wirelessConnection ? client : serialport; + let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + const userCommandHistory = {}; - if (clientIp.includes(',')) { - clientIp = clientIp.split(',')[0].trim(); - } - - if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { - currentUsers++; - } - - dataHandler.showOnlineUsers(currentUsers); - - if(currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { - serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n'); - } - - helpers.handleConnect(clientIp, currentUsers, ws); - - // Anti-spam tracking for each client - const userCommands = {}; - let lastWarn = { time: 0 }; - - ws.on('message', (message) => { - const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text'); - - if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) || - ((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) { - logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`); + if (serverConfig.webserver.banlist?.includes(clientIp)) { + ws.close(1008, 'Banned IP'); return; } - if (command.includes("\'")) { - return; + if (clientIp.includes(',')) { + clientIp = clientIp.split(',')[0].trim(); } - if (command.startsWith('w') && request.session.isAdminAuthenticated) { - switch (command) { - case 'wL1': serverConfig.lockToAdmin = true; break; - case 'wL0': serverConfig.lockToAdmin = false; break; - case 'wT0': serverConfig.publicTuner = true; break; - case 'wT1': serverConfig.publicTuner = false; break; - default: break; - } + if (clientIp !== '::ffff:127.0.0.1' || + (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') || + (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { + currentUsers++; } - if (command.startsWith('T')) { - const tuneFreq = Number(command.slice(1)) / 1000; - const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; - - if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { - return; - } - } + helpers.handleConnect(clientIp, currentUsers, ws, (result) => { + if (result === "User banned") { + ws.close(1008, 'Banned IP'); + return; + } - const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; - - if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) { - output.write(`${command}\n`); - } - - }); - - ws.on('close', (code, reason) => { - if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { - currentUsers--; - } dataHandler.showOnlineUsers(currentUsers); - const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); - if (index !== -1) { - storage.connectedUsers.splice(index, 1); + if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { + serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n'); } - - if(currentUsers === 0) { - storage.connectedUsers = []; - } - - if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { - setTimeout(function() { - if(currentUsers === 0) { - output.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); - dataHandler.resetToDefault(dataHandler.dataToSend); - dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); - dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); - } - }, 10000) - } - - if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { - client.write('X\n'); - } - - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); }); - ws.on('error', console.error); -}); + const userCommands = {}; + let lastWarn = { time: 0 }; + ws.on('message', (message) => { + const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text'); + + if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) || + ((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) { + logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`); + return; + } + + if (command.includes("\'")) { + return; + } + + const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; + + if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) { + switch (command) { + case 'wL1': + if (isAdminAuthenticated) serverConfig.lockToAdmin = true; + break; + case 'wL0': + if (isAdminAuthenticated) serverConfig.lockToAdmin = false; + break; + case 'wT0': + serverConfig.publicTuner = true; + if(!isAdminAuthenticated) tunerLockTracker.delete(ws); + break; + case 'wT1': + serverConfig.publicTuner = false; + if(!isAdminAuthenticated) tunerLockTracker.set(ws, true); + break; + default: + break; + } + } + + if (command.startsWith('T')) { + const tuneFreq = Number(command.slice(1)) / 1000; + const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; + + if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { + return; + } + } + + if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) { + output.write(`${command}\n`); + } + }); + + ws.on('close', (code, reason) => { + if (clientIp !== '::ffff:127.0.0.1' || + (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') || + (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { + currentUsers--; + } + dataHandler.showOnlineUsers(currentUsers); + + const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); + if (index !== -1) { + storage.connectedUsers.splice(index, 1); + } + + if (currentUsers === 0) { + storage.connectedUsers = []; + } + + if (tunerLockTracker.has(ws)) { + logInfo(`User who locked the tuner left. Unlocking the tuner.`); + output.write('wT0\n') + tunerLockTracker.delete(ws); + serverConfig.publicTuner = true; + } + + if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && + serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { + setTimeout(function() { + if (currentUsers === 0) { + output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n'); + dataHandler.resetToDefault(dataHandler.dataToSend); + dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); + } + }, 10000); + } + + if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { + client.write('X\n'); + } + + if (code !== 1008) { + logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); + } + }); + + ws.on('error', console.error); +}); // CHAT WEBSOCKET BLOCK chatWss.on('connection', (ws, request) => { diff --git a/web/css/breadcrumbs.css b/web/css/breadcrumbs.css index 5ed2e92..ec80b74 100644 --- a/web/css/breadcrumbs.css +++ b/web/css/breadcrumbs.css @@ -14,9 +14,16 @@ h1 { } h1#tuner-name { - font-size: 32px; + font-size: 26px; font-weight: 300; text-transform: initial; + user-select: none; + cursor: pointer; + transition: 0.3s ease color; +} + +h1#tuner-name:hover { + color: var(--color-main-bright); } h2 { @@ -43,7 +50,8 @@ h4 { .tooltiptext { position: absolute; - background-color: var(--color-3); + background-color: var(--color-2); + border: 2px solid var(--color-3); color: var(--color-text); text-align: center; font-size: 14px; @@ -54,7 +62,7 @@ h4 { transition: opacity 0.3s ease; } -p#tuner-desc { +p.tuner-desc { margin: 0; } @@ -116,61 +124,63 @@ table .form-group { margin: 0; } -#settings, #back-btn, #users-online-container { - background: transparent; - border: 0; - color: var(--color-text); +.hidden-panel { + display: none; position: absolute; - top: 15px; - right: 15px; - font-size: 16px; - width: 64px; - height: 64px; - line-height: 64px; - text-align: center; - border-radius: 50%; - transition: 500ms ease background; - cursor: pointer; + left: 0; + top: 100%; + width: 100%; + max-width: 1160px; + background: var(--color-1-transparent); + color: white; + text-align: left; + padding: 20px; + backdrop-filter: blur(5px); + z-index: 10; + border-radius: 0 0 15px 15px; } -.chatbutton.hide-desktop { +#settings, #users-online-container, .chatbutton { background: transparent; border: 0; - color: var(--color-text); - position: absolute; - top: 15px; - left: 15px; + color: var(--color-4); font-size: 16px; - width: 64px; - height: 64px; - line-height: 64px; + width: 48px; + height: 48px; text-align: center; - border-radius: 50%; - transition: 500ms ease background; + border-radius: 15px; + transition: 300ms ease background; cursor: pointer; + margin: 2px; } -#settings:hover, #back-btn:hover, #users-online-container:hover { - background: var(--color-3); +#users-online-container { + margin-left: 10px; +} + +.chatbutton, #settings { + background-color: var(--color-1); +} + +#settings:hover, #users-online-container:hover, .chatbutton:hover { + background: var(--color-2); } #users-online-container { top: 80px; } -#back-btn { - left: 15px; - right: auto; +#af-list { + overflow-y: auto; + max-height: 345px; } #af-list ul { - display:list-item; + display: list-item; padding: 0; list-style-type: none; - margin-bottom: 0; + margin: 0; font-size: 14px; - max-height: 380px; - overflow-y: auto; } #af-list a { @@ -192,9 +202,9 @@ table .form-group { margin-bottom: 0; display: none; cursor: pointer; - } - - .checkbox label { +} + +.checkbox label { cursor: pointer; display: block; user-select: none; @@ -204,45 +214,45 @@ table .form-group { border: 2px solid var(--color-4); box-sizing: border-box; transition: 0.35s ease background-color, 0.35s ease color; - } - .checkbox label:hover { +} +.checkbox label:hover { background-color: var(--color-2); - } - - .form-group input:checked + label { +} + +.form-group input:checked + label { background-color: var(--color-4); color: var(--color-main); - } +} - .tuner-info { +.tuner-info { margin-top: 0px !important; margin-bottom: 0px !important; - } +} - h2.settings-heading { +h2.settings-heading { font-size: 42px; padding: 10px 0; font-weight: 300; - } +} - h3.settings-heading { +h3.settings-heading { font-size: 24px; text-transform: uppercase; font-weight: 300; margin-bottom: 5px; - } +} - #tuner-wireless { +#tuner-wireless { display: none; - } +} - #flags-container-phone, - #flags-container-desktop { +#flags-container-phone, +#flags-container-desktop { position: relative; /* Confine overlay within container which is necessary for iPhones */ - } +} - #flags-container-phone .overlay, - #flags-container-desktop .overlay { +#flags-container-phone .overlay, +#flags-container-desktop .overlay { position: absolute; top: 0; left: 0; @@ -321,23 +331,28 @@ pre { position: relative; } +.text-200-px { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 200px; +} + @media (max-width: 768px) { canvas, #flags-container { display: none; } - #tuner-desc { + .tuner-desc { margin-bottom: 20px !important; + text-align: center; } #ps-container { background-color: var(--color-1-transparent); height: 100px !important; margin: auto !important; + margin-top: 30px !important; width: 100%; } - h1#tuner-name { - max-width: 90%; - margin: auto; - } h2 { display: none; } @@ -364,7 +379,7 @@ pre { font-size: 42px; } #data-frequency { - font-size: 64px; + font-size: 58px; } #data-rt0, #data-rt1 { font-size: 10px; @@ -426,7 +441,7 @@ pre { } } -@media only screen and (min-width: 769px) and (max-height: 860px) { +@media only screen and (min-width: 769px) and (max-height: 720px) { #rt-container { height: 90px !important; } @@ -441,38 +456,33 @@ pre { font-size: 24px; text-align: left; } - + .tuner-info #tuner-limit { float: left; text-align: left; } - .tuner-info #tuner-desc { - float: right; - text-align: right; - } h2 { margin-bottom: 10px; font-size: 18px; } - + h2.signal-heading { margin-bottom: 0; } - + .highest-signal-container { margin-bottom: -10px !important; } - + h2.mb-0 { margin-bottom: 0; margin-top: 2px !important; } - #af-list ul { - height: 225px !important; - } - .chatbutton { - height: 88px !important; + + #af-list { + overflow-y: auto; + max-height: 330px; } } diff --git a/web/css/helpers.css b/web/css/helpers.css index 676ea85..6f846f2 100644 --- a/web/css/helpers.css +++ b/web/css/helpers.css @@ -71,6 +71,11 @@ margin: 0 !important; } + +.m-10 { + margin: 10px; +} + .m-left-20 { margin-left: 20px; } @@ -186,6 +191,10 @@ margin-top: 25px; } +.bottom-10 { + margin-bottom: 10px; +} + .bottom-20 { margin-bottom: 20px; } @@ -238,12 +247,34 @@ table .input-text { animation: blinker 1.5s infinite; } +.scrollable-container { + display: flex; + gap: 8px; + overflow-x: auto; /* Enables horizontal scrolling */ + white-space: nowrap; + scrollbar-width: none; /* Hide scrollbar in Firefox */ + -ms-overflow-style: none; /* Hide scrollbar in Edge */ +} + +/* Hide scrollbar for Chrome, Safari */ +.scrollable-container::-webkit-scrollbar { + display: none; +} + +/* Chevron styling */ +.scroll-left, +.scroll-right { + display: none; /* Hidden by default */ + cursor: pointer; + width: 48px; +} + @keyframes blinker { 0% { - background-color: var(--color-4); + background-color: var(--color-3); } 100% { - background-color: var(--color-2); + background-color: var(--color-1); } } @@ -286,7 +317,7 @@ table .input-text { } /* Laptop compact view */ -@media only screen and (min-width: 960px) and (max-height: 860px) { +@media only screen and (min-width: 960px) and (max-height: 720px) { .text-big { font-size: 40px; } diff --git a/web/css/main.css b/web/css/main.css index 77501b1..aa4a554 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -60,14 +60,22 @@ body { min-height: 100%; } -#wrapper-outer { +.wrapper-outer { width: 100%; height: auto; background-color: var(--color-main); display: flex; align-items: center; justify-content: center; - min-height:100vh; + flex-direction: column; +} + +.wrapper-outer:not(.dashboard-panel) { + min-height: calc(100vh - 84px); +} + +.wrapper-outer.wrapper-full { + min-height: 100vh; } .wrapper-outer-static { @@ -79,6 +87,7 @@ body { width: 100%; max-width: calc(0% + 1180px); } + #wrapper.setup-wrapper { margin: auto; position: static; @@ -135,15 +144,6 @@ hr { border-radius: 0px 15px 15px 0px; } -@media (max-width: 1180px) { - #wrapper { - position: static; - transform: none; - margin: 50px auto; - width: 100%; - } -} - @media (max-width: 768px) { #wrapper.setup-wrapper { width: 100%; diff --git a/web/css/modal.css b/web/css/modal.css index 9cbe6a8..0f4b9ed 100644 --- a/web/css/modal.css +++ b/web/css/modal.css @@ -107,11 +107,6 @@ body.modal-open { overflow-y: auto; } -.modal-panel-content .version-info { - margin-top: 20px; - width: 100%; -} - .modal-panel-footer { width: 450px; height: 100px; @@ -171,7 +166,7 @@ body.modal-open { width: 100%; } .modal-panel-chat { - height: 550px; + height: 510px; } #chat-chatbox { height: 333px !important; diff --git a/web/css/panels.css b/web/css/panels.css index 762bcb4..177e5a4 100644 --- a/web/css/panels.css +++ b/web/css/panels.css @@ -93,21 +93,13 @@ .panel-90 { margin-top: 100px; } -} - -/* Laptop compact view */ -@media only screen and (min-width: 960px) and (max-height: 860px) { - *[class^="panel-"] { - margin-top: 20px; + .panel-100-real.bg-phone { + background-color: var(--color-1-transparent); + backdrop-filteR: blur(5px) !important; + padding-left: 10px !important; + padding-right: 10px !important; } - .panel-90 { - margin-top: 0; - } - .panel-10 { - padding-bottom: 20px; - padding-right: 20px; - } - .panel-10.hide-phone { - padding: 0; + #dashboard-panel-description { + backdrop-filter: blur(25px) !important; } } \ No newline at end of file diff --git a/web/css/toast.css b/web/css/toast.css index dd73970..d253481 100644 --- a/web/css/toast.css +++ b/web/css/toast.css @@ -2,7 +2,7 @@ #toast-container { position: fixed; top: 20px; - right: 96px; + right: 32px; z-index: 9999; } diff --git a/web/index.ejs b/web/index.ejs index 7d4fc6e..60a5e0f 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -6,10 +6,10 @@ - + - + @@ -29,471 +29,521 @@
-
- <%- tunerDesc %>
- <% if(tuningLimit && tuningLimit == true){ %>
-
Limit: <%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz
- <% } %>
-
- Current identity: -
-