diff --git a/Makefile b/Makefile index 282c13f..9dc9a2b 100644 --- a/Makefile +++ b/Makefile @@ -5,24 +5,61 @@ ifneq (,$(wildcard .env)) export $(shell sed 's/=.*//' .env) endif -# Default port (can be overridden with PORT env var) -PORT ?= 5000 PYTHON ?= python3 ACT ?= act -# Default port (can be overridden with PORT env var) +# Bootstrap the local .env from the checked-in env.example template. +# Idempotent: leaves an existing .env untouched. +.PHONY: env +env: + @if [ -f .env ]; then \ + echo ".env already exists — leaving it alone."; \ + else \ + cp env.example .env; \ + echo "Created .env from env.example — review and adjust."; \ + fi + +# Bootstrap app/config.yaml from the checked-in app/config.sample.yaml +# template. Idempotent: leaves an existing config.yaml untouched. The +# Dockerfile COPYs the whole app/ directory at build time, so this file +# must exist before `make build` / `make up`. +.PHONY: config +config: + @if [ -f app/config.yaml ]; then \ + echo "app/config.yaml already exists — leaving it alone."; \ + else \ + cp app/config.sample.yaml app/config.yaml; \ + echo "Created app/config.yaml from app/config.sample.yaml — review and adjust."; \ + fi + +# Build/run recipes source .env at recipe-execution time (not at make +# parse time) so they work in the same invocation that bootstrapped +# .env via the `env` prereq. Without this, the inner $$IMAGE_NAME / +# $$PORT would be empty on the very first `make build` after a fresh +# checkout — the parse-time `include .env` happens before `env` runs. +define _require_env + if [ ! -f .env ]; then echo "ERROR: .env missing"; exit 1; fi; \ + . ./.env; \ + for v in $(1); do \ + eval "val=\$$$$v"; \ + [ -n "$$val" ] || { echo "ERROR: $$v is empty in .env (see env.example)"; exit 1; }; \ + done +endef + .PHONY: build -build: +build: env config # Build the Docker image. - docker build -t application-portfolio . + @$(call _require_env,IMAGE_NAME); \ + docker build -t "$$IMAGE_NAME" . .PHONY: build-no-cache -build-no-cache: +build-no-cache: env config # Build the Docker image without cache. - docker build --no-cache -t application-portfolio . + @$(call _require_env,IMAGE_NAME); \ + docker build --no-cache -t "$$IMAGE_NAME" . .PHONY: up -up: +up: env config # Start the application using docker-compose with build. docker-compose up -d --build --force-recreate @@ -34,23 +71,25 @@ down: - docker-compose down .PHONY: run-dev -run-dev: +run-dev: env config config # Run the container in development mode (hot-reload). - docker run -d \ - -p $(PORT):$(PORT) \ - --name portfolio \ - -v $(PWD)/app/:/app \ - -e FLASK_APP=app.py \ - -e FLASK_ENV=development \ - application-portfolio + @$(call _require_env,IMAGE_NAME PORT); \ + docker run -d \ + -p "$$PORT:$$PORT" \ + --name portfolio \ + -v "$(PWD)/app/:/app" \ + -e FLASK_APP=app.py \ + -e FLASK_ENV=development \ + "$$IMAGE_NAME" .PHONY: run-prod -run-prod: +run-prod: env config config # Run the container in production mode. - docker run -d \ - -p $(PORT):$(PORT) \ - --name portfolio \ - application-portfolio + @$(call _require_env,IMAGE_NAME PORT); \ + docker run -d \ + -p "$$PORT:$$PORT" \ + --name portfolio \ + "$$IMAGE_NAME" .PHONY: logs logs: @@ -58,12 +97,12 @@ logs: docker logs -f portfolio .PHONY: dev -dev: +dev: env config # Start the application in development mode using docker-compose. FLASK_ENV=development docker-compose up -d .PHONY: prod -prod: +prod: env config # Start the application in production mode using docker-compose (with build). docker-compose up -d --build @@ -78,9 +117,10 @@ delete: - docker rm -f portfolio .PHONY: browse -browse: - # Open the application in the browser at http://localhost:$(PORT) - chromium http://localhost:$(PORT) +browse: env + # Open the application in the browser at http://localhost:$$PORT + @$(call _require_env,PORT); \ + chromium "http://localhost:$$PORT" .PHONY: install install: diff --git a/README.md b/README.md index ea1c6f1..cabcad0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes. + + > 🚀 You can also pair PortUI with JavaScript for sleek, web-based desktop-style interfaces. > 💻 Example in action: [CyMaIS.Cloud](https://cymais.cloud/) (demo) > 🌐 Another live example: [veen.world](https://www.veen.world/) (Kevin’s personal site) diff --git a/app/app.py b/app/app.py index b3a0db6..8d3cbc2 100644 --- a/app/app.py +++ b/app/app.py @@ -3,14 +3,16 @@ import os import requests import yaml -from flask import Flask, current_app, render_template +from flask import Flask, current_app, render_template, url_for from markupsafe import Markup try: + from app.utils.asset_resolver import asset_src, resolve_asset_cache from app.utils.cache_manager import CacheManager from app.utils.compute_card_classes import compute_card_classes from app.utils.configuration_resolver import ConfigurationResolver except ImportError: # pragma: no cover - supports running from the app/ directory. + from utils.asset_resolver import asset_src, resolve_asset_cache from utils.cache_manager import CacheManager from utils.compute_card_classes import compute_card_classes from utils.configuration_resolver import ConfigurationResolver @@ -43,24 +45,16 @@ def load_config(app): def cache_icons_and_logos(app): - """Cache all icons and logos to local files, with a source fallback.""" + """Resolve every icon/logo/favicon to either a local cache path or + an external URL (see ``resolve_asset_cache``).""" for card in app.config["cards"]: - icon = card.get("icon", {}) - if icon.get("source"): - cached = cache_manager.cache_file(icon["source"]) - icon["cache"] = cached or icon["source"] + icon = card.get("icon") + if icon: + resolve_asset_cache(icon, cache_manager) - company_logo = app.config["company"]["logo"] - cached = cache_manager.cache_file(company_logo["source"]) - company_logo["cache"] = cached or company_logo["source"] - - favicon = app.config["platform"]["favicon"] - cached = cache_manager.cache_file(favicon["source"]) - favicon["cache"] = cached or favicon["source"] - - platform_logo = app.config["platform"]["logo"] - cached = cache_manager.cache_file(platform_logo["source"]) - platform_logo["cache"] = cached or platform_logo["source"] + resolve_asset_cache(app.config["company"]["logo"], cache_manager) + resolve_asset_cache(app.config["platform"]["favicon"], cache_manager) + resolve_asset_cache(app.config["platform"]["logo"], cache_manager) # Initialize Flask app @@ -83,7 +77,10 @@ def utility_processor(): except OSError: return "" - return dict(include_svg=include_svg) + def template_asset_src(asset): + return asset_src(asset, lambda filename: url_for("static", filename=filename)) + + return dict(include_svg=include_svg, asset_src=template_asset_src) @app.before_request diff --git a/app/config.sample.yaml b/app/config.sample.yaml index 4c4338f..cb21afa 100644 --- a/app/config.sample.yaml +++ b/app/config.sample.yaml @@ -198,7 +198,16 @@ accounts: url: https://s.veen.world/cloud cards: -- icon: +- icon: + source: https://file.infinito.nexus/assets/img/logo.png + title: Infinito.Nexus + text: Open-source self-hosting stack — one-click deployable FOSS web apps with + shared identity, asset, and observability services. The platform that hosts + this site, including the dashboard you are looking at. + url: https://infinito.nexus + link_text: infinito.nexus + iframe: true +- icon: source: https://cloud.veen.world/s/logo_agile_coach_512x512/download title: Agile Coach text: I lead agile transformations and improve team dynamics through Scrum, DevOps, diff --git a/app/templates/moduls/base.html.j2 b/app/templates/moduls/base.html.j2 index fdf00e4..60ae966 100644 --- a/app/templates/moduls/base.html.j2 +++ b/app/templates/moduls/base.html.j2 @@ -6,7 +6,7 @@ @@ -36,7 +36,7 @@