Huge role refactoring/cleanup. Other commits will propably follow. Because some bugs will exist. Still important for longrun and also for auto docs/help/slideshow generation

This commit is contained in:
2025-07-08 23:43:13 +02:00
parent 6b87a049d4
commit 563d5fd528
1242 changed files with 2301 additions and 1355 deletions

View File

@@ -0,0 +1,3 @@
# Administrator Notes
It is recommended to don't run this role seperate, because it builds the portfolio page dynamic by checking if applications are in group_names.

View File

@@ -0,0 +1,34 @@
# Portfolio
## Description
This Ansible role deploys and manages a Flask-based [portfolio application](https://github.com/kevinveenbirkenbach/portfolio) in a Docker container. It provides a user-friendly and customizable way to showcase your projects, services, or creative work online. The role leverages Docker, Docker Compose, and integrated web server configurations to ensure a smooth deployment experience.
## Overview
Tailored for creative professionals and developers, this role streamlines the process of setting up a portfolio site. It automates tasks such as Docker container configuration, dynamic routing via Nginx, and repository integration, so you can concentrate on perfecting your content and design. Enjoy a responsive layout and easy-to-modify YAML files that let you rapidly update your online presence without deep technical intervention.
## Purpose
The purpose of the Docker-Portfolio role is to simplify the deployment and management of a personal or professional portfolio. By focusing on usability and a clean presentation, the role helps you:
- Quickly launch a professional-looking website.
- Customize and update your portfolio content effortlessly.
- Integrate seamlessly with complementary roles for Docker Compose and web server management.
- Reduce manual configuration and maintenance tasks.
## Features
- **Flask-Based Portfolio App:** Deploy a modern portfolio application designed to highlight your work.
- **Containerized Deployment:** Uses Docker and Docker Compose for easy, isolated, and reproducible deployment.
- **Customizable Configuration:** Adjust the content and appearance through simple YAML configuration files.
- **Responsive Design:** Optimized for devices of any size, ensuring your portfolio always looks great.
- **Integrated Routing:** Works alongside Nginx-domain and repository setup roles to provide reliable domain routing and version control.
- **Automated Updates:** Automatically re-deploys changes when configuration files are updated, keeping your portfolio up-to-date.
## Credits 📝
Developed and maintained by **Kevin Veen-Birkenbach**.
Learn more at [www.veen.world](https://www.veen.world)
Part of the [CyMaIS Project](https://github.com/kevinveenbirkenbach/cymais)
License: [CyMaIS NonCommercial License (CNCL)](https://s.veen.world/cncl)

View File

@@ -0,0 +1,25 @@
class FilterModule(object):
'''Custom filters for Ansible'''
def filters(self):
return {
'any_in': self.any_in,
}
def any_in(self, list1, list2):
"""
Checks if at least one element from list1 is found in list2.
:param list1: List of elements to check.
:param list2: Target list in which to search for elements.
:return: True if at least one element is found, otherwise False.
"""
# If either parameter is not a list, return False.
if not isinstance(list1, list) or not isinstance(list2, list):
return False
# Iterate over list1 and check if an element exists in list2.
for element in list1:
if element in list2:
return True
return False

View File

@@ -0,0 +1,58 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import requests
import re
from ansible.errors import AnsibleFilterError
def slugify(name):
"""Convert a display name to a simple-icons slug format."""
# Replace spaces and uppercase letters
return re.sub(r'\s+', '', name.strip().lower())
def add_simpleicon_source(cards, domains, web_protocol='https'):
"""
For each card in portfolio_cards, check if an icon exists in the simpleicons server.
If it does, add icon.source with the URL to the card entry.
:param cards: List of card dictionaries (portfolio_cards)
:param domains: Mapping of application_id to domain names
:param web_protocol: Protocol to use (https or http)
:return: New list of cards with icon.source set when available
"""
# Determine simpleicons service domain
simpleicons_domain = domains.get('simpleicons')
if isinstance(simpleicons_domain, list):
simpleicons_domain = simpleicons_domain[0]
if not simpleicons_domain:
raise AnsibleFilterError("Domain for 'simpleicons' not found in domains mapping")
base_url = f"{web_protocol}://{simpleicons_domain}"
enhanced = []
for card in cards:
title = card.get('title', '')
if not title:
enhanced.append(card)
continue
# Create slug from title
slug = slugify(title)
icon_url = f"{base_url}/{slug}.svg"
try:
resp = requests.head(icon_url, timeout=2)
if resp.status_code == 200:
card.setdefault('icon', {})['source'] = icon_url
except requests.RequestException:
# Ignore network errors and move on
pass
enhanced.append(card)
return enhanced
class FilterModule(object):
"""Ansible filter plugin to add simpleicons source URLs to portfolio cards"""
def filters(self):
return {
'add_simpleicon_source': add_simpleicon_source,
}

View File

@@ -0,0 +1,121 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import glob
import os
import re
import yaml
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
"""
This lookup iterates over all roles whose folder name starts with 'web-app-'
and generates a list of dictionaries (cards). For each role, it:
- Extracts the application_id (everything after "web-app-")
- Reads the title from the role's README.md (the first H1 line)
- Retrieves the description from galaxy_info.description in meta/main.yml
- Retrieves the icon class from galaxy_info.logo.class
- Retrieves the tags from galaxy_info.galaxy_tags
- Builds the URL using the 'domains' variable (e.g. domains | get_domain(application_id))
- Sets the iframe flag from applications[application_id].features.iframe
Only cards whose application_id is included in the variable group_names are returned.
"""
# Default to "roles" directory if no path is provided
roles_dir = terms[0] if len(terms) > 0 else "roles"
cards = []
# Retrieve group_names from variables (used to filter roles)
group_names = variables.get("group_names", [])
# Search for all roles starting with "web-app-"
pattern = os.path.join(roles_dir, "web-app-*")
for role_path in glob.glob(pattern):
role_dir = role_path.rstrip("/")
role_basename = os.path.basename(role_dir)
# Skip roles not starting with "web-app-"
if not role_basename.startswith("web-app-"):
continue
# Extract application_id from role name
application_id = role_basename[len("web-app-"):]
# Skip roles not listed in group_names
if application_id not in group_names:
continue
# Define paths to README.md and meta/main.yml
readme_path = os.path.join(role_dir, "README.md")
meta_path = os.path.join(role_dir, "meta", "main.yml")
# Skip role if required files are missing
if not os.path.exists(readme_path) or not os.path.exists(meta_path):
continue
# Extract title from first H1 line in README.md
try:
with open(readme_path, "r", encoding="utf-8") as f:
readme_content = f.read()
title_match = re.search(r'^#\s+(.*)$', readme_content, re.MULTILINE)
title = title_match.group(1).strip() if title_match else application_id
except Exception as e:
raise AnsibleError("Error reading '{}': {}".format(readme_path, str(e)))
# Extract metadata from meta/main.yml
try:
with open(meta_path, "r", encoding="utf-8") as f:
meta_data = yaml.safe_load(f)
galaxy_info = meta_data.get("galaxy_info", {})
# If display is set to False ignore it
if not galaxy_info.get("display", True):
continue
description = galaxy_info.get("description", "")
logo = galaxy_info.get("logo", {})
icon_class = logo.get("class", "fa-solid fa-cube")
tags = galaxy_info.get("galaxy_tags", [])
except Exception as e:
raise AnsibleError("Error reading '{}': {}".format(meta_path, str(e)))
# Retrieve domains and applications from the variables
domains = variables.get("domains", {})
applications = variables.get("applications", {})
domain_url = domains.get(application_id, "")
if isinstance(domain_url, list):
domain_url = domain_url[0]
elif isinstance(domain_url, dict):
domain_url = next(iter(domain_url.values()))
# Construct the URL using the domain_url if available.
url = "https://" + domain_url if domain_url else ""
app_data = applications.get(application_id, {})
iframe = app_data.get("features", {}).get("portfolio_iframe", False)
# Build card dictionary
card = {
"icon": {"class": icon_class},
"title": title,
"text": description,
"url": url,
"link_text": "Explore {}".format(title),
"iframe": iframe,
"tags": tags,
}
cards.append(card)
# Sort A-Z
cards.sort(key=lambda c: c['title'].lower())
# Return the list of cards
return [cards]

View File

@@ -0,0 +1,38 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
"""
Group the given cards into categorized and uncategorized lists
based on the tags from menu_categories.
"""
if len(terms) < 2:
raise AnsibleError("Missing required arguments")
cards = terms[0]
menu_categories = terms[1]
categorized = {}
uncategorized = []
for card in cards:
found = False
for category, data in menu_categories.items():
if any(tag in data.get('tags', []) for tag in card.get('tags', [])):
categorized.setdefault(category, []).append(card)
found = True
break
if not found:
uncategorized.append(card)
return [
{
'categorized': categorized,
'uncategorized': uncategorized,
}
]

View File

@@ -0,0 +1,28 @@
---
galaxy_info:
author: "Kevin Veen-Birkenbach"
description: "Portfolio to showcase your projects and creative work with a focus on user experience and easy customization. 🚀"
license: "CyMaIS NonCommercial License (CNCL)"
license_url: "https://s.veen.world/cncl"
company: |
Kevin Veen-Birkenbach
Consulting & Coaching Solutions
https://www.veen.world
min_ansible_version: "2.9"
platforms:
- name: Docker
versions:
- latest
galaxy_tags:
- docker
- portfolio
- ansible
- flask
- web
repository: "https://github.com/kevinveenbirkenbach/portfolio"
issue_tracker_url: "https://github.com/kevinveenbirkenbach/portfolio/issues"
documentation: "https://github.com/kevinveenbirkenbach/portfolio#readme"
logo:
class: "fa-solid fa-briefcase"
run_after:
- web-app-simpleicons

View File

@@ -0,0 +1,85 @@
---
- name: "include docker-compose role"
include_role:
name: docker-compose
when: run_once_docker_portfolio is not defined
- name: "include role webserver-proxy-domain for {{application_id}}"
include_role:
name: webserver-proxy-domain
vars:
domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}"
when: run_once_docker_portfolio is not defined
- name: "Check if host-specific config.yaml exists in {{ config_inventory_path }}"
stat:
path: "{{ config_inventory_path }}"
delegate_to: localhost
become: false
register: config_file
when: run_once_docker_portfolio is not defined
- name: Load menu categories
include_vars:
file: "menu_categories.yml"
when: run_once_docker_portfolio is not defined
- name: Load docker cards
set_fact:
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
when: run_once_docker_portfolio is not defined
- name: "Load images for applications feature simpleicons is enabled "
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(domains, web_protocol) }}"
when:
- (applications | is_feature_enabled('simpleicons',application_id))
- run_once_docker_portfolio is not defined
- name: Group docker cards
set_fact:
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
when: run_once_docker_portfolio is not defined
- name: Debug portfolio data
debug:
msg:
portfolio_cards: "{{ portfolio_cards }}"
portfolio_menu_categories: "{{ portfolio_menu_categories}}"
portfolio_menu_data: "{{ portfolio_menu_data }}"
service_provider: "{{ service_provider }}"
when:
- enable_debug | bool
- run_once_docker_portfolio is not defined
- name: Copy host-specific config.yaml if it exists
template:
src: "{{ config_inventory_path }}"
dest: "{{docker_repository_path}}/app/config.yaml"
notify: docker compose up
when:
- run_once_docker_portfolio is not defined
- config_file.stat.exists
- name: Copy default config.yaml from the role template if host-specific file does not exist
template:
src: "config.yaml.j2"
dest: "{{docker_repository_path}}/app/config.yaml"
notify: docker compose up
when:
- run_once_docker_portfolio is not defined
- not config_file.stat.exists
- name: add docker-compose.yml
template:
src: docker-compose.yml.j2
dest: "{docker_compose.directories.instance}}docker-compose.yml"
notify: docker compose up
when: run_once_docker_portfolio is not defined
- name: run the portfolio tasks once
set_fact:
run_once_docker_portfolio: true
when: run_once_docker_portfolio is not defined

View File

@@ -0,0 +1,30 @@
{# The Linebreak here are intentional due to tab bugs #}
---
cards:
{{ portfolio_cards | to_nice_yaml(indent=2) }}
{% include 'menu/applications.yml.j2' %}
{% include 'menu/followus.yml.j2' %}
{% include 'menu/contact.yml.j2' %}
{% include 'menu/support.yml.j2' %}
platform:
titel: {{service_provider.platform.titel}}
subtitel: {{service_provider.platform.subtitel}}
logo:
source: {{service_provider.platform.logo}}
favicon:
source: {{service_provider.platform.favicon}}
company:
titel: {{service_provider.company.titel}}
subtitel: {{service_provider.company.slogan}}
logo:
source: {{service_provider.company.logo}}
address:
{{ service_provider.company.address | to_nice_yaml(indent=4) | indent(4, true) }}
imprint_url: {{service_provider.legal.imprint}}
navigation:
{% include 'menu/header.yml.j2' %}
{% include 'menu/footer.yml.j2' %}

View File

@@ -0,0 +1,17 @@
{% include 'roles/docker-compose/templates/base.yml.j2' %}
portfolio:
{% set container_port = 5000 %}
build:
context: {{docker_repository_path}}
dockerfile: Dockerfile
image: application-portfolio
container_name: portfolio
ports:
- 127.0.0.1:{{ports.localhost.http[application_id]}}:{{ container_port }}
volumes:
- {{docker_repository_path}}app:/app
restart: unless-stopped
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %}
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -0,0 +1,30 @@
window.addEventListener("message", function(event) {
const allowedSuffix = ".{{ primary_domain }}";
const origin = event.origin;
// 1. Only allow messages from *.{{ primary_domain }}
if (!origin.endsWith(allowedSuffix)) return;
const data = event.data;
// 2. Only process valid iframeLocationChange messages
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
try {
const hrefUrl = new URL(data.href);
// 3. Only allow redirects to *.{{ primary_domain }}
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
// 4. Update the ?iframe= parameter in the browser URL
const newUrl = new URL(window.location);
newUrl.searchParams.set("iframe", hrefUrl.href);
window.history.replaceState({}, "", newUrl);
} catch (e) {
// Invalid or malformed URL ignore
}
}
});
{% if enable_debug | bool %}
console.log("[iframe-sync] Listener for iframe messages is active.");
{% endif %}

