Stille 4 лет назад
Родитель
Сommit
31f2ee91dd

+ 42 - 0
.github/workflows/vnstat-dashboard.yml

@@ -0,0 +1,42 @@
+name: "vnstat-dashboard docker build"
+
+env:
+  PROJECT: vnstat-dashboard
+
+on:
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    env:
+      ACTIONS_ALLOW_UNSECURE_COMMANDS: true
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Set tag
+        id: tag
+        run: |
+          TAG=$(cat ${{ env.PROJECT }}/Dockerfile | awk 'NR==4 {print $3}')
+          echo "::set-env name=TAG::$TAG"
+      - name: Docker Hub login
+        env:
+          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+        run: |
+          echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
+      - name: Set up Docker Buildx
+        id: buildx
+        uses: crazy-max/ghaction-docker-buildx@v1
+        with:
+          buildx-version: latest
+      - name: Build Dockerfile
+        env:
+          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+        run: |
+          docker buildx build \
+          --platform=linux/amd64,linux/arm64 \
+          --output "type=image,push=true" \
+          --file ${{ env.PROJECT }}/Dockerfile ./${{ env.PROJECT }} \
+          --tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:latest \
+          --tag $(echo "${DOCKER_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ env.PROJECT }}:${TAG}

+ 9 - 0
vnstat-dashboard/.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 17 - 0
vnstat-dashboard/Dockerfile

@@ -0,0 +1,17 @@
+FROM php:7.0-apache
+MAINTAINER Alex Marston <[email protected]>
+
+ENV VERSION 1.0
+# Install Git
+RUN apt-get update && apt-get install -y git unzip
+
+# Install Composer to handle dependencies
+RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
+
+# Copy application source code to html directory
+COPY ./app/ /var/www/html/
+
+# Install dependencies
+RUN composer install
+
+RUN mkdir -p /var/lib/vnstat

+ 8 - 0
vnstat-dashboard/README.md

@@ -0,0 +1,8 @@
+# vnstat-dashboard for docker
+
+GitHub [stilleshan/dockerfile](https://github.com/stilleshan/dockerfile)  
+Docker [stilleshan/vnstat-dashboard](https://hub.docker.com/r/stilleshan/vnstat-dashboard)
+> *docker image support for X86 and ARM*
+
+## 使用
+本仓库参考 [tomangert/vnstat-dashboard](https://github.com/tomangert/vnstat-dashboard) 对原作者仓库 [alexandermarston/vnstat-dashboard](https://github.com/alexandermarston/vnstat-dashboard) 进行部分 bug 修复后构建 docker 镜像,主要用与自用和存档备份,具体使用教程请参考原作者仓库`README`文件.

+ 1 - 0
vnstat-dashboard/_config.yml

@@ -0,0 +1 @@
+theme: jekyll-theme-slate

+ 50 - 0
vnstat-dashboard/app/assets/css/style.css

@@ -0,0 +1,50 @@
+/*
+Copyright (C) 2019 Alexander Marston ([email protected])
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* http://stackoverflow.com/questions/17206631/why-are-bootstrap-tabs-displaying-tab-pane-divs-with-incorrect-widths-when-using */
+/* bootstrap hack: fix content width inside hidden tabs */
+.tab-content > .tab-pane:not(.active),
+.pill-content > .pill-pane:not(.active) {
+    display: block;
+    height: 0;
+    overflow-y: hidden;
+} 
+/* bootstrap hack end */
+
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+  position: relative;
+  min-height: 100%;
+}
+body {
+  margin-bottom: 60px; /* Margin bottom by footer height */
+}
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  height: 60px; /* Set the fixed height of the footer here */
+  line-height: 60px; /* Vertically center the text there */
+  background-color: #f5f5f5;
+}
+.nav-tabs {
+  margin-bottom: 10px;
+}
+.navbar {
+  margin-bottom: 25px;
+}

+ 5 - 0
vnstat-dashboard/app/composer.json

@@ -0,0 +1,5 @@
+{
+    "require": {
+        "smarty/smarty": "~3.1"
+    }
+}

+ 45 - 0
vnstat-dashboard/app/includes/config.php

@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * Copyright (C) 2019 Alexander Marston ([email protected])
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Uncomment to enable error reporting to the screen
+/*ini_set('display_errors', 1);
+ini_set('display_startup_errors', 1);
+ferror_reporting(E_ALL);*/
+
+// Set the default system Timezone
+date_default_timezone_set('Europe/London');
+
+// Path of vnstat
+$vnstat_bin_dir = '/usr/bin/vnstat';
+
+// Path of config file
+/*$vnstat_config = '/etc/vnstat.conf';*/
+
+// linear or logarithmic graphs. Uncomment for logarithmic
+/*$graph_type = 'log';*/
+
+// Set to true to set your own interfaces
+$use_predefined_interfaces = false;
+
+if ($use_predefined_interfaces == true) {
+    $interface_list = ["eth0", "eth1"];
+
+    $interface_name['eth0'] = "Internal #1";
+    $interface_name['eth1'] = "Internal #2";
+}

+ 125 - 0
vnstat-dashboard/app/includes/utilities.php

@@ -0,0 +1,125 @@
+<?php
+
+/*
+ * Copyright (C) 2019 Alexander Marston ([email protected])
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+$logk = log(1024);
+
+function getScale($bytes)
+{
+    global $logk;
+
+    $ui = floor(round(log($bytes)/$logk,3));
+    if ($ui < 0) { $ui = 0; }
+    if ($ui > 8) { $ui = 8; }
+
+    return $ui;
+}
+
+// Get the largest value in an array
+function getLargestValue($array) {
+    return $max = array_reduce($array, function ($a, $b) {
+        return $a > $b['total'] ? $a : $b['total'];
+    });
+}
+
+function getBaseValue($array, $scale)
+{
+    $big = pow(1024,9);
+
+    // Find the smallest non-zero value
+    $sml = array_reduce($array, function ($a, $b) {
+        if  ((1 <= $b['rx']) && ($b['rx'] < $b['tx'])) {
+            $sm = $b['rx'];
+        } else {
+            $sm = $b['tx'];
+        }
+        if (($sm < 1) || ($a < $sm)) {
+            return $a;
+        } else {
+            return $sm;
+        }
+    }, $big);
+
+    if ($sml >= $big/2) {
+        $sml = 1;
+    }
+
+    // divide by scale then round down to a power of 10
+    $base = pow(10,floor(round(log10($sml/pow(1024,$scale)),3)));
+
+    // convert back to bytes
+    $baseByte = $base * pow(1024, $scale);
+
+    // Don't make the bar invisable - must be > 5% difference
+    if ($sml / $baseByte < 1.05) {
+        $base = $base / 10;
+    }
+
+    return $base;
+}
+
+function formatSize($bytes, $vnstatJsonVersion, $decimals = 2) {
+
+    // json version 1 = convert from KiB
+    // json version 2 = convert from bytes
+    if ($vnstatJsonVersion == 1) {
+        $bytes *= 1024;  // convert from kibibytes to bytes
+    }
+
+    return formatBytes($bytes, $decimals);
+}
+
+function getLargestPrefix($scale)
+{
+    $suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+    return $suffixes[$scale];
+}
+
+function formatBytes($bytes, $decimals = 3) {
+
+    $scale = getScale($bytes);
+
+    return round($bytes/pow(1024, $scale), $decimals) .' '. getLargestPrefix($scale);
+}
+
+function formatBytesTo($bytes, $scale, $decimals = 4) {
+
+    if ($bytes == 0) {
+        return '0';
+    }
+
+    return number_format(($bytes / pow(1024, $scale)), $decimals, ".", "");
+}
+
+function kibibytesToBytes($kibibytes, $vnstatJsonVersion) {
+    if ($vnstatJsonVersion == 1) {
+        return $kibibytes *= 1024;
+    } else {
+        return $kibibytes;
+    }
+}
+
+function sortingFunction($item1, $item2) {
+    if ($item1['time'] == $item2['time']) {
+        return 0;
+    } else {
+        return $item1['time'] > $item2['time'] ? -1 : 1;
+    }
+};
+
+?>

+ 284 - 0
vnstat-dashboard/app/includes/vnstat.php

@@ -0,0 +1,284 @@
+<?php
+
+/*
+ * Copyright (C) 2019 Alexander Marston ([email protected])
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class vnStat {
+	protected $executablePath;
+	protected $vnstatVersion;
+	protected $vnstatJsonVersion;
+	protected $vnstatData;
+
+	public function __construct ($executablePath) {
+		if (isset($executablePath)) {
+			$this->executablePath = $executablePath;
+
+			// Execute a command to output a json dump of the vnstat data
+			$vnstatStream = popen("$this->executablePath --json", 'r');
+
+			// Is the stream valid?
+			if (is_resource($vnstatStream)) {
+				$streamBuffer = '';
+
+				while (!feof($vnstatStream)) {
+					$streamBuffer .= fgets($vnstatStream);
+				}
+
+				// Close the handle
+				pclose($vnstatStream);
+
+				$this->processVnstatData($streamBuffer);
+			} else {
+
+			}
+
+
+		} else {
+			die();
+		}
+	}
+
+	private function processVnstatData($vnstatJson) {
+		$decodedJson = json_decode($vnstatJson, true);
+
+		// Check the JSON is valid
+		if (json_last_error() != JSON_ERROR_NONE) {
+			throw new Exception('JSON is invalid');
+		}
+
+		$this->vnstatData = $decodedJson;
+		$this->vnstatVersion = $decodedJson['vnstatversion'];
+		$this->vnstatJsonVersion = $decodedJson['jsonversion'];
+	}
+
+	public function getVnstatVersion() {
+		return $this->vnstatVersion;
+	}
+
+	public function getVnstatJsonVersion() {
+		return $this->vnstatJsonVersion;
+	}
+
+	public function getInterfaces() {
+		// Create a placeholder array
+		$vnstatInterfaces = [];
+
+		foreach($this->vnstatData['interfaces'] as $interface) {
+		    if ($this->vnstatJsonVersion == 1) {
+			array_push($vnstatInterfaces, $interface['id']);
+		    } else {
+			array_push($vnstatInterfaces, $interface['name']);
+		    }
+		}
+
+		return $vnstatInterfaces;
+	}
+
+	public function getInterfaceData($timeperiod, $type, $interface) {
+		// If json version equals 1, add an 's' onto the end of each type.
+		// e.g. 'top' becomes 'tops'
+		$typeAppend = '';
+		if ($this->vnstatJsonVersion == 1) {
+			$typeAppend = 's';
+		}
+
+		// Blank placeholder
+		$trafficData = [];
+		$i = -1;
+
+		// Get the array index for the chosen interface
+		if ($this->vnstatJsonVersion == 1) {
+		    $arrayIndex = array_search($interface, array_column($this->vnstatData['interfaces'], 'id'));
+                } else {
+		    $arrayIndex = array_search($interface, array_column($this->vnstatData['interfaces'], 'name'));
+                }
+ 
+		if ($timeperiod == 'top10') {
+			if ($type == 'table') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['top'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = date('d/m/Y', strtotime($traffic['date']['month'] . "/" . $traffic['date']['day'] . "/" . $traffic['date']['year']));;
+						$trafficData[$i]['rx'] = formatSize($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = formatSize($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = formatSize(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+                                                $trafficData[$i]['totalraw'] = ($traffic['rx'] + $traffic['tx']);
+					}
+				}
+			}
+		}
+
+		if (($this->vnstatJsonVersion > 1) && ($timeperiod == 'five')) {
+			if ($type == 'table') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['fiveminute'] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = date("d/m/Y H:i", mktime($traffic['time']['hour'], $traffic['time']['minute'], 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']));
+                                                $trafficData[$i]['time'] =  mktime($traffic['time']['hour'], $traffic['time']['minute'], 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = formatSize($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = formatSize($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = formatSize(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			} else if ($type == 'graph') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['fiveminute'] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = sprintf("Date(%d, %d, %d, %d, %d)", $traffic['date']['year'], $traffic['date']['month']-1, $traffic['date']['day'], $traffic['time']['hour'], $traffic['time']['minute']);
+                                                $trafficData[$i]['time'] =  mktime($traffic['time']['hour'], $traffic['time']['minute'], 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = kibibytesToBytes($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = kibibytesToBytes($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = kibibytesToBytes(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			}
+		}
+
+		if ($timeperiod == 'hourly') {
+			if ($type == 'table') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['hour'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+                                                if ($this->vnstatJsonVersion == 1) {
+                                                    $hour = $traffic['id'];
+                                                } else {
+                                                    $hour = $traffic['time']['hour'];
+                                                }
+
+						$trafficData[$i]['label'] = date("d/m/Y H:i", mktime($hour, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']));
+                                                $trafficData[$i]['time'] =  mktime($hour, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = formatSize($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = formatSize($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = formatSize(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+
+
+			} else if ($type == 'graph') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['hour'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+                                                if ($this->vnstatJsonVersion == 1) {
+                                                    $hour = $traffic['id'];
+                                                } else {
+                                                    $hour = $traffic['time']['hour'];
+                                                }
+
+						$trafficData[$i]['label'] = sprintf("Date(%d, %d, %d, %d)", $traffic['date']['year'], $traffic['date']['month']-1, $traffic['date']['day'], $hour);
+                                                $trafficData[$i]['time'] =  mktime($hour, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = kibibytesToBytes($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = kibibytesToBytes($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = kibibytesToBytes(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			}
+		}
+
+		if ($timeperiod == 'daily') {
+			if ($type == 'table') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['day'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = date('d/m/Y', mktime(0, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']));
+                                                $trafficData[$i]['time'] =  mktime(0, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = formatSize($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = formatSize($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = formatSize(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			} else if ($type == 'graph') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['day'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = sprintf("Date(%d, %d, %d)", $traffic['date']['year'], $traffic['date']['month']-1, $traffic['date']['day']);
+                                                $trafficData[$i]['time'] =  mktime(0, 0, 0, $traffic['date']['month'], $traffic['date']['day'], $traffic['date']['year']);
+						$trafficData[$i]['rx'] = kibibytesToBytes($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = kibibytesToBytes($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = kibibytesToBytes(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			}
+		}
+
+		if ($timeperiod == 'monthly') {
+			if ($type == 'table') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['month'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+						$trafficData[$i]['label'] = date('F Y', mktime(0, 0, 0, $traffic['date']['month'], 10, $traffic['date']['year']));
+                                                $trafficData[$i]['time'] =  mktime(0, 0, 0, $traffic['date']['month'], 10, $traffic['date']['year']);
+						$trafficData[$i]['rx'] = formatSize($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = formatSize($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = formatSize(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			} else if ($type == 'graph') {
+				foreach ($this->vnstatData['interfaces'][$arrayIndex]['traffic']['month'.$typeAppend] as $traffic) {
+					if (is_array($traffic)) {
+						$i++;
+
+                                                $trafficData[$i]['label'] = sprintf("Date(%d, %d, %d)", $traffic['date']['year'], $traffic['date']['month'] - 1, 10);
+                                                $trafficData[$i]['time'] =  mktime(0, 0, 0, $traffic['date']['month'], 10, $traffic['date']['year']);
+						$trafficData[$i]['rx'] = kibibytesToBytes($traffic['rx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['tx'] = kibibytesToBytes($traffic['tx'], $this->vnstatJsonVersion);
+						$trafficData[$i]['total'] = kibibytesToBytes(($traffic['rx'] + $traffic['tx']), $this->vnstatJsonVersion);
+					}
+				}
+			}
+		}
+
+		if ($timeperiod != 'top10') {
+                    usort($trafficData, 'sortingFunction');
+                }
+
+                if ($type == 'graph') {
+                    // Get the largest value and then prefix (B, KB, MB, GB, etc)
+                    $trafficLargestValue = getLargestValue($trafficData);
+                    $trafficScale = getScale($trafficLargestValue);
+                    $trafficLargestPrefix = getLargestPrefix($trafficScale);
+                    $trafficBase = getBaseValue($trafficData, $trafficScale);
+                    if (($trafficBase < .0099) && ($trafficScale >= 1))
+                    {
+                        $trafficScale = $trafficScale - 1;
+                        $trafficLargestPrefix = getLargestPrefix($trafficScale);
+                        $trafficBase = getBaseValue($trafficData, $trafficScale);
+                    }
+
+                    foreach($trafficData as &$value) {
+                        $value['rx'] = formatBytesTo($value['rx'], $trafficScale);
+                        $value['tx'] = formatBytesTo($value['tx'], $trafficScale);
+                        $value['total'] = formatBytesTo($value['total'], $trafficScale);
+                    }
+
+                    unset($value);
+                    $trafficData[0]['delimiter'] = $trafficLargestPrefix;
+                    $trafficData[0]['base'] = $trafficBase;
+                }
+
+		return $trafficData;
+	}
+}
+
+?>

+ 117 - 0
vnstat-dashboard/app/index.php

@@ -0,0 +1,117 @@
+<?php
+
+/*
+ * Copyright (C) 2019 Alexander Marston ([email protected])
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Require includes
+require __DIR__ . '/vendor/autoload.php';
+require __DIR__ . '/includes/utilities.php';
+require __DIR__ . '/includes/vnstat.php';
+require __DIR__ . '/includes/config.php';
+
+if (isset($vnstat_config)) {
+    $vnstat_cmd = $vnstat_bin_dir.' --config '.$vnstat_config;
+} else {
+    $vnstat_cmd = $vnstat_bin_dir;
+}
+
+if (empty($graph_type)) {
+    $graph_type = 'linear';
+}
+
+// Initiaite vnStat class
+$vnstat = new vnStat($vnstat_cmd);
+
+// Initiate Smarty
+$smarty = new Smarty();
+
+// Set the current year
+$smarty->assign('year', date("Y"));
+
+// Set the list of interfaces
+$interface_list = $vnstat->getInterfaces();
+
+// Set the current interface
+$thisInterface = "";
+
+if (isset($_GET['i'])) {
+    $interfaceChosen = rawurldecode($_GET['i']);
+
+    if (in_array($interfaceChosen, $interface_list, true)) {
+        $thisInterface = $interfaceChosen;
+    } else {
+        $thisInterface = reset($interface_list);
+    }
+} else {
+    // Assume they mean the first interface
+    $thisInterface = reset($interface_list);
+}
+
+$smarty->assign('graph_type', $graph_type);
+
+$smarty->assign('current_interface', $thisInterface);
+
+// Assign interface options
+$smarty->assign('interface_list', $interface_list);
+
+// JsonVersion
+$smarty->assign('jsonVersion', $vnstat->getVnstatJsonVersion());
+
+// Populate table data
+if ($vnstat->getVnstatJsonVersion() > 1) {
+    $fiveData = $vnstat->getInterfaceData('five', 'table', $thisInterface);
+    $smarty->assign('fiveTableData', $fiveData);
+}
+
+$hourlyData = $vnstat->getInterfaceData('hourly', 'table', $thisInterface);
+$smarty->assign('hourlyTableData', $hourlyData);
+
+$dailyData = $vnstat->getInterfaceData('daily', 'table', $thisInterface);
+$smarty->assign('dailyTableData', $dailyData);
+
+$monthlyData = $vnstat->getInterfaceData('monthly', 'table', $thisInterface);
+$smarty->assign('monthlyTableData', $monthlyData);
+
+$top10Data = $vnstat->getInterfaceData('top10', 'table', $thisInterface);
+$smarty->assign('top10TableData', $top10Data);
+
+// Populate graph data
+if ($vnstat->getVnstatJsonVersion() > 1) {
+    $fiveGraphData = $vnstat->getInterfaceData('five', 'graph', $thisInterface);
+    $smarty->assign('fiveGraphData', $fiveGraphData);
+    $smarty->assign('fiveLargestPrefix', $fiveGraphData[0]['delimiter']);
+    $smarty->assign('fiveBase', $fiveGraphData[0]['base']);
+}
+
+$hourlyGraphData = $vnstat->getInterfaceData('hourly', 'graph', $thisInterface);
+$smarty->assign('hourlyGraphData', $hourlyGraphData);
+$smarty->assign('hourlyLargestPrefix', $hourlyGraphData[0]['delimiter']);
+$smarty->assign('hourlyBase', $hourlyGraphData[0]['base']);
+
+$dailyGraphData = $vnstat->getInterfaceData('daily', 'graph', $thisInterface);
+$smarty->assign('dailyGraphData', $dailyGraphData);
+$smarty->assign('dailyLargestPrefix', $dailyGraphData[0]['delimiter']);
+$smarty->assign('dailyBase', $dailyGraphData[0]['base']);
+
+$monthlyGraphData = $vnstat->getInterfaceData('monthly', 'graph', $thisInterface);
+$smarty->assign('monthlyGraphData', $monthlyGraphData);
+$smarty->assign('monthlyLargestPrefix', $monthlyGraphData[0]['delimiter']);
+
+// Display the page
+$smarty->display('templates/site_index.tpl');
+
+?>

+ 16 - 0
vnstat-dashboard/app/templates/module_footer.tpl

@@ -0,0 +1,16 @@
+    <footer class="footer">
+        <div class="container">
+            <span class="text-muted">Copyright (C) {$year} Alexander Marston -
+                <a href="https://github.com/alexandermarston/vnstat-dashboard">vnstat-dashboard</a>
+            </span>
+        </div>
+    </footer>
+
+    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
+    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+{include file="module_graph_js.tpl"}
+
+</body>
+
+</html>

+ 46 - 0
vnstat-dashboard/app/templates/module_graph.tpl

@@ -0,0 +1,46 @@
+    <div class="container">
+        <ul class="nav nav-tabs" id="graphTab" role="tablist">
+            {if $jsonVersion gt 1}
+                <li class="nav-item">
+                    <a class="nav-link active" id="five-graph-tab" data-toggle="tab" href="#five-graph" role="tab" aria-controls="five-graph" aria-selected="true">Five Minute Graph</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" id="hourly-graph-tab" data-toggle="tab" href="#hourly-graph" role="tab" aria-controls="hourly-graph" aria-selected="false">Hourly Graph</a>
+                </li>
+            {else}
+                <li class="nav-item">
+                    <a class="nav-link active" id="hourly-graph-tab" data-toggle="tab" href="#hourly-graph" role="tab" aria-controls="hourly-graph" aria-selected="true">Hourly Graph</a>
+                </li>
+            {/if}
+            <li class="nav-item">
+                <a class="nav-link" id="daily-graph-tab" data-toggle="tab" href="#daily-graph" role="tab" aria-controls="daily-graph" aria-selected="false">Daily Graph</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" id="monthly-graph-tab" data-toggle="tab" href="#monthly-graph" role="tab" aria-controls="monthly-graph" aria-selected="false">Monthly Graph</a>
+            </li>
+        </ul>
+
+        <div class="tab-content">
+            {if $jsonVersion gt 1}
+                <div class="tab-pane fade show active" id="five-graph" role="tabpanel" aria-labelledby="five-graph-tab">
+                    <div id="fiveNetworkTrafficGraph" style="height: 300px;"></div>
+                </div>
+
+                <div class="tab-pane fade" id="hourly-graph" role="tabpanel" aria-labelledby="hourly-graph-tab">
+                    <div id="hourlyNetworkTrafficGraph" style="height: 300px;"></div>
+                </div>
+            {else}
+                <div class="tab-pane fade show active" id="hourly-graph" role="tabpanel" aria-labelledby="hourly-graph-tab">
+                    <div id="hourlyNetworkTrafficGraph" style="height: 300px;"></div>
+                </div>
+            {/if}
+
+            <div class="tab-pane fade" id="daily-graph" role="tabpanel" aria-labelledby="daily-graph-tab">
+                <div id="dailyNetworkTrafficGraph" style="height: 300px;"></div>
+            </div>
+
+            <div class="tab-pane fade" id="monthly-graph" role="tabpanel" aria-labelledby="monthly-graph-tab">
+                <div id="monthlyNetworkTrafficGraph" style="height: 300px;"></div>
+            </div>
+        </div>
+    </div>

+ 256 - 0
vnstat-dashboard/app/templates/module_graph_js.tpl

@@ -0,0 +1,256 @@
+    <script type="text/javascript">
+        google.charts.load('current', { packages: [ 'bar' ] });
+        google.charts.load("current", { packages: [ 'corechart' ] });
+
+        google.charts.setOnLoadCallback(drawFiveChart);
+        google.charts.setOnLoadCallback(drawHourlyChart);
+        google.charts.setOnLoadCallback(drawDailyChart);
+        google.charts.setOnLoadCallback(drawMonthlyChart);
+
+        function drawFiveChart()
+        {
+            {if $jsonVersion gt 1}
+            var data = new google.visualization.DataTable();
+
+            data.addColumn('datetime', 'Time');
+            data.addColumn('number', 'Traffic In');
+            data.addColumn('number', 'Traffic Out');
+            data.addColumn('number', 'Total Traffic');
+
+            data.addRows([
+{foreach from=$fiveGraphData key=key item=value}
+                [new {$value.label}, {$value.rx}, {$value.tx}, {$value.total}],
+{/foreach}
+            ]);
+
+            let endD = (new {$fiveGraphData[0]['label']}).getTime();
+
+            let options = {
+                title: 'Five minute Network Traffic',
+                orientation: 'horizontal',
+                legend: { position: 'right' },
+                explorer: { 
+                    axis: 'horizontal',
+                    zoomDelta: 1.1,
+                    maxZoomIn: 0.1,
+                    maxZoomOut: 10.0
+            	},
+                vAxis: {
+                    format: '###.### {$fiveLargestPrefix}'
+                    {if $graph_type == 'log'}
+                        ,scaleType: 'log',
+                        baseline: {$fiveBase}
+                    {/if}
+                },
+                hAxis: {
+                    direction: -1,
+                    format: 'd/H:mm',
+                    {if $jsonVersion > 1}
+                        title: 'Day/Hour:Minute (Scroll to zoom, Drag to pan)',
+                    {else}
+                        title: 'Day/Hour:Minute',
+                    {/if}
+                    viewWindow: {
+                        min: 'Date('+(endD-7050000).toString()+')',
+                        max: 'Date('+(endD+150000).toString()+')'
+                    },
+                    ticks: [
+{foreach from=$fiveGraphData key=key item=value}
+                        new {$value.label},
+{/foreach}
+                    ]
+                }
+            };
+
+            var formatDate = new google.visualization.DateFormat({ pattern: 'dd/MM/yyyy HH:mm' });
+            formatDate.format(data, 0);
+
+            var formatNumber = new google.visualization.NumberFormat({ pattern: '##.## {$fiveLargestPrefix}' });
+            formatNumber.format(data, 1);
+            formatNumber.format(data, 2);
+            formatNumber.format(data, 3);
+
+            let chart = new google.visualization.BarChart(document.getElementById('fiveNetworkTrafficGraph'));
+            chart.draw(data, google.charts.Bar.convertOptions(options));
+            {/if}
+        }
+
+        function drawHourlyChart()
+        {
+            var data = new google.visualization.DataTable();
+
+            data.addColumn('date', 'Hour');
+            data.addColumn('number', 'Traffic In');
+            data.addColumn('number', 'Traffic Out');
+            data.addColumn('number', 'Total Traffic');
+
+            data.addRows([
+{foreach from=$hourlyGraphData key=key item=value}
+                [new {$value.label}, {$value.rx}, {$value.tx}, {$value.total}],
+{/foreach}
+            ]);
+
+            let endD = (new {$hourlyGraphData[0]['label']}).getTime();
+
+            let options = {
+                title: 'Hourly Network Traffic',
+                orientation: 'horizontal',
+                legend: { position: 'right' },
+                explorer: { 
+                    axis: 'horizontal',
+                    zoomDelta: 1.1,
+                    maxZoomIn: 0.1,
+                    maxZoomOut: 10.0
+            	},
+                vAxis: {
+                    format: '###.### {$hourlyLargestPrefix}'
+                    {if $graph_type == 'log'}
+                        ,scaleType: 'log',
+                        baseline: {$hourlyBase}
+                    {/if}
+                },
+                hAxis: {
+                    {if $jsonVersion > 1}
+                        title: 'Day/Hour (Scroll to zoom, Drag to pan)',
+                    {else}
+                        title: 'Day/Hour',
+                    {/if}
+                    format: 'd/H',
+                    direction: -1,
+                    viewWindow: {
+                        min: 'Date('+(endD-84600000).toString()+')',
+                        max: 'Date('+(endD+1800000).toString()+')'
+                    },
+                    ticks: [
+{foreach from=$hourlyGraphData key=key item=value}
+                        new {$value.label},
+{/foreach}
+                    ]
+                }
+            };
+            
+            var formatDate = new google.visualization.DateFormat({ pattern: 'dd/MM/yyyy HH:mm' });
+            formatDate.format(data, 0);
+            
+            var formatNumber = new google.visualization.NumberFormat({ pattern: '##.## {$hourlyLargestPrefix}' });
+            formatNumber.format(data, 1);
+            formatNumber.format(data, 2);
+            formatNumber.format(data, 3);
+
+            let chart = new google.visualization.BarChart(document.getElementById('hourlyNetworkTrafficGraph'));
+            chart.draw(data, google.charts.Bar.convertOptions(options));
+        }
+
+        function drawDailyChart()
+        {
+            var data = new google.visualization.DataTable();
+
+            data.addColumn('date', 'Day');
+            data.addColumn('number', 'Traffic In');
+            data.addColumn('number', 'Traffic Out');
+            data.addColumn('number', 'Total Traffic');
+
+            data.addRows([
+{foreach from=$dailyGraphData key=key item=value}
+                [new {$value.label}, {$value.rx}, {$value.tx}, {$value.total}],
+{/foreach}
+            ]);
+
+            let endD = (new {$dailyGraphData[0]['label']}).getTime();
+
+            let options = {
+                title: 'Daily Network Traffic',
+                orientation: 'horizontal',
+                legend: { position: 'right' },
+                explorer: { 
+                    axis: 'horizontal',
+                    zoomDelta: 1.1,
+                    maxZoomIn: 0.1,
+                    maxZoomOut: 10.0
+            	},
+                vAxis: {
+                    format: '###.### {$dailyLargestPrefix}'
+                    {if $graph_type == 'log'}
+                        ,scaleType: 'log',
+                        baseline: {$dailyBase}
+                    {/if}
+                },
+                hAxis: {
+                    {if $jsonVersion > 1}
+                        title: 'Day (Scroll to zoom, Drag to pan)',
+                    {else}
+                        title: 'Day',
+                    {/if}
+                    format: 'dd/MM/YYYY',
+                    viewWindow: {
+                        min: 'Date('+(endD-2548800000).toString()+')',
+                        max: 'Date('+(endD+43200000).toString()+')'
+                    },
+                    direction: -1,
+                    ticks: [
+{foreach from=$dailyGraphData key=key item=value}
+                        new {$value.label},
+{/foreach}
+                    ]
+                }
+            };
+
+            var formatDate = new google.visualization.DateFormat({ pattern: 'dd/MM/yyyy' });
+            formatDate.format(data, 0);
+            
+            var formatNumber = new google.visualization.NumberFormat({ pattern: '##.## {$dailyLargestPrefix}' });
+            formatNumber.format(data, 1);
+            formatNumber.format(data, 2);
+            formatNumber.format(data, 3);
+
+            let chart = new google.visualization.BarChart(document.getElementById('dailyNetworkTrafficGraph'));
+            chart.draw(data, google.charts.Bar.convertOptions(options));
+        }
+
+        function drawMonthlyChart()
+        {
+            var data = new google.visualization.DataTable();
+
+            data.addColumn('date', 'Month');
+            data.addColumn('number', 'Traffic In');
+            data.addColumn('number', 'Traffic Out');
+            data.addColumn('number', 'Total Traffic');
+
+            data.addRows([
+{foreach from=$monthlyGraphData key=key item=value}
+                [new {$value.label}, {$value.rx}, {$value.tx}, {$value.total}],
+{/foreach}
+            ]);
+
+            let options = {
+                title: 'Monthly Network Traffic',
+                orientation: 'horizontal',
+                legend: { position: 'right' },
+                explorer: { 
+                    axis: 'horizontal',
+                    zoomDelta: 1.1,
+                    maxZoomIn: 0.1,
+                    maxZoomOut: 10.0
+            	},
+                vAxis: {
+                    format: '##.## {$monthlyLargestPrefix}'
+                },
+                hAxis: {
+                    title: 'Month',
+                    format: 'MMMM YYYY',
+                    direction: -1
+                }
+            };
+            
+            var formatDate = new google.visualization.DateFormat({ pattern: 'MMMM YYYY' });
+            formatDate.format(data, 0);
+            
+            var formatNumber = new google.visualization.NumberFormat({ pattern: '##.## {$monthlyLargestPrefix}' });
+            formatNumber.format(data, 1);
+            formatNumber.format(data, 2);
+            formatNumber.format(data, 3);
+
+            let chart = new google.visualization.BarChart(document.getElementById('monthlyNetworkTrafficGraph'));
+            chart.draw(data, google.charts.Bar.convertOptions(options));
+        }
+    </script>

+ 33 - 0
vnstat-dashboard/app/templates/module_header.tpl

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>Network Traffic</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+
+    <!-- Custom CSS -->
+    <link rel="stylesheet" href="./assets/css/style.css">
+
+</head>
+
+<body>
+    <nav class="navbar sticky-top navbar-light bg-light">
+        <div class="container">
+            <a class="navbar-brand" href="#">Network Traffic ({$current_interface})</a>
+
+            <div class="dropdown">
+                <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                    Interface Selection
+                </button>
+
+                <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+{foreach from=$interface_list item=value}
+                    <a class="dropdown-item" href="?i={$value}">{$value}</a>
+{/foreach}
+                </div>
+            </div>
+        </div>
+    </nav>

+ 146 - 0
vnstat-dashboard/app/templates/module_table.tpl

@@ -0,0 +1,146 @@
+    <div class="container">
+        <ul class="nav nav-tabs" id="tableTab" role="tablist">
+            {if $jsonVersion gt 1}
+                <li class="nav-item">
+                    <a class="nav-link active" id="five-table-tab" data-toggle="tab" href="#five-table" role="tab" aria-controls="five-table" aria-selected="true">Five Minute</a>
+                </li> 
+                <li class="nav-item">
+                    <a class="nav-link" id="hourly-table-tab" data-toggle="tab" href="#hourly-table" role="tab" aria-controls="hourly-table" aria-selected="false">Hourly</a>
+                </li>
+                {else}
+                <li class="nav-item">
+                    <a class="nav-link active" id="hourly-table-tab" data-toggle="tab" href="#hourly-table" role="tab" aria-controls="hourly-table" aria-selected="true">Hourly</a>
+                </li>
+            {/if}
+            <li class="nav-item">
+                <a class="nav-link" id="daily-table-tab" data-toggle="tab" href="#daily-table" role="tab" aria-controls="daily-table" aria-selected="false">Daily</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" id="monthly-table-tab" data-toggle="tab" href="#monthly-table" role="tab" aria-controls="monthly-table" aria-selected="false">Monthly</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" id="top10-table-tab" data-toggle="tab" href="#top10-table" role="tab" aria-controls="top10-table" aria-selected="false">Top 10</a>
+            </li>
+        </ul>
+
+        <div class="tab-content" id="tableTabContent">
+            {if $jsonVersion gt 1}
+            <div class="tab-pane fade show active" id="five-table" role="tabpanel" aria-labelledby="five-table-tab">
+                <table class="table table-bordered">
+                    <thead>
+                        <tr>
+                            <th>Time</th>
+                            <th>Received</th>
+                            <th>Sent</th>
+                            <th>Total</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+{foreach from=$fiveTableData key=key item=value}
+                        <tr>
+                            <td>{$value.label}</td>
+                            <td>{$value.rx}</td>
+                            <td>{$value.tx}</td>
+                            <td>{$value.total}</td>
+                        </tr>
+{/foreach}
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="tab-pane fade" id="hourly-table" role="tabpanel" aria-labelledby="hourly-table-tab">
+            {else}
+            <div class="tab-pane fade show active" id="hourly-table" role="tabpanel" aria-labelledby="hourly-table-tab">
+            {/if}
+                <table class="table table-bordered">
+                    <thead>
+                        <tr>
+                            <th>Hour</th>
+                            <th>Received</th>
+                            <th>Sent</th>
+                            <th>Total</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+{foreach from=$hourlyTableData key=key item=value}
+                        <tr>
+                            <td>{$value.label}</td>
+                            <td>{$value.rx}</td>
+                            <td>{$value.tx}</td>
+                            <td>{$value.total}</td>
+                        </tr>
+{/foreach}
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="tab-pane fade" id="daily-table" role="tabpanel" aria-labelledby="daily-table-tab">
+                <table class="table table-bordered">
+                    <thead>
+                        <tr>
+                            <th>Day</th>
+                            <th>Received</th>
+                            <th>Sent</th>
+                            <th>Total</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+{foreach from=$dailyTableData key=key item=value}
+                        <tr>
+                            <td>{$value.label}</td>
+                            <td>{$value.rx}</td>
+                            <td>{$value.tx}</td>
+                            <td>{$value.total}</td>
+                        </tr>
+{/foreach}
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="tab-pane fade" id="monthly-table" role="tabpanel" aria-labelledby="monthly-table-tab">
+                <table class="table table-bordered">
+                    <thead>
+                        <tr>
+                            <th>Month</th>
+                            <th>Received</th>
+                            <th>Sent</th>
+                            <th>Total</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+{foreach from=$monthlyTableData key=key item=value}
+                        <tr>
+                            <td>{$value.label}</td>
+                            <td>{$value.rx}</td>
+                            <td>{$value.tx}</td>
+                            <td>{$value.total}</td>
+                        </tr>
+{/foreach}
+                    </tbody>
+                </table>
+            </div>
+
+            <div class="tab-pane fade" id="top10-table" role="tabpanel" aria-labelledby="top10-table-tab">
+                <table class="table table-bordered">
+                    <thead>
+                        <tr>
+                            <th>Day</th>
+                            <th>Received</th>
+                            <th>Sent</th>
+                            <th>Total</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+{foreach from=$top10TableData key=key item=value}
+                        <tr>
+                            <td>{$value.label}</td>
+                            <td>{$value.rx}</td>
+                            <td>{$value.tx}</td>
+                            <td>{$value.total}</td>
+                        </tr>
+{/foreach}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>

+ 7 - 0
vnstat-dashboard/app/templates/site_index.tpl

@@ -0,0 +1,7 @@
+{include file="module_header.tpl"}
+
+{include file="module_graph.tpl"}
+
+{include file="module_table.tpl"}
+
+{include file="module_footer.tpl"}

+ 11 - 0
vnstat-dashboard/docker-compose.yml

@@ -0,0 +1,11 @@
+version: '3'
+services:
+  vnstat-dashboard:
+    build: .
+    network_mode: "host"
+    container_name: vnstat-dashboard
+    volumes:
+          - /var/lib/vnstat:/var/lib/vnstat
+          - /usr/bin/vnstat:/usr/local/bin/vnstat
+          - /etc/localtime:/etc/localtime:ro
+          - /var/run/docker.sock:/var/run/docker.sock

+ 0 - 0
vnstat-dashboard/docs/README.md