Files
Dash/apps/backend/internal/widget/adguard.go
T
Tomas Dvorak 8afc7df53b 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>
2026-09-17 20:29:22 +02:00

76 lines
2.2 KiB
Go

package widget
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// AdGuard Home REST API: GET /control/stats with HTTP basic auth.
type adguard struct{}
func init() { register(adguard{}) }
func (adguard) Meta() Type {
return Type{
Type: "adguard",
Name: "AdGuard Home",
Description: "Queries and block rate",
Fields: []Field{
{Key: "endpoint", Label: "Endpoint", Placeholder: "http://adguard.local", Required: true},
{Key: "user", Label: "Username", Required: true},
{Key: "pass", Label: "Password", Secret: true, Required: true},
},
}
}
type adguardData struct {
QueriesToday int `json:"queriesToday"`
BlockedToday int `json:"blockedToday"`
BlockedPercent float64 `json:"blockedPercent"`
AvgProcessMs float64 `json:"avgProcessMs"`
}
func (adguard) Fetch(ctx context.Context, cfg json.RawMessage) (any, error) {
endpoint := strings.TrimRight(cfgStr(cfg, "endpoint"), "/")
user, pass := cfgStr(cfg, "user"), cfgStr(cfg, "pass")
if endpoint == "" || user == "" {
return nil, fmt.Errorf("endpoint and user required")
}
var raw struct {
NumDNSQueries int `json:"num_dns_queries"`
NumBlockedFiltering int `json:"num_blocked_filtering"`
AvgProcessingTimeSec float64 `json:"avg_processing_time_sec"`
}
// AdGuard uses basic auth; encode via the URL userinfo so the helper
// stays header-free — and the secret never touches a log line.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"/control/stats", nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(user, pass)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("upstream unreachable")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("upstream returned %d", resp.StatusCode)
}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, err
}
var pct float64
if raw.NumDNSQueries > 0 {
pct = float64(raw.NumBlockedFiltering) / float64(raw.NumDNSQueries) * 100
}
return adguardData{
QueriesToday: raw.NumDNSQueries,
BlockedToday: raw.NumBlockedFiltering,
BlockedPercent: pct,
AvgProcessMs: raw.AvgProcessingTimeSec * 1000,
}, nil
}