Today, all dashboard widgets are developed in the core of Gladys. An integration (especially an external one, via the SDK) cannot propose a widget specific to its domain, although this is often where the data makes the most sense: solar production tracking, status of a robot vacuum, charging schedule of an electric car, etc.
The proposal
Allow an integration to declare one or more widgets via a JSON schema, on the same principle as the configuration pages of external integrations. The integration describes what to display (values, graphs, buttons, states, gauges…), and it’s Gladys that decides how to display it.
Deliberately, no custom HTML or iframe: it’s a philosophical choice. By remaining declarative, the core of Gladys ensures for all widgets, including third-party ones: visual consistency, dark mode, mobile/tablet responsiveness, translations, performance, and non-regression when the interface evolves. It’s the same model as iOS widgets: purely declarative, and no one finds the ecosystem lacking in richness
The benefits
Integrations become complete: their data finally have a real place on the dashboard
The experience remains clean and unified, regardless of the widget’s author
The JSON being validatable, an AI will be able to generate or repair these widgets reliably (and eventually, build an entire dashboard on demand)
The vocabulary of components can be gradually enriched in the core, without ever breaking anything, and each addition benefits all integrations at once
Areas for reflection
Define the initial vocabulary: what basic components? (value + unit, historical graph, action button, list of states…)
How the widget retrieves its data: existing device features, or endpoint exposed by the integration?
Version the schema so that widgets remain compatible with updates
Development is ready to be tested, and I would love to get feedback from several integration developers to check that the API meets real use cases before release.
@spenceur Could you redo your movie integration as an external integration with this API and let me know if it meets your needs?
The schedule is quite tight, so I’m open to all your feedback as soon as possible. The goal is to publish this as early as Monday if everything goes well!
If it helps, here’s what claude says: Technical details:
The core limits widget images to 300 KB (MAX_WIDGET_IMAGE_BYTES = 300 * 1024 in externalIntegration.normalizeWidgetImage.js), an intentional limit to prevent a widget from loading a huge image in the browser.
I checked by retrieving the posters directly from the AlloCiné CDN (all.web.img.acsta.net): those that display correctly are 182-241 KB, the two broken ones are 341 KB and 378 KB — above the threshold.
When onWidgetGetImage returns an image that is too heavy, the core rejects the response on the server side and the front end simply displays the broken image icon, with a generic error message (REQUEST_TO_THIRD_PARTY_FAILED) that does not explicitly say « too heavy » — it took me a while to trace.
This is not a bug in the integration code: widget.js does exactly the same thing for all images, it’s just that the AlloCiné CDN sometimes serves posters heavier than Gladys’ limit for certain films. There is no resizing parameter in the CGR poster URL to request a lighter version, so nothing to correct on the integration side.
I tested this PR end-to-end with a fake integration provider that serves a tide widget speaking raw WebSocket protocol (no Docker nor SDK — SDK 0.13.0 does not yet expose widget handlers). The mechanism works: the widget appears in the picker, the settings are validated, the content is normalized, the action round-trip and the nudge widget.refresh behave as specified. The plumbing is solid.
I then tried a concrete case: the tide widget from #3028. The result is a useful data point, because the gap is not cosmetic.
First, the core widget of #3028; then, what I was able to do:
Three things are structurally impossible, and not just less pretty:
The tide clock — a dial with a hand indicating the position in the cycle and the time remaining before the stand. There is no dial component, and neither value, nor gauge, nor chart approximates it: a gauge is a radial arc for a ratio, not a dial with two labeled poles PM/BM.
The annotations on the curve — the markers for high and low tide with their time and height, the coefficient badges, the dotted reference of the present moment. Chart only takes point series, so the curve loses exactly the information for which it is read. You don’t look at a tide curve for its shape, but to read « 07:48, 10.69 m, coef. 74 ».
The day tabs (today + 6 days). I understand that this one is deliberate — « a widget is read and touched, it’s not a page » — but a tide without the next day loses its purpose: you consult a tide to prepare an outing, so in advance. A card-list of 7 items would make a good focal, but it would take up the space of the curve, which is the heart of the widget.
This does not call into question. The capability remains the right answer for the TMDB pilot case and for the domain widgets cited in the spec. My point is narrower: the dividing line « domain widgets vs generic control widgets » is not enough to classify a widget like the tide. It’s not a control widget, so the « out of scope » section does not exclude it; but its rendering relies on a domain-specific representation (a dial, an annotated curve) that the vocabulary cannot describe by construction. The spec says « the worst possible third-party widget looks like a slightly loaded core widget » — here, the opposite happens: the best widget that the vocabulary allows is significantly below what the core already knows how to do.
Two suggestions, in the additive spirit of principle 7:
A dial component (value, min, max, two pole labels, central label) would join gauge as the second radial component. It serves the tide, but also a wind rose, a position in a load cycle, a lunar phase…
Optional annotations on chart: a bounded array of { t, label, value, color } that the core renders as markers, plus a now_marker boolean. Additive, ignored by an older core, and useful well beyond the tide (solar production peaks, tariff slots, expected end of charge).
Edit: I wanted to set the chart to 30 days, but Claude gave me this feedback:
Over 30 days, to be clear: the title remains “Last 30 days” and the axis extends to the left as readings are taken until it covers a month. I can’t display a 30-day axis from the first day without making up prices we’ve never measured — the front end doesn’t allow forcing a min/max axis, and fabricating points would be false data on a price chart. If you still prefer a fixed 30-day axis, the only honest way would be a patch on the core side (exposing xaxis.min/max in the chart component)
For the 30 days that are not « filled in », I’m not shocked, and it’s already what happens with other sensors when you only have a few days of data and you display them over 30 days. The graph only shows what it has and starts on the left at the first date of the data in the database.