forked from massivebox/ecodash
Compare commits
28 commits
Author | SHA1 | Date | |
---|---|---|---|
MassiveBox | 79c7dde230 | ||
MassiveBox | 924b96e0db | ||
MassiveBox | 519796d3d1 | ||
MassiveBox | 1f9827505b | ||
MassiveBox | 99acf1fd18 | ||
MassiveBox | 8b81c41bd7 | ||
MassiveBox | 4bf1455ba4 | ||
MassiveBox | 75423645ff | ||
MassiveBox | d5d6aa4d08 | ||
MassiveBox | 153b507a69 | ||
MassiveBox | 07b9571ffa | ||
MassiveBox | 8db3f56ca4 | ||
MassiveBox | ad89006cc4 | ||
MassiveBox | fa28b77c52 | ||
MassiveBox | c650a1fae1 | ||
MassiveBox | 6bfe31de56 | ||
MassiveBox | 97994ab47a | ||
MassiveBox | d0f8950c3c | ||
MassiveBox | 394091d885 | ||
MassiveBox | 90e83eaf62 | ||
MassiveBox | 33f09c93bd | ||
MassiveBox | 4b3af653b8 | ||
MassiveBox | 82114b8c76 | ||
MassiveBox | 002fab4786 | ||
MassiveBox | b720bf4ac0 | ||
MassiveBox | d51f42ecb1 | ||
MassiveBox | 068aae82c3 | ||
MassiveBox | 22ed86d6f3 |
10
.forgejo/workflows/Dockerfile
Normal file
10
.forgejo/workflows/Dockerfile
Normal 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"]
|
123
.forgejo/workflows/build.yaml
Normal file
123
.forgejo/workflows/build.yaml
Normal 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 }}
|
|
@ -234,6 +234,8 @@ linters:
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
- nonamedreturns
|
- nonamedreturns
|
||||||
- gomnd
|
- gomnd
|
||||||
|
- depguard
|
||||||
|
- gosmopolitan
|
||||||
|
|
||||||
enable-all: true
|
enable-all: true
|
||||||
fast: false
|
fast: false
|
||||||
|
|
|
@ -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
|
|
30
BUILD.md
30
BUILD.md
|
@ -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
|
|
14
Dockerfile
14
Dockerfile
|
@ -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"
|
14
README.md
14
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -37,10 +38,17 @@ type Administrator struct {
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash string `json:"password_hash"`
|
||||||
}
|
}
|
||||||
type Dashboard struct {
|
type Dashboard struct {
|
||||||
Name string `json:"name"`
|
MOTD *MessageCard `json:"motd"`
|
||||||
Theme string `json:"theme"`
|
Name string `json:"name"`
|
||||||
FooterLinks []Link `json:"footer_links"`
|
Theme string `json:"theme"`
|
||||||
HeaderLinks []Link `json:"header_links"`
|
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")
|
var errBadHAFormat = errors.New("HomeAssistant base URL is badly formatted")
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,20 +12,18 @@ 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 {
|
log.Fatal(err)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
cr.Start()
|
|
||||||
config.UpdateHistory()
|
|
||||||
}
|
}
|
||||||
|
cr.Start()
|
||||||
|
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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
<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}}
|
||||||
|
|
||||||
<form action="./admin" method="POST">
|
<form action="./admin" method="POST">
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -46,4 +46,14 @@ svg, footer img { width: 100% }
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background-color: #008000; color: white
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #807a00; color: white
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #800000; color: white
|
||||||
}
|
}
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
|
Loading…
Reference in a new issue