docs(research): add smart screen stack, additions, and Nextcloud research

Cover Symfony backend, calendar sync (Exchange Graph API + Mail-in-a-Box
CalDAV), Docker/Gitea CI, frontend stack, Diun/RSS/QR additions, and
Nextcloud Tasks (CalDAV VTODO) integration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 20:48:53 +02:00
co-authored by Claude-Bot
parent e78e411b76
commit 71d54608df
3 changed files with 345 additions and 0 deletions
@@ -0,0 +1,135 @@
# Smart Tablet Screen — Additional Features Research
Research for two additions to the Smart Tablet Screen kiosk dashboard: (1) surfacing Diun container-update notifications, and (2) a mini RSS reader widget with QR hand-off. Primary sources only; each claim is cited.
## Addition 1: Diun + notification service → smart screen
### What Diun is and how it stores state
Diun ("Docker Image Update Notifier") monitors container images and notifies when updates are available ([crazymax.dev/diun](https://crazymax.dev/diun/)).
Diun persists what it has seen in an embedded **BoltDB** key-value file (default path `diun.db`, configurable via `db.path` / env `DIUN_DB_PATH`) storing image manifests used to diff against the registry on each scan ([crazymax.dev/diun/config/db](https://crazymax.dev/diun/config/db/), [GitHub docs/config/db.md](https://github.com/crazy-max/diun/blob/master/docs/config/db.md)). This is a local embedded file, not a network-queryable store — nothing in the docs suggests a REST/query API over it, so a backend cannot poll Diun's own history over HTTP.
Diun optionally exposes a **Prometheus metrics endpoint** (`metrics.enabled`, default addr `:9090`, default path `/metrics`), added in v4.3.0 per the changelog and documented under config ([crazymax.dev/diun/config](https://crazymax.dev/diun/config/); confirmed via [GitHub issue #201](https://github.com/crazy-max/diun/issues/201) and the changelog). This gives counters/aggregates, not the per-event data (image name, tag, timestamp) needed for a "recent updates" widget — so metrics alone are not sufficient for the tablet UI.
**Conclusion: Diun is push-only for event-level data.** The only way to get individual update events out of Diun in real time is one of its notification providers firing on each detected update.
### Diun's supported notification providers
Diun ships a fixed set of built-in notifiers, each documented individually under `crazymax.dev/diun/notif/*`, including: Amqp, Discord, Elasticsearch, Gotify, Kafka, Mail, Matrix, MQTT, **Ntfy**, Pushover, RocketChat, Script, Signal, Slack, Teams, Telegram, and **Webhook**, plus a dedicated **Apprise** provider ([crazymax.dev/diun](https://crazymax.dev/diun/), provider list confirmed via site fetch; individual pages e.g. [ntfy](https://crazymax.dev/diun/notif/ntfy/), [apprise](https://crazymax.dev/diun/notif/apprise/), [webhook](https://crazymax.dev/diun/notif/webhook/)). Note Diun does **not** use Shoutrrr internally — it has its own notifier implementations, one of which happens to be Apprise (calling out to a separate Apprise API instance), and a separate one for ntfy directly.
### Generic Webhook provider (recommended integration point)
Config (`crazymax.dev/diun/notif/webhook/`):
```yaml
notif:
webhook:
endpoint: http://webhook.foo.com/sd54qad89azd5a # required
method: GET # required, e.g. POST
headers:
content-type: application/json
authorization: Token123456
timeout: 10s # default
proxy: ""
tlsSkipVerify: false
tlsCaCertFiles: []
```
Source: [crazymax.dev/diun/notif/webhook](https://crazymax.dev/diun/notif/webhook/).
JSON payload sent on each detected update:
```json
{
"diun_version": "string",
"hostname": "string",
"status": "string",
"provider": "string",
"image": "string",
"hub_link": "string",
"mime_type": "string",
"digest": "string",
"created": "ISO 8601 timestamp",
"platform": "string",
"metadata": {
"ctn_command": "string",
"ctn_createdat": "string",
"ctn_id": "string",
"ctn_names": "string",
"ctn_size": "string",
"ctn_state": "string",
"ctn_status": "string"
}
}
```
Source: [crazymax.dev/diun/notif/webhook](https://crazymax.dev/diun/notif/webhook/) (payload fields).
Diun's docs note title/body templates (`templateTitle`/`templateBody`) don't apply to notifiers that render structured output like Webhook or Apprise — the webhook always sends this full JSON body.
### ntfy as the paired "notify" service
If ntfy is the notify service in use, Diun has a **native ntfy provider** (not the generic webhook) with config:
```yaml
notif:
ntfy:
topic: mytopic # required
endpoint: https://ntfy.sh # default
token: ""
priority: 3
tags: ["package"]
icon: <diun logo>
click: "" # supports templates
timeout: 10s
```
Source: [crazymax.dev/diun/notif/ntfy](https://crazymax.dev/diun/notif/ntfy/).
ntfy itself supports three ways for a backend to receive/pull messages, per its own docs ([docs.ntfy.sh/publish](https://docs.ntfy.sh/publish/)):
- **Publish**: `POST/PUT https://ntfy.sh/<topic>` with body/headers (`Title`, `Priority`, `Tags`, `Click`, etc.) — this is what Diun's ntfy provider (or a webhook aimed at ntfy) uses.
- **Subscribe via SSE**: `GET https://ntfy.sh/<topic>/sse` — the Symfony backend could hold this open and append events to storage.
- **Subscribe via WebSocket**: `ws://ntfy.sh/<topic>/ws`.
- **Poll/pull history**: `GET https://ntfy.sh/<topic>/json?since=<timestamp>` returns cached messages since a Unix timestamp — this is the simplest fit for a periodic Symfony poll, no persistent connection required.
### Gotify as an alternative notify service
Diun has a native Gotify provider (`crazymax.dev/diun/notif/gotify/`, listed among providers on the main docs page). For pulling data back out, Gotify's own REST API (per its docs and Go source) exposes:
- `POST /message` to push (used by Diun) — [gotify.net/docs/pushmsg](https://gotify.net/docs/pushmsg).
- `GET /message` to list/paginate stored messages (`limit` query param), and `GET /stream` as a WebSocket for live delivery — both require a **client token** (not the app token used for pushing), per the Gotify server Swagger spec ([github.com/gotify/server api/stream](https://github.com/gotify/server/blob/master/api/stream/stream.go), confirmed via [gotify.net/api-docs](https://gotify.net/api-docs)).
So Gotify, like ntfy, supports both a pull (`GET /message`) and a push (WebSocket `/stream`) model for the Symfony backend to retrieve recent notifications.
### Apprise as an alternative notify service
Diun's Apprise provider talks to a separate **Apprise API** instance (not the CLI) — config requires `endpoint` (Apprise API host) plus either a `token` (config key on the Apprise server) or a literal `urls` list, with optional `tags` ([crazymax.dev/diun/notif/apprise](https://crazymax.dev/diun/notif/apprise/)). Apprise itself is fundamentally a fan-out sender to ~100 services; it is not designed as a message store the Symfony backend could poll for history — it must be paired with a receiving endpoint (e.g. its own webhook target or one of ntfy/Gotify downstream).
### Recommended integration (Addition 1)
Point Diun's generic **Webhook** notifier (or its native ntfy/Gotify provider, if that's the pre-existing "notify" service) at a small Symfony endpoint (e.g. `POST /api/diun-webhook`) that validates the JSON shape above and appends each event to a capped table/store (last N rows). The smart-screen frontend then simply polls that Symfony endpoint — this avoids depending on Diun's BoltDB file format (undocumented for external reads) and avoids holding an SSE/WebSocket connection to a third-party notify service just to relay events onward. If ntfy/Gotify is already the household's central hub, Symfony can instead poll their pull APIs (`ntfy /json?since=`, Gotify `GET /message`) directly and skip standing up a bespoke webhook receiver — pick whichever the user already has running to avoid a duplicate integration surface.
---
## Addition 2: Mini RSS reader widget with QR hand-off
### RSS/Atom parsing on the Symfony side
Symfony's own component catalog has no feed-reading component — none of the ~380 packages listed at [symfony.com/components](https://symfony.com/components) covers RSS/Atom parsing.
The RSS 2.0 spec ([rssboard.org/rss-specification](https://www.rssboard.org/rss-specification)) defines a simple structure: a `<channel>` with `title`/`link`/`description`, containing repeated `<item>` elements each with `title`, `link`, `description`, optional `pubDate` (RFC 822), and `guid` (a stable unique id, useful for de-duplication/read-state). At least one of `title` or `description` is required per item. Atom's structure (RFC 4287, not re-fetched here as it's a well-known IETF RFC) is analogous (`entry`, `id`, `updated`, `link`/`href`).
Given that shape, three viable approaches:
- **PHP `SimpleXMLElement` / `DOMDocument`** (stdlib, zero dependencies) — sufficient for a small, known set of user-supplied feed URLs since the RSS 2.0 element set is flat and well-specified; no need to handle arbitrary namespaces or the full Atom spec unless the user's feeds are Atom.
- **`laminas/laminas-feed`** — a maintained library "providing functionality for consuming RSS and Atom feeds," reads both formats behind one API and can also write feeds ([github.com/laminas/laminas-feed](https://github.com/laminas/laminas-feed)). No Symfony-specific bundle needed; it's a plain Composer library.
- **`simplepie/simplepie`** — a long-standing dedicated Atom/RSS parser ([github.com/simplepie/simplepie](https://github.com/simplepie/simplepie), [simplepie.org](https://simplepie.org/)) with its own caching layer; a Symfony2-era bundle exists (`FkrSimplePieBundle`) but is unmaintained/outdated, so plain SimplePie via Composer (no bundle) is the only sane path if chosen.
Recommendation: for a handful of user-supplied feed URLs of unknown-but-likely-RSS-2.0 shape, plain **`SimpleXMLElement`** parsing is the leanest option (stdlib, ladder rung 3) — reach for `laminas-feed` only if a feed turns out to need real Atom/RSS-variant normalization that hand-rolled XML parsing makes painful.
### QR code generation on the Symfony side
`endroid/qr-code` is the standard PHP QR generator, MIT-licensed ([github.com/endroid/qr-code](https://github.com/endroid/qr-code), license per [qr-code-bundle LICENSE](https://github.com/endroid/qr-code-bundle/blob/main/LICENSE) — same org/license for the core library). It is a plain Composer library (`composer require endroid/qr-code`); a separate `endroid/qr-code-bundle` exists for direct Twig/route integration in Symfony (auto-config, a `/qr-code/<data>` route, and Twig functions), also MIT ([endroid/qr-code-bundle README](https://github.com/endroid/qr-code-bundle/blob/main/README.md)).
Usage generates either PNG or SVG from an arbitrary string (e.g. an article URL) via a `Builder`/writer API, and the result exposes `getDataUri()` for inline `<img src="data:...">` embedding or `getString()` to stream through a controller response — confirmed against the library's own README (github.com/endroid/qr-code).
### Recommended integration (Addition 2)
Parse each user-configured feed URL server-side with `SimpleXMLElement` on a scheduled/cached fetch (Symfony's own Cache component, already in the framework, avoids re-fetching feeds every page load), store the handful of latest items (title, link, guid, pubDate), and render the widget from that cache. For hand-off, generate a QR code per displayed article's `link` with `endroid/qr-code` (`SvgWriter` for crisp on-screen rendering, `getDataUri()` inline — no need for a bundle or a dedicated image-serving endpoint for a single-purpose kiosk widget).
### Claude Code skill/MCP check
No local skill or MCP server in this session's listing is specific to RSS parsing or QR code generation — general-purpose coding skills (`ponytail`, `code-review`, etc.) apply generically but there is nothing feed- or QR-specific to invoke.