mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 07:41:27 +00:00
feat: phase 2 widgets — registry, fetchers, clock, config dialog
Backend: internal/widget registry (type metadata + Fetcher), Pi-hole v5 / AdGuard / Immich fetchers, /api/widgets/types + /api/widgets/:id/data with 30s TTL cache (failures cached too), kind+config on item create/patch. Config secrets are never logged or echoed in errors. Frontend: WidgetContent host maps config.type to a component, polls /data every 30s for server-backed types; clock renders client-side. Generic WidgetDialog builds its form from the registry's field list. Widget tiles are sortable items in both renderers. Generated with [Devin](https://devin.ai) Authored By: TDvorak <[email protected]> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
6004105038
commit
8afc7df53b
21 files changed
+1072
-40
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tdvorak/dash/internal/db"
|
||||
@@ -122,6 +123,61 @@ func TestSettingsRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWidgetEndpoints(t *testing.T) {
|
||||
srv := testServer(t)
|
||||
defer srv.Close()
|
||||
|
||||
// Types registry serves all integrations incl. the local clock.
|
||||
resp, err := http.Get(srv.URL + "/api/widgets/types")
|
||||
if err != nil {
|
||||
t.Fatalf("get types: %v", err)
|
||||
}
|
||||
var types []struct {
|
||||
Type string `json:"type"`
|
||||
Local bool `json:"local"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&types); err != nil {
|
||||
t.Fatalf("decode types: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
got := map[string]bool{}
|
||||
for _, w := range types {
|
||||
got[w.Type] = w.Local
|
||||
}
|
||||
for _, want := range []string{"clock", "pihole", "adguard", "immich"} {
|
||||
if _, ok := got[want]; !ok {
|
||||
t.Fatalf("missing widget type %s in %v", want, got)
|
||||
}
|
||||
}
|
||||
if !got["clock"] {
|
||||
t.Fatalf("clock must be local, got %v", got)
|
||||
}
|
||||
|
||||
// Widget data on a service item -> 400; missing -> 404.
|
||||
_, a := do(t, "POST", srv.URL+"/api/sections", `{"name":"A"}`)
|
||||
_, it := do(t, "POST", srv.URL+"/api/items", `{"sectionId":"`+a["id"].(string)+`","name":"Svc"}`)
|
||||
res, _ := do(t, "GET", srv.URL+"/api/widgets/"+it["id"].(string)+"/data", "")
|
||||
if res.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("service item data: %d", res.StatusCode)
|
||||
}
|
||||
res, _ = do(t, "GET", srv.URL+"/api/widgets/i_missing/data", "")
|
||||
if res.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("missing item: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
// Widget item with dead upstream -> 502, then cached (second call fast).
|
||||
_, w := do(t, "POST", srv.URL+"/api/items", `{"sectionId":"`+a["id"].(string)+`","kind":"widget","name":"PH","config":{"type":"pihole","endpoint":"http://127.0.0.1:1","token":"x"}}`)
|
||||
res, _ = do(t, "GET", srv.URL+"/api/widgets/"+w["id"].(string)+"/data", "")
|
||||
if res.StatusCode != http.StatusBadGateway {
|
||||
t.Fatalf("dead upstream: %d", res.StatusCode)
|
||||
}
|
||||
start := time.Now()
|
||||
res, _ = do(t, "GET", srv.URL+"/api/widgets/"+w["id"].(string)+"/data", "")
|
||||
if res.StatusCode != http.StatusBadGateway || time.Since(start) > time.Second {
|
||||
t.Fatalf("expected cached 502, got %d in %v", res.StatusCode, time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMidpoint(t *testing.T) {
|
||||
f := func(v float64) *float64 { return &v }
|
||||
if got := midpoint(nil, nil); got != 1024 {
|
||||
|
||||
Reference in new issue
Block a user