View File

@@ -0,0 +1,77 @@
applications:
{% if (portfolio_menu_data.categorized is mapping and portfolio_menu_data.categorized | length > 0)
or (portfolio_menu_data.uncategorized is sequence and portfolio_menu_data.uncategorized | length > 0) %}
- name: Applications
description: Browse, configure and launch all available applications
icon:
class: fa fa-th-large
children:
{# Render all categories #}
{% for category, apps in portfolio_menu_data.categorized.items() %}
- name: {{ category }}
description: {{ portfolio_menu_categories[category].description }}
icon:
class: {{ portfolio_menu_categories[category].icon }}
children:
{% for app in apps %}
- name: {{ app.title }}
description: {{ app.text }}
icon: {{ app.icon }}
url: {{ app.url }}
iframe: {{ app.iframe }}
{% if app.title == 'Keycloak' %}
children:
- name: Administration
description: Access the central admin console
icon:
class: fa-solid fa-shield-halved
url: https://{{domains | get_domain('keycloak')}}/admin
iframe: {{ applications | is_feature_enabled('portfolio_iframe','keycloak') }}
- name: Profile
description: Update your personal admin settings
icon:
class: fa-solid fa-user-gear
url: https://{{ domains | get_domain('keycloak') }}/realms/{{oidc.client.id}}/account
iframe: {{ applications | is_feature_enabled('portfolio_iframe','keycloak') }}
- name: Logout
description: End your admin session securely
icon:
class: fa-solid fa-right-from-bracket
url: https://{{ domains | get_domain('keycloak') }}/realms/{{oidc.client.id}}/protocol/openid-connect/logout
iframe: false
{% endif %}
{% endfor %}
{% endfor %}
{# Render Uncategorized #}
{% if portfolio_menu_data.uncategorized %}
- name: Uncategorized
description: Tools without a defined category
icon:
class: fa-solid fa-question
children:
{% for app in portfolio_menu_data.uncategorized %}
- name: {{ app.title }}
description: {{ app.text }}
icon: {{ app.icon }}
url: {{ app.url }}
iframe: {{ app.iframe }}
{% endfor %}
{% endif %}
{% endif %}

View File

@@ -0,0 +1,37 @@
contact:
name: Contact
description: Get in touch with {{ 'us' if service_provider.type == 'legal' else 'me' }}
icon:
class: fa-solid fa-envelope
children:
{% if service_provider.contact.email is defined %}
- name: Email
description: Send {{ 'us' if service_provider.type == 'legal' else 'me' }} an email
icon:
class: fa-solid fa-envelope
url: mailto:{{service_provider.contact.email}}
identifier: {{service_provider.contact.email}}
{% endif %}
{% if service_provider.contact.phone is defined %}
- name: Mobile
description: Call {{ 'us' if service_provider.type == 'legal' else 'me' }}
icon:
class: fa-solid fa-phone
url: "tel:{{service_provider.contact.phone}}"
identifier: "{{service_provider.contact.phone}}"
target: _top
{% endif %}
{% if service_provider.contact.matrix is defined %}
- name: Matrix
description: Chat with {{ 'us' if service_provider.type == 'legal' else 'me' }} on Matrix
icon:
class: fa-solid fa-cubes
identifier: "{{service_provider.contact.matrix}}"
{% endif %}

View File

@@ -0,0 +1,62 @@
followus:
name: Follow Us
description: Follow us to stay up to recieve the newest CyMaIS updates
icon:
class: fas fa-newspaper
{% if ["mastodon", "bluesky"] | any_in(group_names) %}
children:
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
- name: Mastodon
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} updates on Mastodon.
icon:
class: fa-brands fa-mastodon
url: "{{ web_protocol }}://{{ service_provider.contact.mastodon.split('@')[2] }}/@{{ service_provider.contact.mastodon.split('@')[1] }}"
identifier: "{{service_provider.contact.mastodon}}"
iframe: {{ applications | is_feature_enabled('portfolio_iframe','mastodon') }}
{% endif %}
{% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %}
- name: Bluesky
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} on Bluesky.
icon:
class: fa-brands fa-bluesky
alternatives:
- link: followus.microblogs.mastodon
identifier: "{{service_provider.contact.bluesky}}"
{% endif %}
{% endif %}
{% if service_provider.contact.pixelfed is defined and service_provider.contact.pixelfed != "" %}
- name: Pixelfed
description: Explore {{ 'our' if service_provider.type == 'legal' else 'my' }} photo gallery on Pixelfed.
icon:
class: fa-solid fa-camera
identifier: "{{service_provider.contact.pixelfed}}"
url: "{{ web_protocol }}://{{ service_provider.contact.pixelfed.split('@')[2] }}/@{{ service_provider.contact.pixelfed.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('portfolio_iframe','pixelfed') }}
{% endif %}
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
- name: Peertube
description: Discover {{ 'our' if service_provider.type == 'legal' else 'my' }} videos on Peertube.
icon:
class: fa-solid fa-video
identifier: "{{service_provider.contact.peertube}}"
url: "{{ web_protocol }}://{{ service_provider.contact.peertube.split('@')[2] }}/@{{ service_provider.contact.peertube.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('portfolio_iframe','peertube') }}
{% endif %}
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
- name: Wordpress
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
icon:
class: fa-solid fa-blog
identifier: "{{service_provider.contact.wordpress}}"
url: "{{ web_protocol }}://{{ service_provider.contact.wordpress.split('@')[2] }}/@{{ service_provider.contact.wordpress.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('portfolio_iframe','wordpress') }}
{% endif %}
{% if service_provider.contact.friendica is defined and service_provider.contact.friendica != "" %}
- name: Friendica
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} friendica profile
icon:
class: fas fa-network-wired
identifier: "{{service_provider.contact.friendica}}"
url: "{{ web_protocol }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('portfolio_iframe','friendica') }}
{% endif %}

