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

+93
View File
@@ -214,6 +214,44 @@ paths:
"404":
$ref: "#/components/responses/NotFound"
/api/widgets/types:
get:
operationId: widgetTypes
summary: Widget registry metadata — drives the add-widget dialog
responses:
"200":
description: Registered widget types
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WidgetType"
/api/widgets/{id}/data:
get:
operationId: widgetData
summary: Live data for one widget item (~30s server-side cache)
parameters:
- $ref: "#/components/parameters/id"
responses:
"200":
description: Widget payload; shape depends on config.type
content:
application/json:
schema:
$ref: "#/components/schemas/WidgetData"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"502":
description: Upstream service unreachable
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/api/status:
get:
operationId: getStatus
@@ -386,10 +424,19 @@ components:
properties:
sectionId:
type: string
kind:
type: string
enum: [service, widget]
default: service
name:
type: string
icon:
type: string
config:
type: object
nullable: true
additionalProperties: true
description: Widget config — keys defined by the widget type's fields
urls:
type: array
items:
@@ -401,6 +448,10 @@ components:
type: string
icon:
type: string
config:
type: object
nullable: true
additionalProperties: true
urls:
type: array
description: Full replacement of the item's URL list
@@ -467,6 +518,48 @@ components:
checkedAt:
type: string
format: date-time
WidgetField:
type: object
required: [key, label]
properties:
key:
type: string
label:
type: string
secret:
type: boolean
description: Render as password input; never logged
placeholder:
type: string
required:
type: boolean
WidgetType:
type: object
required: [type, name, fields]
properties:
type:
type: string
name:
type: string
description:
type: string
local:
type: boolean
description: Client-rendered (e.g. clock); no /data fetch needed
fields:
type: array
items:
$ref: "#/components/schemas/WidgetField"
WidgetData:
type: object
required: [data]
properties:
data:
type: object
additionalProperties: true
fetchedAt:
type: string
format: date-time
Export:
type: object
required: [version, sections]