mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
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:
3
roles/web-app-portfolio/Administrator.md
Normal file
3
roles/web-app-portfolio/Administrator.md
Normal 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.
|
34
roles/web-app-portfolio/README.md
Normal file
34
roles/web-app-portfolio/README.md
Normal 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)
|
25
roles/web-app-portfolio/filter_plugins/list_in_filter.py
Normal file
25
roles/web-app-portfolio/filter_plugins/list_in_filter.py
Normal 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
|
58
roles/web-app-portfolio/filter_plugins/simpleicons_source.py
Normal file
58
roles/web-app-portfolio/filter_plugins/simpleicons_source.py
Normal 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,
|
||||
}
|
0
roles/web-app-portfolio/lookup_plugins/__init__.py
Normal file
0
roles/web-app-portfolio/lookup_plugins/__init__.py
Normal file
121
roles/web-app-portfolio/lookup_plugins/docker_cards.py
Normal file
121
roles/web-app-portfolio/lookup_plugins/docker_cards.py
Normal 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]
|
@@ -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,
|
||||
}
|
||||
]
|
||||
|
28
roles/web-app-portfolio/meta/main.yml
Normal file
28
roles/web-app-portfolio/meta/main.yml
Normal 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
|
85
roles/web-app-portfolio/tasks/main.yml
Normal file
85
roles/web-app-portfolio/tasks/main.yml
Normal 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
|
30
roles/web-app-portfolio/templates/config.yaml.j2
Normal file
30
roles/web-app-portfolio/templates/config.yaml.j2
Normal 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' %}
|
17
roles/web-app-portfolio/templates/docker-compose.yml.j2
Normal file
17
roles/web-app-portfolio/templates/docker-compose.yml.j2
Normal 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' %}
|
30
roles/web-app-portfolio/templates/javascript.js.j2
Normal file
30
roles/web-app-portfolio/templates/javascript.js.j2
Normal 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 %}
|
77
roles/web-app-portfolio/templates/menu/applications.yml.j2
Normal file
77
roles/web-app-portfolio/templates/menu/applications.yml.j2
Normal 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 %}
|
37
roles/web-app-portfolio/templates/menu/contact.yml.j2
Normal file
37
roles/web-app-portfolio/templates/menu/contact.yml.j2
Normal 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 %}
|
62
roles/web-app-portfolio/templates/menu/followus.yml.j2
Normal file
62
roles/web-app-portfolio/templates/menu/followus.yml.j2
Normal 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 %}
|
48
roles/web-app-portfolio/templates/menu/footer.yml.j2
Normal file
48
roles/web-app-portfolio/templates/menu/footer.yml.j2
Normal 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
|
14
roles/web-app-portfolio/templates/menu/header.yml.j2
Normal file
14
roles/web-app-portfolio/templates/menu/header.yml.j2
Normal 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()
|
26
roles/web-app-portfolio/templates/menu/support.yml.j2
Normal file
26
roles/web-app-portfolio/templates/menu/support.yml.j2
Normal 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/
|
32
roles/web-app-portfolio/vars/configuration.yml
Normal file
32
roles/web-app-portfolio/vars/configuration.yml
Normal 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 }}"
|
||||
|
4
roles/web-app-portfolio/vars/main.yml
Normal file
4
roles/web-app-portfolio/vars/main.yml
Normal 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
|
171
roles/web-app-portfolio/vars/menu_categories.yml
Normal file
171
roles/web-app-portfolio/vars/menu_categories.yml
Normal 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
|
Reference in New Issue
Block a user