Compare commits

..

11 Commits

11 changed files with 383 additions and 152 deletions

View File

@@ -11,11 +11,4 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ .
# Set default port environment variable
ENV PORT=5000
# Expose port (optional for documentation)
EXPOSE ${PORT}
# Start command using shell to allow env substitution
CMD ["sh", "-c", "exec python app.py --port=${PORT}"]
CMD ["python", "app.py"]

View File

@@ -4,6 +4,12 @@ import yaml
from utils.configuration_resolver import ConfigurationResolver
from utils.cache_manager import CacheManager
from utils.compute_card_classes import compute_card_classes
import logging
logging.basicConfig(level=logging.DEBUG)
FLASK_ENV = os.getenv("FLASK_ENV", "production")
FLASK_PORT = int(os.getenv("PORT", 5000))
print(f"🔧 Starting app on port {FLASK_PORT}, FLASK_ENV={FLASK_ENV}")
# Initialize the CacheManager
cache_manager = CacheManager()
@@ -31,9 +37,6 @@ def cache_icons_and_logos(app):
app.config["platform"]["favicon"]["cache"] = cache_manager.cache_file(app.config["platform"]["favicon"]["source"])
app.config["platform"]["logo"]["cache"] = cache_manager.cache_file(app.config["platform"]["logo"]["source"])
# Get the environment variable FLASK_ENV or set a default value
FLASK_ENV = os.getenv("FLASK_ENV", "production")
# Initialize Flask app
app = Flask(__name__)
@@ -64,5 +67,4 @@ def index():
)
if __name__ == "__main__":
port = int(os.getenv("PORT", 5000))
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=port)
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=FLASK_PORT)

View File

@@ -647,4 +647,36 @@ navigation:
class: fa-solid fa-scale-balanced
url: https://s.veen.world/imprint
iframe: true
- name: Settings
description: Application settings
icon:
class: fa-solid fa-cog
children:
- name: Toggle Fullscreen
description: Enter or exit fullscreen mode
icon:
class: fa-solid fa-expand-arrows-alt
onclick: "toggleFullscreen()"
- name: Toggle Full Width
description: Switch between normal and full-width layout
icon:
class: fa-solid fa-arrows-left-right
onclick: "setFullWidth(!initFullWidthFromUrl())"
- name: Open in new tab
description: Open the currently embedded iframe URL in a fresh browser tab
icon:
class: fa-solid fa-up-right-from-square
onclick: openIframeInNewTab()
- name: Print
description: Print the current view
icon:
class: fa-solid fa-print
onclick: window.print()
- name: Zoom +
icon:
class: fa-solid fa-search-plus
onclick: zoomPage(1.1)
- name: Zoom
icon:
class: fa-solid fa-search-minus
onclick: zoomPage(0.9)

View File

