Commit Graph

230 Commits

Author SHA1 Message Date
0c67f999f6 chore: ignore the local MCP configuration
.mcp.json is written per machine and carries no repository state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:18:30 +02:00
efabbd3b2e fix(modal): stop untrusted content reaching innerHTML and the iframe
Every one of these paths checked a string that the browser reinterprets
afterwards. isSafeUrl now hangs the value on an <a> and reads back
probe.protocol, so the check sees what the browser will see: a pre-parse
test reads "&#106;avascript:" as a relative path and passes it, and the
HTML parser then decodes it to "javascript:".

marked passes raw HTML through and emits hrefs unescaped. renderMarkdown
escapes the angle brackets before parsing, parses into an inert DOMParser
document where no script runs and no image loads, and drops anchors and
images whose scheme is not http, https or mailto. Blockquotes and
<autolinks> stop working as a result; neither appears in the configuration.

modalTitle and the alternatives list interpolated subitem.name and
icon.class into innerHTML. Both are built as nodes now. name is a
translatable key, so it arrives from the machine-written catalogues.

The link kept its click handler and its class across popups, because one
anchor serves all of them: a later, unrelated click opened whatever an
earlier popup pointed at, and addEventListener stacked one handler per
open. Both are reset per popup and the handler is assigned, not added.

openIframe guards its own argument. Removing the href alone left the
handler passing the raw URL on, and ?iframe= in the query string reaches
the same sink with no configuration involved at all.

Verified in headless Chromium: decimal and hex character references,
&Tab;- and &NewLine;-split schemes, reference-style links, raw HTML as a
link's text, and the two name sinks all executed before these changes.
injection.spec.js keeps all fifteen payloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:18:29 +02:00
2a35b2910a feat(i18n): serve every page in 30 languages
The interface ships translated; page content stays English until a
LibreTranslate instance fills app/i18n/content/ through make i18n. A string
without a catalogue entry falls back to its English source, so a half-filled
catalogue degrades instead of breaking.

Translation runs after ConfigurationResolver.resolve_links(), on a copy.
resolve_links matches by the `name` field, so translating it beforehand
would break every `link:` reference in the configuration.

negotiate() normalises to the primary subtag itself. Werkzeug's best_match
returns an exact match before it considers a primary-tag fallback, so the
Chrome default `de-DE,en;q=0.8` resolves to English there. "/" carries
Vary: Accept-Language, without which a shared cache pins the first
visitor's language for everyone.

The route rule lists the known codes as a converter argument. A bare
"/<lang>/" answers /robots.txt and /favicon.ico with a permanently
cacheable 308 to their trailing-slash form.

Templates gain lang, dir, the RTL stylesheet, a canonical URL and 30
hreflang alternates. Those are the first external URLs in this app:
ProxyFix takes the scheme from X-Forwarded-Proto so they do not claim
http:// behind a TLS-terminating proxy, X-Forwarded-Host stays untrusted
because nginx passes a client-supplied one through, and TRUSTED_HOSTS lets
Flask reject a forged Host outright.

Flask only autoescapes .html/.htm/.xml/.xhtml/.svg, so every *.html.j2
template interpolated configuration raw. Enabling it changes two lines of
the shipped page, both an apostrophe.

read_catalog degrades an unreadable catalogue to English rather than
serving a 500, and drops non-string entries that would otherwise render as
"42". i18n_sync writes atomically, never overwrites an existing entry,
refuses to touch a catalogue it could not parse, and leaves the file alone
when a run translated nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:18:29 +02:00
a42930a699 build(make): run e2e without act, lint yaml, js and shell
test-e2e drove Cypress through act, which fails to start where it cannot
resolve a host address for its artifact server. It now starts Flask and
Cypress in one shell via scripts/run-e2e.sh, so both share a network
namespace; the act path stays available as test-e2e-act.

