Initial commit — energy dashboard frontend (TimescaleDB + Vue/Chart.js)

This commit is contained in:
2026-04-18 11:14:12 +02:00
commit 9fa7d36610
28 changed files with 3243 additions and 0 deletions

115
internal/api/router.go Normal file
View File

@@ -0,0 +1,115 @@
package api
import (
"context"
"encoding/json"
"io/fs"
"log"
"net/http"
"strconv"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
"energy-frontend/internal/config"
)
func NewRouter(cfg config.Config, frontendFS fs.FS) http.Handler {
pool, err := pgxpool.New(context.Background(), cfg.DB.DSN)
if err != nil {
log.Fatalf("db pool: %v", err)
}
mux := http.NewServeMux()
auth := newAuthHandler(cfg.Auth)
mux.HandleFunc("POST /api/login", auth.loginHandler)
mux.HandleFunc("POST /api/logout", auth.logoutHandler)
protected := http.NewServeMux()
protected.HandleFunc("GET /api/auth/check", auth.checkHandler)
protected.HandleFunc("GET /api/bars", func(w http.ResponseWriter, r *http.Request) {
period := r.URL.Query().Get("period")
if period == "" {
period = "day"
}
count, err := strconv.Atoi(r.URL.Query().Get("count"))
if err != nil || count <= 0 {
count = periodDefaults[period]
}
bars, err := getBars(r.Context(), pool, period, count)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"bars": bars})
})
protected.HandleFunc("GET /api/power", func(w http.ResponseWriter, r *http.Request) {
tr := r.URL.Query().Get("range")
if tr == "" {
tr = "24h"
}
pts, err := getPower(r.Context(), pool, tr)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"points": pts})
})
protected.HandleFunc("GET /api/battery", func(w http.ResponseWriter, r *http.Request) {
tr := r.URL.Query().Get("range")
if tr == "" {
tr = "24h"
}
pts, err := getBattery(r.Context(), pool, tr)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"points": pts})
})
mux.Handle("/api/", auth.middleware(protected))
if frontendFS != nil {
mux.Handle("/", spaHandler(frontendFS))
}
return corsMiddleware(mux)
}
func spaHandler(frontendFS fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(frontendFS))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
if _, err := fs.Stat(frontendFS, path); err == nil {
fileServer.ServeHTTP(w, r)
return
}
r.URL.Path = "/"
fileServer.ServeHTTP(w, r)
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "true")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}