@@ -0,0 +1,86 @@
/**
* Add or remove the `fullscreen=1` URL parameter.
* @param {boolean} enabled
*/
function updateUrlFullscreen(enabled) {
var url = new URL(window.location);
if (enabled) url.searchParams.set('fullscreen', '1');
else url.searchParams.delete('fullscreen');
window.history.replaceState({}, '', url);
}
/**
* Enter fullscreen: hide header/footer, enable full width, recalc scroll,
* set both URL params, update button.
*/
function enterFullscreen() {
document.querySelectorAll('header, footer')
.forEach(function(el){ el.style.display = 'none'; });
setFullWidth(true);
updateUrlFullscreen(true);
if (typeof adjustScrollContainerHeight === 'function') adjustScrollContainerHeight();
if (typeof updateCustomScrollbar === 'function') updateCustomScrollbar();
}
/**
* Exit fullscreen: show header/footer, disable full width, recalc scroll,
* clear both URL params, update button.
*/
function exitFullscreen() {
document.querySelectorAll('header, footer')
.forEach(function(el){ el.style.display = ''; });
setFullWidth(false);
updateUrlFullscreen(false);
if (typeof adjustScrollContainerHeight === 'function') adjustScrollContainerHeight();
if (typeof updateCustomScrollbar === 'function') updateCustomScrollbar();
if (typeof syncIframeHeight === 'function') syncIframeHeight();
}
/**
* Toggle between enter and exit fullscreen.
*/
function toggleFullscreen() {
const params = new URLSearchParams(window.location.search);
const isFull = params.get('fullscreen') === '1';
if (isFull) exitFullscreen();
else enterFullscreen();
}
/**
* Read `fullscreen` flag from URL on load.
* @returns {boolean}
*/
function initFullscreenFromUrl() {
return new URLSearchParams(window.location.search).get('fullscreen') === '1';
}
// On page load: apply fullwidth & fullscreen flags
window.addEventListener('DOMContentLoaded', function() {
// first fullwidth
var wasFullWidth = initFullWidthFromUrl();
setFullWidth(wasFullWidth);
// now fullscreen
if (initFullscreenFromUrl()) {
enterFullscreen();
}
});
// Mirror native F11/fullscreen API events
document.addEventListener('fullscreenchange', function() {
if (document.fullscreenElement) enterFullscreen();
else exitFullscreen();
});
window.addEventListener('resize', function() {
var isUiFs = Math.abs(window.innerHeight - screen.height) < 2;
if (isUiFs) enterFullscreen();
else exitFullscreen();
});
// Expose globally
window.fullscreen = enterFullscreen;
window.exitFullscreen = exitFullscreen;
window.toggleFullscreen = toggleFullscreen;

View File

@@ -0,0 +1,42 @@
/**
* Toggles the .container class between .container and .container-fluid.
* @param {boolean} enabled true = full width, false = normal.
*/
function setFullWidth(enabled) {
var el = document.querySelector('.container, .container-fluid');
if (!el) return;
console.log(el)
if (enabled) {
el.classList.replace('container', 'container-fluid');
updateUrlFullWidth(true)
} else {
el.classList.replace('container-fluid', 'container');
updateUrlFullWidth(false)
}
}
/**
* Reads the URL parameter `fullwidth` and applies full width if it's set.
* @returns {boolean} current fullwidth state
*/
function initFullWidthFromUrl() {
var isFull = new URLSearchParams(window.location.search).get('fullwidth') === '1';
setFullWidth(isFull);
return isFull;
}
/**
* Adds or removes the `fullwidth=1` URL parameter.
* @param {boolean} enabled
*/
function updateUrlFullWidth(enabled) {
var url = new URL(window.location);
if (enabled) url.searchParams.set('fullwidth', '1');
else url.searchParams.delete('fullwidth');
window.history.replaceState({}, '', url);
}
// Expose globally
window.setFullWidth = setFullWidth;
window.initFullWidthFromUrl = initFullWidthFromUrl;
window.updateUrlFullWidth = updateUrlFullWidth;

View File

