diff --git a/.forgejo/workflows/Dockerfile b/.forgejo/workflows/Dockerfile
deleted file mode 100644
index 82c8879..0000000
--- a/.forgejo/workflows/Dockerfile
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM debian:latest
-
-WORKDIR /app
-COPY ecodash_arm ecodash_arm
-COPY ecodash_x86 ecodash_x86
-COPY templates templates
-
-RUN if [ "$(uname -m)" = "aarch64" ]; then mv ecodash_arm app; rm ecodash_x86; else mv ecodash_x86 app; rm ecodash_arm; fi
-
-CMD ["./app"]
diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml
deleted file mode 100644
index 8f62b00..0000000
--- a/.forgejo/workflows/build.yaml
+++ /dev/null
@@ -1,123 +0,0 @@
-name: CI Pipeline
-
-on:
- push:
- branches:
- - master
- tags:
- - 'v*'
-
-jobs:
-
- build:
- runs-on: ubuntu-22.04
- steps:
-
- - name: Set up Go
- uses: actions/setup-go@v3
- with:
- go-version: '1.22'
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Build
- run: |
- go mod tidy
- echo Building for linux/amd64...
- GOOS=linux GOARCH=amd64 go build -o ecodash_x86 src/main/main.go
- echo Building for linux/arm...
- GOOS=linux GOARCH=arm go build -o ecodash_arm src/main/main.go
-
- - name: Stash artifacts
- uses: actions/upload-artifact@v3
- with:
- path: |
- ecodash_x86
- ecodash_arm
-
- build-and-push-docker:
- runs-on: ubuntu-22.04
- needs: build
- steps:
-
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Docker meta
- id: meta
- uses: https://github.com/docker/metadata-action@v5
- with:
- images: git.massivebox.net/massivebox/ecodash
- tags: |
- type=ref,event=branch
- type=semver,pattern={{version}}
- type=semver,pattern=v{{major}}
- type=semver,pattern={{major}}.{{minor}}
-
- - name: Install QEMU
- run: sudo apt-get update && sudo apt-get install -y qemu-user-static
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- registry: git.massivebox.net
- username: ${{ github.actor }}
- password: ${{ secrets.FORGE_TOKEN }}
-
- - name: Download artifacts
- uses: actions/download-artifact@v3
- with:
- name: artifact
- - name: Move dockerfile
- run: mv .forgejo/workflows/Dockerfile Dockerfile
-
- - name: Build and push
- uses: docker/build-push-action@v5
- with:
- context: .
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
-
- publish-executables:
- runs-on: ubuntu-22.04
- needs: build
- steps:
-
- - name: Checkout code
- uses: actions/checkout@v3
-
- - name: Download artifacts
- uses: actions/download-artifact@v3
- with:
- name: artifact
-
- - name: Prepare build artifacts
- run: |
- mkdir release
- mv ecodash_x86 ecodash
- zip -r release/ecodash-x86.zip templates ecodash
- mv ecodash_arm ecodash
- zip -r release/ecodash-arm.zip templates ecodash
-
- - name: Upload artifacts to CI
- uses: actions/upload-artifact@v3
- with:
- path: |
- ecodash_x86
- ecodash_arm
- templates
- overwrite: true
-
- - name: Create release
- if: ${{ startsWith(github.ref, 'refs/tags/v') }}
- uses: actions/forgejo-release@v1
- with:
- direction: upload
- url: https://git.massivebox.net
- repo: massivebox/ecodash
- release-dir: release
- tag: ${{ github.ref_name }}
- token: ${{ secrets.FORGE_TOKEN }}
diff --git a/.golangci.yml b/.golangci.yml
index edcb24b..c128eff 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -234,8 +234,6 @@ linters:
- wrapcheck
- nonamedreturns
- gomnd
- - depguard
- - gosmopolitan
enable-all: true
fast: false
diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 0000000..d431220
--- /dev/null
+++ b/.woodpecker.yml
@@ -0,0 +1,45 @@
+pipeline:
+
+ docker:
+ image: woodpeckerci/plugin-docker-buildx
+ settings:
+ registry: git.massivebox.net
+ repo: git.massivebox.net/ecodash/ecodash
+ platforms: linux/amd64,linux/arm64
+ auto_tag: true
+ username: massivebox
+ password:
+ from_secret: auth_token
+ when:
+ event: tag
+
+ build:
+ image: golang
+ commands:
+ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
+ - go mod tidy
+ - golangci-lint run
+ - go build -o ecodash-x86 src/main/main.go
+ - env GOOS=linux GOARCH=arm go build -o ecodash-arm src/main/main.go
+
+ prepare-gitea-release:
+ image: alpine
+ commands:
+ - apk update; apk add zip
+ - mv ecodash-x86 ecodash; zip -r ecodash-x86.zip templates ecodash
+ - mv ecodash-arm ecodash; zip -r ecodash-arm.zip templates ecodash
+ when:
+ event: tag
+
+ gitea-publish:
+ image: plugins/gitea-release
+ settings:
+ base_url: https://git.massivebox.net
+ files:
+ - ecodash-x86.zip
+ - ecodash-arm.zip
+ api_key:
+ from_secret: auth_token
+ title: ${CI_COMMIT_TAG}
+ when:
+ event: tag
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 0000000..b9d6e3f
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,30 @@
+# 👷 Building EcoDash
+
+Here's how to build EcoDash in both binaries and as a Docker container. This is not necessary for most cases - we provide both pre-built binaries and containers for Linux ARM and x86_64 - however in devices with unsupported architectures it's necessary.
+
+You're encouraged to first check the installation instructions to see if a pre-built container or binary is already available.
+If you really have to build it yourself, we recommend you Docker over binaries.
+
+## Binaries
+
+### Linux
+
+1. Download the Go Compiler from https://go.dev/dl/ or from your repository's package manager (it's usually called `go` or `golang`)
+2. Download the Git SCM from https://git-scm.com/download/linux or from your package manager (it's always called `git`)
+3. Download `golangci-lint` from https://golangci-lint.run/
+4. Clone the repository by running `git clone https://gitea.massivebox.net/ecodash/ecodash.git ` inside a command prompt
+5. Switch to the project directory with `cd ecodash`
+6. Run `golangci-lint run` to lint all project files
+7. Build with `go build src/main/main.go -o ecodash`. This will generate an executable, `ecodash`, in the same directory.
+
+### Windows
+
+1. Install the latest release of the Go Compiler for Windows from https://go.dev/dl/
+2. Install the Git SCM from https://git-scm.com/download/win. The "Standalone installer" is recommended. All the default settings will work fine.
+3. Download `golangci-lint` from https://golangci-lint.run/
+4. Clone the repository by running `git clone https://gitea.massivebox.net/ecodash/ecodash.git ` inside a command prompt
+5. Switch to the project directory with `cd ecodash`
+6. Run `golangci-lint run` to lint all project files
+7. Build with `go build src/main/main.go -o ecodash`. This will generate an executable, `ecodash.exe`, in the same directory.
+
+## Docker
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index f6f7e04..47f94d0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,9 +9,10 @@ COPY src /app/src
COPY go.mod /app/
COPY .golangci.yml /app/
-RUN go mod tidy; \
- golangci-lint run; \
- go test ./src/...
+RUN go mod tidy
+RUN golangci-lint run
+RUN go test ./src/...
+
RUN CGO_ENABLED=1 go build -o app src/main/main.go
FROM alpine:latest
@@ -20,9 +21,6 @@ WORKDIR /app
COPY --from=1 /app/app .
COPY ./templates /app/templates
+RUN touch config.json database.db
-RUN mkdir data
-ENV DATABASE_PATH=./data/database.db
-ENV CONFIG_PATH=./data/config.json
-
-CMD "./app"
\ No newline at end of file
+CMD ["./app"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 40c1bc7..f3c9f66 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,21 @@
# 🌿 EcoDash
-[](https://massivebox.net/pages/donate.html)
+[](https://woodpecker.massivebox.net/ecodash/ecodash) [](https://ecodash.xyz) [](https://ecodash.xyz/contribute)
EcoDash is a simple way to show your users how much your server consumes.
It's intended as a medium of transparency, that gives your users an idea about the consumption of your machine. It's not meant to be 100% accurate.
-You can see it in action here: https://ecodash.massivebox.net
+You can see it in action here: https://demo.ecodash.xyz
## Get started
-Check out the documentation in our [wiki](https://git.massivebox.net/massivebox/ecodash/wiki) to get started with EcoDash.
+Check out the documentation in our [website](https://ecodash.xyz) to get started with EcoDash.
-- [📖 Introduction](https://git.massivebox.net/massivebox/ecodash/wiki)
-- [⬇️ Installation](https://git.massivebox.net/massivebox/ecodash/wiki/install)
-- [⚙️ Configuration](https://git.massivebox.net/massivebox/ecodash/wiki/config)
+- [📖 Introduction](https://ecodash.xyz/docs)
+- [🛣 Roadmap](https://ecodash.xyz/docs/roadmap)
+- [⬇️ Install](https://ecodash.xyz/docs/install)
+- [⚙️ Setup](https://ecodash.xyz/docs/setup)
+- [🆘 Support](https://ecodash.xyz/docs/support)
## License
diff --git a/src/ecodash/config.go b/src/ecodash/config.go
index b183666..91b7541 100644
--- a/src/ecodash/config.go
+++ b/src/ecodash/config.go
@@ -4,7 +4,6 @@ import (
"database/sql"
"encoding/json"
"errors"
- "html/template"
"os"
"reflect"
"regexp"
@@ -38,17 +37,10 @@ type Administrator struct {
PasswordHash string `json:"password_hash"`
}
type Dashboard struct {
- MOTD *MessageCard `json:"motd"`
- Name string `json:"name"`
- Theme string `json:"theme"`
- FooterLinks []Link `json:"footer_links"`
- HeaderLinks []Link `json:"header_links"`
-}
-
-type MessageCard struct {
- Title string `json:"title"`
- Content template.HTML `json:"content"`
- Style string `json:"style"`
+ Name string `json:"name"`
+ Theme string `json:"theme"`
+ FooterLinks []Link `json:"footer_links"`
+ HeaderLinks []Link `json:"header_links"`
}
var errBadHAFormat = errors.New("HomeAssistant base URL is badly formatted")
@@ -69,14 +61,10 @@ func formatURL(url string) (string, error) {
return url, nil
}
-func LoadConfig() (config *Config, err error) {
- var dbPath string
- if dbPath = os.Getenv("DATABASE_PATH"); dbPath == "" {
- dbPath = "./database.db"
- }
- db, err := sql.Open("sqlite", dbPath)
+func LoadConfig() (config *Config, isFirstRun bool, err error) {
+ db, err := sql.Open("sqlite", "./database.db")
if err != nil {
- return &Config{}, err
+ return &Config{}, false, err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" (
@@ -86,7 +74,7 @@ func LoadConfig() (config *Config, err error) {
PRIMARY KEY("time")
);`)
if err != nil {
- return &Config{}, err
+ return &Config{}, false, err
}
defaultConfig := &Config{}
@@ -103,32 +91,28 @@ func LoadConfig() (config *Config, err error) {
})
defaultConfig.db = db
- var confPath string
- if confPath = os.Getenv("CONFIG_PATH"); confPath == "" {
- confPath = "./config.json"
- }
- data, err := os.ReadFile(confPath)
+ data, err := os.ReadFile("config.json")
if err != nil {
// if the data file doesn't exist, we consider it a first run
if os.IsNotExist(err) {
- return defaultConfig, nil
+ return defaultConfig, true, nil
}
- return &Config{}, err
+ return &Config{}, false, err
}
// if the data file is empty, we consider it as a first run
if len(data) == 0 {
- return defaultConfig, nil
+ return defaultConfig, true, nil
}
conf := &Config{}
err = json.Unmarshal(data, &conf)
if err != nil {
- return &Config{}, err
+ return &Config{}, false, err
}
conf.db = db
- return conf, nil
+ return conf, false, nil
}
func (config *Config) IsAuthorized(c *fiber.Ctx) bool {
diff --git a/src/ecodash/database.go b/src/ecodash/database.go
index f580b15..0c44e35 100644
--- a/src/ecodash/database.go
+++ b/src/ecodash/database.go
@@ -75,7 +75,7 @@ func (config *Config) refreshCacheFromPast(pastTime time.Time) error {
}
defer stmtIgnore.Close()
- for key, day := range historyPolledSmartEnergySummation {
+ for key, day := range greenEnergyPercentage {
var stmt *sql.Stmt
if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 {
stmt = stmtReplace
diff --git a/src/ecodash/http.go b/src/ecodash/http.go
index 02560ae..7c32f2a 100644
--- a/src/ecodash/http.go
+++ b/src/ecodash/http.go
@@ -47,18 +47,16 @@ func (config *Config) AdminEndpoint(c *fiber.Ctx) error {
if config.IsAuthorized(c) { // here the user is submitting the form to change configuration
err := config.saveAdminForm(c)
if err != nil {
- // #nosec the input is admin-defined, and the admin is assumed to be trusted.
- return config.RenderAdminPanel(c, &MessageCard{
- Title: "An error occurred!",
- Content: template.HTML(html.EscapeString(err.Error())),
- Style: "error",
+ return config.RenderAdminPanel(c, Warning{
+ Header: "An error occurred!",
+ Body: html.EscapeString(err.Error()),
})
}
- return config.RenderAdminPanel(c, &MessageCard{
- Title: "Settings applied",
- Content: "Your settings have been tested and applied successfully.
" +
- "You can continue using EcoDash on the Home.",
- Style: "success",
+ return config.RenderAdminPanel(c, Warning{
+ Header: "Restart needed",
+ Body: "In order to apply changes, please restart EcoDash.
" +
+ "If you're running via Docker, click here to restart automatically.",
+ IsSuccess: true,
})
}
@@ -66,28 +64,38 @@ func (config *Config) AdminEndpoint(c *fiber.Ctx) error {
if c.FormValue("username") == config.Administrator.Username && tools.Hash(c.FormValue("password")) == config.Administrator.PasswordHash {
c.Cookie(&fiber.Cookie{Name: "admin_username", Value: c.FormValue("username")})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash(c.FormValue("password"))})
- return config.RenderAdminPanel(c, nil)
+ return config.RenderAdminPanel(c)
}
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
}
if config.IsAuthorized(c) {
- return config.RenderAdminPanel(c, nil)
+ return config.RenderAdminPanel(c)
}
return c.Render("login", config.TemplateDefaultsMap(), "base")
}
-func (config *Config) RenderAdminPanel(c *fiber.Ctx, message *MessageCard) error {
+func (config *Config) RenderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
dirs, err := os.ReadDir("./templates")
if err != nil {
return err
}
+ if len(warning) > 0 {
+ // #nosec // TODO this is dangerous, even if we're escaping the only place where we're passing a non-literal
+ warning[0].BodyHTML = template.HTML(warning[0].Body)
+ return c.Render("admin", fiber.Map{
+ "Defaults": config.getTemplateDefaults(),
+ "Themes": dirs,
+ "Config": config,
+ "Warning": warning[0],
+ }, "base")
+ }
+
return c.Render("admin", fiber.Map{
"Defaults": config.getTemplateDefaults(),
"Themes": dirs,
"Config": config,
- "Message": message,
}, "base")
}
@@ -109,11 +117,11 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
return err
}
- form := Config{
+ form := &Config{
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ APIKey: c.FormValue("api_key"), InstallationDate: dayStart(parsedTime)},
Sensors: Sensors{PolledSmartEnergySummation: c.FormValue("polled_smart_energy_summation"), FossilPercentage: c.FormValue("fossil_percentage")},
Administrator: Administrator{Username: c.FormValue("username") /*PasswordHash to be filled later*/},
- Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks /*MessageCard to be filled later*/},
+ Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks},
}
if c.FormValue("keep_old_password") == "" {
@@ -122,15 +130,6 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
form.Administrator.PasswordHash = config.Administrator.PasswordHash
}
- if c.FormValue("motd_title") != "" || c.FormValue("motd_content") != "" {
- // #nosec the input is admin-defined, and the admin is assumed to be trusted.
- form.Dashboard.MOTD = &MessageCard{
- Title: c.FormValue("motd_title"),
- Content: template.HTML(c.FormValue("motd_content")),
- Style: c.FormValue("motd_style"),
- }
- }
-
fmtURL, err := formatURL(c.FormValue("base_url"))
if err != nil {
return err
@@ -152,17 +151,11 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
return err
}
- *config = form
-
- var confPath string
- if confPath = os.Getenv("CONFIG_PATH"); confPath == "" {
- confPath = "./config.json"
- }
- return os.WriteFile(confPath, js, 0o600)
+ return os.WriteFile("config.json", js, 0o600)
}
func averageExcludingCurrentDay(data []float32) float32 {
- if len(data) <= 1 {
+ if len(data) == 0 {
return 0
}
data = data[:len(data)-1]
@@ -208,6 +201,5 @@ func (config *Config) RenderIndex(c *fiber.Ctx) error {
"EnergyConsumptions": energyConsumptions,
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
"PerDayUsage": perDayUsage,
- "MOTD": config.Dashboard.MOTD,
}, "base")
}
diff --git a/src/main/main.go b/src/main/main.go
index 52e0854..4fcba7a 100644
--- a/src/main/main.go
+++ b/src/main/main.go
@@ -2,7 +2,9 @@ package main
import (
"log"
+ "net/http"
"os"
+ "time"
"git.massivebox.net/ecodash/ecodash/src/ecodash"
"git.massivebox.net/ecodash/ecodash/src/tools"
@@ -12,18 +14,20 @@ import (
)
func main() {
- config, err := ecodash.LoadConfig()
+ config, isFirstRun, err := ecodash.LoadConfig()
if err != nil {
log.Fatal(err)
}
- cr := cron.New()
- _, err = cr.AddFunc("@hourly", config.UpdateHistory)
- if err != nil {
- log.Fatal(err)
+ if !isFirstRun {
+ cr := cron.New()
+ _, err = cr.AddFunc("@hourly", config.UpdateHistory)
+ if err != nil {
+ log.Fatal(err)
+ }
+ cr.Start()
+ config.UpdateHistory()
}
- cr.Start()
- config.UpdateHistory()
engine := html.New("./templates/"+config.Dashboard.Theme, ".html")
engine.AddFunc("divide", tools.TemplateDivide)
@@ -36,10 +40,10 @@ func main() {
app.Static("/assets", "./templates/"+config.Dashboard.Theme+"/assets")
app.Get("/", func(c *fiber.Ctx) error {
- if config.Administrator.Username == "" || config.Administrator.PasswordHash == "" {
+ if isFirstRun {
c.Cookie(&fiber.Cookie{Name: "admin_username", Value: ""})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash("")})
- return config.RenderAdminPanel(c, nil)
+ return config.RenderAdminPanel(c)
}
return config.RenderIndex(c)
})
@@ -50,6 +54,17 @@ func main() {
app.All("/admin", config.AdminEndpoint)
+ app.Get("/restart", func(c *fiber.Ctx) error {
+ if config.IsAuthorized(c) {
+ go func() {
+ time.Sleep(time.Second)
+ os.Exit(1)
+ }()
+ return c.Render("restart", config.TemplateDefaultsMap(), "base")
+ }
+ return c.Redirect("./", http.StatusTemporaryRedirect)
+ })
+
port := os.Getenv("PORT")
if port == "" {
port = "80"
diff --git a/src/tools/tools.go b/src/tools/tools.go
index 3bac1b1..47ab613 100644
--- a/src/tools/tools.go
+++ b/src/tools/tools.go
@@ -17,10 +17,6 @@ func Hash(toHash string) string {
func TemplateDivide(num1, num2 float32) template.HTML {
division := float64(num1 / num2)
- if math.IsNaN(division) || division == 0 {
- return "0"
- }
-
powerOfTen := int(math.Floor(math.Log10(division)))
if powerOfTen >= -2 && powerOfTen <= 2 {
// #nosec G203 // We're only printing floats
diff --git a/templates/default/admin.html b/templates/default/admin.html
index d1e889f..14654a2 100644
--- a/templates/default/admin.html
+++ b/templates/default/admin.html
@@ -4,15 +4,15 @@
Documentation
+ You should be able to continue using EcoDash soon by clicking here.
+ If you get an error like "Address Unreachable", make sure you've allowed your container to restart automatically.
+ Check the error logs if the error persists.
+