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:
Tomas Dvorak committed 2026-09-17 20:29:22 +02:00
1 parent 6004105038
commit 8afc7df53b
21 files changed
+1072 -40

No files matched your search

+10 -2
View File
@@ -15,16 +15,22 @@ import (
"go.uber.org/zap"
)
// Server holds handler dependencies: DB, icon dir, status cache.
// Server holds handler dependencies: DB, icon dir, status + widget caches.
type Server struct {
db *sql.DB
iconsDir string
status *statusCache
widgets *widgetCache
}
// NewRouter builds the HTTP handler: zap access log, recovery, /api routes.
func NewRouter(logger *zap.Logger, db *sql.DB, iconsDir string) *gin.Engine {
s := &Server{db: db, iconsDir: iconsDir, status: newStatusCache(60 * time.Second)}
s := &Server{
db: db,
iconsDir: iconsDir,
status: newStatusCache(60 * time.Second),
widgets: newWidgetCache(30 * time.Second),
}
r := gin.New()
r.Use(accessLog(logger), gin.Recovery())
@@ -46,6 +52,8 @@ func NewRouter(logger *zap.Logger, db *sql.DB, iconsDir string) *gin.Engine {
v1.GET("/icons/:file", s.getIcon)
v1.GET("/status", s.getStatus)
v1.GET("/widgets/types", s.widgetTypes)
v1.GET("/widgets/:id/data", s.widgetData)
v1.GET("/settings", s.getSettings)
v1.PUT("/settings", s.putSettings)
v1.GET("/export", s.exportBoard)