mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-04-16 17:26:49 +00:00
Compare commits
8 Commits
252b50d2a7
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3132aab2a5 | |||
| 3d1db1f8ba | |||
| 58872ced81 | |||
| 13b3af3330 | |||
| eca7084f4e | |||
| 6861b2c0eb | |||
| 66b1f0d029 | |||
| a29a0b1862 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
security:
|
security:
|
||||||
name: Run security workflow
|
name: Run security workflow
|
||||||
uses: ./.github/workflows/security.yml
|
uses: ./.github/workflows/security.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
name: Run test workflow
|
name: Run test workflow
|
||||||
@@ -23,6 +27,9 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
name: Run lint workflow
|
name: Run lint workflow
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish image
|
name: Publish image
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ app/static/cache/*
|
|||||||
app/cypress/screenshots/*
|
app/cypress/screenshots/*
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
app/node_modules/
|
app/node_modules/
|
||||||
|
app/static/vendor/
|
||||||
hadolint-results.sarif
|
hadolint-results.sarif
|
||||||
build/
|
build/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|||||||
13
AGENTS.md
Normal file
13
AGENTS.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## Pre-Commit Validation
|
||||||
|
|
||||||
|
- You MUST run `make test` before every commit whenever the staged change includes at least one file that is not `.md` or `.rst`, unless explicitly instructed otherwise.
|
||||||
|
- You MUST commit only after all tests pass.
|
||||||
|
- You MUST NOT commit automatically without explicit confirmation from the user.
|
||||||
|
|
||||||
|
## Vendor Assets
|
||||||
|
|
||||||
|
- Browser vendor assets (Bootstrap, Font Awesome, etc.) are managed via npm.
|
||||||
|
- Run `npm install` inside `app/` to populate `app/static/vendor/` before starting the dev server or running e2e tests.
|
||||||
|
- Never commit `app/node_modules/` or `app/static/vendor/` — both are gitignored and generated at build time.
|
||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,3 +1,14 @@
|
|||||||
|
## [1.1.0] - 2026-03-30
|
||||||
|
|
||||||
|
* *CI stabilization and modularization*: Split into reusable workflows (lint, security, tests) with correct permissions for CodeQL and SARIF uploads
|
||||||
|
* *Modern Python packaging*: Migration to pyproject.toml and updated Dockerfile using Python 3.12
|
||||||
|
* *Improved test coverage*: Added unit, integration, lint, security, and E2E tests using act
|
||||||
|
* *Local vendor assets*: Replaced external CDNs with npm-based local asset pipeline
|
||||||
|
* *Enhanced build workflow*: Extended Makefile with targets for test, lint, security, and CI plus vendor build process
|
||||||
|
* *Frontend fix*: Prevented navbar wrapping and improved layout behavior
|
||||||
|
* *Developer guidelines*: Introduced AGENTS.md and CLAUDE.md with enforced pre-commit rules
|
||||||
|
|
||||||
|
|
||||||
## [1.0.0] - 2026-02-19
|
## [1.0.0] - 2026-02-19
|
||||||
|
|
||||||
* Official Release🥳
|
* Official Release🥳
|
||||||
|
|||||||
5
CLAUDE.md
Normal file
5
CLAUDE.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Startup
|
||||||
|
|
||||||
|
You MUST read `AGENTS.md` and follow all instructions in it at the start of every conversation before doing anything else.
|
||||||
@@ -4,6 +4,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
FLASK_HOST=0.0.0.0
|
FLASK_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# hadolint ignore=DL3008
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
COPY pyproject.toml README.md main.py ./
|
COPY pyproject.toml README.md main.py ./
|
||||||
@@ -12,5 +15,6 @@ RUN python -m pip install --no-cache-dir .
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY app/ .
|
COPY app/ .
|
||||||
|
RUN npm install --prefix /app
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
CMD ["python", "app.py"]
|
||||||
|
|||||||
4
MIRRORS
4
MIRRORS
@@ -1,2 +1,4 @@
|
|||||||
https://pypi.org/project/portfolio-ui/
|
|
||||||
git@github.com:kevinveenbirkenbach/port-ui.git
|
git@github.com:kevinveenbirkenbach/port-ui.git
|
||||||
|
ssh://git@code.infinito.nexus:2201/kevinveenbirkenbach/port-ui.git
|
||||||
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/port-ui.git
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -145,7 +145,7 @@ security: install-dev test-security
|
|||||||
$(PYTHON) -m pip_audit -r /tmp/portfolio-runtime-requirements.txt
|
$(PYTHON) -m pip_audit -r /tmp/portfolio-runtime-requirements.txt
|
||||||
|
|
||||||
.PHONY: test-e2e
|
.PHONY: test-e2e
|
||||||
test-e2e:
|
test-e2e: npm-install
|
||||||
# Run Cypress end-to-end tests via act (stop portfolio container to free port first).
|
# Run Cypress end-to-end tests via act (stop portfolio container to free port first).
|
||||||
-docker stop portfolio 2>/dev/null || true
|
-docker stop portfolio 2>/dev/null || true
|
||||||
$(ACT) workflow_dispatch -W .github/workflows/tests.yml -j e2e
|
$(ACT) workflow_dispatch -W .github/workflows/tests.yml -j e2e
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# PortUI 🖥️✨
|
# PortUI 🖥️✨
|
||||||
|
|
||||||
[](https://github.com/sponsors/kevinveenbirkenbach)
|
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
||||||
[](https://www.patreon.com/c/kevinveenbirkenbach)
|
|
||||||
[](https://buymeacoffee.com/kevinveenbirkenbach)
|
|
||||||
[](https://s.veen.world/paypaldonate)
|
|
||||||
|
|
||||||
A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes.
|
A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"bootstrap": "5.2.2",
|
||||||
|
"bootstrap-icons": "1.9.1",
|
||||||
|
"jquery": "3.6.0",
|
||||||
|
"marked": "^4.3.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cypress": "^14.5.1"
|
"cypress": "^14.5.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/copy-vendor.js",
|
||||||
|
"postinstall": "node scripts/copy-vendor.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
71
app/scripts/copy-vendor.js
Normal file
71
app/scripts/copy-vendor.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* Copies third-party browser assets from node_modules into static/vendor/
|
||||||
|
* so Flask can serve them without any CDN dependency.
|
||||||
|
* Runs automatically via the "postinstall" npm hook.
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const NM = path.join(__dirname, '..', 'node_modules');
|
||||||
|
const VENDOR = path.join(__dirname, '..', 'static', 'vendor');
|
||||||
|
|
||||||
|
function copyFile(src, dest) {
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDir(src, dest) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
const s = path.join(src, entry.name);
|
||||||
|
const d = path.join(dest, entry.name);
|
||||||
|
entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap CSS + JS bundle
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, 'bootstrap', 'dist', 'css', 'bootstrap.min.css'),
|
||||||
|
path.join(VENDOR, 'bootstrap', 'css', 'bootstrap.min.css')
|
||||||
|
);
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, 'bootstrap', 'dist', 'js', 'bootstrap.bundle.min.js'),
|
||||||
|
path.join(VENDOR, 'bootstrap', 'js', 'bootstrap.bundle.min.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bootstrap Icons CSS + embedded fonts
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, 'bootstrap-icons', 'font', 'bootstrap-icons.css'),
|
||||||
|
path.join(VENDOR, 'bootstrap-icons', 'font', 'bootstrap-icons.css')
|
||||||
|
);
|
||||||
|
copyDir(
|
||||||
|
path.join(NM, 'bootstrap-icons', 'font', 'fonts'),
|
||||||
|
path.join(VENDOR, 'bootstrap-icons', 'font', 'fonts')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Font Awesome Free CSS + webfonts
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, '@fortawesome', 'fontawesome-free', 'css', 'all.min.css'),
|
||||||
|
path.join(VENDOR, 'fontawesome', 'css', 'all.min.css')
|
||||||
|
);
|
||||||
|
copyDir(
|
||||||
|
path.join(NM, '@fortawesome', 'fontawesome-free', 'webfonts'),
|
||||||
|
path.join(VENDOR, 'fontawesome', 'webfonts')
|
||||||
|
);
|
||||||
|
|
||||||
|
// marked – browser UMD build (path varies by version)
|
||||||
|
const markedCandidates = [
|
||||||
|
path.join(NM, 'marked', 'marked.min.js'), // v4.x
|
||||||
|
path.join(NM, 'marked', 'lib', 'marked.umd.min.js'), // v5.x
|
||||||
|
path.join(NM, 'marked', 'dist', 'marked.min.js'), // v9+
|
||||||
|
];
|
||||||
|
const markedSrc = markedCandidates.find(p => fs.existsSync(p));
|
||||||
|
if (!markedSrc) throw new Error('marked: no browser UMD build found in node_modules');
|
||||||
|
copyFile(markedSrc, path.join(VENDOR, 'marked', 'marked.min.js'));
|
||||||
|
|
||||||
|
// jQuery
|
||||||
|
copyFile(
|
||||||
|
path.join(NM, 'jquery', 'dist', 'jquery.min.js'),
|
||||||
|
path.join(VENDOR, 'jquery', 'jquery.min.js')
|
||||||
|
);
|
||||||
@@ -111,6 +111,19 @@ div#navbarNavfooter li.nav-item {
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Prevent nav items from wrapping to a second line */
|
||||||
|
div#navbarNavheader .navbar-nav,
|
||||||
|
div#navbarNavfooter .navbar-nav {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
div#navbarNavheader .navbar-nav::-webkit-scrollbar,
|
||||||
|
div#navbarNavfooter .navbar-nav::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
|
||||||
main, footer, header, nav {
|
main, footer, header, nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -168,13 +181,16 @@ iframe{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#navbar_logo {
|
#navbar_logo {
|
||||||
/* start invisible but in the layout (d-none will actually hide it) */
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity var(--anim-duration) ease-in-out;
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity var(--anim-duration) ease-in-out,
|
||||||
|
max-width var(--anim-duration) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#navbar_logo.visible {
|
#navbar_logo.visible {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,22 +9,19 @@
|
|||||||
href="{% if platform.favicon.cache %}{{ url_for('static', filename=platform.favicon.cache) }}{% endif %}"
|
href="{% if platform.favicon.cache %}{{ url_for('static', filename=platform.favicon.cache) }}{% endif %}"
|
||||||
>
|
>
|
||||||
<!-- Bootstrap CSS only -->
|
<!-- Bootstrap CSS only -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
||||||
<!-- Bootstrap JavaScript Bundle with Popper -->
|
<!-- Bootstrap JavaScript Bundle with Popper -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||||
<!-- Bootstrap Icons -->
|
<!-- Bootstrap Icons -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/font/bootstrap-icons.css') }}">
|
||||||
<!-- Fontawesome -->
|
<!-- Fontawesome -->
|
||||||
<script src="https://kit.fontawesome.com/56f96da298.js" crossorigin="anonymous"></script>
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fontawesome/css/all.min.css') }}">
|
||||||
<!-- Markdown -->
|
<!-- Markdown -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="{{ url_for('static', filename='vendor/marked/marked.min.js') }}"></script>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom_scrollbar.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom_scrollbar.css') }}">
|
||||||
<!-- JQuery -->
|
<!-- JQuery -->
|
||||||
<script
|
<script src="{{ url_for('static', filename='vendor/jquery/jquery.min.js') }}"></script>
|
||||||
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
|
||||||
crossorigin="anonymous">
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
{% if apod_bg %}
|
{% if apod_bg %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portfolio-ui"
|
name = "portfolio-ui"
|
||||||
version = "0.0.0"
|
version = "1.1.0"
|
||||||
description = "A lightweight YAML-driven portfolio and landing-page generator."
|
description = "A lightweight YAML-driven portfolio and landing-page generator."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user