Compare commits

...

28 commits

Author SHA1 Message Date
MassiveBox 79c7dde230
Reorganize docs
All checks were successful
CI Pipeline / build (push) Successful in 1m17s
CI Pipeline / build-and-push-docker (push) Successful in 40s
CI Pipeline / publish-executables (push) Successful in 7s
2024-04-19 18:30:11 +02:00
MassiveBox 924b96e0db
Move to Forgejo CI
All checks were successful
CI Pipeline / build (push) Successful in 1m12s
CI Pipeline / build-and-push-docker (push) Successful in 36s
CI Pipeline / publish-executables (push) Successful in 8s
2024-04-18 12:14:36 +02:00
MassiveBox 519796d3d1
Improve data location selection 2024-04-12 21:40:22 +02:00
MassiveBox 1f9827505b
Disable again gosmopolitan, which for some reason figures as non-existent on my machine, but breaks builds on the CI if it's not disabled.
All checks were successful
ecodash/pipeline/head This commit looks good
2023-11-03 23:10:07 +01:00
MassiveBox 99acf1fd18
Fix linter errors
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-11-03 22:53:54 +01:00
MassiveBox 8b81c41bd7
Add admin-settable MOTD, rewrite existing warning system to use it
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-10-31 23:59:09 +01:00
MassiveBox 4bf1455ba4
Remove need for restart on settings change
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-10-29 18:32:35 +01:00
MassiveBox 75423645ff
Work around Jenkins'BS
All checks were successful
ecodash/pipeline/head This commit looks good
2023-07-22 15:40:00 +02:00
MassiveBox d5d6aa4d08
Work around docker's BS
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 15:37:59 +02:00
MassiveBox 153b507a69
Load image into docker
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 12:27:56 +02:00
MassiveBox 07b9571ffa
Fix
All checks were successful
ecodash/pipeline/head This commit looks good
2023-07-22 11:34:01 +02:00
MassiveBox 8db3f56ca4
Hopefully fix docker complaining about nonsense
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 11:14:54 +02:00
MassiveBox ad89006cc4
Fix published artifacts
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 10:34:21 +02:00
MassiveBox fa28b77c52
Add missing commas
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 10:26:14 +02:00
MassiveBox c650a1fae1
Fix multi-arch container build
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-22 10:24:01 +02:00
MassiveBox 6bfe31de56
Whoops
All checks were successful
ecodash/pipeline/head This commit looks good
2023-07-21 18:59:59 +02:00
MassiveBox 97994ab47a
Minor inconvenience
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-21 18:46:31 +02:00
MassiveBox d0f8950c3c
Add jenkins'Dockerfile
Some checks failed
ecodash/pipeline/head There was a failure building this commit
whoops I forgot
2023-07-21 18:30:25 +02:00
MassiveBox 394091d885
Fix pipeline
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-21 18:10:35 +02:00
MassiveBox 90e83eaf62
Pipeline improvements
Some checks failed
ecodash/pipeline/head There was a failure building this commit
2023-07-21 17:01:53 +02:00
MassiveBox 33f09c93bd
Attempt at adding Jenkins CI
All checks were successful
ecodash/pipeline/head This commit looks good
2023-07-21 09:43:36 +02:00
MassiveBox 4b3af653b8
Merge remote-tracking branch 'origin/master' 2023-06-24 09:02:49 +02:00
MassiveBox 82114b8c76
Optimize CI 2023-06-24 09:02:30 +02:00
MassiveBox 002fab4786
Optimize CI 2023-06-24 09:00:20 +02:00
MassiveBox b720bf4ac0
Squash bugs
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2023-06-23 20:39:37 +02:00
MassiveBox d51f42ecb1
Fix linter
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-06-23 16:51:43 +02:00
MassiveBox 068aae82c3
Fix NaN*10^(-9223372036854775808) glitch
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-06-23 13:43:03 +02:00
MassiveBox 22ed86d6f3 Merge pull request 'Switch to modernc.org/sqlite' (#3) from danog/ecodash:master into master
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
ci/woodpecker/tag/woodpecker Pipeline was successful
Reviewed-on: ecodash/ecodash#3
2023-05-02 15:52:19 +02:00
16 changed files with 267 additions and 170 deletions

View file

@ -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"]

View file

@ -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 }}