View File

@@ -0,0 +1,48 @@
footer:
children:
# - link: support
{% if "sphinx" in group_names %}
- name: Documentation
description: Access our comprehensive documentation and support resources to help you get the most out of the software.
icon:
class: fas fa-book
url: https://{{domains | get_domain('sphinx')}}
iframe: {{ applications | is_feature_enabled('portfolio_iframe','sphinx') }}
{% endif %}
{% if "presentation" in group_names %}
- name: Slides
description: Checkout the presentation
icon:
class: "fas fa-chalkboard-teacher"
url: https://{{domains | get_domain('presentation')}}
iframe: {{ applications | is_feature_enabled('portfolio_iframe','presentation') }}
{% endif %}
- name: Solutions
description: "Software and IT Infrastructure Solutions by Kevin Veen-Birkenbach"
icon:
class: fa-solid fa-rocket
url: "https://cybermaster.space/"
iframe: false
{% if service_provider.legal.source_code is defined and service_provider.legal.source_code != "" %}
- name: Source Code
description: Explore {{ 'our' if service_provider.type == 'legal' else 'my' }} code.
icon:
class: fa-solid fa-code
url: "{{service_provider.legal.source_code}}"
{% endif %}
- link: followus
- link: contact
- name: Imprint
description: Check out the imprint information
icon:
class: fa-solid fa-scale-balanced
url: "{{service_provider.legal.imprint}}"
iframe: true

