Files
Dash/docs/widgets.md
T
Tomas DvorakandDevin a89a81d46c docs+release: Phase 4 launch artifacts
README hero with real screenshots, install/config/widget docs, goreleaser
for dash + dash-agent, GHCR release workflow, changelog, community files,
agent systemd unit and Dockerfile, version stamping via -X main.version.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-21 19:39:21 +02:00

90 lines
2.7 KiB
Markdown

# Widget development
A widget is two pieces:
1. a Go **fetcher** in `apps/backend/internal/widget/` that declares config
fields and pulls live data, and
2. a React **view** in `apps/frontend/src/widgets/` that renders the payload
inside a tile.
Renderers own the tile chrome — widgets never touch layout, drag-drop, or
popovers.
## Backend fetcher
One file, registered in `init()`:
```go
package widget
type pihole struct{}
func init() { Register(pihole{}) }
func (pihole) Meta() Type {
return Type{
Type: "pihole",
Name: "Pi-hole",
Description: "Queries and block rate",
Fields: []Field{
{Key: "endpoint", Label: "Endpoint", Placeholder: "http://pi.hole", Required: true},
{Key: "token", Label: "API token", Secret: true},
},
}
}
func (pihole) Fetch(ctx context.Context, cfg json.RawMessage) (any, error) {
// cfg holds the item's config JSON. Never log it — it may hold keys.
}
```
- `Meta()` drives the add-widget dialog: `GET /api/widgets/types` returns
every registered type with its fields.
- `Fetch` is called by `GET /api/widgets/:id/data`; responses are cached ~30s
in-process. Return any JSON-marshalable value — it reaches the frontend as
`data`.
- Read config with the `cfgStr`/`cfgNum` helpers in the package.
- Fetchers needing the database (monitor, domain, system) live in
`internal/api` and register via `widget.Register` at router setup — the
`widget` package stays a leaf with no DB dependency.
## Frontend view
One component in `apps/frontend/src/widgets/`, one line in the registry at
`src/widgets/index.tsx`:
```tsx
export function PiholeWidget({ data, error }: { item: Item; data?: WidgetData; error?: string }) {
if (error) return <TileError msg={error} />;
if (!data) return <TileLoading />;
return <div>{data.blockedPercent}%</div>;
}
```
```ts
const registry: WidgetRegistry = {
pihole: { view: PiholeWidget },
};
```
- `useWidgetData` polls `/api/widgets/:id/data` every 30s. `data` is the
fetcher's payload, `error` is a human-readable upstream failure.
- Payloads are free-form; read fields defensively (values may be absent on
first paint).
- `local: true` marks a widget as frontend-only (e.g. clock) — no fetcher, no
polling.
## Config fields
`Fields` in `Meta()` render inputs in the add-widget dialog. `Secret: true`
masks the input. Config is stored in `items.config` JSON, sent back to the
fetcher on every poll, and never logged.
## Checklist
- [ ] `apps/backend/internal/widget/<name>.go` — fetcher + `init()` + `Meta()`
- [ ] `apps/frontend/src/widgets/<name>.tsx` — view component
- [ ] registry entry in `src/widgets/index.tsx`
- [ ] error and empty-data states render cleanly in the tile
- [ ] upstream timeouts bounded (respect `ctx`, keep HTTP client timeouts)