Kevin Veen-Birkenbach 747ce379cc fix(app): stop trusting X-Forwarded-For, and pin what the audit found
ProxyFix defaults x_for to 1, so ProxyFix(app.wsgi_app, x_proto=1) never
disabled it: request.remote_addr and the access log were forgeable by any
client that reached the app directly. It is x_for=0 now, asserted rather
than assumed.

A mutation audit over the change set reverted 196 deliberate behaviours
and found 47 that no test noticed. This closes the ones that carry damage:

- apod_background lost its key check, its transport guard, its status
  guard and its media-type check without a single test failing. Each one
  turns a slow or unhappy NASA into a 500 on every page.
- Untrusted values reached innerHTML through window.I18N, which the
  translation backend writes, and the modal's click handlers stacked so a
  later click opened an earlier popup's URL.
- The sync tool could ask for HTML instead of text, translate from "auto"
  instead of English, run without a timeout, store an empty translation
  that marks the string done for good, abandon 28 languages because one
  could not be written, and report success after reaching nothing.
- Neither the lint target, the CI jobs, the vendored RTL stylesheet, the
  documented environment keys, nor any of the four hardenings in
  scripts/run-e2e.sh was observed by anything.

Three of the new tests passed for the wrong reason on their first cut —
a mock that answered None whether or not the guard existed, a
raise_for_status that was never called, a string that stayed in the file
after the mutation. The audit found those too; all 24 reverts now fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:19:15 +02:00
2026-05-18 12:26:26 +02:00
2020-10-08 15:06:16 +02:00
2026-03-30 10:46:39 +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. Only prose (description, text, warning, info, subtitel) is filled automatically; name and title are left to you, because a machine cannot tell the menu label "Pictures" from the brand "Mastodon". Write those into the content catalogue yourself when you want them translated.


🚢 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.3 MiB
Languages
Python 53.4%
JavaScript 33.5%
Jinja 5.9%
Makefile 3.3%
CSS 2.5%
Other 1.4%