├── server
├── assets
│ ├── btns
│ │ ├── new.png
│ │ ├── clean.png
│ │ ├── logo.png
│ │ ├── reboot.png
│ │ ├── shutdown.png
│ │ └── update.png
│ ├── devices
│ │ ├── linux.png
│ │ └── raspberrypi.png
│ ├── favicons
│ │ ├── linux.ico
│ │ ├── linux_light.ico
│ │ └── raspberrypi.ico
│ ├── css
│ │ ├── login.css
│ │ ├── index.css
│ │ └── common.css
│ ├── js
│ │ ├── login.js
│ │ ├── solid-gauge.js
│ │ ├── common.js
│ │ ├── exporting.js
│ │ └── index.js
│ └── views
│ │ ├── login.tmpl
│ │ └── index.tmpl
├── server_test.go
└── server.go
├── screenshots
├── screenshot_index.png
├── screenshot_login.png
├── screenshot_index_dark.png
└── screenshot_login_dark.png
├── .gitignore
├── go.mod
├── config
└── config.go
├── device
├── device_test.go
└── device.go
├── Makefile
├── README.md
├── main.go
├── CHANGELOG.md
└── LICENSE
/server/assets/btns/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/new.png
--------------------------------------------------------------------------------
/server/assets/btns/clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/clean.png
--------------------------------------------------------------------------------
/server/assets/btns/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/logo.png
--------------------------------------------------------------------------------
/server/assets/btns/reboot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/reboot.png
--------------------------------------------------------------------------------
/server/assets/btns/shutdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/shutdown.png
--------------------------------------------------------------------------------
/server/assets/btns/update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/update.png
--------------------------------------------------------------------------------
/server/assets/devices/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/devices/linux.png
--------------------------------------------------------------------------------
/screenshots/screenshot_index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_index.png
--------------------------------------------------------------------------------
/screenshots/screenshot_login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_login.png
--------------------------------------------------------------------------------
/server/assets/favicons/linux.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/linux.ico
--------------------------------------------------------------------------------
/screenshots/screenshot_index_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_index_dark.png
--------------------------------------------------------------------------------
/screenshots/screenshot_login_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_login_dark.png
--------------------------------------------------------------------------------
/server/assets/devices/raspberrypi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/devices/raspberrypi.png
--------------------------------------------------------------------------------
/server/assets/favicons/linux_light.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/linux_light.ico
--------------------------------------------------------------------------------
/server/assets/favicons/raspberrypi.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/raspberrypi.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | command.md
18 | build/
19 |
--------------------------------------------------------------------------------
/server/assets/css/login.css:
--------------------------------------------------------------------------------
1 | /*
2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
3 | @Description: Golang implementation of pi-dashboard
4 | @Author: github.com/plutobell
5 | @Creation: 2020-08-01
6 | @Last modification: 2021-09-02
7 | @Version: 1.6.0
8 | */
9 |
10 | .box-radius {
11 | border-radius: var(--box-radius);
12 | }
13 | #login-box {
14 | width: 70%;
15 | margin: 0 auto;
16 | margin-top: 30%;
17 | }
18 |
19 | input:focus {
20 | outline: none !important;
21 | box-shadow: none !important;
22 | border: 1px solid var(--label-color) !important;
23 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/plutobell/pi-dashboard-go
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/gorilla/sessions v1.2.1
7 | github.com/labstack/echo-contrib v0.14.1
8 | github.com/labstack/echo/v4 v4.10.2
9 | github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
10 | github.com/mattn/go-isatty v0.0.18 // indirect
11 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
12 | github.com/shirou/gopsutil/v3 v3.23.3
13 | github.com/shoenig/go-m1cpu v0.1.5 // indirect
14 | golang.org/x/crypto v0.7.0 // indirect
15 | golang.org/x/sys v0.7.0 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/server/server_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/plutobell/pi-dashboard-go/config"
8 | )
9 |
10 | func Test_getNowUsernameAndPassword(t *testing.T) {
11 | if username, password := getNowUsernameAndPassword(); username+":"+password == config.Auth {
12 | t.Log("Pass")
13 | } else {
14 | t.Error("Fail")
15 | }
16 | }
17 |
18 | func Test_getFileSystem(t *testing.T) {
19 | if _, ok := getFileSystem(false, "btns").(http.FileSystem); ok {
20 | t.Log("Pass")
21 | } else {
22 | t.Error("Fail")
23 | }
24 | }
25 |
26 | func Test_getRandomString(t *testing.T) {
27 | if res := getRandomString(16); len(res) == 16 {
28 | t.Log("Pass")
29 | } else {
30 | t.Error("Fail")
31 | }
32 | }
33 |
34 | func Test_getLatestVersionFromGitHub(t *testing.T) {
35 | if nowVersion, _, downloadURL := getLatestVersionFromGitHub(); nowVersion != "" && len(downloadURL) > 0 {
36 | t.Log("Pass")
37 | } else {
38 | t.Error("Fail")
39 | }
40 | }
41 |
42 | func Test_isRootUser(t *testing.T) {
43 | if res := isRootUser(); res == true || res == false {
44 | t.Log("Pass")
45 | } else {
46 | t.Error("Fail")
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | // @Description: Golang implementation of pi-dashboard
3 | // @Author: github.com/plutobell
4 | // @Creation: 2020-08-01
5 | // @Last modification: 2023-04-05
6 | // @Version: 1.7.0
7 |
8 | package config
9 |
10 | import "os/user"
11 |
12 | const (
13 | //PROJECT 项目地址
14 | PROJECT string = "https://github.com/plutobell/pi-dashboard-go"
15 | //AUTHOR 作者信息
16 | AUTHOR string = "github:plutobell"
17 | //VERSION 版本信息
18 | VERSION string = "1.7.0"
19 | //USERNAME 默认用户
20 | USERNAME string = "pi"
21 | //PASSWORD 默认密码
22 | PASSWORD string = "123"
23 | )
24 |
25 | var (
26 | Help bool
27 | Version bool
28 | // Port 端口
29 | Port string
30 | // Title 网站标题
31 | Title string
32 | // Net 网卡名称
33 | Net string
34 | // Disk 硬盘路径
35 | Disk string
36 | // Auth 用户名和密码
37 | Auth string
38 | // Interval 页面更新间隔
39 | Interval string
40 | // SessionMaxAge 登录状态有效期
41 | SessionMaxAge string
42 | // 启用日志显示
43 | EnableLogger bool
44 | // SessionName Session名称
45 | SessionName string
46 | // FileName 当前文件名
47 | FileName string
48 | // LinuxUserInfo 当前Linux用户信息
49 | LinuxUserInfo *user.User
50 | // Theme 主题
51 | Theme string
52 | )
53 |
--------------------------------------------------------------------------------
/device/device_test.go:
--------------------------------------------------------------------------------
1 | package device
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func Test_Popen(t *testing.T) {
9 | if res, err := Popen("uptime"); res != "False" && err == nil {
10 | t.Log("Pass")
11 | } else {
12 | t.Error("Fail")
13 | }
14 | }
15 |
16 | func Benchmark_TimeConsumingFunction(b *testing.B) {
17 | for i := 0; i < 10000; i++ {
18 | Info()
19 | }
20 | }
21 |
22 | func Test_resolveTime(t *testing.T) {
23 | if uptime := resolveTime("1000000"); uptime == "11 days 13:46" {
24 | t.Log("Pass")
25 | } else {
26 | t.Error("Fail")
27 | }
28 | }
29 |
30 | func Test_bytesRound(t *testing.T) {
31 | if last := bytesRound(1073741824, 2); last == "1.0GB" {
32 | t.Log("Pass")
33 | } else {
34 | t.Error("Fail")
35 | }
36 | }
37 |
38 | func Test_struct2Map(t *testing.T) {
39 | host := new(Host)
40 | host.Get()
41 | hostMap, _ := struct2Map(host, "json")
42 | if typeOf := reflect.TypeOf(hostMap); typeOf.Kind() == reflect.Map {
43 | t.Log("Pass")
44 | } else {
45 | t.Error("Fail")
46 | }
47 | }
48 |
49 | func Test_mergeMap(t *testing.T) {
50 | map1 := make(map[string]interface{})
51 | map2 := map[string]interface{}{"a": "Apple"}
52 | if map1 := mergeMap(map1, map2); map1["a"] == "Apple" {
53 | t.Log("Pass")
54 | } else {
55 | t.Error("Fail")
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/assets/js/login.js:
--------------------------------------------------------------------------------
1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | // @Description: Golang implementation of pi-dashboard
3 | // @Author: github.com/plutobell
4 | // @Creation: 2020-08-01
5 | // @Last modification: 2021-09-02
6 | // @Version: 1.6.0
7 |
8 | $("form").keyup(function(event){
9 | if(event.keyCode == 13){
10 | $("#login-btn").trigger("click");
11 | }
12 | });
13 |
14 | $("#login-btn").click(function(){
15 | $("#login-btn").attr("disabled", true);
16 | $("input").attr("disabled", true);
17 |
18 | var username = $("#username").val();
19 | var password = $("#password").val();
20 | var json = {
21 | "username": username,
22 | "password": password,
23 | };
24 | if (username == "" || password == "") {
25 | $("#login-tips").text("Username or password is empty")
26 | $("#login-btn").attr("disabled", false);
27 | $("input").attr("disabled", false);
28 | } else {
29 | $.ajaxSetup(csrfAddToAjaxHeader());
30 | $.post('/api/login', JSON.stringify(json), function(result){
31 | if (result.status == true) {
32 | $("#login-tips").text("")
33 | $(window).attr('location','/');
34 | } else if (result.status == false) {
35 | $("#login-tips").text("Wrong credentials")
36 | $("#login-btn").attr("disabled", false);
37 | $("input").attr("disabled", false);
38 | }
39 | }).fail(function() {
40 | $("#login-tips").text("Unknown error")
41 | $("#login-btn").attr("disabled", false);
42 | $("input").attr("disabled", false);
43 | });
44 | }
45 |
46 | });
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | # @Description: Golang implementation of pi-dashboard
3 | # @Author: github.com/plutobell
4 | # @Creation: 2020-08-10
5 | # @Last modification: 2023-04-05
6 | # @Version: 1.7.0
7 |
8 | PROGRAM = pi-dashboard-go
9 | OUTPUT = build
10 | GOOS = linux
11 | OS_NAME = $(shell uname -o)
12 |
13 | build: clean vet main.go server device config go.mod go.sum
14 | @echo "-> Building"
15 |
16 | @echo "-> 1 Building the "${PROGRAM}_${GOOS}_armv5_32
17 | @GOOS=${GOOS} GOARCH=arm GOARM=5 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv5_32
18 |
19 | @echo "-> 2 Building the "${PROGRAM}_${GOOS}_armv6_32
20 | @GOOS=${GOOS} GOARCH=arm GOARM=6 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv6_32
21 |
22 | @echo "-> 3 Building the "${PROGRAM}_${GOOS}_armv7_32
23 | @GOOS=${GOOS} GOARCH=arm GOARM=7 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv7_32
24 |
25 | @echo "-> 4 Building the "${PROGRAM}_${GOOS}_armv8_64
26 | @GOOS=${GOOS} GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv8_64
27 |
28 | @echo "-> 5 Building the "${PROGRAM}_${GOOS}_386
29 | @GOOS=${GOOS} GOARCH=386 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_386
30 |
31 | @echo "-> 6 Building the "${PROGRAM}_${GOOS}_amd64
32 | @GOOS=${GOOS} GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_amd64
33 |
34 | @echo "-> Complete"
35 |
36 | run: clean vet
37 | @echo "-> Running"
38 | @go run ./
39 | @echo "-> Complete"
40 |
41 | vet:
42 | @echo "-> Checking"
43 | @go vet
44 | @echo "-> Complete"
45 |
46 | test:
47 | @echo "-> Testing"
48 | @go test -v
49 | @go test -test.bench=".*"
50 | @echo "-> Complete"
51 |
52 | clean:
53 | @echo "-> Cleaning"
54 | @rm -rf ./build
55 | @echo "-> Complete"
56 |
57 | help:
58 | @echo "-> Commands: build | run | vet | test | clean | help"
--------------------------------------------------------------------------------
/server/assets/css/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
3 | @Description: Golang implementation of pi-dashboard
4 | @Author: github.com/plutobell
5 | @Creation: 2020-08-01
6 | @Last modification: 2021-09-02
7 | @Version: 1.6.0
8 | */
9 |
10 | #loading{
11 | background: var(--backdrop-color);
12 | position: fixed;
13 | left: 0px;
14 | top: 0px;
15 | width: 100%;
16 | height: 100%;
17 | display: block;
18 | z-index: 2000;
19 | filter: alpha(opacity=70);
20 | opacity: 0.7 !important;
21 | }
22 |
23 | .spinner {
24 | width: 50px;
25 | height: 60px;
26 | text-align: center;
27 | font-size: 10px;
28 | position: absolute;
29 | left: 50%;
30 | top: 50%;
31 | transform: translate(-50%,-50%);
32 | }
33 |
34 | .spinner > div {
35 | background-color: #fefefd;
36 | height: 100%;
37 | width: 6px;
38 | border-radius: var(--box-radius);
39 | display: inline-block;
40 |
41 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out;
42 | animation: stretchdelay 1.2s infinite ease-in-out;
43 | }
44 |
45 | .spinner .rect2 {
46 | -webkit-animation-delay: -1.1s;
47 | animation-delay: -1.1s;
48 | }
49 |
50 | .spinner .rect3 {
51 | -webkit-animation-delay: -1.0s;
52 | animation-delay: -1.0s;
53 | }
54 |
55 | .spinner .rect4 {
56 | -webkit-animation-delay: -0.9s;
57 | animation-delay: -0.9s;
58 | }
59 |
60 | .spinner .rect5 {
61 | -webkit-animation-delay: -0.8s;
62 | animation-delay: -0.8s;
63 | }
64 |
65 | @-webkit-keyframes stretchdelay {
66 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
67 | 20% { -webkit-transform: scaleY(1.0) }
68 | }
69 |
70 | @keyframes stretchdelay {
71 | 0%, 40%, 100% {
72 | transform: scaleY(0.4);
73 | -webkit-transform: scaleY(0.4);
74 | } 20% {
75 | transform: scaleY(1.0);
76 | -webkit-transform: scaleY(1.0);
77 | }
78 | }
79 |
80 | #command-btns{
81 | list-style-type:none;
82 | display: block;
83 | margin-top: 0 auto;
84 | margin-top: 10px;
85 | padding: 0;
86 | }
87 | #command-btns li{
88 | display: inline;
89 | white-space:nowrap;
90 | margin: auto 7px;
91 | cursor: pointer;
92 | }
93 | #command-btns li img:hover{
94 | border: 1px solid #e5e6e4;
95 | border-radius: 90px;
96 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pi Dashboard Go
2 | **Pi Dashboard Go** is a Golang implementation of pi-dashboard
3 |
4 | * **[中文文档](https://ojoll.com/archives/86/)**
5 |
6 |
7 |
8 | 
9 |
10 | 
11 |
12 | 
13 |
14 | 
15 |
16 |
17 |
18 |
19 |
20 | ## Install
21 |
22 | Thanks to the characteristics of the **[Golang](https://golang.org/)** language, the deployment of **Pi Dashboard Go** is very simple: **single binary executable file**.
23 |
24 | #### Download
25 |
26 | Just download the executable file from the project **[Releases](https://github.com/plutobell/pi-dashboard-go/releases)** page, **no other dependencies**.
27 |
28 | #### Authority
29 |
30 | Grant executable permissions
31 |
32 | ```
33 | chmod +x pi-dashboard-go
34 | ```
35 |
36 | **Note:Pi Dashboard Go requires root privileges.**
37 |
38 |
39 |
40 | ## Use
41 |
42 | #### Usage
43 |
44 | **Pi Dashboard Go** can be configured via command line parameters:
45 |
46 | ```bash
47 | Pi Dashboard Go version: v1.7.0
48 | Project address: https://github.com/plutobell/pi-dashboard-go
49 |
50 | Usage: Pi Dashboard Go [-auth USR:PSW] [-disk Paths] [-help]
51 | [-interval Seconds] [-log] [-net NIC] [-port Port]
52 | [-session Days] [-theme Theme] [-title Title] [-version]
53 |
54 | Options:
55 | -auth string
56 | specify username and password (default "pi:123")
57 | -disk string
58 | specify the filesystem path (default "/")
59 | -help
60 | this help
61 | -interval string
62 | specify the update interval in seconds (default "1")
63 | -log
64 | enable log display
65 | -net string
66 | specify the network device (default "lo")
67 | -port string
68 | specify the running port (default "8080")
69 | -session string
70 | specify the login status validity in days (default "7")
71 | -theme string
72 | specify the theme between 'light' and 'dark' (default "light")
73 | -title string
74 | specify the website title (default "Pi Dashboard Go")
75 | -version
76 | show version and exit
77 | ```
78 |
79 |
80 |
81 | ## Thanks
82 |
83 | * **[Pi Dashboard](https://github.com/spoonysonny/pi-dashboard)**
84 | * **[echo](https://github.com/labstack/echo)**
85 | * **[gopsutil](https://github.com/shirou/gopsutil)**
86 |
87 | * **[bootstrap](https://github.com/twbs/bootstrap)**
88 | * **[jquery](https://github.com/jquery/jquery)**
89 | * **[highcharts](https://github.com/highcharts/highcharts)**
90 |
91 | ## Changelog
92 |
93 | * **[Changelog](./CHANGELOG.md)**
--------------------------------------------------------------------------------
/server/assets/js/solid-gauge.js:
--------------------------------------------------------------------------------
1 | /*
2 | Highcharts JS v9.1.2 (2021-06-16)
3 |
4 | Solid angular gauge module
5 |
6 | (c) 2010-2021 Torstein Honsi
7 |
8 | License: www.highcharts.com/license
9 | */
10 | 'use strict';(function(a){"object"===typeof module&&module.exports?(a["default"]=a,module.exports=a):"function"===typeof define&&define.amd?define("highcharts/modules/solid-gauge",["highcharts","highcharts/highcharts-more"],function(f){a(f);a.Highcharts=f;return a}):a("undefined"!==typeof Highcharts?Highcharts:void 0)})(function(a){function f(a,k,l,c){a.hasOwnProperty(k)||(a[k]=c.apply(null,l))}a=a?a._modules:{};f(a,"Core/Axis/SolidGaugeAxis.js",[a["Core/Color/Color.js"],a["Core/Utilities.js"]],function(a,
11 | k){var l=a.parse,c=k.extend,e=k.merge,m;(function(a){var b={initDataClasses:function(a){var c=this.chart,n,p=0,g=this.options;this.dataClasses=n=[];a.dataClasses.forEach(function(b,d){b=e(b);n.push(b);b.color||("category"===g.dataClassColor?(d=c.options.colors,b.color=d[p++],p===d.length&&(p=0)):b.color=l(g.minColor).tweenTo(l(g.maxColor),d/(a.dataClasses.length-1)))})},initStops:function(a){this.stops=a.stops||[[0,this.options.minColor],[1,this.options.maxColor]];this.stops.forEach(function(a){a.color=
12 | l(a[1])})},toColor:function(a,c){var b=this.stops,l=this.dataClasses,g;if(l)for(g=l.length;g--;){var e=l[g];var d=e.from;b=e.to;if(("undefined"===typeof d||a>=d)&&("undefined"===typeof b||a<=b)){var k=e.color;c&&(c.dataClass=g);break}}else{this.logarithmic&&(a=this.val2lin(a));a=1-(this.max-a)/(this.max-this.min);for(g=b.length;g--&&!(a>b[g][0]););d=b[g]||b[g+1];b=b[g+1]||d;a=1-(b[0]-a)/(b[0]-d[0]||1);k=d.color.tweenTo(b.color,a)}return k}};a.init=function(a){c(a,b)}})(m||(m={}));return m});f(a,"Series/SolidGauge/SolidGaugeComposition.js",
13 | [a["Core/Renderer/SVG/SVGRenderer.js"]],function(a){a=a.prototype;var k=a.symbols.arc;a.symbols.arc=function(a,c,e,m,b){a=k(a,c,e,m,b);b&&b.rounded&&(e=((b.r||e)-(b.innerR||0))/2,c=a[0],b=a[2],"M"===c[0]&&"L"===b[0]&&(c=["A",e,e,0,1,1,c[1],c[2]],a[2]=["A",e,e,0,1,1,b[1],b[2]],a[4]=c));return a}});f(a,"Series/SolidGauge/SolidGaugeSeries.js",[a["Mixins/LegendSymbol.js"],a["Core/Series/SeriesRegistry.js"],a["Core/Axis/SolidGaugeAxis.js"],a["Core/Utilities.js"]],function(a,k,l,c){var e=this&&this.__extends||
14 | function(){var a=function(b,h){a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(a,b){a.__proto__=b}||function(a,b){for(var h in b)b.hasOwnProperty(h)&&(a[h]=b[h])};return a(b,h)};return function(b,h){function c(){this.constructor=b}a(b,h);b.prototype=null===h?Object.create(h):(c.prototype=h.prototype,new c)}}(),m=k.seriesTypes,b=m.gauge,f=m.pie.prototype,p=c.clamp,u=c.extend,n=c.isNumber,w=c.merge,g=c.pick,v=c.pInt,d={colorByPoint:!0,dataLabels:{y:0}};c=function(a){function c(){var b=
15 | null!==a&&a.apply(this,arguments)||this;b.data=void 0;b.points=void 0;b.options=void 0;b.axis=void 0;b.yAxis=void 0;b.startAngleRad=void 0;b.thresholdAngleRad=void 0;return b}e(c,a);c.prototype.translate=function(){var a=this.yAxis;l.init(a);!a.dataClasses&&a.options.dataClasses&&a.initDataClasses(a.options);a.initStops(a.options);b.prototype.translate.call(this)};c.prototype.drawPoints=function(){var a=this,b=a.yAxis,c=b.center,e=a.options,k=a.chart.renderer,d=e.overshoot,l=n(d)?d/180*Math.PI:0,
16 | f;n(e.threshold)&&(f=b.startAngleRad+b.translate(e.threshold,null,null,null,!0));this.thresholdAngleRad=g(f,b.startAngleRad);a.points.forEach(function(d){if(!d.isNull){var h=d.graphic,f=b.startAngleRad+b.translate(d.y,null,null,null,!0),m=v(g(d.options.radius,e.radius,100))*c[2]/200,q=v(g(d.options.innerRadius,e.innerRadius,60))*c[2]/200,r=b.toColor(d.y,d),t=Math.min(b.startAngleRad,b.endAngleRad),n=Math.max(b.startAngleRad,b.endAngleRad);"none"===r&&(r=d.color||a.color||"none");"none"!==r&&(d.color=
17 | r);f=p(f,t-l,n+l);!1===e.wrap&&(f=p(f,t,n));t=Math.min(f,a.thresholdAngleRad);f=Math.max(f,a.thresholdAngleRad);f-t>2*Math.PI&&(f=t+2*Math.PI);d.shapeArgs=q={x:c[0],y:c[1],r:m,innerR:q,start:t,end:f,rounded:e.rounded};d.startR=m;h?(m=q.d,h.animate(u({fill:r},q)),m&&(q.d=m)):d.graphic=h=k.arc(q).attr({fill:r,"sweep-flag":0}).add(a.group);a.chart.styledMode||("square"!==e.linecap&&h.attr({"stroke-linecap":"round","stroke-linejoin":"round"}),h.attr({stroke:e.borderColor||"none","stroke-width":e.borderWidth||
18 | 0}));h&&h.addClass(d.getClassName(),!0)}})};c.prototype.animate=function(a){a||(this.startAngleRad=this.thresholdAngleRad,f.animate.call(this,a))};c.defaultOptions=w(b.defaultOptions,d);return c}(b);u(c.prototype,{drawLegendSymbol:a.drawRectangle});k.registerSeriesType("solidgauge",c);"";return c});f(a,"masters/modules/solid-gauge.src.js",[],function(){})});
19 | //# sourceMappingURL=solid-gauge.js.map
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | // @Description: Golang implementation of pi-dashboard
3 | // @Author: github.com/plutobell
4 | // @Creation: 2020-08-01
5 | // @Last modification: 2023-04-05
6 | // @Version: 1.7.0
7 |
8 | package main
9 |
10 | import (
11 | "flag"
12 | "fmt"
13 | "log"
14 | "os"
15 | "os/user"
16 | "path/filepath"
17 | "strconv"
18 | "strings"
19 | "unicode"
20 |
21 | "github.com/plutobell/pi-dashboard-go/config"
22 | "github.com/plutobell/pi-dashboard-go/device"
23 | "github.com/plutobell/pi-dashboard-go/server"
24 | )
25 |
26 | func init() {
27 | flag.BoolVar(&config.Help, "help", false, "this help")
28 | flag.BoolVar(&config.Version, "version", false, "show version and exit")
29 | flag.StringVar(&config.Port, "port", "8080", "specify the running port")
30 | flag.StringVar(&config.Title, "title", "Pi Dashboard Go", "specify the website title")
31 | flag.StringVar(&config.Net, "net", "lo", "specify the network device")
32 | flag.StringVar(&config.Disk, "disk", "/", "specify the filesystem path")
33 | flag.StringVar(&config.Auth, "auth", config.USERNAME+":"+config.PASSWORD, "specify username and password")
34 | flag.StringVar(&config.Interval, "interval", "1", "specify the update interval in seconds")
35 | flag.StringVar(&config.SessionMaxAge, "session", "7", "specify the login status validity in days")
36 | flag.StringVar(&config.Theme, "theme", "light", "specify the theme between 'light' and 'dark'")
37 | flag.BoolVar(&config.EnableLogger, "log", false, "enable log display")
38 |
39 | config.SessionName = "logged_in"
40 | config.FileName = filepath.Base(os.Args[0])
41 | config.LinuxUserInfo, _ = user.Current()
42 |
43 | flag.Usage = usage
44 | }
45 |
46 | func main() {
47 | flag.Parse()
48 |
49 | if config.Help {
50 | flag.Usage()
51 | return
52 | }
53 | if config.Version {
54 | fmt.Println("Pi Dashboard Go v" + config.VERSION)
55 | fmt.Println("Project address: " + config.PROJECT)
56 | return
57 | }
58 | netDevs, err := device.Popen("cat /proc/net/dev")
59 | if err != nil {
60 | log.Fatal(err)
61 | return
62 | }
63 | if !strings.Contains(netDevs, config.Net+":") {
64 | fmt.Println("Network card does not exist")
65 | return
66 | }
67 | pathExists := false
68 | _, err = os.Stat(config.Disk)
69 | if err == nil {
70 | pathExists = true
71 | }
72 | if os.IsNotExist(err) {
73 | pathExists = false
74 | }
75 |
76 | if config.Disk != "/" {
77 | if !pathExists {
78 | fmt.Println("Disk does not exist")
79 | return
80 | }
81 | }
82 | authSlice := strings.Split(config.Auth, ":")
83 | if len(authSlice) != 2 {
84 | fmt.Println("Auth format error")
85 | return
86 | }
87 | if len([]rune(authSlice[0])) > 15 || len([]rune(authSlice[0])) == 0 {
88 | fmt.Println("Username is too long")
89 | return
90 | }
91 | if len([]rune(authSlice[1])) > 15 || len([]rune(authSlice[1])) == 0 {
92 | fmt.Println("Password is too long")
93 | return
94 | }
95 | if len([]rune(config.Title)) > 25 {
96 | fmt.Println("Title is too long")
97 | return
98 | }
99 |
100 | isDigit := true
101 | for _, r := range config.Interval {
102 | if !unicode.IsDigit(rune(r)) {
103 | isDigit = false
104 | break
105 | }
106 | }
107 | if !isDigit {
108 | fmt.Println("Interval parameter value is invalid")
109 | return
110 | }
111 |
112 | IntervalInt, err := strconv.Atoi(config.Interval)
113 | if err != nil {
114 | log.Fatal(err)
115 | return
116 | }
117 | if IntervalInt > 900 {
118 | fmt.Println("Interval is too long")
119 | return
120 | } else if IntervalInt < 0 {
121 | fmt.Println("Interval should be no less than 0")
122 | return
123 | }
124 |
125 | SessionMaxAgeInt, err := strconv.Atoi(config.SessionMaxAge)
126 | if err != nil {
127 | log.Fatal(err)
128 | return
129 | }
130 | if SessionMaxAgeInt > 365 {
131 | fmt.Println("Session days is too long")
132 | return
133 | } else if SessionMaxAgeInt < 0 {
134 | fmt.Println("Session days should be no less than 0")
135 | return
136 | }
137 |
138 | if config.Theme != "light" && config.Theme != "dark" {
139 | fmt.Println("Theme name not supported")
140 | return
141 | }
142 |
143 | server.Run()
144 | }
145 |
146 | func usage() {
147 | fmt.Fprintf(os.Stderr, `Pi Dashboard Go version: v%s
148 | Project address: %s
149 |
150 | Usage: %s [-auth USR:PSW] [-disk Paths] [-help]
151 | [-interval Seconds] [-log] [-net NIC] [-port Port]
152 | [-session Days] [-theme Theme] [-title Title] [-version]
153 |
154 | Options:
155 | `, config.VERSION, config.PROJECT, config.FileName)
156 | flag.PrintDefaults()
157 | }
158 |
--------------------------------------------------------------------------------
/server/assets/views/login.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Login
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {{.favicon}}
78 |
79 |
80 |
87 |
88 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog #
2 |
3 | **2023-04-05**
4 |
5 | * v1.7.0 :
6 | * Added a cli parameter called theme #4
7 | * Added proxy for version detection
8 | * Fixed the bug of disk path detection
9 | * Some other optimizations
10 | * Updated dependencies
11 | * Use Go v1.20.3
12 |
13 | **2021-09-02**
14 |
15 | * v1.6.0 :
16 | * Refactored device package to significantly improve performance and speed
17 | * Improved front-end display details
18 | * Updated dependencies
19 |
20 | **2021-08-28**
21 |
22 | * v1.5.1 :
23 | * Fixed the bug of load average display
24 | * Added the navbar to close automatically when clicked on mobile devices
25 |
26 | **2021-08-27**
27 |
28 | * v1.5.0 :
29 | * Added dark mode that follows the device switch
30 | * Added automatic jumping to the login page when not logged in
31 | * Some other optimizations
32 | * Updated dependencies
33 |
34 | **2021-08-24**
35 |
36 | * v1.4.1 :
37 | * Added operation functions permission detection
38 | * Added a prompt to the console for non-root users to run
39 | * v1.4.0 :
40 | * Adjusted the front-end page display style
41 | * Adjusted static file path structure
42 | * Added page loading time statistics
43 | * Added pop-up window for new version release notes display
44 | * Some other optimizations
45 | * Use Go v1.17
46 | * Updated dependencies
47 |
48 | **2021-08-14**
49 |
50 | * v1.3.3 :
51 | * Added automatic recognition and switching of favicon
52 | * Adjusted static file path structure
53 | * Adjusted project structure
54 | * Updated dependencies
55 |
56 | **2021-08-13**
57 |
58 | * v1.3.2 :
59 | * Added automatic version detection and prompting
60 | * v1.3.1 :
61 | * Added csrf protection
62 | * Adjusted some details
63 | * Updated dependencies
64 |
65 | **2021-08-12**
66 |
67 | * v1.3.0 :
68 | * Adjusted routing structure
69 | * Adjusted log formatting
70 | * Added gzip compression
71 | * Enhanced web security
72 | * Login request changed to asynchronous
73 | * Refactored part of the code
74 | * Updated dependencies
75 |
76 | **2021-08-10**
77 |
78 | * v1.2.1 :
79 | * Added login empty field checks
80 | * Added login failure prompt message
81 | * Added new command line parameter: log
82 |
83 | * v1.2.0 :
84 | * Rewrite login authentication
85 | * Added new login page
86 | * Added new command line parameter: session
87 | * Use Go v1.16.7
88 | * Updated dependencies
89 |
90 | **2021-06-17**
91 |
92 | * v1.1.2 :
93 | * Changed the way cpu usage is calculated
94 | * Updated dependencies
95 |
96 | **2021-06-16**
97 |
98 | * v1.1.1 :
99 | * Fix the bug of abnormal display of cpu information
100 | * Use Go v1.16.5
101 | * Updated dependencies
102 |
103 | **2021-04-05**
104 |
105 | * v1.1.0 :
106 | * Replace go.rice with go embed
107 | * Fix the bug of program error when unable to get cpu model
108 | * Added new command line parameter: interval
109 | * Added a different header image for non-Raspberry Pi devices
110 |
111 | **2021-03-31**
112 |
113 | * v1.0.10 :
114 | * Fix the bug of panic caused by empty device model information #1
115 | * Fix the bug of not finding dashboard.min.js #2
116 | * Fix the bug of invalid hostname command on arch
117 | * Added golang version display on view
118 |
119 | **2020-8-14**
120 |
121 | * v1.0.9 :
122 | * Fix swap display bug
123 | * Adapt to linux system under 386 and amd64
124 | * Optimize error handling for function Popen
125 |
126 | **2020-8-9**
127 |
128 | * v1.0.8 :
129 | * Optimize swap display details
130 | * Added shortcut buttons such as shutdown and reboot
131 |
132 | **2020-8-7**
133 |
134 | * v1.0.7 :
135 | * Optimize network card flow and curve display
136 | * Interface detail adjustment
137 |
138 | **2020-8-6**
139 |
140 | * v1.0.6 :
141 | * Fix the bug that the network card data display error
142 | * Fixed navigation bar at the top
143 | * Interface detail adjustment
144 | * v1.0.5 :
145 | * Interface color adjustment
146 | * Data update detection and prompt
147 | * Optimize code for server
148 | * Detail adjustment
149 | * v1.0.4 :
150 | * Adjust Cached calculation method
151 | * Added theme-color for mobile browser
152 | * Added display login user statistics
153 | * Bug fixes and details optimization
154 |
155 | **2020-8-5**
156 |
157 | * v1.0.3 :
158 | * Newly added time formatting function resolveTime
159 | * Detail optimization
160 | * v1.0.2 :
161 | * Improve command line parameter verification
162 | * Detail optimization
163 | * Added test case device_test.go
164 | * New page loading animation
165 |
166 | **2020-8-4**
167 |
168 | * v1.0.1 : Bug fixes, detail optimization
169 | * v1.0.0
--------------------------------------------------------------------------------
/server/assets/css/common.css:
--------------------------------------------------------------------------------
1 | /*
2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
3 | @Description: Golang implementation of pi-dashboard
4 | @Author: github.com/plutobell
5 | @Creation: 2020-08-01
6 | @Last modification: 2023-04-05
7 | @Version: 1.7.0
8 | */
9 |
10 | ::-webkit-scrollbar {
11 | width: 6.5px;
12 | height: 6.5px;
13 | }
14 | ::-webkit-scrollbar-track {
15 | border-radius: 3.5px;
16 | background: rgba(0,0,0,0.06);
17 | -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.08);
18 | }
19 | ::-webkit-scrollbar-thumb {
20 | border-radius: 3.5px;
21 | background: rgba(0,0,0,0.12);
22 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
23 | }
24 |
25 | .label {color: var(--label-color); font-size: 75%; font-weight: bolder;}
26 |
27 | body{
28 | -moz-user-select:none;
29 | -webkit-user-select:none;
30 | -ms-user-select:none;
31 | -khtml-user-select:none;
32 | user-select:none;
33 | margin-top: 70px;
34 | }
35 |
36 |
37 | .navbar, .dropdown-menu, .modal-content,
38 | .tooltip-arrow, .tooltip-inner, .tooltip, .arrow::before {
39 | filter: alpha(opacity=90) !important;
40 | opacity: 0.9 !important;
41 | border: 0 !important;
42 | box-shadow: 0 !important;
43 | }
44 | .navbar-toggler {
45 | color: #616161 !important;
46 | border-color: var(--navbar-color) !important;
47 | padding: 0 10px 0 10px !important;
48 | border-radius: var(--box-radius) !important;
49 | }
50 | .dropdown-menu {
51 | border-radius: var(--box-radius) !important;
52 | background-color: var(--navbar-color) !important;
53 | }
54 | .dropdown-item:active {
55 | border-radius: var(--box-radius) !important;
56 | background-color:#616161 !important;
57 | }
58 | .dropdown-item:hover {
59 | border-radius: var(--box-radius) !important;
60 | }
61 |
62 | .tooltip-inner {
63 | border-radius: var(--box-radius);
64 | padding: 5px 10px 5px 10px;
65 | background-color: var(--navbar-color) !important;
66 | }
67 | .tooltip.bs-tooltip-top .tooltip-arrow::before {
68 | border-top-color: var(--navbar-color);
69 | }
70 |
71 | .tooltip.bs-tooltip-bottom .tooltip-arrow::before {
72 | border-bottom-color: var(--navbar-color);
73 | }
74 |
75 | .tooltip.bs-tooltip-start .tooltip-arrow::before {
76 | border-left-color: var(--navbar-color);
77 | }
78 |
79 | .tooltip.bs-tooltip-end .tooltip-arrow::before {
80 | border-right-color: var(--navbar-color);
81 | }
82 | @media only screen
83 | and (max-device-width : 768px) {
84 | .tooltip {
85 | display: none !important;
86 | }
87 | }
88 |
89 | .modal-header, .modal-body, .modal-footer {
90 | border: 0 !important;
91 | }
92 | .modal-backdrop {
93 | background-color: var(--backdrop-color) !important;
94 | }
95 | .modal-open {
96 | overflow-y: hidden !important;
97 | }
98 |
99 | .btn-dark,
100 | .btn-dark:hover,
101 | .btn-dark:active,
102 | .btn-dark:visited,
103 | .btn-dark:focus {
104 | border-radius: var(--box-radius) !important;
105 |
106 | background-color: var(--navbar-color) !important;
107 | border-color: var(--navbar-color) !important;
108 |
109 | outline: none !important;
110 | box-shadow: none !important;
111 | border: 1px solid var(--navbar-color) !important;
112 |
113 | filter: alpha(opacity=90) !important;
114 | opacity: 0.9 !important;
115 | border: 0 !important;
116 | box-shadow: 0 !important;
117 | }
118 | .btn-close:focus {
119 | outline: none !important;
120 | box-shadow: none !important;
121 | }
122 | .inverted {
123 | filter: invert(100%);
124 | }
125 |
126 |
127 | @media (prefers-color-scheme: dark) {
128 | ::-webkit-scrollbar-track {
129 | background: #3f3f3f;
130 | -webkit-box-shadow: inset 0 0 5px #3f3f3f;
131 | }
132 | ::-webkit-scrollbar-thumb {
133 | background: #7c7c7c;
134 | -webkit-box-shadow: inset 0 0 10px #7c7c7c;
135 | }
136 |
137 | body{
138 | background-color: #2d2d2d;
139 | color: #c9d1d9;
140 | }
141 | input {
142 | background-color: #3b3b3b !important;
143 | border-color: #3b3b3b !important;
144 | color: #c9d1d9 !important;
145 | }
146 | input:focus {
147 | border: 1px solid #6b6b6b !important;
148 | }
149 |
150 | :root {
151 | --label-color: #959c9c;
152 | --navbar-color: #474747;
153 | --cache-color: #414141;
154 | --cpu-memory-title-color: #5e5e5e;
155 | --temperature-color: #4b4b4b;
156 | --ip-color: #414141;
157 | --time-color: #5e5e5e;
158 | --uptime-color: #4b4b4b;
159 | --cpu-color: #414141;
160 | --memory-color: #414141;
161 | --real-memory-color: #414141;
162 | --swap-color: #414141;
163 | --disk-color: #5e5e5e;
164 | --box-bg-color: #373938;
165 | --backdrop-color: #363636;
166 |
167 | --guage-font-color: #b9bebe;
168 | --guage-stops-color-1: #626464;
169 | --guage-stops-color-5: #878b8b;
170 | --guage-stops-color-9: #b6b6b6;
171 |
172 | --net-in-color: #414141;
173 | --net-out-color: #5e5e5e;
174 | --net-grid-line-color: #3d3d3d;
175 | --net-line-color: #575757;
176 |
177 | --box-radius: 15px;
178 | }
179 |
180 | .modal-content {
181 | background-color: #252525;
182 | color: #c9d1d9;
183 | }
184 | .dark-bg {
185 | background-color: #505050 !important;
186 | }
187 | .spinner > div {
188 | background-color: #c9d1d9;
189 | }
190 | .navbar-toggler {
191 | color: #505050 !important;
192 | }
193 |
194 | #login-tips {
195 | color: #c9d1d9;
196 | }
197 | #pimodel {
198 | color: #c9d1d9;
199 | }
200 | #command-btns li img:hover{
201 | border: 1px solid #606060 !important;
202 | }
203 | }
--------------------------------------------------------------------------------
/server/assets/js/common.js:
--------------------------------------------------------------------------------
1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | // @Description: Golang implementation of pi-dashboard
3 | // @Author: github.com/plutobell
4 | // @Creation: 2020-08-01
5 | // @Last modification: 2023-04-05
6 | // @Version: 1.7.0
7 |
8 | window.oncontextmenu=function(){return false;}
9 | window.onkeydown = window.onkeyup = window.onkeypress = function (event) {
10 | if (event.keyCode === 123) {
11 | event.preventDefault();
12 | window.event.returnValue = false;
13 | }
14 | }
15 | window.addEventListener('keydown', function (event) {
16 | if (event.ctrlKey) {
17 | event.preventDefault();
18 | }
19 | })
20 |
21 |
22 | const themeVarLight = `
23 | :root {
24 | --label-color: #979d9e;
25 | --navbar-color: #555555;
26 | --cache-color: #D9E4DD;
27 | --cpu-memory-title-color: #CDC9C3;
28 | --temperature-color: #FBF7F0;
29 | --ip-color: #D9E4DD;
30 | --time-color: #CDC9C3;
31 | --uptime-color: #FBF7F0;
32 | --cpu-color: #D9E4DD;
33 | --memory-color: #D9E4DD;
34 | --real-memory-color: #D9E4DD;
35 | --swap-color: #D9E4DD;
36 | --disk-color: #CDC9C3;
37 | --box-bg-color: #eaebe9; /* #E8EAE6 */
38 | --backdrop-color: #363636;
39 |
40 | --guage-font-color: black;
41 | --guage-stops-color-1: #D9E4DD;
42 | --guage-stops-color-5: #CDC9C3;
43 | --guage-stops-color-9: #919191;
44 |
45 | --net-in-color: #D9E4DD;
46 | --net-out-color: #CDC9C3;
47 | --net-grid-line-color: #e6e6e6;
48 | --net-line-color: #ccd6eb;
49 |
50 | --box-radius: 15px;
51 | }
52 | `;
53 | const themeVarDark = `
54 | ::-webkit-scrollbar-track {
55 | background: #3f3f3f;
56 | -webkit-box-shadow: inset 0 0 5px #3f3f3f;
57 | }
58 | ::-webkit-scrollbar-thumb {
59 | background: #7c7c7c;
60 | -webkit-box-shadow: inset 0 0 10px #7c7c7c;
61 | }
62 |
63 | body{
64 | background-color: #2d2d2d;
65 | color: #c9d1d9;
66 | }
67 | input {
68 | background-color: #3b3b3b !important;
69 | border-color: #3b3b3b !important;
70 | color: #c9d1d9 !important;
71 | }
72 | input:focus {
73 | border: 1px solid #6b6b6b !important;
74 | }
75 |
76 | :root {
77 | --label-color: #959c9c;
78 | --navbar-color: #474747;
79 | --cache-color: #414141;
80 | --cpu-memory-title-color: #5e5e5e;
81 | --temperature-color: #4b4b4b;
82 | --ip-color: #414141;
83 | --time-color: #5e5e5e;
84 | --uptime-color: #4b4b4b;
85 | --cpu-color: #414141;
86 | --memory-color: #414141;
87 | --real-memory-color: #414141;
88 | --swap-color: #414141;
89 | --disk-color: #5e5e5e;
90 | --box-bg-color: #373938;
91 | --backdrop-color: #363636;
92 |
93 | --guage-font-color: #b9bebe;
94 | --guage-stops-color-1: #626464;
95 | --guage-stops-color-5: #878b8b;
96 | --guage-stops-color-9: #b6b6b6;
97 |
98 | --net-in-color: #414141;
99 | --net-out-color: #5e5e5e;
100 | --net-grid-line-color: #3d3d3d;
101 | --net-line-color: #575757;
102 |
103 | --box-radius: 15px;
104 | }
105 |
106 | .modal-content {
107 | background-color: #252525;
108 | color: #c9d1d9;
109 | }
110 | .dark-bg {
111 | background-color: #505050 !important;
112 | }
113 | .spinner > div {
114 | background-color: #c9d1d9;
115 | }
116 | .navbar-toggler {
117 | color: #505050 !important;
118 | }
119 |
120 | #login-tips {
121 | color: #c9d1d9;
122 | }
123 | #pimodel {
124 | color: #c9d1d9;
125 | }
126 | #command-btns li img:hover{
127 | border: 1px solid #606060 !important;
128 | }
129 | `;
130 | var theme = $("meta[name='theme']").attr('content');
131 | $(document).ready(function() {
132 | if (theme == "dark" || window.matchMedia('(prefers-color-scheme: dark)').matches) {
133 | $("#theme-var").text(themeVarDark);
134 | $("#modal-close-btn").addClass("btn-close-white");
135 | $("footer").eq(0).addClass("border-secondary");
136 | $("meta[name='theme-color']").attr('content', '#474747');
137 | if ($("#favicon").text() == "linux.ico") {
138 | $("#device-photo").addClass("inverted");
139 | $("#icon").attr("href", "favicons/linux_light.ico");
140 | $("#shortcut-icon").attr("href", "favicons/linux_light.ico");
141 | }
142 | $.getScript('js/index.js', function() {});
143 | } else if (theme == "light" || window.matchMedia('(prefers-color-scheme: light)').matches) {
144 | $("#theme-var").text(themeVarLight);
145 | $("#modal-close-btn").removeClass("btn-close-white");
146 | $("footer").eq(0).removeClass("border-secondary");
147 | $("meta[name='theme-color']").attr('content', '#555555');
148 | $("#device-photo").removeClass("inverted");
149 | if ($("#favicon").text() == "linux.ico") {
150 | $("#icon").attr("href", "favicons/linux.ico");
151 | $("#shortcut-icon").attr("href", "favicons/linux.ico");
152 | } else {
153 | $("#icon").attr("href", "favicons/raspberrypi.ico");
154 | $("#shortcut-icon").attr("href", "favicons/raspberrypi.ico");
155 | }
156 | $.getScript('js/index.js', function() {});
157 | }
158 | });
159 |
160 | let media = window.matchMedia('(prefers-color-scheme: dark)');
161 | let callback = (e) => {
162 | let prefersDarkMode = e.matches;
163 | if (prefersDarkMode) {
164 | $("#theme-var").text(themeVarDark);
165 | $("#modal-close-btn").addClass("btn-close-white");
166 | $("footer").eq(0).addClass("border-secondary");
167 | $("meta[name='theme-color']").attr('content', '#474747');
168 | if ($("#favicon").text() == "linux.ico") {
169 | $("#device-photo").addClass("inverted");
170 | $("#icon").attr("href", "favicons/linux_light.ico");
171 | $("#shortcut-icon").attr("href", "favicons/linux_light.ico");
172 | }
173 | $.getScript('js/index.js', function() {});
174 | } else {
175 | $("#theme-var").text(themeVarLight);
176 | $("#modal-close-btn").removeClass("btn-close-white");
177 | $("footer").eq(0).removeClass("border-secondary");
178 | $("meta[name='theme-color']").attr('content', '#555555');
179 | $("#device-photo").removeClass("inverted");
180 | if ($("#favicon").text() == "linux.ico") {
181 | $("#icon").attr("href", "favicons/linux.ico");
182 | $("#shortcut-icon").attr("href", "favicons/linux.ico");
183 | } else {
184 | $("#icon").attr("href", "favicons/raspberrypi.ico");
185 | $("#shortcut-icon").attr("href", "favicons/raspberrypi.ico");
186 | }
187 | $.getScript('js/index.js', function() {});
188 | }
189 | };
190 | if (typeof media.addEventListener === 'function') {
191 | media.addEventListener('change', callback);
192 | } else if (typeof media.addEventListener === 'function') {
193 | media.addEventListener(callback);
194 | }
195 |
196 | $('.dropdown-item').on('click',function() {
197 | $('.navbar-collapse').collapse('hide');
198 | });
199 | $('#logout').on('click',function() {
200 | $('.navbar-collapse').collapse('hide');
201 | });
202 |
203 | function getCookie(name) {
204 | var cookieValue = null;
205 | if (document.cookie && document.cookie !== '') {
206 | var cookies = document.cookie.split(';');
207 | for (var i = 0; i < cookies.length; i++) {
208 | var cookie = jQuery.trim(cookies[i]);
209 | // Does this cookie string begin with the name we want?
210 | if (cookie.substring(0, name.length + 1) === (name + '=')) {
211 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
212 | break;
213 | }
214 | }
215 | }
216 | return cookieValue;
217 | }
218 |
219 | function csrfSafeMethod(method) {
220 | // 这些HTTP方法不要求携带CSRF令牌。test()是js正则表达式方法,若模板匹配成功,则返回true
221 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
222 | }
223 |
224 | function csrfAddToAjaxHeader() {
225 | var csrftoken = getCookie('cf_sid');
226 |
227 | return {
228 | beforeSend: function(xhr, settings) {
229 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
230 | xhr.setRequestHeader("X-XSRF-TOKEN", csrftoken);
231 | }
232 | }
233 | }
234 | }
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go)
2 | // @Description: Golang implementation of pi-dashboard
3 | // @Author: github.com/plutobell
4 | // @Creation: 2020-08-01
5 | // @Last modification: 2023-04-05
6 | // @Version: 1.7.0
7 |
8 | package server
9 |
10 | import (
11 | "embed"
12 | "encoding/json"
13 | "fmt"
14 | "io"
15 | "io/fs"
16 | "io/ioutil"
17 | "math/rand"
18 | "net/http"
19 | "os"
20 | "runtime"
21 | "strconv"
22 | "strings"
23 | "text/template"
24 | "time"
25 |
26 | "github.com/plutobell/pi-dashboard-go/config"
27 | "github.com/plutobell/pi-dashboard-go/device"
28 |
29 | "github.com/gorilla/sessions"
30 | "github.com/labstack/echo-contrib/session"
31 | "github.com/labstack/echo/v4"
32 | "github.com/labstack/echo/v4/middleware"
33 | )
34 |
35 | //go:embed assets
36 | var assets embed.FS
37 |
38 | // Template 模板
39 | type Template struct {
40 | templates *template.Template
41 | }
42 |
43 | // Render 渲染器
44 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
45 | return t.templates.ExecuteTemplate(w, name, data)
46 | }
47 |
48 | // Server 实例
49 | func Run() {
50 | //Echo 实例
51 | e := echo.New()
52 | port := ":" + config.Port
53 |
54 | //注册中间件
55 | e.Use(middleware.Recover())
56 | e.Use(middleware.Secure())
57 | e.Use(middleware.Decompress())
58 | e.Use(middleware.Timeout())
59 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
60 | Level: 9,
61 | }))
62 | e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
63 | TokenLookup: "header:X-XSRF-TOKEN",
64 | CookieName: "cf_sid",
65 | CookieMaxAge: 86400,
66 | }))
67 | // e.Use(session.Middleware(sessions.NewFilesystemStore("./", []byte(getRandomString(16)))))
68 | e.Use(session.Middleware(sessions.NewCookieStore([]byte(getRandomString(32)))))
69 | if config.EnableLogger {
70 | // e.Use(middleware.Logger())
71 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
72 | Format: "${remote_ip} - - [${time_rfc3339}] ${method} ${uri} ${status} ${latency_human} ${bytes_in} ${bytes_out} ${user_agent}\n",
73 | }))
74 | }
75 |
76 | //静态文件
77 | btnsHandler := http.FileServer(getFileSystem(false, "btns"))
78 | cssHandler := http.FileServer(getFileSystem(false, "css"))
79 | devicesHandler := http.FileServer(getFileSystem(false, "devices"))
80 | faviconsHandler := http.FileServer(getFileSystem(false, "favicons"))
81 | jsHandler := http.FileServer(getFileSystem(false, "js"))
82 |
83 | e.GET("/btns/*", echo.WrapHandler(http.StripPrefix("/btns/", btnsHandler)))
84 | e.GET("/css/*", echo.WrapHandler(http.StripPrefix("/css/", cssHandler)))
85 | e.GET("/devices/*", echo.WrapHandler(http.StripPrefix("/devices/", devicesHandler)))
86 | e.GET("/favicons/*", echo.WrapHandler(http.StripPrefix("/favicons/", faviconsHandler)))
87 | e.GET("/js/*", echo.WrapHandler(http.StripPrefix("/js/", jsHandler)))
88 |
89 | //初始化模版引擎
90 | t := &Template{
91 | templates: template.Must(template.New("").ParseFS(assets, "assets/views/*.tmpl")),
92 | }
93 |
94 | //向echo实例注册模版引擎
95 | e.Renderer = t
96 |
97 | // 路由
98 | e.GET("/", Index)
99 | e.GET("/login", Login)
100 | e.POST("/api/*", API)
101 |
102 | // 启动服务
103 | e.HideBanner = true
104 | fmt.Println("⇨ Pi Dashboard Go v" + config.VERSION)
105 | if isRootUser() != true {
106 | fmt.Println("⇨ Some functions are unavailable to non-root users")
107 | }
108 | e.Logger.Fatal(e.Start(port))
109 | }
110 |
111 | func Index(c echo.Context) error {
112 | username, _ := getNowUsernameAndPassword()
113 |
114 | sess, _ := session.Get(config.SessionName, c)
115 | //通过sess.Values读取会话数据
116 | userName, _ := sess.Values["id"]
117 | isLogin, _ := sess.Values["isLogin"]
118 |
119 | if userName != username || isLogin != true {
120 | return c.Redirect(http.StatusTemporaryRedirect, "/login")
121 | }
122 |
123 | device := device.Info()
124 | device["version"] = config.VERSION
125 | device["site_title"] = config.Title
126 | device["interval"] = config.Interval
127 | device["theme"] = config.Theme
128 | device["go_version"] = runtime.Version()
129 |
130 | return c.Render(http.StatusOK, "index.tmpl", device)
131 | }
132 |
133 | func Login(c echo.Context) error {
134 | username, _ := getNowUsernameAndPassword()
135 |
136 | sess, _ := session.Get(config.SessionName, c)
137 | //通过sess.Values读取会话数据
138 | userName := sess.Values["id"]
139 | isLogin := sess.Values["isLogin"]
140 |
141 | if userName == username && isLogin == true {
142 | return c.Redirect(http.StatusTemporaryRedirect, "/")
143 | }
144 |
145 | tempDevice := device.Info()
146 | device := make(map[string]string)
147 | device["version"] = config.VERSION
148 | device["site_title"] = config.Title
149 | device["theme"] = config.Theme
150 | device["go_version"] = runtime.Version()
151 | device["device_photo"] = tempDevice["device_photo"].(string)
152 | device["favicon"] = tempDevice["favicon"].(string)
153 |
154 | return c.Render(http.StatusOK, "login.tmpl", device)
155 | }
156 |
157 | func API(c echo.Context) error {
158 | switch method := strings.Split(c.Request().URL.Path, "api/")[1]; {
159 |
160 | case method == "login":
161 | username, password := getNowUsernameAndPassword()
162 |
163 | sess, _ := session.Get(config.SessionName, c)
164 | //通过sess.Values读取会话数据
165 | userName := sess.Values["id"]
166 | isLogin := sess.Values["isLogin"]
167 |
168 | if userName == username && isLogin == true {
169 | status := map[string]bool{
170 | "status": true,
171 | }
172 | return c.JSON(http.StatusOK, status)
173 | }
174 |
175 | //获取登录信息
176 | json_map := make(map[string]interface{})
177 | err := json.NewDecoder(c.Request().Body).Decode(&json_map)
178 | var loginUsername, loginPassword interface{}
179 | if err != nil {
180 | loginUsername = ""
181 | loginPassword = ""
182 | } else {
183 | loginUsername = json_map["username"]
184 | loginPassword = json_map["password"]
185 | }
186 |
187 | if loginUsername == username && loginPassword == password {
188 | maxAge, _ := strconv.Atoi(config.SessionMaxAge)
189 |
190 | sess, _ := session.Get(config.SessionName, c)
191 | sess.Options = &sessions.Options{
192 | Path: "/", //所有页面都可以访问会话数据
193 | MaxAge: 86400 * maxAge, //会话有效期,单位秒
194 | HttpOnly: true,
195 | }
196 | //记录会话数据, sess.Values 是map类型,可以记录多个会话数据
197 | sess.Values["id"] = loginUsername
198 | sess.Values["isLogin"] = true
199 | //保存用户会话数据
200 | sess.Save(c.Request(), c.Response())
201 |
202 | status := map[string]bool{
203 | "status": true,
204 | }
205 | return c.JSON(http.StatusOK, status)
206 | } else {
207 |
208 | status := map[string]bool{
209 | "status": false,
210 | }
211 | return c.JSON(http.StatusOK, status)
212 | }
213 |
214 | case method == "logout":
215 | sess, _ := session.Get(config.SessionName, c)
216 | sess.Options = &sessions.Options{
217 | Path: "/",
218 | MaxAge: -1,
219 | HttpOnly: false,
220 | }
221 | sess.Values["id"] = ""
222 | sess.Values["isLogin"] = ""
223 |
224 | sess.Save(c.Request(), c.Response())
225 |
226 | status := map[string]bool{
227 | "status": true,
228 | }
229 | return c.JSON(http.StatusOK, status)
230 |
231 | case method == "device":
232 | username, _ := getNowUsernameAndPassword()
233 |
234 | sess, _ := session.Get(config.SessionName, c)
235 | //通过sess.Values读取会话数据
236 | userName := sess.Values["id"]
237 | isLogin := sess.Values["isLogin"]
238 |
239 | if userName != username || isLogin != true {
240 | status := map[string]string{
241 | "status": "Unauthorized",
242 | }
243 | return c.JSON(http.StatusOK, status)
244 | }
245 |
246 | device := device.Info()
247 | device["version"] = config.VERSION
248 | device["site_title"] = config.Title
249 | device["interval"] = config.Interval
250 | device["go_version"] = runtime.Version()
251 |
252 | return c.JSON(http.StatusOK, device)
253 |
254 | case method == "operation":
255 | username, _ := getNowUsernameAndPassword()
256 |
257 | sess, _ := session.Get(config.SessionName, c)
258 | //通过sess.Values读取会话数据
259 | userName := sess.Values["id"]
260 | isLogin := sess.Values["isLogin"]
261 |
262 | if userName != username || isLogin != true {
263 | status := map[string]string{
264 | "status": "Unauthorized",
265 | }
266 | return c.JSON(http.StatusOK, status)
267 | }
268 |
269 | operation := c.QueryParam("action")
270 |
271 | if operation != "checknewversion" && !isRootUser() {
272 | status := map[string]string{
273 | "status": "NotRootUser",
274 | }
275 |
276 | return c.JSON(http.StatusOK, status)
277 | }
278 |
279 | status := map[string]bool{
280 | "status": true,
281 | }
282 |
283 | switch operation {
284 | case "reboot":
285 | go device.Popen("reboot")
286 | return c.JSON(http.StatusOK, status)
287 | case "shutdown":
288 | go device.Popen("shutdown -h now")
289 | return c.JSON(http.StatusOK, status)
290 | case "dropcaches":
291 | go device.Popen("echo 3 > /proc/sys/vm/drop_caches")
292 | return c.JSON(http.StatusOK, status)
293 | case "checknewversion":
294 | nowVersion, releaseNotes, _ := getLatestVersionFromGitHub()
295 | result := make(map[string]string)
296 | if nowVersion > config.VERSION {
297 | result["new_version"] = nowVersion
298 | result["new_version_notes"] = releaseNotes
299 | result["new_version_url"] = config.PROJECT + "/releases/tag/v" + nowVersion
300 | } else {
301 | result["new_version"] = ""
302 | result["new_version_notes"] = ""
303 | result["new_version_url"] = ""
304 | }
305 |
306 | return c.JSON(http.StatusOK, result)
307 |
308 | }
309 |
310 | }
311 |
312 | status := map[string]string{
313 | "status": "UnknownMethod",
314 | }
315 | return c.JSON(http.StatusOK, status)
316 | }
317 |
318 | func getNowUsernameAndPassword() (username, password string) {
319 | username = config.USERNAME
320 | password = config.PASSWORD
321 | auth := strings.Split(config.Auth, ":")
322 | if len(auth) == 2 {
323 | username = auth[0]
324 | password = auth[1]
325 | }
326 |
327 | return username, password
328 | }
329 |
330 | func getFileSystem(useOS bool, dir string) http.FileSystem {
331 | assetsDir := "assets/"
332 |
333 | if useOS {
334 | // using live mode.
335 | return http.FS(os.DirFS(assetsDir + dir))
336 | }
337 |
338 | // using embed mode.
339 | fsys, err := fs.Sub(assets, assetsDir+dir)
340 | if err != nil {
341 | panic(err)
342 | }
343 |
344 | return http.FS(fsys)
345 | }
346 |
347 | func getRandomString(len int) string {
348 | str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
349 | bytes := []byte(str)
350 | result := []byte{}
351 |
352 | r := rand.New(rand.NewSource(time.Now().UnixNano()))
353 | for i := 0; i < len; i++ {
354 | result = append(result, bytes[r.Intn(62)])
355 | }
356 |
357 | return string(result)
358 | }
359 |
360 | func getLatestVersionFromGitHub() (
361 | nowVersion string, releaseNotes string, downloadURL []string) {
362 |
363 | url := "https://api.github.com/repos/plutobell/pi-dashboard-go/releases/latest"
364 |
365 | client := http.Client{
366 | Transport: &http.Transport{
367 | Proxy: http.ProxyFromEnvironment,
368 | },
369 | }
370 |
371 | resp, err := client.Get(url)
372 | if err != nil {
373 | panic(err)
374 | }
375 | defer resp.Body.Close()
376 |
377 | body, err := ioutil.ReadAll(resp.Body)
378 | if err != nil {
379 | panic(err)
380 | }
381 |
382 | result := make(map[string]interface{})
383 | err = json.Unmarshal(body, &result)
384 | if err != nil {
385 | panic(err)
386 | }
387 |
388 | for key, value := range result {
389 | if key == "tag_name" {
390 | nowVersion = value.(string)[1:]
391 | }
392 | if key == "assets" {
393 | assets := value.([]interface{})
394 | for _, architecture := range assets {
395 | for key, value := range architecture.(map[string]interface{}) {
396 | if key == "browser_download_url" {
397 | downloadURL = append(downloadURL, value.(string))
398 | }
399 | }
400 | }
401 | }
402 | if key == "body" {
403 | releaseNotes = value.(string)
404 | }
405 | }
406 |
407 | return nowVersion, releaseNotes, downloadURL
408 | }
409 |
410 | func isRootUser() bool {
411 | if config.LinuxUserInfo.Gid == "0" &&
412 | config.LinuxUserInfo.Uid == "0" &&
413 | config.LinuxUserInfo.Username == "root" {
414 | return true
415 | }
416 |
417 | return false
418 | }
419 |
--------------------------------------------------------------------------------
/server/assets/js/exporting.js:
--------------------------------------------------------------------------------
1 | /*
2 | Highcharts JS v9.1.2 (2021-06-16)
3 |
4 | Exporting module
5 |
6 | (c) 2010-2021 Torstein Honsi
7 |
8 | License: www.highcharts.com/license
9 | */
10 | 'use strict';(function(c){"object"===typeof module&&module.exports?(c["default"]=c,module.exports=c):"function"===typeof define&&define.amd?define("highcharts/modules/exporting",["highcharts"],function(q){c(q);c.Highcharts=q;return c}):c("undefined"!==typeof Highcharts?Highcharts:void 0)})(function(c){function q(c,m,h,k){c.hasOwnProperty(m)||(c[m]=k.apply(null,h))}c=c?c._modules:{};q(c,"Extensions/FullScreen.js",[c["Core/Chart/Chart.js"],c["Core/Globals.js"],c["Core/Renderer/HTML/AST.js"],c["Core/Utilities.js"]],
11 | function(c,m,h,k){var n=k.addEvent;k=function(){function c(e){this.chart=e;this.isOpen=!1;e=e.renderTo;this.browserProps||("function"===typeof e.requestFullscreen?this.browserProps={fullscreenChange:"fullscreenchange",requestFullscreen:"requestFullscreen",exitFullscreen:"exitFullscreen"}:e.mozRequestFullScreen?this.browserProps={fullscreenChange:"mozfullscreenchange",requestFullscreen:"mozRequestFullScreen",exitFullscreen:"mozCancelFullScreen"}:e.webkitRequestFullScreen?this.browserProps={fullscreenChange:"webkitfullscreenchange",
12 | requestFullscreen:"webkitRequestFullScreen",exitFullscreen:"webkitExitFullscreen"}:e.msRequestFullscreen&&(this.browserProps={fullscreenChange:"MSFullscreenChange",requestFullscreen:"msRequestFullscreen",exitFullscreen:"msExitFullscreen"}))}c.prototype.close=function(){var e=this.chart,c=e.options.chart;if(this.isOpen&&this.browserProps&&e.container.ownerDocument instanceof Document)e.container.ownerDocument[this.browserProps.exitFullscreen]();this.unbindFullscreenEvent&&(this.unbindFullscreenEvent=
13 | this.unbindFullscreenEvent());e.setSize(this.origWidth,this.origHeight,!1);this.origHeight=this.origWidth=void 0;c.width=this.origWidthOption;c.height=this.origHeightOption;this.origHeightOption=this.origWidthOption=void 0;this.isOpen=!1;this.setButtonText()};c.prototype.open=function(){var e=this,c=e.chart,h=c.options.chart;h&&(e.origWidthOption=h.width,e.origHeightOption=h.height);e.origWidth=c.chartWidth;e.origHeight=c.chartHeight;if(e.browserProps){var k=n(c.container.ownerDocument,e.browserProps.fullscreenChange,
14 | function(){e.isOpen?(e.isOpen=!1,e.close()):(c.setSize(null,null,!1),e.isOpen=!0,e.setButtonText())}),m=n(c,"destroy",k);e.unbindFullscreenEvent=function(){k();m()};if(h=c.renderTo[e.browserProps.requestFullscreen]())h["catch"](function(){alert("Full screen is not supported inside a frame.")})}};c.prototype.setButtonText=function(){var e=this.chart,c=e.exportDivElements,k=e.options.exporting,m=k&&k.buttons&&k.buttons.contextButton.menuItems;e=e.options.lang;k&&k.menuItemDefinitions&&e&&e.exitFullscreen&&
15 | e.viewFullscreen&&m&&c&&c.length&&h.setElementHTML(c[m.indexOf("viewFullscreen")],this.isOpen?e.exitFullscreen:k.menuItemDefinitions.viewFullscreen.text||e.viewFullscreen)};c.prototype.toggle=function(){this.isOpen?this.close():this.open()};return c}();m.Fullscreen=k;n(c,"beforeRender",function(){this.fullscreen=new m.Fullscreen(this)});return m.Fullscreen});q(c,"Mixins/Navigation.js",[],function(){return{initUpdate:function(c){c.navigation||(c.navigation={updates:[],update:function(c,h){this.updates.forEach(function(k){k.update.call(k.context,
16 | c,h)})}})},addUpdate:function(c,m){m.navigation||this.initUpdate(m);m.navigation.updates.push({update:c,context:m})}}});q(c,"Extensions/Exporting.js",[c["Core/Chart/Chart.js"],c["Mixins/Navigation.js"],c["Core/Globals.js"],c["Core/DefaultOptions.js"],c["Core/Color/Palette.js"],c["Core/Renderer/SVG/SVGRenderer.js"],c["Core/Utilities.js"]],function(c,m,h,k,n,q,e){var z=h.doc,H=h.isTouchDevice,B=h.win;k=k.defaultOptions;var D=q.prototype.symbols,x=e.addEvent,r=e.css,y=e.createElement,F=e.discardElement,
17 | A=e.extend,I=e.find,E=e.fireEvent,J=e.isObject,p=e.merge,G=e.objectEach,t=e.pick,K=e.removeEvent,L=e.uniqueKey;A(k.lang,{viewFullscreen:"View in full screen",exitFullscreen:"Exit from full screen",printChart:"Print chart",downloadPNG:"Download PNG image",downloadJPEG:"Download JPEG image",downloadPDF:"Download PDF document",downloadSVG:"Download SVG vector image",contextButtonTitle:"Chart context menu"});k.navigation||(k.navigation={});p(!0,k.navigation,{buttonOptions:{theme:{},symbolSize:14,symbolX:12.5,
18 | symbolY:10.5,align:"right",buttonSpacing:3,height:22,verticalAlign:"top",width:24}});p(!0,k.navigation,{menuStyle:{border:"1px solid "+n.neutralColor40,background:n.backgroundColor,padding:"5px 0"},menuItemStyle:{padding:"0.5em 1em",color:n.neutralColor80,background:"none",fontSize:H?"14px":"11px",transition:"background 250ms, color 250ms"},menuItemHoverStyle:{background:n.highlightColor80,color:n.backgroundColor},buttonOptions:{symbolFill:n.neutralColor60,symbolStroke:n.neutralColor60,symbolStrokeWidth:3,
19 | theme:{padding:5}}});k.exporting={type:"image/png",url:"https://export.highcharts.com/",printMaxWidth:780,scale:2,buttons:{contextButton:{className:"highcharts-contextbutton",menuClassName:"highcharts-contextmenu",symbol:"menu",titleKey:"contextButtonTitle",menuItems:"viewFullscreen printChart separator downloadPNG downloadJPEG downloadPDF downloadSVG".split(" ")}},menuItemDefinitions:{viewFullscreen:{textKey:"viewFullscreen",onclick:function(){this.fullscreen.toggle()}},printChart:{textKey:"printChart",
20 | onclick:function(){this.print()}},separator:{separator:!0},downloadPNG:{textKey:"downloadPNG",onclick:function(){this.exportChart()}},downloadJPEG:{textKey:"downloadJPEG",onclick:function(){this.exportChart({type:"image/jpeg"})}},downloadPDF:{textKey:"downloadPDF",onclick:function(){this.exportChart({type:"application/pdf"})}},downloadSVG:{textKey:"downloadSVG",onclick:function(){this.exportChart({type:"image/svg+xml"})}}}};h.post=function(a,b,f){var d=y("form",p({method:"post",action:a,enctype:"multipart/form-data"},
21 | f),{display:"none"},z.body);G(b,function(a,b){y("input",{type:"hidden",name:b,value:a},null,d)});d.submit();F(d)};h.isSafari&&h.win.matchMedia("print").addListener(function(a){h.printingChart&&(a.matches?h.printingChart.beforePrint():h.printingChart.afterPrint())});A(c.prototype,{sanitizeSVG:function(a,b){var f=a.indexOf("")+6,d=a.substr(f);a=a.substr(0,f);b&&b.exporting&&b.exporting.allowHTML&&d&&(d=''+
22 | d.replace(/(<(?:img|br).*?(?=>))>/g,"$1 />")+"",a=a.replace("",d+""));a=a.replace(/zIndex="[^"]+"/g,"").replace(/symbolName="[^"]+"/g,"").replace(/jQuery[0-9]+="[^"]+"/g,"").replace(/url\(("|")(.*?)("|");?\)/g,"url($2)").replace(/url\([^#]+#/g,"url(#").replace(/