The script guards the cases that made the old target lie: it aborts when
something already serves the port instead of testing that server, checks
that its own Flask is alive before trusting a response, pins Cypress to
the same origin Flask binds, and drops ELECTRON_RUN_AS_NODE, which VS Code
exports and which makes Cypress' bundled Electron reject its own flags.

30 YAML and 18 JavaScript files had no linter. yamllint runs correctness
rules only, because the repository predates it and its cosmetic findings
would be noise; key-duplicates is the one that earns its keep, since
PyYAML keeps the last of two identical keys without complaining. eslint
runs the recommended set and already found a dead getBoundingClientRect()
call in navigation.js. Both get a CI job so make lint and the workflows
stop diverging.

flask>=3.1 because app.config["TRUSTED_HOSTS"] arrived in 3.1 and an older
Flask accepts the key and ignores it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:18:27 +02:00
03b7cef90e Release version 2.0.0 2026-05-18 12:26:26 +02:00
4b29448b33 lint 2026-05-18 12:21:05 +02:00
3f30621630 feat(assets): probe-first resolver + SPOT for IMAGE_NAME/PORT + README screenshot
Probe-first asset resolution (regression fix)
---------------------------------------------

cache_manager.cache_file() returned either a relative cache path
(success) or None (failure). The previous app.py fallback
asset['cache'] = cached or asset['source'] mixed both types into
one field, which the template wrapped in url_for('static', ...)
regardless — producing broken
/static/https://file.infinito.nexus/.../logo.png URLs whenever the
source couldn't be downloaded.

