2025-07-08 17:16:57 +02:00
|
|
|
# Load environment variables from .env
|
|
|
|
|
ifneq (,$(wildcard .env))
|
|
|
|
|
include .env
|
|
|
|
|
# Export variables defined in .env
|
|
|
|
|
export $(shell sed 's/=.*//' .env)
|
|
|
|
|
endif
|
|
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
PYTHON ?= python3
|
|
|
|
|
ACT ?= act
|
2025-07-08 17:16:57 +02:00
|
|
|
|
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
|
|
|
# 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
|
|
|
|
|
|
2025-07-08 17:16:57 +02:00
|
|
|
.PHONY: build
|
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
|
|
|
build: env config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Build the Docker image.
|
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
|
|
|
@$(call _require_env,IMAGE_NAME); \
|
|
|
|
|
docker build -t "$$IMAGE_NAME" .
|
2025-07-08 17:16:57 +02:00
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
.PHONY: build-no-cache
|
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
|
|
|
build-no-cache: env config
|
2026-03-29 23:03:09 +02:00
|
|
|
# Build the Docker image without cache.
|
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
|
|
|
@$(call _require_env,IMAGE_NAME); \
|
|
|
|
|
docker build --no-cache -t "$$IMAGE_NAME" .
|
2026-03-29 23:03:09 +02:00
|
|
|
|
2025-07-08 17:16:57 +02:00
|
|
|
.PHONY: up
|
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
|
|
|
up: env config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Start the application using docker-compose with build.
|
2026-03-29 23:03:09 +02:00
|
|
|
docker-compose up -d --build --force-recreate
|
2025-07-08 17:16:57 +02:00
|
|
|
|
|
|
|
|
.PHONY: down
|
|
|
|
|
down:
|
|
|
|
|
# Stop and remove the 'portfolio' container, ignore errors, and bring down compose.
|
|
|
|
|
- docker stop portfolio || true
|
|
|
|
|
- docker rm portfolio || true
|
|
|
|
|
- docker-compose down
|
|
|
|
|
|
|
|
|
|
.PHONY: run-dev
|
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
|
|
|
run-dev: env config config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Run the container in development mode (hot-reload).
|
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
|
|
|
@$(call _require_env,IMAGE_NAME PORT); \
|
|
|
|
|
docker run -d \
|
|
|
|
|
-p "$$PORT:$$PORT" \
|
|
|
|
|
--name portfolio \
|
|
|
|
|
-v "$(PWD)/app/:/app" \
|
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-21 23:51:40 +02:00
|
|
|
-e PORT="$$PORT" \
|
|
|
|
|
-e TRUSTED_HOSTS="$$TRUSTED_HOSTS" \
|
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
|
|
|
-e FLASK_APP=app.py \
|
|
|
|
|
-e FLASK_ENV=development \
|
|
|
|
|
"$$IMAGE_NAME"
|
2025-07-08 17:16:57 +02:00
|
|
|
|
|
|
|
|
.PHONY: run-prod
|
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
|
|
|
run-prod: env config config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Run the container in production mode.
|
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
|
|
|
@$(call _require_env,IMAGE_NAME PORT); \
|
|
|
|
|
docker run -d \
|
|
|
|
|
-p "$$PORT:$$PORT" \
|
|
|
|
|
--name portfolio \
|
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-21 23:51:40 +02:00
|
|
|
-e PORT="$$PORT" \
|
|
|
|
|
-e TRUSTED_HOSTS="$$TRUSTED_HOSTS" \
|
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
|
|
|
"$$IMAGE_NAME"
|
2025-07-08 17:16:57 +02:00
|
|
|
|
|
|
|
|
.PHONY: logs
|
|
|
|
|
logs:
|
|
|
|
|
# Display the logs of the 'portfolio' container.
|
|
|
|
|
docker logs -f portfolio
|
|
|
|
|
|
|
|
|
|
.PHONY: dev
|
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
|
|
|
dev: env config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Start the application in development mode using docker-compose.
|
|
|
|
|
FLASK_ENV=development docker-compose up -d
|
|
|
|
|
|
|
|
|
|
.PHONY: prod
|
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
|
|
|
prod: env config
|
2025-07-08 17:16:57 +02:00
|
|
|
# Start the application in production mode using docker-compose (with build).
|
|
|
|
|
docker-compose up -d --build
|
|
|
|
|
|
|
|
|
|
.PHONY: cleanup
|
|
|
|
|
cleanup:
|
|
|
|
|
# Remove all stopped Docker containers to reclaim space.
|
|
|
|
|
docker container prune -f
|
|
|
|
|
|
|
|
|
|
.PHONY: delete
|
|
|
|
|
delete:
|
|
|
|
|
# Force remove the 'portfolio' container if it exists.
|
|
|
|
|
- docker rm -f portfolio
|
|
|
|
|
|
|
|
|
|
.PHONY: browse
|
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
|
|
|
browse: env
|
|
|
|
|
# Open the application in the browser at http://localhost:$$PORT
|
|
|
|
|
@$(call _require_env,PORT); \
|
|
|
|
|
chromium "http://localhost:$$PORT"
|
2025-07-08 17:16:57 +02:00
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
.PHONY: install
|
|
|
|
|
install:
|
|
|
|
|
# Install runtime Python dependencies from pyproject.toml.
|
|
|
|
|
$(PYTHON) -m pip install -e .
|
|
|
|
|
|
|
|
|
|
.PHONY: install-dev
|
|
|
|
|
install-dev:
|
|
|
|
|
# Install runtime and developer dependencies from pyproject.toml.
|
|
|
|
|
$(PYTHON) -m pip install -e ".[dev]"
|
|
|
|
|
|
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-21 23:51:40 +02:00
|
|
|
.PHONY: i18n
|
|
|
|
|
i18n: env config
|
|
|
|
|
# Fill missing content translations in app/i18n/content/ via LibreTranslate.
|
|
|
|
|
@$(call _require_env,LIBRETRANSLATE_URL); \
|
|
|
|
|
$(PYTHON) utils/i18n_sync.py \
|
|
|
|
|
--url "$$LIBRETRANSLATE_URL" \
|
|
|
|
|
--api-key "$$LIBRETRANSLATE_API_KEY"
|
|
|
|
|
|
feat(i18n): offer every ISO 639-1 language
The table was thirty languages typed by hand. It is now generated:
utils/generate_languages.py takes the 184 alpha-2 codes from pycountry,
the display names from CLDR through babel, and the writing direction from
CLDR character order. 159 languages carry their endonym; the remaining 25
have no CLDR entry and carry their English ISO name.
That corrects the right-to-left set, which had four entries and needs ten
— dv, ks, ps, sd, ug and yi were simply missed.
Only 29 languages ship an interface catalogue, so the other 155 render in
English until one is filled. make i18n-ui fills app/i18n/ui/ for them, and
make i18n now covers the interface strings as well; neither asks for a
string a shipped catalogue already answers, so hand-written entries stay.
184 entries do not fit on a screen, so the language menu scrolls inside
itself. overscroll-behavior keeps the page behind it from moving once the
list reaches its end.
babel and pycountry are dev dependencies: the generator needs them, the
application does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:08:28 +02:00
|
|
|
.PHONY: i18n-ui
|
|
|
|
|
i18n-ui: env
|
|
|
|
|
# Fill missing interface translations in app/i18n/ui/ via LibreTranslate.
|
|
|
|
|
@$(call _require_env,LIBRETRANSLATE_URL); \
|
|
|
|
|
$(PYTHON) utils/i18n_sync.py \
|
|
|
|
|
--catalog ui \
|
|
|
|
|
--url "$$LIBRETRANSLATE_URL" \
|
|
|
|
|
--api-key "$$LIBRETRANSLATE_API_KEY"
|
|
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
.PHONY: lint-actions
|
|
|
|
|
lint-actions:
|
|
|
|
|
# Lint GitHub Actions workflows.
|
|
|
|
|
docker run --rm -v "$$PWD:/repo" -w /repo rhysd/actionlint:latest
|
|
|
|
|
|
|
|
|
|
.PHONY: lint-python
|
|
|
|
|
lint-python: install-dev
|
|
|
|
|
# Run Python lint and format checks.
|
|
|
|
|
$(PYTHON) -m ruff check .
|
|
|
|
|
$(PYTHON) -m ruff format --check .
|
|
|
|
|
|
|
|
|
|
.PHONY: lint-docker
|
|
|
|
|
lint-docker:
|
|
|
|
|
# Lint the Dockerfile.
|
|
|
|
|
docker run --rm -i hadolint/hadolint < Dockerfile
|
|
|
|
|
|
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-21 23:51:40 +02:00
|
|
|
.PHONY: lint-yaml
|
|
|
|
|
lint-yaml: install-dev
|
|
|
|
|
# Lint YAML for duplicate keys and ambiguous scalars (see .yamllint).
|
|
|
|
|
$(PYTHON) -m yamllint --strict .
|
|
|
|
|
|
|
|
|
|
.PHONY: lint-js
|
|
|
|
|
lint-js: node-deps
|
|
|
|
|
# Lint the browser and Cypress JavaScript.
|
|
|
|
|
cd app && env -u ELECTRON_RUN_AS_NODE npx eslint .
|
|
|
|
|
|
|
|
|
|
.PHONY: lint-shell
|
|
|
|
|
lint-shell:
|
|
|
|
|
# Lint the shell scripts.
|
|
|
|
|
docker run --rm -v "$$PWD:/mnt" -w /mnt koalaman/shellcheck:stable scripts/*.sh
|
|
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
.PHONY: test-lint
|
|
|
|
|
test-lint:
|
|
|
|
|
# Run lint guardrail tests.
|
|
|
|
|
$(PYTHON) -m unittest discover -s tests/lint -t .
|
|
|
|
|
|
|
|
|
|
.PHONY: test-integration
|
|
|
|
|
test-integration: install
|
|
|
|
|
# Run repository integration tests.
|
|
|
|
|
$(PYTHON) -m unittest discover -s tests/integration -t .
|
|
|
|
|
|
|
|
|
|
.PHONY: test-unit
|
|
|
|
|
test-unit: install
|
|
|
|
|
# Run repository unit tests.
|
|
|
|
|
$(PYTHON) -m unittest discover -s tests/unit -t .
|
|
|
|
|
|
|
|
|
|
.PHONY: test-security
|
|
|
|
|
test-security: install
|
|
|
|
|
# Run repository security guardrail tests.
|
|
|
|
|
$(PYTHON) -m unittest discover -s tests/security -t .
|
|
|
|
|
|
|
|
|
|
.PHONY: lint
|
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-21 23:51:40 +02:00
|
|
|
lint: lint-actions lint-python lint-yaml lint-js lint-docker lint-shell test-lint
|
2026-03-29 23:03:09 +02:00
|
|
|
# Run the full lint suite.
|
|
|
|
|
|
|
|
|
|
.PHONY: security
|
|
|
|
|
security: install-dev test-security
|
|
|
|
|
# Run security checks.
|
|
|
|
|
$(PYTHON) -m bandit -q -ll -ii -r app main.py
|
|
|
|
|
$(PYTHON) utils/export_runtime_requirements.py > /tmp/portfolio-runtime-requirements.txt
|
|
|
|
|
$(PYTHON) -m pip_audit -r /tmp/portfolio-runtime-requirements.txt
|
|
|
|
|
|
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-21 23:51:40 +02:00
|
|
|
.PHONY: node-deps
|
|
|
|
|
node-deps:
|
|
|
|
|
# Install the Cypress binary and the browser vendor assets into app/.
|
|
|
|
|
cd app && npm install
|
|
|
|
|
|
2026-03-29 23:03:09 +02:00
|
|
|
.PHONY: test-e2e
|
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-21 23:51:40 +02:00
|
|
|
test-e2e: env config node-deps
|
|
|
|
|
# Run Cypress against a locally started Flask app — no act, no runner image.
|
|
|
|
|
@$(call _require_env,PORT); \
|
|
|
|
|
PORT="$$PORT" PYTHON="$(PYTHON)" scripts/run-e2e.sh
|
|
|
|
|
|
|
|
|
|
.PHONY: test-e2e-act
|
|
|
|
|
test-e2e-act:
|
|
|
|
|
# Run the CI end-to-end job through act (stop portfolio container to free port first).
|
2026-03-29 23:03:09 +02:00
|
|
|
-docker stop portfolio 2>/dev/null || true
|
|
|
|
|
$(ACT) workflow_dispatch -W .github/workflows/tests.yml -j e2e
|
|
|
|
|
-docker start portfolio 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
.PHONY: test-workflow
|
|
|
|
|
test-workflow:
|
|
|
|
|
# Run the GitHub test workflow locally via act.
|
|
|
|
|
$(ACT) workflow_dispatch -W .github/workflows/tests.yml
|
|
|
|
|
|
|
|
|
|
.PHONY: lint-workflow
|
|
|
|
|
lint-workflow:
|
|
|
|
|
# Run the GitHub lint workflow locally via act.
|
|
|
|
|
$(ACT) workflow_dispatch -W .github/workflows/lint.yml
|
|
|
|
|
|
|
|
|
|
.PHONY: quality
|
|
|
|
|
quality: lint-workflow test-workflow
|
|
|
|
|
# Run the GitHub lint and test workflows locally via act.
|
|
|
|
|
|
|
|
|
|
.PHONY: ci
|
|
|
|
|
ci: lint security test-unit test-integration test-e2e
|
|
|
|
|
# Run the local CI suite.
|
|
|
|
|
|
|
|
|
|
.PHONY: test
|
|
|
|
|
test: ci
|
|
|
|
|
# Run the full validation suite.
|