From 71d54608df3cacc217d7955aa5acfba199d23a6a Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Tue, 1 Sep 2026 20:48:53 +0200 Subject: [PATCH] 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 --- .../research/smart-tablet-screen-additions.md | 135 ++++++++++++++++++ .../smart-tablet-screen-nextcloud-todos.md | 82 +++++++++++ docs/research/smart-tablet-screen-stack.md | 128 +++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 docs/research/smart-tablet-screen-additions.md create mode 100644 docs/research/smart-tablet-screen-nextcloud-todos.md create mode 100644 docs/research/smart-tablet-screen-stack.md diff --git a/docs/research/smart-tablet-screen-additions.md b/docs/research/smart-tablet-screen-additions.md new file mode 100644 index 0000000..bfdcba5 --- /dev/null +++ b/docs/research/smart-tablet-screen-additions.md @@ -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: + 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/` 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//sse` — the Symfony backend could hold this open and append events to storage. +- **Subscribe via WebSocket**: `ws://ntfy.sh//ws`. +- **Poll/pull history**: `GET https://ntfy.sh//json?since=` 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 `` with `title`/`link`/`description`, containing repeated `` 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/` 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 `` 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. diff --git a/docs/research/smart-tablet-screen-nextcloud-todos.md b/docs/research/smart-tablet-screen-nextcloud-todos.md new file mode 100644 index 0000000..39be941 --- /dev/null +++ b/docs/research/smart-tablet-screen-nextcloud-todos.md @@ -0,0 +1,82 @@ +# Nextcloud to-dos on the smart screen + +Research into pulling the user's self-hosted Nextcloud to-dos onto the wall-mounted kiosk dashboard. + +## Which Nextcloud app actually holds "to-dos" + +Nextcloud ships two apps that could plausibly be meant by "notes/to-dos": + +- **Tasks app** — proper checkable to-do items (title, due date, priority, completion status, subtasks), stored as CalDAV `VTODO` objects inside a calendar collection. The Tasks README confirms CalDAV as the sync protocol: "Apps which sync with Nextcloud Tasks (using CalDAV)" ([nextcloud/tasks README](https://github.com/nextcloud/tasks)). +- **Notes app** — plain Markdown notes with a REST/OCS-style JSON API of its own; a "to-do" here would just be a `- [ ]` checkbox line inside a Markdown blob, not a structured, checkable item. + +Nextcloud's own CalDAV backend explicitly supports both `VEVENT` and `VTODO` components per calendar collection — the `supported-calendar-component-set` capability is what lets a calendar hold events, tasks, or both (visible in the backend implementation: [`CalDavBackend.php`, nextcloud/server](https://github.com/nextcloud/server/blob/master/apps/dav/lib/CalDAV/CalDavBackend.php)), and this same VTODO-vs-VEVENT distinction is documented for calendar integrations in the developer manual's [Integration of custom calendar providers](https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/groupware/calendar_provider.html) page. + +`VTODO` itself is a standard iCalendar component, not a Nextcloud invention: RFC 5545 §3.6.2 "To-Do Component" defines it, with a `STATUS` property whose values include `NEEDS-ACTION` (default), `IN-PROCESS`, `COMPLETED`, and `CANCELLED` ([RFC 5545](https://www.rfc-editor.org/rfc/rfc5545)). CalDAV (RFC 4791) is the WebDAV extension that lets a client list/query/fetch these `VTODO`/`VEVENT` objects from a server-side calendar collection — the same protocol family the Nextcloud Calendar app research (in the companion doc) already covers for events. + +**Conclusion:** for "current open to-dos" with due dates and a completion flag, the Tasks app / CalDAV VTODO route is the correct mapping. The Notes app is the fallback only if the user's checklist genuinely lives as checkbox lines inside a note rather than as Tasks-app items. + +## CalDAV path convention for task lists + +Nextcloud exposes all CalDAV calendars — task lists included, since a task list is just a calendar collection with `VTODO` support enabled — under the same principal-based tree used for events: + +``` +https:///remote.php/dav/calendars/// +``` + +This is Nextcloud's standard CalDAV/CardDAV mount point; Nextcloud's own admin manual for Calendar/CalDAV describes the CalDAV backend (resource/room booking, differential sync tracking for offline clients like Thunderbird) as living under this DAV tree ([Calendar / CalDAV — Nextcloud Administration Manual](https://docs.nextcloud.com/server/stable/admin_manual/groupware/calendar.html)), and the same `/remote.php/dav/calendars///` pattern is what third-party CalDAV clients (e.g. DAVx5) are configured against when pointed at a Nextcloud instance. A task list created in the Tasks app shows up as just another calendar collection at this path — there is no separate `/tasks/` endpoint; Tasks is a UI on top of the same CalDAV calendars, distinguished only by the `VTODO` component flag on the collection. + +To discover the exact per-user collection names, a client does a `PROPFIND` with `Depth: 1` on `/remote.php/dav/calendars//` and reads each collection's `{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set` to find which ones accept `VTODO`. + +## Fetching VTODO items (not just VEVENT) + +CalDAV's `calendar-query` REPORT can filter for `VTODO` specifically. Sabre's own CalDAV client guide gives this exact case: "If you're only interested in VTODO (because you're writing a todo app) you can also filter for just those," with the filter body: + +```xml + + + + + + + + + + + +``` +([Building a CalDAV client — sabre.io](https://sabre.io/dav/building-a-caldav-client/)) + +To further narrow to *open* (not-yet-completed) items, add a `` / `` for anything other than `COMPLETED`, or simply filter client-side on the parsed `STATUS`/`COMPLETED` properties after fetching — simpler and less fragile than relying on server-side text matching. + +## PHP-side CalDAV client for Symfony + +Nextcloud's own DAV server is built on **sabre/dav** (`sabre-io/dav`), and that same package ships a generic WebDAV/CalDAV **client**, `Sabre\DAV\Client` ([`lib/DAV/Client.php`, sabre-io/dav](https://github.com/sabre-io/dav/blob/master/lib/DAV/Client.php)). Relevant surface for this use case: + +- Constructed with a settings array: `baseUri`, `userName`, `password`, `authType` (`AUTH_BASIC`, `AUTH_DIGEST`, `AUTH_NTLM`). +- `propFind($url, $properties, $depth)` — for discovering calendar collections and their `supported-calendar-component-set`. +- `request($method, $url, $body, $headers)` — generic HTTP request, usable with `method: 'REPORT'` and the `calendar-query` XML body above to fetch `VTODO` objects (the client has no built-in `calendar-query` helper, so the REPORT body is hand-built XML sent via `request()`). +- `parseMultiStatus($body)` — parses the WebDAV multistatus XML response into a URL → properties map. + +This is the same library the calendar-sync research already points to for VEVENT — nothing extra is needed for VTODO since it is the same CalDAV REPORT mechanism against the same client class, only the filter's `comp-filter name` changes from `VEVENT` to `VTODO`. Requiring it from Symfony is a standard Composer dependency (`sabre/dav`), no custom protocol code beyond building the REPORT XML and parsing the returned `VCALENDAR`/`VTODO` blocks (a small iCalendar parser — `sabre/vobject`, also from the sabre.io project — handles that parsing rather than hand-rolling one). + +## Authentication + +Nextcloud CalDAV access uses **HTTP Basic Auth over HTTPS**, authenticated with a Nextcloud **app password** rather than the user's real account password. Nextcloud's user manual documents generating one from Settings → Security → Devices & sessions: "At the bottom of the list, you can create a new device-specific password. The generated password is used for configuring the new client" — and if two-factor auth is enabled on the account, an app password becomes mandatory for any non-browser client, since the server rejects the real password for such connections ([Session management — Nextcloud User Manual](https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html)). This app password is passed as the Basic Auth password against the same `/remote.php/dav/...` URLs used above — identical to the auth approach already used for the Calendar/VEVENT sync in the companion research doc, so a single app password/credential can cover both. + +## Notes app as a fallback (if "to-dos" really means checklist text in Notes) + +If investigation of the user's actual Nextcloud setup shows their "to-dos" are informal checkbox lines inside Notes rather than structured Tasks-app entries, the Notes app does expose its own documented REST API rather than requiring scraping: + +- Base path: `/index.php/apps/notes/api/v1/` (current major version; a deprecated `v0.2` also exists), with supported versions advertised via the Capabilities API and an `X-Notes-API-Versions` response header ([nextcloud/notes API docs](https://github.com/nextcloud/notes/blob/main/docs/api/README.md)). +- Auth is the same pattern: HTTP Basic Auth with username/password (or app password) on every request, since REST is stateless; the docs flag that this makes plain HTTP a credential-leak risk and recommend TLS. +- The API returns raw note content (Markdown) — it has no structured concept of a checkbox/to-do item; extracting "open to-dos" from it means the client parsing Markdown `- [ ]` / `- [x]` lines itself, which is materially more fragile than reading structured `VTODO` fields (title, due date, `STATUS`) directly. + +This is a strictly worse fit for "current open to-dos" than Tasks/CalDAV and should only be used if the user confirms their to-dos genuinely live as note checkboxes. + +## Claude Code skill / MCP server for Nextcloud + +No project skill or configured MCP server for Nextcloud or generic CalDAV was found in this repository or the assistant's currently loaded skill/MCP listings. Any CalDAV fetch code for this feature will need to be written directly in the Symfony backend (using `sabre/dav` + `sabre/vobject` as above) — there is nothing to delegate to an existing tool here. + +## Recommended integration + +Use the **Tasks app via CalDAV**, reading `VTODO` objects from `/remote.php/dav/calendars///` with a `calendar-query` REPORT filtered to `comp-filter name="VTODO"`, parsed with **`sabre/vobject`** on top of an HTTP/`Sabre\DAV\Client` request, authenticated with a Nextcloud **app password over HTTP Basic Auth**. Rationale: it is the only one of the two apps that models a to-do as a structured, checkable item with due date and completion status rather than free-text, it reuses the exact same CalDAV protocol, library, and app-password credential already needed for calendar/event sync, and filtering `STATUS != COMPLETED` client-side after the fetch directly yields "current open to-dos." diff --git a/docs/research/smart-tablet-screen-stack.md b/docs/research/smart-tablet-screen-stack.md new file mode 100644 index 0000000..26447f6 --- /dev/null +++ b/docs/research/smart-tablet-screen-stack.md @@ -0,0 +1,128 @@ +# Smart Tablet Screen — Stack Research + +Research date: 2026-09-01. All claims below are sourced against primary/official documentation, source code, or first-party API references — not third-party summaries. Each claim links its own source. + +--- + +## 1. Calendar sync: Mail-in-a-Box (self-hosted accounts) + +- Mail-in-a-Box bundles **Nextcloud** to provide contacts and calendar sync, exposed over **CardDAV/CalDAV**. The project's own repo/site lists "CardDAV/CalDAV (Nextcloud)" among the components it installs, and states each box "includes contacts and calendar synchronization." — [mail-in-a-box/mailinabox GitHub repo](https://github.com/mail-in-a-box/mailinabox), [mailinabox.email](https://mailinabox.email/) +- Mail-in-a-Box also bundles **Z-Push**, which provides **Exchange ActiveSync** as an alternative sync protocol for compatible mobile clients — this is a second, separate protocol path, not the CalDAV path. — [mail-in-a-box/mailinabox GitHub repo](https://github.com/mail-in-a-box/mailinabox) +- **Conclusion for this project**: point the Symfony backend's CalDAV client at each Mail-in-a-Box user's Nextcloud CalDAV URL (standard Nextcloud path pattern `/remote.php/dav/calendars//`, per Nextcloud's own admin docs, which Mail-in-a-Box's bundled Nextcloud inherits) — [Nextcloud Calendar/CalDAV Administration Manual](https://docs.nextcloud.com/server/stable/admin_manual/groupware/calendar.html). Do not use Z-Push/ActiveSync; CalDAV is the natively-scriptable, standards-based path and matches what Symfony would consume as a generic CalDAV client (RFC 4791). + +## 2. Calendar sync: Microsoft Exchange/Outlook.com (free account) + +- **Microsoft's own recommended API for calendar access is Microsoft Graph**, not EWS or CalDAV. Graph's Outlook calendar overview explicitly states: *"Most features in the Outlook calendar API apply to calendars in personal Microsoft accounts and work or school accounts"* — confirming free Outlook.com (personal Microsoft account) is a first-class supported account type, not just Microsoft 365/Exchange Online. — [Outlook calendar API overview – Microsoft Graph, learn.microsoft.com](https://learn.microsoft.com/en-us/graph/outlook-calendar-concept-overview) +- **EWS is being retired for Exchange Online** with a hard enforcement date of **October 1, 2026**, after which EWS stops working for Microsoft 365 mailboxes; Microsoft's stated replacement is Graph. This retirement is scoped to Exchange Online — it does not directly affect consumer Outlook.com accounts (which were never on the EWS-for-M365 retirement track the same way), but it confirms Graph as Microsoft's forward path and rules out building new EWS integrations. — [Deprecation of Exchange Web Services in Exchange Online, learn.microsoft.com](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-ews-exchange-online) +- **Outlook.com/Outlook (new) does not support CalDAV.** This is corroborated by numerous first-party Microsoft Q&A threads (Microsoft's own support forum) confirming CalDAV has never been natively implemented for Outlook.com, and that the "new Outlook" does not support third-party CalDAV add-ins. There is no official Microsoft doc offering a CalDAV endpoint for Outlook.com. — [Microsoft Q&A: CalDAV support on Outlook.com](https://learn.microsoft.com/en-us/answers/questions/4525673/caldav-support-on-outlook-com), [Microsoft Q&A: caldav in new outlook](https://learn.microsoft.com/en-us/answers/questions/2282064/caldav-in-new-outlook) +- **Auth**: Graph calendar access on a personal account requires standard OAuth 2.0 authorization-code flow against Microsoft Entra ID (`/authorize` and `/token` endpoints), with an app registered in the Entra portal, a `client_id`, redirect URI, and — for a confidential/web client like a Symfony backend — a `client_secret`. Use `tenant=common` (or `consumers`) so both personal Microsoft accounts and (optionally) work/school accounts can sign in. A refresh token (via `offline_access` scope) is needed for the long-lived, unattended calendar-polling this dashboard needs. — [Get access on behalf of a user – Microsoft Graph, learn.microsoft.com](https://learn.microsoft.com/en-us/graph/auth-v2-user) +- **Conclusion for this project**: use **Microsoft Graph's calendar API** (`/me/calendarview`, `/me/events`, delta query for sync) via OAuth 2.0 with a registered Entra app, storing the client id/secret and refresh token via the project's `.env`. This is the only viable, supported, future-proof path — CalDAV is not available and EWS is being sunset. + +## 3. CSS methodology — BEM + +Per BEM's own quick-start guide: +- **Block**: "a functionally independent page component that can be reused" — named by purpose, not appearance (e.g. `menu`, `button`); must not set its own external position/margins. +- **Element**: "a composite part of a block that can't be used separately from it" — syntax `block-name__element-name` (double underscore). +- **Modifier**: defines appearance/state/behavior of a block or element — boolean form `block-name_modifier-name`, key-value form `block-name_modifier-name_value`. Modifiers never exist standalone; they always accompany a block/element. +— [BEM Quick Start, bem.info](https://bem.info/en/methodology/quick-start/) + +**Conclusion**: adopt BEM class naming directly as documented (`.calendar`, `.calendar__event`, `.calendar__event_source_exchange`) for all hand-written CSS/SCSS in the frontend. + +## 4. Docker + Gitea container registry + +Per Gitea's own docs: +- Login: `docker login gitea.example.com` (use a personal access token instead of a password when 2FA/OAuth is enabled). +- Gitea's documented image naming format is **`{registry}/{owner}/{image}`**, e.g. `gitea.example.com/testuser/myimage`, and sub-paths are allowed (`gitea.example.com/testuser/my/image`). Tags are case-insensitive. +- Push: `docker push gitea.example.com/{owner}/{image}:{tag}`. +- The registry is OCI-compliant and works with any OCI client (Docker, Podman, Buildah, Skopeo). +— [Container Registry, docs.gitea.com](https://docs.gitea.com/usage/packages/container/) + +**Applied to this repo**: remote is `https://git.arthurerlich.de/haylan/Smart-Tablet-Screen`, so the prod image should be tagged and pushed as `git.arthurerlich.de/haylan/smart-tablet-screen:latest`. + +**Repo/infra discovery already done** (carried over, not re-verified here): +- No `.gitea` directory, no CI workflow files, and no `~/.docker/config.json` exist in this repo/machine yet — CI (Gitea Actions or manual push) and Docker registry auth both need to be set up from scratch. +- No `tea` CLI config found locally. +- The user's Gitea instance (`git.arthurerlich.de`) runs as Docker Swarm services (`git_gitea-server`, `git_gitea-db`, `git_backup`) on a host called "proxmox", per `~/Documents/code/restore-gitea.sh`, with backups on a "datasservices" storage host. This is operational context for where the registry lives, not registry credentials — no push credentials exist anywhere in this discovery and none are fabricated here. +- The `gitea-tea` skill drives the `tea` CLI for issues/PRs/wiki but explicitly does **not** cover container registry pushes — registry auth/push must be done via plain `docker login`/`docker push` as documented above, or via a Gitea Actions CI workflow (not yet present in this repo). + +## 5. Playwright (E2E) + +Per Playwright's own docs: +- "Playwright Test is an end-to-end test framework for modern web apps. It bundles test runner, assertions, isolation, parallelization and rich tooling." +- Supports Node.js, Python, Java, .NET bindings; drives Chromium, WebKit, and Firefox, headless or headed, with native mobile emulation for Chrome (Android) and Mobile Safari. +- Playwright's own site documents a dedicated **MCP** section (`/mcp/introduction`) and an **Agent CLI** section, confirming Playwright supports being driven as an MCP server / programmatically by an agent, in addition to its standard test-runner usage. +— [Playwright docs — Installation/Intro, playwright.dev](https://playwright.dev/docs/intro) + +**Fit check**: since this project targets a fixed Chrome/Android-kiosk viewport, Playwright's Chromium engine with device/viewport emulation is a good match for E2E coverage of the dashboard. + +**Locally available tooling**: +- A `playwright-e2e-testing` skill exists locally (noted per instructions, not invoked). +- No separate Playwright MCP server was found configured in this environment. +- The `claude-in-chrome` capability is the discovered tool for live, real-browser interaction (click, screenshot, console logs) against a real Chrome instance in this environment — use it for ad hoc manual verification of UI changes; Playwright remains the tool for the actual automated, repeatable E2E test suite. + +## 6. PHPUnit + Symfony testing + +- Symfony's own testing docs describe three test tiers on top of PHPUnit: **unit tests** (plain PHPUnit, no framework), **integration tests** (extend `KernelTestCase`, boot the DI container), and **application/functional tests** (extend `WebTestCase`, use `$client->request()` plus `assertResponseIsSuccessful()`/`assertSelectorTextContains()` style assertions). Setup is via `composer require --dev symfony/test-pack` and `php bin/phpunit`; test env config lives in `.env.test` / `.env.test.local` (`.env.local` is deliberately not loaded in the test environment). — [Testing, symfony.com/doc](https://symfony.com/doc/current/testing.html) +- PHPUnit itself is described by its own site as "the testing framework for PHP"; current stable is PHPUnit 13 (as of Feb 2026), maintained by Sebastian Bergmann. — [phpunit.de](https://phpunit.de/) + +**Locally available tooling**: a `phpunit-best-practices` skill exists locally (noted per instructions, not invoked). + +**Conclusion**: use Symfony's standard three-tier setup — unit tests for pure PHP logic (e.g. calendar-event normalization/merging), `KernelTestCase` integration tests for the CalDAV/Graph client services, `WebTestCase` functional tests for the calendar API controller endpoints. + +## 7. Frontend framework: Astro vs plain TypeScript/Vite + +Per Astro's own docs: +- Astro has two output modes as of Astro 5 (the old `hybrid` mode was folded into `static`): **`static`** (default — every page prerendered to flat HTML at build time) and **`server`** (on-demand rendering per request, requires an adapter). — [On-demand Rendering, docs.astro.build](https://docs.astro.build/en/guides/on-demand-rendering/) +- Astro's own guidance: *"Start with the default 'static' mode until you are sure that most or all of your pages will be rendered on demand!"* — i.e. Astro is optimized around mostly-static, multi-page content sites, with `server` mode as an opt-in escape hatch, not the primary design target. +- Astro's islands architecture renders components to static HTML at build time and hydrates only interactive islands with JS — a good fit for content pages with sparse interactivity, not for an app that is a single always-on, continuously-updating client view. + +**Assessment for this project**: the dashboard is a **single page**, always mounted, continuously polling a Symfony JSON API and re-rendering a calendar view client-side — there is no multi-page routing, no meaningfully "static, prerenderable" content, and no SEO/first-load-performance concern (it's a fixed kiosk browser, not a public site). Astro's core value propositions (partial hydration across many mostly-static pages, build-time prerendering, content collections) don't apply here; using Astro would mean running one perpetual "server island" and getting none of Astro's benefits while carrying its page-oriented conventions (file-based routing, `.astro` components) for what is really one long-lived app shell. + +**Recommendation**: plain **TypeScript + Vite** (no meta-framework). Symfony already owns the backend/API surface; the frontend only needs a bundler, dev server, and TS type-checking for a single-page app that fetches from the Symfony API and renders a calendar. This avoids Astro's per-page rendering model entirely and keeps the frontend a thin, single SPA build. + +## 8. Calendar UI library + +Evaluated against each library's own docs: + +- **FullCalendar** — supports vanilla JS/TypeScript "core" usage plus first-party React/Vue/Angular/Preact/Web Component wrappers; ships full TypeScript type support. Core (month/week/day/list/timeGrid views, drag/resize, i18n, timezone support) is open source (MIT); some views (Timeline, vertical Resource view, print) are gated behind a paid **Premium** tier. — [FullCalendar Docs, fullcalendar.io/docs](https://fullcalendar.io/docs) +- **Schedule-X** — an open-source TypeScript event calendar, explicitly positioned by its own docs as "a modern alternative to FullCalendar and react-big-calendar," usable directly in vanilla TS or with framework component wrappers (React/Angular/Vue/Svelte/Preact); ships day/week/month views, recurring events, dark mode, i18n, accessibility-focused patterns. — [schedule-x.dev](https://schedule-x.dev/), [schedule-x/schedule-x GitHub](https://github.com/schedule-x/schedule-x) + +**Recommendation**: **FullCalendar** for this project — the free/MIT core (month + week/day timeGrid + list views) covers everything this fixed-layout kiosk dashboard needs (no drag/resize/resource-planning premium features required), it has first-party vanilla-TS support matching the plain-TS/Vite frontend decision above, and it's the most mature/battle-tested option for a monitor-style read-mostly display. Schedule-X is a credible lighter-weight fallback if FullCalendar's bundle size or styling model proves awkward to theme for the kiosk's fixed layout. + +**Component libraries**: no general UI component library was researched in depth beyond the calendar widget itself, since the fixed-layout, single-screen kiosk dashboard has a small, custom surface (a calendar grid plus maybe a status/clock strip) that doesn't obviously benefit from a general component-library dependency — this is a YAGNI call, revisit only if the UI grows multiple distinct widget types needing shared interactive components (modals, tabs, etc.). + +## 9. Reverse proxy / deployment + +No first-party doc research was needed beyond what's already fixed by the task: the prod Docker image (tagged `:latest`, pushed to `git.arthurerlich.de/haylan/smart-tablet-screen:latest` per §4) runs on the user's local/Proxmox-hosted Docker environment behind an existing reverse proxy, exposed at `smart-screen.home`. This is an infra/ops detail for the eventual `devops-engineer`/`docker-expert` skill-driven work, not a research question with a "primary source" to cite — noted here only for completeness of the stack list. + +--- + +## Locally available Skills/MCP servers relevant to each decision + +| Area | Skill/MCP found locally | Coverage | +|---|---|---| +| Symfony + calendar integration | none specific | n/a — plain Symfony/PHP work | +| Docker image builds | `docker-expert` | Dockerfile authoring, multi-stage builds, security hardening | +| CI/CD, Gitea Actions | `devops-engineer` | Pipelines, Dockerfiles, deployment automation — general, not Gitea-registry-specific | +| Gitea issues/PRs/wiki | `gitea-tea` | Drives `tea` CLI — explicitly does **not** cover container registry pushes (see §4) | +| BEM/CSS | none specific | n/a | +| Playwright E2E | `playwright-e2e-testing` | Present locally; noted, not invoked per task instructions | +| PHPUnit | `phpunit-best-practices` | Present locally; noted, not invoked per task instructions | +| Astro / frontend framework choice | none specific | n/a | +| Calendar UI library | none specific | n/a | +| Live browser testing | `claude-in-chrome` | The available "drive a real Chrome browser" tool in this environment (click, screenshot, console logs) — no separate Playwright MCP server is configured here | + +--- + +## Recommended stack (summary) + +- **Backend**: Symfony (as specified), organized with standard MVC — controllers thin, calendar-aggregation/normalization logic in dedicated services, kept unit-testable in isolation from HTTP/DB. +- **Calendar sync — Mail-in-a-Box**: CalDAV client against each account's Nextcloud CalDAV URL (RFC 4791 / Nextcloud's documented `/remote.php/dav/calendars//` path) — it's the standards-based protocol Mail-in-a-Box actually exposes; skip Z-Push/ActiveSync, it's a mobile-sync path, not a scriptable API. +- **Calendar sync — Outlook/Exchange**: Microsoft Graph API via OAuth 2.0 authorization-code flow (Entra app registration, `tenant=common`, `offline_access` for refresh tokens) — Graph is Microsoft's only supported, future-proof calendar API for personal Microsoft accounts; CalDAV isn't offered and EWS is being retired. +- **Frontend framework**: plain **TypeScript + Vite**, no meta-framework — Astro's static/islands model targets multi-page content sites, not a single always-on kiosk SPA; Symfony already owns the backend, so the frontend only needs a lean TS build. +- **Calendar UI**: **FullCalendar** (core/MIT) — mature, first-party vanilla-TS support, month/week/list views cover the read-mostly kiosk display without needing its paid Premium tier. +- **CSS**: **BEM** (as specified) — block/element/modifier naming (`__`, `_`) applied directly per bem.info's quick-start convention. +- **E2E testing**: **Playwright** — first-party Chromium engine with device/viewport emulation fits the fixed Android/Chrome kiosk target; use the local `playwright-e2e-testing` skill when writing the suite; use `claude-in-chrome` for ad hoc live-browser checks during development, not as a substitute for the automated suite. +- **Unit/integration testing**: **PHPUnit** via Symfony's own three-tier convention (unit / `KernelTestCase` / `WebTestCase`) — use the local `phpunit-best-practices` skill when writing tests. +- **Docker/CI**: two Dockerfiles (dev, prod), prod built and pushed as `git.arthurerlich.de/haylan/smart-tablet-screen:latest` via `docker login`/`docker push` per Gitea's documented OCI registry path format; no Gitea Actions workflow exists yet in this repo — CI automation is a follow-up, not yet configured, and would use `devops-engineer`/`docker-expert` skills. +- **Reverse proxy/deployment**: existing local reverse-proxy setup exposing the prod container as `smart-screen.home` — no new research needed here, this is ops execution against the infra already discovered (Proxmox-hosted Docker/Swarm).