diff --git a/.forgejo/workflows/Dockerfile b/.forgejo/workflows/Dockerfile
new file mode 100644
index 0000000..82c8879
--- /dev/null
+++ b/.forgejo/workflows/Dockerfile
@@ -0,0 +1,10 @@
+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
new file mode 100644
index 0000000..8f62b00
--- /dev/null
+++ b/.forgejo/workflows/build.yaml
@@ -0,0 +1,123 @@
+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 c128eff..edcb24b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -234,6 +234,8 @@ linters:
- wrapcheck
- nonamedreturns
- gomnd
+ - depguard
+ - gosmopolitan
enable-all: true
fast: false
diff --git a/.woodpecker.yml b/.woodpecker.yml
deleted file mode 100644
index d431220..0000000
--- a/.woodpecker.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index b9d6e3f..0000000
--- a/BUILD.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# 👷 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 47f94d0..f6f7e04 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,10 +9,9 @@ COPY src /app/src
COPY go.mod /app/
COPY .golangci.yml /app/
-RUN go mod tidy
-RUN golangci-lint run
-RUN go test ./src/...
-
+RUN go mod tidy; \
+ golangci-lint run; \
+ go test ./src/...
RUN CGO_ENABLED=1 go build -o app src/main/main.go
FROM alpine:latest
@@ -21,6 +20,9 @@ WORKDIR /app
COPY --from=1 /app/app .
COPY ./templates /app/templates
-RUN touch config.json database.db
-CMD ["./app"]
\ No newline at end of file
+RUN mkdir data
+ENV DATABASE_PATH=./data/database.db
+ENV CONFIG_PATH=./data/config.json
+
+CMD "./app"
\ No newline at end of file
diff --git a/README.md b/README.md
index f3c9f66..40c1bc7 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,19 @@
# 🌿 EcoDash
-[](https://woodpecker.massivebox.net/ecodash/ecodash) [](https://ecodash.xyz) [](https://ecodash.xyz/contribute)
+[](https://massivebox.net/pages/donate.html)
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://demo.ecodash.xyz
+You can see it in action here: https://ecodash.massivebox.net
## Get started
-Check out the documentation in our [website](https://ecodash.xyz) to get started with EcoDash.
+Check out the documentation in our [wiki](https://git.massivebox.net/massivebox/ecodash/wiki) to get started with EcoDash.
-- [📖 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)
+- [📖 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)
## License
diff --git a/src/ecodash/config.go b/src/ecodash/config.go
index 91b7541..b183666 100644
--- a/src/ecodash/config.go
+++ b/src/ecodash/config.go
@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"errors"
+ "html/template"
"os"
"reflect"
"regexp"
@@ -37,10 +38,17 @@ type Administrator struct {
PasswordHash string `json:"password_hash"`
}
type Dashboard struct {
- Name string `json:"name"`
- Theme string `json:"theme"`
- FooterLinks []Link `json:"footer_links"`
- HeaderLinks []Link `json:"header_links"`
+ 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"`
}
var errBadHAFormat = errors.New("HomeAssistant base URL is badly formatted")
@@ -61,10 +69,14 @@ func formatURL(url string) (string, error) {
return url, nil
}
-func LoadConfig() (config *Config, isFirstRun bool, err error) {
- db, err := sql.Open("sqlite", "./database.db")
+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)
if err != nil {
- return &Config{}, false, err
+ return &Config{}, err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" (
@@ -74,7 +86,7 @@ func LoadConfig() (config *Config, isFirstRun bool, err error) {
PRIMARY KEY("time")
);`)
if err != nil {
- return &Config{}, false, err
+ return &Config{}, err
}
defaultConfig := &Config{}
@@ -91,28 +103,32 @@ func LoadConfig() (config *Config, isFirstRun bool, err error) {
})
defaultConfig.db = db
- data, err := os.ReadFile("config.json")
+ var confPath string
+ if confPath = os.Getenv("CONFIG_PATH"); confPath == "" {
+ confPath = "./config.json"
+ }
+ data, err := os.ReadFile(confPath)
if err != nil {
// if the data file doesn't exist, we consider it a first run
if os.IsNotExist(err) {
- return defaultConfig, true, nil
+ return defaultConfig, nil
}
- return &Config{}, false, err
+ return &Config{}, err
}
// if the data file is empty, we consider it as a first run
if len(data) == 0 {
- return defaultConfig, true, nil
+ return defaultConfig, nil
}
conf := &Config{}
err = json.Unmarshal(data, &conf)
if err != nil {
- return &Config{}, false, err
+ return &Config{}, err
}
conf.db = db
- return conf, false, nil
+ return conf, nil
}
func (config *Config) IsAuthorized(c *fiber.Ctx) bool {
diff --git a/src/ecodash/database.go b/src/ecodash/database.go
index 0c44e35..f580b15 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 greenEnergyPercentage {
+ for key, day := range historyPolledSmartEnergySummation {
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 7c32f2a..02560ae 100644
--- a/src/ecodash/http.go
+++ b/src/ecodash/http.go
@@ -47,16 +47,18 @@ 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 {
- return config.RenderAdminPanel(c, Warning{
- Header: "An error occurred!",
- Body: html.EscapeString(err.Error()),
+ // #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: "Restart needed",
- Body: "In order to apply changes, please restart EcoDash.
" +
- "If you're running via Docker, click here to restart automatically.",
- IsSuccess: true,
+ 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",
})
}
@@ -64,38 +66,28 @@ 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)
+ return config.RenderAdminPanel(c, nil)
}
return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
}
if config.IsAuthorized(c) {
- return config.RenderAdminPanel(c)
+ return config.RenderAdminPanel(c, nil)
}
return c.Render("login", config.TemplateDefaultsMap(), "base")
}
-func (config *Config) RenderAdminPanel(c *fiber.Ctx, warning ...Warning) error {
+func (config *Config) RenderAdminPanel(c *fiber.Ctx, message *MessageCard) 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")
}
@@ -117,11 +109,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},
+ Dashboard: Dashboard{Theme: c.FormValue("theme"), Name: c.FormValue("name"), HeaderLinks: config.Dashboard.HeaderLinks, FooterLinks: config.Dashboard.FooterLinks /*MessageCard to be filled later*/},
}
if c.FormValue("keep_old_password") == "" {
@@ -130,6 +122,15 @@ 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
@@ -151,11 +152,17 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
return err
}
- return os.WriteFile("config.json", js, 0o600)
+ *config = form
+
+ var confPath string
+ if confPath = os.Getenv("CONFIG_PATH"); confPath == "" {
+ confPath = "./config.json"
+ }
+ return os.WriteFile(confPath, js, 0o600)
}
func averageExcludingCurrentDay(data []float32) float32 {
- if len(data) == 0 {
+ if len(data) <= 1 {
return 0
}
data = data[:len(data)-1]
@@ -201,5 +208,6 @@ 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 4fcba7a..52e0854 100644
--- a/src/main/main.go
+++ b/src/main/main.go
@@ -2,9 +2,7 @@ package main
import (
"log"
- "net/http"
"os"
- "time"
"git.massivebox.net/ecodash/ecodash/src/ecodash"
"git.massivebox.net/ecodash/ecodash/src/tools"
@@ -14,20 +12,18 @@ import (
)
func main() {
- config, isFirstRun, err := ecodash.LoadConfig()
+ config, err := ecodash.LoadConfig()
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 := cron.New()
+ _, err = cr.AddFunc("@hourly", config.UpdateHistory)
+ if err != nil {
+ log.Fatal(err)
}
+ cr.Start()
+ config.UpdateHistory()
engine := html.New("./templates/"+config.Dashboard.Theme, ".html")
engine.AddFunc("divide", tools.TemplateDivide)
@@ -40,10 +36,10 @@ func main() {
app.Static("/assets", "./templates/"+config.Dashboard.Theme+"/assets")
app.Get("/", func(c *fiber.Ctx) error {
- if isFirstRun {
+ if config.Administrator.Username == "" || config.Administrator.PasswordHash == "" {
c.Cookie(&fiber.Cookie{Name: "admin_username", Value: ""})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash("")})
- return config.RenderAdminPanel(c)
+ return config.RenderAdminPanel(c, nil)
}
return config.RenderIndex(c)
})
@@ -54,17 +50,6 @@ 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 47ab613..3bac1b1 100644
--- a/src/tools/tools.go
+++ b/src/tools/tools.go
@@ -17,6 +17,10 @@ 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 14654a2..d1e889f 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.
-