View File

@@ -0,0 +1,14 @@
header:
children:
- link: applications
- name: Toggle
description: Enter or exit fullscreen mode
icon:
class: fa-solid fa-expand-arrows-alt
onclick: "toggleFullscreen()"
- name: Tab
description: Open the currently embedded iframe URL in a fresh browser tab
icon:
class: fa-solid fa-up-right-from-square
onclick: openIframeInNewTab()

View File

@@ -0,0 +1,26 @@
support:
name: Support Us
description: "Discover all the ways you can support our work."
icon:
class: fa-solid fa-hands-helping
children:
- name: Buy me a Coffee
description: "Support our work with a coffee every cup helps!"
icon:
class: fa-solid fa-mug-hot
url: https://s.veen.world/buymeacoffee
- name: Patreon
description: "Become a member and support me monthly with exclusive content."
icon:
class: fa-brands fa-patreon
url: https://s.veen.world/patreon
- name: PayPal
description: "Donate to our open source projects with a one-time or monthly PayPal contribution."
icon:
class: fa-brands fa-paypal
url: https://s.veen.world/paypaldonate
- name: GitHub Sponsors
description: "Directly support our projects through GitHub Sponsors."
icon:
class: fa-brands fa-github
url: https://s.veen.world/

View File

@@ -0,0 +1,32 @@
features:
matomo: true
css: true
portfolio_iframe: false
simpleicons: true # Activate Brand Icons for your groups
javascript: true # Necessary for URL sync
csp:
whitelist:
script-src-elem:
- https://cdn.jsdelivr.net
- https://kit.fontawesome.com
- https://code.jquery.com/
style-src:
- https://cdn.jsdelivr.net
font-src:
- https://ka-f.fontawesome.com
- https://cdn.jsdelivr.net
connect-src:
- https://ka-f.fontawesome.com
frame-src:
- "{{ web_protocol }}://*.{{primary_domain}}"
flags:
style-src:
unsafe-inline: true
script-src:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
domains:
canonical:
- "{{ primary_domain }}"

