mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-05-19 19:44:14 +00:00
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>
210 lines
5.8 KiB
Makefile
210 lines
5.8 KiB
Makefile
# Load environment variables from .env
|
|
ifneq (,$(wildcard .env))
|
|
include .env
|
|
# Export variables defined in .env
|
|
export $(shell sed 's/=.*//' .env)
|
|
endif
|
|
|
|
PYTHON ?= python3
|
|
ACT ?= act
|
|
|
|
# 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
|
|
|
|
.PHONY: build
|
|
build: env config
|
|
# Build the Docker image.
|
|
@$(call _require_env,IMAGE_NAME); \
|
|
docker build -t "$$IMAGE_NAME" .
|
|
|
|
.PHONY: build-no-cache
|
|
build-no-cache: env config
|
|
# Build the Docker image without cache.
|
|
@$(call _require_env,IMAGE_NAME); \
|
|
docker build --no-cache -t "$$IMAGE_NAME" .
|
|
|
|
.PHONY: up
|
|
up: env config
|
|
# Start the application using docker-compose with build.
|
|
docker-compose up -d --build --force-recreate
|
|
|
|
.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
|
|
run-dev: env config config
|
|
# Run the container in development mode (hot-reload).
|
|
@$(call _require_env,IMAGE_NAME PORT); \
|
|
docker run -d \
|
|
-p "$$PORT:$$PORT" \
|
|
--name portfolio \
|
|
-v "$(PWD)/app/:/app" \
|
|
-e FLASK_APP=app.py \
|
|
-e FLASK_ENV=development \
|
|
"$$IMAGE_NAME"
|
|
|
|
.PHONY: run-prod
|
|
run-prod: env config config
|
|
# Run the container in production mode.
|
|
@$(call _require_env,IMAGE_NAME PORT); \
|
|
docker run -d \
|
|
-p "$$PORT:$$PORT" \
|
|
--name portfolio \
|
|
"$$IMAGE_NAME"
|
|
|
|
.PHONY: logs
|
|
logs:
|
|
# Display the logs of the 'portfolio' container.
|
|
docker logs -f portfolio
|
|
|
|
.PHONY: dev
|
|
dev: env config
|
|
# Start the application in development mode using docker-compose.
|
|
FLASK_ENV=development docker-compose up -d
|
|
|
|
.PHONY: prod
|
|
prod: env config
|
|
# 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
|
|
browse: env
|
|
# Open the application in the browser at http://localhost:$$PORT
|
|
@$(call _require_env,PORT); \
|
|
chromium "http://localhost:$$PORT"
|
|
|
|
.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]"
|
|
|
|
.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
|
|
|
|
.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
|
|
lint: lint-actions lint-python lint-docker test-lint
|
|
# 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
|
|
|
|
.PHONY: test-e2e
|
|
test-e2e:
|
|
# Run Cypress end-to-end tests via act (stop portfolio container to free port first).
|
|
-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.
|