Kevin Veen-Birkenbach f830153cf4 build(make): run every local suite from make test in a project virtualenv
make test installed the project into whatever PYTHON pointed at, which is
the system python3 by default. On Manjaro, Debian and Ubuntu that Python
is externally managed, so pip refuses (PEP 668) and not a single suite
runs, although AGENTS.md requires make test before every commit. It also
stopped at the first failing suite, so one broken linter hid the result
of every test behind it.

make test now creates .venv on first use and runs ci with --keep-going
and that interpreter, so every suite runs and make lists each failed
target. The path is fixed to the checkout: an exported VENV, which other
tooling sets, would otherwise redirect it. It is absolute because
run-e2e.sh changes into app/ before starting Flask. All other targets
keep PYTHON ?= python3.

Verified with a full make test: actionlint, ruff, yamllint, eslint,
hadolint, shellcheck, the lint, security, unit and integration suites,
bandit, pip-audit and 107 Cypress tests all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:17:05 +02:00
2026-09-10 16:07:26 +02:00
2020-10-08 15:06:16 +02:00
2026-03-30 10:46:39 +02:00
2026-09-10 16:07:26 +02:00

PortUI 🖥️

GitHub Sponsors Patreon Buy Me a Coffee PayPal

A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes.

PortUI screenshot

🚀 You can also pair PortUI with JavaScript for sleek, web-based desktop-style interfaces.
💻 Example in action: CyMaIS.Cloud (demo)
🌐 Another live example: veen.world (Kevins personal site)


Key Features

  • Dynamic Navigation
    Create dropdowns & nested menus with ease.
  • Customizable Cards
    Highlight skills, projects, or services—with icons, titles, and links.
  • Smart Cache Management
    Auto-cache assets for lightning-fast loading.
  • Responsive Design
    Built on Bootstrap; looks great on desktop, tablet & mobile.
  • 184 Languages
    Every ISO 639-1 code, browser-negotiated, RTL-aware, with machine translation for your own content.
  • YAML-Driven
    All content & structure defined in a simple config.yaml.
  • CLI Control
    Manage Docker containers via the portfolio command.

🌐 Quick Access


🏁 Getting Started

🔧 Prerequisites

  • Docker & Docker Compose
  • Basic Python & YAML knowledge

🛠️ Installation via Git

  1. Clone & enter repo

    git clone <repository_url>
    cd <repository_directory>
    
  2. Configure Copy config.sample.yamlconfig.yaml & customize.

  3. Build & run

    docker-compose up --build
    
  4. Browse Open http://localhost:5000

📦 Installation via Kevins Package Manager

pkgmgr install portui

Once installed, the portui CLI is available system-wide.


🖥️ CLI Commands

portui --help
  • buildBuild the Docker image
  • upStart containers (with build)
  • downStop & remove containers
  • run-devDev mode (hot-reload)
  • run-prodProduction mode
  • logsView container logs
  • devDocker-Compose dev environment
  • prodDocker-Compose prod environment
  • cleanupPrune stopped containers

🔧 YAML Configuration Guide

Define your sites structure in config.yaml:

accounts:
  name: Online Accounts
  description: Discover my online presence.
  icon:
    class: fa-solid fa-users
  children:
    - name: Channels
      description: Platforms where I share content.
      icon:
        class: fas fa-newspaper
      children:
        - name: Mastodon
          description: Follow me on Mastodon.
          icon:
            class: fa-brands fa-mastodon
          url: https://microblog.veen.world/@kevinveenbirkenbach
          identifier: "@kevinveenbirkenbach@microblog.veen.world"
  cards:
    - 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 and Agile Coaching.
      url: https://www.agile-coach.world
      link_text: www.agile-coach.world

company:
  title: Kevin Veen-Birkenbach
  subtitle: Consulting & Coaching Solutions
  logo:
    source: https://cloud.veen.world/s/logo_face_512x512/download
  favicon:
    source: https://cloud.veen.world/s/veen_world_favicon/download
  address:
    street: Afrikanische Straße 43
    postal_code: DE-13351
    city: Berlin
    country: Germany
  imprint_url: https://s.veen.world/imprint
  • children enables multi-level menus.
  • link references other YAML paths to avoid duplication.

🌍 Languages

Every ISO 639-1 language — all 184 two-letter codes — has a URL, a display name in its own script and a writing direction. The interface ships translated for 29 of them; the rest fall back to English string by string until a catalogue is filled. / serves the best match for the visitor's Accept-Language header, /<code>/ forces one, and a switcher in the navbar lists them all. The ten right-to-left languages get dir="rtl" and Bootstrap's RTL stylesheet automatically.

Translations live in two catalogues, both keyed by the English source string:

Path Tracked Holds
app/i18n/ui/<code>.yaml yes Interface strings. Shipped for 29 languages; English is the source and has no file.
app/i18n/content/<code>.yaml no Your config.yaml prose, generated per deployment.

A string with no catalogue entry falls back to English, so a half-filled catalogue degrades instead of breaking.

Fill the content catalogues from a LibreTranslate instance — set LIBRETRANSLATE_URL in .env, then:

make i18n

This fills the interface strings of the languages that ship no catalogue as well. Existing entries are never overwritten, and a string the shipped catalogue already covers is never requested, so corrections you make by hand survive later runs.

name, title, description, text, warning, info and subtitel are translated; url, link_text, identifier and icon classes never are.

A machine cannot tell the menu label "Pictures" from the brand "Mastodon", so list the brands in app/i18n/keep.txt, one per line — they are then stored as themselves in every language and cost no request:

# Strings utils/i18n_sync.py stores as themselves instead of translating.
Mastodon
Nextcloud
freelancermap.de

Add one-off entries with --keep Foo Bar, or point somewhere else with --keep-file. A protected string never replaces an entry you already wrote by hand.


🚢 Production Deployment

  • Use a reverse proxy (NGINX/Apache).
  • Secure with SSL/TLS.
  • Swap to a production database if needed.

Because every page carries a canonical URL and 184 hreflang alternates, two details of the proxy setup now matter:

  • Set TRUSTED_HOSTS in .env to your public hostname(s), comma-separated. Left empty, the app reflects whatever Host header arrives into its canonical, hreflang and redirect URLs — so a shared cache in front of it can be made to store a redirect pointing somewhere else.
  • Have the proxy send X-Forwarded-Proto. Without it the app cannot know TLS terminated upstream and every canonical URL claims http://. X-Forwarded-Host is deliberately not trusted; set Host to the public name instead.

📜 License

Licensed under GNU AGPLv3. See LICENSE for details.


✍️ Author

Created by Kevin Veen-Birkenbach

Enjoy building your portfolio! 🌟

Description
No description provided
Readme AGPL-3.0 5.4 MiB
Languages
Python 53.5%
JavaScript 33.4%
Jinja 5.8%
Makefile 3.4%
CSS 2.4%
Other 1.5%