View File

@@ -0,0 +1,4 @@
application_id: "portfolio"
docker_repository_address: "https://github.com/kevinveenbirkenbach/portfolio"
config_inventory_path: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/portfolio/config.yaml.j2"
docker_repository: true

View File

@@ -0,0 +1,171 @@
portfolio_menu_categories:
Community:
description: "Tools to manage the community"
icon: "fa-solid fa-users"
tags:
- community
- forum
- learning
- newsletter
- discourse
- listmonk
- moodle
- mybb
- mobilizon
- friendica
Project Management:
description: "Project Management Tools"
icon: "fa-solid fa-chart-line"
tags:
- project
- kanban
- openproject
- taiga
- espocrm
Social Media:
description: "Social Media Tools"
icon: "fa-solid fa-share-nodes"
tags:
- microblog
- blog
- video platform
- streaming platform
- music platform
- social network
- bluesky
- funkwhale
- mastodon
- peertube
- pixelfed
- friendica
Communication:
description: "Tools for communication"
icon: "fa-solid fa-comments"
tags:
- chat
- communication
- video
- mail
- email
- bigbluebutton
- etherpad
- mailu
- matrix
- xmpp
Cloud:
description: "Self-hosted cloud solutions for file synchronization, collaboration, and data sharing."
icon: "fa-solid fa-cloud"
tags:
- nextcloud
- owncloud
- cloud
IAM:
description: "Tools for Identity and Access Management, including authentication, user provisioning, and secure access control."
icon: "fa-solid fa-user-shield"
tags:
- iam
- identity-management
- authentication
- access-control
- sso
- keycloak
- lam
- ldap
- fusiondirectory
- user-management
Server Administration:
description: "Administration Tools für servers"
icon: "fas fa-building"
tags:
- administration
- database
- central-database
- elk
- mariadb
- matomo
- pgadmin
- phpldapadmin
- phpmyadmin
- postgres
Tools:
description: "Helpful Tools"
icon: "fas fa-tools"
tags:
- tools
- utility
- baserow
- compose
- repository-setup
- roulette-wheel
- yourls
Presentation:
description: "Presentation and Documentation Tools"
icon: "fas fa-tools"
tags:
- presentation
- sphinx
- portfolio
Finance & Accounting:
description: "Financial and accounting software"
icon: "fa-solid fa-dollar-sign"
tags:
- finance
- accounting
- invoices
- akaunting
- snipe-it
Events:
description: "Event and ticket management tools"
icon: "fa-solid fa-ticket-alt"
tags:
- events
- ticketing
- attendize
Infrastructure:
description: "Infrastructure and networking tools"
icon: "fa-solid fa-network-wired"
tags:
- infrastructure
- networking
- proxy
- turn
- stun
- coturn
- oauth2-proxy
- registry
Development:
description: "Development and CI/CD tools"
icon: "fa-solid fa-code-branch"
tags:
- development
- version control
- ci/cd
- git
- gitea
- gitlab
- jenkins
Content Management:
description: "CMS and web publishing platforms"
icon: "fa-solid fa-file-alt"
tags:
- cms
- blogging
- publishing
- website
- joomla
- mediawiki
- wordpress