mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-03 23:12:56 +00:00
b17a06fbba
A clean, customizable homelab dashboard inspired by CasaOS. Features: - Empty-first dashboard (no demo data) - 3 themes: Light, Dark, CasaOS glassmorphism - Widgets: Clock (multi-timezone), Pi-hole, Memos, Immich, Image - Drag & drop app organization - Grid + list view for apps - Groups with collapse/expand - Proper widget refresh handling - Visual timezone picker - Square app cards with hover effects Stack: Go + Gin + PostgreSQL + Next.js 15 + React 19 + Tailwind CSS + shadcn/ui
94 lines
2.2 KiB
Go
94 lines
2.2 KiB
Go
package widgets
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"dash/backend/internal/store"
|
|
)
|
|
|
|
type Registry struct {
|
|
store *store.Store
|
|
client *http.Client
|
|
cacheTTL time.Duration
|
|
}
|
|
|
|
func NewRegistry(st *store.Store, timeout time.Duration, cacheTTL time.Duration) *Registry {
|
|
return &Registry{
|
|
store: st,
|
|
client: &http.Client{Timeout: timeout},
|
|
cacheTTL: cacheTTL,
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Data(ctx context.Context, widget store.WidgetInstance) (store.WidgetData, error) {
|
|
cached, err := r.store.WidgetData(ctx, widget.ID)
|
|
if err == nil && store.Fresh(cached, time.Now()) {
|
|
return cached, nil
|
|
}
|
|
|
|
tmpl, ok := GetTemplate(widget.Type)
|
|
if !ok {
|
|
return store.WidgetData{WidgetID: widget.ID, Status: "fresh", Data: json.RawMessage(`{}`)}, nil
|
|
}
|
|
if !tmpl.NeedsDataFetch {
|
|
if err == nil {
|
|
return cached, nil
|
|
}
|
|
return store.WidgetData{WidgetID: widget.ID, Status: "fresh", Data: json.RawMessage(`{}`)}, nil
|
|
}
|
|
return r.Refresh(ctx, widget)
|
|
}
|
|
|
|
func (r *Registry) Refresh(ctx context.Context, widget store.WidgetInstance) (store.WidgetData, error) {
|
|
now := time.Now()
|
|
|
|
tmpl, ok := GetTemplate(widget.Type)
|
|
if !ok || !tmpl.NeedsDataFetch {
|
|
data := store.WidgetData{
|
|
WidgetID: widget.ID,
|
|
Status: "fresh",
|
|
Data: json.RawMessage(`{}`),
|
|
FetchedAt: &now,
|
|
ExpiresAt: ptr(now.Add(r.cacheTTL)),
|
|
}
|
|
return data, r.store.SaveWidgetData(ctx, data)
|
|
}
|
|
|
|
payload, err := tmpl.Fetch(ctx, r.client, widget.Config)
|
|
if err != nil {
|
|
message := "[ERROR: " + err.Error() + "]"
|
|
if cached, cacheErr := r.store.WidgetData(ctx, widget.ID); cacheErr == nil && len(cached.Data) > 0 {
|
|
cached.Status = "stale"
|
|
cached.Error = &message
|
|
cached.ExpiresAt = ptr(now.Add(r.cacheTTL))
|
|
_ = r.store.SaveWidgetData(ctx, cached)
|
|
return cached, nil
|
|
}
|
|
data := store.WidgetData{
|
|
WidgetID: widget.ID,
|
|
Status: "error",
|
|
Error: &message,
|
|
FetchedAt: &now,
|
|
ExpiresAt: ptr(now.Add(r.cacheTTL)),
|
|
}
|
|
_ = r.store.SaveWidgetData(ctx, data)
|
|
return data, nil
|
|
}
|
|
|
|
data := store.WidgetData{
|
|
WidgetID: widget.ID,
|
|
Status: "fresh",
|
|
Data: payload,
|
|
FetchedAt: &now,
|
|
ExpiresAt: ptr(now.Add(r.cacheTTL)),
|
|
}
|
|
return data, r.store.SaveWidgetData(ctx, data)
|
|
}
|
|
|
|
func ptr[T any](value T) *T {
|
|
return &value
|
|
}
|