- New app/utils/asset_resolver.py: HEAD-probes the URL (3 s
  timeout, image/* content type). On hit, embed directly via a
  new external_url field — no download required. On miss, fall
  back to cache_manager.cache_file. If that also fails, expose
  the source URL via external_url so the browser shows the alt
  text instead of an empty src.
- app.py exposes an asset_src(asset) context processor that
  picks external_url first, then url_for('static', cache),
  so the template never wraps an absolute URL in a static prefix.
- Templates (base, navigation, card) switch to asset_src(...) and
  gate the card image branch on cache or external_url.
- 16 unit tests cover every probe/cache/fallback branch; one live
  integration test exercises the canonical
  https://file.infinito.nexus/assets/img/logo.png to prove the
  probe-first path works end-to-end (cache dir stays empty).
- config.sample.yaml: new Infinito.Nexus card driven by the same
  canonical asset URL.

Single source of truth for IMAGE_NAME and PORT
----------------------------------------------

- env.example is now the only place the literal values live.
- Makefile and docker-compose.yml reference \$(IMAGE_NAME) /
  \${IMAGE_NAME:?…} (same for PORT); no defaults, no silent
  fallbacks.
- New make env / make config bootstrap .env / app/config.yaml
  from their checked-in templates. Idempotent.
- All container-using targets depend on the two bootstrap targets
  so a fresh checkout runs in a single invocation.
- Recipes source .env at recipe-execution time so they pick up a
  freshly bootstrapped .env in the same make invocation.

README
------

- Screenshot added under the title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:19:15 +02:00
39a41e561c Release version 1.2.0 2026-05-11 02:51:17 +02:00
4424db22cb fix(cypress): allow scrollTo on non-scrollable pages in footer dropup test
cy.scrollTo('bottom') threw on CI ("element is not scrollable") whenever
the rendered page fit inside the viewport. Pass ensureScrollable:false
so the call is a no-op on short pages — the footer is already in view
and the subsequent rect-position pre-check enforces the actual
precondition that chooseDirection() needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:38:06 +02:00
28a796e24f chore(claude): enable sandbox and consolidate bash allowlist
Activate the harness sandbox (enabled + autoAllowBashIfSandboxed +
filesystem write/deny rules) and replace the ~30 specific Bash(...)
permission entries with a single Bash(*) wildcard. The existing deny
list (git push --force, git reset --hard, rm -rf, sudo) and ask list
(git push, docker run, curl) keep their precedence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:36:28 +02:00
f3c15e3e1c fix(navigation): unclip dropdowns and flip toward the side with more space
- Move <header> overflow:hidden into body.fullscreen scope and drop the
  implicit-vertical-clip overflow-x:auto from .navbar-nav so dropdown
  menus can escape the navbar.
- Drive top-level dropdowns through bootstrap.Dropdown (popperConfig
  strategy:'fixed'), and add a chooseDirection() helper that toggles
  .dropup/.dropdown on the .nav-item based on space above vs below
  before each show. Split the navigation.css rules to position the menu
  with top:100% or bottom:100% accordingly.
- Mark the dropdown toggle with data-bs-toggle="dropdown" in the
  template; cover that with a Jinja-rendered unit test and add Cypress
  specs for the header (opens downward) and footer (flips to .dropup)
  cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:26:02 +02:00
3301f8d95f chore(claude): allow jobs bash command in harness
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:22:06 +02:00
a575fddaa2 build(compose): run npm install in container, persist deps in named volumes
Replace the host-side `make npm-install` step with a compose-level
`command:` override that runs `npm install` inside the container on
every start. Back node_modules and static/vendor with named volumes so
the bind-mounted source tree no longer shadows the install while state
still survives container restarts. Drop the now-redundant npm-install
target and its references in up/dev/prod/test-e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:17:53 +02:00
c9fe7d8583 chore(claude): expand harness allowlist and ignore local state
Add permissions for read-only test/inspection commands (make test-e2e,
docker exec/restart, /tmp reads) and gitignore everything under .claude
except the shared settings/gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:15:04 +02:00
03f17a6e05 build(make): run npm-install before docker-compose targets
Add npm-install dependency to up/dev/prod targets so app/static/vendor
and app/node_modules exist on the host before the ./app:/app bind mount
masks the build-time artifacts inside the container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:40:19 +02:00
3132aab2a5 Release version 1.1.0
Some checks failed
CI / Run security workflow (push) Has been cancelled
CI / Run test workflow (push) Has been cancelled
CI / Run lint workflow (push) Has been cancelled
CI / Publish image (push) Has been cancelled
v1.1.0
2026-03-30 10:47:32 +02:00
3d1db1f8ba Updated mirrors
Some checks failed
CI / Run security workflow (push) Has been cancelled
CI / Run test workflow (push) Has been cancelled
CI / Run lint workflow (push) Has been cancelled
CI / Publish image (push) Has been cancelled
2026-03-30 10:46:39 +02:00
58872ced81 fix(ci): grant security-events: write to lint job
The lint-docker job in lint.yml requires security-events: write
for SARIF upload; must be explicitly granted to the caller job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 10:18:14 +02:00
13b3af3330 Entered and removed whitespaces in README.md 2026-03-30 10:16:53 +02:00
eca7084f4e fix(ci): grant security-events and packages permissions to security job
Reusable workflow calls inherit only explicitly granted permissions.
The nested security job requires packages: read and security-events: write
for CodeQL analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 10:16:30 +02:00
6861b2c0eb fix(nav): prevent navbar items from wrapping to second line
- Set flex-wrap: nowrap on navbar-nav to keep all items in one row
- Add hidden overflow-x scroll (no visible scrollbar) as fallback
- Fix #navbar_logo taking up space when invisible via max-width transition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 10:10:55 +02:00
66b1f0d029 feat(agents): add AGENTS.md and CLAUDE.md with pre-commit rules
- Add AGENTS.md: require make test before every non-doc commit and
  document the npm vendor asset workflow
- Add CLAUDE.md: instruct agents to read AGENTS.md at conversation start
- Add npm-install dependency to test-e2e Makefile target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:59:09 +02:00
a29a0b1862 feat(vendor): replace CDN dependencies with local npm packages
Introduces a vendor build pipeline so all third-party browser assets
(Bootstrap, Bootstrap Icons, Font Awesome, marked, jQuery) are served
from local static files instead of external CDNs.

- Add app/package.json with vendor deps and postinstall/build scripts
- Add app/scripts/copy-vendor.js to copy assets to static/vendor/
- Update base.html.j2 to use url_for('static', ...) for all vendor assets
- Update Dockerfile to install Node.js/npm and run npm install
- Update .gitignore to exclude app/node_modules/ and app/static/vendor/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:29:28 +02:00
252b50d2a7 feat: migrate to pyproject.toml, add test suites, split CI workflows
- Replace requirements.txt with pyproject.toml for modern Python packaging
- Add unit, integration, lint and security test suites under tests/
- Add utils/export_runtime_requirements.py and utils/check_hadolint_sarif.py
- Split monolithic CI into reusable lint.yml, security.yml and tests.yml
- Refactor ci.yml to orchestrate reusable workflows; publish on semver tag only
- Modernize Dockerfile: pin python:3.12-slim, install via pyproject.toml
- Expand Makefile with lint, security, test and CI targets
- Add test-e2e via act with portfolio container stop/start around run
- Fix navbar_logo_visibility.spec.js: win.fullscreen() → win.enterFullscreen()
- Set use_reloader=False in app.run() to prevent double-start in CI
- Add app/core.* and build artifacts to .gitignore
- Fix apt-get → sudo apt-get in tests.yml e2e job
- Fix pip install --ignore-installed to handle stale act cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 23:03:09 +02:00
2c61da9fc3 chore: remove comments from settings.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:37:10 +01:00
2d8185b747 chore: add Claude Code project permissions settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:16:29 +01:00
a47a5babce chore: remove unused imports 2026-03-28 19:08:57 +01:00
3f6c90cc3c Release version 1.0.0 2026-02-19 11:18:50 +01:00
69c4f15ce7 Prepare config.yaml from sample in CI before Cypress 2026-02-19 11:14:52 +01:00
56c1b0d0cd Fix Cypress action for repositories without lockfile 2026-02-19 11:13:12 +01:00
91e9caea48 Fix CI node cache path and npm install strategy 2026-02-19 11:11:11 +01:00
feb6af28ef Add CI workflow for tests and conditional image publish 2026-02-19 11:05:26 +01:00
f8c2b4236b Added d-flex to place logo next to brand 2025-07-21 14:24:28 +02:00
dc2626e020 Added test for log 2025-07-21 12:18:07 +02:00
46b0b744ca Added logo to navbar when in fullscreen 2025-07-21 11:39:59 +02:00
5f2e7ef696 Changed pkgmgr commands 2025-07-12 18:54:42 +02:00
152a85bfb8 Merge branch 'main' of github.com:kevinveenbirkenbach/portfolio 2025-07-12 18:53:03 +02:00
fdfe301868 Renamed PortWebUI to PortUI 2025-07-12 18:52:51 +02:00
cbfe1ed8ae Update README.md
Solved another layout bug
2025-07-12 18:27:47 +02:00
9470162236 Solved formatation bug 2025-07-12 18:26:51 +02:00
6a57fa1e00 Optimized README.md 2025-07-12 18:24:49 +02:00
ab67fc0b29 Renamed portfolio to PortWebUI 2025-07-12 18:22:19 +02:00
e18566d801 Solved some bugs 2025-07-09 22:20:58 +02:00
7bc0f32145 Added cypress tests 2025-07-08 17:16:57 +02:00
6ed3e60dd0 Solved 2tap fullscreen hight bug 2025-07-08 14:39:13 +02:00
ab8ea0dbd6 Added iframe observer 2025-07-07 23:40:35 +02:00
b0446dcd29 Added include svgs 2025-07-07 19:14:29 +02:00
55d309b2d7 Changed fade between html iframe animation 2025-07-07 15:37:24 +02:00
d99a8c8452 Added restore functionality to small logo 2025-07-07 15:06:36 +02:00
3f6a195ecb Optimizid hover 2025-07-07 13:37:02 +02:00