feat(web-svc-design): derive one corporate design from a single base color

web-svc-css becomes web-svc-design and owns the configuration that lived in group_vars/all/13_design.yml. Consumers switch their css service key to design, and sys-front-inj-css becomes sys-front-inj-design. javascript stays an independent key; a role's optional design.js.j2 is injected together with the design and CSP-hashed like the functional script.

An in-repo OKLCH lookup replaces colorscheme_generator. From design.colors.base alone it derives surfaces, text, borders, the primary and status fills with their on-colors, hover and active states and a dedicated dark palette; a unit test proves WCAG AA, and AAA for body text, across six base colors in both modes. Semantic --design-* tokens replace --color-NN-SS in every role stylesheet. The shared element styles sit in the cascade layer infinito-design, so an app's own design system always wins and takes the palette through a variable mapping in its role stylesheet or through its own theming mechanism.

design.logo and design.title resolve globally from web-svc-design or per role from its design entry, and false or 0 disables the replacement. The design_assets action renders the logo lockup per slot as SVG, PNG and ICO, and the legacy title keys move into the design entries.

Three roles carry the design end to end. The dashboard takes logo, favicon and title from the generator and replaces the hard-coded port-ui colors. Gitea maps its theme variables onto the tokens and mounts the generated logo and a branded start page. Nextcloud receives primary color, logo, name and slogan through occ theming and ships no stylesheet.

make design-sync re-renders the static design files of a deployed app, make design-gallery captures before and after screenshots in light, dark, desktop and mobile, and make design-queue orders the roles that are due for a design pass. The i8-designer skill and docs/agents/action/design.md describe that loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-04 06:04:14 +02:00
1 parent eb8550517d
commit 86cefeff26
427 files changed
+5838 -2337

No files matched your search