@@ -1,101 +1,158 @@
// Global variables to store elements and original state
let mainElement, originalContent, originalMainStyle, container, customScrollbar;
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
// Function to open a URL in an iframe using global variables
// Synchronize the height of the iframe to match the scroll-container or main element
function syncIframeHeight() {
const iframe = mainElement.querySelector("iframe");
if (iframe) {
console.log("Setting iframe height based on scroll-container inline styles...");
if (scrollbarContainer) {
// Prefer inline height, otherwise inline max-height
const inlineHeight = scrollbarContainer.style.height;
const inlineMax = scrollbarContainer.style.maxHeight;
const target = inlineHeight || inlineMax;
if (target) {
console.log("Using scroll-container inline style:", target);
iframe.style.height = target;
} else {
console.warn("No inline height or max-height set on scroll-container. Using main element height instead.");
iframe.style.height = mainElement.style.height;
}
} else {
console.log("No scroll-container found. Using main element height:", mainElement.style.height);
iframe.style.height = mainElement.style.height;
}
} else {
console.log("No iframe to resize.");
}
}
// Function to open a URL in an iframe
function openIframe(url) {
// Set a fixed height for the main element if not already set
if (!mainElement.style.height) {
mainElement.style.height = `${mainElement.clientHeight}px`;
// Hide the container (and its scroll-container) so the iframe can appear in its place
if (scrollbarContainer) {
scrollbarContainer.style.display = 'none';
}
// Replace the container class with container-fluid if not already applied
if (container && !container.classList.contains("container-fluid")) {
container.classList.replace("container", "container-fluid");
}
// Hide the custom scrollbar
// Hide any custom scrollbar element if present
if (customScrollbar) {
customScrollbar.style.display = "none";
customScrollbar.style.display = 'none';
}
// Check if an iframe already exists in the main element
// Create or retrieve the iframe in the main element
let iframe = mainElement.querySelector("iframe");
if (!iframe) {
// Create a new iframe element
iframe = document.createElement("iframe");
iframe.width = "100%";
iframe.style.border = "none";
iframe.style.height = mainElement.style.height; // Apply fixed height
iframe.style.overflow = "auto"; // Enable internal scrollbar
iframe.scrolling = "auto"; // Ensure scrollability
// Clear the main content before appending the iframe
mainElement.innerHTML = "";
iframe.style.overflow = "auto"; // Enable internal scrolling
iframe.scrolling = "auto";
mainElement.appendChild(iframe);
syncIframeHeight();
}
// Set the URL of the iframe
// Set the iframe's source URL
iframe.src = url;
// Push the new URL state without reloading the page
const newUrl = new URL(window.location);
newUrl.searchParams.set('iframe', url);
window.history.pushState({ iframe: url }, '', newUrl);
}
document.addEventListener("DOMContentLoaded", function () {
// Initialize global variables
// Function to restore the original content and show the container again
function restoreOriginal() {
// Remove the iframe from the DOM
const iframe = mainElement.querySelector("iframe");
if (iframe) {
iframe.remove();
}
// Show the original container
if (scrollbarContainer) {
scrollbarContainer.style.display = '';
}
// Restore any custom scrollbar
if (customScrollbar) {
customScrollbar.style.display = '';
}
// Restore the original inline style of the main element
if (originalMainStyle !== null) {
mainElement.setAttribute("style", originalMainStyle);
} else {
mainElement.removeAttribute("style");
}
// Update the URL to remove the iframe parameter
const newUrl = new URL(window.location);
newUrl.searchParams.delete("iframe");
window.history.pushState({}, '', newUrl);
}
// Initialize event listeners after DOM content is loaded
document.addEventListener("DOMContentLoaded", function() {
// Cache references to elements and original state
mainElement = document.querySelector("main");
originalContent = mainElement.innerHTML;
originalMainStyle = mainElement.getAttribute("style"); // might be null if no inline style exists
originalMainStyle = mainElement.getAttribute("style"); // may be null
container = document.querySelector(".container");
customScrollbar = document.getElementById("custom-scrollbar");
scrollbarContainer = container.querySelector(".scroll-container")
// Get all links that should open in an iframe
const links = document.querySelectorAll(".iframe-link");
// Add click event listener to each iframe link
links.forEach(link => {
link.addEventListener("click", function (event) {
event.preventDefault(); // Prevent default link behavior
const url = this.getAttribute("href");
openIframe(url);
// Attach click handlers to links that should open in an iframe
document.querySelectorAll(".iframe-link").forEach(link => {
link.addEventListener("click", function(event) {
event.preventDefault(); // prevent full page navigation
openIframe(this.href);
updateUrlFullWidth(true);
});
});
// Add click event listener to header h1 to restore the original main content and style
// Clicking the header's H1 will restore the original view
const headerH1 = document.querySelector("header h1");
if (headerH1) {
headerH1.style.cursor = "pointer";
headerH1.addEventListener("click", function () {
// Restore the original content of the main element (removing the iframe)
mainElement.innerHTML = originalContent;
// Restore the original inline style of the main element
if (originalMainStyle !== null) {
mainElement.setAttribute("style", originalMainStyle);
} else {
mainElement.removeAttribute("style");
}
// Optionally revert the container class back to "container" if needed
if (container && container.classList.contains("container-fluid")) {
container.classList.replace("container-fluid", "container");
}
// Optionally show the custom scrollbar again
if (customScrollbar) {
customScrollbar.style.display = "";
}
// Adjust scroll container height if that function exists
if (typeof adjustScrollContainerHeight === "function") {
adjustScrollContainerHeight();
}
});
headerH1.addEventListener("click", restoreOriginal);
}
// Adjust iframe height on window resize (optional, to keep it responsive)
window.addEventListener("resize", function () {
const iframe = mainElement.querySelector("iframe");
if (iframe) {
iframe.style.height = mainElement.style.height;
// On full page load, check URL parameters to auto-open an iframe
window.addEventListener("load", function() {
const params = new URLSearchParams(window.location.search);
const iframeUrl = params.get('iframe');
if (iframeUrl) {
openIframe(iframeUrl);
}
});
});
// Handle browser back/forward navigation
window.addEventListener('popstate', function(event) {
const params = new URLSearchParams(window.location.search);
const iframeUrl = params.get('iframe');
if (iframeUrl) {
openIframe(iframeUrl);
} else {
restoreOriginal();
}
});
/**
* Opens the current iframe URL in a new browser tab.
*/
function openIframeInNewTab() {
const params = new URLSearchParams(window.location.search);
const iframeUrl = params.get('iframe');
if (iframeUrl) {
window.open(iframeUrl, '_blank');
} else {
alert('No iframe is currently open.');
}
}
// expose globally so your templates onclick can find it
window.openIframeInNewTab = openIframeInNewTab;
// Adjust iframe height on window resize
window.addEventListener('resize', syncIframeHeight);

View File

@@ -49,10 +49,16 @@
</div>
<!-- Include modal -->
{% include "moduls/modal.html.j2" %}
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
<script src="{{ url_for('static', filename='js/tooltip.js') }}"></script>
<script src="{{ url_for('static', filename='js/custom_scrollbar.js') }}"></script>
<script src="{{ url_for('static', filename='js/iframe.js') }}"></script>
{% for name in [
'modal',
'navigation',
'tooltip',
'container',
'fullwidth',
'fullscreen',
'iframe',
] %}
<script src="{{ url_for('static', filename='js/' ~ name ~ '.js') }}"></script>
{% endfor %}
</body>
</html>

View File

@@ -8,65 +8,83 @@
{% endmacro %}
<!-- Template for children -->
{% macro render_children(children) %}
{% for children in children %}
{% if children.children %}
<li class="dropdown-submenu position-relative">
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
{{ render_icon_and_name(children) }}
</a>
<ul class="dropdown-menu">
{{ render_children(children.children) }}
</ul>
</li>
{% elif children.identifier or children.warning or children.info %}
<li>
<a class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
{{ render_icon_and_name(children) }}
</a>
</li>
{% else %}
<li>
<a class="dropdown-item {% if children.iframe %}iframe-link{% endif %}" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
{{ render_icon_and_name(children) }}
</a>
</li>
{% endif %}
{% endfor %}
{% for child in children %}
{% if child.children %}
<li class="dropdown-submenu position-relative">
<a class="dropdown-item dropdown-toggle" title="{{ child.description }}">
{{ render_icon_and_name(child) }}
</a>
<ul class="dropdown-menu">
{{ render_children(child.children) }}
</ul>
</li>
{% elif child.identifier or child.warning or child.info %}
<li>
<a class="dropdown-item"
onclick='openDynamicPopup({{ child|tojson|safe }})'
data-bs-toggle="tooltip"
title="{{ child.description }}">
{{ render_icon_and_name(child) }}
</a>
</li>
{% else %}
<li>
<a class="dropdown-item {% if child.iframe %}iframe-link{% endif %}"
{% if child.onclick %}
onclick="{{ child.onclick }}"
{% else %}
href="{{ child.url }}"
{% endif %}
target="{{ child.target|default('_blank') }}"
data-bs-toggle="tooltip"
title="{{ child.description }}">
{{ render_icon_and_name(child) }}
</a>
</li>
{% endif %}
{% endfor %}
{% endmacro %}
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light menu-{{menu_type}}">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %} btn-group">
{% for item in navigation[menu_type].children %}
{% if item.url %}
<!-- Single Item -->
<li class="nav-item">
<a class="nav-link btn btn-light {% if item.iframe %}iframe-link{% endif %}" href="{{ item.url }}" target="{{ item.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ item.description }}">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %} btn-group">
{% for item in navigation[menu_type].children %}
{% if item.url or item.onclick %}
<li class="nav-item">
<a class="nav-link btn btn-light {% if item.iframe %}iframe-link{% endif %}"
{% if item.onclick %}
onclick="{{ item.onclick }}"
{% else %}
href="{{ item.url }}"
{% endif %}
target="{{ item.target|default('_blank') }}"
data-bs-toggle="tooltip"
title="{{ item.description }}">
{{ render_icon_and_name(item) }}
</a>
</li>
{% else %}
<!-- Dropdown Menu -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle btn btn-light" id="navbarDropdown{{ loop.index }}" role="button" data-bs-display="dynamic" aria-expanded="false">
{% if item.icon is defined and item.icon.class is defined %}
{{ render_icon_and_name(item) }}
</a>
</li>
{% else %}
<!-- Dropdown Menu -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle btn btn-light" id="navbarDropdown{{ loop.index }}" role="button" data-bs-display="dynamic" aria-expanded="false">
{% if item.icon is defined and item.icon.class is defined %}
{{ render_icon_and_name(item) }}
{% else %}
<p>Missing icon in item: {{ item }}</p>
{% endif %}
</a>
<ul class="dropdown-menu">
{{ render_children(item.children) }}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% else %}
<p>Missing icon in item: {{ item }}</p>
{% endif %}
</a>
<ul class="dropdown-menu">
{{ render_children(item.children) }}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</nav>

View File

@@ -11,6 +11,8 @@ services:
- "${PORT:-5000}:${PORT:-5000}"
volumes:
- ./app:/app
- ./.env:/app./.env
environment:
- PORT=${PORT:-5000}
- FLASK_ENV=${FLASK_ENV:-production}
restart: unless-stopped

17
main.py
View File

@@ -25,7 +25,6 @@ import os
from dotenv import load_dotenv
from pathlib import Path
# Always load .env from the script's directory
dotenv_path = Path(__file__).resolve().parent / ".env"
if dotenv_path.exists():
@@ -34,14 +33,14 @@ else:
print(f"⚠️ Warning: No .env file found at {dotenv_path}")
PORT = int(os.getenv("PORT", 5000))
def run_command(command, dry_run=False):
def run_command(command, dry_run=False, env=None):
"""Utility function to run a shell command."""
print(f"Executing: {' '.join(command)}")
if dry_run:
print("Dry run enabled: command not executed.")
return
try:
subprocess.check_call(command)
subprocess.check_call(command, env=env)
except subprocess.CalledProcessError as e:
print(f"Error: Command failed with exit code {e.returncode}")
sys.exit(e.returncode)
@@ -126,7 +125,7 @@ def run_prod(args):
"""
command = [
"docker", "run", "-d",
"-p", "{PORT}:{PORT}",
"-p", "{PORT}:5000",
"--name", "portfolio",
"application-portfolio"
]
@@ -148,18 +147,12 @@ def logs(args):
def dev(args):
"""
Run the application in development mode using docker-compose.
Command:
FLASK_ENV=development docker-compose up -d
This command sets the FLASK_ENV environment variable to 'development'
and starts the application using docker-compose, enabling hot-reloading.
"""
env = os.environ.copy()
env["FLASK_ENV"] = "development"
command = ["docker-compose", "up", "-d"]
print("Setting FLASK_ENV=development")
run_command(command, args.dry_run)
print("▶️ Starting in development mode (FLASK_ENV=development)")
run_command(command, args.dry_run, env=env)
def prod(args):
"""