├── .github └── workflows │ └── build.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── backend ├── .gitignore ├── .goreleaser.yaml ├── config │ ├── config.go │ └── models.go ├── go.mod ├── go.sum ├── main.go ├── sample-config.daemon.yaml ├── sample-config.server.yaml ├── sysinfo │ ├── models.go │ └── sysinfo.go ├── utils │ └── http.go └── weather │ ├── models.go │ └── weather.go ├── docs ├── dark_mode.jpeg ├── index.md ├── installation.md └── light_mode.jpeg ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── cloud-network.svg │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── AppBar.jsx │ │ ├── Configuration.jsx │ │ ├── Home.jsx │ │ ├── Server.jsx │ │ ├── ThemeSwitcher.jsx │ │ └── Weather.jsx │ ├── index.css │ ├── main.jsx │ ├── theme.js │ └── utils │ │ ├── apis.js │ │ └── strings.js └── vite.config.js ├── mkdocs.yml └── requires.docs.txt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Release 5 | 6 | permissions: 7 | contents: write 8 | id-token: write 9 | 10 | on: 11 | workflow_dispatch: 12 | push: 13 | tags: ['v*'] 14 | 15 | jobs: 16 | backend: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: "1.22" 26 | cache-dependency-path: backend/go.sum 27 | 28 | - name: Set up Node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: "20.x" 32 | cache-dependency-path: frontend/package-lock.json 33 | 34 | - name: Install Frontend Dependencies 35 | working-directory: frontend 36 | run: npm ci 37 | 38 | - name: Create Frontend Bundle 39 | working-directory: frontend 40 | run: npm run build 41 | 42 | - name: Run GoReleaser 43 | uses: goreleaser/goreleaser-action@v6 44 | with: 45 | workdir: backend 46 | distribution: goreleaser 47 | version: "~> v2" 48 | args: release --clean 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | venv/ 4 | .cache/ 5 | backend/static/ -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | golang: "1.20" 13 | 14 | # Build documentation in the "docs/" directory with MkDocs 15 | mkdocs: 16 | configuration: mkdocs.yml 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | # formats: all 20 | 21 | # Optional but recommended, declare the Python requirements required 22 | # to build your documentation 23 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 24 | python: 25 | install: 26 | - requirements: requires.docs.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shrey Dabhi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FrontPorch 2 | 3 | FrontPorch is a configurable dashboard designed for homelabs. It provides a comprehensive view of your server status, Docker-related information, and configurable links and widgets through a YAML file. 4 | 5 | ## Goals 6 | 7 | - Have basic monitoring for all of the servers included in my homelab setup without the need to setup a full blown monitoring stack 8 | - Add links for frequently accessed services 9 | - Add some useful interactive apps 10 | 11 | ## Features 12 | 13 | - **Server Status:** Monitor the status of your servers, including uptime, RAM usage, and disk usage. 14 | - **Docker Integration:** Display Docker-related information if Docker is installed on your system. 15 | - **Configurable Dashboard:** Customize links and widgets through a YAML configuration file. 16 | - **Dark and Light Mode:** Switch between dark and light mode for better readability. 17 | 18 | ## Screenshots 19 | 20 | ### Dark Mode 21 | 22 |  23 | 24 | ### Light Mode 25 | 26 |  27 | 28 | ## Contributing 29 | 30 | Contributions are welcome! Please fork the repository and create a pull request. 31 | 32 | ## License 33 | 34 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 35 | 36 | ## Contact 37 | 38 | For any questions or suggestions, feel free to open an issue! 39 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # Build directory 28 | dist/ -------------------------------------------------------------------------------- /backend/.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Make sure to check the documentation at https://goreleaser.com 2 | 3 | # The lines below are called `modelines`. See `:help modeline` 4 | # Feel free to remove those if you don't want/need to use them. 5 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 6 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 7 | 8 | version: 2 9 | 10 | project_name: frontporch 11 | 12 | before: 13 | hooks: 14 | - go mod tidy 15 | 16 | builds: 17 | - id: frontporch-backend 18 | env: 19 | - CGO_ENABLED=0 20 | goos: 21 | - linux 22 | - windows 23 | - darwin 24 | - freebsd 25 | goarch: 26 | - amd64 27 | - '386' 28 | - arm64 29 | - arm 30 | ignore: 31 | - goos: darwin 32 | goarch: '386' 33 | - goos: darwin 34 | goarch: arm 35 | binary: frontporch 36 | flags: 37 | - -v 38 | 39 | archives: 40 | - format: tar.gz 41 | # this name template makes the OS and Arch compatible with the results of `uname`. 42 | name_template: >- 43 | {{ .ProjectName }}_ 44 | {{- title .Os }}_ 45 | {{- if eq .Arch "amd64" }}x86_64 46 | {{- else if eq .Arch "386" }}i386 47 | {{- else }}{{ .Arch }}{{ end }} 48 | {{- if .Arm }}v{{ .Arm }}{{ end }} 49 | # use zip for windows archives 50 | format_overrides: 51 | - goos: windows 52 | format: zip 53 | files: 54 | - none* 55 | wrap_in_directory: false 56 | 57 | release: 58 | draft: true 59 | replace_existing_draft: true 60 | replace_existing_artifacts: true 61 | mode: replace 62 | 63 | changelog: 64 | sort: asc 65 | filters: 66 | exclude: 67 | - "^docs:" 68 | - "^test:" 69 | -------------------------------------------------------------------------------- /backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func ParseConfigFile(configFile *string) *Config { 11 | appConfig := Config{} 12 | 13 | /// Read the YAML file 14 | data, err := os.ReadFile(*configFile) 15 | if err != nil { 16 | log.Fatalf("error reading YAML file: %v", err) 17 | } 18 | 19 | // Parse the YAML file and store in appConfig 20 | err = yaml.Unmarshal(data, &appConfig) 21 | if err != nil { 22 | log.Fatalf("error parsing YAML file: %v", err) 23 | } 24 | 25 | return &appConfig 26 | } 27 | -------------------------------------------------------------------------------- /backend/config/models.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type TaskType string 4 | 5 | const ( 6 | DaemonTask TaskType = "daemon" 7 | ServerTask TaskType = "server" 8 | ) 9 | 10 | type ServerConfig struct { 11 | Host string `yaml:"host" json:"host"` 12 | Port int `yaml:"port" json:"port"` 13 | } 14 | 15 | type HttpConfig struct { 16 | Host string `yaml:"host" json:"host"` 17 | Port int `yaml:"port" json:"port"` 18 | } 19 | 20 | type WidgetType string 21 | 22 | const ( 23 | OpenWeatherMap WidgetType = "open_weather_map" 24 | LinkChecker WidgetType = "link_checker" 25 | Link WidgetType = "link" 26 | ) 27 | 28 | type WidgetConfig struct { 29 | Name string `yaml:"name" json:"name"` 30 | Type WidgetType `yaml:"type" json:"type"` 31 | Properties map[string]string `yaml:"properties" json:"properties"` 32 | } 33 | 34 | type Config struct { 35 | Task TaskType `yaml:"task" json:"task"` 36 | Servers []ServerConfig `yaml:"servers" json:"servers"` 37 | Widgets []WidgetConfig `yaml:"widgets" json:"widgets"` 38 | HTTP HttpConfig `yaml:"http" json:"http"` 39 | } 40 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module frontporch 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.5.2 7 | github.com/shirou/gopsutil/v4 v4.24.5 8 | gopkg.in/yaml.v3 v3.0.1 9 | ) 10 | 11 | require ( 12 | github.com/felixge/httpsnoop v1.0.3 // indirect 13 | github.com/go-ole/go-ole v1.3.0 // indirect 14 | github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect 15 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 16 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 17 | github.com/tklauser/go-sysconf v0.3.14 // indirect 18 | github.com/tklauser/numcpus v0.8.0 // indirect 19 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 20 | golang.org/x/sys v0.21.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 4 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 5 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 6 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 7 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 11 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 12 | github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= 13 | github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 17 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 18 | github.com/shirou/gopsutil/v4 v4.24.5 h1:gGsArG5K6vmsh5hcFOHaPm87UD003CaDMkAOweSQjhM= 19 | github.com/shirou/gopsutil/v4 v4.24.5/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= 20 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 21 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 22 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 23 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 24 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 25 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= 27 | github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= 28 | github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= 29 | github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= 30 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 31 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 32 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 36 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io/fs" 9 | "log" 10 | "net/http" 11 | "os" 12 | 13 | "github.com/gorilla/handlers" 14 | 15 | "frontporch/config" 16 | "frontporch/sysinfo" 17 | "frontporch/utils" 18 | "frontporch/weather" 19 | ) 20 | 21 | // subFS is a helper function to serve files from a subdirectory of an embed.FS 22 | func subFS(fsys embed.FS, dir string) http.FileSystem { 23 | sub, err := fs.Sub(fsys, dir) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | return http.FS(sub) 28 | } 29 | 30 | //go:embed static/* 31 | var staticFiles embed.FS 32 | 33 | func main() { 34 | var configPath string 35 | flag.StringVar(&configPath, "config-file", "", "YAML file for config management") 36 | flag.Parse() 37 | 38 | appConfig := config.ParseConfigFile(&configPath) 39 | 40 | if appConfig.Task != config.DaemonTask && appConfig.Task != config.ServerTask { 41 | log.Fatalf("Invalid task: %s", appConfig.Task) 42 | os.Exit(1) 43 | } 44 | 45 | if appConfig.Task == config.ServerTask { 46 | fs := http.FileServer(subFS(staticFiles, "static")) 47 | http.Handle("/", fs) 48 | } 49 | 50 | http.HandleFunc("/api/sysinfo", func(w http.ResponseWriter, r *http.Request) { 51 | var response []sysinfo.SysInfo 52 | 53 | switch appConfig.Task { 54 | case config.DaemonTask: 55 | response = sysinfo.GetDaemonSystemInfo() 56 | case config.ServerTask: 57 | response = sysinfo.GetServerSystemInfo(&appConfig.Servers) 58 | default: 59 | response = []sysinfo.SysInfo{} 60 | } 61 | 62 | utils.JSONResponse(w, response) 63 | }) 64 | 65 | http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) { 66 | w.Header().Set("Access-Control-Allow-Origin", "*") 67 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 68 | 69 | w.Header().Set("Content-Type", "application/json") 70 | json.NewEncoder(w).Encode(*appConfig) 71 | 72 | utils.JSONResponse(w, *appConfig) 73 | }) 74 | 75 | http.HandleFunc("/api/widgets", func(w http.ResponseWriter, r *http.Request) { 76 | var response []map[string]string 77 | 78 | // loop over widget configs 79 | for _, widget := range appConfig.Widgets { 80 | switch widget.Type { 81 | case config.OpenWeatherMap: 82 | weather, err := weather.GetWeatherInfo(&widget) 83 | if err != nil { 84 | errMap := map[string]string{ 85 | "message": err.Error(), 86 | } 87 | utils.JSONError(w, errMap, http.StatusInternalServerError) 88 | return 89 | } 90 | response = append(response, *weather) 91 | default: 92 | errMap := map[string]string{ 93 | "message": fmt.Sprintf("widget type handling not implemented: %v", widget.Type), 94 | } 95 | utils.JSONError(w, errMap, http.StatusInternalServerError) 96 | } 97 | } 98 | utils.JSONResponse(w, response) 99 | }) 100 | 101 | serverAddress := fmt.Sprintf("%s:%d", appConfig.HTTP.Host, appConfig.HTTP.Port) 102 | fmt.Printf("Server is running at http://%s\n", serverAddress) 103 | if err := http.ListenAndServe(serverAddress, handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)); err != nil { 104 | log.Fatal(err, "http server startup failed") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /backend/sample-config.daemon.yaml: -------------------------------------------------------------------------------- 1 | task: daemon 2 | http: 3 | port: 8080 4 | host: 0.0.0.0 5 | -------------------------------------------------------------------------------- /backend/sample-config.server.yaml: -------------------------------------------------------------------------------- 1 | task: server 2 | http: 3 | port: 8080 4 | host: 0.0.0.0 5 | servers: 6 | - host: rpi 7 | port: 8080 8 | - host: hella 9 | port: 8080 10 | - host: thor 11 | port: 8080 12 | widgets: 13 | - name: Bangalore 14 | type: open_weather_map 15 | properties: 16 | apikey: <---insert-api-key-here----> 17 | units: metric 18 | lat: 12.9116 19 | lon: 77.6839 -------------------------------------------------------------------------------- /backend/sysinfo/models.go: -------------------------------------------------------------------------------- 1 | package sysinfo 2 | 3 | import "frontporch/config" 4 | 5 | type UsageStat struct { 6 | Total uint64 `json:"total"` 7 | Available uint64 `json:"available"` 8 | Used uint64 `json:"used"` 9 | UsedPercent float64 `json:"used_percent"` 10 | } 11 | 12 | type CpuInfo struct { 13 | PhysicalCores int `json:"physical_cores"` 14 | LogicalCores int `json:"logical_cores"` 15 | Model string `json:"model"` 16 | Vendor string `json:"vendor"` 17 | Family string `json:"family"` 18 | } 19 | 20 | type HostInfo struct { 21 | Uptime uint64 `json:"uptime"` 22 | UptimeHours float64 `json:"uptime_hours"` 23 | Hostname string `json:"hostname"` 24 | OS string `json:"os"` 25 | Platform string `json:"platform"` 26 | PlatformVersion string `json:"platform_version"` 27 | KernelVersion string `json:"kernel_version"` 28 | KernelArch string `json:"kernel_arch"` 29 | } 30 | 31 | type StatusInfo string 32 | 33 | const ( 34 | Online StatusInfo = "online" 35 | Offline StatusInfo = "offline" 36 | ) 37 | 38 | type SysInfo struct { 39 | Host HostInfo `json:"host"` 40 | CPU CpuInfo `json:"cpu"` 41 | RAM UsageStat `json:"ram"` 42 | Disk UsageStat `json:"disk"` 43 | IpAddresses []string `json:"ip_addresses"` 44 | Status StatusInfo `json:"status"` 45 | } 46 | 47 | func NewOfflineServer(server config.ServerConfig) *SysInfo { 48 | return &SysInfo{ 49 | Status: Offline, 50 | Host: HostInfo{ 51 | Hostname: server.Host, 52 | }, 53 | } 54 | } -------------------------------------------------------------------------------- /backend/sysinfo/sysinfo.go: -------------------------------------------------------------------------------- 1 | package sysinfo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "frontporch/config" 7 | "io" 8 | "net/http" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | "github.com/shirou/gopsutil/v4/cpu" 14 | "github.com/shirou/gopsutil/v4/disk" 15 | "github.com/shirou/gopsutil/v4/host" 16 | "github.com/shirou/gopsutil/v4/mem" 17 | "github.com/shirou/gopsutil/v4/net" 18 | ) 19 | 20 | func GetDaemonSystemInfo() []SysInfo { 21 | hostStat, _ := host.Info() 22 | cpuStat, _ := cpu.Info() 23 | vmStat, _ := mem.VirtualMemory() 24 | diskStat, _ := disk.Usage("/") 25 | 26 | info := new(SysInfo) 27 | info.Host = HostInfo{ 28 | Uptime: hostStat.Uptime, 29 | UptimeHours: float64(hostStat.Uptime) / 3600, 30 | Hostname: hostStat.Hostname, 31 | OS: hostStat.OS, 32 | Platform: hostStat.Platform, 33 | PlatformVersion: hostStat.PlatformVersion, 34 | KernelVersion: hostStat.KernelVersion, 35 | KernelArch: hostStat.KernelArch, 36 | } 37 | info.CPU = CpuInfo{ 38 | Model: cpuStat[0].ModelName, 39 | Vendor: cpuStat[0].VendorID, 40 | Family: cpuStat[0].Family, 41 | } 42 | info.CPU.PhysicalCores, _ = cpu.Counts(false) 43 | info.CPU.LogicalCores, _ = cpu.Counts(true) 44 | info.RAM = UsageStat{ 45 | Total: vmStat.Total / 1024 / 1024, 46 | Available: vmStat.Available / 1024 / 1024, 47 | Used: vmStat.Used / 1024 / 1024, 48 | UsedPercent: vmStat.UsedPercent, 49 | } 50 | info.Disk = UsageStat{ 51 | Total: diskStat.Total / 1024 / 1024, 52 | Available: diskStat.Free / 1024 / 1024, 53 | Used: diskStat.Used / 1024 / 1024, 54 | UsedPercent: diskStat.UsedPercent, 55 | } 56 | interfaceList, _ := net.Interfaces() 57 | info.IpAddresses = []string{} 58 | 59 | // regex to match cidr ipv4 address 60 | cidrIP := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}`) 61 | 62 | for _, netInterface := range interfaceList { 63 | for _, addr := range netInterface.Addrs { 64 | match := cidrIP.MatchString(addr.Addr) 65 | if match { 66 | info.IpAddresses = append(info.IpAddresses, strings.Split(addr.Addr, "/")[0]) 67 | } 68 | } 69 | } 70 | 71 | info.Status = Online 72 | 73 | var output []SysInfo 74 | output = append(output, *info) 75 | 76 | return output 77 | } 78 | 79 | func GetServerSystemInfo(servers *[]config.ServerConfig) []SysInfo { 80 | var output []SysInfo 81 | 82 | client := http.Client{ 83 | Timeout: 3 * time.Second, 84 | } 85 | 86 | for _, server := range *servers { 87 | url := fmt.Sprintf("http://%s:%d/api/sysinfo", server.Host, server.Port) 88 | var sysInfo []SysInfo 89 | 90 | req, _ := http.NewRequest("GET", url, nil) 91 | resp, err := client.Do(req) 92 | if err != nil { 93 | fmt.Printf("Error performing GET request: %v\n", err) 94 | output = append(output, *NewOfflineServer(server)) 95 | continue 96 | } 97 | defer resp.Body.Close() 98 | 99 | body, err := io.ReadAll(resp.Body) 100 | if err != nil { 101 | fmt.Printf("Error reading response body: %v\n", err) 102 | output = append(output, *NewOfflineServer(server)) 103 | continue 104 | } 105 | 106 | err = json.Unmarshal(body, &sysInfo) 107 | if err != nil { 108 | fmt.Printf("Error unmarshaling JSON: %v\n", err) 109 | output = append(output, *NewOfflineServer(server)) 110 | continue 111 | } 112 | 113 | output = append(output, sysInfo...) 114 | } 115 | 116 | selfSysInfo := GetDaemonSystemInfo() 117 | output = append(output, selfSysInfo...) 118 | 119 | return output 120 | } 121 | -------------------------------------------------------------------------------- /backend/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func JSONError(w http.ResponseWriter, err interface{}, code int) { 9 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 10 | w.Header().Set("X-Content-Type-Options", "nosniff") 11 | w.WriteHeader(code) 12 | json.NewEncoder(w).Encode(err) 13 | } 14 | 15 | func JSONResponse(w http.ResponseWriter, response interface{}) { 16 | w.Header().Set("Access-Control-Allow-Origin", "*") 17 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 18 | w.Header().Set("Content-Type", "application/json") 19 | json.NewEncoder(w).Encode(response) 20 | } -------------------------------------------------------------------------------- /backend/weather/models.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "frontporch/config" 5 | "strconv" 6 | ) 7 | 8 | type WeatherInfoRequest struct { 9 | CityName string `json:"name"` 10 | Temp struct { 11 | Actual float64 `json:"temp"` 12 | FeelsLike float64 `json:"feels_like"` 13 | Pressure int `json:"pressure"` 14 | Humidity int `json:"humidity"` 15 | } `json:"main"` 16 | Weather []struct { 17 | Id int `json:"id"` 18 | Main string `json:"main"` 19 | Description string `json:"description"` 20 | Icon string `json:"icon"` 21 | } `json:"weather"` 22 | Sys struct { 23 | Country string `json:"country"` 24 | Sunrise int `json:"sunrise"` 25 | Sunset int `json:"sunset"` 26 | } `json:"sys"` 27 | } 28 | 29 | func (w *WeatherInfoRequest) ToMap(c *config.WidgetConfig) map[string]string { 30 | result := make(map[string]string) 31 | 32 | result["type"] = string(config.OpenWeatherMap) 33 | 34 | result["city_name"] = w.CityName 35 | result["temp_actual"] = strconv.FormatFloat(w.Temp.Actual, 'f', 2, 64) 36 | result["temp_feels_like"] = strconv.FormatFloat(w.Temp.FeelsLike, 'f', 2, 64) 37 | result["pressure"] = strconv.Itoa(w.Temp.Pressure) 38 | result["humidity"] = strconv.Itoa(w.Temp.Humidity) 39 | 40 | weather := w.Weather[0] 41 | result["weather_id"] = strconv.Itoa(weather.Id) 42 | result["weather_main"] = weather.Main 43 | result["weather_description"] = weather.Description 44 | result["weather_icon"] = weather.Icon 45 | 46 | result["country"] = w.Sys.Country 47 | result["sunrise"] = strconv.Itoa(w.Sys.Sunrise) 48 | result["sunset"] = strconv.Itoa(w.Sys.Sunset) 49 | 50 | result["units"] = c.Properties["units"] 51 | 52 | return result 53 | } 54 | -------------------------------------------------------------------------------- /backend/weather/weather.go: -------------------------------------------------------------------------------- 1 | package weather 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "frontporch/config" 8 | "io" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | func GetWeatherInfo(widget *config.WidgetConfig) (*map[string]string, error) { 14 | client := http.Client{ 15 | Timeout: 3 * time.Second, 16 | } 17 | 18 | var weatherInfoRequest WeatherInfoRequest 19 | 20 | url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&units=%s&appid=%s", widget.Properties["lat"], widget.Properties["lon"], widget.Properties["units"], widget.Properties["apikey"]) 21 | 22 | req, _ := http.NewRequest("GET", url, nil) 23 | resp, err := client.Do(req) 24 | if err != nil { 25 | fmt.Printf("Error performing GET request: %v\n", err) 26 | return nil, err 27 | } 28 | defer resp.Body.Close() 29 | 30 | if resp.StatusCode != 200 { 31 | fmt.Printf("Received non 200 response code: %v\n", resp.StatusCode) 32 | return nil, errors.New(fmt.Sprintf("Received a non 200 response code from Open Weather Map API: %v", resp.StatusCode)) 33 | } 34 | 35 | body, err := io.ReadAll(resp.Body) 36 | if err != nil { 37 | fmt.Printf("Error reading response body: %v\n", err) 38 | return nil, err 39 | } 40 | 41 | err = json.Unmarshal(body, &weatherInfoRequest) 42 | if err != nil { 43 | fmt.Printf("Error unmarshaling JSON: %v\n", err) 44 | return nil, err 45 | } 46 | 47 | weather := weatherInfoRequest.ToMap(widget) 48 | 49 | return &weather, nil 50 | } 51 | -------------------------------------------------------------------------------- /docs/dark_mode.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdabhi23/frontporch/864b1925edb1904df3cb19f7398a37415fb83593/docs/dark_mode.jpeg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | FrontPorch is a configurable dashboard designed for homelabs. It provides a comprehensive view of your server status, Docker-related information, and configurable links and widgets through a YAML file. 4 | 5 | ## Features 6 | 7 | - **Server Status:** Monitor the status of your servers, including uptime, RAM usage, and disk usage. 8 | - **Docker Integration:** Display Docker-related information if Docker is installed on your system. 9 | - **Configurable Dashboard:** Customize links and widgets through a YAML configuration file. 10 | - **Dark and Light Mode:** Switch between dark and light mode for better readability. 11 | 12 | ## Screenshots 13 | 14 | ### Dark Mode 15 | 16 |  17 | 18 | ### Light Mode 19 | 20 |  21 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Download 4 | 5 | Pre-built binaries for the following platforms are being published for every version on [Github Releases](https://github.com/sdabhi23/frontporch/releases). 6 | 7 | | | Linux | Windows | MacOS (darwin) | FreeBSD | 8 | | ---------------------- | :---: | :-----: | :------------: | :-----: | 9 | | **amd64 (64-bit)** | ✅ | ✅ | ✅ | ✅ | 10 | | **386 (32-bit)** | ✅ | ✅ | ❌ | ✅ | 11 | | **arm64 (64-bit arm)** | ✅ | ✅ | ✅ | ✅ | 12 | | **arm (32-bit arm)** | ✅ | ✅ | ❌ | ✅ | 13 | 14 | Download the correct binary for your platform and create a server / daemon config file. 15 | 16 | ## Configuration 17 | 18 | Configure FrontPorch using the config.yaml file. Here’s an example configuration: 19 | 20 | ```yaml 21 | # config.yaml 22 | task: server # can be server / daemon 23 | http: 24 | port: 8080 25 | host: 0.0.0.0 26 | static_file_dir: ../frontend/dist # only required for server mode 27 | # sections below are only required for server mode 28 | servers: 29 | - host: server-1 # ip or dns name of your other server 30 | port: 8080 31 | - host: server-2 32 | port: 8080 33 | - host: server-3 34 | port: 8000 35 | widgets: 36 | # open weather map widget to show weather on the dashboard 37 | - name: Bangalore 38 | type: open_weather_map 39 | properties: 40 | apikey: <---insert-api-key-here----> # api key generated from OWM 41 | units: metric 42 | lat: 12.9116 43 | lon: 77.6839 44 | ``` 45 | 46 | ## Start FrontPorch 47 | 48 | Run the program using the following command: 49 | 50 | ```bash 51 | ./frontporch --config-file=config.yaml 52 | ``` 53 | 54 | If you are running the server, the dashboard will be available at the port configured in the config file. If you are running the daemon, the APIs for this machine will now be available on the configured port. -------------------------------------------------------------------------------- /docs/light_mode.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdabhi23/frontporch/864b1925edb1904df3cb19f7398a37415fb83593/docs/light_mode.jpeg -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Dependency directories 21 | node_modules/ 22 | jspm_packages/ 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Optional eslint cache 28 | .eslintcache 29 | 30 | # Optional REPL history 31 | .node_repl_history 32 | 33 | # dotenv environment variable files 34 | .env 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | .env.local 39 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
23 | {config}
24 |
25 |