View file

@ -234,6 +234,8 @@ linters:
- wrapcheck - wrapcheck
- nonamedreturns - nonamedreturns
- gomnd - gomnd
- depguard
- gosmopolitan
enable-all: true enable-all: true
fast: false fast: false

View file

@ -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

View file

@ -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

View file

@ -9,10 +9,9 @@ COPY src /app/src
COPY go.mod /app/ COPY go.mod /app/
COPY .golangci.yml /app/ COPY .golangci.yml /app/
RUN go mod tidy RUN go mod tidy; \
RUN golangci-lint run golangci-lint run; \
RUN go test ./src/... go test ./src/...
RUN CGO_ENABLED=1 go build -o app src/main/main.go RUN CGO_ENABLED=1 go build -o app src/main/main.go
FROM alpine:latest FROM alpine:latest
@ -21,6 +20,9 @@ WORKDIR /app
COPY --from=1 /app/app . COPY --from=1 /app/app .
COPY ./templates /app/templates COPY ./templates /app/templates
RUN touch config.json database.db
CMD ["./app"] RUN mkdir data
ENV DATABASE_PATH=./data/database.db
ENV CONFIG_PATH=./data/config.json
CMD "./app"

View file

@ -1,21 +1,19 @@
# 🌿 EcoDash # 🌿 EcoDash
[![status-badge](https://woodpecker.massivebox.net/api/badges/ecodash/ecodash/status.svg)](https://woodpecker.massivebox.net/ecodash/ecodash) [![Visit our website](https://cloud.massivebox.net/api/public/dl/yEzoZyW8?inline=true)](https://ecodash.xyz) [![Support the project](https://cloud.massivebox.net/api/public/dl/dthbBylL?inline=true)](https://ecodash.xyz/contribute) [![Support the project](https://cloud.massivebox.net/index.php/s/DcxB6KwkDZALbXw/download?path=%2FEcoDash&files=support-the-project.svg)](https://massivebox.net/pages/donate.html)
EcoDash is a simple way to show your users how much your server consumes. 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. 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 ## 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) - [📖 Introduction](https://git.massivebox.net/massivebox/ecodash/wiki)
- [🛣 Roadmap](https://ecodash.xyz/docs/roadmap) - [⬇️ Installation](https://git.massivebox.net/massivebox/ecodash/wiki/install)
- [⬇️ Install](https://ecodash.xyz/docs/install) - [⚙️ Configuration](https://git.massivebox.net/massivebox/ecodash/wiki/config)
- [⚙️ Setup](https://ecodash.xyz/docs/setup)
- [🆘 Support](https://ecodash.xyz/docs/support)
## License ## License

View file

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"html/template"
"os" "os"
"reflect" "reflect"
"regexp" "regexp"
@ -37,12 +38,19 @@ type Administrator struct {
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
} }
type Dashboard struct { type Dashboard struct {
MOTD *MessageCard `json:"motd"`
Name string `json:"name"` Name string `json:"name"`
Theme string `json:"theme"` Theme string `json:"theme"`
FooterLinks []Link `json:"footer_links"` FooterLinks []Link `json:"footer_links"`
HeaderLinks []Link `json:"header_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") var errBadHAFormat = errors.New("HomeAssistant base URL is badly formatted")
func formatURL(url string) (string, error) { func formatURL(url string) (string, error) {
@ -61,10 +69,14 @@ func formatURL(url string) (string, error) {
return url, nil return url, nil
} }
func LoadConfig() (config *Config, isFirstRun bool, err error) { func LoadConfig() (config *Config, err error) {
db, err := sql.Open("sqlite", "./database.db") var dbPath string
if dbPath = os.Getenv("DATABASE_PATH"); dbPath == "" {
dbPath = "./database.db"
}
db, err := sql.Open("sqlite", dbPath)
if err != nil { if err != nil {
return &Config{}, false, err return &Config{}, err
} }
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" ( _, err = db.Exec(`CREATE TABLE IF NOT EXISTS "cache" (
@ -74,7 +86,7 @@ func LoadConfig() (config *Config, isFirstRun bool, err error) {
PRIMARY KEY("time") PRIMARY KEY("time")
);`) );`)
if err != nil { if err != nil {
return &Config{}, false, err return &Config{}, err
} }
defaultConfig := &Config{} defaultConfig := &Config{}
@ -91,28 +103,32 @@ func LoadConfig() (config *Config, isFirstRun bool, err error) {
}) })
defaultConfig.db = db 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 err != nil {
// if the data file doesn't exist, we consider it a first run // if the data file doesn't exist, we consider it a first run
if os.IsNotExist(err) { 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 the data file is empty, we consider it as a first run
if len(data) == 0 { if len(data) == 0 {
return defaultConfig, true, nil return defaultConfig, nil
} }
conf := &Config{} conf := &Config{}
err = json.Unmarshal(data, &conf) err = json.Unmarshal(data, &conf)
if err != nil { if err != nil {
return &Config{}, false, err return &Config{}, err
} }
conf.db = db conf.db = db
return conf, false, nil return conf, nil
} }
func (config *Config) IsAuthorized(c *fiber.Ctx) bool { func (config *Config) IsAuthorized(c *fiber.Ctx) bool {

View file

@ -75,7 +75,7 @@ func (config *Config) refreshCacheFromPast(pastTime time.Time) error {
} }
defer stmtIgnore.Close() defer stmtIgnore.Close()
for key, day := range greenEnergyPercentage { for key, day := range historyPolledSmartEnergySummation {
var stmt *sql.Stmt var stmt *sql.Stmt
if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 { if greenEnergyPercentage[key].Value != 0 && historyPolledSmartEnergySummation[key].Value != 0 {
stmt = stmtReplace stmt = stmtReplace

View file

@ -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 if config.IsAuthorized(c) { // here the user is submitting the form to change configuration
err := config.saveAdminForm(c) err := config.saveAdminForm(c)
if err != nil { if err != nil {
return config.RenderAdminPanel(c, Warning{ // #nosec the input is admin-defined, and the admin is assumed to be trusted.
Header: "An error occurred!", return config.RenderAdminPanel(c, &MessageCard{
Body: html.EscapeString(err.Error()), Title: "An error occurred!",
Content: template.HTML(html.EscapeString(err.Error())),
Style: "error",
}) })
} }
return config.RenderAdminPanel(c, Warning{ return config.RenderAdminPanel(c, &MessageCard{
Header: "Restart needed", Title: "Settings applied",
Body: "In order to apply changes, please <b>restart EcoDash</b>.<br>" + Content: "Your settings have been tested and <b>applied successfully</b>.<br>" +
"If you're running via Docker, click <a href='./restart'>here</a> to restart automatically.", "You can continue using EcoDash on the <a href='./'>Home</a>.",
IsSuccess: true, 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 { 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_username", Value: c.FormValue("username")})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash(c.FormValue("password"))}) 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") return c.Render("login", fiber.Map{"Defaults": config.getTemplateDefaults(), "Failed": true}, "base")
} }
if config.IsAuthorized(c) { if config.IsAuthorized(c) {
return config.RenderAdminPanel(c) return config.RenderAdminPanel(c, nil)
} }
return c.Render("login", config.TemplateDefaultsMap(), "base") 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") dirs, err := os.ReadDir("./templates")
if err != nil { if err != nil {
return err 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{ return c.Render("admin", fiber.Map{
"Defaults": config.getTemplateDefaults(), "Defaults": config.getTemplateDefaults(),
"Themes": dirs, "Themes": dirs,
"Config": config, "Config": config,
"Message": message,
}, "base") }, "base")
} }
@ -117,11 +109,11 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
return err return err
} }
form := &Config{ form := Config{
HomeAssistant: HomeAssistant{ /*BaseURL to be filled later*/ APIKey: c.FormValue("api_key"), InstallationDate: dayStart(parsedTime)}, 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")}, 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*/}, 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") == "" { if c.FormValue("keep_old_password") == "" {
@ -130,6 +122,15 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
form.Administrator.PasswordHash = config.Administrator.PasswordHash 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")) fmtURL, err := formatURL(c.FormValue("base_url"))
if err != nil { if err != nil {
return err return err
@ -151,11 +152,17 @@ func (config *Config) saveAdminForm(c *fiber.Ctx) error {
return err 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 { func averageExcludingCurrentDay(data []float32) float32 {
if len(data) == 0 { if len(data) <= 1 {
return 0 return 0
} }
data = data[:len(data)-1] data = data[:len(data)-1]
@ -201,5 +208,6 @@ func (config *Config) RenderIndex(c *fiber.Ctx) error {
"EnergyConsumptions": energyConsumptions, "EnergyConsumptions": energyConsumptions,
"GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents), "GreenEnergyPercent": averageExcludingCurrentDay(greenEnergyPercents),
"PerDayUsage": perDayUsage, "PerDayUsage": perDayUsage,
"MOTD": config.Dashboard.MOTD,
}, "base") }, "base")
} }

View file

@ -2,9 +2,7 @@ package main
import ( import (
"log" "log"
"net/http"
"os" "os"
"time"
"git.massivebox.net/ecodash/ecodash/src/ecodash" "git.massivebox.net/ecodash/ecodash/src/ecodash"
"git.massivebox.net/ecodash/ecodash/src/tools" "git.massivebox.net/ecodash/ecodash/src/tools"
@ -14,12 +12,11 @@ import (
) )
func main() { func main() {
config, isFirstRun, err := ecodash.LoadConfig() config, err := ecodash.LoadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !isFirstRun {
cr := cron.New() cr := cron.New()
_, err = cr.AddFunc("@hourly", config.UpdateHistory) _, err = cr.AddFunc("@hourly", config.UpdateHistory)
if err != nil { if err != nil {
@ -27,7 +24,6 @@ func main() {
} }
cr.Start() cr.Start()
config.UpdateHistory() config.UpdateHistory()
}
engine := html.New("./templates/"+config.Dashboard.Theme, ".html") engine := html.New("./templates/"+config.Dashboard.Theme, ".html")
engine.AddFunc("divide", tools.TemplateDivide) engine.AddFunc("divide", tools.TemplateDivide)
@ -40,10 +36,10 @@ func main() {
app.Static("/assets", "./templates/"+config.Dashboard.Theme+"/assets") app.Static("/assets", "./templates/"+config.Dashboard.Theme+"/assets")
app.Get("/", func(c *fiber.Ctx) error { 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_username", Value: ""})
c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash("")}) c.Cookie(&fiber.Cookie{Name: "admin_password_hash", Value: tools.Hash("")})
return config.RenderAdminPanel(c) return config.RenderAdminPanel(c, nil)
} }
return config.RenderIndex(c) return config.RenderIndex(c)
}) })
@ -54,17 +50,6 @@ func main() {
app.All("/admin", config.AdminEndpoint) 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") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "80" port = "80"

View file

@ -17,6 +17,10 @@ func Hash(toHash string) string {
func TemplateDivide(num1, num2 float32) template.HTML { func TemplateDivide(num1, num2 float32) template.HTML {
division := float64(num1 / num2) division := float64(num1 / num2)
if math.IsNaN(division) || division == 0 {
return "0"
}
powerOfTen := int(math.Floor(math.Log10(division))) powerOfTen := int(math.Floor(math.Log10(division)))
if powerOfTen >= -2 && powerOfTen <= 2 { if powerOfTen >= -2 && powerOfTen <= 2 {
// #nosec G203 // We're only printing floats // #nosec G203 // We're only printing floats

View file

@ -4,13 +4,13 @@
<a href="https://ecodash.xyz/docs/setup/admin-panel">Documentation</a> <a href="https://ecodash.xyz/docs/setup/admin-panel">Documentation</a>
</p> </p>
{{if .Warning}} {{if .Message}}
<article class="card" style="background-color: {{if .Warning.IsSuccess}}#008000{{else}}#ff5050{{end}}; color: white"> <article class="card {{.Message.Style}}">
<header> <header>
<h3>{{.Warning.Header}}</h3> <h3>{{.Message.Title}}</h3>
</header> </header>
<footer> <footer>
<p>{{.Warning.BodyHTML}}</p> <p>{{.Message.Content}}</p>
</footer> </footer>
</article> </article>
{{end}} {{end}}
@ -45,6 +45,16 @@
</select> </select>
</label> </label>
<label>Dashboard name <input type="text" name="name" value="{{.Config.Dashboard.Name}}"></label> <label>Dashboard name <input type="text" name="name" value="{{.Config.Dashboard.Name}}"></label>
<label>MOTD title <input type="text" name="motd_title" value="{{if .Config.Dashboard.MOTD}}{{.Config.Dashboard.MOTD.Title}}{{end}}"></label>
<label>MOTD content <input type="text" name="motd_content" value="{{if .Config.Dashboard.MOTD}}{{.Config.Dashboard.MOTD.Content}}{{end}}"></label>
<label>MOTD style
<select name="motd_style">
<option value="" {{if .Config.Dashboard.MOTD}}{{if eq .Config.Dashboard.MOTD.Style ""}}selected{{end}}{{end}}>Default</option>
<option value="success" {{if .Config.Dashboard.MOTD}}{{if eq .Config.Dashboard.MOTD.Style "success"}}selected{{end}}{{end}}>Success</option>
<option value="warning" {{if .Config.Dashboard.MOTD}}{{if eq .Config.Dashboard.MOTD.Style "warning"}}selected{{end}}{{end}}>Warning</option>
<option value="error" {{if .Config.Dashboard.MOTD}}{{if eq .Config.Dashboard.MOTD.Style "error"}}selected{{end}}{{end}}>Error</option>
</select>
</label>
<input type="submit" placeholder="Submit" style="margin-top: 2em; width: 100%"> <input type="submit" placeholder="Submit" style="margin-top: 2em; width: 100%">
</form> </form>

View file

@ -47,3 +47,13 @@ svg, footer img { width: 100% }
} }
} }
.success {
background-color: #008000; color: white
}
.warning {
background-color: #807a00; color: white
}
.error {
background-color: #800000; color: white
}

View file

@ -1,5 +1,16 @@
<script src="assets/chartjs/chart.js"></script> <script src="assets/chartjs/chart.js"></script>
{{if .MOTD}}
<article class="card {{.MOTD.Style}}">
<header>
<h3>{{.MOTD.Title}}</h3>
</header>
<footer>
<p>{{.MOTD.Content}}</p>
</footer>
</article>
{{end}}
<h1>Green report</h1> <h1>Green report</h1>
<canvas id="report"></canvas> <canvas id="report"></canvas>
@ -7,7 +18,6 @@
This server's energy statistics for the last eight days (current day included) This server's energy statistics for the last eight days (current day included)
</p> </p>
<div class="flex two"> <div class="flex two">
<div> <div>
<div class="home-cards card"> <div class="home-cards card">

View file

@ -1,6 +0,0 @@
<h1>Restarting...</h1>
<p>
You should be able to continue using EcoDash soon by clicking <a href="/">here</a>.<br>
If you get an error like "Address Unreachable", make sure you've allowed your container to restart automatically.<br>
Check the error logs if the error persists.
</p>