Files
Smart-Tablet-Screen/docs/research/smart-tablet-screen-additions.md
haylanandClaude-Bot 71d54608df 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>
2026-09-01 20:48:53 +02:00

11 KiB

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).

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, GitHub 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; confirmed via GitHub issue #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, provider list confirmed via site fetch; individual pages e.g. ntfy, apprise, 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.

Config (crazymax.dev/diun/notif/webhook/):

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.

JSON payload sent on each detected update:

{
  "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 (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:

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.

ntfy itself supports three ways for a backend to receive/pull messages, per its own docs (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:

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). 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).

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 covers RSS/Atom parsing.

The RSS 2.0 spec (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). No Symfony-specific bundle needed; it's a plain Composer library.
  • simplepie/simplepie — a long-standing dedicated Atom/RSS parser (github.com/simplepie/simplepie, 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, license per qr-code-bundle 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).

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).

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.