diff --git a/app/css/all.css b/app/css/all.css index 1e38a6a4..d95c8390 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -9,6 +9,7 @@ License: GNU General Public License v3.0 :root { --raspap-content-main: #495057; --raspap-text-muted: #858796; + --raspap-text-light: #999999; --raspap-brand-color: #2b8080; --raspap-offwhite: #faf9f6; } @@ -171,11 +172,6 @@ canvas#divDBChartBandwidthhourly { height: 350px!important; } -.chart-container { - height: 150px; - width: 200px; -} - .dbChart { display: flex; height: 80%; @@ -190,7 +186,7 @@ canvas#divDBChartBandwidthhourly { } .check-progress { - color: #999; + color: var(--raspap-text-light); } .fa-check { @@ -388,3 +384,280 @@ textarea.plugin-log { font-family: monospace; font-size: 0.9rem; } + +.card-wrapper { + margin: 1rem; +} + +.dashboard-container { + display: flex; + position: relative; + width: 100%; + min-height: 400px; + padding: 2rem; +} + +.connections-left, +.connections-right { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.connection-item { + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + z-index: 5; + color: var(--raspap-text-light); +} + +.connection-right { + align-items: center; + margin-left: 10rem; +} + +.connections-left i { + height: 40px; + display: flex; + align-items: center; + justify-content: left; +} + +.connections-left i:first-child { + margin-top: 0; +} + +.connections-left i:last-child { + margin-bottom: 0; + margin-left: 0.5rem; +} + +.center-device { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + position: relative; + z-index: 1; +} + +.center-device-top { + margin-bottom: 30px; +} + +.client-group { + display: flex; + align-items: center; + flex-direction: row-reverse; + gap: 0.5rem; +} + +.client-count { + text-align: right; +} + +.clients-status { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + padding-right: 1rem; +} + +.dashed-lines, +.solid-lines { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + padding: 1rem; + left: 112px; +} + +.dashed-lines-right, +.solid-lines-right { + left: -80px; +} + +.solid-lines, .solid-lines-right { + z-index: 3; +} + +.dashed-lines, .dashed-lines-right { + z-index 0; +} + +.device-status { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin: 0.8rem 0; +} + +.wifi-bands { + display: flex; + gap: 0.5rem; +} + +.band { + padding: 0.25rem 1rem; + border: 2px solid var(--raspap-text-light); + border-radius: 4px; + background: transparent; + font-weight: 600; + color: var(--raspap-text-light); +} + +.band.active { + border-color: var(--raspap-theme-color); + color: var(--raspap-theme-color); +} + +.device-label { + font-size: 1.3rem; + text-align: center; + color: var(--raspap-theme-color); + margin-top: 1rem; +} + +.status-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.3rem; + color: var(--raspap-text-light); +} + +.bottom { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.3rem; + width: 100%; +} + +.status-item .fa-stack { + width: 1.5em!important; +} + +.connection-item>i { + color: var(--raspap-text-light); +} + +.connection-item .fa-stack { + min-width: 2.5em; +} + +.connections-left>.connection-item>span { + color: var(--raspap-text-light); + margin-right: 0.5rem; +} + +.inactive { + color: var(--raspap-text-light)!important; +} + +a.inactive:hover, +a.inactive:focus { + color: var(--raspap-text-light) !important; +} + +@media (max-width: 1200px) { + .connection-item a > span:not(.fa-stack) { + display: none!important; + } +} + +@media (max-width: 991px) { + .connections-right, + .connections-left { + display: none!important; + } + .dashboard-container { + width: auto; + padding: 0; + + } + .device-status { + gap: 0.5rem; + } + .clients-mobile { + display: flex!important; + flex-direction: row!important; + } +} +.connection-item.active > span { + color: var(--raspap-theme-color)!important; +} +.connection-item.active > i { + color: var(--raspap-theme-color)!important; +} +.status-item.active > span { + color: var(--raspap-theme-color)!important; +} +.status-item.active > i { + color: var(--raspap-theme-color)!important; +} + +.clients-mobile { + display: none; + flex-direction: column; + gap: 1rem; + margin-top: 2rem; +} + +.client-type { + position: relative; + display: inline-flex; + align-items: center; + gap: 1rem; +} + +.client-type i { + font-size: 1.5rem; + color: var(--raspap-theme-color); + width: 45px; + height: 45px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid var(--raspap-theme-color); +} + +.client-type i.badge-icon { + font-size: 0.7rem; + background: var(--raspap-theme-color); + color: var(--raspap-offwhite); + width: 20px; + height: 20px; + border: none; +} + +.client-count { + position: absolute; + top: -5px; + right: -5px; + background: var(--raspap-theme-color); + color: var(--raspap-offwhite); + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; +} + +.device-illustration { + min-width: 220px; + max-width: 250px; +} + diff --git a/app/img/dashed.svg b/app/img/dashed.svg new file mode 100644 index 00000000..6d6ea39f --- /dev/null +++ b/app/img/dashed.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/img/device.php b/app/img/device.php new file mode 100644 index 00000000..193e0130 --- /dev/null +++ b/app/img/device.php @@ -0,0 +1,4450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/img/right-dashed.svg b/app/img/right-dashed.svg new file mode 100644 index 00000000..1c5c3244 --- /dev/null +++ b/app/img/right-dashed.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/img/right-solid.php b/app/img/right-solid.php new file mode 100644 index 00000000..27ca8f79 --- /dev/null +++ b/app/img/right-solid.php @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/img/solid.php b/app/img/solid.php new file mode 100644 index 00000000..9952e7fa --- /dev/null +++ b/app/img/solid.php @@ -0,0 +1,65 @@ + + + + 0.75, + 'out' => 297.75, + 'device-2' => 198.75, + 'device-3' => 397.058, + 'device-4' => 595.211 +]; + +// Calculate joint line segments +if ($showJoint) { + $activeDevices = array_filter([$showDevice1, $showDevice2, $showDevice3, $showDevice4]); + $activeYs = []; + + foreach ($devicePositions as $device => $y) { + if (isset($_GET[$device])) { + $activeYs[] = $y; + } + } + + // Add top/bottom if first/last device is connected + if ($showDevice1) array_unshift($activeYs, 0); + if ($showDevice4) $activeYs[] = 596; + + // Draw segments between consecutive points + for ($i = 1; $i < count($activeYs); $i++) { + $y1 = $activeYs[$i-1]; + $y2 = $activeYs[$i]; + echo ""; + } +} +?> + + + + + + + + + + + + + + + + + diff --git a/app/js/custom.js b/app/js/custom.js index 22f5a1dc..ed5ec2dd 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -994,42 +994,9 @@ function getCookie(cname) { // Define themes var themes = { "default": "custom.php", - "hackernews" : "hackernews.css", - "lightsout" : "lightsout.php", - "material-light" : "material-light.php", - "material-dark" : "material-dark.php", + "hackernews" : "hackernews.css" } -// Toggles the sidebar navigation. -// Overrides the default SB Admin 2 behavior -$("#sidebarToggleTopbar").on('click', function(e) { - $("body").toggleClass("sidebar-toggled"); - $(".sidebar").toggleClass("toggled d-none"); -}); - -// Overrides SB Admin 2 -$("#sidebarToggle, #sidebarToggleTop").on('click', function(e) { - var toggled = $(".sidebar").hasClass("toggled"); - // Persist state in cookie - setCookie('sidebarToggled',toggled, 90); -}); - -$(function() { - if ($(window).width() < 768) { - $('.sidebar').addClass('toggled'); - setCookie('sidebarToggled',false, 90); - } -}); - -$(window).on("load resize",function(e) { - if ($(window).width() > 768) { - $('.sidebar').removeClass('d-none d-md-block'); - if (getCookie('sidebarToggled') == 'false') { - $('.sidebar').removeClass('toggled'); - } - } -}); - // Adds active class to current nav-item $(window).bind("load", function() { var url = window.location; @@ -1038,6 +1005,19 @@ $(window).bind("load", function() { }).parent().addClass('active'); }); +// Sets focus on a specified tab +document.addEventListener("DOMContentLoaded", function () { + const params = new URLSearchParams(window.location.search); + const targetTab = params.get("tab"); + if (targetTab) { + let tabElement = document.querySelector(`[data-bs-toggle="tab"][href="#${targetTab}"]`); + if (tabElement) { + let tab = new bootstrap.Tab(tabElement); + tab.show(); + } + } +}); + $(document).ready(function() { const $htmlElement = $('html'); const $modeswitch = $('#night-mode'); diff --git a/config/hostapd.conf b/config/hostapd.conf index b88b5469..944ce151 100644 --- a/config/hostapd.conf +++ b/config/hostapd.conf @@ -4,7 +4,7 @@ ctrl_interface_group=0 beacon_int=100 auth_algs=1 wpa_key_mgmt=WPA-PSK -ssid=raspi-webgui +ssid=RaspAP channel=1 hw_mode=g wpa_passphrase=ChangeMe diff --git a/includes/dashboard.php b/includes/dashboard.php index ea307051..f77c6e55 100755 --- a/includes/dashboard.php +++ b/includes/dashboard.php @@ -5,226 +5,170 @@ require_once 'includes/wifi_functions.php'; require_once 'includes/functions.php'; /** - * Show dashboard page. + * Displays the dashboard */ -function DisplayDashboard(&$extraFooterScripts) +function DisplayDashboard(): void { - getWifiInterface(); + // instantiate RaspAP objects + $system = new \RaspAP\System\Sysinfo; + $dashboard = new \RaspAP\UI\Dashboard; $status = new \RaspAP\Messages\StatusMessage; - // Need this check interface name for proper shell execution. - if (!preg_match('/^([a-zA-Z0-9]+)$/', $_SESSION['wifi_client_interface'])) { - $status->addMessage(_('Interface name invalid.'), 'danger'); - $status->showMessages(); - return; - } + $pluginManager = \RaspAP\Plugins\PluginManager::getInstance(); - if (!function_exists('exec')) { - $status->addMessage(_('Required exec function is disabled. Check if exec is not added to php disable_functions.'), 'danger'); - $status->showMessages(); - return; - } - exec('ip a show '.$_SESSION['ap_interface'], $stdoutIp); - $stdoutIpAllLinesGlued = implode(" ", $stdoutIp); - $stdoutIpWRepeatedSpaces = preg_replace('/\s\s+/', ' ', $stdoutIpAllLinesGlued); + // set AP and client interface session vars + getWifiInterface(); - preg_match('/link\/ether ([0-9a-f:]+)/i', $stdoutIpWRepeatedSpaces, $matchesMacAddr) || $matchesMacAddr[1] = _('No MAC Address Found'); - $macAddr = $matchesMacAddr[1]; - - $ipv4Addrs = ''; - $ipv4Netmasks = ''; - if (!preg_match_all('/inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/([0-3][0-9])/i', $stdoutIpWRepeatedSpaces, $matchesIpv4AddrAndSubnet, PREG_SET_ORDER)) { - $ipv4Addrs = _('No IPv4 Address Found'); - } else { - foreach ($matchesIpv4AddrAndSubnet as $inet) { - $address = $inet[1]; - $suffix = (int) $inet[2]; - $netmask = long2ip(-1 << (32 - $suffix)); - $ipv4Addrs .= " $address"; - $ipv4Netmasks .= " $netmask"; - } - $ipv4Addrs = trim($ipv4Addrs); - $ipv4Netmasks = trim($ipv4Netmasks); - } - $ipv4Netmasks = empty($ipv4Netmasks) ? "-" : $ipv4Netmasks; - - $ipv6Addrs = ''; - if (!preg_match_all('/inet6 ([a-f0-9:]+)/i', $stdoutIpWRepeatedSpaces, $matchesIpv6Addr)) { - $ipv6Addrs = _('No IPv6 Address Found'); - } else { - if (isset($matchesIpv6Addr[1])) { - $ipv6Addrs = implode(' ', $matchesIpv6Addr[1]); - } - } - - preg_match('/state (UP|DOWN)/i', $stdoutIpWRepeatedSpaces, $matchesState) || $matchesState[1] = 'unknown'; - $interfaceState = $matchesState[1]; - - // Because of table layout used in the ip output we get the interface statistics directly from - // the system. One advantage of this is that it could work when interface is disable. - exec('cat /sys/class/net/'.$_SESSION['ap_interface'].'/statistics/rx_packets ', $stdoutCatRxPackets); - $strRxPackets = _('No data'); - if (ctype_digit($stdoutCatRxPackets[0])) { - $strRxPackets = $stdoutCatRxPackets[0]; - } - - exec('cat /sys/class/net/'.$_SESSION['ap_interface'].'/statistics/tx_packets ', $stdoutCatTxPackets); - $strTxPackets = _('No data'); - if (ctype_digit($stdoutCatTxPackets[0])) { - $strTxPackets = $stdoutCatTxPackets[0]; - } - - exec('cat /sys/class/net/'.$_SESSION['ap_interface'].'/statistics/rx_bytes ', $stdoutCatRxBytes); - $strRxBytes = _('No data'); - if (ctype_digit($stdoutCatRxBytes[0])) { - $strRxBytes = $stdoutCatRxBytes[0]; - $strRxBytes .= getHumanReadableDatasize($strRxBytes); - } - - exec('cat /sys/class/net/'.$_SESSION['ap_interface'].'/statistics/tx_bytes ', $stdoutCatTxBytes); - $strTxBytes = _('No data'); - if (ctype_digit($stdoutCatTxBytes[0])) { - $strTxBytes = $stdoutCatTxBytes[0]; - $strTxBytes .= getHumanReadableDatasize($strTxBytes); - } - - exec ('vnstat --dbiflist', $stdoutVnStatDB); - if (!preg_match('/'.$_SESSION['ap_interface'].'/', $stdoutVnStatDB[0])) { - exec('sudo vnstat --add --iface '.$_SESSION['ap_interface'], $return); - } - - define('SSIDMAXLEN', 32); - // Warning iw comes with: "Do NOT screenscrape this tool, we don't consider its output stable." - exec('iw dev ' .$_SESSION['wifi_client_interface']. ' link ', $stdoutIw); - $stdoutIwAllLinesGlued = implode('+', $stdoutIw); // Break lines with character illegal in SSID and MAC addr - $stdoutIwWRepSpaces = preg_replace('/\s\s+/', ' ', $stdoutIwAllLinesGlued); - - preg_match('/Connected to (([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2}))/', $stdoutIwWRepSpaces, $matchesBSSID) || $matchesBSSID[1] = ''; - $connectedBSSID = $matchesBSSID[1]; - $connectedBSSID = empty($connectedBSSID) ? "-" : $connectedBSSID; - - $wlanHasLink = false; - if ($interfaceState === 'UP') { - $wlanHasLink = true; - } - - if (!preg_match('/SSID: ([^+]{1,'.SSIDMAXLEN.'})/', $stdoutIwWRepSpaces, $matchesSSID)) { - $wlanHasLink = false; - $matchesSSID[1] = 'None'; - } - $connectedSSID = str_replace('\x20', '', $matchesSSID[1]); - - preg_match('/freq: (\d+)/i', $stdoutIwWRepSpaces, $matchesFrequency) || $matchesFrequency[1] = ''; - $frequency = $matchesFrequency[1].' MHz'; - - preg_match('/signal: (-?[0-9]+ dBm)/i', $stdoutIwWRepSpaces, $matchesSignal) || $matchesSignal[1] = ''; - $signalLevel = $matchesSignal[1]; - $signalLevel = empty($signalLevel) ? "-" : $signalLevel; - - preg_match('/tx bitrate: ([0-9\.]+ [KMGT]?Bit\/s)/', $stdoutIwWRepSpaces, $matchesBitrate) || $matchesBitrate[1] = ''; - $bitrate = $matchesBitrate[1]; - $bitrate = empty($bitrate) ? "-" : $bitrate; - - // txpower is now displayed on iw dev(..) info command, not on link command. - exec('iw dev '.$_SESSION['wifi_client_interface'].' info ', $stdoutIwInfo); - $stdoutIwInfoAllLinesGlued = implode(' ', $stdoutIwInfo); - $stdoutIpInfoWRepSpaces = preg_replace('/\s\s+/', ' ', $stdoutIwInfoAllLinesGlued); - - preg_match('/txpower ([0-9\.]+ dBm)/i', $stdoutIpInfoWRepSpaces, $matchesTxPower) || $matchesTxPower[1] = ''; - $txPower = $matchesTxPower[1]; - - // iw does not have the "Link Quality". This is a is an aggregate value, - // and depends on the driver and hardware. - // Display link quality as signal quality for now. - $strLinkQuality = 0; - if ($signalLevel > -100 && $wlanHasLink) { - if ($signalLevel >= 0) { - $strLinkQuality = 100; - } else { - $strLinkQuality = 100 + intval($signalLevel); - } - } - - $wlan0up = false; - $classMsgDevicestatus = 'warning'; - if ($interfaceState === 'UP') { - $wlan0up = true; - $classMsgDevicestatus = 'success'; - } - - if (!RASPI_MONITOR_ENABLED) { - if (isset($_POST['ifdown_wlan0'])) { - // Pressed stop button - if ($interfaceState === 'UP') { - $status->addMessage(sprintf(_('Interface is going %s.'), _('down')), 'warning'); - exec('sudo ip link set '.$_SESSION['ap_interface'].' down'); - $wlan0up = false; - $status->addMessage(sprintf(_('Interface is now %s.'), _('down')), 'success'); - } elseif ($interfaceState === 'unknown') { - $status->addMessage(_('Interface state unknown.'), 'danger'); - } else { - $status->addMessage(sprintf(_('Interface already %s.'), _('down')), 'warning'); - } - } elseif (isset($_POST['ifup_wlan0'])) { - // Pressed start button - if ($interfaceState === 'DOWN') { - $status->addMessage(sprintf(_('Interface is going %s.'), _('up')), 'warning'); - exec('sudo ip link set ' .$_SESSION['ap_interface']. ' up'); - exec('sudo ip -s a f label ' .$_SESSION['ap_interface']); - $wlan0up = true; - $status->addMessage(sprintf(_('Interface is now %s.'), _('up')), 'success'); - } elseif ($interfaceState === 'unknown') { - $status->addMessage(_('Interface state unknown.'), 'danger'); - } else { - $status->addMessage(sprintf(_('Interface already %s.'), _('up')), 'warning'); - } - } else { - $status->addMessage(sprintf(_('Interface is %s.'), strtolower($interfaceState)), $classMsgDevicestatus); - } - } - // brought in from template + $interface = $_SESSION['ap_interface'] ?? 'wlan0'; + $clientInterface = $_SESSION['wifi_client_interface']; + $hostname = $system->hostname(); + $revision = $system->rpiRevision(); + $hostapd = $system->hostapdStatus(); + $adblock = $system->adBlockStatus(); + $vpn = $system->getActiveVpnInterface(); + $frequency = $dashboard->getFrequencyBand($interface); + $details = $dashboard->getInterfaceDetails($interface); + $wireless = $dashboard->getWirelessDetails($interface); + $connectionType = $dashboard->getConnectionType(); + $connectionIcon = $dashboard->getConnectionIcon($connectionType); + $state = strtolower($details['state']); + $wirelessClients = $dashboard->getWirelessClients(); + $ethernetClients = $dashboard->getEthernetClients(); + $totalClients = $wirelessClients + $ethernetClients; + $plugins = $pluginManager->getInstalledPlugins(); $arrHostapdConf = parse_ini_file(RASPI_CONFIG.'/hostapd.ini'); $bridgedEnable = $arrHostapdConf['BridgedEnable']; - $clientInterface = $_SESSION['wifi_client_interface']; - $apInterface = $_SESSION['ap_interface']; - $MACPattern = '"([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}"'; - if (getBridgedState()) { - $moreLink = "hostapd_conf"; - exec('iw dev ' . $apInterface . ' station dump | grep -oE ' . $MACPattern, $clients); - } else { - $moreLink = "dhcpd_conf"; - exec('cat ' . RASPI_DNSMASQ_LEASES . '| grep -E $(iw dev ' . $apInterface . ' station dump | grep -oE ' . $MACPattern . ' | paste -sd "|")', $clients); + // handle page actions + if (!empty($_POST)) { + $status = $dashboard->handlePageAction($state, $_POST, $status, $interface); + // refresh interface details + state + $details = $dashboard->getInterfaceDetails($interface); + $state = strtolower($details['state']); + } + + $ipv4Address = $details['ipv4']; + $ipv4Netmask = $details['ipv4_netmask']; + $macAddress = $details['mac']; + $ssid = $wireless['ssid']; + $ethernetActive = ($connectionType === 'ethernet') ? "active" : "inactive"; + $wirelessActive = ($connectionType === 'wireless') ? "active" : "inactive"; + $tetheringActive = ($connectionType === 'tethering') ? "active" : "inactive"; + $cellularActive = ($connectionType === 'cellular') ? "active" : "inactive"; + $bridgedStatus = ($bridgedEnable == 1) ? "active" : ""; + $hostapdStatus = ($hostapd[0] == 1) ? "active" : ""; + $adblockStatus = ($adblock == true) ? "active" : ""; + $wirelessClientActive = ($wirelessClients > 0) ? "active" : "inactive"; + $wirelessClientLabel = sprintf( + _('%d WLAN %s'), + $wirelessClients, + $dashboard->formatClientLabel($wirelessClients) + ); + $ethernetClientActive = ($ethernetClients > 0) ? "active" : "inactive"; + $ethernetClientLabel = sprintf( + _('%d LAN %s'), + $ethernetClients, + $dashboard->formatClientLabel($ethernetClients) + ); + $totalClientsActive = ($totalClients > 0) ? "active": "inactive"; + $freq5active = $freq24active = ""; + $varName = "freq" . str_replace('.', '', $frequency) . "active"; + $$varName = "active"; + $vpnStatus = $vpn ? "active" : "inactive"; + if ($vpn) { + $vpnManaged = $dashboard->getVpnManged($vpn); + } + $firewallManaged = $firewallStatus = ""; + $firewallInstalled = array_filter($plugins, fn($p) => str_ends_with($p, 'Firewall')) ? true : false; + if (!$firewallInstalled) { + $firewallUnavailable = ''; + } else { + $firewallManaged = ''; + $firewallStatus = ($dashboard->firewallEnabled() == true) ? "active" : ""; } - $ifaceStatus = $wlan0up ? "up" : "down"; echo renderTemplate( "dashboard", compact( - "clients", - "moreLink", - "apInterface", + "revision", + "interface", "clientInterface", - "ifaceStatus", - "bridgedEnable", - "status", - "ipv4Addrs", - "ipv4Netmasks", - "ipv6Addrs", - "macAddr", - "strRxPackets", - "strRxBytes", - "strTxPackets", - "strTxBytes", - "connectedSSID", - "connectedBSSID", - "bitrate", - "signalLevel", - "txPower", + "state", + "bridgedStatus", + "hostapdStatus", + "adblockStatus", + "vpnStatus", + "vpnManaged", + "firewallUnavailable", + "firewallStatus", + "firewallManaged", + "ipv4Address", + "ipv4Netmask", + "macAddress", + "ssid", "frequency", - "strLinkQuality", - "wlan0up" + "freq5active", + "freq24active", + "wirelessClients", + "wirelessClientLabel", + "wirelessClientActive", + "ethernetClients", + "ethernetClientLabel", + "ethernetClientActive", + "totalClients", + "totalClientsActive", + "connectionType", + "connectionIcon", + "ethernetActive", + "wirelessActive", + "tetheringActive", + "cellularActive", + "status" ) ); - $extraFooterScripts[] = array('src'=>'app/js/dashboardchart.js', 'defer'=>false); - $extraFooterScripts[] = array('src'=>'app/js/linkquality.js', 'defer'=>false); } +/** + * Renders a URL for an svg solid line representing the associated + * connection type + * + * @param string $connectionType + * @return string + */ +function renderConnection(string $connectionType): string +{ + $deviceMap = [ + 'ethernet' => 'device-1', + 'wireless' => 'device-2', + 'tethering' => 'device-3', + 'cellular' => 'device-4' + ]; + $device = $deviceMap[$connectionType] ?? 'device-unknown'; + + // return generated URL for solid.php + return sprintf('app/img/solid.php?joint&%s&out', $device); +} + +/** + * Renders a URL for an svg solid line representing associated + * client connection(s) + * + * @param int $wirelessClients + * @param int $ethernetClients + * @return string + */ +function renderClientConnections(int $wirelessClients, int $ethernetClients): string +{ + $devices = []; + + if ($wirelessClients > 0) { + $devices[] = 'device-1&out'; + } + if ($ethernetClients > 0) { + $devices[] = 'device-2&out'; + } + return empty($devices) ? '' : sprintf( + 'Client connections', + implode('&', $devices) + ); +} + + diff --git a/includes/footer.php b/includes/footer.php index b12c0f1c..427d68a6 100755 --- a/includes/footer.php +++ b/includes/footer.php @@ -3,10 +3,10 @@
v | - Created by the RaspAP Team + %s'), 'https://github.com/RaspAP', _('RaspAP Team')); ?>
- Get Insiders +
diff --git a/includes/sysstats.php b/includes/sysstats.php index 4bc599e6..7f10d3e5 100755 --- a/includes/sysstats.php +++ b/includes/sysstats.php @@ -50,6 +50,6 @@ if ($hostapd[0] ==1) { $hostapd_led = "service-status-up"; } else { $hostapd_status = "down"; - $hostapd_led = "service-status-down"; + $hostapd_led = "service-status-warn"; } diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index d1c38e1f..799128f8 100644 Binary files a/locale/en_US/LC_MESSAGES/messages.mo and b/locale/en_US/LC_MESSAGES/messages.mo differ diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index aa887cba..da8c34b3 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -25,8 +25,8 @@ msgstr "RaspAP Wifi Configuration Portal" msgid "Toggle navigation" msgstr "Toggle navigation" -msgid "RaspAP Wifi Portal" -msgstr "RaspAP Wifi Portal" +msgid "RaspAP Admin Panel" +msgstr "RaspAP Admin Panel" msgid "Dashboard" msgstr "Dashboard" @@ -244,8 +244,8 @@ msgstr "Frequency" msgid "Link Quality" msgstr "Link Quality" -msgid "Information provided by ip and iw and from system" -msgstr "Information provided by ip and iw and from system" +msgid "Information provided by raspap.system" +msgstr "Information provided by raspap.system" msgid "No MAC Address Found" msgstr "No MAC Address Found" @@ -283,9 +283,53 @@ msgstr "Connected Devices" msgid "Client: Ethernet cable" msgstr "Client: Ethernet cable" +msgid "Current status" +msgstr "Current status" + msgid "Ethernet" msgstr "Ethernet" +msgid "Repeater" +msgstr "Repeater" + +msgid "Tethering" +msgstr "Tethering" + +msgid "Cellular" +msgstr "Cellular" + +msgid "AP" +msgstr "AP" + +msgid "Bridged" +msgstr "Bridged" + +msgid "Adblock" +msgstr "Adblock" + +msgid "VPN" +msgstr "VPN" + +msgid "Firewall" +msgstr "Firewall" + +msgid "Netmask" +msgstr "Netmask" + +msgid "5G" +msgstr "5G" + +msgid "2.4G" +msgstr "2.4G" + +msgid "%d WLAN %s" +msgstr "%d WLAN %s" + +msgid "client" +msgid_plural "clients" +msgstr[0] "client" +msgstr[1] "clients" + msgid "Client: Smartphone (USB tethering)" msgstr "Client: Smartphone (USB tethering)" @@ -334,6 +378,16 @@ msgstr "Signal strength" msgid "No Client device or not yet configured" msgstr "No Client device or not yet configured" +#: includes/footer.php +msgid "Created by the %s" +msgstr "Created by the %s" + +msgid "RaspAP Team" +msgstr "RaspAP Team" + +msgid "Get Insiders" +msgstr "Get Insiders" + #: includes/dhcp.php msgid "DHCP server settings" msgstr "DHCP server settings" diff --git a/src/RaspAP/System/Sysinfo.php b/src/RaspAP/System/Sysinfo.php index 3cb493c9..cfb641d3 100755 --- a/src/RaspAP/System/Sysinfo.php +++ b/src/RaspAP/System/Sysinfo.php @@ -100,43 +100,43 @@ class Sysinfo public function rpiRevision() { $revisions = array( - '0002' => 'Model B Revision 1.0', - '0003' => 'Model B Revision 1.0 + ECN0001', - '0004' => 'Model B Revision 2.0 (256 MB)', - '0005' => 'Model B Revision 2.0 (256 MB)', - '0006' => 'Model B Revision 2.0 (256 MB)', - '0007' => 'Model A', - '0008' => 'Model A', - '0009' => 'Model A', - '000d' => 'Model B Revision 2.0 (512 MB)', - '000e' => 'Model B Revision 2.0 (512 MB)', - '000f' => 'Model B Revision 2.0 (512 MB)', - '0010' => 'Model B+', - '0013' => 'Model B+', + '0002' => 'Raspberry Pi Model B Rev 1.0', + '0003' => 'Raspberry Pi Model B Rev 1.0', + '0004' => 'Raspberry Pi Model B Rev 2.0', + '0005' => 'Raspberry Pi Model B Rev 2.0', + '0006' => 'Raspberry Pi Model B Rev 2.0', + '0007' => 'Raspberry Pi Model A', + '0008' => 'Raspberry Pi Model A', + '0009' => 'Raspberry Pi Model A', + '000d' => 'Raspberry Pi Model B Rev 2.0', + '000e' => 'Raspberry Pi Model B Rev 2.0', + '000f' => 'Raspberry Pi Model B Rev 2.0', + '0010' => 'Raspberry Pi Model B+', + '0013' => 'Raspberry Pi Model B+', '0011' => 'Compute Module', - '0012' => 'Model A+', + '0012' => 'Raspberry Pi Model A+', 'a01041' => 'a01041', 'a21041' => 'a21041', - '900092' => 'PiZero 1.2', - '900093' => 'PiZero 1.3', - '9000c1' => 'PiZero W', - 'a02082' => 'Pi 3 Model B', - 'a22082' => 'Pi 3 Model B', - 'a32082' => 'Pi 3 Model B', - 'a52082' => 'Pi 3 Model B', - 'a020d3' => 'Pi 3 Model B+', + '900092' => 'Raspberry Pi Zero 1.2', + '900093' => 'Raspberry Pi Zero 1.3', + '9000c1' => 'Raspberry Pi Zero W', + 'a02082' => 'Raspberry Pi 3 Model B', + 'a22082' => 'Raspberry Pi 3 Model B', + 'a32082' => 'Raspberry Pi 3 Model B', + 'a52082' => 'Raspberry Pi 3 Model B', + 'a020d3' => 'Raspberry Pi 3 Model B+', 'a220a0' => 'Compute Module 3', 'a020a0' => 'Compute Module 3', 'a02100' => 'Compute Module 3+', - 'a03111' => 'Model 4B Revision 1.1 (1 GB)', - 'b03111' => 'Model 4B Revision 1.1 (2 GB)', - 'c03111' => 'Model 4B Revision 1.1 (4 GB)', + 'a03111' => 'Raspberry Pi 4B Rev 1.1 (1 GB)', + 'b03111' => 'Raspberry Pi 4B Rev 1.1 (2 GB)', + 'c03111' => 'Raspberry Pi 4B Rev 1.1 (4 GB)', 'a03140' => 'Compute Module 4 (1 GB)', 'b03140' => 'Compute Module 4 (2 GB)', 'c03140' => 'Compute Module 4 (4 GB)', 'd03140' => 'Compute Module 4 (8 GB)', - 'c04170' => 'Pi 5 (4 GB)', - 'd04170' => 'Pi 5 (8 GB)' + 'c04170' => 'Raspberry Pi 5 (4 GB)', + 'd04170' => 'Raspberry Pi 5 (8 GB)' ); $cpuinfo_array = ''; @@ -155,5 +155,48 @@ class Sysinfo } } } + + /** + * Determines if ad blocking is enabled and active + * + * @return bool $status + */ + public function adBlockStatus(): bool + { + exec('cat '. RASPI_ADBLOCK_CONFIG, $return); + $arrConf = ParseConfig($return); + if (sizeof($arrConf) > 0) { + $enabled = true; + } + exec('pidof dnsmasq | wc -l', $dnsmasq); + $dnsmasq_state = ($dnsmasq[0] > 0); + $status = $dnsmasq_state && $enabled ? true : false; + return $status; + } + + /** + * Determines if a VPN interface is active + * + * @return string $interface + */ + public function getActiveVpnInterface(): ?string + { + $output = shell_exec('ip a 2>/dev/null'); + if (!$output) { + return null; + } + $vpnInterfaces = ['wg0', 'tun0', 'tailscale0']; + + // interface must have an 'UP' status and an IP address + foreach ($vpnInterfaces as $interface) { + if (strpos($output, "$interface:") !== false) { + if (preg_match("/\d+: $interface: .*<.*UP.*>/", $output) && + preg_match("/inet\b.*$interface/", $output)) { + return $interface; + } + } + } + return null; + } } diff --git a/src/RaspAP/UI/Dashboard.php b/src/RaspAP/UI/Dashboard.php new file mode 100644 index 00000000..a08f6a49 --- /dev/null +++ b/src/RaspAP/UI/Dashboard.php @@ -0,0 +1,347 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +namespace RaspAP\UI; + +class Dashboard { + + private string $firewallConfig; + + public function __construct() { + $this->firewallConfig = RASPI_CONFIG.'/networking/firewall.conf'; + } + + /* + * Returns the management page for an associated VPN + * + * @param string $interface + * @return string + */ + public function getVpnManged(?string $interface = null): ?string + { + return match ($interface) { + 'wg0' => '/wg_conf', + 'tun0' => '/openvpn_conf', + 'tailscale0' => '/plugin__Tailscale', + default => null, + }; + } + + /* + * Parses output of iw, extracts frequency (MHz) and classifies + * it as 2.4 or 5 GHz. Returns null if not found + * + * @param string $interface + * @return string frequency + */ + public function getFrequencyBand(string $interface): ?string + { + $output = shell_exec("iw dev " . escapeshellarg($interface) . " info 2>/dev/null"); + if (!$output) { + return null; + } + + if (preg_match('/channel\s+\d+\s+\((\d+)\s+MHz\)/', $output, $matches)) { + $frequency = (int)$matches[1]; + + if ($frequency >= 2400 && $frequency < 2500) { + return "2.4"; + } elseif ($frequency >= 5000 && $frequency < 6000) { + return "5"; + } + } + return null; + } + + /* + * Aggregate function that fetches output of ip and calls + * functions to parse output into discreet network properties + * + * @param string $interface + * @return array + */ + public function getInterfaceDetails(string $interface): array + { + $output = shell_exec('ip a show ' . escapeshellarg($interface)); + if (!$output) { + return [ + 'mac' => _('No MAC Address Found'), + 'ipv4' => 'None', + 'ipv4_netmask' => '-', + 'ipv6' => _('No IPv6 Address Found'), + 'state' => 'unknown' + ]; + } + $cleanOutput = preg_replace('/\s\s+/', ' ', implode(' ', explode("\n", $output))); + + return [ + 'mac' => $this->getMacAddress($cleanOutput), + 'ipv4' => $this->getIPv4Addresses($cleanOutput), + 'ipv4_netmask' => $this->getIPv4Netmasks($cleanOutput), + 'ipv6' => $this->getIPv6Addresses($cleanOutput), + 'state' => $this->getInterfaceState($cleanOutput), + ]; + } + + private function getMacAddress(string $output): string + { + return preg_match('/link\/ether ([0-9a-f:]+)/i', $output, $matches) ? $matches[1] : _('No MAC Address Found'); + } + + private function getIPv4Addresses(string $output): string + { + if (!preg_match_all('/inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/([0-3][0-9])/i', $output, $matches, PREG_SET_ORDER)) { + return 'None'; + } + + $addresses = array_column($matches, 1); + return implode(' ', $addresses); + } + + private function getIPv4Netmasks(string $output): string + { + if (!preg_match_all('/inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/([0-3][0-9])/i', $output, $matches, PREG_SET_ORDER)) { + return '-'; + } + + $netmasks = array_map(fn($match) => long2ip(-1 << (32 - (int)$match[2])), $matches); + return implode(' ', $netmasks); + } + + private function getIPv6Addresses(string $output): string + { + return preg_match_all('/inet6 ([a-f0-9:]+)/i', $output, $matches) && isset($matches[1]) + ? implode(' ', $matches[1]) + : _('No IPv6 Address Found'); + } + + private function getInterfaceState(string $output): string + { + return preg_match('/state (UP|DOWN)/i', $output, $matches) ? $matches[1] : 'unknown'; + } + + public function getWirelessDetails(string $interface): array + { + $output = shell_exec('iw dev ' . escapeshellarg($interface) . ' info'); + if (!$output) { + return ['bssid' => '-', 'ssid' => '-']; + } + $cleanOutput = preg_replace('/\s\s+/', ' ', trim($output)); // Fix here + + return [ + 'bssid' => $this->getConnectedBSSID($cleanOutput), + 'ssid' => $this->getSSID($cleanOutput), + ]; + } + + private function getConnectedBSSID(string $output): string + { + return preg_match('/Connected to (([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2}))/i', $output, $matches) + ? $matches[1] + : '-'; + } + + private function getSSID(string $output): string + { + return preg_match('/ssid ([^\n\s]+)/i', $output, $matches) + ? $matches[1] + : '-'; + } + + /* + * Parses the output of iw to obtain a list of wireless clients + * + * @return integer $clientCount + */ + public function getWirelessClients() + { + exec('iw dev wlan0 station dump', $output, $status); + + if ($status !== 0) { + return 0; + } + // enumerate 'station' entries (each represents a wireless client) + $clientCount = 0; + foreach ($output as $line) { + if (strpos($line, 'Station') === 0) { + $clientCount++; + } + } + return $clientCount; + } + + /* + * Retrieves ethernet neighbors from ARP cache, parses DHCP leases + * to find matching MAC addresses and returns only clients that + * exist in both sources + * + * @return int $ethernetClients + */ + public function getEthernetClients(): int + { + $ethernetClients = []; + + // Get ARP table entries and filter ethernet clients + $arpOutput = shell_exec("ip neigh show"); + if ($arpOutput) { + foreach (explode("\n", trim($arpOutput)) as $line) { + /* match both traditional interface names (eth0...n) and predictable names like + * enp3s0 (PCI ethernet) + * eno1 (onboard ethernet) + * ens160, etc. + * ...ignoring STALE entries + */ + if (preg_match('/^(\S+) dev (eth[0-9]+|en\w+) lladdr (\S+) (REACHABLE|DELAY|PROBE)/', $line, $matches)) { + $ethernetClients[$matches[3]] = $matches[1]; // MAC => IP + } + } + } + + // compare against active DHCP leases + $leaseFile = RASPI_DNSMASQ_LEASES; + if (file_exists($leaseFile)) { + $leases = file($leaseFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $activeLeases = []; + foreach ($leases as $lease) { + $fields = preg_split('/\s+/', $lease); + if (count($fields) >= 3) { + $activeLeases[$fields[1]] = true; // MAC as key + } + } + // keep only clients that exist in the DHCP lease file + $ethernetClients = array_intersect_key($ethernetClients, $activeLeases); + } + return count($ethernetClients); + } + + public function formatClientLabel($clientCount) + { + return ngettext('client', 'clients', $clientCount); + } + + /* + * Determines the device's primary connection type by + * parsing the output of ip route; the interface listed + * as the default gateway is used for internet connectivity. + * + * The following interface classifications are assumed: + * - ethernet (eth0, en*) + * - wireless (wlan0, wlan1, wlp*) + * - tethered USB (usb*, eth1) + * - cellular (ppp0, wwan0, wwp*) + * - fallback + * @return string + */ + public function getConnectionType(): string + { + // get the interface associated with the default route + $interface = trim(shell_exec("ip route show default | awk '{print $5}'")); + + if (empty($interface)) { + return 'unknown'; + } + // classify interface type + if (preg_match('/^eth\d+|enp\d+s\d+/', $interface)) { + return 'ethernet'; + } + if (preg_match('/^wlan\d+|wlp\d+s\d+/', $interface)) { + return 'wireless'; + } + if (preg_match('/^usb\d+|eth1$/', $interface)) { + return 'tethering'; + } + if (preg_match('/^ppp\d+|wwan\d+|wwp\d+s\d+/', $interface)) { + return 'cellular'; + } + + // if none match, return the interface name as a fallback + return "other ($interface)"; + } + + /** + * Returns a fontawesome icon associated with a connection + * type/class + * + * @param $type + * @return string + */ + public function getConnectionIcon($type): string + { + switch (strtolower($type)) { + case 'ethernet': + return 'fa-ethernet'; + case 'wireless': + return 'fa-wifi'; + case 'tethering': + return 'fa-mobile-alt'; + case 'cellular': + return 'fa-broadcast-tower'; + default: + return 'fa-question-circle'; // unknown + } + } + + /** + * Retrieves the firewall's current status + * + * @return bool status + */ + public function firewallEnabled(): bool + { + $conf = array(); + if (file_exists($this->firewallConfig) ) { + $conf = parse_ini_file($this->firewallConfig); + } + if ($conf["firewall-enable"] == 1) { + return true; + } + return false; + } + + /** + * Handles dashboard page actions + * + * @param string $state + * @param array $post + * @param object $status + * @param string $interface + */ + public function handlePageAction(string $state, array $post, $status, string $interface): object + { + if (!RASPI_MONITOR_ENABLED) { + if (isset($post['ifdown_wlan0'])) { + if ($state === 'up') { + $status->addMessage(sprintf(_('Interface %s is going %s'), $interface, _('down')), 'warning'); + exec('sudo ip link set ' .escapeshellarg($interface). ' down'); + $status->addMessage(sprintf(_('Interface %s is %s'), $interface, _('down')), 'success'); + } elseif ($details['state'] === 'unknown') { + $status->addMessage(_('Interface state unknown'), 'danger'); + } else { + $status->addMessage(sprintf(_('Interface %s is already %s'), $interface, _('down')), 'warning'); + } + } elseif (isset($post['ifup_wlan0'])) { + if ($state === 'down') { + $status->addMessage(sprintf(_('Interface %s is going %s'), $interface, _('up')), 'warning'); + exec('sudo ip link set ' .escapeshellarg($interface). ' up'); + exec('sudo ip -s a f label ' .escapeshellarg($interface)); + usleep(250000); + $status->addMessage(sprintf(_('Interface %s is %s'), $interface, _('up')), 'success'); + } elseif ($state === 'unknown') { + $status->addMessage(_('Interface state unknown'), 'danger'); + } else { + $status->addMessage(sprintf(_('Interface %s is already %s'), $interface, _('up')), 'warning'); + } + } + return $status; + } + } + +} + diff --git a/src/RaspAP/UI/Sidebar.php b/src/RaspAP/UI/Sidebar.php index 62408e27..8d79f9de 100644 --- a/src/RaspAP/UI/Sidebar.php +++ b/src/RaspAP/UI/Sidebar.php @@ -16,7 +16,7 @@ class Sidebar { public function __construct() { // Load default sidebar items $this->addItem(_('Dashboard'), 'fa-solid fa-gauge-high', 'wlan0_info', 10); - $this->addItem(_('Hotspot'), 'far fa-dot-circle', 'hostapd_conf', 20, + $this->addItem(_('Hotspot'), 'fas fa-bullseye', 'hostapd_conf', 20, fn() => RASPI_HOTSPOT_ENABLED ); $this->addItem(_('DHCP Server'), 'fas fa-exchange-alt', 'dhcpd_conf', 30, diff --git a/templates/dashboard.php b/templates/dashboard.php index d85653c8..b4c34cda 100755 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -1,149 +1,171 @@ + + + + " name="ifup_wlan0" /> + + " name="ifdown_wlan0" /> + + + - +
+ + +
+
+ +
+
-
-
+
+
+ showMessages(); ?> +

+ +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Network connection +
+
+
+ <?php echo htmlspecialchars($revision, ENT_QUOTES); ?> +
+
:
+
:
+
:
+
:
+
-
-
-
-

-
-
-
- +
+ + +
+ +
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- -
-
-
-
-
-
-
-
-
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 2) : ?> - -
-
+ +
+ +
+
-
-
-
- - - - " name="ifup_wlan0" /> - - " name="ifdown_wlan0" /> - - -
- -
- +
+
+
+
+ + +
+
+
+
- + diff --git a/templates/dhcp/clients.php b/templates/dhcp/clients.php index c0d9d06d..14e9fa47 100644 --- a/templates/dhcp/clients.php +++ b/templates/dhcp/clients.php @@ -20,11 +20,17 @@ - - - + + + + - +
diff --git a/templates/hostapd.php b/templates/hostapd.php index 6e352fb2..e4eaee58 100755 --- a/templates/hostapd.php +++ b/templates/hostapd.php @@ -36,7 +36,7 @@
- +