+24
View File
@@ -272,6 +272,30 @@ console:
cosmos:
@"$${PYTHON}" -m cli.build.docs.readme $(role) --update-cosmos
.PHONY: design-gallery
# Rerun the design spec of a deployed app and copy its before/after screenshots to /tmp/design-gallery/<app>/ (no redeploy).
# Usage: make design-gallery app=<application_id>
# Param app: deployed application id
design-gallery:
@: $${app:?app=<application_id> required, e.g. app=web-app-gitea}
@app='$(app)' bash scripts/tests/design/gallery.sh
.PHONY: design-queue
# Print the roles that are due for a design pass in working order (new roles first, then by version gap).
# Usage: make design-queue [args="--next|--all|--format json"]
# Param args: extra flags for the queue CLI
design-queue:
@"$${PYTHON}" -m cli.meta.roles.design $(args)
.PHONY: design-sync
# Re-render shared CSS, role style.css and the branding assets of a deployed app (no redeploy; injected scripts stay as deployed).
# Usage: make design-sync app=<application_id> [variant=<idx>]
# Param app: deployed application id
# Param variant: matrix round index the app was deployed with
design-sync:
@: $${app:?app=<application_id> required, e.g. app=web-app-gitea}
@app='$(app)' variant='$(variant)' bash scripts/tests/design/sync.sh
.PHONY: diagnose-disk-usage
# Show disk and Docker resource usage to identify what to clean up.
diagnose-disk-usage:
+79 -79
View File
@@ -60,7 +60,7 @@ Every invokable role in the tested lifecycle envelope, with its upstream homepag
| Name | Status | Description | More | Integrated with |
|---|---|---|---|---|
| [Akaunting](roles/web-app-akaunting/) | beta | Self-hosted platform for online accounting, bookkeeping, and business finance management | [🌐](https://akaunting.com/) [🎬](https://www.youtube.com/watch?v=UTVe_Zob_k0) [🛠️](roles/web-app-akaunting/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Akaunting](roles/web-app-akaunting/) | beta | Self-hosted platform for online accounting, bookkeeping, and business finance management | [🌐](https://akaunting.com/) [🎬](https://www.youtube.com/watch?v=UTVe_Zob_k0) [🛠️](roles/web-app-akaunting/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Asset Mirror (Privacy Proxy)](roles/web-svc-mirror/) | alpha | Nginx-based privacy proxy that mirrors whitelisted third-party assets under a first-party domain | [🛠️](roles/web-svc-mirror/README.md#quick-setup) | [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Assets Server](roles/web-svc-asset/) | beta | Nginx-based server for hosting static assets copied from a source directory to the web data path | [🛠️](roles/web-svc-asset/README.md#quick-setup) | [File Server](roles/web-svc-file/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) | beta | Scheduled deduplicating backup system for Docker container volumes to a local directory, with consistent SQL dumps | [🛠️](roles/svc-bkp-volume-2-local/README.md#quick-setup) | [Backup Host Secrets](roles/svc-bkp-secrets-2-local/) |
@@ -68,130 +68,130 @@ Every invokable role in the tested lifecycle envelope, with its upstream homepag
| [Backup NFS](roles/svc-bkp-nfs-2-local/) | beta | Scheduled backup job for hard-linked differential rsync snapshots of an NFS export into the local backup tree | [🛠️](roles/svc-bkp-nfs-2-local/README.md#quick-setup) | [NFS Server](roles/svc-storage-nfs-server/) |
| [Backup Remote Server](roles/svc-bkp-remote-2-local/) | beta | Scheduled pull-style backup replicating remote hosts onto this host over SSH and rsync as hard-linked snapshots | [🛠️](roles/svc-bkp-remote-2-local/README.md#quick-setup) | [Backup to External Device](roles/svc-bkp-local-2-device/) |
| [Backup to External Device](roles/svc-bkp-local-2-device/) | beta | Plug-triggered systemd backup for hard-link deduplicated snapshots of the local backup tree onto a swappable USB device | [🛠️](roles/svc-bkp-local-2-device/README.md#quick-setup) | |
| [Baserow](roles/web-app-baserow/) | beta | Self-hosted no-code database platform with a spreadsheet-like interface for building and collaborating on data | [🌐](https://baserow.io/) [🎬](https://www.youtube.com/watch?v=5yFMfP96_4M) [🛠️](roles/web-app-baserow/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BigBlueButton](roles/web-app-bigbluebutton/) | beta | Self-hosted web conferencing platform for virtual classrooms, online meetings, and real-time collaboration | [🌐](https://bigbluebutton.org/) [🎬](https://www.youtube.com/watch?v=YRkQB6MJllI) [🛠️](roles/web-app-bigbluebutton/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Bluesky](roles/web-app-bluesky/) | beta | Self-hosted decentralized social network on the open AT Protocol, with its own Personal Data Server | [🌐](https://bsky.social/) [🛠️](roles/web-app-bluesky/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BookWyrm](roles/web-app-bookwyrm/) | beta | Self-hosted federated reading platform for book reviews and reading tracking across the Fediverse | [🌐](https://joinbookwyrm.com/) [🛠️](roles/web-app-bookwyrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Bridgy Fed](roles/web-app-bridgy-fed/) | beta | Federation bridge connecting ActivityPub (Fediverse), the AT Protocol (Bluesky), and the IndieWeb | [🌐](https://fed.brid.gy/) [🛠️](roles/web-app-bridgy-fed/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Checkmk](roles/web-app-checkmk/) | beta | Open-source IT and application monitoring server behind SSO with native LDAP user management | [🌐](https://checkmk.com/) [🎬](https://www.youtube.com/watch?v=opO-SOgOJ1I) [🛠️](roles/web-app-checkmk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Chess](roles/web-app-chess/) | beta | Federated chess server for playing and following rule-validated games over ActivityPub across the Fediverse | [🌐](https://castling.club/) [🛠️](roles/web-app-chess/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Baserow](roles/web-app-baserow/) | beta | Self-hosted no-code database platform with a spreadsheet-like interface for building and collaborating on data | [🌐](https://baserow.io/) [🎬](https://www.youtube.com/watch?v=5yFMfP96_4M) [🛠️](roles/web-app-baserow/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BigBlueButton](roles/web-app-bigbluebutton/) | beta | Self-hosted web conferencing platform for virtual classrooms, online meetings, and real-time collaboration | [🌐](https://bigbluebutton.org/) [🎬](https://www.youtube.com/watch?v=YRkQB6MJllI) [🛠️](roles/web-app-bigbluebutton/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-design/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Bluesky](roles/web-app-bluesky/) | beta | Self-hosted decentralized social network on the open AT Protocol, with its own Personal Data Server | [🌐](https://bsky.social/) [🛠️](roles/web-app-bluesky/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BookWyrm](roles/web-app-bookwyrm/) | beta | Self-hosted federated reading platform for book reviews and reading tracking across the Fediverse | [🌐](https://joinbookwyrm.com/) [🛠️](roles/web-app-bookwyrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Bridgy Fed](roles/web-app-bridgy-fed/) | beta | Federation bridge connecting ActivityPub (Fediverse), the AT Protocol (Bluesky), and the IndieWeb | [🌐](https://fed.brid.gy/) [🛠️](roles/web-app-bridgy-fed/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Checkmk](roles/web-app-checkmk/) | beta | Open-source IT and application monitoring server behind SSO with native LDAP user management | [🌐](https://checkmk.com/) [🎬](https://www.youtube.com/watch?v=opO-SOgOJ1I) [🛠️](roles/web-app-checkmk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Chess](roles/web-app-chess/) | beta | Federated chess server for playing and following rule-validated games over ActivityPub across the Fediverse | [🌐](https://castling.club/) [🛠️](roles/web-app-chess/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Collabora](roles/web-svc-collabora/) | beta | Self-hosted online office backend for real-time in-browser editing of documents, spreadsheets, and presentations | [🌐](https://www.collaboraonline.com/) [🎬](https://www.youtube.com/watch?v=8pqkclWG2BY) [🛠️](roles/web-svc-collabora/README.md#quick-setup) | [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Content Delivery Network](roles/web-svc-cdn/) | beta | Nginx-based content delivery network for serving shared static assets to internal web applications | [🛠️](roles/web-svc-cdn/README.md#quick-setup) | [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Corporate Design](roles/web-svc-css/) | beta | Service-registry role owning the canonical `css` flag and redirecting the CSS hostname to the CDN upstream | [🛠️](roles/web-svc-css/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Corporate Design](roles/web-svc-design/) | beta | Corporate design configuration owner and `design` service flag, redirecting its hostname to the CDN | [🛠️](roles/web-svc-design/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Coturn](roles/web-svc-coturn/) | beta | Free and open-source TURN/STUN relay server for NAT traversal in WebRTC and real-time communication | [🌐](https://github.com/coturn/coturn) [🛠️](roles/web-svc-coturn/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Prometheus](roles/web-app-prometheus/) |
| [Dashboard](roles/web-app-dashboard/) | beta | Self-hosted portal for unified access to all deployed applications via a customizable tile and menu interface | [🛠️](roles/web-app-dashboard/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Simple Icons](roles/web-svc-simpleicons/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Decidim](roles/web-app-decidim/) | beta | Open-source participatory democracy platform for public consultations, civic processes, and community voting | [🌐](https://decidim.org/) [🎬](https://www.youtube.com/watch?v=luSdPZ-wUIY) [🛠️](roles/web-app-decidim/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Discourse](roles/web-app-discourse/) | beta | Open-source discussion and community forum platform with moderation, notifications, and plugin support | [🌐](https://www.discourse.org/) [🎬](https://www.youtube.com/watch?v=z3qp_uB46FA) [🛠️](roles/web-app-discourse/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Dashboard](roles/web-app-dashboard/) | beta | Self-hosted portal for unified access to all deployed applications via a customizable tile and menu interface | [🛠️](roles/web-app-dashboard/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-design/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Simple Icons](roles/web-svc-simpleicons/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Decidim](roles/web-app-decidim/) | beta | Open-source participatory democracy platform for public consultations, civic processes, and community voting | [🌐](https://decidim.org/) [🎬](https://www.youtube.com/watch?v=luSdPZ-wUIY) [🛠️](roles/web-app-decidim/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Discourse](roles/web-app-discourse/) | beta | Open-source discussion and community forum platform with moderation, notifications, and plugin support | [🌐](https://www.discourse.org/) [🎬](https://www.youtube.com/watch?v=z3qp_uB46FA) [🛠️](roles/web-app-discourse/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Docker Registry](roles/svc-registry-docker/) | beta | Cluster-local container image registry for serving custom-built images to Docker Swarm worker nodes | [🌐](https://distribution.github.io/distribution/) [🛠️](roles/svc-registry-docker/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Docker Registry Cache](roles/svc-registry-cache/) | beta | Cluster-local pull-through cache for shared Docker Hub base-image pulls, cutting duplicate transfers and rate limits | [🛠️](roles/svc-registry-cache/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Docker Swarm](roles/svc-swarm-node/) | beta | Container orchestration cluster for scheduling services across pooled manager and worker nodes | [🛠️](roles/svc-swarm-node/README.md#quick-setup) | |
| [Docker Swarm Manager](roles/svc-swarm-manager/) | beta | Marker role tagging a host as the Docker Swarm manager node, with the init logic delegated to svc-swarm-node | [🛠️](roles/svc-swarm-manager/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Elasticsearch](roles/svc-db-elasticsearch/) | beta | Central search and analytics engine with per-consumer index namespaces isolating tenants in a shared Docker deployment | [🌐](https://www.elastic.co/elasticsearch) [🛠️](roles/svc-db-elasticsearch/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [ERPNext](roles/web-app-erpnext/) | beta | Self-hosted open-source ERP suite for finance, CRM, inventory, manufacturing, HR, sales, and projects | [🌐](https://erpnext.com/) [🎬](https://www.youtube.com/watch?v=JlqE-BfDEUY) [🛠️](roles/web-app-erpnext/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [EspoCRM](roles/web-app-espocrm/) | beta | Open-source CRM platform for managing sales, customer interactions, and business processes across teams | [🌐](https://www.espocrm.com/) [🎬](https://www.youtube.com/watch?v=TH6yJPTPLO0) [🛠️](roles/web-app-espocrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Fediwall](roles/web-app-fediwall/) | beta | Self-hosted Fediverse wall for showing recent public posts by hashtag or account in a live masonry grid | [🌐](https://github.com/defnull/fediwall) [🛠️](roles/web-app-fediwall/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Friendica](roles/web-app-friendica/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Fider](roles/web-app-fider/) | beta | Open-source community feedback platform for collecting, voting on, and prioritizing feature requests and ideas | [🌐](https://www.fider.io/) [🛠️](roles/web-app-fider/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [File Server](roles/web-svc-file/) | beta | Static file server over HTTPS with directory listing, .well-known handling, and automatic TLS certificates | [🛠️](roles/web-svc-file/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [ERPNext](roles/web-app-erpnext/) | beta | Self-hosted open-source ERP suite for finance, CRM, inventory, manufacturing, HR, sales, and projects | [🌐](https://erpnext.com/) [🎬](https://www.youtube.com/watch?v=JlqE-BfDEUY) [🛠️](roles/web-app-erpnext/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [EspoCRM](roles/web-app-espocrm/) | beta | Open-source CRM platform for managing sales, customer interactions, and business processes across teams | [🌐](https://www.espocrm.com/) [🎬](https://www.youtube.com/watch?v=TH6yJPTPLO0) [🛠️](roles/web-app-espocrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Fediwall](roles/web-app-fediwall/) | beta | Self-hosted Fediverse wall for showing recent public posts by hashtag or account in a live masonry grid | [🌐](https://github.com/defnull/fediwall) [🛠️](roles/web-app-fediwall/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Friendica](roles/web-app-friendica/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Fider](roles/web-app-fider/) | beta | Open-source community feedback platform for collecting, voting on, and prioritizing feature requests and ideas | [🌐](https://www.fider.io/) [🛠️](roles/web-app-fider/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [File Server](roles/web-svc-file/) | beta | Static file server over HTTPS with directory listing, .well-known handling, and automatic TLS certificates | [🛠️](roles/web-svc-file/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Firewall ruleset](roles/svc-net-firewall/) | beta | nftables packet filter whose rules each role owns as its own table, replaced atomically and reloaded at boot | [🌐](https://www.netfilter.org/projects/nftables/) [🎬](https://www.youtube.com/watch?v=EGKhIljDPCw) [🛠️](roles/svc-net-firewall/README.md#quick-setup) | |
| [Flowise](roles/web-app-flowise/) | beta | Low-code visual builder for creating, testing, and publishing LLM and RAG workflows as HTTP endpoints | [🌐](https://flowiseai.com/) [🎬](https://www.youtube.com/watch?v=h_0TJTY6Ulg) [🛠️](roles/web-app-flowise/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Ollama](roles/svc-ai-ollama/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Qdrant](roles/svc-db-qdrant/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Friendica](roles/web-app-friendica/) | beta | Decentralized, federated social-networking platform for peer-to-peer communication and community building | [🌐](https://friendi.ca/) [🛠️](roles/web-app-friendica/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Funkwhale](roles/web-app-funkwhale/) | beta | Self-hosted, federated platform for sharing, hosting, and streaming your own music and audio library | [🌐](https://www.funkwhale.audio/) [🛠️](roles/web-app-funkwhale/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Typesense](roles/svc-db-typesense/), [Universal Logout](roles/web-svc-logout/) |
| [FusionDirectory](roles/web-app-fusiondirectory/) | beta | Web-based LDAP administration interface for managing users, groups, and directory objects | [🌐](https://www.fusiondirectory.org/) [🎬](https://www.youtube.com/watch?v=dThpssnXGyk) [🛠️](roles/web-app-fusiondirectory/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Gitea](roles/web-app-gitea/) | beta | Lightweight self-hosted Git service for code collaboration, version control, and repository management | [🌐](https://about.gitea.com/) [🛠️](roles/web-app-gitea/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [GitLab](roles/web-app-gitlab/) | beta | Self-hosted DevOps platform for Git source-code management, CI/CD pipelines, and issue tracking | [🌐](https://about.gitlab.com/) [🎬](https://www.youtube.com/watch?v=7q9Y1Cv-ib0) [🛠️](roles/web-app-gitlab/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Hermes Agent](roles/web-app-hermes/) | beta | Kernel-isolated agent runtime serving an OpenAI-compatible API, routed to models through the shared LLM gateway | [🌐](https://hermes-agent.nousresearch.com/) [🛠️](roles/web-app-hermes/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Flowise](roles/web-app-flowise/) | beta | Low-code visual builder for creating, testing, and publishing LLM and RAG workflows as HTTP endpoints | [🌐](https://flowiseai.com/) [🎬](https://www.youtube.com/watch?v=h_0TJTY6Ulg) [🛠️](roles/web-app-flowise/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Ollama](roles/svc-ai-ollama/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Qdrant](roles/svc-db-qdrant/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Friendica](roles/web-app-friendica/) | beta | Decentralized, federated social-networking platform for peer-to-peer communication and community building | [🌐](https://friendi.ca/) [🛠️](roles/web-app-friendica/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Funkwhale](roles/web-app-funkwhale/) | beta | Self-hosted, federated platform for sharing, hosting, and streaming your own music and audio library | [🌐](https://www.funkwhale.audio/) [🛠️](roles/web-app-funkwhale/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Typesense](roles/svc-db-typesense/), [Universal Logout](roles/web-svc-logout/) |
| [FusionDirectory](roles/web-app-fusiondirectory/) | beta | Web-based LDAP administration interface for managing users, groups, and directory objects | [🌐](https://www.fusiondirectory.org/) [🎬](https://www.youtube.com/watch?v=dThpssnXGyk) [🛠️](roles/web-app-fusiondirectory/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Gitea](roles/web-app-gitea/) | beta | Lightweight self-hosted Git service for code collaboration, version control, and repository management | [🌐](https://about.gitea.com/) [🛠️](roles/web-app-gitea/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [GitLab](roles/web-app-gitlab/) | beta | Self-hosted DevOps platform for Git source-code management, CI/CD pipelines, and issue tracking | [🌐](https://about.gitlab.com/) [🎬](https://www.youtube.com/watch?v=7q9Y1Cv-ib0) [🛠️](roles/web-app-gitlab/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Hermes Agent](roles/web-app-hermes/) | beta | Kernel-isolated agent runtime serving an OpenAI-compatible API, routed to models through the shared LLM gateway | [🌐](https://hermes-agent.nousresearch.com/) [🛠️](roles/web-app-hermes/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Home Assistant](roles/web-app-homeassistant/) | beta | Smart-home automation hub with device integrations, dashboards and a built-in MCP server for AI agent access | [🌐](https://www.home-assistant.io/) [🛠️](roles/web-app-homeassistant/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Dashboard](roles/web-app-dashboard/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [HTML Server](roles/web-svc-html/) | beta | NGINX vhost for serving static HTML websites over HTTPS with automatic Let's Encrypt certificates | [🛠️](roles/web-svc-html/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Hugo](roles/web-app-hugo/) | beta | Static site generator building HTML from an upstream Git repository at build time and serving it via nginx | [🌐](https://gohugo.io/) [🛠️](roles/web-app-hugo/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [HTML Server](roles/web-svc-html/) | beta | NGINX vhost for serving static HTML websites over HTTPS with automatic Let's Encrypt certificates | [🛠️](roles/web-svc-html/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Hugo](roles/web-app-hugo/) | beta | Static site generator building HTML from an upstream Git repository at build time and serving it via nginx | [🌐](https://gohugo.io/) [🛠️](roles/web-app-hugo/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Imprint](roles/web-svc-legal/) | beta | NGINX-served static page rendering a legal imprint (Impressum) from a Jinja2 template | [🛠️](roles/web-svc-legal/README.md#quick-setup) | [HTML Server](roles/web-svc-html/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Intel Driver](roles/drv-intel/) | alpha | Intel VA-API media driver for hardware-accelerated video decode and encode across Linux distributions | [🌐](https://github.com/intel/media-driver) [🛠️](roles/drv-intel/README.md#quick-setup) | |
| [Jellyfin](roles/web-app-jellyfin/) | beta | Self-hosted media server for streaming and organizing movies, TV, music, and photos, with OIDC and LDAP login | [🌐](https://jellyfin.org/) [🎬](https://www.youtube.com/watch?v=0t3lSJL3B_Q) [🛠️](roles/web-app-jellyfin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jenkins](roles/web-app-jenkins/) | beta | Open-source automation server for CI/CD pipelines, builds, and plugin-driven deployment workflows | [🌐](https://www.jenkins.io/) [🎬](https://www.youtube.com/watch?v=rNy00_gOo8o) [🛠️](roles/web-app-jenkins/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jitsi Meet](roles/web-app-jitsi/) | beta | Self-hosted WebRTC platform for end-to-end-encryptable video conferencing with federated authentication | [🌐](https://jitsi.org/) [🎬](https://www.youtube.com/watch?v=TB7LlM4erx8) [🛠️](roles/web-app-jitsi/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Joomla](roles/web-app-joomla/) | beta | Open-source content management system for building websites, intranets, and online applications without writing code | [🌐](https://www.joomla.org/) [🎬](https://www.youtube.com/watch?v=mXFSIC_zM2o) [🛠️](roles/web-app-joomla/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jellyfin](roles/web-app-jellyfin/) | beta | Self-hosted media server for streaming and organizing movies, TV, music, and photos, with OIDC and LDAP login | [🌐](https://jellyfin.org/) [🎬](https://www.youtube.com/watch?v=0t3lSJL3B_Q) [🛠️](roles/web-app-jellyfin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jenkins](roles/web-app-jenkins/) | beta | Open-source automation server for CI/CD pipelines, builds, and plugin-driven deployment workflows | [🌐](https://www.jenkins.io/) [🎬](https://www.youtube.com/watch?v=rNy00_gOo8o) [🛠️](roles/web-app-jenkins/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jitsi Meet](roles/web-app-jitsi/) | beta | Self-hosted WebRTC platform for end-to-end-encryptable video conferencing with federated authentication | [🌐](https://jitsi.org/) [🎬](https://www.youtube.com/watch?v=TB7LlM4erx8) [🛠️](roles/web-app-jitsi/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Joomla](roles/web-app-joomla/) | beta | Open-source content management system for building websites, intranets, and online applications without writing code | [🌐](https://www.joomla.org/) [🎬](https://www.youtube.com/watch?v=mXFSIC_zM2o) [🛠️](roles/web-app-joomla/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Kata & gVisor](roles/svc-virt-kata/) | beta | Kernel-isolating container runtimes that sandbox untrusted workloads in a microVM or a userspace kernel | [🌐](https://katacontainers.io/) [🛠️](roles/svc-virt-kata/README.md#quick-setup) | |
| [Keycloak](roles/web-app-keycloak/) | beta | Self-hosted identity and access management platform for single sign-on, multi-factor authentication, and user federation | [🌐](https://www.keycloak.org/) [🛠️](roles/web-app-keycloak/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/) |
| [KIX](roles/web-app-kix/) | beta | Open-source ITSM and helpdesk platform (OTRS fork) for ticketing, CMDB, knowledge base, and reporting | [🌐](https://www.kixdesk.com/en/) [🎬](https://www.youtube.com/watch?v=D62emos1FQ8) [🛠️](roles/web-app-kix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LAM](roles/web-app-lam/) | beta | Web interface for administering LDAP directories: managing users, groups, and other directory objects | [🌐](https://www.ldap-account-manager.org/) [🛠️](roles/web-app-lam/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LibreTranslate](roles/web-svc-libretranslate/) | beta | Self-hosted open-source machine translation API for translating and detecting text across languages | [🌐](https://libretranslate.com/) [🛠️](roles/web-svc-libretranslate/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Keycloak](roles/web-app-keycloak/) | beta | Self-hosted identity and access management platform for single sign-on, multi-factor authentication, and user federation | [🌐](https://www.keycloak.org/) [🛠️](roles/web-app-keycloak/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/) |
| [KIX](roles/web-app-kix/) | beta | Open-source ITSM and helpdesk platform (OTRS fork) for ticketing, CMDB, knowledge base, and reporting | [🌐](https://www.kixdesk.com/en/) [🎬](https://www.youtube.com/watch?v=D62emos1FQ8) [🛠️](roles/web-app-kix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LAM](roles/web-app-lam/) | beta | Web interface for administering LDAP directories: managing users, groups, and other directory objects | [🌐](https://www.ldap-account-manager.org/) [🛠️](roles/web-app-lam/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LibreTranslate](roles/web-svc-libretranslate/) | beta | Self-hosted open-source machine translation API for translating and detecting text across languages | [🌐](https://libretranslate.com/) [🛠️](roles/web-svc-libretranslate/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LID Switch Driver](roles/drv-lid-switch/) | alpha | Power-aware systemd logind configuration for predictable laptop lid-close behavior across battery, AC, and docked states | [🛠️](roles/drv-lid-switch/README.md#quick-setup) | |
| [Listmonk](roles/web-app-listmonk/) | beta | Self-hosted platform for newsletters and mailing lists with subscriber segmentation and campaign analytics | [🌐](https://listmonk.app/) [🛠️](roles/web-app-listmonk/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Listmonk](roles/web-app-listmonk/) | beta | Self-hosted platform for newsletters and mailing lists with subscriber segmentation and campaign analytics | [🌐](https://listmonk.app/) [🛠️](roles/web-app-listmonk/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LiteLLM Admin UI](roles/web-app-litellm/) | beta | Browser-facing admin interface for the shared LLM gateway, published on its own domain behind the front proxy | [🌐](https://docs.litellm.ai/) [🛠️](roles/web-app-litellm/README.md#quick-setup) | [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LiteLLM Gateway](roles/svc-ai-litellm/) | beta | OpenAI-compatible LLM gateway routing a single /v1 endpoint to local and hosted model backends with per-consumer keys | [🌐](https://docs.litellm.ai/) [🛠️](roles/svc-ai-litellm/README.md#quick-setup) | [LM Studio](roles/svc-ai-lmstudio/), [Ollama](roles/svc-ai-ollama/), [PostgreSQL](roles/svc-db-postgres/) |
| [LittleJS](roles/web-app-littlejs/) | alpha | Self-hosted HTML5 game sandbox for browsing and running lightweight engine demos and games in the browser | [🌐](https://killedbyapixel.github.io/LittleJS/) [🎬](https://youtu.be/chuBzGjv7Ms) [🛠️](roles/web-app-littlejs/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LittleJS](roles/web-app-littlejs/) | alpha | Self-hosted HTML5 game sandbox for browsing and running lightweight engine demos and games in the browser | [🌐](https://killedbyapixel.github.io/LittleJS/) [🎬](https://youtu.be/chuBzGjv7Ms) [🛠️](roles/web-app-littlejs/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LM Studio](roles/svc-ai-lmstudio/) | beta | Headless local model server for open LLMs with an OpenAI-compatible API on the container network | [🌐](https://lmstudio.ai/) [🛠️](roles/svc-ai-lmstudio/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) |
| [Magento](roles/web-app-magento/) | alpha | Self-hosted e-commerce platform for catalog management, checkout, promotions, and multi-store online retail | [🌐](https://business.adobe.com/products/commerce/magento/open-source.html) [🎬](https://www.youtube.com/watch?v=nFdz9DZrdFc) [🛠️](roles/web-app-magento/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Magento](roles/web-app-magento/) | alpha | Self-hosted e-commerce platform for catalog management, checkout, promotions, and multi-store online retail | [🌐](https://business.adobe.com/products/commerce/magento/open-source.html) [🎬](https://www.youtube.com/watch?v=nFdz9DZrdFc) [🛠️](roles/web-app-magento/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [MariaDB](roles/svc-db-mariadb/) | beta | Shared relational database server for applications needing central SQL storage, run in Docker | [🌐](https://mariadb.org/) [🎬](https://www.youtube.com/watch?v=YsIJB15fq_c) [🛠️](roles/svc-db-mariadb/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mastodon](roles/web-app-mastodon/) | beta | Self-hosted federated social network for microblogging, real-time timelines, and community moderation | [🌐](https://joinmastodon.org/) [🛠️](roles/web-app-mastodon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matomo](roles/web-app-matomo/) | beta | Self-hosted, privacy-first web analytics platform for real-time visitor tracking and site metrics | [🌐](https://matomo.org/) [🎬](https://www.youtube.com/watch?v=Qc2kooLNDiU) [🛠️](roles/web-app-matomo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [MariaDB](roles/svc-db-mariadb/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matrix](roles/web-app-matrix/) | beta | Federated, end-to-end-encrypted messaging platform with a web client and protocol bridges to external chat networks | [🌐](https://matrix.org/) [🎬](https://www.youtube.com/watch?v=3AVsNqH_-9M) [🛠️](roles/web-app-matrix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Mattermost](roles/web-app-mattermost/) | beta | Open-source, self-hosted team messaging platform for real-time collaboration via channels, threads, and integrations | [🌐](https://mattermost.com/) [🎬](https://www.youtube.com/watch?v=szfZfQFUSnU) [🛠️](roles/web-app-mattermost/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Mastodon](roles/web-app-mastodon/) | beta | Self-hosted federated social network for microblogging, real-time timelines, and community moderation | [🌐](https://joinmastodon.org/) [🛠️](roles/web-app-mastodon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matomo](roles/web-app-matomo/) | beta | Self-hosted, privacy-first web analytics platform for real-time visitor tracking and site metrics | [🌐](https://matomo.org/) [🎬](https://www.youtube.com/watch?v=Qc2kooLNDiU) [🛠️](roles/web-app-matomo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [MariaDB](roles/svc-db-mariadb/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matrix](roles/web-app-matrix/) | beta | Federated, end-to-end-encrypted messaging platform with a web client and protocol bridges to external chat networks | [🌐](https://matrix.org/) [🎬](https://www.youtube.com/watch?v=3AVsNqH_-9M) [🛠️](roles/web-app-matrix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Mattermost](roles/web-app-mattermost/) | beta | Open-source, self-hosted team messaging platform for real-time collaboration via channels, threads, and integrations | [🌐](https://mattermost.com/) [🎬](https://www.youtube.com/watch?v=szfZfQFUSnU) [🛠️](roles/web-app-mattermost/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [MCP Adapter](roles/svc-ai-mcp-adapter/) | alpha | Reusable per-provider MCP adapter exposing a narrow allowlisted read surface | [🌐](https://modelcontextprotocol.io/) [🛠️](roles/svc-ai-mcp-adapter/README.md#quick-setup) | |
| [MediaWiki](roles/web-app-mediawiki/) | beta | Collaborative wiki platform for user-driven documentation with an extensible extension ecosystem and content management | [🌐](https://www.mediawiki.org/) [🎬](https://www.youtube.com/watch?v=-JnIjpRvgNY) [🛠️](roles/web-app-mediawiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [MediaWiki](roles/web-app-mediawiki/) | beta | Collaborative wiki platform for user-driven documentation with an extensible extension ecosystem and content management | [🌐](https://www.mediawiki.org/) [🎬](https://www.youtube.com/watch?v=-JnIjpRvgNY) [🛠️](roles/web-app-mediawiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Memcached](roles/svc-db-memcached/) | beta | Central in-memory caching engine with per-consumer key-prefix isolation, usable centrally or as an embedded sidecar | [🌐](https://memcached.org/) [🛠️](roles/svc-db-memcached/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [MIG](roles/web-app-mig/) | alpha | Interactive graph visualization of all Infinito.Nexus roles and their dependencies for architecture transparency | [🛠️](roles/web-app-mig/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mini-QR](roles/web-app-mini-qr/) | beta | Self-hosted browser tool for generating QR codes from text or URLs with privacy-friendly local rendering | [🌐](https://github.com/lyqht/mini-qr) [🛠️](roles/web-app-mini-qr/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mobilizon](roles/web-app-mobilizon/) | beta | Federated, self-hosted platform for organizing events and building communities and groups | [🌐](https://joinmobilizon.org/) [🎬](https://framatube.org/w/2mtugeJXaihsQnTHVWHf2E) [🛠️](roles/web-app-mobilizon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Moodle](roles/web-app-moodle/) | beta | Open-source learning management system for creating, managing, and delivering online courses and e-learning | [🌐](https://moodle.org/) [🎬](https://www.youtube.com/watch?v=3ORsUGVNxGs) [🛠️](roles/web-app-moodle/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [n8n](roles/web-app-n8n/) | beta | Self-hosted low-code automation platform for connecting services and building workflow integrations visually | [🌐](https://n8n.io/) [🎬](https://youtu.be/4cQWJViybAQ) [🛠️](roles/web-app-n8n/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Nextcloud](roles/web-app-nextcloud/) | beta | Self-hosted cloud platform for file sync, sharing, real-time collaboration, and team workspaces under your own domain | [🌐](https://nextcloud.com/) [🎬](https://youtu.be/3jcYJGQgenI?si=FDmoMSrAb9_WvviC) [🛠️](roles/web-app-nextcloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [BigBlueButton](roles/web-app-bigbluebutton/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [GitLab](roles/web-app-gitlab/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Matrix](roles/web-app-matrix/), [Mattermost](roles/web-app-mattermost/), [OnlyOffice](roles/web-svc-onlyoffice/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [OpenProject](roles/web-app-openproject/), [PeerTube](roles/web-app-peertube/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/), [XWiki](roles/web-app-xwiki/), [Zammad](roles/web-app-zammad/) |
| [MIG](roles/web-app-mig/) | alpha | Interactive graph visualization of all Infinito.Nexus roles and their dependencies for architecture transparency | [🛠️](roles/web-app-mig/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mini-QR](roles/web-app-mini-qr/) | beta | Self-hosted browser tool for generating QR codes from text or URLs with privacy-friendly local rendering | [🌐](https://github.com/lyqht/mini-qr) [🛠️](roles/web-app-mini-qr/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mobilizon](roles/web-app-mobilizon/) | beta | Federated, self-hosted platform for organizing events and building communities and groups | [🌐](https://joinmobilizon.org/) [🎬](https://framatube.org/w/2mtugeJXaihsQnTHVWHf2E) [🛠️](roles/web-app-mobilizon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Moodle](roles/web-app-moodle/) | beta | Open-source learning management system for creating, managing, and delivering online courses and e-learning | [🌐](https://moodle.org/) [🎬](https://www.youtube.com/watch?v=3ORsUGVNxGs) [🛠️](roles/web-app-moodle/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [n8n](roles/web-app-n8n/) | beta | Self-hosted low-code automation platform for connecting services and building workflow integrations visually | [🌐](https://n8n.io/) [🎬](https://youtu.be/4cQWJViybAQ) [🛠️](roles/web-app-n8n/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Nextcloud](roles/web-app-nextcloud/) | beta | Self-hosted cloud platform for file sync, sharing, real-time collaboration, and team workspaces under your own domain | [🌐](https://nextcloud.com/) [🎬](https://youtu.be/3jcYJGQgenI?si=FDmoMSrAb9_WvviC) [🛠️](roles/web-app-nextcloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [BigBlueButton](roles/web-app-bigbluebutton/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-design/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [GitLab](roles/web-app-gitlab/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Matrix](roles/web-app-matrix/), [Mattermost](roles/web-app-mattermost/), [OnlyOffice](roles/web-svc-onlyoffice/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [OpenProject](roles/web-app-openproject/), [PeerTube](roles/web-app-peertube/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/), [XWiki](roles/web-app-xwiki/), [Zammad](roles/web-app-zammad/) |
| [NFS Client](roles/svc-storage-nfs-client/) | beta | Client-side NFS setup for mounting a remote server export on every swarm node, with a deploy-time reachability probe | [🛠️](roles/svc-storage-nfs-client/README.md#quick-setup) | [NFS Server](roles/svc-storage-nfs-server/) |
| [NFS Server](roles/svc-storage-nfs-server/) | beta | Kernel-level NFS server for exporting a shared directory tree with IP-restricted access for swarm-node hosts | [🛠️](roles/svc-storage-nfs-server/README.md#quick-setup) | [Backup NFS](roles/svc-bkp-nfs-2-local/) |
| [Odoo ERP](roles/web-app-odoo/) | beta | Open-source ERP suite for CRM, sales, accounting, inventory, HR, and website management | [🌐](https://www.odoo.com/) [🎬](https://www.youtube.com/watch?v=FCmFgHVzd3E) [🛠️](roles/web-app-odoo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Odoo ERP](roles/web-app-odoo/) | beta | Open-source ERP suite for CRM, sales, accounting, inventory, HR, and website management | [🌐](https://www.odoo.com/) [🎬](https://www.youtube.com/watch?v=FCmFgHVzd3E) [🛠️](roles/web-app-odoo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Ollama](roles/svc-ai-ollama/) | beta | Self-hosted model server for running open LLMs locally behind a simple HTTP API | [🌐](https://ollama.com/) [🛠️](roles/svc-ai-ollama/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) |
| [OnlyOffice](roles/web-svc-onlyoffice/) | beta | Document server backend for real-time in-browser editing of documents, spreadsheets, and presentations | [🌐](https://www.onlyoffice.com/) [🎬](https://www.youtube.com/watch?v=uiFxnRK_aWM) [🛠️](roles/web-svc-onlyoffice/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/) |
| [Open WebUI](roles/web-app-openwebui/) | beta | Self-hosted chat interface for interacting with local and private AI models via backends such as Ollama | [🌐](https://openwebui.com/) [🛠️](roles/web-app-openwebui/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenBao](roles/web-app-openbao/) | beta | Identity-based secrets, PKI and encryption manager with OIDC login, group-mapped policies and static-seal auto-unseal | [🌐](https://openbao.org/) [🎬](https://www.youtube.com/watch?v=hPBT-kSS-68) [🛠️](roles/web-app-openbao/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenClaw](roles/web-app-openclaw/) | beta | Self-hosted personal AI agent gateway with a browser control UI, sandboxed in a Kata microVM or gVisor runtime | [🌐](https://openclaw.ai/) [🛠️](roles/web-app-openclaw/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenCloud](roles/web-app-opencloud/) | beta | Self-hosted platform for file sync, sharing, and collaboration with Keycloak SSO and central LDAP directory | [🌐](https://opencloud.eu/) [🎬](https://www.youtube.com/watch?v=Cg48rFNY0R4) [🛠️](roles/web-app-opencloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OnlyOffice](roles/web-svc-onlyoffice/) | beta | Document server backend for real-time in-browser editing of documents, spreadsheets, and presentations | [🌐](https://www.onlyoffice.com/) [🎬](https://www.youtube.com/watch?v=uiFxnRK_aWM) [🛠️](roles/web-svc-onlyoffice/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/) |
| [Open WebUI](roles/web-app-openwebui/) | beta | Self-hosted chat interface for interacting with local and private AI models via backends such as Ollama | [🌐](https://openwebui.com/) [🛠️](roles/web-app-openwebui/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenBao](roles/web-app-openbao/) | beta | Identity-based secrets, PKI and encryption manager with OIDC login, group-mapped policies and static-seal auto-unseal | [🌐](https://openbao.org/) [🎬](https://www.youtube.com/watch?v=hPBT-kSS-68) [🛠️](roles/web-app-openbao/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenClaw](roles/web-app-openclaw/) | beta | Self-hosted personal AI agent gateway with a browser control UI, sandboxed in a Kata microVM or gVisor runtime | [🌐](https://openclaw.ai/) [🛠️](roles/web-app-openclaw/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenCloud](roles/web-app-opencloud/) | beta | Self-hosted platform for file sync, sharing, and collaboration with Keycloak SSO and central LDAP directory | [🌐](https://opencloud.eu/) [🎬](https://www.youtube.com/watch?v=Cg48rFNY0R4) [🛠️](roles/web-app-opencloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenLDAP](roles/svc-db-openldap/) | beta | Self-hosted LDAP directory service for centralized user, group, and schema management with TLS-secured access | [🌐](https://www.openldap.org/) [🛠️](roles/svc-db-openldap/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [OpenProject](roles/web-app-openproject/) | beta | Self-hosted platform for project management, work-package tracking, Gantt planning, and team collaboration | [🌐](https://www.openproject.org/) [🎬](https://www.youtube.com/watch?v=rvCd0xH7rCs) [🛠️](roles/web-app-openproject/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenProject](roles/web-app-openproject/) | beta | Self-hosted platform for project management, work-package tracking, Gantt planning, and team collaboration | [🌐](https://www.openproject.org/) [🎬](https://www.youtube.com/watch?v=rvCd0xH7rCs) [🛠️](roles/web-app-openproject/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenResty](roles/svc-prx-openresty/) | beta | NGINX-based web platform and reverse proxy with validated config reloads and Let's Encrypt integration | [🌐](https://openresty.org/) [🎬](https://www.youtube.com/watch?v=NR8U1qYHGqk) [🛠️](roles/svc-prx-openresty/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [OpenTalk](roles/web-app-opentalk/) | beta | Sovereign open-source video conferencing platform for audio/video calls, screen sharing, breakout rooms, and SSO | [🌐](https://opentalk.eu/) [🛠️](roles/web-app-opentalk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [OpenTalk](roles/web-app-opentalk/) | beta | Sovereign open-source video conferencing platform for audio/video calls, screen sharing, breakout rooms, and SSO | [🌐](https://opentalk.eu/) [🛠️](roles/web-app-opentalk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Package update](roles/update/) | beta | System-maintenance role for refreshing the package index and upgrading installed packages via the OS package manager | [🛠️](roles/update/README.md#quick-setup) | |
| [PeerTube](roles/web-app-peertube/) | beta | Decentralized, federated platform for peer-to-peer video hosting, streaming, and live broadcasting | [🌐](https://joinpeertube.org/) [🎬](https://framatube.org/w/cX4KAPLTwha6hzLZ5xePGZ) [🛠️](roles/web-app-peertube/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Penpot](roles/web-app-penpot/) | beta | Self-hosted design and prototyping platform for UI/UX work, real-time collaboration, and developer handoff | [🌐](https://penpot.app/) [🎬](https://www.youtube.com/watch?v=vAkb4QQmbfk) [🛠️](roles/web-app-penpot/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [pgAdmin](roles/web-app-pgadmin/) | beta | Web-based administration and development platform for managing multiple PostgreSQL servers, with optional OAuth2 login | [🌐](https://www.pgadmin.org/) [🎬](https://www.youtube.com/watch?v=WFT5MaZN6g4) [🛠️](roles/web-app-pgadmin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PhpMyAdmin](roles/web-app-phpmyadmin/) | beta | Web-based administration tool for MySQL and MariaDB with schema management, querying, and import/export | [🌐](https://www.phpmyadmin.net/) [🛠️](roles/web-app-phpmyadmin/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pixelfed](roles/web-app-pixelfed/) | beta | Decentralized, federated platform for photo sharing and image feeds across the fediverse | [🌐](https://pixelfed.org/) [🛠️](roles/web-app-pixelfed/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PeerTube](roles/web-app-peertube/) | beta | Decentralized, federated platform for peer-to-peer video hosting, streaming, and live broadcasting | [🌐](https://joinpeertube.org/) [🎬](https://framatube.org/w/cX4KAPLTwha6hzLZ5xePGZ) [🛠️](roles/web-app-peertube/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Universal Logout](roles/web-svc-logout/) |
| [Penpot](roles/web-app-penpot/) | beta | Self-hosted design and prototyping platform for UI/UX work, real-time collaboration, and developer handoff | [🌐](https://penpot.app/) [🎬](https://www.youtube.com/watch?v=vAkb4QQmbfk) [🛠️](roles/web-app-penpot/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [pgAdmin](roles/web-app-pgadmin/) | beta | Web-based administration and development platform for managing multiple PostgreSQL servers, with optional OAuth2 login | [🌐](https://www.pgadmin.org/) [🎬](https://www.youtube.com/watch?v=WFT5MaZN6g4) [🛠️](roles/web-app-pgadmin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PhpMyAdmin](roles/web-app-phpmyadmin/) | beta | Web-based administration tool for MySQL and MariaDB with schema management, querying, and import/export | [🌐](https://www.phpmyadmin.net/) [🛠️](roles/web-app-phpmyadmin/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pixelfed](roles/web-app-pixelfed/) | beta | Decentralized, federated platform for photo sharing and image feeds across the fediverse | [🌐](https://pixelfed.org/) [🛠️](roles/web-app-pixelfed/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PostgreSQL](roles/svc-db-postgres/) | beta | Containerized relational database with automated database, user, and privilege provisioning | [🌐](https://www.postgresql.org/) [🛠️](roles/svc-db-postgres/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Postmarks](roles/web-app-postmarks/) | beta | Single-user ActivityPub bookmarking site federating public bookmarks to Mastodon and the Fediverse | [🌐](https://github.com/ckolderup/postmarks) [🛠️](roles/web-app-postmarks/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pretix](roles/web-app-pretix/) | beta | Self-hosted ticketing platform for events with online sales, registration management, and secure payments | [🌐](https://pretix.eu/) [🎬](https://www.youtube.com/watch?v=5F6QkZP1Zf0) [🛠️](roles/web-app-pretix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Prometheus](roles/web-app-prometheus/) | beta | Open-source monitoring and alerting toolkit for storing metrics as time-series data and querying them with PromQL | [🌐](https://prometheus.io/) [🎬](https://www.youtube.com/watch?v=PzFUwBflXYc) [🛠️](roles/web-app-prometheus/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Postmarks](roles/web-app-postmarks/) | beta | Single-user ActivityPub bookmarking site federating public bookmarks to Mastodon and the Fediverse | [🌐](https://github.com/ckolderup/postmarks) [🛠️](roles/web-app-postmarks/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pretix](roles/web-app-pretix/) | beta | Self-hosted ticketing platform for events with online sales, registration management, and secure payments | [🌐](https://pretix.eu/) [🎬](https://www.youtube.com/watch?v=5F6QkZP1Zf0) [🛠️](roles/web-app-pretix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Prometheus](roles/web-app-prometheus/) | beta | Open-source monitoring and alerting toolkit for storing metrics as time-series data and querying them with PromQL | [🌐](https://prometheus.io/) [🎬](https://www.youtube.com/watch?v=PzFUwBflXYc) [🛠️](roles/web-app-prometheus/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Proprietary Drivers](roles/drv-non-free/) | alpha | Proprietary GPU driver installer for Arch-based systems via mhwd nonfree hardware detection | [🛠️](roles/drv-non-free/README.md#quick-setup) | |
| [Qdrant](roles/svc-db-qdrant/) | beta | Central vector database engine for storing and searching per-consumer embeddings shared across the stack | [🌐](https://qdrant.tech/) [🎬](https://www.youtube.com/watch?v=_opNdOa48Lc) [🛠️](roles/svc-db-qdrant/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Tor Onion Service](roles/svc-net-tor/) |
| [RabbitMQ](roles/svc-db-rabbitmq/) | beta | Shared message broker for async messaging with per-consumer virtual host and user isolation | [🌐](https://www.rabbitmq.com/) [🛠️](roles/svc-db-rabbitmq/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Redirector](roles/web-opt-rdr-domains/) | beta | NGINX glue role for 301 redirects from source to target domains with automatic SSL for the source hosts | [🛠️](roles/web-opt-rdr-domains/README.md#quick-setup) | |
| [Redis](roles/svc-db-redis/) | beta | In-memory key-value cache and store, shared centrally with per-consumer ACL isolation or embedded as a sidecar | [🌐](https://redis.io/) [🛠️](roles/svc-db-redis/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [Roulette Wheel](roles/web-app-roulette-wheel/) | alpha | Browser-based spinning wheel app for decision making, random selection, and gamification | [🌐](https://github.com/p-wojt/roulette-wheel) [🛠️](roles/web-app-roulette-wheel/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Roulette Wheel](roles/web-app-roulette-wheel/) | alpha | Browser-based spinning wheel app for decision making, random selection, and gamification | [🌐](https://github.com/p-wojt/roulette-wheel) [🛠️](roles/web-app-roulette-wheel/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [SeaweedFS](roles/web-app-seaweedfs/) | beta | Distributed S3-compatible object store for shared, per-consumer bucket provisioning across web-app roles | [🌐](https://seaweedfs.org/) [🛠️](roles/web-app-seaweedfs/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) |
| [Self-Hosted Runner](roles/svc-runner/) | beta | Self-hosted CI runner infrastructure for executing GitHub Actions workflow jobs in ephemeral Docker containers | [🌐](https://github.com/actions/runner) [🛠️](roles/svc-runner/README.md#quick-setup) | |
| [Semaphore UI](roles/web-app-semaphore/) | beta | Web UI and API for running Ansible, Terraform/OpenTofu, and shell automation on a schedule or on demand | [🌐](https://semaphoreui.com/) [🎬](https://www.youtube.com/watch?v=nzR4yDuZQMU) [🛠️](roles/web-app-semaphore/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Shopware](roles/web-app-shopware/) | beta | Open-source ecommerce platform built on Symfony and Vue.js for building and scaling online storefronts | [🌐](https://www.shopware.com/en/) [🎬](https://www.youtube.com/watch?v=bG57TZPYsyw) [🛠️](roles/web-app-shopware/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Semaphore UI](roles/web-app-semaphore/) | beta | Web UI and API for running Ansible, Terraform/OpenTofu, and shell automation on a schedule or on demand | [🌐](https://semaphoreui.com/) [🎬](https://www.youtube.com/watch?v=nzR4yDuZQMU) [🛠️](roles/web-app-semaphore/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Shopware](roles/web-app-shopware/) | beta | Open-source ecommerce platform built on Symfony and Vue.js for building and scaling online storefronts | [🌐](https://www.shopware.com/en/) [🎬](https://www.youtube.com/watch?v=bG57TZPYsyw) [🛠️](roles/web-app-shopware/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Simple Icons](roles/web-svc-simpleicons/) | beta | Containerized icon server for hosting the Simple Icons brand SVG and PNG collection for web and documentation use | [🌐](https://simpleicons.org/) [🛠️](roles/web-svc-simpleicons/README.md#quick-setup) | [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Snipe‑IT](roles/web-app-snipe-it/) | beta | Open-source IT asset management system for tracking hardware and software inventory | [🌐](https://snipeitapp.com/) [🎬](https://www.youtube.com/watch?v=q0V6FAPc1ls) [🛠️](roles/web-app-snipe-it/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Social Home](roles/web-app-socialhome/) | alpha | Privacy-first social platform for households, federating peer-to-peer over WebRTC on an embedded SQLite store | [🌐](https://github.com/social-home-io/socialhome) [🛠️](roles/web-app-socialhome/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Sphinx](roles/web-app-sphinx/) | beta | Documentation generator that builds HTML from a source repository and serves it through a lightweight HTTP server | [🌐](https://www.sphinx-doc.org/) [🛠️](roles/web-app-sphinx/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Stalwart Mail Server](roles/web-app-stalwart/) | beta | All-in-one Stalwart mail server with SMTP, IMAP, JMAP, POP3, ManageSieve, CalDAV, CardDAV and built-in spam filtering | [🌐](https://stalw.art/) [🛠️](roles/web-app-stalwart/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Tor SMTP Gateway (svc-net-tor-smtp)](roles/svc-net-tor-smtp/), [Universal Logout](roles/web-svc-logout/) |
| [Snipe‑IT](roles/web-app-snipe-it/) | beta | Open-source IT asset management system for tracking hardware and software inventory | [🌐](https://snipeitapp.com/) [🎬](https://www.youtube.com/watch?v=q0V6FAPc1ls) [🛠️](roles/web-app-snipe-it/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Social Home](roles/web-app-socialhome/) | alpha | Privacy-first social platform for households, federating peer-to-peer over WebRTC on an embedded SQLite store | [🌐](https://github.com/social-home-io/socialhome) [🛠️](roles/web-app-socialhome/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Sphinx](roles/web-app-sphinx/) | beta | Documentation generator that builds HTML from a source repository and serves it through a lightweight HTTP server | [🌐](https://www.sphinx-doc.org/) [🛠️](roles/web-app-sphinx/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Stalwart Mail Server](roles/web-app-stalwart/) | beta | All-in-one Stalwart mail server with SMTP, IMAP, JMAP, POP3, ManageSieve, CalDAV, CardDAV and built-in spam filtering | [🌐](https://stalw.art/) [🛠️](roles/web-app-stalwart/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Tor SMTP Gateway (svc-net-tor-smtp)](roles/svc-net-tor-smtp/), [Universal Logout](roles/web-svc-logout/) |
| [Storage Optimizer](roles/svc-opt-ssd-hdd/) | beta | Tiering role placing Docker volumes on SSD or HDD by workload type, with symlinks keeping paths consistent | [🛠️](roles/svc-opt-ssd-hdd/README.md#quick-setup) | |
| [SuiteCRM](roles/web-app-suitecrm/) | beta | Open-source CRM platform for sales, service, cases, and campaign management with workflow automation | [🌐](https://suitecrm.com/) [🎬](https://www.youtube.com/watch?v=pOAs_C3Fp_E) [🛠️](roles/web-app-suitecrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [SuiteCRM](roles/web-app-suitecrm/) | beta | Open-source CRM platform for sales, service, cases, and campaign management with workflow automation | [🌐](https://suitecrm.com/) [🎬](https://www.youtube.com/watch?v=pOAs_C3Fp_E) [🛠️](roles/web-app-suitecrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Swapfile](roles/svc-opt-swapfile/) | beta | On-target automation for creating and activating swap space via a cloned swapfile script | [🛠️](roles/svc-opt-swapfile/README.md#quick-setup) | |
| [Taiga](roles/web-app-taiga/) | beta | Open-source agile project management platform for Scrum, Kanban, and hybrid team workflows | [🌐](https://taiga.io/) [🎬](https://www.youtube.com/watch?v=TYPX8nQEU5I) [🛠️](roles/web-app-taiga/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Taiga](roles/web-app-taiga/) | beta | Open-source agile project management platform for Scrum, Kanban, and hybrid team workflows | [🌐](https://taiga.io/) [🎬](https://www.youtube.com/watch?v=TYPX8nQEU5I) [🛠️](roles/web-app-taiga/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Tor Onion Service](roles/svc-net-tor/) | beta | Tor daemon publishing the node as a v3 onion service | [🛠️](roles/svc-net-tor/README.md#quick-setup) | [Asset Mirror (Privacy Proxy)](roles/web-svc-mirror/) |
| [Tor SMTP Gateway (svc-net-tor-smtp)](roles/svc-net-tor-smtp/) | alpha | SMTP gateway that relays .onion recipients through Tor's SOCKS listener | [🛠️](roles/svc-net-tor-smtp/README.md#quick-setup) | |
| [Typesense](roles/svc-db-typesense/) | beta | Central Dockerized search engine shared by multiple apps with per-consumer scoped API keys | [🌐](https://typesense.org/) [🛠️](roles/svc-db-typesense/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Unbound](roles/svc-dns-unbound/) | beta | Central recursive/forwarding DNSSEC resolver on a static IP for consumer stacks to target as their DNS | [🌐](https://nlnetlabs.nl/projects/unbound/about/) [🛠️](roles/svc-dns-unbound/README.md#quick-setup) | |
| [Universal Logout](roles/web-svc-logout/) | beta | Central logout proxy for single-action sign-out across all OIDC-connected apps, with Nginx snippets and a live status UI | [🛠️](roles/web-svc-logout/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [web-app-pihole](roles/web-app-pihole/) | beta | Network-wide DNS sinkhole for blocking ads and trackers across all devices, with DHCP and a web admin dashboard | [🌐](https://pi-hole.net/) [🎬](https://youtu.be/KBXTnrD_Zs4) [🛠️](roles/web-app-pihole/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Universal Logout](roles/web-svc-logout/) | beta | Central logout proxy for single-action sign-out across all OIDC-connected apps, with Nginx snippets and a live status UI | [🛠️](roles/web-svc-logout/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [web-app-pihole](roles/web-app-pihole/) | beta | Network-wide DNS sinkhole for blocking ads and trackers across all devices, with DHCP and a web admin dashboard | [🌐](https://pi-hole.net/) [🎬](https://youtu.be/KBXTnrD_Zs4) [🛠️](roles/web-app-pihole/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Wireguard](roles/svc-net-wireguard-core/) | alpha | Host-level WireGuard VPN service with wg-quick config, package install, and IP-forwarding sysctl for secure tunnels | [🌐](https://www.wireguard.com/) [🛠️](roles/svc-net-wireguard-core/README.md#quick-setup) | |
| [Wireguard Client](roles/svc-net-wireguard-plain/) | alpha | Client-side WireGuard VPN role for stable connectivity with a systemd service and script that tune interface MTU | [🌐](https://www.wireguard.com/) [🛠️](roles/svc-net-wireguard-plain/README.md#quick-setup) | |
| [WireGuard Client behind NAT](roles/svc-net-wireguard-firewalled/) | alpha | Networking helper for iptables forwarding and NAT masquerading of a WireGuard client running behind NAT or a firewall | [🛠️](roles/svc-net-wireguard-firewalled/README.md#quick-setup) | |
| [WordPress](roles/web-app-wordpress/) | beta | Content management system for building and managing websites, blogs, and multisite networks with themes and plugins | [🌐](https://wordpress.org/) [🎬](https://www.youtube.com/watch?v=QU8GFqvXRxE) [🛠️](roles/web-app-wordpress/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [WordPress](roles/web-app-wordpress/) | beta | Content management system for building and managing websites, blogs, and multisite networks with themes and plugins | [🌐](https://wordpress.org/) [🎬](https://www.youtube.com/watch?v=QU8GFqvXRxE) [🛠️](roles/web-app-wordpress/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [World Wide Web](roles/web-opt-rdr-www/) | beta | NGINX server blocks that permanently redirect www subdomains to their non-www apex domains | [🛠️](roles/web-opt-rdr-www/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [XMPP](roles/web-svc-xmpp/) | beta | Placeholder stub role for a future XMPP server/container deployment, implementation pending | [🌐](https://ejabberd.im/) [🛠️](roles/web-svc-xmpp/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Keycloak](roles/web-app-keycloak/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [XWiki](roles/web-app-xwiki/) | beta | Self-hosted enterprise wiki and knowledge-management platform with collaboration and app-based extensibility | [🌐](https://www.xwiki.org/) [🎬](https://www.youtube.com/watch?v=8DePx30dh2g) [🛠️](roles/web-app-xwiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [YOURLS](roles/web-app-yourls/) | beta | Self-hosted URL shortener for creating, tracking, and managing short links with built-in click analytics | [🌐](https://yourls.org/) [🛠️](roles/web-app-yourls/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Zammad](roles/web-app-zammad/) | beta | Open-source helpdesk and ticketing system for handling customer requests via email, chat, phone, and web | [🌐](https://zammad.org/) [🎬](https://www.youtube.com/watch?v=K30V0Uimr-s) [🛠️](roles/web-app-zammad/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Memcached](roles/svc-db-memcached/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [XWiki](roles/web-app-xwiki/) | beta | Self-hosted enterprise wiki and knowledge-management platform with collaboration and app-based extensibility | [🌐](https://www.xwiki.org/) [🎬](https://www.youtube.com/watch?v=8DePx30dh2g) [🛠️](roles/web-app-xwiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [YOURLS](roles/web-app-yourls/) | beta | Self-hosted URL shortener for creating, tracking, and managing short links with built-in click analytics | [🌐](https://yourls.org/) [🛠️](roles/web-app-yourls/README.md#quick-setup) | [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Zammad](roles/web-app-zammad/) | beta | Open-source helpdesk and ticketing system for handling customer requests via email, chat, phone, and web | [🌐](https://zammad.org/) [🎬](https://www.youtube.com/watch?v=K30V0Uimr-s) [🛠️](roles/web-app-zammad/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-design/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Memcached](roles/svc-db-memcached/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Stalwart Mail Server](roles/web-app-stalwart/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
## Guides 📚
+3
View File
@@ -38,6 +38,9 @@ alias i8cos='m cosmos'
alias i8cose='m compose-system-purge'
alias i8coso='m compose-stop'
alias i8coxe='m compose-exec'
alias i8dega='m design-gallery'
alias i8dequ='m design-queue'
alias i8desy='m design-sync'
alias i8di='m diagnose-disk-usage'
alias i8dine='m diagnose-network'
alias i8do='m dotenv'
@@ -134,7 +134,7 @@ flowchart TD
KC --> MDB["svc-db-mariadb"]
KC --> LDAP["svc-db-openldap"]
MAILU --> MDB
KC --> CSS["web-svc-css"]
KC --> CSS["web-svc-design"]
KC --> LOGOUT["web-svc-logout"]
MAT --> DASH["web-app-dashboard"]
CDN --> FILE["web-svc-file"]
@@ -31,7 +31,7 @@ FRAMEWORK = {
"prometheus": "prometheus",
"email": "mailu",
"dashboard": "dashboard",
"css": "css",
"design": "design",
"logout": "logout",
"cdn": "cdn",
"coturn": "coturn",
@@ -119,7 +119,7 @@ WEB_SVC = [
"cdn",
"collabora",
"coturn",
"css",
"design",
"file",
"html",
"legal",
+2 -2
View File
@@ -23,7 +23,7 @@ FRAMEWORK: dict[str, str] = {
"prometheus": "prometheus",
"email": "mailu",
"dashboard": "dashboard",
"css": "css",
"design": "design",
"logout": "logout",
"cdn": "cdn",
"coturn": "coturn",
@@ -108,7 +108,7 @@ WEB_SVC: list[str] = [
"cdn",
"collabora",
"coturn",
"css",
"design",
"file",
"html",
"legal",
+465
View File
@@ -0,0 +1,465 @@
"""Work queue of the corporate-design routine.
A role is a candidate when it is a ``web-app-`` or ``web-svc-`` role with a web
UI of its own and its lifecycle sits inside the tested envelope. Every
candidate carries exactly one state:
* ``review``: its design spec is untracked or modified and waits for approval.
* ``new``: HEAD carries no design spec for it.
* ``stale``: the image version or the shared design base moved since the last
commit of its design spec.
* ``current``: nothing moved since then.
``new`` and ``stale`` roles are due, in that order.
CLI: ``python -m cli.meta.roles.design --help``
"""
from __future__ import annotations
import re
import subprocess
from collections import defaultdict
from dataclasses import dataclass
from operator import attrgetter
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING
from utils.cache.yaml import load_yaml_str
from utils.roles.applications.services.registry import read_yaml_file
from utils.roles.entity.name import get_entity_name
from utils.roles.lifecycle import tested_lifecycles
from utils.roles.mapping import ROLE_FILE_META_SERVICES, ROLE_FILE_PLAYWRIGHT_SPEC
from utils.roles.meta_lookup import get_role_lifecycle
if TYPE_CHECKING:
from collections.abc import Callable
Gap = tuple[int, int, int]
SPEC_FILE = (
PurePosixPath(ROLE_FILE_PLAYWRIGHT_SPEC).with_name("test-design.js").as_posix()
)
BASE_PATHS: tuple[str, ...] = (
"roles/sys-front-inj-design/templates/css",
"utils/design/palette.py",
)
UI_PREFIXES: tuple[str, ...] = ("web-app-", "web-svc-")
UI_LESS_ROLES: frozenset[str] = frozenset(
{
"web-svc-asset",
"web-svc-cdn",
"web-svc-coturn",
"web-svc-design",
"web-svc-file",
"web-svc-simpleicons",
"web-svc-xmpp",
}
)
STATE_REVIEW = "review"
STATE_NEW = "new"
STATE_STALE = "stale"
STATE_CURRENT = "current"
NO_GAP: Gap = (0, 0, 0)
MINIMAL_GAP: Gap = (0, 0, 1)
_LEADING_NUMBER_RE = re.compile(r"v?(\d+(?:\.\d+)*)")
class GitError(RuntimeError):
"""Raised when git is missing or a git command fails."""
@dataclass(frozen=True)
class DesignCommit:
"""Last commit of a design spec: full hash and commit time in epoch seconds."""
sha: str
timestamp: int
@dataclass(frozen=True)
class RoleFacts:
"""Git and working-tree facts of one candidate role.
Args:
role: Role directory name.
created: Commit time of the oldest commit reachable from HEAD that
changed a file below ``roles/<role>``, ``None`` when no commit did.
The whole directory is read instead of the commit that added one
file, so a file that was removed and added again cannot reset it.
changed: Commit time of the newest such commit, ``None`` when none.
designed: Last commit that touched the design spec, ``None`` when HEAD
carries no design spec.
in_review: Whether the design spec exists in the working tree and is
untracked or has uncommitted changes.
base_changed: Whether a commit that ``designed`` does not contain
touched the shared design base. Ancestry decides instead of the
commit time, so a rebase that stamps both commits with the same
second cannot hide a base change.
designed_version: Image version of the primary entity at ``designed``.
current_version: Image version of the primary entity in the working
tree. Both versions are ``None`` when undeclared.
"""
role: str
created: int | None
changed: int | None
designed: DesignCommit | None
in_review: bool
base_changed: bool
designed_version: str | None
current_version: str | None
@property
def version_gap(self) -> Gap:
"""Distance the image version moved since the last design pass."""
if self.designed is None:
return NO_GAP
return version_gap(self.designed_version, self.current_version)
@dataclass(frozen=True)
class QueueEntry:
"""One row of the design queue.
Args:
rank: Position in the due queue, counted from 1, ``None`` when not due.
role: Role directory name.
state: ``new``, ``stale``, ``review`` or ``current``.
reason: Why the role carries that state.
version_gap: ``(major, minor, patch)`` distance of the image version.
created: ``RoleFacts.created`` in Unix epoch seconds.
changed: ``RoleFacts.changed`` in Unix epoch seconds.
"""
rank: int | None
role: str
state: str
reason: str
version_gap: Gap
created: int | None
changed: int | None
def _leading_number(version: str | None) -> Gap | None:
match = _LEADING_NUMBER_RE.match(version) if version else None
if match is None:
return None
parts = [int(part) for part in match.group(1).split(".")][:3]
major, minor, patch = (*parts, *([0] * (3 - len(parts))))
return (major, minor, patch)
def version_gap(designed: str | None, current: str | None) -> Gap:
"""Measure how far an image version moved.
Both versions lose a leading ``v`` and are read up to the end of their
leading dotted number, padded or cut to three components.
Args:
designed: Version at the last design pass, ``None`` when undeclared.
current: Version in the working tree, ``None`` when undeclared.
Returns:
The absolute ``(major, minor, patch)`` differences, ``(0, 0, 0)`` for
equal versions and ``(0, 0, 1)`` for versions that differ without a
numeric distance: one side is missing or not numeric, or the change
sits behind the third component.
"""
if designed == current:
return NO_GAP
old, new = _leading_number(designed), _leading_number(current)
if old is None or new is None:
return MINIMAL_GAP
major, minor, patch = (abs(a - b) for a, b in zip(old, new, strict=True))
gap = (major, minor, patch)
return gap if gap != NO_GAP else MINIMAL_GAP
def state_of(facts: RoleFacts) -> str:
"""Return the state of a candidate: review, new, stale or current."""
if facts.in_review:
return STATE_REVIEW
if facts.designed is None:
return STATE_NEW
if facts.base_changed or facts.version_gap != NO_GAP:
return STATE_STALE
return STATE_CURRENT
def _shown(version: str | None) -> str:
return "none" if version is None else version
def reason_of(facts: RoleFacts) -> str:
"""Return a short human-readable reason for the state of a candidate."""
state = state_of(facts)
if state == STATE_REVIEW:
return "design spec awaits approval"
if state == STATE_NEW:
return "never designed"
if state == STATE_CURRENT:
return "up to date"
reasons = []
if facts.version_gap != NO_GAP:
old, new = _shown(facts.designed_version), _shown(facts.current_version)
reasons.append(f"version {old} -> {new}")
if facts.base_changed:
reasons.append("design base changed")
return ", ".join(reasons)
def _newest_first(timestamp: int | None) -> tuple[bool, int]:
if timestamp is None:
return (True, 0)
return (False, -timestamp)
def _new_order(facts: RoleFacts) -> tuple[object, ...]:
return (_newest_first(facts.created), _newest_first(facts.changed), facts.role)
def _stale_order(facts: RoleFacts) -> tuple[object, ...]:
major, minor, patch = facts.version_gap
return ((-major, -minor, -patch), _newest_first(facts.changed), facts.role)
def _in_state(
facts: list[RoleFacts], state: str, order: Callable[[RoleFacts], object]
) -> list[RoleFacts]:
return sorted((item for item in facts if state_of(item) == state), key=order)
def _entry(facts: RoleFacts, rank: int | None) -> QueueEntry:
return QueueEntry(
rank=rank,
role=facts.role,
state=state_of(facts),
reason=reason_of(facts),
version_gap=facts.version_gap,
created=facts.created,
changed=facts.changed,
)
def build_queue(facts: list[RoleFacts]) -> list[QueueEntry]:
"""Order the roles that are due for a design pass.
Args:
facts: Facts of every candidate role.
Returns:
The ``new`` roles, newest first, followed by the ``stale`` roles,
largest version gap first. A tie falls to the most recently changed
role and then to the role name. A role no commit touched sorts behind
the committed ones. Ranks count from 1.
"""
due = [
*_in_state(facts, STATE_NEW, _new_order),
*_in_state(facts, STATE_STALE, _stale_order),
]
return [_entry(item, rank) for rank, item in enumerate(due, start=1)]
def not_due(facts: list[RoleFacts]) -> list[QueueEntry]:
"""List the ``review`` roles, then the ``current`` ones, without a rank.
Args:
facts: Facts of every candidate role.
"""
by_name = attrgetter("role")
idle = [
*_in_state(facts, STATE_REVIEW, by_name),
*_in_state(facts, STATE_CURRENT, by_name),
]
return [_entry(item, None) for item in idle]
def _git(root: Path, *args: str, stdin: str | None = None) -> str:
"""Run a read-only git command in a directory and return its output.
``--no-optional-locks`` keeps ``git status`` from taking ``index.lock``,
which would abort a commit that another process runs in the same tree.
Raises:
GitError: When the git binary is missing or the command exits non-zero.
"""
try:
result = subprocess.run(
["git", "--no-optional-locks", *args],
cwd=root,
input=stdin,
capture_output=True,
encoding="utf-8",
errors="replace",
check=False,
)
except OSError as exc:
raise GitError(f"cannot run git in {root}: {exc}") from exc
if result.returncode != 0:
raise GitError(f"git {args[0]} failed in {root}: {result.stderr.strip()}")
return result.stdout
def repository_root(start: Path) -> Path:
"""Resolve the root of the work tree that contains a directory.
Raises:
GitError: When git is unavailable or ``start`` is outside a work tree.
"""
return Path(_git(start, "rev-parse", "--show-toplevel").strip())
def _primary_version(services: object, role: str) -> str | None:
if not isinstance(services, dict):
return None
entry = services.get(get_entity_name(role))
if not isinstance(entry, dict) or entry.get("version") is None:
return None
return str(entry["version"]).strip() or None
def _is_candidate(role_dir: Path, envelope: frozenset[str]) -> bool:
role = role_dir.name
if not role.startswith(UI_PREFIXES) or role in UI_LESS_ROLES:
return False
return get_role_lifecycle(role_dir, role_name=role) in envelope
def candidate_roles(roles_dir: Path) -> list[str]:
"""Select the roles that take part in the design queue.
Args:
roles_dir: Directory holding the roles.
Returns:
Sorted names of the ``web-app-`` and ``web-svc-`` roles that have a web
UI of their own and carry a lifecycle inside the tested envelope. A
role without a ``design`` service entry is included: its design pass
adds the entry.
"""
envelope = tested_lifecycles()
return [
role_dir.name
for role_dir in sorted(roles_dir.iterdir())
if role_dir.is_dir() and _is_candidate(role_dir, envelope)
]
def _spec(role: str) -> str:
return f"roles/{role}/{SPEC_FILE}"
def _committed_specs(root: Path, roles: list[str]) -> set[str]:
if not roles:
return set()
specs = [_spec(role) for role in roles]
return set(_git(root, "ls-tree", "--name-only", "HEAD", "--", *specs).splitlines())
def _commit_times(root: Path) -> dict[str, list[int]]:
"""Map every role to the commit times of the commits that changed it.
``--full-history`` is load-bearing. The default history simplification
follows a single parent of each merge, and in a shallow clone that walk
ends at whichever boundary commit it reaches first: a boundary commit has
no parent, so it counts as adding every file it carries. Roles that rarely
change then report that boundary as their oldest commit and pass for the
newest roles. A merge commit lists no files and never counts. Git wraps a
path with unusual bytes in double quotes, hence the stripped leading quote.
"""
commits = _git(root, "rev-list", "--full-history", "HEAD", "--", "roles")
history = _git(
root,
"diff-tree",
"--stdin",
"--root",
"-r",
"--name-only",
"--format=%x00%ct",
"--",
"roles",
stdin=commits,
)
times: defaultdict[str, list[int]] = defaultdict(list)
for record in history.split("\0")[1:]:
commit_time, *paths = record.splitlines()
for role in {path.lstrip('"').split("/")[1] for path in paths if path}:
times[role].append(int(commit_time))
return dict(times)
def _designed(root: Path, spec: str) -> DesignCommit | None:
last = _git(root, "rev-list", "--timestamp", "-1", "HEAD", "--", spec).split()
if not last:
return None
timestamp, sha = last
return DesignCommit(sha=sha, timestamp=int(timestamp))
def _in_review(root: Path, spec: str) -> bool:
"""Tell whether the design spec is untracked or has uncommitted changes.
``--untracked-files=all`` overrides a ``status.showUntrackedFiles=no`` in
the user configuration, which would hide a spec that was never added.
"""
if not (root / spec).is_file():
return False
status = _git(root, "status", "--porcelain", "--untracked-files=all", "--", spec)
return bool(status.strip())
def _base_changed(root: Path, designed: DesignCommit) -> bool:
newer = _git(root, "rev-list", "-1", f"{designed.sha}..HEAD", "--", *BASE_PATHS)
return bool(newer.strip())
def _designed_version(root: Path, role: str, designed: DesignCommit) -> str | None:
services = f"roles/{role}/{ROLE_FILE_META_SERVICES}"
if not _git(root, "ls-tree", "--name-only", designed.sha, "--", services).strip():
return None
content = _git(root, "cat-file", "blob", f"{designed.sha}:{services}")
return _primary_version(load_yaml_str(content), role)
def _role_facts(
root: Path, role: str, times: list[int] | None, committed_specs: set[str]
) -> RoleFacts:
spec = _spec(role)
designed = _designed(root, spec) if spec in committed_specs else None
services = read_yaml_file(root / "roles" / role / ROLE_FILE_META_SERVICES)
return RoleFacts(
role=role,
created=min(times) if times else None,
changed=max(times) if times else None,
designed=designed,
in_review=_in_review(root, spec),
base_changed=designed is not None and _base_changed(root, designed),
designed_version=(
None if designed is None else _designed_version(root, role, designed)
),
current_version=_primary_version(services, role),
)
def collect_facts(repo_root: Path) -> list[RoleFacts]:
"""Collect the facts of every candidate role from git and the working tree.
Args:
repo_root: Root of the repository work tree.
Returns:
One entry per candidate role, sorted by role name.
Raises:
GitError: When git is unavailable or a git command fails.
"""
roles = candidate_roles(repo_root / "roles")
times = _commit_times(repo_root)
committed_specs = _committed_specs(repo_root, roles)
return [
_role_facts(repo_root, role, times.get(role), committed_specs) for role in roles
]
+123
View File
@@ -0,0 +1,123 @@
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from typing import TYPE_CHECKING
from cli import PROJECT_ROOT
from . import (
STATE_CURRENT,
STATE_REVIEW,
GitError,
build_queue,
collect_facts,
not_due,
repository_root,
)
if TYPE_CHECKING:
from . import QueueEntry
_COLUMNS = ("rank", "role", "state", "reason")
_DESCRIPTION = """\
Print the web UI roles that need a corporate-design pass, in working order.
states:
new HEAD carries no design spec for the role; newest role first
stale image version or shared design base moved since the last design
pass; largest version gap first, a base-only change last
review the design spec is uncommitted and awaits approval; not due
current nothing moved since the last design pass; not due
"""
def _build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="python -m cli.meta.roles.design",
description=_DESCRIPTION,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
scope = p.add_mutually_exclusive_group()
scope.add_argument(
"--next",
action="store_true",
help="Print only the role name of rank 1, nothing when no role is due.",
)
scope.add_argument(
"--all",
action="store_true",
help="Also list the review and current roles after the due queue.",
)
p.add_argument(
"--format",
choices=("text", "json"),
help="Output format, the aligned text table when omitted. json emits a "
"list of objects with rank, role, state, reason, version_gap, created "
"and changed; the last two are commit times in Unix epoch seconds.",
)
return p
def render_table(rows: list[QueueEntry]) -> str:
"""Render queue entries as an aligned text table.
Args:
rows: Entries in output order.
Returns:
The header line and one line per entry, columns padded to equal width.
"""
lines = [
_COLUMNS,
*(
("" if row.rank is None else str(row.rank), row.role, row.state, row.reason)
for row in rows
),
]
widths = [max(map(len, column)) for column in zip(*lines, strict=True)]
return "\n".join(
" ".join(
cell.ljust(width) for cell, width in zip(line, widths, strict=True)
).rstrip()
for line in lines
)
def _summary(queue: list[QueueEntry], idle: list[QueueEntry]) -> str:
review = sum(row.state == STATE_REVIEW for row in idle)
current = sum(row.state == STATE_CURRENT for row in idle)
return f"due: {len(queue)}, in review: {review}, current: {current}"
def main(argv: list[str] | None = None) -> int:
args = _build_parser().parse_args(argv)
try:
facts = collect_facts(repository_root(PROJECT_ROOT))
except GitError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
return 1
queue = build_queue(facts)
if args.next:
if queue:
print(queue[0].role)
return 0
idle = not_due(facts)
rows = [*queue, *idle] if args.all else queue
if args.format == "json":
print(json.dumps([asdict(row) for row in rows], indent=2))
return 0
print(render_table(rows))
print()
print(_summary(queue, idle))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -26,7 +26,7 @@ def _build_parser() -> argparse.ArgumentParser:
p.add_argument(
"--apps",
required=True,
help="Comma-separated deployed role ids (e.g. `web-app-yourls,web-svc-css`).",
help="Comma-separated deployed role ids (e.g. `web-app-yourls,web-svc-design`).",
)
return p
+1 -1
View File
@@ -1,4 +1,4 @@
# Agent action guides 🧭
This directory contains action-oriented runbooks for agent work.
Its scope is how agents should develop, debug, test, refactor, commit, and handle pull requests while staying aligned with repository policy.
Its scope is how agents should develop, debug, test, refactor, design, commit, and handle pull requests while staying aligned with repository policy.
+75
View File
@@ -0,0 +1,75 @@
# Design Loop
Use this page for bringing the web UI of a role into the corporate design and for the recurring design maintenance of all UI roles.
For the deploy mechanics see the [Compose Loop](iteration/compose.md), for spec iteration the [Playwright Spec Loop](iteration/playwright.md).
For tokens, the cascade layer and the role stylesheet contract see [style.css](../../contributing/artefact/files/role/style.css.md).
## When to use
- A role with a web UI has no design pass yet, or its app version or the shared design base moved since its last pass.
- A recurring agent maintains the design of every UI role.
## The queue
- `make design-queue` prints the roles that are due, in the order you MUST work them:
1. `new`: roles without a committed `files/playwright/test-design.js`, newest role first.
2. `stale`: roles whose image version moved since their last design pass, largest version gap first, followed by roles for which only the shared design base changed.
3. Ties: the most recently changed role first.
- `review` roles carry an uncommitted design spec. They wait for the operator and MUST NOT be picked again unless the operator rejected their gallery.
- `current` roles need no work.
- You MUST work exactly one role and run exactly one compose stack at a time.
## Cadence
- The loop iterates every 30 minutes. On each tick: read the queue, continue the role in progress, or start the first due role.
- A long-running deploy, gate or gallery run wakes the loop on completion. The 30 minute tick is the fallback.
- When nothing is due and no gallery awaits an answer, report the status matrix (approved, in review, rejected, due) and wait for the next tick.
## One role pass
1. Read `roles/<role>/AGENTS.md` when present. Role changes stay under `roles/<role>/`. A defect of the shared base goes into its own base commit.
2. Run `make quality-high` to green, then deploy against the running stack with `INFINITO_PLAYWRIGHT_KEEP=true make compose-deploy apps=<role> variant=0 disable=<every disableable service except design>`. Keep the default `initialize` mode, a single pass, no `full_cycle`, no teardown between roles.
3. Find the carrier of the app's colors in the live stack before you write CSS. Fetch the app's stylesheets through `make compose-exec cmd="curl -sk <url>"` and read its real variable names.
4. Apply the palette through the strongest carrier the app offers:
- **Theming mechanism** (config, CLI or API): feed the palette values server side, as [web-app-nextcloud](../../../roles/web-app-nextcloud/tasks/02_manager_ops/08_design.yml) does with `occ theming:config`. Ship no stylesheet.
- **CSS variable design system**: map the app's variables onto the tokens in `templates/style.css.j2`, as [web-app-gitea](../../../roles/web-app-gitea/templates/style.css.j2) does.
- **Bootstrap app**: the shared mapping applies. Replace the app's hard-coded colors in the role stylesheet, as [web-app-dashboard](../../../roles/web-app-dashboard/files/style.css) does.
- **No design of its own**: the shared element defaults apply. The role stylesheet only covers what stays hard-coded.
5. Set logo and title through the app's own mechanism from `lookup('design', application_id)`. Declare the slot sizes the app renders in the role's `design:` entry.
6. An app with its own theme switch ships `templates/design.js.j2` that mirrors the chosen theme into `data-design-theme` on `<html>`.
7. Iterate the stylesheet with `make design-sync app=<role> variant=0`. A changed `design.js.j2`, task or template needs `make compose-deploy mode=update apps=<role> variant=0 disable=<same list>`.
8. Write `files/playwright/test-design.js` and register it in `playwright.spec.js`:
- one test that calls `assertDesignTokens`,
- assertions that a core surface, the primary action and the body text take the palette colors (the tokens, or the values the app's theming mechanism received), and that logo and title are the configured ones,
- one gallery test with at least 20 views of the frontend and, where the app has one, the backend. Prefer settings, administration, forms, lists and detail pages. Log in where the app has accounts and seed showcase content idempotently where a list would be empty.
9. Capture with `make design-gallery app=<role>` and look at every screenshot in both modes and both viewports.
10. Run `make quality-high` to green.
11. Publish the gallery as one private Artifact for the role, link it under "Further Resources" in the role README and report the link. Do not wait for the answer; take the next role.
12. On approval stage only `roles/<role>/`, commit, and ask the operator to push. On rejection rework from the comment, run the gate again and republish to the same Artifact.
## Rules for the stylesheet
- Colors come from `--design-*` tokens only.
- Text and its background MUST come from the same source. A token color on an app-owned background, or the reverse, voids the contrast guarantee.
- Keep the app's contrast relations: frame against content, panel against page, selected against unselected. Flattening them is a regression.
- Hovered, pressed, open and selected controls use `--design-surface-hover`, `--design-surface-active` and `--design-<fill>-hover`, `--design-<fill>-active`.
- No gradients, no filter effects for state, no hard-coded hex values.
- Delete rules whose selectors match nothing in the deployed app.
## Review checklist for every screenshot
- Text, icons and control borders are readable against their background in light and in dark mode.
- Hover, focus, pressed, open and selected states are visible and readable.
- The view shows the state it is named after. A view that equals the start page is dead and MUST be replaced.
- No image is broken, no menu is clipped, nothing is caught in the middle of an animation.
- "Before" and "after" show the same page state.
## Traps
- **Measure, do not guess.** Add a temporary `diag:` test to the role spec, run it with `make compose-exec cmd="bash scripts/tests/e2e/rerun-spec.sh <role> --grep diag:"`, read the computed styles or console errors, then delete the test.
- **Animated apps.** `captureDesignGallery` waits up to 5 seconds for finite animations before each screenshot. An app that animates longer, or that swaps content after the animation, needs its own wait in the view's `prepare`.
- **Framework state colors.** Frameworks hard-code hover, active and disabled colors per button variant. Map every state variable, not only the resting one.
- **Legacy stylesheets.** A role stylesheet inherited from before the overhaul may assign variables the app never reads. Verify every mapped name against the app's real CSS.
- **`getComputedStyle` and visited links.** It reports the unvisited color. A link that looks different in a screenshot is not proven different by the computed value.
- **`design-sync` and scripts.** It renders static files only. An injected script keeps its deployed content because its CSP hash lives in the vhost.
- **Gallery links.** An Artifact whose sharing the operator changed can stop accepting updates from the session. Publish a new one, tell the operator that the old link stays on the old state, and update the README link.
+3 -3
View File
@@ -15,12 +15,12 @@ For iterating on GitHub Actions workflows with Act, see [Workflow Loop](workflow
- Before starting the loop, you MUST propose disabling all non-necessary services via the `disable=` make arg to reduce resource usage. In the typical case, this means keeping only the database and disabling everything else. Only proceed without this proposal if the user has already confirmed a full-stack setup.
- Non-essential provider toggle:
- WHEN: before first deploy of iteration.
- ACTION: ask user "disable matomo, dashboard, prometheus, email, css providers? [Y/n]".
- ACTION: ask user "disable matomo, dashboard, prometheus, email, design providers? [Y/n]".
- DEFAULT: yes (disable all five).
- SKIP ASK: only if user already answered explicitly in this iteration.
- ON YES: pass `disable="matomo,dashboard,prometheus,email,css"` verbatim to every deploy command. The value is a comma-separated list of provider keys, NOT a glob, NOT a `web-app-*.services.*` path.
- ON YES: pass `disable="matomo,dashboard,prometheus,email,design"` verbatim to every deploy command. The value is a comma-separated list of provider keys, NOT a glob, NOT a `web-app-*.services.*` path.
- ON NO: omit the arg entirely.
- SIDE EFFECT (yes): inventory initializer auto-removes the provider roles `web-app-matomo`, `web-app-dashboard`, `web-app-prometheus`, `web-app-mailu`, and `web-svc-css`. Do NOT list them in `apps=`.
- SIDE EFFECT (yes): inventory initializer auto-removes the provider roles `web-app-matomo`, `web-app-dashboard`, `web-app-prometheus`, `web-app-mailu`, and `web-svc-design`. Do NOT list them in `apps=`.
- PERSIST: record answer at top of iteration. Reuse for all subsequent deploys without re-asking.
- You MUST run `make test` before every deploy. Only proceed with the deploy if all tests pass.
- You MUST prepend `INFINITO_PLAYWRIGHT_KEEP=true` to every `make compose-deploy` command in the iteration (any mode), so trace, screenshot, and video of passing Playwright tests stay inspectable. Omit only when the user has explicitly opted out of per-test artefacts. For the full propagation chain see [Playwright Tests](../../../contributing/actions/testing/playwright.md#artefact-retention-).
+18 -11
View File
@@ -11,18 +11,22 @@ For repository wiring, inventory keys, and the generated palette contract, see [
- The result MUST feel like one coherent brand theme, not a collection of unrelated overrides.
- The result MUST be visually appealing and pleasant for humans to look at.
## Color Categories
## Tokens
- You MUST use `--color-01-*` and `--color-rgb-01-*` as the primary palette. They MUST cover the majority of themed surfaces.
- Semantic state colors such as warnings, errors, and success indicators SHOULD keep their original application colors. You MUST add a contrast border in `--color-01-00` or `--color-01-99` around them so they remain visually integrated.
- Categories `02`–`09` MAY be used but are NOT RECOMMENDED. Prefer `--color-01-*` for all surfaces where possible.
- If the application's primary background is dark or black, you MUST use `--color-01-00` as the default theme color. If it is light or white, you MUST use `--color-01-99`. All other application colors MUST be mapped to one of these two anchors. Colors from categories `02`–`09` MUST be derived from and kept in visual relation to the chosen anchor.
- You MUST use the semantic `--design-*` tokens listed in [Contributing `style.css`](../../../contributing/artefact/files/role/style.css.md#design-tokens-) for every color. Hard-coded colors are forbidden.
- You MUST prefer semantic tokens (`--design-surface-*`, `--design-text*`, `--design-border*`, `--design-primary`, `--design-link`, status tokens) over the `--design-primary-*` and `--design-neutral-*` scales. Use a scale step only for brand accents a semantic token does not cover.
- You MUST map the app's status colors onto `--design-success`, `--design-warning`, `--design-danger` and `--design-info`, and the text on filled status surfaces onto the matching `--design-on-*` token.
- You MUST NOT add your own `prefers-color-scheme` block for colors. The tokens switch with the mode.
- If the app has its own theme switch, you MUST mirror its state onto `<html data-design-theme="light|dark">` from `templates/design.js.j2` so the tokens follow the app, not only the browser.
## Replace
- You MUST replace hard-coded theme values: colors, RGB channels, gradients, borders, shadows, overlays, placeholder and focus colors.
- You MUST map `--color-01-*` and `--color-rgb-01-*` onto existing framework or app variables first. Add selector-level overrides only when variable mapping is insufficient.
- You MUST map `--design-*` tokens onto existing framework or app variables first. Add selector-level overrides only when variable mapping is insufficient.
- You MUST replace related values together: surface color, text color, border, hover, active, disabled, and alpha variants.
- A text color and its background MUST come from the same source. A token color on an app-owned background, or the reverse, voids the contrast guarantee.
- You MUST map hovered, pressed, open and selected controls onto `--design-surface-hover`, `--design-surface-active` and `--design-<fill>-hover`, `--design-<fill>-active`.
- You MUST verify every variable name you assign against the app's real stylesheet in the running stack. A mapping onto a name the app never reads has no effect.
## Do Not Replace
@@ -32,14 +36,17 @@ For repository wiring, inventory keys, and the generated palette contract, see [
## Workflow
1. Preserve the role's base CSS so the application styling stays intact.
2. Inspect which variables the role or framework already exposes (semantic tokens, palette variables, custom properties).
3. Map the repository palette onto those variables. Add selector overrides only for surfaces that cannot be reached through variables.
4. Keep gradients, transparency, text contrast, and surface hierarchy aligned.
1. Preserve the role's base CSS so the application styling stays intact, including its contrast relations (frame against content, panel against page, selected against unselected).
2. When the app has its own theming mechanism (config, CLI or API), feed the palette through it and ship no stylesheet.
3. Otherwise inspect which variables the role or framework already exposes (semantic tokens, palette variables, custom properties).
4. Map the design tokens onto those variables. Add selector overrides only for surfaces that cannot be reached through variables.
5. Keep transparency, text contrast, and surface hierarchy aligned. Replace upstream gradients with flat token surfaces.
The full role pass, the review gallery and the work queue are described in the [Design Loop](../../action/design.md).
## Review
- You MUST verify that primary actions, links, forms, cards, dialogs, navigation, and overlays still belong to the same visual system.
- You MUST verify readable contrast for normal, hover, focus, active, and disabled states.
- You MUST inspect the running application, at minimum the start page and login page.
- You MUST inspect the running application in light and dark mode, at minimum the start page and login page, and the backend (admin surface) when the app has one.
- If the change affects user-visible behavior, you MUST add or update the matching end-to-end coverage in [Role `playwright.spec.js`](playwright.spec.js.md).
@@ -35,7 +35,7 @@ Ansible forks a fresh worker process for every task. The in-process cache inside
The constructor stage renders once through the [applications_carrier](../../../../../../plugins/action/applications_carrier.py) action and parks the result together with its cache key in the host fact `_INFINITO_APPLICATIONS_RENDERED` (the action calls `lookup('applications', carrier=True)`). Every later worker inherits that fact at fork time; `get_merged_applications` serves it whenever the key still matches and renders only when it does not (a changed `applications`, `users`, `DOMAIN_PRIMARY` or `SYSTEM_EMAIL_DOMAIN`).
- The task sits in `tasks/stages/01_constructor.yml` after the `networks`, `design` and `service_provider` merges, after the `add_host` placements and after the token store, so the payload renders with the same `group_names`, `groups` and user records a role would see. Moving it earlier bakes stale values into the play.
- The task sits in `tasks/stages/01_constructor.yml` after the `networks` and `service_provider` merges, after the `add_host` placements and after the token store, so the payload renders with the same `group_names`, `groups` and user records a role would see. Moving it earlier bakes stale values into the play.
- The action masks its own result: the fact is the whole rendered tree with every application's credentials, so it never reaches a log regardless of `MASK_CREDENTIALS_IN_LOGS`.
- User records inside the payload are the token store's state at render time. Readers of tokens captured later in the play MUST go through `lookup('users', ...)` or `lookup('config', <app>, 'users.<key>...')`, both of which read the users cache fresh.
- The carrier is private to the lookup. Consumers MUST NOT read `_INFINITO_APPLICATIONS_RENDERED`; they keep going through `lookup('applications')`, which is what makes the carrier transparent.
@@ -1,25 +1,55 @@
# `style.css` 🎨
This page covers role-local CSS and theming.
Use this page for repository wiring, inventory keys, and palette mapping.
Use this page for repository wiring, inventory keys, and the design token contract.
For implementation scope, override strategy, and live review, see [Agent `style.css`](../../../../agents/files/role/style.css.md).
For browser-side validation requirements after visible UI changes, see [Playwright Tests](../../../actions/testing/playwright.md).
## `style.css.j2` ⚙️
- `roles/web-app-taiga/templates/style.css.j2` is the Taiga role override.
- `sys-front-inj-css` renders it when `services.css.enabled` is enabled.
- The template maps the generated palette to semantic variables such as `--color-01-solid-primary`, `--color-01-link-primary`, grayscale steps, and `--color-rgb-01-*`.
- `CSS_GRADIENT_ANGLE` keeps the gradients consistent across surfaces.
- A role ships its override as `templates/style.css.j2` or `files/style.css`.
- `sys-front-inj-design` renders it when `services.design.enabled` is enabled.
- The shared element styles (`body`, links, buttons, form fields, tables, focus outline) sit in the cascade layer `infinito-design`. Every unlayered rule of the app wins over them, so they only style what the app leaves unstyled.
- An app with its own design system gets the corporate palette by mapping its CSS variables onto the tokens in the role stylesheet (see [web-app-gitea](../../../../../roles/web-app-gitea/templates/style.css.j2)), or through its own theming mechanism (Nextcloud takes the primary color, logo, name and slogan via `occ theming:config` and ships no stylesheet).
- The Bootstrap mapping in `bootstrap.css` is unlayered and applies to every Bootstrap app.
- A role MAY ship `templates/design.js.j2`. `sys-front-inj-design` collapses it, adds its CSP hash and injects it whenever `services.design.enabled` is enabled, independent of `services.javascript.enabled`.
## Inventory 📋
- You MUST set `design.css.colors.base` in the inventory as the single base color.
- You MUST keep `services.css.enabled` enabled in the role configuration or override it in the inventory when CSS injection should run.
- You SHOULD add `design.font.import_url` when typography and colors should be controlled together.
- You MUST set `applications.web-svc-design.services.design.colors.base` in the inventory as the single base color.
- You MUST keep `services.design.enabled` enabled in the role configuration or override it in the inventory when CSS injection should run.
- You MAY set `applications.web-svc-design.services.design.font.type` and `font.import_url` to change the typography.
## Color Logic 🎨
## Design Tokens 🎨
- The palette is generated from a base color through `colorscheme-generator`.
- Role-local CSS MAY use the full generated palette space: all color categories `01` through `07` and all brightness values `00` through `99`.
- You MUST combine the matching `--color-XX-*` and `--color-rgb-XX-*` tokens in a way that is both meaningful and aesthetically coherent so semantic mappings, surfaces, gradients, borders, and overlays work together as one design system.
The [design_palette](../../../../../plugins/lookup/design_palette.py) lookup derives every token from the base color in OKLCH and guarantees WCAG 2.2 contrast by construction.
| Token | Use |
|---|---|
| `--design-surface-1` | Page background |
| `--design-surface-2` | Cards, panels, inputs |
| `--design-surface-3` | Hover, selected and inset areas, table heads |
| `--design-surface-hover`, `--design-surface-active` | Hovered and pressed or open neutral controls; body text stays AAA and muted text AA on both |
| `--design-text` | Body text, AAA against every surface |
| `--design-text-muted` | Secondary text, AA against every surface |
| `--design-border` | Decorative dividers |
| `--design-border-strong` | Input and control boundaries, 3:1 against surfaces |
| `--design-primary` / `--design-on-primary` | Brand surfaces and the text on them |
| `--design-link` | Links and focus, AA against surfaces |
| `--design-success`, `--design-warning`, `--design-danger`, `--design-info` | Status text and icons, AA against surfaces |
| `--design-on-<status>` | Text on a filled status surface |
| `--design-primary-hover`, `--design-primary-active`, `--design-<status>-hover`, `--design-<status>-active` | Hovered and pressed filled controls, shifted away from their on-color so its contrast only grows |
| `--design-<status>-subtle`, `--design-<status>-border` | Alert backgrounds and borders |
| `--design-primary-50` to `--design-primary-950` | Brand scale |
| `--design-neutral-50` to `--design-neutral-950` | Tinted neutral scale |
| `--design-radius-sm`, `--design-radius`, `--design-radius-lg` | Corner radii |
| `--design-space-1` to `--design-space-6` | 4/8 px spacing steps |
| `--design-shadow-1` to `--design-shadow-3` | Elevation |
| `--design-focus-ring` | Focus indicator for Bootstrap buttons and nav links |
| `--design-focus-ring-inset` | Focus indicator for Bootstrap form fields, drawn inside the field so it never overlaps neighbours |
| `--design-invert` | `0` in light and `1` in dark mode. Use `filter: brightness(0) invert(var(--design-invert))` to render a monochrome image in the text tone |
- Every color token also exists as `<token>-rgb` for `rgba()` use.
- Scale step `50` sits next to the surface and `950` next to the text in light and dark mode, so a mapping onto the scale follows dark mode without a media query.
- Dark mode follows `prefers-color-scheme`. A role whose app has its own theme switch sets `data-design-theme="light"` or `data-design-theme="dark"` on `<html>` from its `design.js.j2`; the attribute wins over the browser preference.
- Role CSS MUST use only `--design-*` tokens for colors. Hard-coded colors break the base color override.
@@ -23,6 +23,7 @@ from its procedure file. Run `make install-skills` to (re)install them.
| Iterating on `svc-runner` or the self-hosted runner infrastructure | `i8-iterate-runner` |
| Iterating on a GitHub Actions workflow | `i8-iterate-workflow` |
| Writing or updating a Playwright spec for a `web-*` role | `i8-iterate-playwright` |
| Bringing UI roles into the corporate design, role by role from the design queue | `i8-designer` |
| Running or validating tests for a specific scope | `i8-test` |
| Running the quality gate (`make quality`) to green | `i8-quality` |
| Running the full gate (`make quality-high`: quality plus every lint) to green | `i8-quality-high` |
+11 -11
View File
@@ -25,7 +25,7 @@ Notes:
## Matrix
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **akaunting** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | ❌ | ❌ | **akaunting** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **akaunting** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **akaunting** | [☑️](../../roles/web-app-akaunting/meta/services.yml) | ❌ | ❌ | ❌ |
| **baserow** | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-baserow/meta/services.yml) | ❌ | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [🪙](https://baserow.io/user-docs/configure-openid-connect-for-oauth-2-sso) | ❌ | ❌ | ❌ | **baserow** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-baserow/meta/services.yml) | ❌ | [☑️](../../roles/web-app-baserow/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-baserow/meta/services.yml) | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **baserow** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-baserow/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **baserow** | [☑️](../../roles/web-app-baserow/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -37,7 +37,7 @@ Notes:
| **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | ❌ | ❌ | **confluence** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-confluence/meta/services.yml) | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **confluence** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **confluence** | [☑️](../../roles/web-app-confluence/meta/services.yml) | ❌ | ❌ | ❌ |
| **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-dashboard/meta/services.yml) | ❌ | ❌ | ❌ | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-dashboard/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-dashboard/meta/services.yml) | **dashboard** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **dashboard** | ❌ | ❌ | [☑️](../../roles/web-app-dashboard/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-dashboard/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **dashboard** | [☑️](../../roles/web-app-dashboard/meta/services.yml) | ❌ | ❌ | ❌ |
| **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | — | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | ❌ | ❌ | ❌ | **decidim** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **decidim** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-decidim/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **decidim** | [☑️](../../roles/web-app-decidim/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **discourse** | ❌ | ❌ | [✅](https://github.com/discourse/discourse-bbb) | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-discourse/meta/services.yml) | ❌ | **discourse** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **discourse** | ❌ | ❌ | ❌ | ❌ | [✅](https://github.com/discourse/discourse-jitsi) | ❌ | [✅](https://github.com/discourse/discourse-openid-connect) | ❌ | ❌ | ❌ | **discourse** | [☑️](../../roles/web-app-discourse/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-discourse/meta/services.yml) | [✅](https://github.com/discourse/discourse-activity-pub) | [✅](https://github.com/discourse/discourse-matomo-analytics) | [✅](https://github.com/discourse/discourse-chat-integration) | [✅](https://github.com/discourse/discourse-chat-integration) | ❌ | ❌ | **discourse** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **discourse** | ❌ | [✅](https://github.com/discourse/discourse-ai) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://github.com/discourse/discourse-prometheus) | **discourse** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **discourse** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-discourse/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **discourse** | [☑️](../../roles/web-app-discourse/meta/services.yml) | ❌ | ❌ | ❌ |
| **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-erpnext/meta/services.yml) | ❌ | **erpnext** | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://docs.frappe.io/framework/user/en/guides/integration/openid_connect_and_frappe_social_login) | ❌ | ❌ | ❌ | **erpnext** | ❌ | ❌ | ❌ | [✅](https://docs.frappe.io/erpnext/user/manual/en/setting-up-email-account) | ❌ | [☑️](../../roles/web-app-erpnext/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-erpnext/meta/services.yml) | **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **erpnext** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-erpnext/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **erpnext** | [☑️](../../roles/web-app-erpnext/meta/services.yml) | ❌ | ❌ | ❌ |
| **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-espocrm/meta/services.yml) | ❌ | **espocrm** | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://docs.espocrm.com/administration/oidc/) | ❌ | ❌ | ❌ | **espocrm** | ❌ | ❌ | ❌ | [✅](https://docs.espocrm.com/user-guide/imap-smtp-configuration/) | ❌ | [☑️](../../roles/web-app-espocrm/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-espocrm/meta/services.yml) | **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **espocrm** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-espocrm/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **espocrm** | [☑️](../../roles/web-app-espocrm/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -48,7 +48,7 @@ Notes:
| **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | ❌ | ❌ | **funkwhale** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **funkwhale** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **funkwhale** | [☑️](../../roles/web-app-funkwhale/meta/services.yml) | ❌ | ❌ | ❌ |
| **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | ❌ | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **fusiondirectory** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **fusiondirectory** | [☑️](../../roles/web-app-fusiondirectory/meta/services.yml) | ❌ | ❌ | ❌ |
| **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | ❌ | ❌ | **gitea** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **gitea** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **gitea** | [☑️](../../roles/web-app-gitea/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://docs.gitlab.com/user/project/integrations/confluence/) | [☑️](../../roles/web-app-gitlab/meta/services.yml) | ❌ | **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **gitlab** | — | ❌ | [✅](https://docs.gitlab.com/integration/jenkins/) | [✅](https://docs.gitlab.com/integration/jira/) | ❌ | ❌ | [✅](https://docs.gitlab.com/integration/saml/) | ❌ | ❌ | ❌ | **gitlab** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitlab/meta/services.yml) | ❌ | [☑️](../../roles/web-app-gitlab/meta/services.yml) | [✅](https://docs.gitlab.com/user/project/integrations/matrix/) | [✅](https://docs.gitlab.com/user/project/integrations/mattermost_notifications/) | ❌ | ❌ | **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitlab/meta/services.yml) | **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **gitlab** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-gitlab/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **gitlab** | [☑️](../../roles/web-app-gitlab/meta/services.yml) | ❌ | ❌ | ❌ |
| **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **hugo** | ❌ | — | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | ❌ | ❌ | **hugo** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **hugo** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **hugo** | [☑️](../../roles/web-app-hugo/meta/services.yml) | ❌ | ❌ | ❌ |
| **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-jenkins/meta/services.yml) | ❌ | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://plugins.jenkins.io/gitea/) | **jenkins** | [✅](https://plugins.jenkins.io/gitlab-plugin) | ❌ | — | ❌ | ❌ | ❌ | [✅](https://plugins.jenkins.io/oic-auth/) | ❌ | ❌ | ❌ | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-jenkins/meta/services.yml) | ❌ | [✅](https://plugins.jenkins.io/mattermost/) | ❌ | ❌ | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://plugins.jenkins.io/prometheus) | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **jenkins** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-jenkins/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **jenkins** | [☑️](../../roles/web-app-jenkins/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -59,7 +59,7 @@ Notes:
| **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | ❌ | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | — | ❌ | ❌ | **kix** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **kix** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-kix/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **kix** | [☑️](../../roles/web-app-kix/meta/services.yml) | ❌ | ❌ | ❌ |
| **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-lam/meta/services.yml) | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-lam/meta/services.yml) | ❌ | — | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-lam/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-lam/meta/services.yml) | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **lam** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-lam/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **lam** | [☑️](../../roles/web-app-lam/meta/services.yml) | ❌ | ❌ | ❌ |
| **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | ❌ | — | **listmonk** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **listmonk** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **listmonk** | [☑️](../../roles/web-app-listmonk/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-litellm/meta/services.yml) | ❌ | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-litellm/meta/services.yml) | ❌ | ❌ | ❌ | **litellm** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-litellm/meta/services.yml) | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **litellm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **litellm** | [☑️](../../roles/web-app-litellm/meta/services.yml) | ❌ | ❌ | ❌ |
| **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | ❌ | ❌ | **littlejs** | ❌ | — | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **littlejs** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **littlejs** | [☑️](../../roles/web-app-littlejs/meta/services.yml) | ❌ | ❌ | ❌ |
| **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | ❌ | ❌ | **magento** | ❌ | ❌ | — | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-magento/meta/services.yml) | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **magento** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **magento** | [☑️](../../roles/web-app-magento/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -70,7 +70,7 @@ Notes:
| **mattermost** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | **mattermost** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mattermost** | [✅](https://github.com/mattermost/mattermost-plugin-gitlab) | ❌ | [✅](https://github.com/mattermost/mattermost-plugin-jenkins) | [✅](https://github.com/mattermost/mattermost-plugin-jira) | [✅](https://github.com/mattermost-community/mattermost-plugin-jitsi) | ❌ | [🪙](https://docs.mattermost.com/administration-guide/onboard/sso-saml.html) | ❌ | ❌ | ❌ | **mattermost** | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | — | ❌ | ❌ | **mattermost** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mattermost** | ❌ | [✅](https://github.com/mattermost/mattermost-plugin-agents) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://github.com/cpanato/mattermost-plugin-alertmanager) | **mattermost** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mattermost** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mattermost** | [☑️](../../roles/web-app-mattermost/meta/services.yml) | ❌ | ❌ | ❌ |
| **mediawiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | ❌ | **mediawiki** | [✅](https://www.mediawiki.org/wiki/Extension:DiscourseSsoConsumer) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mediawiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://www.mediawiki.org/wiki/Extension:OpenID_Connect) | ❌ | ❌ | ❌ | **mediawiki** | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | ❌ | [✅](https://www.mediawiki.org/wiki/Extension:Matomo) | ❌ | ❌ | — | ❌ | **mediawiki** | [✅](https://www.mediawiki.org/wiki/Extension:AWS) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mediawiki** | ❌ | [✅](https://www.mediawiki.org/wiki/Extension:AIEditingAssistant) | [✅](https://www.mediawiki.org/wiki/Extension:PeerTubeEmbed) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | **mediawiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mediawiki** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | ❌ | ❌ | ❌ | [✅](https://www.mediawiki.org/wiki/Extension:MachineTranslation) | **mediawiki** | [☑️](../../roles/web-app-mediawiki/meta/services.yml) | ❌ | ❌ | ❌ |
| **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mig/meta/services.yml) | ❌ | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mig/meta/services.yml) | ❌ | ❌ | ❌ | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mig/meta/services.yml) | ❌ | ❌ | ❌ | — | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mig/meta/services.yml) | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mig** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mig/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mig** | [☑️](../../roles/web-app-mig/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-minio/meta/services.yml) | ❌ | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-minio/meta/services.yml) | ❌ | ❌ | ❌ | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-minio/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **minio** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-minio/meta/services.yml) | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **minio** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-minio/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **minio** | [☑️](../../roles/web-app-minio/meta/services.yml) | ❌ | ❌ | ❌ |
| **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | ❌ | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | ❌ | ❌ | ❌ | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mini-qr** | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mini-qr** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mini-qr** | [☑️](../../roles/web-app-mini-qr/meta/services.yml) | ❌ | ❌ | ❌ |
| **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | ❌ | ❌ | **mobilizon** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mobilizon** | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **mobilizon** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **mobilizon** | [☑️](../../roles/web-app-mobilizon/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -81,7 +81,7 @@ Notes:
| **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-odoo/meta/services.yml) | ❌ | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://github.com/OCA/server-auth/tree/16.0/auth_oidc) | ❌ | ❌ | ❌ | **odoo** | ❌ | ❌ | ❌ | [✅](https://www.odoo.com/documentation/latest/applications/general/email_communication.html) | ❌ | [☑️](../../roles/web-app-odoo/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [🪙](https://apps.odoo.com/apps/modules/19.0/nextcloud_odoo_integration) | — | ❌ | ❌ | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-odoo/meta/services.yml) | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **odoo** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-odoo/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **odoo** | [☑️](../../roles/web-app-odoo/meta/services.yml) | ❌ | ❌ | ❌ |
| **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | ❌ | ❌ | **opencloud** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **opencloud** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **opencloud** | [☑️](../../roles/web-app-opencloud/meta/services.yml) | ❌ | ❌ | ❌ |
| **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openproject/meta/services.yml) | ❌ | **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **openproject** | [✅](https://www.openproject.org/docs/system-admin-guide/integrations/gitlab-integration/) | ❌ | ❌ | ❌ | ❌ | ❌ | [🪙](https://www.openproject.org/docs/system-admin-guide/authentication/openid-providers/) | ❌ | ❌ | ❌ | **openproject** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openproject/meta/services.yml) | ❌ | [☑️](../../roles/web-app-openproject/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/) | ❌ | ❌ | — | **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openproject/meta/services.yml) | **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **openproject** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openproject/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **openproject** | [☑️](../../roles/web-app-openproject/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **opentalk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | **opentalk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **opentalk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | ❌ | ❌ | **opentalk** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **opentalk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **opentalk** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | **opentalk** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **opentalk** | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-opentalk/meta/services.yml) | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **opentalk** | [☑️](../../roles/web-app-opentalk/meta/services.yml) | ❌ | ❌ | ❌ |
| **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | ❌ | ❌ | **openwebui** | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **openwebui** | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **openwebui** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **openwebui** | [☑️](../../roles/web-app-openwebui/meta/services.yml) | ❌ | ❌ | ❌ |
| **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-peertube/meta/services.yml) | ❌ | **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://www.npmjs.com/package/peertube-plugin-auth-openid-connect) | ❌ | ❌ | ❌ | **peertube** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-peertube/meta/services.yml) | ❌ | [☑️](../../roles/web-app-peertube/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **peertube** | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-peertube/meta/services.yml) | **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **peertube** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-peertube/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **peertube** | [☑️](../../roles/web-app-peertube/meta/services.yml) | ❌ | ❌ | ❌ |
@@ -92,29 +92,29 @@ Notes:
| **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | ❌ | ❌ | **postmarks** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **postmarks** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **postmarks** | [☑️](../../roles/web-app-postmarks/meta/services.yml) | ❌ | ❌ | ❌ |
| **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-pretix/meta/services.yml) | ❌ | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://docs.pretix.eu/en/latest/admin/installation/index.html) | ❌ | ❌ | ❌ | **pretix** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-pretix/meta/services.yml) | ❌ | [☑️](../../roles/web-app-pretix/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | [☑️](../../roles/web-app-pretix/meta/services.yml) | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **pretix** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-pretix/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **pretix** | [☑️](../../roles/web-app-pretix/meta/services.yml) | ❌ | ❌ | ❌ |
| **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | ❌ | ❌ | **prometheus** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **prometheus** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **prometheus** | [☑️](../../roles/web-app-prometheus/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | ❌ | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | **roulette-wheel** | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **roulette-wheel** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **roulette-wheel** | [☑️](../../roles/web-app-roulette-wheel/meta/services.yml) | ❌ | ❌ | ❌ |
| **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | ❌ | ❌ | **shopware** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | **shopware** | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **shopware** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **shopware** | [☑️](../../roles/web-app-shopware/meta/services.yml) | ❌ | ❌ | ❌ |
| **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | ❌ | ❌ | **snipe-it** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | **snipe-it** | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **snipe-it** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **snipe-it** | [☑️](../../roles/web-app-snipe-it/meta/services.yml) | ❌ | ❌ | ❌ |
| **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | **socialhome** | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ |
| **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | **socialhome** | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **socialhome** | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-socialhome/meta/services.yml) | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **socialhome** | [☑️](../../roles/web-app-socialhome/meta/services.yml) | ❌ | ❌ | ❌ |
| **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | ❌ | ❌ | **sphinx** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | **sphinx** | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | **sphinx** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **sphinx** | [☑️](../../roles/web-app-sphinx/meta/services.yml) | ❌ | ❌ | ❌ |
| **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-suitecrm/meta/services.yml) | ❌ | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://docs.suitecrm.com/8.x/admin/configuration/saml/8.7.0-saml-configuration/) | ❌ | ❌ | ❌ | **suitecrm** | ❌ | ❌ | ❌ | [✅](https://github.com/SuiteCRM/SuiteCRM) | ❌ | [☑️](../../roles/web-app-suitecrm/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-suitecrm/meta/services.yml) | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | **suitecrm** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-suitecrm/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **suitecrm** | [☑️](../../roles/web-app-suitecrm/meta/services.yml) | ❌ | ❌ | ❌ |
| **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-taiga/meta/services.yml) | ❌ | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://github.com/taigaio/taiga-contrib-oidc-auth) | ❌ | ❌ | ❌ | **taiga** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-taiga/meta/services.yml) | ❌ | [☑️](../../roles/web-app-taiga/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-taiga/meta/services.yml) | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | **taiga** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-taiga/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **taiga** | [☑️](../../roles/web-app-taiga/meta/services.yml) | ❌ | ❌ | ❌ |
| **wordpress** | ❌ | ❌ | [✅](https://wordpress.org/plugins/video-conferencing-with-bbb/) | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-wordpress/meta/services.yml) | ❌ | **wordpress** | [✅](https://wordpress.org/plugins/wp-discourse/) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **wordpress** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://wordpress.org/plugins/daggerhart-openid-connect-generic/) | ❌ | ❌ | [✅](https://wordpress.org/plugins/integration-for-listmonk-mailing-list-and-newsletter-manager/) | **wordpress** | [☑️](../../roles/web-app-wordpress/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-wordpress/meta/services.yml) | [✅](https://wordpress.org/plugins/activitypub/) | [✅](https://wordpress.org/plugins/matomo/) | ❌ | ❌ | ❌ | ❌ | **wordpress** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **wordpress** | ❌ | ❌ | [✅](https://wordpress.org/plugins/video-manager-for-peertube/) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-wordpress/meta/services.yml) | **wordpress** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | **wordpress** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-wordpress/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **wordpress** | [☑️](../../roles/web-app-wordpress/meta/services.yml) | ❌ | ❌ | ❌ |
| **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-xwiki/meta/services.yml) | ❌ | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://extensions.xwiki.org/xwiki/bin/view/Extension/OpenID%20Connect/) | ❌ | ❌ | ❌ | **xwiki** | [☑️](../../roles/web-app-xwiki/meta/services.yml) | ❌ | ❌ | [☑️](../../roles/web-app-xwiki/meta/services.yml) | ❌ | [✅](https://extensions.xwiki.org/xwiki/bin/view/Extension/Piwiki%20Integration) | ❌ | ❌ | ❌ | ❌ | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-xwiki/meta/services.yml) | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | **xwiki** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-xwiki/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **xwiki** | [☑️](../../roles/web-app-xwiki/meta/services.yml) | ❌ | ❌ | ❌ |
| **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | ❌ | ❌ | **yourls** | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | **yourls** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **yourls** | [☑️](../../roles/web-app-yourls/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-zammad/meta/services.yml) | ❌ | **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [✅](https://admin-docs.zammad.org/en/latest/settings/security/third-party/saml.html) | ❌ | ❌ | ❌ | **zammad** | [☑️](../../roles/web-app-zammad/meta/services.yml) | ❌ | ❌ | [✅](https://admin-docs.zammad.org/en/latest/channels/email/index.html) | ❌ | [☑️](../../roles/web-app-zammad/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-zammad/meta/services.yml) | **zammad** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **zammad** | — | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-app-zammad/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **zammad** | [☑️](../../roles/web-app-zammad/meta/services.yml) | ❌ | ❌ | ❌ |
| **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-asset/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-asset/meta/services.yml) | **asset** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | — | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-asset/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **asset** | ❌ | ❌ | ❌ | ❌ |
| **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-cdn/meta/services.yml) | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-cdn/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-cdn/meta/services.yml) | **cdn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **cdn** | ❌ | ❌ | ❌ | ❌ |
| **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-collabora/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-collabora/meta/services.yml) | **collabora** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **collabora** | ❌ | ❌ | ❌ | ❌ |
| **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-coturn/meta/services.yml) | **coturn** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | ❌ | **coturn** | ❌ | ❌ | ❌ | ❌ |
| **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-css/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-css/meta/services.yml) | **css** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | [☑️](../../roles/web-svc-css/meta/services.yml) | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | **css** | ❌ | ❌ | ❌ | ❌ |
| **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-design/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-design/meta/services.yml) | **design** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | [☑️](../../roles/web-svc-design/meta/services.yml) | ❌ | ❌ | — | ❌ | ❌ | ❌ | ❌ | **design** | ❌ | ❌ | ❌ | ❌ |
| **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-file/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-file/meta/services.yml) | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-file/meta/services.yml) | — | ❌ | ❌ | ❌ | **file** | ❌ | ❌ | ❌ | ❌ |
| **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-html/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-html/meta/services.yml) | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-html/meta/services.yml) | ❌ | — | ❌ | ❌ | **html** | ❌ | ❌ | ❌ | ❌ |
| **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-legal/meta/services.yml) | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | — | ❌ | **legal** | ❌ | ❌ | ❌ | ❌ |
| **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-libretranslate/meta/services.yml) | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-libretranslate/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-libretranslate/meta/services.yml) | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **libretranslate** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-libretranslate/meta/services.yml) | ❌ | ❌ | ❌ | — | **libretranslate** | [☑️](../../roles/web-svc-libretranslate/meta/services.yml) | ❌ | ❌ | ❌ |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | css | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| ↓ row wires → col | akaunting | baserow | bigbluebutton | bluesky | bookwyrm | bridgy-fed | chess | confluence | dashboard | decidim | ↓ row wires → col | discourse | erpnext | espocrm | fediwall | fider | flowise | friendica | funkwhale | fusiondirectory | gitea | ↓ row wires → col | gitlab | hugo | jenkins | jira | jitsi | joomla | keycloak | kix | lam | listmonk | ↓ row wires → col | litellm | littlejs | magento | mailu | mastodon | matomo | matrix | mattermost | mediawiki | mig | ↓ row wires → col | minio | mini-qr | mobilizon | moodle | n8n | navigator | nextcloud | odoo | opencloud | openproject | ↓ row wires → col | opentalk | openwebui | peertube | pgadmin | phpldapadmin | phpmyadmin | pixelfed | postmarks | pretix | prometheus | ↓ row wires → col | roulette-wheel | shopware | snipe-it | socialhome | sphinx | suitecrm | taiga | wordpress | xwiki | yourls | ↓ row wires → col | zammad | asset | cdn | collabora | coturn | design | file | html | legal | libretranslate | ↓ row wires → col | logout | onlyoffice | simpleicons | xmpp |
| **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-logout/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-logout/meta/services.yml) | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **logout** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-logout/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **logout** | — | ❌ | ❌ | ❌ |
| **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-onlyoffice/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-onlyoffice/meta/services.yml) | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-onlyoffice/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **onlyoffice** | ❌ | — | ❌ | ❌ |
| **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-simpleicons/meta/services.yml) | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-simpleicons/meta/services.yml) | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-simpleicons/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-simpleicons/meta/services.yml) | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | ❌ | ❌ | ❌ | [☑️](../../roles/web-svc-simpleicons/meta/services.yml) | ❌ | ❌ | ❌ | ❌ | **simpleicons** | ❌ | ❌ | — | ❌ |
+3 -3
View File
@@ -101,9 +101,9 @@ mobilizon:
matomo:
enabled: "{{ 'web-app-matomo' in group_names }}"
shared: "{{ 'web-app-matomo' in group_names }}"
css:
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
design:
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
atmo:
image: atmo # self-built (Open Question 2)
version: "<upstream-git-ref>" # concrete ref, never :latest
+1 -1
View File
@@ -501,7 +501,7 @@ These UIs expose identity or database administration rather than a bounded appli
**`duplicate_owner` (1):** `web-app-litellm`.
This role is only the UI of the separately classified LiteLLM service role, which owns any future MCP gateway contract.
**`no_remote_surface` (24):** `svc-ai-ollama`, `svc-ai-robot`, `web-app-chess`, `web-app-dashboard`, `web-app-fediwall`, `web-app-hugo`, `web-app-littlejs`, `web-app-mig`, `web-app-mini-qr`, `web-app-navigator`, `web-app-roulette-wheel`, `web-app-sphinx`, `web-opt-rdr-domains`, `web-opt-rdr-www`, `web-svc-asset`, `web-svc-cdn`, `web-svc-coturn`, `web-svc-css`, `web-svc-file`, `web-svc-html`, `web-svc-legal`, `web-svc-logout`, `web-svc-mirror`, `web-svc-simpleicons`.
**`no_remote_surface` (24):** `svc-ai-ollama`, `svc-ai-robot`, `web-app-chess`, `web-app-dashboard`, `web-app-fediwall`, `web-app-hugo`, `web-app-littlejs`, `web-app-mig`, `web-app-mini-qr`, `web-app-navigator`, `web-app-roulette-wheel`, `web-app-sphinx`, `web-opt-rdr-domains`, `web-opt-rdr-www`, `web-svc-asset`, `web-svc-cdn`, `web-svc-coturn`, `web-svc-design`, `web-svc-file`, `web-svc-html`, `web-svc-legal`, `web-svc-logout`, `web-svc-mirror`, `web-svc-simpleicons`.
These roles have no independently useful authenticated remote action contract at the pinned implementation.
Static published content MAY later be consumed through one owning application's bounded `resource_readonly` adapter, but a new MCP sidecar that merely reimplements a static site's behavior does not count as integration.
Model tool calling in Ollama does not by itself make Ollama an MCP client or server.
@@ -0,0 +1,145 @@
# 037 - Corporate Design Overhaul
## User Story
As an operator of Infinito.Nexus, I want every web UI to follow one configurable corporate design (palette, typography, logo, title, light and dark mode) so that all apps read as one platform while the base color stays changeable from a single setting.
## Confirmed Decisions
These choices are settled at requirement creation time and bound the implementation. Re-opening any of them MUST be recorded in the implementing PR.
1. **One SPOT role.** `web-svc-css` is renamed to `web-svc-design`. The content of `group_vars/all/13_design.yml` moves into the `design` entity of `roles/web-svc-design/meta/services.yml`, the group_vars file is deleted, and the unused `filters` block is dropped. Inventories and docs are migrated. No compatibility shim.
2. **Service keys.** Consumers replace their `css:` key with `design:`. CSS injection is driven by `design` only. An enabled `design` always brings its own JavaScript channel: the role's optional `templates/design.js.j2`, collapsed, CSP-hashed and injected by `sys-front-inj-design`. The `javascript:` key stays independent and keeps gating the role's functional script (`javascript.js.j2`, which carries SSO and logout glue that must not run without SSO). `sys-front-inj-css` is renamed to `sys-front-inj-design` because injector feature names map one to one onto `sys-front-inj-<feature>` roles; `sys-front-inj-javascript` stays.
3. **Palette engine.** An in-repo OKLCH lookup plugin replaces the `colorscheme_generator` dependency. Its only input is `design.colors.base`. It derives a primary scale 50 to 950, tinted neutrals, status colors (success, warning, danger, info) with fixed hues and harmonised chroma, on-colors chosen by contrast, and a dedicated dark palette (no lightness inversion). Text tokens meet WCAG 2.2 AA (4.5:1), UI and large text meet 3:1, body text meets AAA (7:1). An invalid hex value fails the deploy.
4. **Semantic tokens.** `--design-primary`, `--design-on-primary`, `--design-link`, `--design-surface-1` to `--design-surface-3`, `--design-text`, `--design-text-muted`, `--design-border`, `--design-border-strong`, `--design-success`, `--design-warning`, `--design-danger`, `--design-info` with their `--design-on-*` partners, the interaction states `--design-surface-hover`, `--design-surface-active` and `--design-<fill>-hover`, `--design-<fill>-active` (each keeps its text at AA or better), and the `--design-primary-*` and `--design-neutral-*` scales 50 to 950 replace `--color-NN-SS` and `--color-rgb-NN-SS`. Every color also exists as `<token>-rgb`. `--design-invert` is `0` in light and `1` in dark mode, so `filter: invert(var(--design-invert))` flips a monochrome image with the mode. Scale step 50 sits next to the surface and 950 next to the text in both modes, so a role mapping onto the scale follows dark mode without its own media query. Bootstrap `--bs-*` variables map onto the tokens. The 22 existing role stylesheets are migrated mechanically in the base and refined in the role loop.
5. **Clean, flat base style.** No gradients on form controls and tables, subtle elevation shadows, one radius scale, 4/8 px spacing, visible focus rings, form state shown by border and icon. Font is the `system-ui` stack; `design.font` stays configurable. The shared element styles sit in the cascade layer `infinito-design`, so an app's own design system always wins over them and keeps its contrast relations. Such an app takes the palette through a variable mapping in its role stylesheet or through its own theming mechanism.
6. **Dark mode.** An app's native theme toggle wins. Without one, the design follows `prefers-color-scheme`. No theme toggle is injected.
7. **Logo and title override semantics.** `design.logo` defaults to `assets/img/logo.png`, `design.title` defaults to `true`, which resolves to the role title (README H1). Set in `web-svc-design`, a value applies to every role. Set in a role's own `design:` entry, it applies to that role only. `false` or `0` disables the replacement for that scope. The legacy keys `title`, `titel`, `sitename` and `site_name` are migrated, the hardcoded titles in `web-app-joomla` and `web-app-pretix` are removed, and `web-app-decidim` and `web-app-socialhome` read the resolved title instead of `defaults_*`.
8. **Logo generator.** An SVG template renders the lockup, Pillow rasterises PNG and ICO variants. The variant is computed from the target slot format: square slot shows the logo only, wide slot shows logo and title, very wide or two-line slot adds the domain, text-only slot shows the title only. The background stays transparent. The text uses palette colors with an outline so it stands out on light and dark surfaces.
9. **Branch and stack.** One branch `feature/designer` and one draft PR, opened once the base is approved. Exactly one compose stack runs at a time. Base changes land as base commits on the same branch. Role changes stay under `roles/<role>/` and are committed per role after approval.
10. **Verification.** Every change passes `make quality-high` and a minimal compose deploy against the running stack: `make compose-deploy apps=<role> variant=0 disable=<every disableable service except design>` (default `initialize` mode, no teardown, single pass, no `full_cycle`). CSS and branding iterations run without a redeploy through `make design-sync app=<role> variant=0` (re-renders shared CSS, role `style.css` and the generated logo assets). A changed `design.js` needs a `mode=update` deploy, because its CSP hash lives in the vhost. Swarm verification is out of scope.
11. **Review gallery.** Each role gets its own private Artifact with at least 20 views, each captured in light and dark mode and in a desktop (1440×900) and a mobile (390×844) viewport, so at least 80 before/after pairs per role. The views favour detail-rich surfaces: settings, administration, forms, lists and detail pages, logged in where the app has accounts, with idempotently seeded showcase content where a list or detail page would otherwise be empty. "Before" is the deployed app with every injected snippet stripped from the document (the body filter wraps its injections in `<!--infinito-inj-->` markers), "after" is the same page with the corporate design. `make design-gallery app=<role>` captures both sides in one spec run and copies them to `/tmp/design-gallery/<role>/`. The operator approves or rejects by comment.
12. **Frontend and backend.** Every role is styled and tested on its frontend and, where the app has one, on its separate backend (admin surface such as `wp-admin`, the Shopware or Magento admin, the Joomla administrator). A role with a backend shows backend views in its gallery in addition to the frontend views, and its Playwright spec covers both surfaces.
13. **Scope.** In-scope roles are invokable, carry a lifecycle inside the tested envelope (`alpha` to `maintenance`), and expose a web UI.
## Acceptance Criteria
### Base
- [x] `roles/web-svc-css` no longer exists and `roles/web-svc-design` deploys and serves the shared CSS.
- [x] `group_vars/all/13_design.yml` no longer exists and the design configuration resolves from `roles/web-svc-design/meta/services.yml`, overridable per inventory.
- [x] No `roles/*/meta/services.yml` declares a `css:` key.
- [x] A role with `design` enabled, `javascript` disabled and a `templates/design.js.j2` receives the injected CSS and its design script, and no functional script.
- [ ] A role with `design` disabled and `javascript` enabled receives its functional script and no injected CSS or design script.
- [ ] A minimal deploy with `disable=design` injects neither the shared CSS nor a design script.
- [x] `colorscheme_generator` is no longer a dependency and the OKLCH lookup plugin produces the full token set.
- [x] A unit test proves AA contrast for text and UI tokens and AAA contrast for body text in light and dark mode across the base colors `#001f3f`, `#FFA500`, `#FFFF00`, `#00FF00`, `#808080` and `#FF00FF`.
- [x] An invalid `design.colors.base` fails the deploy with an error naming the value.
- [x] No `--color-NN-SS` or `--color-rgb-NN-SS` reference remains under `roles/`.
- [x] The shared CSS contains no gradient on form controls or tables.
- [x] The shared CSS switches palettes via `prefers-color-scheme` and injects no theme toggle.
- [x] The logo generator emits SVG, PNG and ICO variants with transparent background for square, wide, very wide and text-only slots.
- [x] A unit test proves the logo and title precedence: global value, per-role override, and `false`/`0` disabling.
- [x] No `roles/*/meta/services.yml` declares a site title as `title`, `titel`, `sitename`, `site_name` or `site_titel` on its primary entity; every site title lives in the role's `design` entry and is read through `lookup('design', application_id)`.
- [x] `web-app-joomla` and `web-app-pretix` carry no hardcoded site title.
- [ ] `web-app-decidim` and `web-app-socialhome` honour an inventory title override.
- [x] The base gallery on `web-app-dashboard`, `web-app-gitea` and `web-app-nextcloud` is approved by the operator.
- [ ] The draft PR from `feature/designer` is open and linked below.
### Roles
A role is checked when its frontend and, where present, its backend are styled, its CSS and JS use only `--design-*` tokens, its logo and title are set through the app's own mechanism in the slot format, its Playwright spec covers frontend and backend and asserts token colors, contrast of core elements, light and dark mode, the native toggle where one exists, and the visible logo and title, `make quality-high` and the minimal compose deploy are green, its gallery is approved and linked under "Further Resources" in the role README, and the change is committed.
- [ ] web-app-akaunting
- [ ] web-app-baserow
- [ ] web-app-bigbluebutton
- [ ] web-app-bluesky
- [ ] web-app-bookwyrm
- [ ] web-app-bridgy-fed
- [ ] web-app-checkmk
- [ ] web-app-chess
- [ ] web-app-dashboard
- [ ] web-app-decidim
- [ ] web-app-discourse
- [ ] web-app-erpnext
- [ ] web-app-espocrm
- [ ] web-app-fediwall
- [ ] web-app-fider
- [ ] web-app-flowise
- [ ] web-app-friendica
- [ ] web-app-funkwhale
- [ ] web-app-fusiondirectory
- [ ] web-app-gitea
- [ ] web-app-gitlab
- [ ] web-app-hermes
- [ ] web-app-homeassistant
- [ ] web-app-hugo
- [ ] web-app-jellyfin
- [ ] web-app-jenkins
- [ ] web-app-jitsi
- [ ] web-app-joomla
- [ ] web-app-keycloak
- [ ] web-app-kix
- [ ] web-app-lam
- [ ] web-app-listmonk
- [ ] web-app-litellm
- [ ] web-app-littlejs
- [ ] web-app-magento
- [ ] web-app-mastodon
- [ ] web-app-matomo
- [ ] web-app-matrix
- [ ] web-app-mattermost
- [ ] web-app-mediawiki
- [ ] web-app-mig
- [ ] web-app-mini-qr
- [ ] web-app-mobilizon
- [ ] web-app-moodle
- [ ] web-app-n8n
- [ ] web-app-nextcloud
- [ ] web-app-odoo
- [ ] web-app-openbao
- [ ] web-app-openclaw
- [ ] web-app-opencloud
- [ ] web-app-openproject
- [ ] web-app-opentalk
- [ ] web-app-openwebui
- [ ] web-app-peertube
- [ ] web-app-penpot
- [ ] web-app-pgadmin
- [ ] web-app-phpmyadmin
- [ ] web-app-pihole
- [ ] web-app-pixelfed
- [ ] web-app-postmarks
- [ ] web-app-pretix
- [ ] web-app-prometheus
- [ ] web-app-roulette-wheel
- [ ] web-app-seaweedfs
- [ ] web-app-semaphore
- [ ] web-app-shopware
- [ ] web-app-snipe-it
- [ ] web-app-socialhome
- [ ] web-app-sphinx
- [ ] web-app-stalwart
- [ ] web-app-suitecrm
- [ ] web-app-taiga
- [ ] web-app-wordpress
- [ ] web-app-xwiki
- [ ] web-app-yourls
- [ ] web-app-zammad
- [ ] web-svc-collabora
- [ ] web-svc-html
- [ ] web-svc-legal
- [ ] web-svc-libretranslate
- [ ] web-svc-logout
- [ ] web-svc-mirror
- [ ] web-svc-onlyoffice
## Cross-linking
- Implementing PR: *to be linked*.
## See Also
- Role style files: [style.css.md](../contributing/artefact/files/role/style.css.md)
- Lifecycle envelope: [lifecycle.md](../contributing/design/role/services/lifecycle.md)
- Compose loop: [compose.md](../agents/action/iteration/compose.md)
-12
View File
@@ -1,12 +0,0 @@
---
defaults_design:
enabled: true
css:
colors:
base: "#001f3f"
filters:
saturation_change: 70
hue_shift: 0
font:
import_url: false
type: '"Liberation Sans", Arial, sans-serif'
+6 -4
View File
@@ -32,10 +32,12 @@ networks:
ip6: "{{ lookup('env', 'INFINITO_IP6') }}"
dns: "{{ lookup('env', 'INFINITO_DNS_IP') }}"
design:
css:
colors:
base: "#FFA500"
applications:
web-svc-design:
services:
design:
colors:
base: "#FFA500"
API:
openai:
+30
View File
@@ -0,0 +1,30 @@
---
- name: "Sync the corporate design of one deployed application"
hosts: all
any_errors_fatal: true
become: true
vars:
design_application_id: "{{ APPLICATIONS_WHITELIST | first }}"
tasks:
- name: ✅ Assert exactly one application is targeted
ansible.builtin.assert:
that:
- APPLICATIONS_WHITELIST | length == 1
fail_msg: "Pass exactly one application id in APPLICATIONS_WHITELIST, got {{ APPLICATIONS_WHITELIST }}"
quiet: true
- name: 🔀 Merge variables
ansible.builtin.include_tasks: tasks/utils/constructor/variables.yml
- name: 🧊 Render the applications SPOT once and carry it into every task's worker
applications_carrier: {}
- name: "📦 Load the role variables of '{{ design_application_id }}'"
ansible.builtin.include_vars:
file: "{{ lookup('path_absolute', 'roles', design_application_id, 'vars', 'main.yml') }}"
- name: "🎨 Re-render the design files of '{{ design_application_id }}'"
ansible.builtin.include_role:
name: sys-front-inj-design
vars:
application_id: "{{ design_application_id }}"
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from typing import Any
from plugins.action.stack_host_copy import ActionModule as StackHostCopyAction
from utils.design.branding import is_disabled
from utils.design.logo import render_assets
_REQUIRED = ("logo", "title", "domain", "fill", "stroke", "slots", "dest")
class ActionModule(StackHostCopyAction):
"""
Render the branding assets of one role on the controller and copy them to
the stack host.
Args (task):
logo absolute logo path
title title text, or false
domain role domain
fill text color
stroke text outline color
slots {name: {width, height, text_only}}
dest destination directory on the stack host
"""
def run(
self, tmp: Any = None, task_vars: dict[str, Any] | None = None
) -> dict[str, Any]:
args = dict(self._task.args)
missing = [key for key in _REQUIRED if key not in args]
if missing:
return {
"failed": True,
"msg": f"design_assets: missing {', '.join(missing)}",
}
workdir = Path(tempfile.mkdtemp(prefix="design-assets-"))
try:
title = False if is_disabled(args["title"]) else str(args["title"])
assets = render_assets(
args["logo"],
title,
args["domain"],
args["fill"],
args["stroke"],
args["slots"],
)
for name, content in assets.items():
(workdir / name).write_bytes(content)
self._task.args = {
"src": f"{workdir}/",
"dest": f"{str(args['dest']).rstrip('/')}/",
}
return super().run(tmp=tmp, task_vars=task_vars)
finally:
shutil.rmtree(workdir, ignore_errors=True)
-18
View File
@@ -1,18 +0,0 @@
from ansible.plugins.lookup import LookupBase
from colorscheme_generator import generate_full_palette
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
base_color = terms[0]
count = kwargs.get("count")
shades = kwargs.get("shades")
invert_lightness = kwargs.get("invert_lightness", False)
return [
generate_full_palette(
base_color,
count=count,
shades=shades,
invert_lightness=invert_lightness,
)
]
+69
View File
@@ -0,0 +1,69 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from utils.design.branding import resolve_branding
ASSET_DIR = "design"
class LookupModule(LookupBase):
"""
lookup('design', application_id)
Returns the resolved corporate branding of one role:
logo absolute logo path, or False when the replacement is disabled
title title text, or False when the replacement is disabled
name role README H1
label title, else name (for installers that require a site name)
slots {name: {width, height, text_only}}
domain canonical domain of the role
dest CDN directory the generated assets are written to
urls {"favicon": url, "<slot>": {"png": url, "svg": url}}
"""
def _sub(self, name: str, terms: list, variables: dict[str, Any]) -> Any:
plugin = lookup_loader.get(
name,
loader=getattr(self, "_loader", None),
templar=getattr(self, "_templar", None),
)
return plugin.run(terms, variables=variables)[0]
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
variables = variables or {}
if len(terms) != 1:
raise AnsibleError("design: expects exactly one application_id")
application_id = str(terms[0]).strip()
root = variables.get("playbook_dir")
if not root:
raise AnsibleError("design: 'playbook_dir' is unavailable")
applications = self._sub("applications", [], variables)
try:
branding = resolve_branding(applications, application_id, Path(str(root)))
except (KeyError, ValueError) as exc:
raise AnsibleError(f"design: {exc}") from exc
cdn = self._sub("cdn", [application_id], variables)
base_url = f"{cdn['urls']['role']['release']['img'].rstrip('/')}/{ASSET_DIR}"
urls: dict[str, Any] = {"favicon": f"{base_url}/favicon.ico"}
for slot in branding["slots"]:
urls[slot] = {
"png": f"{base_url}/{slot}.png",
"svg": f"{base_url}/{slot}.svg",
}
return [
{
**branding,
"domain": self._sub("domain", [application_id], variables),
"dest": str(Path(cdn["role"]["release"]["img"]) / ASSET_DIR),
"urls": urls,
}
]
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from utils.design.palette import build_palette
class LookupModule(LookupBase):
"""
Return the corporate design palette for one base color.
Usage:
{{ lookup('design_palette', '#001f3f') }}
-> {"scale": {...}, "light": {...}, "dark": {...}}
Each mapping is keyed by CSS custom property name (``--design-*``) and
carries a ``<name>-rgb`` triplet for every color.
"""
def run(self, terms, variables=None, **kwargs):
if len(terms) != 1:
raise AnsibleError("design_palette: expects exactly one base color")
try:
return [build_palette(terms[0])]
except ValueError as exc:
raise AnsibleError(f"design_palette: {exc}") from exc
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
[project.optional-dependencies]
deploy = [
"ansible",
"colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip",
"Pillow>=10.1",
"tld",
"passlib",
"requests",
+3 -3
View File
@@ -28,10 +28,10 @@
public: true
when: inj_enabled.dashboard
- name: "🟢 Activate Corporate CSS for '{{ domain }}'"
- name: "🟢 Activate Corporate Design for '{{ domain }}'"
include_role:
name: sys-front-inj-css
when: inj_enabled.css
name: sys-front-inj-design
when: inj_enabled.design
- name: "🟢 Activate Matomo Tracking for '{{ domain }}'"
include_role:
@@ -68,6 +68,10 @@ local function onionize(s)
return s
end
local function marked(snippets)
return "<!--infinito-inj-->" .. table.concat(snippets, "\n") .. "<!--/infinito-inj-->"
end
-- Remove inline CSP <meta http-equiv="Content-Security-Policy"> (case-insensitive)
local meta_re = [[<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]]
whole = ngx.re.gsub(whole, meta_re, "", "ijo")
@@ -75,7 +79,7 @@ whole = ngx.re.gsub(whole, meta_re, "", "ijo")
-- Build head snippets (rendered by Jinja at template time)
local head_snippets = {}
{{ push_snippets('head_snippets', inj_head_features) }}
local head_payload = onionize(table.concat(head_snippets, "\n")) .. "</head>"
local head_payload = onionize(marked(head_snippets)) .. "</head>"
-- Inject before </head> (first occurrence)
local function repl_head(_) return head_payload end
@@ -86,7 +90,7 @@ else
ngx.log(ngx.WARN, "No </head> found; trying <body> fallback: ", err or "nil")
-- Fallback: inject right AFTER the opening <body ...> tag
local body_open_re = [[<body\b[^>]*>]]
new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. table.concat(head_snippets, "\n"), "ijo")
new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. marked(head_snippets), "ijo")
if new then
whole = new
else
@@ -97,7 +101,7 @@ end
-- Build body snippets (rendered by Jinja at template time)
local body_snippets = {}
{{ push_snippets('body_snippets', inj_body_features) }}
local body_payload = onionize(table.concat(body_snippets, "\n")) .. "</body>"
local body_payload = onionize(marked(body_snippets)) .. "</body>"
-- Inject before </body> (first occurrence), or append if missing
local function repl_body(_) return body_payload end
@@ -106,7 +110,7 @@ if new then
whole = new
else
ngx.log(ngx.WARN, "No </body> found; appending body snippets at end: ", err or "nil")
whole = whole .. table.concat(body_snippets, "\n")
whole = whole .. marked(body_snippets)
end
-- Emit the modified HTML
+1 -1
View File
@@ -4,6 +4,6 @@
SRV_WEB_INJ_COMP_FEATURES_ALL:
- "javascript"
- "logout"
- "css"
- "design"
- "matomo"
- "dashboard"
-45
View File
@@ -1,45 +0,0 @@
# Global CSS Injection for NGINX
## Description
This Ansible role ensures **consistent global theming** across all NGINX-served applications by injecting CSS files.
The role leverages [`colorscheme-generator`](https://github.com/kevinveenbirkenbach/colorscheme-generator/) to generate a dynamic, customizable color palette for light and dark mode, compatible with popular web tools like **Bootstrap**, **Keycloak**, **Nextcloud**, **Taiga**, **Mastodon**, and many more.
## Overview
This role deploys a centralized global stylesheet that overrides the default theming of web applications served via NGINX. It's optimized to run only once per deployment and generates a **cache-busting version number** based on file modification timestamps.
It includes support for **dark mode**, **custom fonts**, and **extensive Bootstrap and UI component overrides**.
## Cosmos
The diagram places Global CSS Injection for NGINX in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
```mermaid
flowchart LR
subgraph role [sys-front-inj-css 💻]
svc_css["css"]
end
```
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
## Purpose
The goal of this role is to provide a **single source of truth for theming** across your infrastructure.
It makes all applications feel like part of the same ecosystem, visually and functionally.
## Features
- 🎨 **Dynamic Theming** via [`colorscheme-generator`](https://github.com/kevinveenbirkenbach/colorscheme-generator/)
- 📁 **Unified CSS Base Configuration** deployment for all NGINX applications
- 🌒 **Dark mode support** out of the box
- 🚫 **No duplication** – tasks run once per deployment
- ⏱️ **Versioning logic** to bust browser cache
- 🎯 **Bootstrap override compatibility**
- 🧩 **Theme support for Keycloak, Nextcloud, Gitea, LAM, Peertube, and more**
## Credits
Implemented by **[Kevin Veen-Birkenbach](https://www.veen.world)**.
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).
-33
View File
@@ -1,33 +0,0 @@
---
- name: ⚙️ Generate color palette with colorscheme-generator
set_fact:
CSS_COLOR_PALETTE: "{{ lookup('colorscheme', CSS_BASE_COLOR, count=CSS_COUNT, shades=CSS_SHADES) }}"
- name: ⚙️ Generate inverted color palette with colorscheme-generator
set_fact:
CSS_COLOR_PALETTE_INVERTED: "{{ lookup('colorscheme', CSS_BASE_COLOR, count=CSS_COUNT, shades=CSS_SHADES, invert_lightness=True) }}"
- name: "🧮 Compute deterministic gradient angle from default.css template mtime"
set_fact:
CSS_GRADIENT_ANGLE: >-
{{
(
lookup(
'local_mtime_qs',
lookup('path_absolute', 'roles', 'sys-front-inj-css', 'templates', 'css', 'default.css.j2')
)
| regex_replace('^.*=', '')
| int
) % 360
}}
- name: 🚀 Deploy default CSS files
stack_host_template:
src: "{{ ['css', css_file ~ '.j2'] | path_join }}"
dest: "{{ [lookup('cdn', 'web-svc-cdn').shared.css, css_file] | path_join }}"
mode: "0644"
loop: "{{ CSS_FILES }}"
loop_control:
loop_var: css_file
- include_tasks: utils/once/flag.yml
-22
View File
@@ -1,22 +0,0 @@
---
- include_tasks: 00_core.yml
when: run_once_sys_front_inj_css is not defined
- name: "🔍 Resolve optional app style.css source for '{{ application_id }}'"
vars:
app_role_dir: "{{ lookup('path_absolute', 'roles', application_id) }}"
_app_style_src: >-
{{ lookup('first_found', {
'files': ['templates/style.css.j2','files/style.css'],
'paths': [app_role_dir]
}, errors='ignore') | default('', true) }}
set_fact:
app_style_src: "{{ _app_style_src }}"
app_style_present: "{{ _app_style_src | length > 0 }}"
- name: "🚀 Deploy per-app '{{ app_style_src }}' to '{{ css_app_dst }}'"
when: app_style_present | bool
copy:
content: "{{ lookup('template', app_style_src) }}"
dest: "{{ css_app_dst }}"
mode: "0644"
@@ -1,63 +0,0 @@
/* Buttons (Background, Text, Border, and Shadow)
Now using a button background that is only slightly darker than the overall background */
html[native-dark-active] .btn, .btn {
background-color: var(--color-01-87);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-70), var(--color-01-91), var(--color-01-95), var(--color-01-95));
color: var(--color-01-50);
border-color: var(--color-01-80);
cursor: pointer;
}
/* Navigation (Background and Text Colors) */
.navbar, .navbar-light, .navbar-dark, .navbar.bg-light {
background-color: var(--color-01-90);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-85), var(--color-01-90), var(--color-01-91), var(--color-01-95));
color: var(--color-01-50);
border-color: var(--color-01-85);
}
.navbar a {
color: var(--color-01-40);
}
.navbar a.dropdown-item {
color: var(--color-01-43);
}
/* Cards / Containers (Background, Border, and Shadow)
Cards now use a slightly lighter background and a bold, clear shadow */
.card {
background-color: var(--color-01-90);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-85), var(--color-01-90), var(--color-01-91), var(--color-01-95));
border-color: var(--color-01-85);
color: var(--color-01-12);
}
.card-body {
color: var(--color-01-40);
}
.navbar .dropdown-menu,
.nav-item .dropdown-menu {
background-color: var(--color-01-80);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-75), var(--color-01-80), var(--color-01-81), var(--color-01-85));
color: var(--color-01-40);
}
.navbar-nav {
--bs-nav-link-hover-color: var(--color-01-17);
}
.dropdown-item {
color: var(--color-01-40);
background-color: var(--color-01-80);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-75), var(--color-01-80), var(--color-01-81), var(--color-01-85));
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: var(--color-01-65);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-60), var(--color-01-65), var(--color-01-66), var(--color-01-70));
color: var(--color-01-40);
}
@@ -1,251 +0,0 @@
/***
Global Theming Styles – Color and Shadow Variables
HINT:
- Better overwritte CSS variables instead of individual elements.
- Don't use !important. If possible use a specific selector.
*/
{% if design.font.import_url %}
@import url('{{ design.font.import_url }}');
{% endif %}
/* Auto-generated by colorscheme-generator */
:root {
{% for var_name, color in CSS_COLOR_PALETTE.items() %}
{{ var_name }}: {{ color }};
{% endfor %}
}
@media (prefers-color-scheme: dark) {
:root {
{% for var_name, color in CSS_COLOR_PALETTE_INVERTED.items() %}
{{ var_name }}: {{ color }};
{% endfor %}
}
}
:root, ::after, ::before, ::backdrop {
--native-dark-accent-color: var(--color-01-60);
--native-dark-bg-color: var(--color-01-10);
--native-dark-bg-image-color: rgba(var(--color-rgb-01-01), 0.10);
--native-dark-border-color: var(--color-01-40);
--native-dark-box-shadow: 0 0 0 1px rgb(var(--color-rgb-01-99), / 10%);
--native-dark-cite-color: var(--color-01-70);
--native-dark-fill-color: var(--color-01-50);
--native-dark-font-color: var(--color-01-95);
--native-dark-link-color: var(--color-01-80);
--native-dark-visited-link-color: var(--color-01-85);
}
/* Bootstrap Overrides (Color/Shadow Variables Only) */
:root {
--bs-black: var(--color-01-01);
--bs-white: var(--color-01-99);
--bs-gray: var(--color-01-50);
--bs-gray-dark: var(--color-01-20);
{% for i in range(1, 10) %}
{% set gray = i * 100 %}
{% set color = 100 - i * 10 %}
--bs-gray-{{ gray }}: var(--color-01-{{ "%02d" % color }});
{% endfor %}
--bs-primary: var(--color-01-65);
--bs-light: var(--color-01-95);
--bs-dark: var(--color-01-10);
--bs-primary-rgb: var(--color-rgb-01-65);
--bs-secondary-rgb: var(--color-rgb-01-50);
--bs-light-rgb: var(--color-rgb-01-95);
--bs-dark-rgb: var(--color-rgb-01-10);
--bs-white-rgb: var(--color-rgb-01-99);
--bs-black-rgb: var(--color-rgb-01-01);
--bs-body-color-rgb: var(--color-rgb-01-10);
--bs-body-bg-rgb: var(--color-rgb-01-99);
--bs-body-color: var(--color-01-10);
--bs-body-bg: var(--color-01-99);
--bs-border-color: var(--color-01-85);
--bs-link-color: var(--color-01-65);
--bs-link-hover-color: var(--color-01-60);
--bs-code-color: var(--color-01-55);
--bs-highlight-bg: var(--color-01-93);
--bs-list-group-bg: var(--color-01-40);
--bs-emphasis-color: var(--color-01-01);
--bs-emphasis-color-rgb: var(--color-rgb-01-01);
--bs-secondary-color: rgba(var(--color-rgb-01-10), 0.75);
--bs-secondary-color-rgb: var(--color-rgb-01-10);
--bs-secondary-bg: var(--color-01-90);
--bs-secondary-bg-rgb: var(--color-rgb-01-90);
--bs-tertiary-color: rgba(var(--color-rgb-01-10), 0.5);
--bs-tertiary-color-rgb: var(--color-rgb-01-10);
--bs-tertiary-bg: var(--color-01-95);
--bs-tertiary-bg-rgb: var(--color-rgb-01-95);
--bs-link-color-rgb: var(--color-rgb-01-65);
--bs-link-hover-color-rgb: var(--color-rgb-01-60);
--bs-highlight-color: var(--color-01-10);
--bs-border-color-translucent: rgba(var(--color-rgb-01-01), 0.175);
--bs-focus-ring-color: rgba(var(--color-rgb-01-65), 0.25);
--bs-table-color: var(--bs-emphasis-color);
--bs-table-bg: var(--color-01-99);
--bs-table-border-color: var(--color-01-99);
--bs-table-striped-bg: var(--color-01-85);
--bs-table-hover-color: var(--color-01-01);
--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);
}
/* Global Defaults (Colors Only) */
body, html[native-dark-active] {
background-color: var(--color-01-93);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-93), var(--color-01-91), var(--color-01-95), var(--color-01-93));
background-attachment: fixed;
color: var(--color-01-40);
font-family: {{ design.font.type }};
}
{# All links (applies to all anchor elements regardless of state) #}
a {
color: var(--color-01-50);
}
{# Unvisited links (applies only to links that have not been visited) #}
a:link {
color: var(--color-01-55);
}
{# Visited links (applies only to links that have been visited) #}
a:visited {
color: var(--color-01-45);
}
{# Hover state (applies when the mouse pointer is over the link) #}
a:hover {
color: var(--color-01-60);
}
{# Active state (applies during the time the link is being activated, e.g., on click) #}
a:active {
color: var(--color-01-65);
}
/** Set default buttons transparent **/
html[native-dark-active] button, button{
background-color: var(--color-01-87);
}
button:hover,
.btn:not(.stretched-link):hover {
filter: brightness(0.9);
}
input:invalid,
textarea:invalid,
select:invalid {
background-color: var(--color-01-01);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-01), var(--color-01-10));
color: var(--bs-danger);
border-color: var(--color-01-20);
}
input:valid,
textarea:valid,
select:valid {
background-color: var(--color-01-80);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-80), var(--color-01-90));
color: var(--bs-success);
border-color: var(--color-01-70);
}
input:required,
textarea:required,
select:required {
background-color: var(--color-01-50);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-50), var(--color-01-60));
color: var(--bs-warning);
border-color: var(--color-01-70);
}
input:optional,
textarea:optional,
select:optional {
background-color: var(--color-01-60);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-60), var(--color-01-70));
color: var(--bs-info);
border-color: var(--color-01-70);
}
input:read-only,
textarea:read-only,
select:read-only {
background-color: var(--color-01-80);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-90), var(--color-01-70));
color: var(--color-01-20);
border-color: var(--color-01-50);
}
input:read-write,
textarea:read-write,
select:read-write {
background-color: var(--color-01-70);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-70), var(--color-01-80));
color: var(--color-01-40);
border-color: var(--color-01-70);
}
input:in-range,
textarea:in-range,
select:in-range {
background-color: var(--color-01-70);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-70), var(--color-01-85));
color: var(--color-01-40);
border-color: var(--color-01-70);
}
input:out-of-range,
textarea:out-of-range,
select:out-of-range {
background-color: var(--color-01-10);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-10), var(--color-01-30));
color: var(--color-01-10);
border-color: var(--color-01-50);
}
input:placeholder-shown,
textarea:placeholder-shown,
select:placeholder-shown {
background-color: var(--color-01-82);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-82), var(--color-01-90));
color: var(--color-01-40);
border-color: var(--color-01-70);
}
input:focus,
textarea:focus,
select:focus {
background-color: var(--color-01-75);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-75), var(--color-01-85));
color: var(--color-01-40);
border-color: var(--color-01-50);
}
input:hover,
textarea:hover,
select:hover {
background-color: var(--color-01-78);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-78), var(--color-01-88));
color: var(--color-01-40);
border-color: var(--color-01-65);
}
input:active,
textarea:active,
select:active {
background-color: var(--color-01-68);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-68), var(--color-01-78));
color: var(--color-01-40);
border-color: var(--color-01-60);
}
/* {# Checked state: specifically for radio buttons and checkboxes when selected. #} */
input:checked {
background-color: var(--color-01-90);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-90), var(--color-01-99));
color: var(--color-01-40);
border-color: var(--color-01-70);
}
input[type="checkbox"] {
appearance: auto;
-webkit-appearance: auto;
-moz-appearance: auto;
background: none;
}
option {
background-color: var(--color-01-82);
color: var(--color-01-07);
}
/* Tables (Borders and Header Colors) */
th, td {
border-color: var(--color-01-70);
}
thead {
background-color: var(--color-01-80);
background: linear-gradient({{ CSS_GRADIENT_ANGLE }}deg, var(--color-01-75), var(--color-01-80), var(--color-01-81), var(--color-01-85));
color: var(--color-01-40);
}
/* Headings (Text Color) */
h1, h2, h3, h4, h5, h6, p{
color: var(--color-01-10);
}
label {
color: var(--color-01-20);
}
+47
View File
@@ -0,0 +1,47 @@
# Corporate Design Injection for NGINX
## Description
This Ansible role injects the corporate design into every NGINX-served application whose `design` service is enabled.
It derives the design tokens from the single base color in `web-svc-design` through the [design_palette](../../plugins/lookup/design_palette.py) lookup (OKLCH, WCAG 2.2 contrast by construction) and maps them onto Bootstrap and common app variables.
## Overview
The role renders the shared `default.css` and `bootstrap.css` once per deployment onto the CDN, links them with a cache-busting version, and adds the role's own `style.css` and `design.js` when the role ships them.
Light and dark tokens switch with `prefers-color-scheme`; a role mirrors an app-native theme switch through `<html data-design-theme="light|dark">`.
## Cosmos
The diagram places Corporate Design Injection for NGINX in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
```mermaid
flowchart LR
subgraph role [sys-front-inj-design 💻]
svc_design["design"]
end
```
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
## Purpose
The goal of this role is to provide a **single source of truth for theming** across your infrastructure.
It makes all applications feel like part of the same ecosystem, visually and functionally.
## Features
- 🎨 **Semantic design tokens** derived from one base color in OKLCH
- ♿ **WCAG 2.2 contrast** guaranteed for text, links, controls and status colors
- 📁 **Unified CSS Base Configuration** deployment for all NGINX applications
- 🌒 **Dark mode** via `prefers-color-scheme` or an app-native switch
- 🧩 **Design script channel** for a role's `design.js.j2`
- 🚫 **No duplication** – tasks run once per deployment
- ⏱️ **Versioning logic** to bust browser cache
- 🎯 **Bootstrap override compatibility**
- 🧩 **Theme support for Keycloak, Nextcloud, Gitea, LAM, Peertube, and more**
## Credits
Implemented by **[Kevin Veen-Birkenbach](https://www.veen.world)**.
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).
@@ -1,7 +1,7 @@
---
galaxy_info:
author: Kevin Veen-Birkenbach
description: Global stylesheet injection for NGINX-served apps, unifying light and dark theming from a dynamic colorscheme
description: Corporate design injection for NGINX-served apps, unifying light and dark theming from OKLCH design tokens
license: Infinito.Nexus Community License (Non-Commercial)
company: |
Kevin Veen-Birkenbach
@@ -1,4 +1,4 @@
---
# nocheck: required-by-coverage — TODO: declare proper required_by
css:
design:
lifecycle: beta
@@ -0,0 +1,15 @@
---
- name: 🎨 Derive the corporate design palette from the base color
set_fact:
CSS_PALETTE: "{{ lookup('design_palette', CSS_BASE_COLOR) }}"
- name: 🚀 Deploy default CSS files
stack_host_template:
src: "{{ ['css', css_file ~ '.j2'] | path_join }}"
dest: "{{ [lookup('cdn', 'web-svc-cdn').shared.css, css_file] | path_join }}"
mode: "0644"
loop: "{{ CSS_FILES }}"
loop_control:
loop_var: css_file
- include_tasks: utils/once/flag.yml
@@ -0,0 +1,17 @@
---
- name: "🏷️ Resolve corporate branding for '{{ application_id }}'"
set_fact:
design_branding: "{{ lookup('design', application_id) }}"
- name: "🖼️ Render the branding assets of '{{ application_id }}'"
when: design_branding.logo is string
vars:
_palette: "{{ lookup('design_palette', CSS_BASE_COLOR) }}"
design_assets:
logo: "{{ design_branding.logo }}"
title: "{{ design_branding.title }}"
domain: "{{ design_branding.domain }}"
fill: "{{ _palette.light['--design-primary'] }}"
stroke: "{{ _palette.light['--design-on-primary'] }}"
slots: "{{ design_branding.slots }}"
dest: "{{ design_assets_dest }}"
+45
View File
@@ -0,0 +1,45 @@
---
- include_tasks: 00_core.yml
when: run_once_sys_front_inj_design is not defined
- name: "🔍 Resolve optional app style.css source for '{{ application_id }}'"
vars:
app_role_dir: "{{ lookup('path_absolute', 'roles', application_id) }}"
_app_style_src: >-
{{ lookup('first_found', {
'files': ['templates/style.css.j2','files/style.css'],
'paths': [app_role_dir]
}, errors='ignore') | default('', true) }}
set_fact:
app_style_src: "{{ _app_style_src }}"
app_style_present: "{{ _app_style_src | length > 0 }}"
- name: "🚀 Deploy per-app '{{ app_style_src }}' to '{{ css_app_dst }}'"
when: app_style_present | bool
copy:
content: "{{ lookup('template', app_style_src) }}"
dest: "{{ css_app_dst }}"
mode: "0644"
- name: "🖼️ Publish corporate branding assets of '{{ application_id }}' on the CDN"
include_tasks: assets.yml
vars:
design_assets_dest: "{{ lookup('design', application_id).dest }}"
- name: "🔍 Resolve optional design script for '{{ application_id }}'"
vars:
_design_js_src: >-
{{ lookup('first_found', {
'files': ['templates/design.js.j2'],
'paths': [lookup('path_absolute', 'roles', application_id)]
}, errors='ignore') | default('', true) }}
set_fact:
design_js_one_liner: "{{ (lookup('template', _design_js_src) | to_one_liner) if _design_js_src | length > 0 else '' }}"
- name: "➕ Append design script CSP hash for '{{ application_id }}'"
when: design_js_one_liner | length > 0
set_fact:
webserver_csp_hashes_extra_by_app: >-
{{ webserver_csp_hashes_extra_by_app | default({})
| add_csp_hash(application_id, 'script-src-elem', design_js_one_liner) }}
changed_when: false
@@ -0,0 +1,112 @@
.btn {
border-radius: var(--design-radius);
cursor: pointer;
}
.btn:focus-visible,
.nav-link:focus-visible {
box-shadow: var(--design-focus-ring);
}
.btn-close:not(.btn-close-white),
.navbar-toggler-icon {
filter: brightness(0) invert(var(--design-invert));
}
{% for fill in ['primary', 'success', 'warning', 'danger', 'info'] %}
.btn-{{ fill }} {
--bs-btn-bg: var(--design-{{ fill }});
--bs-btn-border-color: var(--design-{{ fill }});
--bs-btn-color: var(--design-on-{{ fill }});
--bs-btn-hover-bg: var(--design-{{ fill }}-hover);
--bs-btn-hover-border-color: var(--design-{{ fill }}-hover);
--bs-btn-hover-color: var(--design-on-{{ fill }});
--bs-btn-active-bg: var(--design-{{ fill }}-active);
--bs-btn-active-border-color: var(--design-{{ fill }}-active);
--bs-btn-active-color: var(--design-on-{{ fill }});
--bs-btn-disabled-bg: var(--design-{{ fill }});
--bs-btn-disabled-border-color: var(--design-{{ fill }});
--bs-btn-disabled-color: var(--design-on-{{ fill }});
}
{% endfor %}
.btn-secondary,
.btn-light,
.btn-outline-secondary {
--bs-btn-border-color: var(--design-border);
--bs-btn-color: var(--design-text);
--bs-btn-hover-bg: var(--design-surface-hover);
--bs-btn-hover-border-color: var(--design-border-strong);
--bs-btn-hover-color: var(--design-text);
--bs-btn-active-bg: var(--design-surface-active);
--bs-btn-active-border-color: var(--design-border-strong);
--bs-btn-active-color: var(--design-text);
--bs-btn-disabled-border-color: var(--design-border);
--bs-btn-disabled-color: var(--design-text-muted);
}
.btn-secondary,
.btn-light {
--bs-btn-bg: var(--design-surface-3);
--bs-btn-disabled-bg: var(--design-surface-3);
}
.btn-outline-secondary {
--bs-btn-border-color: var(--design-border-strong);
}
.btn-outline-primary {
--bs-btn-color: var(--design-link);
--bs-btn-border-color: var(--design-link);
--bs-btn-hover-bg: var(--design-primary);
--bs-btn-hover-border-color: var(--design-primary);
--bs-btn-hover-color: var(--design-on-primary);
--bs-btn-active-bg: var(--design-primary-active);
--bs-btn-active-border-color: var(--design-primary-active);
--bs-btn-active-color: var(--design-on-primary);
--bs-btn-disabled-border-color: var(--design-border);
--bs-btn-disabled-color: var(--design-text-muted);
}
.navbar,
.navbar.bg-light,
.navbar.bg-dark {
background-color: var(--design-surface-2);
border-bottom: 1px solid var(--design-border);
--bs-navbar-color: var(--design-text-muted);
--bs-navbar-hover-color: var(--design-text);
--bs-navbar-active-color: var(--design-text);
--bs-navbar-brand-color: var(--design-text);
--bs-navbar-brand-hover-color: var(--design-text);
}
.card {
--bs-card-bg: var(--design-surface-2);
--bs-card-border-color: var(--design-border);
--bs-card-color: var(--design-text);
--bs-card-cap-bg: var(--design-surface-3);
border-radius: var(--design-radius-lg);
box-shadow: var(--design-shadow-1);
}
.dropdown-menu {
--bs-dropdown-bg: var(--design-surface-2);
--bs-dropdown-color: var(--design-text);
--bs-dropdown-border-color: var(--design-border);
--bs-dropdown-link-color: var(--design-text);
--bs-dropdown-link-hover-color: var(--design-text);
--bs-dropdown-link-hover-bg: var(--design-surface-3);
--bs-dropdown-link-active-color: var(--design-on-primary);
--bs-dropdown-link-active-bg: var(--design-primary);
box-shadow: var(--design-shadow-2);
}
.form-control,
.form-select {
background-color: var(--design-surface-2);
color: var(--design-text);
border-color: var(--design-border-strong);
}
.form-control:focus,
.form-select:focus {
border-color: var(--design-link);
box-shadow: var(--design-focus-ring-inset);
}
.alert {
border-radius: var(--design-radius);
}
.modal-content {
--bs-modal-bg: var(--design-surface-2);
--bs-modal-color: var(--design-text);
--bs-modal-border-color: var(--design-border);
box-shadow: var(--design-shadow-3);
}
@@ -0,0 +1,202 @@
{% macro tokens(mode) -%}
{% for name, value in CSS_PALETTE[mode].items() %}
{{ name }}: {{ value }};
{% endfor %}
--design-shadow-color: {{ '0, 0, 0, 0.10' if mode == 'light' else '0, 0, 0, 0.45' }};
--design-invert: {{ '0' if mode == 'light' else '1' }};
{%- endmacro %}
{% if CSS_FONT.import_url %}
@import url('{{ CSS_FONT.import_url }}');
{% endif %}
:root {
{{ tokens('light') }}
}
@media (prefers-color-scheme: dark) {
:root:not([data-design-theme="light"]) {
{{ tokens('dark') }}
}
}
:root[data-design-theme="dark"] {
{{ tokens('dark') }}
}
:root {
--design-font: {{ CSS_FONT.type }};
--design-radius-sm: 4px;
--design-radius: 8px;
--design-radius-lg: 12px;
--design-space-1: 4px;
--design-space-2: 8px;
--design-space-3: 12px;
--design-space-4: 16px;
--design-space-5: 24px;
--design-space-6: 32px;
--design-shadow-1: 0 1px 2px rgba(var(--design-shadow-color));
--design-shadow-2: 0 2px 8px rgba(var(--design-shadow-color));
--design-shadow-3: 0 8px 24px rgba(var(--design-shadow-color));
--design-focus-ring: 0 0 0 3px color-mix(in srgb, var(--design-link) 45%, transparent);
--design-focus-ring-inset: inset 0 0 0 1px var(--design-link);
{% for status in ['success', 'warning', 'danger', 'info'] %}
--design-{{ status }}-subtle: color-mix(in srgb, var(--design-{{ status }}) 12%, var(--design-surface-2));
--design-{{ status }}-border: color-mix(in srgb, var(--design-{{ status }}) 40%, var(--design-surface-2));
{% endfor %}
}
:root {
--bs-font-sans-serif: var(--design-font);
--bs-body-font-family: var(--design-font);
--bs-body-color: var(--design-text);
--bs-body-color-rgb: var(--design-text-rgb);
--bs-body-bg: var(--design-surface-1);
--bs-body-bg-rgb: var(--design-surface-1-rgb);
--bs-emphasis-color: var(--design-text);
--bs-emphasis-color-rgb: var(--design-text-rgb);
--bs-secondary-color: var(--design-text-muted);
--bs-secondary-color-rgb: var(--design-text-muted-rgb);
--bs-secondary-bg: var(--design-surface-3);
--bs-secondary-bg-rgb: var(--design-surface-3-rgb);
--bs-tertiary-color: var(--design-text-muted);
--bs-tertiary-color-rgb: var(--design-text-muted-rgb);
--bs-tertiary-bg: var(--design-surface-2);
--bs-tertiary-bg-rgb: var(--design-surface-2-rgb);
--bs-primary: var(--design-primary);
--bs-primary-rgb: var(--design-primary-rgb);
--bs-secondary: var(--design-neutral-600);
--bs-secondary-rgb: var(--design-neutral-600-rgb);
{% for status in ['success', 'warning', 'danger', 'info'] %}
--bs-{{ status }}: var(--design-{{ status }});
--bs-{{ status }}-rgb: var(--design-{{ status }}-rgb);
--bs-{{ status }}-text-emphasis: var(--design-{{ status }});
--bs-{{ status }}-bg-subtle: var(--design-{{ status }}-subtle);
--bs-{{ status }}-border-subtle: var(--design-{{ status }}-border);
{% endfor %}
--bs-primary-text-emphasis: var(--design-link);
--bs-primary-bg-subtle: var(--design-primary-100);
--bs-primary-border-subtle: var(--design-primary-300);
--bs-light: var(--design-surface-2);
--bs-light-rgb: var(--design-surface-2-rgb);
--bs-dark: var(--design-neutral-900);
--bs-dark-rgb: var(--design-neutral-900-rgb);
--bs-white: var(--design-surface-2);
--bs-white-rgb: var(--design-surface-2-rgb);
--bs-black: var(--design-text);
--bs-black-rgb: var(--design-text-rgb);
--bs-gray: var(--design-neutral-500);
--bs-gray-dark: var(--design-neutral-800);
{% for step in [100, 200, 300, 400, 500, 600, 700, 800, 900] %}
--bs-gray-{{ step }}: var(--design-neutral-{{ step }});
{% endfor %}
--bs-link-color: var(--design-link);
--bs-link-color-rgb: var(--design-link-rgb);
--bs-link-hover-color: var(--design-primary-800);
--bs-link-hover-color-rgb: var(--design-primary-800-rgb);
--bs-code-color: var(--design-danger);
--bs-highlight-color: var(--design-text);
--bs-highlight-bg: var(--design-warning-subtle);
--bs-border-color: var(--design-border);
--bs-border-color-translucent: rgba(var(--design-text-rgb), 0.15);
--bs-border-radius-sm: var(--design-radius-sm);
--bs-border-radius: var(--design-radius);
--bs-border-radius-lg: var(--design-radius-lg);
--bs-box-shadow-sm: var(--design-shadow-1);
--bs-box-shadow: var(--design-shadow-2);
--bs-box-shadow-lg: var(--design-shadow-3);
--bs-focus-ring-color: color-mix(in srgb, var(--design-link) 45%, transparent);
--bs-table-color: var(--design-text);
--bs-table-bg: transparent;
--bs-table-border-color: var(--design-border);
--bs-table-striped-bg: var(--design-surface-3);
--bs-table-hover-bg: rgba(var(--design-primary-rgb), 0.06);
}
/* A cascade layer loses to every unlayered rule, so an app's own design system always wins over these element defaults. */
@layer infinito-design {
:root {
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root:not([data-design-theme="light"]) {
color-scheme: dark;
}
}
:root[data-design-theme="dark"] {
color-scheme: dark;
}
html {
accent-color: var(--design-primary);
scrollbar-color: var(--design-neutral-400) var(--design-surface-1);
}
body {
background-color: var(--design-surface-1);
color: var(--design-text);
font-family: var(--design-font);
}
::selection {
background-color: var(--design-primary-200);
color: var(--design-text);
}
a {
color: var(--design-link);
}
a:hover {
color: var(--bs-link-hover-color);
}
:focus-visible {
outline: 2px solid var(--design-link);
outline-offset: 2px;
}
button {
background-color: var(--design-surface-3);
color: var(--design-text);
border: 1px solid var(--design-border);
border-radius: var(--design-radius);
}
button:hover {
background-color: var(--design-surface-hover);
}
button:active {
background-color: var(--design-surface-active);
}
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
textarea,
select {
background-color: var(--design-surface-2);
color: var(--design-text);
border: 1px solid var(--design-border-strong);
border-radius: var(--design-radius);
}
input::placeholder,
textarea::placeholder {
color: var(--design-text-muted);
}
input:hover,
textarea:hover,
select:hover {
border-color: var(--design-primary);
}
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
border-color: var(--design-link);
outline-offset: -2px;
}
input:user-invalid,
textarea:user-invalid,
select:user-invalid {
border-color: var(--design-danger);
}
input:disabled,
textarea:disabled,
select:disabled {
background-color: var(--design-surface-3);
color: var(--design-text-muted);
}
option {
background-color: var(--design-surface-2);
color: var(--design-text);
}
th, td {
border-color: var(--design-border);
}
thead {
background-color: var(--design-surface-3);
color: var(--design-text);
}
}
@@ -1,4 +1,4 @@
{% set __css_tpl_dir = lookup('path_absolute', 'roles', 'sys-front-inj-css', 'templates', 'css') %}
{% set __css_tpl_dir = lookup('path_absolute', 'roles', 'sys-front-inj-design', 'templates', 'css') %}
{% for css_file in ['default.css','bootstrap.css'] %}
<link rel="stylesheet" href="{{ [ lookup('cdn', 'web-svc-cdn').urls.shared.css, css_file, lookup('local_mtime_qs', [__css_tpl_dir, css_file ~ '.j2'] | path_join)] | url_join }}">
@@ -6,3 +6,6 @@
{% if app_style_present | default(false) | bool %}
<link rel="stylesheet" href="{{ [ lookup('cdn', application_id).urls.role.release.css, 'style.css', lookup('local_mtime_qs', app_style_src)] | url_join }}">
{% endif %}
{% if design_js_one_liner | length > 0 %}
<script>{{ design_js_one_liner }}</script>
{% endif %}
@@ -1,9 +1,8 @@
---
# Constants
CSS_FILES: ["default.css", "bootstrap.css"]
CSS_BASE_COLOR: "{{ design.css.colors.base }}"
CSS_COUNT: 7
CSS_SHADES: 100
CSS_BASE_COLOR: "{{ lookup('config', 'web-svc-design', 'services.design.colors.base') }}"
CSS_FONT: "{{ lookup('config', 'web-svc-design', 'services.design.font') }}"
# Variables
css_app_dst: "{{ [lookup('cdn', application_id).role.release.css, 'style.css'] | path_join }}"
+170
View File
@@ -0,0 +1,170 @@
/**
* Corporate design assertions and the before/after gallery capture shared by
* every role spec whose role consumes the `design` service.
*
* - `assertDesignTokens(page, label)`: the `--design-*` tokens are present and
* switch between light and dark mode.
* - `assertReadable(page, selectors, label, min)`: each visible element's text
* color reaches `min` contrast against its effective background.
* - `galleryEnabled()`: true when `INFINITO_PLAYWRIGHT_KEEP=true`.
* - `captureDesignGallery(page, views)`: per view, color mode and viewport one
* screenshot with the injected CSS/JS (`/reports/design/after/`) and one with
* every injected snippet stripped from the document
* (`/reports/design/before/`), named `<view>-<mode>-<viewport>.png`. Each
* screenshot waits up to `SETTLE_TIMEOUT_MS` for finite animations to end.
*/
const { expect } = require("@playwright/test");
const { gotoOnion } = require("./personas");
const { resolveTimeout } = require("./timeouts");
const MODES = ["light", "dark"];
const VIEWPORTS = {
desktop: { width: 1440, height: 900 },
mobile: { width: 390, height: 844 },
};
const GALLERY_DIR = "/reports/design";
const INJECTED_SNIPPETS = /<!--infinito-inj-->[\s\S]*?<!--\/infinito-inj-->/g;
const SETTLE_TIMEOUT_MS = 5_000;
async function settle(page) {
await page
.waitForFunction(
() =>
document
.getAnimations()
.every((a) => a.playState !== "running" || a.effect?.getComputedTiming().iterations === Infinity),
null,
{ timeout: resolveTimeout(SETTLE_TIMEOUT_MS) },
)
.catch(() => {});
}
async function stripInjectedSnippets(route) {
if (route.request().resourceType() !== "document") return route.continue();
const response = await route.fetch({ maxRedirects: 0 });
if (response.status() >= 300 && response.status() < 400) return route.fulfill({ response });
const body = (await response.text()).replace(INJECTED_SNIPPETS, "");
return route.fulfill({ response, body });
}
async function readTokens(page) {
return page.evaluate(() => {
const probe = (name) => {
const el = document.createElement("span");
el.style.color = `var(${name})`;
document.body.appendChild(el);
const value = getComputedStyle(el).color;
el.remove();
return value;
};
const root = getComputedStyle(document.documentElement);
return {
primary: root.getPropertyValue("--design-primary").trim(),
surface: probe("--design-surface-1"),
text: probe("--design-text"),
};
});
}
async function assertDesignTokens(page, label) {
const seen = {};
for (const mode of MODES) {
await page.emulateMedia({ colorScheme: mode });
seen[mode] = await readTokens(page);
expect(seen[mode].primary, `${label}: --design-primary missing in ${mode} mode`).not.toBe("");
}
expect(
seen.dark.surface,
`${label}: --design-surface-1 must change between light and dark mode`,
).not.toBe(seen.light.surface);
await page.emulateMedia({ colorScheme: null });
}
async function contrastOf(page, selector) {
return page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el || !el.offsetParent) return null;
const rgba = (value) => (value.match(/[\d.]+/g) || []).map(Number);
const background = (node) => {
for (let cur = node; cur; cur = cur.parentElement) {
const [r, g, b, a = 1] = rgba(getComputedStyle(cur).backgroundColor);
if (a > 0.5) return [r, g, b];
}
return [255, 255, 255];
};
const luminance = ([r, g, b]) => {
const lin = (c) => {
const s = c / 255;
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
};
const fg = luminance(rgba(getComputedStyle(el).color));
const bg = luminance(background(el));
return (Math.max(fg, bg) + 0.05) / (Math.min(fg, bg) + 0.05);
}, selector);
}
async function assertReadable(page, selectors, label, min = 4.5) {
for (const mode of MODES) {
await page.emulateMedia({ colorScheme: mode });
for (const selector of selectors) {
const ratio = await contrastOf(page, selector);
if (ratio === null) continue;
expect(
ratio,
`${label}: '${selector}' reaches contrast ${ratio.toFixed(2)} in ${mode} mode (needs ${min})`,
).toBeGreaterThanOrEqual(min);
}
}
await page.emulateMedia({ colorScheme: null });
}
/**
* @param {import('@playwright/test').Page} page
* @param {{name: string, url: string, prepare?: (page) => Promise<void>}[]} views
*/
function galleryEnabled() {
return process.env.INFINITO_PLAYWRIGHT_KEEP === "true";
}
async function captureDesignGallery(page, views) {
if (!galleryEnabled()) return;
const original = page.viewportSize();
const failures = [];
// Chromium counts the fulfilled "before" document as public address space and blocks its cross-origin assets on the local stack.
await page.context().grantPermissions(["local-network-access"]);
for (const side of ["after", "before"]) {
if (side === "before") await page.route("**/*", stripInjectedSnippets);
for (const [viewport, size] of Object.entries(VIEWPORTS)) {
await page.setViewportSize(size);
for (const view of views) {
for (const mode of MODES) {
const file = `${view.name}-${mode}-${viewport}.png`;
try {
await page.emulateMedia({ colorScheme: mode });
await gotoOnion(page, view.url);
if (view.prepare) await view.prepare(page);
await settle(page);
await page.screenshot({ path: `${GALLERY_DIR}/${side}/${file}` });
} catch (error) {
failures.push(`${side}/${file}: ${error.message.split("\n")[0]}`);
}
}
}
}
if (side === "before") await page.unroute("**/*", stripInjectedSnippets);
}
await page.emulateMedia({ colorScheme: null });
if (original) await page.setViewportSize(original);
expect(failures, `design gallery views failed:\n${failures.join("\n")}`).toEqual([]);
}
module.exports = {
assertDesignTokens,
assertReadable,
captureDesignGallery,
galleryEnabled,
};
@@ -8,7 +8,7 @@
* covered by the role's CSP.
*
* The intent is symmetric: when an injector role like `mastodon`,
* `web-svc-asset`, `web-svc-cdn`, `web-svc-css`, `web-svc-javascript`
* `web-svc-asset`, `web-svc-cdn`, `web-svc-design`, `web-svc-javascript`
* or `web-svc-simpleicons` is enabled, its origin MUST appear in the
* CSP. When the injector is disabled, its origin MUST NOT appear in
* any rendered `<script>` / `<link>` / `<img>` tag — otherwise the
@@ -35,8 +35,8 @@ function injectorBaseUrl(service) {
return process.env.ASSET_BASE_URL || "";
case "cdn":
return process.env.CDN_BASE_URL || "";
case "css":
return process.env.CSS_BASE_URL || "";
case "design":
return process.env.DESIGN_BASE_URL || "";
case "javascript":
return process.env.JAVASCRIPT_BASE_URL || "";
case "simpleicons":
@@ -48,7 +48,7 @@ function injectorBaseUrl(service) {
}
}
const INJECTOR_SERVICES = ["asset", "cdn", "css", "javascript", "simpleicons", "matomo"];
const INJECTOR_SERVICES = ["asset", "cdn", "design", "javascript", "simpleicons", "matomo"];
function hostOf(url) {
if (!url) return "";
+3 -3
View File
@@ -35,7 +35,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-akaunting 🐳🐝]
@@ -49,7 +49,7 @@ flowchart LR
svc_akaunting["akaunting"]
svc_redis["redis"]
svc_seaweedfs["seaweedfs"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_container_backup["container_backup"]
@@ -65,7 +65,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
linkStyle 2 stroke:red;
```
+4 -4
View File
@@ -82,11 +82,11 @@ seaweedfs:
bond: 1
enabled: "{{ 'web-app-seaweedfs' in group_names }}"
shared: "{{ 'web-app-seaweedfs' in group_names }}"
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -30,7 +30,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
seaweedfs:
@@ -64,7 +64,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
seaweedfs:
+3 -3
View File
@@ -28,7 +28,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-baserow 🐳🐝]
@@ -43,7 +43,7 @@ flowchart LR
svc_seaweedfs["seaweedfs"]
svc_baserow["baserow"]
svc_baserowmcp["baserowmcp"]
svc_css["css"]
svc_design["design"]
svc_javascript["javascript"]
svc_prometheus["prometheus"]
svc_tor["tor"]
@@ -66,7 +66,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
```
+4 -4
View File
@@ -110,11 +110,11 @@ baserowmcp:
ports:
internal:
http: 8080
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
# nocheck: dynamic-flag
# nocheck: playwright-service-flag — canonical:cdn alias of static-asset provider
javascript:
+2 -2
View File
@@ -32,7 +32,7 @@
seaweedfs:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -75,7 +75,7 @@
seaweedfs:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
+3 -3
View File
@@ -28,7 +28,7 @@ flowchart LR
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_collabora["web-svc-collabora 🐳🐝"]
dep_web_svc_coturn["web-svc-coturn 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-bigbluebutton 🐳🐝]
@@ -43,7 +43,7 @@ flowchart LR
svc_greenlight["greenlight"]
svc_coturn["coturn"]
svc_collabora["collabora"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_bbb_web["bbb-web"]
svc_freeswitch["freeswitch"]
@@ -76,7 +76,7 @@ flowchart LR
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_collabora -. "0..1" .-> svc_collabora
dep_web_svc_coturn -. "0..1" .-> svc_coturn
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
svc_sso -. "0..1" .-> dpt_web_app_nextcloud
```
@@ -87,11 +87,11 @@ collabora:
bond: 1
shared: "{{ 'web-svc-collabora' in group_names }}"
enabled: "{{ 'web-svc-collabora' in group_names }}"
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
@@ -35,7 +35,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -68,7 +68,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-bluesky 🐳🐝]
@@ -40,7 +40,7 @@ flowchart LR
svc_pds["pds"]
svc_broker["broker"]
svc_sso_proxy["sso-proxy"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_api["api"]
svc_bluesky["bluesky"]
@@ -55,7 +55,7 @@ flowchart LR
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
linkStyle 1 stroke:red;
```
+4 -4
View File
@@ -101,11 +101,11 @@ sso-proxy:
mem_reservation: 64m
mem_limit: 256m
pids_limit: 512
# nocheck: playwright-service-flag - css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag - design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -27,7 +27,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -54,7 +54,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-bookwyrm 🐳🐝]
@@ -41,7 +41,7 @@ flowchart LR
svc_worker["worker"]
svc_minio["minio ❌"]
svc_seaweedfs["seaweedfs"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_container_backup["container_backup"]
@@ -56,7 +56,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
```
+7 -7
View File
@@ -1,21 +1,21 @@
.title, .subtitle {
color: var(--color-01-10);
color: var(--design-primary-950);
}
.is-child {
background-color: rgba( var(--color-rgb-01-80), 0.3 );
color: var(--color-01-10);
background-color: rgba( var(--design-primary-300-rgb), 0.3 );
color: var(--design-primary-950);
}
.footer{
background-color: rgba( var(--color-rgb-01-80), 0.3 );
color: var(--color-01-90);
background-color: rgba( var(--design-primary-300-rgb), 0.3 );
color: var(--design-primary-200);
}
.has-background-primary-light{
background-color: rgba( var(--color-rgb-01-80), 0.3 ) !important;
background-color: rgba( var(--design-primary-300-rgb), 0.3 ) !important;
}
.has-background-secondary{
background-color: rgba( var(--color-rgb-01-80), 0.3 ) !important;
background-color: rgba( var(--design-primary-300-rgb), 0.3 ) !important;
}
+4 -4
View File
@@ -112,11 +112,11 @@ seaweedfs:
enabled: "{{ 'web-app-seaweedfs' in group_names }}"
shared: "{{ 'web-app-seaweedfs' in group_names }}"
public: true
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -33,7 +33,7 @@
seaweedfs:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -67,7 +67,7 @@
seaweedfs:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
+3 -3
View File
@@ -21,7 +21,7 @@ flowchart LR
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
end
subgraph role [web-app-bridgy-fed 🐳🐝]
svc_logout["logout ❌"]
@@ -29,7 +29,7 @@ flowchart LR
svc_dashboard["dashboard"]
svc_email["email ❌"]
svc_matomo["matomo"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_bridgy_fed["bridgy-fed"]
svc_tor["tor"]
@@ -40,7 +40,7 @@ flowchart LR
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -- "0..0" --> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
linkStyle 2,5 stroke:red;
```
+4 -4
View File
@@ -27,11 +27,11 @@ matomo:
shared: "{{ 'web-app-matomo' in group_names }}"
# nocheck: matomo-track - root serves Werkzeug's default 404 without </head>/<body>, head injection cannot anchor
track: false
# nocheck: playwright-service-flag - css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag - design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
# nocheck: matomo-track - same 404-without-anchors page, shared CSS <link> cannot inject
track: false
prometheus:
+2 -2
View File
@@ -14,7 +14,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -29,7 +29,7 @@
prometheus:
enabled: false
shared: false
css:
design:
enabled: false
shared: false
tor:
+3 -3
View File
@@ -26,7 +26,7 @@ flowchart LR
dep_web_app_openwebui["web-app-openwebui 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-checkmk 🐳🐝]
@@ -36,7 +36,7 @@ flowchart LR
svc_dashboard["dashboard"]
svc_matomo["matomo"]
svc_email["email"]
svc_css["css"]
svc_design["design"]
svc_javascript["javascript"]
svc_prometheus["prometheus"]
svc_checkmk["checkmk"]
@@ -59,7 +59,7 @@ flowchart LR
dep_web_app_openwebui -. "0..1" .-> svc_openwebui
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
```
+3 -3
View File
@@ -33,10 +33,10 @@ email:
bond: 1
enabled: "{{ MAIL_PROVIDER in group_names }}"
shared: "{{ MAIL_PROVIDER in group_names }}"
css:
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
# nocheck: dynamic-flag - gated on services.sso.enabled, not on group_names
# nocheck: playwright-service-flag - runtime DOM patch (injected logout), no probe-able surface flag
javascript:
+3 -3
View File
@@ -2,7 +2,7 @@
- services:
container_backup:
enabled: true
css:
design:
enabled: true
shared: true
dashboard:
@@ -42,7 +42,7 @@
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
dashboard:
@@ -82,7 +82,7 @@
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
dashboard:
@@ -13,7 +13,7 @@ CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
CHECKMK_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }}
# nocheck: playwright-service-gate - CSS injection is asserted by the central CSP helper, not a per-persona surface
CSS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.css.enabled') | string | lower }}
DESIGN_SERVICE_ENABLED={{ lookup('config', application_id, 'services.design.enabled') | string | lower }}
# nocheck: playwright-service-gate - covered by sys-svc-mail integration tests, not the persona surface
EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }}
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
dep_web_app_keycloak["web-app-keycloak 🐳🐝"]
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
end
subgraph role [web-app-chess 🐳🐝]
svc_logout["logout ❌"]
@@ -34,7 +34,7 @@ flowchart LR
svc_matomo["matomo"]
svc_postgres["postgres"]
svc_chess["chess"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_container_backup["container_backup"]
@@ -46,7 +46,7 @@ flowchart LR
dep_web_app_keycloak -- "0..0" --> svc_sso
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
linkStyle 4 stroke:red;
```
+3 -3
View File
@@ -48,10 +48,10 @@ chess:
proxy:
service_key: chess
lifecycle: beta
css:
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -18,7 +18,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -38,7 +38,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
@@ -2,8 +2,8 @@ APP_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }}
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
# nocheck: playwright-service-gate — web-svc-css injects the shared stylesheet; the persona CSP helper resolves injectors through a computed loop, never a literal gate argument
CSS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.css.enabled') | string | lower }}
# nocheck: playwright-service-gate — web-svc-design injects the shared stylesheet; the persona CSP helper resolves injectors through a computed loop, never a literal gate argument
DESIGN_SERVICE_ENABLED={{ lookup('config', application_id, 'services.design.enabled') | string | lower }}
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
+3 -3
View File
@@ -23,7 +23,7 @@ flowchart LR
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-confluence 🐳🐝]
@@ -34,7 +34,7 @@ flowchart LR
svc_email["email ❌"]
svc_postgres["postgres"]
svc_confluence["confluence"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_container_backup["container_backup"]
@@ -47,7 +47,7 @@ flowchart LR
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -- "0..0" --> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
linkStyle 4,7 stroke:red;
```
+3 -3
View File
@@ -56,10 +56,10 @@ confluence:
run_after:
- web-app-matomo
lifecycle: eol
css:
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -21,7 +21,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -44,7 +44,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
@@ -2,8 +2,8 @@ APP_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }}
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
# nocheck: playwright-service-gate — web-svc-css injects the shared stylesheet; the persona CSP helper resolves injectors through a computed loop, never a literal gate argument
CSS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.css.enabled') | string | lower }}
# nocheck: playwright-service-gate — web-svc-design injects the shared stylesheet; the persona CSP helper resolves injectors through a computed loop, never a literal gate argument
DESIGN_SERVICE_ENABLED={{ lookup('config', application_id, 'services.design.enabled') | string | lower }}
# nocheck: playwright-service-gate — Confluence keeps outbound mail in its own admin UI and the role wires no SMTP env, so no persona-visible surface exists
EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }}
+7 -3
View File
@@ -21,7 +21,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_svc_asset["web-svc-asset 💻"]
dep_web_svc_cdn["web-svc-cdn 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
dep_web_svc_simpleicons["web-svc-simpleicons 🐳🐝"]
end
@@ -33,7 +33,7 @@ flowchart LR
svc_logout["logout"]
svc_matomo["matomo"]
svc_dashboard["dashboard"]
svc_css["css"]
svc_design["design"]
svc_javascript["javascript"]
svc_prometheus["prometheus"]
svc_tor["tor"]
@@ -59,7 +59,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_svc_asset -. "0..1" .-> svc_asset
dep_web_svc_cdn -- "1:1" --> svc_cdn
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
dep_web_svc_simpleicons -. "0..1" .-> svc_simpleicons
svc_sso -- "1:1" --> dpt_more
@@ -137,6 +137,10 @@ docker run --rm -it \
--diff -vv'
```
## Further Resources
- [Corporate design review: before/after screenshots in light, dark, desktop and mobile](https://claude.ai/artifact/J5Qb5EVNRNxnaTkVQZqxDp)
## Persona contract opt-outs
Both authenticated Playwright personas are blocked while `services.sso.enabled` is false. The dashboard's entire Keycloak integration is the browser-side `oidc.js` bundle, and `DASHBOARD_JS_FILES` in `vars/main.yml` ships that file only when SSO is enabled, so with SSO off the page renders neither a Login link nor a logout control.
@@ -17,6 +17,7 @@ require("./test-no-console-noise").register(shared);
// Per-service shared / role-local asset coverage.
require("./test-shared-css").register(shared);
require("./test-design").register(shared);
require("./test-cdn-role-stylesheet").register(shared);
require("./test-logout-js-injection").register(shared);
require("./test-simpleicons-cards").register(shared);
@@ -9,7 +9,7 @@ exports.register = function (shared) {
test("dashboard CDN serves role-local stylesheet when cdn service is enabled", async ({ page }) => {
shared.skipUnlessServiceEnabled("cdn");
shared.skipUnlessServiceEnabled("asset");
shared.skipUnlessServiceEnabled("css");
shared.skipUnlessServiceEnabled("design");
const diagnostics = shared.attachDiagnostics(page);
const documentResponse = await gotoOnion(page,"/");
@@ -0,0 +1,117 @@
const { test } = require("./fixtures/onion-test");
const { assertDesignTokens, captureDesignGallery, galleryEnabled } = require("./design");
const { decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { resolveTimeout } = require("./timeouts");
function baseUrl() {
return decodeDotenvQuotedValue(process.env.APP_BASE_URL).replace(/\/$/, "");
}
async function clickIfVisible(locator) {
if (await locator.isVisible().catch(() => false)) {
await locator.click({ timeout: resolveTimeout(10_000) });
}
}
async function hoverIfVisible(locator) {
if (await locator.isVisible().catch(() => false)) {
await locator.hover({ timeout: resolveTimeout(10_000) });
}
}
function openMenu(menu, pick) {
return async (page) => {
await clickIfVisible(page.locator(`.menu-${menu} .navbar-toggler`).first());
const toggles = page.locator(`.menu-${menu} .nav-item.dropdown > .dropdown-toggle`);
await clickIfVisible(pick === "last" ? toggles.last() : toggles.nth(pick));
};
}
exports.register = function () {
test("design: corporate tokens apply in light and dark mode", async ({ page }) => {
skipUnlessServiceEnabled("design");
await gotoOnion(page, `${baseUrl()}/`);
await assertDesignTokens(page, "dashboard");
});
test("design: gallery of portal states", async ({ page }) => {
skipUnlessServiceEnabled("design");
test.skip(!galleryEnabled(), "INFINITO_PLAYWRIGHT_KEEP is not true");
test.setTimeout(resolveTimeout(900_000));
const base = baseUrl();
await captureDesignGallery(page, [
{ name: "start", url: `${base}/` },
{ name: "start-de", url: `${base}/de` },
{ name: "start-ja", url: `${base}/ja` },
{ name: "start-ar-rtl", url: `${base}/ar` },
{
name: "nav-burger",
url: `${base}/`,
prepare: (page) => clickIfVisible(page.locator(".menu-header .navbar-toggler").first()),
},
{ name: "header-menu-first", url: `${base}/`, prepare: openMenu("header", 0) },
{ name: "header-menu-second", url: `${base}/`, prepare: openMenu("header", 1) },
{ name: "header-menu-last", url: `${base}/`, prepare: openMenu("header", "last") },
{
name: "header-submenu",
url: `${base}/`,
prepare: async (page) => {
await openMenu("header", 0)(page);
await hoverIfVisible(page.locator(".menu-header .dropdown-submenu > .dropdown-toggle").first());
},
},
{
name: "language-item-hover",
url: `${base}/`,
prepare: async (page) => {
await openMenu("header", "last")(page);
await hoverIfVisible(page.locator(".menu-header .dropdown-menu.show .dropdown-item").nth(2));
},
},
{ name: "footer-menu-first", url: `${base}/`, prepare: openMenu("footer", 0) },
{ name: "footer-menu-last", url: `${base}/`, prepare: openMenu("footer", "last") },
{
name: "cards",
url: `${base}/`,
prepare: (page) => page.locator(".card").nth(3).scrollIntoViewIfNeeded().catch(() => {}),
},
{ name: "card-hover", url: `${base}/`, prepare: (page) => hoverIfVisible(page.locator(".card").first()) },
{
name: "focus-ring",
url: `${base}/`,
prepare: async (page) => {
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
},
},
{
name: "tooltip",
url: `${base}/`,
prepare: async (page) => {
await clickIfVisible(page.locator(".menu-header .navbar-toggler").first());
await hoverIfVisible(page.locator('.menu-header .nav-link[data-bs-toggle="tooltip"]').first());
await page.locator(".tooltip.show").waitFor({ state: "visible", timeout: resolveTimeout(10_000) });
},
},
{ name: "fullscreen", url: `${base}/?fullscreen=1` },
{ name: "fullwidth", url: `${base}/?fullwidth=1` },
{
name: "modal-contact",
url: `${base}/`,
prepare: async (page) => {
await openMenu("footer", "last")(page);
await clickIfVisible(page.locator(".menu-footer .dropdown-menu.show .dropdown-item").first());
await page.locator(".modal.show").waitFor({ state: "visible", timeout: resolveTimeout(10_000) });
},
},
{
name: "footer",
url: `${base}/`,
prepare: (page) => page.locator(".footer").scrollIntoViewIfNeeded().catch(() => {}),
},
]);
});
};
@@ -5,6 +5,7 @@ const { decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const platformLogoUrl = decodeDotenvQuotedValue(process.env.PLATFORM_LOGO_URL);
const platformFaviconUrl = decodeDotenvQuotedValue(process.env.PLATFORM_FAVICON_URL);
const platformTitle = decodeDotenvQuotedValue(process.env.PLATFORM_TITLE);
async function getCurrentImageSource(locator) {
return locator.evaluate((img) => img.currentSrc || img.src || "");
@@ -36,7 +37,10 @@ async function expectImageLoaded(locator, label, expectedUrl) {
exports.register = function (shared) {
test("dashboard loads role-core JavaScript modules and renders header/navbar logos", async ({ page }) => {
shared.skipUnlessServiceEnabled("cdn");
shared.skipUnlessServiceEnabled("asset");
test.skip(
!shared.isServiceEnabled("asset") && !shared.isServiceEnabled("design"),
"neither the asset service nor the corporate design provides the platform logo"
);
const diagnostics = shared.attachDiagnostics(page);
const documentResponse = await gotoOnion(page,"/");
@@ -60,6 +64,7 @@ exports.register = function (shared) {
await expectImageLoaded(headerLogo, "Header logo", platformLogoUrl);
await expectImageLoaded(navbarLogo, "Navbar logo", platformLogoUrl);
expect(await getCurrentImageSource(headerLogo)).toBe(await getCurrentImageSource(navbarLogo));
await expect(page.locator("header.header h1").first()).toHaveText(platformTitle);
// Favicon — a <link rel="icon"> is not "visible" in Playwright's sense,
// so just assert that its href is the resolved PLATFORM_FAVICON_URL.
@@ -13,11 +13,20 @@ async function getComputedStyleProperty(locator, propertyName) {
}
async function expectDashboardCssEffects(page) {
const styledCardIcon = page.locator(".card-img-top i").first();
const card = page.locator(".card").first();
if ((await styledCardIcon.count().catch(() => 0)) > 0) {
const iconFilter = await getComputedStyleProperty(styledCardIcon, "filter");
expect(iconFilter, "Expected dashboard card icons to receive the role-local drop shadow style").not.toBe("none");
if ((await card.count().catch(() => 0)) > 0) {
const [actual, expected] = await card.evaluate((element) => {
const probe = document.createElement("span");
probe.style.backgroundColor = "var(--design-surface-2)";
document.body.appendChild(probe);
const token = window.getComputedStyle(probe).backgroundColor;
probe.remove();
return [window.getComputedStyle(element).backgroundColor, token];
});
expect(actual, "Expected dashboard cards to take the corporate surface token instead of the port-ui default").toBe(
expected
);
return;
}
@@ -37,7 +46,7 @@ async function expectDashboardCssEffects(page) {
exports.register = function (shared) {
test("dashboard injects shared CSS assets when css service is enabled", async ({ page }) => {
shared.skipUnlessServiceEnabled("css");
shared.skipUnlessServiceEnabled("design");
const diagnostics = shared.attachDiagnostics(page);
const documentResponse = await gotoOnion(page, "/");
+33 -26
View File
@@ -1,39 +1,46 @@
.card-img-top i {
filter: drop-shadow(4px 4px 4px rgba(var(--color-rgb-01-23), 0.6));
a {
color: var(--design-link);
}
/* Recolor Simple Icons (loaded as <img>) to match the theme accent.
SimpleIcons SVGs ship as flat black; <img> blocks CSS from reaching the
inner paths, so we normalise to pure black via `brightness(0)` and then
re-tint with a filter chain that maps black -> #ffa800-ish theme orange.
The drop-shadow matches the <i>-based icons above. */
.card-img-top img[src*='//icon.'] {
filter:
brightness(0) saturate(100%)
invert(72%) sepia(95%) saturate(2700%) hue-rotate(8deg) brightness(106%) contrast(106%)
drop-shadow(4px 4px 4px rgba(var(--color-rgb-01-23), 0.6));
.navbar,
.card {
background-color: var(--design-surface-2);
border-color: var(--design-border);
color: var(--design-text) !important;
}
.card .btn.stretched-link:hover,
.card .btn.stretched-link:focus,
.card .btn.stretched-link:active {
.navbar {
border-radius: var(--design-radius);
box-shadow: var(--design-shadow-1);
}
main,
header,
footer {
box-shadow: none;
}
.card {
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.card:hover {
filter: none;
border-color: var(--design-primary);
box-shadow: var(--design-shadow-3);
}
.kanban-table-body .kanban-uses-box {
background-color: var(--color-01-81);
}
.kanban-swimlane-title {
border-bottom: none;
/* Simple Icons load as <img>, which CSS color cannot reach: brightness(0) plus the mode inversion renders them in the text tone. */
.card-img-top img[src*='//icon.'] {
filter: brightness(0) invert(var(--design-invert));
}
.navbar-toggler {
background-color: rgba(var(--color-rgb-01-75), 0.9);
border-color: var(--color-01-67)
background-color: var(--design-surface-3);
border-color: var(--design-border-strong);
}
.sys-ctl-alm-info {
--bs-sys-ctl-alm-color: var(--color-03-14);
--bs-sys-ctl-alm-bg: var(--color-01-86);
/* port-ui pins top and bottom of a submenu inline, which collapses its box to the padding. */
.dropdown-submenu > .dropdown-menu {
height: max-content;
}
+8 -3
View File
@@ -55,10 +55,15 @@ dashboard:
run_after:
- web-app-matomo
lifecycle: beta
css:
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
title: "{{ SOFTWARE_NAME }}"
slots:
logo:
width: 512
height: 512
# nocheck: dynamic-flag
# nocheck: playwright-service-flag — canonical:cdn alias of static-asset provider
javascript:
+2 -2
View File
@@ -24,7 +24,7 @@
simpleicons:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -48,7 +48,7 @@
simpleicons:
enabled: false
shared: false
css:
design:
enabled: false
shared: false
tor:
@@ -9,7 +9,7 @@ cards:
{% include 'menu/support.yml.j2' %}
platform:
titel: {{ service_provider.platform.titel | to_json }}
titel: {{ DASHBOARD_PLATFORM_TITLE | to_json }}
subtitel: {{ service_provider.platform.subtitel | to_json }}
logo:
source: {{ DASHBOARD_PLATFORM_LOGO_URL }}
@@ -16,12 +16,12 @@ CDN_BASE_URL={{ lookup('tls', 'web-svc-cdn', 'url.base') | dotenv_quote }}
CDN_SERVICE_ENABLED={{ lookup('config', application_id, 'services.cdn.enabled') | string | lower }}
CSS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.css.enabled') | string | lower }}
DASHBOARD_JS_BASE_URL={{ DASHBOARD_JS_BASE_URL | dotenv_quote }}
DASHBOARD_TARGET_ROLES_JSON={{ lookup('roles_with_service', 'dashboard') | to_json | dotenv_quote }}
DESIGN_SERVICE_ENABLED={{ lookup('config', application_id, 'services.design.enabled') | string | lower }}
LOGIN_PASSWORD={{ lookup('users', 'administrator').password | dotenv_quote }}
LOGIN_USERNAME={{ lookup('users', 'administrator').username | dotenv_quote }}
@@ -44,6 +44,8 @@ PLATFORM_FAVICON_URL={{ DASHBOARD_PLATFORM_FAVICON_URL | dotenv_quote }}
PLATFORM_LOGO_URL={{ DASHBOARD_PLATFORM_LOGO_URL | dotenv_quote }}
PLATFORM_TITLE={{ DASHBOARD_PLATFORM_TITLE | dotenv_quote }}
SIMPLEICONS_SERVICE_ENABLED={{ lookup('config', application_id, 'services.simpleicons.enabled') | string | lower }}
SSO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.sso.enabled') | string | lower }}
+9 -2
View File
@@ -34,10 +34,17 @@ DASHBOARD_OIDC_ENABLED: "{{ lookup('config', DASHBOARD_APPLICATION_ID, 'services
DASHBOARD_CONTAINER: "{{ lookup('config', DASHBOARD_APPLICATION_ID, 'services.dashboard.name') }}"
DASHBOARD_PLATFORM_LOGO_SOURCE: "{{ service_provider.platform.logo | string }}"
DASHBOARD_DESIGN_ENABLED: "{{ lookup('config', DASHBOARD_APPLICATION_ID, 'services.design.enabled') | bool }}"
DASHBOARD_DESIGN_LOGO_ENABLED: "{{ DASHBOARD_DESIGN_ENABLED | bool and lookup('design', DASHBOARD_APPLICATION_ID).logo is string }}"
DASHBOARD_DESIGN_TITLE_ENABLED: "{{ DASHBOARD_DESIGN_ENABLED | bool and lookup('design', DASHBOARD_APPLICATION_ID).title is string }}"
DASHBOARD_PLATFORM_TITLE: "{{ lookup('design', DASHBOARD_APPLICATION_ID).title if DASHBOARD_DESIGN_TITLE_ENABLED | bool else service_provider.platform.titel }}"
DASHBOARD_PLATFORM_LOGO_SOURCE: "{{ lookup('design', DASHBOARD_APPLICATION_ID).urls.logo.png if DASHBOARD_DESIGN_LOGO_ENABLED | bool else service_provider.platform.logo
| string }}"
DASHBOARD_PLATFORM_LOGO_URL: "{{ ('https:' ~ DASHBOARD_PLATFORM_LOGO_SOURCE) if DASHBOARD_PLATFORM_LOGO_SOURCE.startswith('//') else DASHBOARD_PLATFORM_LOGO_SOURCE
}}"
DASHBOARD_PLATFORM_FAVICON_SOURCE: "{{ service_provider.platform.favicon | string }}"
DASHBOARD_PLATFORM_FAVICON_SOURCE: "{{ lookup('design', DASHBOARD_APPLICATION_ID).urls.favicon if DASHBOARD_DESIGN_LOGO_ENABLED | bool else service_provider.platform.favicon
| string }}"
DASHBOARD_PLATFORM_FAVICON_URL: "{{ ('https:' ~ DASHBOARD_PLATFORM_FAVICON_SOURCE) if DASHBOARD_PLATFORM_FAVICON_SOURCE.startswith('//') else DASHBOARD_PLATFORM_FAVICON_SOURCE
}}"
DASHBOARD_COMPANY_LOGO_SOURCE: "{{ service_provider.company.logo | string }}"
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-decidim 🐳🐝]
@@ -37,7 +37,7 @@ flowchart LR
svc_postgres["postgres"]
svc_decidim["decidim"]
svc_redis["redis"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_seaweedfs["seaweedfs"]
svc_tor["tor"]
@@ -53,7 +53,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
```
+5 -4
View File
@@ -65,11 +65,12 @@ redis:
mem_reservation: 128m
mem_limit: 256m
pids_limit: 128
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
title: "{{ service_provider.platform.titel }}"
# nocheck: dynamic-flag
prometheus:
bond: 1
+2 -2
View File
@@ -29,7 +29,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
seaweedfs:
@@ -63,7 +63,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
seaweedfs:
+1 -1
View File
@@ -17,7 +17,7 @@ DECIDIM_OIDC_AUTHORIZE_URL: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
DECIDIM_OIDC_TOKEN_URL: "{{ OIDC.CLIENT.TOKEN_URL }}"
DECIDIM_OIDC_USER_INFO_URL: "{{ OIDC.CLIENT.USER_INFO_URL }}"
DECIDIM_ORGANIZATION_NAME: "{{ defaults_service_provider.platform.titel }}"
DECIDIM_ORGANIZATION_NAME: "{{ lookup('design', application_id).label }}"
DECIDIM_PATCH_SCRIPT_FILE: "patch.py"
DECIDIM_PATCH_SCRIPT_HOST_ABS: "{{ [ lookup('container', application_id, 'directories.volumes'), DECIDIM_PATCH_SCRIPT_FILE ] | path_join }}"
+3 -3
View File
@@ -27,7 +27,7 @@ flowchart LR
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_asset["web-svc-asset 💻"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-discourse 🐳🐝]
@@ -42,7 +42,7 @@ flowchart LR
svc_postgres["postgres"]
svc_redis["redis"]
svc_discourse["discourse"]
svc_css["css"]
svc_design["design"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_container_backup["container_backup"]
@@ -63,7 +63,7 @@ flowchart LR
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_asset -. "0..1" .-> svc_asset
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
svc_litellm -. "0..1" .-> dpt_web_app_nextcloud
svc_litellm -. "0..1" .-> dpt_web_app_wordpress
+4 -4
View File
@@ -86,11 +86,11 @@ discourse:
pids_limit: 1024
local_login_enabled: "{{ not (lookup('config', 'web-app-discourse', 'services.sso.enabled') | bool) or (lookup('config', 'web-app-discourse', 'services.ldap.enabled')
| bool) }}"
# nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css)
css:
# nocheck: playwright-service-flag — design consumer, no Playwright surface (provider: web-svc-design)
design:
bond: 1
enabled: "{{ 'web-svc-css' in group_names }}"
shared: "{{ 'web-svc-css' in group_names }}"
enabled: "{{ 'web-svc-design' in group_names }}"
shared: "{{ 'web-svc-design' in group_names }}"
prometheus:
bond: 1
enabled: "{{ 'web-app-prometheus' in group_names }}"
+2 -2
View File
@@ -32,7 +32,7 @@
prometheus:
enabled: true
shared: true
css:
design:
enabled: true
shared: true
tor:
@@ -68,7 +68,7 @@
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
css:
design:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
tor:
+74 -78
View File
@@ -1,104 +1,100 @@
/* Discourse */
:root section#main{
--primary: var(--color-01-20);
--secondary: var(--color-01-95);
--tertiary: var(--color-01-40);
--quaternary: var(--color-01-50);
--header_background: var(--color-01-86);
--header_primary: var(--color-01-20);
--highlight: var(--color-01-70);
--d-selected: var(--color-01-85);
--d-hover: var(--color-01-90);
--d-sidebar-background: var(--color-01-92);
--d-sidebar-footer-fade: var(--color-01-92);
--always-black-rgb: var(--color-rgb-01-01);
--primary-rgb: var(--color-rgb-01-20);
--primary-low-rgb: var(--color-rgb-01-95);
--primary-very-low-rgb: var(--color-rgb-01-99);
--secondary-rgb: var(--color-rgb-01-95);
--header_background-rgb: var(--color-rgb-01-70);
--tertiary-rgb: var(--color-rgb-01-40);
--highlight-rgb: var(--color-rgb-01-70);
--success-rgb: var(--color-rgb-01-50);
--primary-very-low: var(--color-01-99);
--primary-low: var(--color-01-95);
--primary-low-mid: var(--color-01-75);
--primary-medium: var(--color-01-60);
--primary-high: var(--color-01-40);
--primary-very-high: var(--color-01-20);
--primary-50: var(--color-01-99);
{% for i in range(1, 10) %}
{% set primary = i * 100 %}
{% set color = 100 - i * 8 %}
--primary-{{ primary }}: var(--color-01-{{ "%02d" % color }});
--primary: var(--design-primary-950);
--secondary: var(--design-primary-100);
--tertiary: var(--design-primary-800);
--quaternary: var(--design-primary-600);
--header_background: var(--design-primary-200);
--header_primary: var(--design-primary-950);
--highlight: var(--design-primary-400);
--d-selected: var(--design-primary-200);
--d-hover: var(--design-primary-200);
--d-sidebar-background: var(--design-primary-100);
--d-sidebar-footer-fade: var(--design-primary-100);
--always-black-rgb: var(--design-text-rgb);
--primary-rgb: var(--design-primary-950-rgb);
--primary-low-rgb: var(--design-primary-100-rgb);
--primary-very-low-rgb: var(--design-surface-2-rgb);
--secondary-rgb: var(--design-primary-100-rgb);
--header_background-rgb: var(--design-primary-400-rgb);
--tertiary-rgb: var(--design-primary-800-rgb);
--highlight-rgb: var(--design-primary-400-rgb);
--success-rgb: var(--design-primary-600-rgb);
--primary-very-low: var(--design-surface-2);
--primary-low: var(--design-primary-100);
--primary-low-mid: var(--design-primary-400);
--primary-medium: var(--design-primary-500);
--primary-high: var(--design-primary-800);
--primary-very-high: var(--design-primary-950);
--primary-50: var(--design-surface-2);
{% for step in range(100, 1000, 100) %}
--primary-{{ step }}: var(--design-neutral-{{ step }});
{% endfor %}
--header_primary-low: rgb(var(--color-rgb-01-75));
--header_primary-low-mid: rgb(var(--color-rgb-01-70));
--header_primary-medium: rgb(var(--color-rgb-01-60));
--header_primary-high: rgb(var(--color-rgb-01-50));
--header_primary-very-high: rgb(var(--color-rgb-01-20));
--secondary-low: var(--color-01-20);
--secondary-medium: var(--color-01-40);
--secondary-high: var(--color-01-60);
--secondary-very-high: var(--color-01-90);
--tertiary-very-low: var(--color-01-99);
--tertiary-low: var(--color-01-95);
--tertiary-medium: var(--color-01-60);
--tertiary-high: var(--color-01-40);
--tertiary-hover: var(--color-01-20);
--tertiary-50: var(--color-01-99);
{% for i in range(1, 10) %}
{% set tertiary = i * 100 %}
{% set color = 100 - i * 5 %}
--tertiary-{{ tertiary }}: var(--color-01-{{ "%02d" % color }});
--header_primary-low: rgb(var(--design-primary-400-rgb));
--header_primary-low-mid: rgb(var(--design-primary-400-rgb));
--header_primary-medium: rgb(var(--design-primary-500-rgb));
--header_primary-high: rgb(var(--design-primary-600-rgb));
--header_primary-very-high: rgb(var(--design-primary-950-rgb));
--secondary-low: var(--design-primary-950);
--secondary-medium: var(--design-primary-800);
--secondary-high: var(--design-primary-500);
--secondary-very-high: var(--design-primary-200);
--tertiary-very-low: var(--design-surface-2);
--tertiary-low: var(--design-primary-100);
--tertiary-medium: var(--design-primary-500);
--tertiary-high: var(--design-primary-800);
--tertiary-hover: var(--design-primary-950);
--tertiary-50: var(--design-surface-2);
{% for step in range(100, 1000, 100) %}
--tertiary-{{ step }}: var(--design-primary-{{ step }});
{% endfor %}
--quaternary-low: var(--color-01-80);
--highlight-bg: var(--color-01-90);
--highlight-low: var(--color-01-90);
--highlight-medium: var(--color-01-80);
--highlight-high: var(--color-01-30);
--blend-primary-secondary-5: rgb(var(--color-rgb-01-95));
--primary-med-or-secondary-med: var(--color-01-60);
--primary-med-or-secondary-high: var(--color-01-60);
--primary-high-or-secondary-low: var(--color-01-40);
--primary-low-mid-or-secondary-high: var(--color-01-75);
--primary-low-mid-or-secondary-low: var(--color-01-75);
--primary-or-primary-low-mid: var(--color-01-20);
--highlight-low-or-medium: var(--color-01-90);
--tertiary-or-tertiary-low: var(--color-01-40);
--tertiary-low-or-tertiary-high: var(--color-01-95);
--tertiary-med-or-tertiary: var(--color-01-60);
--secondary-or-primary: var(--color-01-95);
--tertiary-or-white: var(--color-01-40);
--quaternary-low: var(--design-primary-300);
--highlight-bg: var(--design-primary-200);
--highlight-low: var(--design-primary-200);
--highlight-medium: var(--design-primary-300);
--highlight-high: var(--design-primary-900);
--blend-primary-secondary-5: rgb(var(--design-primary-100-rgb));
--primary-med-or-secondary-med: var(--design-primary-500);
--primary-med-or-secondary-high: var(--design-primary-500);
--primary-high-or-secondary-low: var(--design-primary-800);
--primary-low-mid-or-secondary-high: var(--design-primary-400);
--primary-low-mid-or-secondary-low: var(--design-primary-400);
--primary-or-primary-low-mid: var(--design-primary-950);
--highlight-low-or-medium: var(--design-primary-200);
--tertiary-or-tertiary-low: var(--design-primary-800);
--tertiary-low-or-tertiary-high: var(--design-primary-100);
--tertiary-med-or-tertiary: var(--design-primary-500);
--secondary-or-primary: var(--design-primary-100);
--tertiary-or-white: var(--design-primary-800);
--float-kit-arrow-stroke-color: var(--primary-low);
--float-kit-arrow-fill-color: var(--secondary);
--d-content-background: rgba( var(--color-rgb-01-83),1 );
--d-content-background: rgba( var(--design-primary-300-rgb),1 );
}
.search-input--header{
background-color: rgba( var(--color-rgb-01-83),0.2 ) !important;
color: var(--color-01-01)
background-color: rgba( var(--design-primary-300-rgb),0.2 ) !important;
color: var(--design-text)
}
div#main-outlet, #list-area{
background-color: rgba( var(--color-rgb-02-90),1 ) !important;
background-color: rgba( var(--design-primary-200-rgb),1 ) !important;
}
.list-controls{
background-color: rgba( var(--color-rgb-01-90), 0.9) !important;
background-color: rgba( var(--design-primary-200-rgb), 0.9) !important;
border-radius: 30px;
}
.topic-list-item{
background-color: rgba( var(--color-rgb-01-85), 0.4) !important;
background-color: rgba( var(--design-primary-200-rgb), 0.4) !important;
}
.topic-list-item a{
color: var(--color-01-10) !important;
color: var(--design-primary-950) !important;
}
div#main-outlet div.regular.ember-view{
background-color: rgba( var(--color-rgb-01-85),0.3 );
background-color: rgba( var(--design-primary-200-rgb),0.3 );
border-radius: 06px;
}
button.btn{
background-color: rgba( var(--color-rgb-01-85),0.9 );
background-color: rgba( var(--design-primary-200-rgb),0.9 );
}
div.timeline-scrollarea div.timeline-scroller{
background-color: rgba( var(--color-rgb-01-85),0.9 ) !important;
background-color: rgba( var(--design-primary-200-rgb),0.9 ) !important;
border-radius: 06px;
}
+3 -3
View File
@@ -25,7 +25,7 @@ flowchart LR
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
dep_web_svc_css["web-svc-css 💻"]
dep_web_svc_design["web-svc-design 💻"]
dep_web_svc_logout["web-svc-logout 🐳🐝"]
end
subgraph role [web-app-erpnext 🐳🐝]
@@ -37,7 +37,7 @@ flowchart LR
svc_matomo["matomo"]
svc_prometheus["prometheus"]
svc_tor["tor"]
svc_css["css"]
svc_design["design"]
svc_mariadb["mariadb"]
svc_redis["redis"]
svc_erpnext["erpnext"]
@@ -60,7 +60,7 @@ flowchart LR
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
dep_web_app_stalwart -. "0..1" .-> svc_email
dep_web_svc_css -. "0..1" .-> svc_css
dep_web_svc_design -. "0..1" .-> svc_design
dep_web_svc_logout -. "0..1" .-> svc_logout
```
Loaded 100 of 427 files, more files were not shown because too many files have changed in this diff. Show more