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>
18 KiB
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, 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
- 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/<username>/, per Nextcloud's own admin docs, which Mail-in-a-Box's bundled Nextcloud inherits) — Nextcloud Calendar/CalDAV Administration Manual. 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
- 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
- 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, Microsoft Q&A: caldav in new outlook
- Auth: Graph calendar access on a personal account requires standard OAuth 2.0 authorization-code flow against Microsoft Entra ID (
/authorizeand/tokenendpoints), with an app registered in the Entra portal, aclient_id, redirect URI, and — for a confidential/web client like a Symfony backend — aclient_secret. Usetenant=common(orconsumers) so both personal Microsoft accounts and (optionally) work/school accounts can sign in. A refresh token (viaoffline_accessscope) is needed for the long-lived, unattended calendar-polling this dashboard needs. — Get access on behalf of a user – Microsoft Graph, learn.microsoft.com - 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 formblock-name_modifier-name_value. Modifiers never exist standalone; they always accompany a block/element. — BEM Quick Start, bem.info
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
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
.giteadirectory, no CI workflow files, and no~/.docker/config.jsonexist in this repo/machine yet — CI (Gitea Actions or manual push) and Docker registry auth both need to be set up from scratch. - No
teaCLI 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-teaskill drives theteaCLI for issues/PRs/wiki but explicitly does not cover container registry pushes — registry auth/push must be done via plaindocker login/docker pushas 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
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-testingskill exists locally (noted per instructions, not invoked). - No separate Playwright MCP server was found configured in this environment.
- The
claude-in-chromecapability 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 (extendWebTestCase, use$client->request()plusassertResponseIsSuccessful()/assertSelectorTextContains()style assertions). Setup is viacomposer require --dev symfony/test-packandphp bin/phpunit; test env config lives in.env.test/.env.test.local(.env.localis deliberately not loaded in the test environment). — Testing, symfony.com/doc - 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
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
hybridmode was folded intostatic):static(default — every page prerendered to flat HTML at build time) andserver(on-demand rendering per request, requires an adapter). — On-demand Rendering, docs.astro.build - 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
servermode 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
- 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, schedule-x/schedule-x GitHub
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/<user>/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_accessfor 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-testingskill when writing the suite; useclaude-in-chromefor 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 localphpunit-best-practicesskill when writing tests. - Docker/CI: two Dockerfiles (dev, prod), prod built and pushed as
git.arthurerlich.de/haylan/smart-tablet-screen:latestviadocker login/docker pushper 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 usedevops-engineer/docker-expertskills. - 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).