Dashboard v2 (#15)

* dashboardV2 addition -  still needs some work but not to bad

* Mitigated possible DoS Vulnerability
dev
ioClarity 5 years ago committed by GitHub
parent 08e5bc7e3e
commit d7e7ab587a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -17,3 +17,4 @@ qnvoice
qnrelay qnrelay
qnmodem qnmodem
My_Hosts.txt My_Hosts.txt
dashboardV2/jsonData/*.json

@ -27,3 +27,25 @@ will want to use a hardened server, like apache, and a different index.php file.
Please note that if you are tryping in a URCALL when the webpage is refreshing, Please note that if you are tryping in a URCALL when the webpage is refreshing,
the entry field will loose focus and you'll have to try again. the entry field will loose focus and you'll have to try again.
#### DASHBOARD V2 ####
A new responsive dashboard built on the Bootstrap 4.5 framework has been
added. If you have configured the dashboard with the commands above you can
access the new dashboard by appending "/dashboardV2" to the URL. For example
http://<hostname>.local/dashboardV2
Notes:
The responsive dashboard currently has the following features
* Page does not have to reload to refresh its data solving the URCall focus issue
* Dashboard refresh is suggested to be set at 10 seconds. Any value less than 8 will
have no effect.
* Mobile design friendly. On smaller screens some columns in the tables will be
hidden, however, simply rotate your mobile device to landscape view and they
should appear
* QnRemote section (aka URCall section) has improved design for mobile device use
Future improvements will be made to
* The layout to respect order of the dash_show_order configuration item
* Misc performance enhancements

@ -97,6 +97,7 @@ installbase : $(BASE_PROGRAMS) gwys.txt qn.cfg
/bin/cp -f qnremote qnvoice $(BINDIR) /bin/cp -f qnremote qnvoice $(BINDIR)
/bin/ln -f -s $(shell pwd)/qn.cfg $(CFGDIR) /bin/ln -f -s $(shell pwd)/qn.cfg $(CFGDIR)
/bin/ln -f -s $(shell pwd)/index.php $(WWWDIR) /bin/ln -f -s $(shell pwd)/index.php $(WWWDIR)
/bin/ln -f -s $(shell pwd)/dashboardV2 $(WWWDIR)
/bin/cp -f defaults $(CFGDIR) /bin/cp -f defaults $(CFGDIR)
/bin/cp -f system/qngateway.service $(SYSDIR) /bin/cp -f system/qngateway.service $(SYSDIR)
systemctl enable qngateway.service systemctl enable qngateway.service
@ -174,6 +175,7 @@ installdash : index.php
/usr/bin/apt install -y php-common php-fpm sqlite3 php-sqlite3 /usr/bin/apt install -y php-common php-fpm sqlite3 php-sqlite3
mkdir -p $(WWWDIR) mkdir -p $(WWWDIR)
/bin/ln -f -s $(shell pwd)/index.php $(WWWDIR) /bin/ln -f -s $(shell pwd)/index.php $(WWWDIR)
/bin/ln -f -s $(shell pwd)/dashboardV2 $(WWWDIR)
/bin/cp -f system/qndash.service $(SYSDIR) /bin/cp -f system/qndash.service $(SYSDIR)
systemctl enable qndash.service systemctl enable qndash.service
systemctl daemon-reload systemctl daemon-reload
@ -262,4 +264,5 @@ uninstalldash :
/bin/rm -f $(SYSDIR)/qndash.service /bin/rm -f $(SYSDIR)/qndash.service
systemctl daemon-reload systemctl daemon-reload
/bin/rm -f $(WWWDIR)/index.php /bin/rm -f $(WWWDIR)/index.php
/bin/rm -f $(WWWDIR)/dashboardV2
/bin/rm -f $(CFGDIR)/qn.db /bin/rm -f $(CFGDIR)/qn.db

@ -0,0 +1,144 @@
<?php
# Load functions and read config file
include '../init.php';
$lastRunFile = "/tmp/lastJsonGen.tmp";
$lastRunTime = filemtime($lastRunFile);
$now = time();
# Only Generage jSon Data if it hasnt been generated in the last 8 seconds
if( ($now-$lastRunTime) > 8 )
{
# OpenDatabase
$dbname = $cfgdir.'/qn.db';
$db = new SQLite3($dbname, SQLITE3_OPEN_READONLY);
# Only proccess if defined in show list
if( in_array("LH", $showlist) ) {
$jsonArray = [];
$ss = 'SELECT callsign,sfx,message,module,reflector,maidenhead,latitude,longitude,strftime("%s","now")-lasttime as lastTime FROM LHEARD ORDER BY lastTime LIMIT '.GetCFGValue('dash_lastheard_count').' ';
if ($stmnt = $db->prepare($ss)) {
if ($result = $stmnt->execute()) {
while ($row = $result->FetchArray(SQLITE3_ASSOC)) {
//transform the lastTimeHeard to a printable string
$row['lastTime'] = SecToString($row['lastTime']);
$row['maidenheadProcessed'] = Maidenhead($row['maidenhead'], $row['latitude'], $row['longitude']);
$row['callsignProcessed'] = MyAndSfxToQrz($row['callsign'], $row['sfx']);
$jsonArray[] = $row;
}
$result->finalize();
}
$stmnt->close();
}
# Write the lastHeard JSON file
$lhJsonFile = fopen("../jsonData/lastHeard.json", "w");
fwrite($lhJsonFile, json_encode($jsonArray));
fclose($lhJsonFile);
} else { echo "Section disabled";
$lhJsonFile = fopen("../jsonData/lastHeard.json", "w");
fwrite($lhJsonFile, "{ }\n");
fclose($lhJsonFile);
}
# Only proccess if defined in show list
if( in_array("MO", $showlist) ) {
$jsonArray = [];
foreach (array('a', 'b', 'c') as $mod) {
$linkstatus = 'Unlinked';
$address = '';
$ctime = '';
$module = 'module_'.$mod;
if (array_key_exists($module, $cfg)) {
$freq = 0.0;
if (array_key_exists($module.'_tx_frequency', $cfg)) {
$freq = $cfg[$module.'_tx_frequency'];
}
else if (array_key_exists($module.'_frequency', $cfg)) {
$freq = $cfg[$module.'_frequency'];
}
$ss = 'SELECT ip_address,to_callsign,to_mod,strftime("%s","now")-linked_time as linkedTime FROM LINKSTATUS WHERE from_mod=' . "'" . strtoupper($mod) . "';";
if ($stmnt = $db->prepare($ss)) {
if ($result = $stmnt->execute()) {
if ($row = $result->FetchArray(SQLITE3_ASSOC)) {
$row['linkedTime'] = SecToString(intval($row['linkedTime']));
$row['module'] = strtoupper($mod);
$row['modem'] = $cfg[$module];
$row['freq'] = $freq;
$row['link'] = $row['to_callsign']." ".$row['to_mod'];
$jsonArray[] = $row;
} else {
$jsonArray[] = array('linkedTime' => '',
'module' =>strtoupper($mod),
'modem' => $cfg[$module],
'freq' => $freq,
'link' => 'Unlinked',
'ip_address' => '',
'to_callsign' => '',
'to_mod' => '');
}
$result->finalize();
}
$stmnt->close();
}
}
}
$modJsonFile = fopen("../jsonData/modules.json", "w");
fwrite($modJsonFile, json_encode($jsonArray));
fclose($modJsonFile);
} else {
$modJsonFile = fopen("../jsonData/modules.json", "w");
fwrite($modJsonFile, "{ }\n");
fclose($modJsonFile);
}
# Close database it is not needed anymore
$db->Close();
# Only proccess if defined in show list
if( in_array("PS", $showlist) ) {
$jsonArray = [];
$lines = explode("\n", `ps -eo user,pid,pcpu,size,cmd | grep -e qngateway -e qnlink -e qndtmf -e qndvap -e qnitap -e qnrelay -e qndvrptr -e qnmodem -e MMDVMHost | grep -v grep`);
foreach ($lines as $line) {
$items = preg_split ('/\s+/', $line, 5);
if( isset( $items[1] ) ) {
$jsonArray[] = array('user' => $items[0],
'pid' => $items[1],
'pcpu' => $items[2],
'size' => $items[3],
'cmd' => $items[4]);
}
}
$psJsonFile = fopen("../jsonData/ps.json", "w");
if ($jsonArray) {
fwrite($psJsonFile, json_encode($jsonArray));
} else {
fwrite($psJsonFile, "{ }\n");
}
fclose($psJsonFile);
} else {
# Section is disabled, replace with blank JSON file
$psJsonFile = fopen("../jsonData/ps.json", "w");
fwrite($psJsonFile, "{ }\n");
fclose($psJsonFile);
}
# Update last run time
`touch $lastRunFile`;
}
# If the jsonFile is in the URL lets get the file
if( isset($_GET['jsonFile']) )
{
if( $_GET['jsonFile'] == "lastHeard" )
readfile("../jsonData/lastHeard.json");
else if( $_GET['jsonFile'] == "modules" )
readfile("../jsonData/modules.json");
else if( $_GET['jsonFile'] == "ps" )
readfile("../jsonData/ps.json");
}
?>

@ -0,0 +1,22 @@
<!DOCTYPE html>
<?php
# Load functions and read config file
include '../init.php';
# Only proccess if defined in show list
if( in_array("UR", $showlist) ) {
if ( isset($_POST['URCall']) && isset($_POST['mod'])) {
$urcall = str_replace(' ', '_', trim(preg_replace('/[^0-9a-z_ ]/', '', strtolower($_POST['URCall']))));
if (strlen($urcall)>0 && strlen($_POST['mod'])>0) {
$command = 'qnremote '.strtolower($_POST['mod']).' '.strtolower($cfg['ircddb_login']).' '.$urcall;
$unused = `$command`;
# Return the command sent for the front-end display
echo $command;
}
}
} else {
echo "Section disabled";
}
?>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,294 @@
<!DOCTYPE html>
<?php
# If visiting this page by /dashboardV2 (no / suffix) it causes issues referring to CSS / JS
# So we check to see the URL and redirect to /dashboardV2/ if we need to
function endsWith($string, $test) {
$strlen = strlen($string);
$testlen = strlen($test);
if ($testlen > $strlen) return false;
return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
}
if ( !( endsWith($_SERVER['REQUEST_URI'], "/index.php") || (endsWith($_SERVER['REQUEST_URI'], "/") ) ) )
{
header("Location: dashboardV2/");
}
# Load functions and read config file
include 'init.php';
?>
<html lang="en">
<head>
<title>QnetGateway Dashboard V2</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="css/bootstrap-table.min.css" rel="stylesheet">
</head>
<body>
<!-- Include jQuery and Bootstrap-->
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<!-- Header - maybe turn this into a Nav Bar in the future -->
<nav class="navbar navbar-light bg-light">
<div class="navbar-header navbar-default">
<a class="navbar-brand" href="#">QnetGateway <?php echo GetCFGValue('ircddb_login'); ?> Dashboard</a>
</div>
</nav>
<br>
<!-- Large fixed width container for our layout -->
<div class="container-lg">
<?php if( in_array("LH", $showlist) ) {
# Only show this section if LH is in the show list
?>
<div class="row"> <!-- R1 Start -->
<div class="col-md-12"> <!-- R1C1 Start-->
<div class="card border-dark mb-3"> <!-- Start of LH Card -->
<div class="card-body text-dark">
<h5 class="card-title">Last Heard</h5>
<table class="table table-sm"
id="lhTable"
data-auto-refresh="true"
data-auto-refresh-interval="<?php echo GetCFGValue('dash_refresh'); ?>"
data-pagination="false"
data-url="bin/getJson.php?jsonFile=lastHeard"
data-check-on-init="true">
<thead>
<tr class="d-flex">
<th data-field="callsignProcessed" data-sortable="false">CallSign/Suffix</th>
<th scope="col" data-field="message" data-sortable="false" class="d-none d-md-table-cell">Message</th>
<th scope="col" data-field="maidenheadProcessed" data-sortable="false" class="d-none d-md-table-cell">Maidenhead</th>
<th scope="col" data-field="module" data-sortable="false" class="d-none d-sm-table-cell">Module</th>
<th data-field="reflector" data-sortable="false">Via</th>
<th data-field="lastTime" data-sortable="false">Time</th>
</tr>
</thead>
</table>
</div>
</div> <!-- End of LH Card -->
</div> <!-- R1C1 End -->
</div> <!-- R1 End -->
<?php } ?>
<div class="row"> <!-- R2 Start -->
<div class="col-md-6"> <!-- R2C1 Start -->
<?php if( in_array("UR", $showlist) ) {
# If UR is in show list - dispaly QnRemote card
?>
<div class="card border-dark mb-3"> <!-- Start of UR Card -->
<div class="card-body text-dark">
<h5 class="card-title">QnRemote Control</h5>
<div id="last_cmd_sent"> </div> <!-- Area for Last Command dispaly -->
<form name="URCall_form">
<fieldset>
<div class="input-group">
<div class="input-group-prepend"> <!-- Module button group -->
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<?php
# Code to determine which module buttons are going to be disabled
# or enabled. If they are in the config, enable them for use, otherwise
# keep them disabled
$somethingChecked = false;
foreach (array('a', 'b', 'c') as $mod) {
$module = 'module_'.$mod;
$modUpper = strtoupper($mod);
#
# If module is configured, make the button active
if (array_key_exists($module, $cfg)) {
echo '<label class="btn btn-primary active" >';
echo '<input type="radio" name="moduleSelection" id="mod'.$modUpper.'opt" value="'.$modUpper.'" ';
if( ! $somethingChecked ) {
echo 'checked';
$somethingChecked = true;
}
echo '/>'.$modUpper;
echo '</label>';
} else {
#if the button is not configured, make the button disabled
echo '<label class="btn btn-outline-primary disabled" >';
echo '<input type="radio" name="moduleSelection" id="mod'.$modUpper.'opt" value="'.$modUpper.'" />'.$modUpper;
echo '</label>';
}
}
?>
</div>
</div>
<!-- URCall data input Field -->
<input type="text" class="form-control" placeholder="URCall data" id="URCall_field" maxlength="8" />
<div class="input-group-append"> <!-- Submit button -->
<input class="btn btn-primary button" type="submit" value="Submit" id="URCall_SubmitBtn" />
</div>
</div>
</fieldset>
</form>
</div>
</div> <!-- End of UR card -->
<?php } if( in_array("MO", $showlist) ) {
# Show card if MO is in the show list
?>
<div class="card border-dark mb-3"> <!-- Start of MO Card -->
<div class="card-body text-dark">
<h5 class="card-title">Configured Modules</h5>
<table class="table table-sm"
id="modulesTable"
data-auto-refresh="true"
data-auto-refresh-interval="<?php echo GetCFGValue('dash_refresh'); ?>"
data-pagination="false"
data-card-view="true"
data-url="bin/getJson.php?jsonFile=modules">
<thead>
<tr class="d-flex">
<th data-field="module" data-sortable="false">Module</th>
<th data-field="modem" data-sortable="false">Modem</th>
<th data-field="freq" data-sortable="false">Frequency</th>
<th data-field="link" data-sortable="false">Link</th>
<th data-field="linkedTime" data-sortable="false">Linked Time</th>
<th data-field="ip_address" data-sortable="false">Link IP</th>
</tr>
</thead>
</table>
</div>
</div> <!-- End of MO card -->
<?php } ?>
</div> <!-- R2C1 End -->
<div class="col-xs-12 col-md-6"> <!-- R2C2 Start -->
<?php if( in_array("SY", $showlist) ) {
# Only show this card if the SY was listed in the show list
?>
<div class="card border-dark mb-3"> <!-- SY Card Start -->
<div class="card-body text-dark">
<h5 class="card-title">System Information</h5>
<table class="table table-sm table-hover" cellpadding="1" style="font-family: monospace">
<?php
# Code to Generate the System Information for the SY card
$hn = trim(`uname -n`);
$kn = trim(`uname -rmo`);
$osinfo = file('/etc/os-release', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($osinfo as $line) {
list( $key, $value ) = explode('=', $line);
if ($key == 'PRETTY_NAME') {
$os = trim($value, '"');
}
}
$cu = trim(`cat /proc/cpuinfo | grep Model`);
if (0 == strlen($cu))
$cu = trim(`cat /proc/cpuinfo | grep "model name"`);
$culist = explode("\n", $cu);
$mnlist = explode(':', $culist[0]);
$cu = trim($mnlist[1]);
if (count($culist) > 1)
$cu .= ' ' . count($culist) . ' Threads';
if (file_exists('/opt/vc/bin/vcgencmd'))
$cu .= ' ' . str_replace("'", '&deg;', trim(`/opt/vc/bin/vcgencmd measure_temp`));
?>
<tr>
<th scope="row" style="text-align:right">CPU</th>
<td><?php echo $cu; ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">Kernel</th>
<td><?php echo $kn; ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">OS</th>
<td><?php echo $os; ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">Hostname</th>
<td><?php echo $hn; ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">Internal IP</th>
<td><?php echo GetIP('internal'); ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">IPV4</th>
<td><?php echo GetIP('ipv4'); ?></td>
</tr>
<tr>
<th scope="row" style="text-align:right">IPV6</th>
<td><?php echo GetIP('ipv6'); ?></td>
</tr>
</table>
</div>
</div> <!-- End of SY Card -->
<?php } ?>
</div> <!-- R2C2 End -->
</div> <!-- R2 End -->
<?php if( in_array("PS", $showlist) ) {
# Only show 3rd row if PS is in show list
?>
<div class="row"> <!-- R3 Start -->
<div class="col-12"> <!-- R3C1 Start -->
<div class="card border-dark mb-3"> <!-- PS Card Start -->
<div class="card-body text-dark">
<h5 class="card-title">Processes</h5>
<table class="table table-sm"
id="procTable"
data-auto-refresh="true"
data-auto-refresh-interval="<?php echo GetCFGValue('dash_refresh'); ?>"
data-pagination="false"
data-url="bin/getJson.php?jsonFile=ps">
<thead>
<tr class="d-flex">
<th scope="col" data-field="user" data-sortable="false" class="d-none d-sm-table-cell">User</th>
<th data-field="pid" data-sortable="false">PID</th>
<th scope="col" data-field="pcpu" data-sortable="false" class="d-none d-md-table-cell">CPU</th>
<th scope="col" data-field="size" data-sortable="false" class="d-none d-md-table-cell">Mem</th>
<th data-field="cmd" data-sortable="false">Command</th>
</tr>
</thead>
</table>
</div>
</div> <!-- End of PS card -->
</div> <!-- End of R3C1 -->
</div> <!-- End of R3 -->
<?php } ?>
</div> <!-- End of Container -->
<!-- Bootstrap table Javascript -->
<script src="js/bootstrap-table.min.js"></script>
<script src="js/bootstrap-table-auto-refresh.min.js"></script>
<!-- Enable Bootstrap tables and jQuery AJAX for QnRemote Control -->
<script>
$(function() {
$('#lhTable').bootstrapTable()
$('#modulesTable').bootstrapTable()
$('#procTable').bootstrapTable()
$(".button").click(function() {
var processMod = $("input[type='radio'][name='moduleSelection']:checked").val();
var processURCall = $("input#URCall_field").val();
var dataString = 'mod='+ processMod + '&URCall=' + processURCall;
$.ajax({
type: "POST",
url: "bin/qnRemoteCmd.php",
data: dataString,
success: function( returnData ) {
$('#last_cmd_sent').html("<div class=\"pb-2\" id=\"message\"></div>");
$('#message').html("Last command sent: <kbd>" + returnData + "</kbd><br>")
.hide()
.fadeIn( 1500 );
$("input#URCall_field").val("");
}
});
return false; //Dont actually submit the form, let the above JS do the work
});
});
</script>
</body>
</html>

@ -0,0 +1,119 @@
<?php
$cfg = array();
$defaults = array();
$cfgdir = '/usr/local/etc';
function ParseKVFile(string $filename, &$kvarray)
{
if ($lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)) {
foreach ($lines as $line) {
$line = trim($line);
if ($line[0] == '#') continue;
if (! strpos($line, '=')) continue;
list( $key, $value ) = explode('=', $line);
if ("'" == $value[0])
list ( $value ) = explode("'", substr($value, 1));
else
list ( $value ) = explode(' ', $value);
$value = trim($value);
$kvarray[$key] = $value;
}
}
}
function GetCFGValue(string $key)
{
global $cfg, $defaults;
if (array_key_exists($key, $cfg))
return $cfg[$key];
if ('module_' == substr($key, 0, 7)) {
$mod = substr($key, 0, 8);
if (array_key_exists($mod, $cfg)) {
$key = $cfg[$mod].substr($key, 8);
if (array_key_exists($key, $defaults))
return $defaults[$key];
}
} else {
if (array_key_exists($key.'_d', $defaults))
return $defaults[$key.'_d'];
}
return '';
}
function GetIP(string $type)
{
if ('internal' == $type) {
$iplist = explode(' ', `hostname -I`);
foreach ($iplist as $ip) {
if (strpos($ip, '.')) break;
}
} else if ('ipv6' == $type)
$ip = trim(`curl --silent -6 icanhazip.com`);
else if ('ipv4' == $type)
$ip = trim(`curl --silent -4 icanhazip.com`);
else
$ip = '';
return $ip;
}
function SecToString(int $sec) {
if ($sec >= 86400)
return sprintf("%0.2f days", $sec/86400);
$hrs = intdiv($sec, 3600);
$sec %= 3600;
$min = intdiv($sec, 60);
$sec %= 60;
if ($hrs) return sprintf("%2d hr %2d min", $hrs, $min);
if ($min) return sprintf("%2d min %2d sec", $min, $sec);
return sprintf("%2d sec", $sec);
}
function MyAndSfxToQrz(string $my, string $sfx)
{
$my = trim($my);
$sfx = trim($sfx);
if (0 == strlen($my)) {
$my = 'Empty MYCall ';
} else {
if (strpos($my, ' '))
$link = strstr($my, ' ', true);
else
$link = $my;
if (strlen($sfx))
$my .= '/'.$sfx;
$len = strlen($my);
$my = '<a target="_blank" href="https://www.qrz.com/db/'.$link.'">'.$my.'</a>';
while ($len < 13) {
$my .= ' ';
$len += 1;
}
}
return $my;
}
function Maidenhead(string $maid, float $lat, float $lon)
{
$str = trim($maid);
if (6 > strlen($str))
return $maid;
if ($lat >= 0.0)
$slat = '+'.$lat;
else
$slat = $lat;
if ($lon >= 0.0)
$slon = '+'.$lon;
else
$slon = $lon;
$str = '<a target="_blank" href="https://www.google.com/maps?q='.$slat.','.$slon.'">'.$maid.'</a>';
return $str;
}
ParseKVFile($cfgdir.'/qn.cfg', $cfg);
ParseKVFile($cfgdir.'/defaults', $defaults);
$showorder = GetCFGValue('dash_show_order');
$showlist = explode(',', trim($showorder));
?>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.