diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000000..c96df56c9b --- /dev/null +++ b/.containerignore @@ -0,0 +1,25 @@ +.github/ +.claude/ +.venv/ +.vscode/ +.idea/ +__pycache__/ +site/ +tmp-site/ +tests/ +script/ +templates/ +*.pyc +.DS_Store +.gitignore +.mlcrc +.pre-commit-config.yaml +mlc_config.json +package.json +package-lock.json +container/Containerfile +.containerignore +README.md +CONTRIBUTING.md +LICENSE.md +relatie-ak-tot-toetsingskaders.csv diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 94b5bb43af..f441c4a8ed 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,8 +8,6 @@ updates: time: "10:00" timezone: "Europe/Amsterdam" open-pull-requests-limit: 5 - reviewers: - - "ruthkoole" groups: all-github-actions: patterns: @@ -23,8 +21,6 @@ updates: time: "10:00" timezone: "Europe/Amsterdam" open-pull-requests-limit: 5 - reviewers: - - "ruthkoole" groups: all-pip-packages: patterns: diff --git a/.github/workflows/preview-build-and-deploy.yml b/.github/workflows/preview-build-and-deploy.yml new file mode 100644 index 0000000000..05f90fb0e1 --- /dev/null +++ b/.github/workflows/preview-build-and-deploy.yml @@ -0,0 +1,161 @@ +name: Preview build and deploy + +on: + push: + branches: + - main + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - closed + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/preview + ZAD_PROJECT_ID: algor-1ha + ZAD_COMPONENT: component-2 + +jobs: + build: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get version + id: version + run: | + echo "version=$(git describe --tags 2>/dev/null || echo 'development')" >> "$GITHUB_OUTPUT" + + - name: Determine deployment name and URL + id: deployment + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + NAME="pr${{ github.event.pull_request.number }}" + TAG_PREFIX="pr-${{ github.event.pull_request.number }}-" + else + NAME="main" + TAG_PREFIX="main-" + fi + URL="https://${{ env.ZAD_COMPONENT }}-${NAME}-${{ env.ZAD_PROJECT_ID }}.rig.prd1.gn2.quattro.rijksapps.nl/kader/" + echo "name=${NAME}" >> "$GITHUB_OUTPUT" + echo "tag_prefix=${TAG_PREFIX}" >> "$GITHUB_OUTPUT" + echo "url=${URL}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,format=short,prefix=${{ steps.deployment.outputs.tag_prefix }} + + - name: Build and push container image + uses: docker/build-push-action@v6 + with: + context: . + file: ./container/Containerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + SITE_URL=${{ steps.deployment.outputs.url }} + VERSION=${{ steps.version.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + deployments: write + pull-requests: write + needs: build + environment: + name: ${{ github.event_name == 'pull_request' && format('pr{0}', github.event.pull_request.number) || 'main' }} + url: ${{ steps.deploy.outputs.url }} + steps: + - name: Deploy to ZAD + id: deploy + uses: RijksICTGilde/zad-actions/deploy@v2 + with: + api-key: ${{ secrets.ZAD_API_KEY }} + project-id: ${{ env.ZAD_PROJECT_ID }} + deployment-name: ${{ github.event_name == 'pull_request' && format('pr{0}', github.event.pull_request.number) || 'main' }} + component: ${{ env.ZAD_COMPONENT }} + image: ${{ needs.build.outputs.image }} + comment-on-pr: ${{ github.event_name == 'pull_request' }} + qr-code: ${{ github.event_name == 'pull_request' }} + wait-for-ready: true + path-suffix: /kader/ + + cleanup-preview: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + permissions: + deployments: write + packages: write + pull-requests: write + steps: + - name: Cleanup ZAD deployment + uses: RijksICTGilde/zad-actions/cleanup@v2 + with: + api-key: ${{ secrets.ZAD_API_KEY }} + project-id: ${{ env.ZAD_PROJECT_ID }} + deployment-name: pr${{ github.event.pull_request.number }} + delete-github-env: true + delete-github-deployments: true + delete-pr-comment: true + + - name: Delete container images for this PR + shell: bash + env: + GH_TOKEN: ${{ github.token }} + CONTAINER_ORG: ${{ github.repository_owner }} + TAG_PREFIX: pr-${{ github.event.pull_request.number }}- + run: | + echo "Deleting container images with tag prefix: $TAG_PREFIX" + CONTAINER_NAME="${{ github.event.repository.name }}/preview" + ENCODED_NAME=$(printf '%s' "$CONTAINER_NAME" | jq -sRr @uri) + VERSIONS=$(gh api "orgs/$CONTAINER_ORG/packages/container/$ENCODED_NAME/versions" 2>/dev/null | \ + jq -r --arg prefix "$TAG_PREFIX" '.[] | select(.metadata.container.tags[]? | startswith($prefix)) | .id') + if [ -z "$VERSIONS" ]; then + echo "No versions found with tag prefix: $TAG_PREFIX (may already be deleted)" + exit 0 + fi + DELETED_COUNT=0 + for VERSION_ID in $VERSIONS; do + if gh api "orgs/$CONTAINER_ORG/packages/container/$ENCODED_NAME/versions/$VERSION_ID" -X DELETE 2>/dev/null; then + echo "Deleted version: $VERSION_ID" + DELETED_COUNT=$((DELETED_COUNT + 1)) + else + echo "Failed to delete version: $VERSION_ID" + fi + done + echo "Deleted $DELETED_COUNT container version(s) for PR ${{ github.event.pull_request.number }}" diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml deleted file mode 100644 index 704ba59db5..0000000000 --- a/.github/workflows/preview.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Deploy PR preview -on: - pull_request: - types: - - opened - - reopened - - synchronize - - closed - -concurrency: preview-${{ github.ref }} - -jobs: - deploy: - name: deploy PR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: get tag - id: get_commit_hash - run: | - echo "commit_hash=$(git describe --tags)" >> "$GITHUB_OUTPUT" - - - name: inject version - run: | - sed -i 's/development/${{ steps.get_commit_hash.outputs.commit_hash }}/g' docs/version.md - - - name: Add url - run: | - echo "site_url: https://minbzk.github.io/Algoritmekader/pr-preview/pr-${{github.event.number}}" >> mkdocs.yml - - - uses: actions/setup-python@v6 - if: github.event.action != 'closed' - with: - python-version: 3.x - cache: 'pip' - - - name: install dependencies - if: github.event.action != 'closed' - run: pip install -r requirements.txt - - - name: build preview - if: github.event.action != 'closed' - run: mkdocs build - - - uses: actions/upload-artifact@v6 - if: github.event.action != 'closed' - with: - name: AlgoritmeKaderWebsite-${{github.event.number}} - path: ./site/ - - - name: Deploy preview - if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' - uses: rossjrw/pr-preview-action@v1 - with: - source-dir: ./site/ diff --git a/.github/workflows/production-build-and-deploy.yml b/.github/workflows/production-build-and-deploy.yml new file mode 100644 index 0000000000..b0d8248d06 --- /dev/null +++ b/.github/workflows/production-build-and-deploy.yml @@ -0,0 +1,74 @@ +name: Production build and deploy + +on: + push: + tags: + - v* + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + ZAD_PROJECT_ID: algor-1ha + ZAD_COMPONENT: component-2 + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + + - name: Build and push container image + uses: docker/build-push-action@v6 + with: + context: . + file: ./container/Containerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + SITE_URL=https://algoritmes.overheid.nl/kader/ + VERSION=${{ github.ref_name }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + runs-on: ubuntu-latest + needs: build + permissions: + deployments: write + environment: + name: production + steps: + - name: Deploy to ZAD + uses: RijksICTGilde/zad-actions/deploy@v2 + with: + api-key: ${{ secrets.ZAD_API_KEY }} + project-id: ${{ env.ZAD_PROJECT_ID }} + component: ${{ env.ZAD_COMPONENT }} + image: ${{ needs.build.outputs.image }} diff --git a/.gitignore b/.gitignore index 8b2697f80d..68018a2fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ site # IDE files .idea/ +# Claude Code +.claude/ + .Rproj.user diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 899e216126..d74c71cb1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,7 +12,7 @@ repos: - id: check-added-large-files - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.9 + rev: v0.15.1 hooks: - id: ruff - id: ruff-format diff --git a/README.md b/README.md index ccd5635821..bbee7ac180 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,27 @@ Dat kan op verschillende manieren. Zie onze ### Lokaal ontwikkelen -Het Algoritmekader project kan lokaal met behulp van [Python](https://www.python.org/) worden gedraaid. Installeer -hiervoor de benodigde packages in een [virtual environment](https://docs.python.org/3/library/venv.html): +Het Algoritmekader project kan lokaal worden gedraaid met [Python](https://www.python.org/) of met een container. + +#### Met Python + +Installeer de benodigde packages in een [virtual environment](https://docs.python.org/3/library/venv.html): ```bash pip install -r requirements.txt +mkdocs serve ``` -Vervolgens kun je een preview van het Algoritmekader bekijken: +#### Met Podman/Docker + +Bouw en draai het Algoritmekader als container: ```bash -mkdocs serve +podman build -t algoritmekader -f container/Containerfile . +podman run -p 8080:8080 algoritmekader ``` +Open vervolgens [http://localhost:8080](http://localhost:8080). ## Validatie Tools diff --git a/container/Containerfile b/container/Containerfile new file mode 100644 index 0000000000..b8606774c0 --- /dev/null +++ b/container/Containerfile @@ -0,0 +1,28 @@ +FROM python:3.14-slim AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ARG SITE_URL=http://localhost:8080/ +RUN sed -i "s|^site_url:.*|site_url: ${SITE_URL}|" mkdocs.yml + +ARG VERSION +RUN if [ -n "$VERSION" ]; then \ + sed -i "s/development/${VERSION}/g" docs/version.md; \ + fi + +RUN mkdocs build + +FROM nginxinc/nginx-unprivileged:stable-alpine-slim +COPY --from=builder /app/site /usr/share/nginx/html +COPY container/nginx.conf /etc/nginx/nginx.conf +COPY container/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 8080 diff --git a/container/default.conf b/container/default.conf new file mode 100644 index 0000000000..611035a2b3 --- /dev/null +++ b/container/default.conf @@ -0,0 +1,28 @@ +server { + listen 8080; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + absolute_redirect off; + + error_page 404 /404.html; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://github.com https://release-assets.githubusercontent.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" always; + add_header Strict-Transport-Security "max-age=31536000" always; + + location /.well-known/security.txt { + return 302 https://www.ncsc.nl/.well-known/security.txt; + } + + location / { + if (-d $request_filename) { + rewrite ^(.+[^/])$ $1/ permanent; + } + try_files $uri $uri/ =404; + } +} diff --git a/container/nginx.conf b/container/nginx.conf new file mode 100644 index 0000000000..136a9ddfde --- /dev/null +++ b/container/nginx.conf @@ -0,0 +1,26 @@ +worker_processes 1; + +error_log /dev/stderr warn; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log off; + server_tokens off; + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js index 4d0a8ab55f..06dafdbabd 100644 --- a/docs/javascripts/extra.js +++ b/docs/javascripts/extra.js @@ -20,7 +20,7 @@ const focusedElement = document.activeElement; const isMainContent = focusedElement && focusedElement.id === 'main-content'; const isSkipLink = focusedElement && focusedElement.classList.contains('skip-link'); - + if (!isMainContent && !isSkipLink) { // Reset focus to document body to ensure skiplink is first tab stop document.body.setAttribute('tabindex', '-1'); @@ -123,34 +123,34 @@ // Function to setup custom tooltips function setupCustomTooltip(element) { let originalTitle = element.getAttribute('title'); - + // Store original title in data attribute for CSS to use element.setAttribute('data-tooltip', originalTitle); - + // Remove title immediately to prevent native tooltip element.removeAttribute('title'); } - // Function to fix search result semantic structure + // Function to fix search result semantic structure // Converts h1 tags in search results to h2 for proper semantic hierarchy function fixSearchResultSemantics() { // Find all search result h1 tags within the search output area const searchResults = document.querySelectorAll('.md-search-result h1, .md-search-result__title'); - + searchResults.forEach(function(h1Element) { // Only convert h1 tags, not other elements with md-search-result__title class if (h1Element.tagName.toLowerCase() === 'h1') { // Create a new h2 element const h2Element = document.createElement('h2'); - + // Copy all attributes from h1 to h2 Array.from(h1Element.attributes).forEach(function(attr) { h2Element.setAttribute(attr.name, attr.value); }); - + // Copy all content from h1 to h2 h2Element.innerHTML = h1Element.innerHTML; - + // Replace h1 with h2 in the DOM h1Element.parentNode.replaceChild(h2Element, h1Element); } @@ -166,7 +166,7 @@ const parent = mark.parentNode; const textNode = document.createTextNode(mark.textContent); parent.replaceChild(textNode, mark); - + // Normalize the parent to merge adjacent text nodes parent.normalize(); }); diff --git a/docs/javascripts/modal.js b/docs/javascripts/modal.js index e35d8b2d6c..2cfd04ff07 100644 --- a/docs/javascripts/modal.js +++ b/docs/javascripts/modal.js @@ -67,20 +67,13 @@ function onDynamicContentLoaded(targetDiv, callback) { } function getBasePath() { - const path = window.location.pathname; - const isLocal = window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost"; - const isPRPreview = path.includes('/pr-preview/'); - - if (isLocal) { - return '/Algoritmekader'; - } else if (isPRPreview) { - // Extract everything up to and including the PR number - const prMatch = path.match(/(\/Algoritmekader\/pr-preview\/pr-\d+)/); - return prMatch ? prMatch[1] : '/Algoritmekader'; - } else { - // Production - return '/Algoritmekader'; + // Use the base element that MkDocs generates, which respects site_url + const baseEl = document.querySelector('base'); + if (baseEl && baseEl.href) { + return new URL(baseEl.href).pathname.replace(/\/$/, ''); } + // Fallback: root + return ''; } // Enhanced showModal function to support redirect functionality diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index e332aa43a7..58634212e7 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -121,27 +121,27 @@ td { table, thead, tbody, th, td, tr { display: block !important; } - + thead tr { position: absolute !important; top: -9999px !important; left: -9999px !important; } - + tr { border: 1px solid #B9C7D5 !important; margin-bottom: 10px !important; padding: 10px !important; border-radius: 4px; } - + td { border: none !important; position: relative !important; padding-left: 50% !important; min-width: auto !important; } - + td:before { content: attr(data-label) ": " !important; position: absolute !important; @@ -762,7 +762,7 @@ font-size: 0.875rem !important; .responsive-grid-2 { grid-template-columns: 1fr !important; } - + .responsive-grid-3 { grid-template-columns: 1fr !important; } @@ -776,20 +776,20 @@ font-size: 0.875rem !important; /* WCAG AA: Make images scalable for 320px viewport */ @media (max-width: 320px) { - img, + img, .block-image, .md-header__button.md-logo img { max-width: 100% !important; height: auto !important; width: auto !important; } - + /* Ensure grid cards stack properly on very small screens */ .grid.cards { grid-template-columns: 1fr !important; gap: 8px !important; } - + /* Reduce padding for very small screens */ .float-child, .float-child-white { diff --git a/docs/stylesheets/navigation.css b/docs/stylesheets/navigation.css index 2dbe81ab68..b6b9d8f233 100644 --- a/docs/stylesheets/navigation.css +++ b/docs/stylesheets/navigation.css @@ -650,7 +650,7 @@ abbr[data-tooltip]:hover { position: static !important; z-index: auto !important; } - + .md-main { margin-top: 0 !important; } @@ -720,4 +720,3 @@ abbr[data-tooltip]:hover { display: none !important; } } - diff --git a/src/overrides/hooks/lists.py b/src/overrides/hooks/lists.py index 00b30ec282..8f68b59bdb 100644 --- a/src/overrides/hooks/lists.py +++ b/src/overrides/hooks/lists.py @@ -450,14 +450,17 @@ def generate_filters( has_search = filter_options.get("search", True) has_column_filters = len(column_filters_html) > 0 has_ai_act_labels = filter_options.get("ai-act-labels", False) - has_export = should_show_export(current_file) and content_type in ["vereisten", "maatregelen"] + has_export = should_show_export(current_file) and content_type in [ + "vereisten", + "maatregelen", + ] has_any_filters = has_search or has_column_filters or has_ai_act_labels # Only add wrapper div if there are actually filters to show if has_any_filters: wrapper_class = "filter-wrapper" filters.append( - f'
' + f'
' ) filters.append('
') @@ -487,7 +490,9 @@ def generate_filters( # AI-act labels info as separate div below the filter container if has_ai_act_labels: - filters.append("
") + filters.append( + "
" + ) filters.append( "" ) diff --git a/src/overrides/main.html b/src/overrides/main.html index 03af978442..2cf7da16d5 100644 --- a/src/overrides/main.html +++ b/src/overrides/main.html @@ -18,4 +18,4 @@ {% block content %}{{ super() }}{% endblock %}
-{% endblock %} \ No newline at end of file +{% endblock %}