Compare commits
61 Commits
v1.3.14-rc
...
f4aad36ea6
| Author | SHA1 | Date | |
|---|---|---|---|
| f4aad36ea6 | |||
| 88edf08993 | |||
| e0cdac971f | |||
| cc728410c2 | |||
| 59e1ae7926 | |||
| a36a7fe19d | |||
| 13cbc0291f | |||
| 1315985ba2 | |||
| 04309e3c02 | |||
| 8e2a6a0378 | |||
| 5a48dcd75f | |||
| 73cb55ee8d | |||
| 5ffd78d60f | |||
| b0b8902f30 | |||
| 0dcb64ddca | |||
| 6bd4914586 | |||
| 4de69aa7ed | |||
| e217d9c7f7 | |||
| f97b37d3c8 | |||
| d4705d616b | |||
| d336980f27 | |||
| f663497277 | |||
| 3dee7c73f1 | |||
| ac4677ca50 | |||
| ce492c221c | |||
| fbd405c270 | |||
| acfee13df7 | |||
| a5d43df53a | |||
| adda98eb00 | |||
| 3523547eca | |||
| 2ddef98924 | |||
| 272d3eccbf | |||
| 24475adef3 | |||
| 473079181f | |||
| 67e86b0fa3 | |||
| 1c441b2d99 | |||
| e7446a5444 | |||
| d50daa8db9 | |||
| b69986572e | |||
| 18e02e3a33 | |||
| f726b3e978 | |||
| 832be8c6bf | |||
| 2b934dd70e | |||
| 33e84fb538 | |||
| 52f4f30336 | |||
| e8b8a4073e | |||
| 5821a5dee4 | |||
| dfc7536268 | |||
| 9f26a179fa | |||
| 5043058660 | |||
| a37091ad71 | |||
| 9539aa35eb | |||
| 3ad24ef615 | |||
| 3018ae5412 | |||
| fd3f4c82c5 | |||
| 3906838dc1 | |||
| 785e36af08 | |||
| d4c32476d6 | |||
| 9b9d4634dd | |||
| c9e48cd5a4 | |||
| bb5828cbfc |
@@ -25,7 +25,9 @@ jobs:
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
pnpm lint &
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Infrastructure Maintenance: Production stabilization and email adapter fix triggered on 2026-04-12.
|
||||
# Infrastructure Maintenance: Production stabilization and runner cleanup triggered on 2026-04-23.
|
||||
name: 🚀 Build & Deploy
|
||||
|
||||
on:
|
||||
@@ -140,6 +140,12 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🧹 Clean Runner Infrastructure
|
||||
run: |
|
||||
docker builder prune -f --filter "until=24h"
|
||||
docker image prune -f --filter "until=24h"
|
||||
docker system prune -f --volumes --filter "until=24h" || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -156,6 +162,7 @@ jobs:
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
@@ -185,6 +192,11 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: 🧹 Pre-Build Cache Cleanup
|
||||
run: |
|
||||
docker builder prune -a -f --filter "until=12h"
|
||||
docker image prune -a -f --filter "until=12h"
|
||||
docker system prune -f --volumes || true
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
- name: 🏗️ Build and Push
|
||||
|
||||
@@ -1,17 +1,258 @@
|
||||
name: Nightly QA
|
||||
name: Nightly QA (Inlined)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
- cron: '0 2 * * *' # Every night at 2:00 AM
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.gitea/workflows/qa.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call-qa-workflow:
|
||||
uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main
|
||||
with:
|
||||
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
||||
PROJECT_NAME: 'mb-grid-solutions'
|
||||
secrets:
|
||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
||||
prepare:
|
||||
name: 🏗️ Prepare & Install
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile --reporter=append-only
|
||||
|
||||
static:
|
||||
name: 🔍 Static Analysis
|
||||
needs: prepare
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
- name: 🛠️ Fix @mintel/cli Syntax Error
|
||||
run: |
|
||||
CLI_PATH=$(find node_modules -name index.js | grep "@mintel/cli/dist/index.js" | head -n 1)
|
||||
if [ -n "$CLI_PATH" ] && [ -f "$CLI_PATH" ]; then
|
||||
echo "Patching CLI at $CLI_PATH"
|
||||
sed -i '2{/^#!\/usr\/bin\/env node/d;}' "$CLI_PATH"
|
||||
fi
|
||||
- name: 🌐 HTML Validation
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com'
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||
run: pnpm run check:html
|
||||
- name: 🔍 Install Browser Libs
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||
- name: 🕷️ Install Puppeteer Chrome
|
||||
run: pnpm dlx puppeteer browsers install chrome
|
||||
- name: 🖼️ Asset Scan
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com'
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||
run: pnpm run check:assets
|
||||
|
||||
accessibility:
|
||||
name: ♿ Accessibility
|
||||
needs: prepare
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
- name: 🌐 Install Chromium
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y software-properties-common gnupg wget ca-certificates
|
||||
sudo add-apt-repository -y ppa:xtradeb/apps
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium
|
||||
sudo ln -sf /usr/bin/chromium-browser /usr/bin/google-chrome || sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
- name: ♿ WCAG Scan
|
||||
continue-on-error: true
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: 'https://testing.mb-grid-solutions.com'
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||
run: pnpm run check:wcag
|
||||
|
||||
analysis:
|
||||
name: 🧪 Maintenance & Links
|
||||
needs: prepare
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
- name: 📦 Depcheck
|
||||
run: |
|
||||
pnpm dlx depcheck . --ignores="framer-motion,lucide-react,next-intl,vitest,@testing-library/jest-dom,@testing-library/react,@vitejs/plugin-react,autoprefixer,axios,cheerio,eslint-config-next,happy-dom,html-validate,husky,jsdom,lint-staged,pa11y-ci,pino-pretty,postcss,prettier,puppeteer,tailwindcss,tsx,typescript,@mintel/eslint-config,@mintel/husky-config,@mintel/tsconfig,@tailwindcss/postcss,@types/node,@types/nodemailer,@types/react,@types/react-dom"
|
||||
- name: 🔗 Lychee Link Check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: --accept 200,204,429 --timeout 15 app/ public/
|
||||
fail: true
|
||||
failIfEmpty: false
|
||||
|
||||
performance:
|
||||
name: 🎭 Lighthouse
|
||||
needs: prepare
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
- name: 🌐 Install Chromium
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y software-properties-common gnupg wget ca-certificates
|
||||
sudo add-apt-repository -y ppa:xtradeb/apps
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium
|
||||
sudo ln -sf /usr/bin/chromium-browser /usr/bin/google-chrome || sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
- name: 🛠️ Fix @mintel/cli Syntax Error
|
||||
run: |
|
||||
CLI_PATH=$(find node_modules -name index.js | grep "@mintel/cli/dist/index.js" | head -n 1)
|
||||
if [ -n "$CLI_PATH" ] && [ -f "$CLI_PATH" ]; then
|
||||
echo "Patching CLI at $CLI_PATH"
|
||||
sed -i '2{/^#!\/usr\/bin\/env node/d;}' "$CLI_PATH"
|
||||
fi
|
||||
- name: 🎭 LHCI Desktop
|
||||
env:
|
||||
LHCI_URL: 'https://testing.mb-grid-solutions.com'
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=desktop
|
||||
- name: 📱 LHCI Mobile
|
||||
env:
|
||||
LHCI_URL: 'https://testing.mb-grid-solutions.com'
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD || 'lassmichrein' }}
|
||||
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=mobile
|
||||
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
needs: [prepare, static, accessibility, analysis, performance]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
PREPARE="${{ needs.prepare.result }}"
|
||||
STATIC="${{ needs.static.result }}"
|
||||
A11Y="${{ needs.accessibility.result }}"
|
||||
ANALYSIS="${{ needs.analysis.result }}"
|
||||
PERF="${{ needs.performance.result }}"
|
||||
|
||||
PROJECT="mb-grid-solutions"
|
||||
URL="https://testing.mb-grid-solutions.com"
|
||||
|
||||
if [[ "$PREPARE" != "success" || "$STATIC" != "success" || "$PERF" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
STATUS_LINE="Nightly QA Failed! Action required."
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS_LINE="Nightly QA Passed."
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI $PROJECT Nightly QA"
|
||||
MESSAGE="$STATUS_LINE
|
||||
Prepare: $PREPARE | Static: $STATIC | A11y: $A11Y
|
||||
Analysis: $ANALYSIS | Perf: $PERF
|
||||
$URL"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=$MESSAGE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
6
.htmlvalidate-tmp/_.html
Normal file
6
.htmlvalidate-tmp/_.html
Normal file
File diff suppressed because one or more lines are too long
1
.htmlvalidate-tmp/_agb.html
Normal file
1
.htmlvalidate-tmp/_agb.html
Normal file
File diff suppressed because one or more lines are too long
1
.htmlvalidate-tmp/_datenschutz.html
Normal file
1
.htmlvalidate-tmp/_datenschutz.html
Normal file
File diff suppressed because one or more lines are too long
2
.htmlvalidate-tmp/_impressum.html
Normal file
2
.htmlvalidate-tmp/_impressum.html
Normal file
File diff suppressed because one or more lines are too long
3
.htmlvalidate-tmp/_kontakt.html
Normal file
3
.htmlvalidate-tmp/_kontakt.html
Normal file
File diff suppressed because one or more lines are too long
5
.htmlvalidate-tmp/_ueber_uns.html
Normal file
5
.htmlvalidate-tmp/_ueber_uns.html
Normal file
File diff suppressed because one or more lines are too long
@@ -2,65 +2,51 @@ import { Download } from "lucide-react";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default function AGB() {
|
||||
export default function AVB() {
|
||||
const filePath = path.join(process.cwd(), "context/avbs.md");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs)
|
||||
const blocks = fileContent
|
||||
// Split by double newlines to get major blocks
|
||||
const rawBlocks = fileContent
|
||||
.split(/\n\s*\n/)
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b !== "");
|
||||
|
||||
const title = blocks[0] || "Liefer- und Zahlungsbedingungen";
|
||||
const stand = blocks[1] || "Stand Januar 2026";
|
||||
// Extract title and stand more robustly
|
||||
const title =
|
||||
rawBlocks.find((b) => b.startsWith("# "))?.replace(/^#\s+/, "") ||
|
||||
"Allgemeine Verkaufsbedingungen";
|
||||
const stand =
|
||||
rawBlocks.find((b) => b.toLowerCase().startsWith("stand:")) ||
|
||||
"Stand: April 2026";
|
||||
|
||||
const sections: { title: string; content: string[] }[] = [];
|
||||
let currentSection: { title: string; content: string[] } | null = null;
|
||||
|
||||
// Skip title and stand
|
||||
blocks.slice(2).forEach((block) => {
|
||||
const lines = block
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l !== "");
|
||||
if (lines.length === 0) return;
|
||||
// Process sections, skipping title and stand blocks
|
||||
rawBlocks.forEach((block) => {
|
||||
if (block.startsWith("# ") || block.toLowerCase().startsWith("stand:"))
|
||||
return;
|
||||
|
||||
const firstLine = lines[0];
|
||||
|
||||
if (/^\d+\./.test(firstLine)) {
|
||||
// New section
|
||||
if (block.startsWith("##")) {
|
||||
// New section header: e.g. "## 1. Geltungsbereich"
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
currentSection = { title: firstLine, content: [] };
|
||||
|
||||
// If there are more lines in this block, they form the first paragraph(s)
|
||||
if (lines.length > 1) {
|
||||
// Join subsequent lines as they might be part of the same paragraph
|
||||
// In this MD, we'll assume lines in the same block belong together
|
||||
// unless they are clearly separate paragraphs (but we already split by double newline)
|
||||
const remainingText = lines.slice(1).join(" ");
|
||||
if (remainingText) currentSection.content.push(remainingText);
|
||||
}
|
||||
currentSection = {
|
||||
title: block.replace(/^##\s+/, "").trim(),
|
||||
content: [],
|
||||
};
|
||||
} else if (currentSection) {
|
||||
// Continuation of current section
|
||||
const blockText = lines.join(" ");
|
||||
if (blockText) currentSection.content.push(blockText);
|
||||
// Clean up bold markers for better display
|
||||
const cleanedBlock = block.replace(/\*\*(.*?)\*\*/g, "$1");
|
||||
currentSection.content.push(cleanedBlock);
|
||||
}
|
||||
});
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
// The last block is the footer
|
||||
const footer = blocks[blocks.length - 1];
|
||||
if (sections.length > 0) {
|
||||
const lastSection = sections[sections.length - 1];
|
||||
if (lastSection.content.includes(footer) || lastSection.title === footer) {
|
||||
lastSection.content = lastSection.content.filter((c) => c !== footer);
|
||||
if (sections[sections.length - 1].title === footer) {
|
||||
sections.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
// The very last block might be a footer/schlussbestimmung if it's not in a section
|
||||
// In our MD, everything is in a section, so we just use the last block of the last section as potential footer if we want,
|
||||
// but the current UI expects a separate 'footer' variable.
|
||||
const footer = "MB Grid Solutions & Services";
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
|
||||
@@ -74,7 +60,7 @@ export default function AGB() {
|
||||
<p className="text-slate-500 font-medium">{stand}</p>
|
||||
</div>
|
||||
<a
|
||||
href="/assets/AVB MB Grid 4-2026.pdf"
|
||||
href="/assets/avb-mb-grid-4-2026.pdf"
|
||||
download
|
||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||
>
|
||||
@@ -114,8 +100,9 @@ export default function AGB() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-8 border-t border-slate-100">
|
||||
<p className="font-bold text-primary">{footer}</p>
|
||||
<div className="pt-8 border-t border-slate-100 flex justify-between items-center text-slate-400 text-sm italic">
|
||||
<p>{footer}</p>
|
||||
<p>{stand}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,14 +102,15 @@ export default function About() {
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="card-modern !p-6 hover:-translate-y-1 transition-[box-shadow,transform] duration-300 relative overflow-hidden tech-card-border">
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<h3 className="text-xl font-bold text-primary">
|
||||
<h2 className="text-xl font-bold text-primary">
|
||||
{person.name}
|
||||
</h3>
|
||||
</h2>
|
||||
<a
|
||||
href={person.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#0077b5] hover:scale-110 transition-transform"
|
||||
aria-label={`LinkedIn Profile of ${person.name}`}
|
||||
>
|
||||
<Linkedin size={20} />
|
||||
</a>
|
||||
@@ -154,9 +155,9 @@ export default function About() {
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white mb-4">
|
||||
<h3 className="text-xl font-bold text-white mb-4">
|
||||
{i + 1}. {item.title}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-slate-400 leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
|
||||
@@ -125,9 +125,9 @@ export default function Contact() {
|
||||
<Mail size={24} />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
{t("info.email")}
|
||||
</h4>
|
||||
</h2>
|
||||
<a
|
||||
href="mailto:info@mb-grid-solutions.com"
|
||||
className="text-white text-lg md:text-xl font-bold hover:text-accent transition-colors break-all"
|
||||
@@ -144,9 +144,9 @@ export default function Contact() {
|
||||
<MapPin size={24} />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
{t("info.address")}
|
||||
</h4>
|
||||
</h2>
|
||||
<p className="text-white text-lg md:text-xl font-bold leading-relaxed">
|
||||
{t("info.company")}
|
||||
<br />
|
||||
@@ -162,12 +162,8 @@ export default function Contact() {
|
||||
<div className="w-full h-[300px] rounded-[2.5rem] overflow-hidden border border-white/10 shadow-sm grayscale hover:grayscale-0 transition-all duration-700 relative group">
|
||||
<div className="absolute inset-0 border-2 border-accent/0 group-hover:border-accent/20 transition-all duration-500 z-10 pointer-events-none rounded-[2.5rem]" />
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
marginHeight={0}
|
||||
marginWidth={0}
|
||||
className="w-full h-full border-0"
|
||||
title={t("info.mapTitle") || "Location Map"}
|
||||
src="https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552"
|
||||
></iframe>
|
||||
</div>
|
||||
@@ -184,9 +180,9 @@ export default function Contact() {
|
||||
<div className="w-20 h-20 rounded-full bg-accent/10 text-accent flex items-center justify-center mx-auto mb-8">
|
||||
<CheckCircle size={40} />
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-primary mb-4">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">
|
||||
{t("form.successTitle")}
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10">
|
||||
{t("form.successMessage")}
|
||||
</p>
|
||||
|
||||
@@ -184,7 +184,11 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
||||
<div className="lg:col-span-2">
|
||||
<Link href="/" className="inline-block mb-6 md:mb-8 group">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-block mb-6 md:mb-8 group"
|
||||
aria-label={t("nav.home")}
|
||||
>
|
||||
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
@@ -203,9 +207,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.navigation")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
@@ -226,9 +230,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.legal")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/impressum"
|
||||
@@ -246,7 +250,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
href="/agb"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("footer.agb")}
|
||||
{t("footer.avb")}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -75,9 +75,9 @@ export const StatusModal = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
||||
<h2 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
||||
{title}
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
@@ -59,9 +59,9 @@ export const TechnicalSpecsSection = () => {
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="p-10 rounded-3xl bg-white/5 border border-white/10 backdrop-blur-sm hover:bg-white/10 transition-colors h-full relative group overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||
<h4 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
||||
<h3 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
||||
{item.label}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-white mb-4 leading-tight">
|
||||
{item.value}
|
||||
</p>
|
||||
|
||||
591
format_avb.py
591
format_avb.py
@@ -1,591 +0,0 @@
|
||||
import re
|
||||
|
||||
text = """Allgemeine Verkaufsbedingungen
|
||||
1 Geltungsbereich
|
||||
1.1 Ausschließlich die nachfolgenden Allgemeinen Verkaufsbedingungen (nachfolgend „AVB“)
|
||||
gelten für den gesamten Geschäftsverkehr (nachfolgend „Lieferung von Waren“) der KLZ
|
||||
Vertriebs GmbH, Raiffeisenstraße 22, 73630 Remshalden (nachfolgend „KLZ“) und
|
||||
Unternehmern in Ausübung ihrer gewerblichen oder selbständigen beruflichen Tätigkeit und
|
||||
juristischen Personen des öffentlichen Rechts (nachfolgend „Kunde“). Der Geltung von
|
||||
abweichenden, zusätzlichen oder entgegenstehenden allgemeinen Geschäftsbedingungen
|
||||
des Kunden wird hiermit widersprochen.
|
||||
1.2 Diese AVB gelten auch dann, wenn KLZ die Lieferung von Waren an den Kunden vorbehaltlos
|
||||
in Kenntnis entgegenstehender oder von diesen AVB abweichenden allgemeinen
|
||||
Geschäftsbedingungen des Kunden ausführt.
|
||||
1.3 Im Rahmen laufender Geschäftsbeziehungen gelten diese AVB auch dann für künftige
|
||||
Lieferungen von Waren, wenn sie nicht ausdrücklich erneut in den Vertrag einbezogen
|
||||
wurden.
|
||||
1.4 Änderungen dieser AVB werden dem Kunden schriftlich, per Telefax oder per E-Mail mitgeteilt.
|
||||
Widerspricht der Kunde diesen Änderungen nicht innerhalb von vier (4) Wochen nach Zugang
|
||||
der Mitteilung, gelten die Änderungen als durch den Kunden anerkannt. Auf das
|
||||
Widerspruchsrecht und die Rechtsfolgen des Schweigens wird der Kunde im Rahmen der
|
||||
Mitteilung über die Änderung der Geschäftsbedingungen gesondert hingewiesen.
|
||||
1.5 Hinweise auf die Geltung gesetzlicher Vorschriften im Rahmen dieser AVB haben nur
|
||||
klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen
|
||||
Vorschriften, soweit sie in diesen AVB nicht unmittelbar abgeändert oder ausdrücklich
|
||||
ausgeschlossen werden.
|
||||
2 Vertragsschluss
|
||||
2.1 Der Vertrag über die Lieferung von Waren kommt grundsätzlich durch ein Angebot des
|
||||
Kunden, z.B. über ein Auftragsformular, per E-Mail oder per Brief, und eine Annahme dieses
|
||||
Angebots durch KLZ in Textform z.B. durch eine Auftragsbestätigung per E-Mail oder Fax
|
||||
zustande. Der Annahme des Angebots durch KLZ steht es gleich, wenn KLZ den Auftrag oder
|
||||
die Bestellung binnen vierzehn (14) Kalendertagen ab Zugang des Angebots bei KLZ
|
||||
vorbehaltlos ausführt.
|
||||
2.2 Alle Angebote von KLZ sind – insbesondere hinsichtlich Verfügbarkeit, angegebener Mengen,
|
||||
Lieferfristen und Nebenleistungen – freibleibend und unverbindlich, sofern sie nicht
|
||||
ausdrücklich in Textform als verbindlich gekennzeichnet wurden. Die Angebote stehen unter
|
||||
dem Vorbehalt der zeitgerechten, qualitativen und quantitativen Selbstbelieferung von KLZ.
|
||||
2.3 Der Umfang der von KLZ zu liefernden Waren ergibt sich aus dem jeweiligen Angebot oder
|
||||
der jeweiligen Auftragsbestätigung von KLZ.
|
||||
2.4 Technische Angaben, Muster, Preise, Abbildungen, Zeichnungen, Gewichts-, Maß-
|
||||
,
|
||||
Leistungs- und Gebrauchsangaben, sowie sonstige Beschreibungen der zu liefernden Ware
|
||||
in dem Angebot oder der Auftragsbestätigung sind nur annähernd maßgebend, soweit sie nicht
|
||||
1
|
||||
ausdrücklich von KLZ in Textform als verbindlich gekennzeichnet wurden. Sie stellen keine
|
||||
Beschaffenheitsvereinbarung oder Garantie einer entsprechenden Beschaffenheit oder
|
||||
Haltbarkeit der zu liefernden Ware dar, es sei denn, sie wurden ausdrücklich in Textform als
|
||||
solche vereinbart. Entsprechendes gilt für Erwartungen des Kunden hinsichtlich der zu
|
||||
liefernden Ware oder deren Verwendung.
|
||||
2.5 Handelsübliche und branchenübliche Abweichungen vom Leistungsumfang der zu liefernden
|
||||
Ware durch KLZ sind zulässig, soweit sie die Verwendbarkeit der zu liefernden Ware zum
|
||||
vertraglich vorgesehenen Zweck nicht beeinträchtigen. Dies umfasst auch Über- oder
|
||||
Unterlieferungen. Abweichungen der zu liefernden Ware, die durch zwingende rechtliche oder
|
||||
technische Normen, die nach Auftragsbestätigung in Kraft treten, bedingt sind, sind zulässig,
|
||||
soweit sie nicht erheblich und dem Kunden zumutbar sind. Änderungen, die zu einer
|
||||
technischen Verbesserung der zu liefernden Ware führen, sind stets zulässig.
|
||||
2.6 Änderungen des Lieferumfangs durch den Kunden bedürfen zu ihrer Wirksamkeit stets der in
|
||||
Textform erklärten Einwilligung von KLZ. Mehraufwände und sonstige Kosten, die durch die
|
||||
Änderung des Lieferumfangs auf Wunsch des Kunden entstehen, sind von dem Kunden
|
||||
gesondert zu vergüten.
|
||||
2.7 Soweit das Angebot oder die Auftragsbestätigung von KLZ offensichtliche Irrtümer, Schreib-
|
||||
oder Rechenfehler enthält, ist das Dokument für KLZ hinsichtlich der offensichtlichen Fehler
|
||||
nicht verbindlich. Der Kunde wird KLZ auf entsprechende Fehler hinweisen.
|
||||
2.8 Verschlechtern sich die Vermögensverhältnisse des Kunden wesentlich oder wird der Antrag
|
||||
zur Eröffnung eines Insolvenz- oder vergleichbaren Verfahrens über das Vermögen des
|
||||
Kunden mangels Masse abgelehnt, ist KLZ berechtigt, ganz oder teilweise vom Vertrag
|
||||
zurückzutreten.
|
||||
3 Abrufaufträge
|
||||
3.1 Erteilt der Kunde KLZ einen Abrufauftrag und werden über die Abruftermine keine gesonderten
|
||||
Vereinbarungen in Textform getroffen, ist der Kunde verpflichtet, KLZ die einzelnen
|
||||
Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei KLZ und
|
||||
Auslieferung der Ware mindestens vierzehn (14) Werktage und zwischen Eingang der
|
||||
Abrufmitteilung und der letzten Auslieferung der Ware maximal neunzig (90) Werktage liegen.
|
||||
Werktage sind dabei alle Kalendertage, bei denen es sich nicht um einen Sonntag oder einen
|
||||
gesetzlichen Feiertag am Sitz von KLZ handelt.
|
||||
4 Preise und Zahlungsbedingungen
|
||||
4.1 Die in dem Angebot oder der Auftragsbestätigung von KLZ angegebenen Preise gelten für den
|
||||
in dem Angebot oder der Auftragsbestätigung von KLZ aufgeführten Lieferumfang und
|
||||
verstehenden sich grundsätzlich in EURO und ohne Abzüge zuzüglich Versandkosten ab dem
|
||||
jeweiligen Werk des Herstellers der Ware, exklusive Verpackung und zuzüglich der jeweils
|
||||
geltenden gesetzlichen Umsatzsteuer, sofern nicht ausdrücklich etwas anderes in Textform
|
||||
vereinbart wurde. Die in dem Angebot oder der Auftragsbestätigung von KLZ ausgewiesenen
|
||||
Hohlpreise verstehen sich darüber hinaus zuzüglich Metallzuschlag und auftragsspezifischer
|
||||
Schnittkosten.
|
||||
2
|
||||
4.2 Sollte sich nach Vertragsschluss herausstellen, dass zur Durchführung des Vertrages bzw. zur
|
||||
Lieferung der Waren Mehrleistungen erforderlich sind, so werden diese Mehrleistungen
|
||||
gesondert abgerechnet und vergütet.
|
||||
4.3 Hat sich der Preis für eine zu liefernde Ware, bzw. der vereinbarte Hohlpreis z.B. aufgrund
|
||||
gestiegener Preise für Energie, Transport, Arbeitskraft oder Material zwischen Vertragsschluss
|
||||
und Lieferdatum um mehr als fünf (5) % erhöht, werden die Parteien partnerschaftlich und
|
||||
konstruktiv über eine Anpassung des Preises bzw. des Hohlpreises diskutieren. KLZ ist zum
|
||||
Rücktritt von dem betroffenen Vertrag berechtigt, wenn innerhalb von
|
||||
vierzehn (14) Kalendertagen ab Anzeige der Preiserhöhung durch KLZ keine einvernehmliche
|
||||
Lösung gefunden wird.
|
||||
4.4 Der Kaufpreis wird sofort mit Erhalt der Rechnung oder Lieferung der Ware, je nachdem
|
||||
welcher Zeitpunkt früher eintritt, zur Zahlung fällig, sofern keine anderen Zahlungskonditionen
|
||||
in Textform vereinbart sind. Der Kunde hat den ausstehenden Kaufpreis innerhalb von
|
||||
zehn (10) Kalendertagen nach Erhalt der Rechnung oder Erhalt der Ware, je nachdem welcher
|
||||
Zeitpunkt früher eintritt, frei Konto von KLZ und ohne Abzug zu bezahlen. Die Zahlungsfrist gilt
|
||||
mit vollständiger Gutschrift des ausstehenden Kaufpreises auf dem Konto von KLZ als
|
||||
gewahrt. Nach Ablauf der Zahlungsfrist kommt der Kunde automatisch mit der Zahlung des
|
||||
ausstehenden Kaufpreises in Verzug, ohne dass es hierfür einer gesonderten Mahnung von
|
||||
KLZ bedarf.
|
||||
4.5 Befindet sich der Kunde mit einer Zahlung in Verzug, ist KLZ berechtigt, die zu liefernde Ware
|
||||
nur Zug-um-Zug gegen Zahlung der Summe, mit der sich der Kunde in Verzug befindet, zu
|
||||
liefern. Dies gilt auch für alle künftigen Lieferungen von Waren von KLZ an den Kunden.
|
||||
Darüber hinaus stehen KLZ sämtliche Ansprüche gegen den Kunden aufgrund dessen
|
||||
Zahlungsverzug uneingeschränkt zu.
|
||||
4.6 Befindet sich der Kunde seit mehr als vierzehn (14) Kalendertagen in Zahlungsverzug, ist KLZ
|
||||
dazu berechtigt, vom Vertrag zurückzutreten und die gelieferte Ware unverzüglich zurück zu
|
||||
verlangen, ohne dass dem Kunden hieraus Ansprüche entstehen.
|
||||
4.7 Das Recht des Kunden, Zahlungen zurückzuhalten oder mit Gegenansprüchen aufzurechnen,
|
||||
steht dem Kunden nur insoweit zu, als seine Gegenansprüche unbestritten oder rechtskräftig
|
||||
festgestellt sind, oder wenn die jeweiligen Ansprüche in einem Gegenseitigkeitsverhältnis
|
||||
zueinanderstehen.
|
||||
5 Metallnotierung
|
||||
5.1 Basis zur Berechnung des Kupferpreises ist der Durchschnitt des Liefervormonats der
|
||||
Notierung „LME Copper official price cash offer“, zuzüglich der jeweils aktuellen von KLZ
|
||||
benannten Kupfer-Prämie.
|
||||
5.2 Basis zur Berechnung des Aluminiumpreises ist der Durchschnitt des Liefervormonats der
|
||||
Notierung „LME Aluminium official price cash offer" zuzüglich der jeweils von KLZ benannten
|
||||
Aluminium-Prämie.
|
||||
5.3 USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die
|
||||
entsprechenden Notierungen können der Webseite www.westmetall.com entnommen werden.
|
||||
Die Prämienzuschläge können stark variieren und KLZ behält sich das Recht vor, diese
|
||||
fristgerecht anzupassen, ungeachtet der Angebotslegung.
|
||||
3
|
||||
6 Metallzahl
|
||||
6.1 Die von KLZ in dem Angebot oder der Auftragsbestätigung ausgewiesene Metallzahl ist eine
|
||||
rein kaufmännische Berechnungsgröße für den Metallinhalt, der in die Berechnung des
|
||||
Gesamtpreises eines Kabels eingeht.
|
||||
6.2 Die Angabe der Metallzahl dient ausschließlich dazu, dem Kunden eine Vergleichbarkeit auf
|
||||
Grundlage der Holpreisbasis zu ermöglichen. Die Metallzahl definiert nicht das Gewicht des
|
||||
tatsächlich im Kabel enthaltenen Leitermetalls. Die Metallzahl ist ein rein kalkulatorischer
|
||||
Berechnungsfaktor, der keine unmittelbaren Rückschlüsse auf die im Kabel verwendeten
|
||||
Kupfer- bzw. Aluminiummengen zulässt. KLZ weist den Kunden hiermit ausdrücklich darauf
|
||||
hin, final nur den Vollpreis für Vergleichszwecke heranzuziehen.
|
||||
6.3 Auf Anfrage des Kunden in Textform ist KLZ grundsätzlich dazu bereit, andere Metallzahlen
|
||||
zu berechnen und auszuweisen. Bei jeglicher Änderung bleibt der Vollpreis der gleiche Betrag.
|
||||
Ein Anspruch des Kunden auf Berechnung und Angabe anderer Metallzahlen besteht nicht.
|
||||
7 Lieferung
|
||||
7.1 Lieferungen erfolgen grundsätzlich DAP (INCOTERMS® 2020) sofern die Parteien keine
|
||||
anderweitige Vereinbarung in Textform getroffen haben.
|
||||
7.2 Die Lieferung der Ware wird nach Möglichkeit geschlossen vorgenommen. KLZ ist jedoch
|
||||
berechtigt, Teillieferungen vorzunehmen, wenn dies für den Kunden zumutbar ist,
|
||||
insbesondere wenn wegen Fehlens von Waren eine erhebliche Verzögerung der gesamten
|
||||
Lieferung eintreten würde.
|
||||
7.3 Lieferfristen aus Angeboten und Auftragsbestätigungen sind grundsätzlich für KLZ
|
||||
unverbindlich. Eine verbindliche Lieferfrist muss ausdrücklich in Textform als solche
|
||||
gekennzeichnet werden.
|
||||
7.4 Die Lieferfrist beginnt mit dem Zugang der Auftragsbestätigung bei dem Kunden. Die
|
||||
Einhaltung der Lieferfrist setzt die rechtzeitige Klärung aller kaufmännischen und technischen
|
||||
Fragen, sowie die Einhaltung der vereinbarten Zahlungsbedingungen und sonstigen
|
||||
Verpflichtungen des Kunden voraus. Werden diese Voraussetzungen nicht rechtzeitig erfüllt,
|
||||
so verlängern sich die Fristen angemessen; dies gilt nicht, wenn ausschließlich KLZ die
|
||||
Verzögerung zu vertreten hat.
|
||||
7.5 Die Einhaltung auch einer verbindlichen Lieferfrist steht unter dem Vorbehalt
|
||||
ordnungsgemäßer, insbesondere rechtzeitiger quantitativer und qualitativer Selbstbelieferung
|
||||
von KLZ durch dessen Vorlieferanten. KLZ ist im Fall der nicht ordnungsgemäßen
|
||||
Selbstbelieferung zum Rücktritt vom Vertrag berechtigt. KLZ informiert den Kunden
|
||||
unverzüglich, wenn und soweit KLZ von seinem Recht zum Rücktritt vom Vertrag Gebrauch
|
||||
macht und gewährt etwa erbrachte Vorleistungen des Kunden zurück.
|
||||
7.6 Die Lieferfrist gilt als gewahrt, wenn KLZ die zu liefernde Ware dem Kunden bis zum Ablauf
|
||||
der Lieferfrist an dem benannten Lieferort innerhalb der üblichen Geschäftszeiten zur
|
||||
Annahme erstmalig angeboten hat. .
|
||||
7.7 Auch wenn eine verbindliche Lieferfrist vereinbart wurde, kommt KLZ mit der Lieferung der
|
||||
Ware nur dann in Verzug, wenn der Kunde KLZ nach Ablauf der Lieferfrist eine angemessene
|
||||
4
|
||||
Nachfrist zur Lieferung der Ware in Textform gesetzt hat und diese Nachfrist erfolglos
|
||||
verstrichen ist.
|
||||
7.8 Wird die Lieferung der Ware, oder das Abladen der Ware, auf Wunsch des Kunden verzögert,
|
||||
oder verzögert sich die Lieferung der Ware, oder das Abladen der Ware, aus sonstigen
|
||||
Gründen, die der Kunde zu vertreten hat, ist KLZ berechtigt, dem Kunden nach Ablauf der
|
||||
vertraglich vereinbarten Lieferfrist ein Lagergeld in Höhe von zwei (2) % des jeweiligen
|
||||
Nettoauftragswertes für jeden angefangenen Monat, maximal jedoch fünf (5) % des jeweiligen
|
||||
Nettoauftragswertes in Rechnung zu stellen. Das Lagergeld ist der Höhe nach auf maximal
|
||||
fünf (5) % des Nettoauftragswertes pro Vertrag beschränkt.
|
||||
7.9 KLZ behält sich die Geltendmachung aller darüberhinausgehenden Ansprüche gegenüber
|
||||
dem Kunden aufgrund dessen verzögerter Übernahme der Ware uneingeschränkt vor. Das
|
||||
Lagergeld ist auf weitergehende Zahlungsansprüche von KLZ gegenüber dem Kunden
|
||||
aufgrund dessen verzögerter Übernahme der Ware anzurechnen. Dem Kunden bleibt der
|
||||
Nachweis gestattet, dass KLZ überhaupt kein oder nur ein geringerer Schaden entstanden ist.
|
||||
7.10 Kabel werden von KLZ auf stabilen Vollholztrommeln geliefert. Auf Wunsch vermittelt KLZ dem
|
||||
Kunden Partner, die diese Trommeln gegen eine Gebühr abholen. Bei Lagerzeiten über
|
||||
drei (3) Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine
|
||||
einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließt
|
||||
KLZ aus.
|
||||
7.11 Rücksendungen von Waren an KLZ, die nicht vorher von KLZ in Textform bestätigt wurden,
|
||||
erfolgen auf alleinige Gefahr des Kunden.
|
||||
8 Gefahrübergang
|
||||
8.1 Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der zu liefernden
|
||||
Ware geht grundsätzlich DAP (INCOTERMS® 2020) auf den Kunden über, sofern die Parteien
|
||||
keine anderweitige Vereinbarung in Textform getroffen haben.
|
||||
8.2 Verzögert sich die Versendung bzw. Übergabe der Ware aus Gründen, die der Kunde zu
|
||||
vertreten hat, geht die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung
|
||||
der Ware mit der Durchführung des ersten Zustellversuchs innerhalb der gewöhnlichen
|
||||
Öffnungszeiten des Kunden durch KLZ auf den Kunden über.
|
||||
9 Eigentumsvorbehalt
|
||||
9.1 Die jeweils gelieferten Waren bleiben bis zur vollständigen Bezahlung sämtlicher fälliger
|
||||
Forderungen aus der Geschäftsbeziehung zwischen dem Kunden und KLZ in dem Eigentum
|
||||
von KLZ (nachfolgend „Vorbehaltsware“). Dabei ist unerheblich, aus welchem Rechtsgrund
|
||||
die Forderungen von KLZ gegen den Kunden bestehen. Erfasst sind insbesondere auch
|
||||
Saldoforderungen aus laufender Rechnung.
|
||||
9.2 Der Kunde ist berechtigt, die gelieferte Ware im Rahmen des ordnungsgemäßen
|
||||
Geschäftsbetriebes zu veräußern, umzubilden oder weiterzuverarbeiten.
|
||||
9.3 Die Verarbeitung oder Umbildung der Vorbehaltsware durch den Kunden erfolgt jedoch stets
|
||||
für KLZ und KLZ erwirbt Eigentum an der neuen Sache. Wird die Vorbehaltsware mit anderen
|
||||
Sachen zu einer einheitlichen Sache verbunden, verarbeitet oder untrennbar vermischt oder
|
||||
5
|
||||
vermengt oder in sonstiger Weise verbunden, erwirbt KLZ Miteigentum an der neuen Sache
|
||||
im Verhältnis des Wertes der Vorbehaltsware zu den anderen Gegenständen zur Zeit der
|
||||
Verarbeitung bzw. Vermischung, Vermengung oder sonstigen Verbindung. Erfolgt die
|
||||
Vermischung, Vermengung oder sonstige Verbindung dergestalt, dass die Sache des Kunden
|
||||
als Hauptsache anzusehen ist, so willigt der Kunde bereits jetzt dazu ein, KLZ anteiliges
|
||||
Miteigentum an der neuen Sache, in dem Mengenverhältnis der Vorbehaltsware zu den
|
||||
fremden Sachen, zu übertragen. Für die so entstandenen Sachen gelten die Bestimmungen
|
||||
für Vorbehaltsware.
|
||||
9.4 Der Kunde hat die Vorbehaltsware mit kaufmännischer Sorgfalt für KLZ zu verwahren, pfleglich
|
||||
zu behandeln und auf seine Kosten gegen Feuer, Wasser und Diebstahl zum Neuwert zu
|
||||
versichern. Der Kunde tritt seine entsprechenden Ansprüche aus den Versicherungsverträgen
|
||||
an KLZ ab; KLZ nimmt die Abtretung an.
|
||||
9.5 Der Kunde darf die Vorbehaltsware nur im ordnungsgemäßen Geschäftsgang bis zum Eintritt
|
||||
des Verwertungsfalles veräußern. Verpfändungen und Sicherungsübereignungen hinsichtlich
|
||||
der Vorbehaltsware sind unzulässig. Eingriffe Dritter, wie etwa die Pfändung oder
|
||||
Zwangsvollstreckung, durch welche Rechte von KLZ beeinträchtigt werden, hat der Kunde
|
||||
KLZ unverzüglich in Textform anzuzeigen. Der Kunde hat den Dritten darüber hinaus
|
||||
unverzüglich in Textform darauf hinzuweisen, dass KLZ Eigentümer der Vorbehaltsware ist.
|
||||
Soweit der Dritte nicht in der Lage ist, KLZ die gerichtlichen und außergerichtlichen Kosten
|
||||
einer Klage nach § 771 ZPO zu erstatten, haftet der Kunde für den dadurch entstandenen
|
||||
Ausfall. Weitergehende Ansprüche gegen den Kunden bleiben unberührt.
|
||||
9.6 Für den Fall der Weiterveräußerung der Vorbehaltsware tritt der Kunde die ihm daraus
|
||||
seinerseits erwachsenden Forderungen gegen seinen Abnehmer bereits jetzt an KLZ ab; KLZ
|
||||
nimmt die Abtretung an. Gleiches gilt für sonstige Forderungen, die an die Stelle der
|
||||
Vorbehaltsware treten oder sonst hinsichtlich der Vorbehaltsware entstehen, wie z. B.
|
||||
Versicherungsansprüche oder Ansprüche aus unerlaubter Handlung bei Verlust oder
|
||||
Zerstörung.
|
||||
9.7 Wird die Vorbehaltsware nach Verarbeitung oder zusammen mit anderen Waren, die KLZ nicht
|
||||
gehören, weiterveräußert oder wird sie mit einem Grundstück oder mit beweglichen Sachen
|
||||
verbunden, so erklärt der Kunde bereits jetzt die Abtretung der Forderung des Kunden gegen
|
||||
seine Abnehmer in Höhe des zwischen dem Kunden und KLZ vereinbarten Kaufpreises für die
|
||||
Vorbehaltsware; KLZ nimmt die Abtretung an.
|
||||
9.8 Der Kunde wird ermächtigt, die abgetretenen Forderungen für KLZ einzuziehen. KLZ bleibt
|
||||
auch weiterhin zur Einziehung der abgetretenen Forderungen berechtigt, wird die
|
||||
abgetretenen Forderungen jedoch nicht selbst einziehen, solange kein Grund nach Ziffer 9.9
|
||||
vorliegt, der KLZ zum Widerruf der Einziehungsermächtigung berechtigt. Zur Abtretung der
|
||||
Forderung an Dritte, einschließlich Forderungsverkauf an Factoring Banken, ist der Kunde nur
|
||||
mit vorheriger schriftlicher Zustimmung von KLZ berechtigt. KLZ wird die Zustimmung nicht
|
||||
verweigern, sofern eine gleichwertige Sicherheit gestellt wird.
|
||||
9.9 KLZ kann die Ermächtigung zur Weiterveräußerung und Einziehung widerrufen, sobald sich
|
||||
der Kunde gegenüber KLZ im Zahlungsverzug befindet, oder das Insolvenzverfahren oder ein
|
||||
vergleichbares Verfahren über das Vermögen des Kunden eröffnet oder mangels Masse
|
||||
abgewiesen wurde.
|
||||
6
|
||||
9.10 Der Kunde hat die eingezogenen Forderungen binnen zehn (10) Werktagen auf ein von KLZ
|
||||
benanntes Konto zu überweisen.
|
||||
9.11 KLZ ist zur Offenlegung der Abtretung berechtigt und kann von dem Kunden verlangen, dass
|
||||
dieser die Abtretung seinen Abnehmern anzeigt. Auf Verlangen ist der Kunde verpflichtet, KLZ
|
||||
die Namen seiner Abnehmer bekanntzugeben und alle erforderlichen Unterlagen
|
||||
herauszugeben, die für die Einziehung der Forderung durch KLZ selbst erforderlich sind. Auch
|
||||
ist der Kunde ab Verzugseintritt verpflichtet, die Abtretung der Forderung an KLZ seinem
|
||||
Abnehmer gegenüber schriftlich mitzuteilen.
|
||||
9.12 KLZ wird die Vorbehaltsware sowie die an ihre Stelle tretenden Sachen oder Forderungen
|
||||
freigeben, soweit ihr Wert die Höhe der gesicherten Forderungen um mehr als zehn (10) %
|
||||
übersteigt. Die Auswahl der danach freizugebenden Gegenstände liegt bei KLZ.
|
||||
9.13 Tritt KLZ bei vertragswidrigem Verhalten des Kunden, insbesondere Zahlungsverzug, vom
|
||||
Vertrag zurück (Verwertungsfall), ist KLZ berechtigt, die Vorbehaltsware herauszuverlangen.
|
||||
Der Kunde hat in diesem Fall KLZ alle Informationen und Unterlagen zu übergeben, die
|
||||
erforderlich sind, um die Vorbehaltsware herauszuverlangen und die an KLZ abgetretenen
|
||||
Ansprüche des Kunden gegenüber seinen Abnehmern geltend zu machen, mindestens jedoch
|
||||
eine Aufstellung über die bei dem Kunden oder bei den Abnehmern befindliche
|
||||
Vorbehaltsware, auch soweit sie bereits verarbeitet ist, und eine Aufstellung aller abgetretenen
|
||||
Forderungen nebst Rechnungsgutschriften.
|
||||
10 Höhere Gewalt
|
||||
10.1 „Höhere Gewalt“ bedeutet das Eintreten eines Ereignisses oder Umstands, welcher eine Partei
|
||||
daran hindert, eine oder mehrere ihrer vertraglichen Verpflichtungen zu erfüllen, wenn und
|
||||
soweit die von dem Hindernis betroffene Partei nachweist, dass (a) dieses Hindernis
|
||||
außerhalb der ihr zumutbaren Kontrolle liegt und (b) es zu dem Zeitpunkt des
|
||||
Vertragsschlusses nicht in zumutbarer Weise vorhersehbar war und (c) die Auswirkungen des
|
||||
Hindernisses von der betroffenen Partei nicht in zumutbarer Weise hätte vermieden oder
|
||||
überwunden werden können.
|
||||
10.2 Bei dem Beweis des Gegenteils wird insbesondere, aber nicht abschließend, bei den
|
||||
folgenden Ereignissen ein Fall Höherer Gewalt im Sinne des vorstehenden Absatzes vermutet:
|
||||
Krieg, Terrorakte, Währungs- und Handelsbeschränkungen, Embargo, Sanktionen,
|
||||
Amtshandlungen, Befolgung von Gesetzen oder Regierungsanordnungen, staatliche
|
||||
Ausreise- und Exportverbote oder staatliche Einreise- und Importverbote, Enteignung,
|
||||
Epidemie, Naturkatastrophe, extremes Naturereignis, Explosionen, Feuer, Zerstörung von
|
||||
Ausrüstungen, Demonstrationen oder Versammlungen, Ereignisse, die das Passieren von
|
||||
wichtigen Transportrouten verhindern, allgemeine Arbeitsunruhen, insbesondere Boykott,
|
||||
Streik und Aussperrungen, Energieknappheit oder Beeinträchtigung von Transportmitteln.
|
||||
10.3 Eine Partei, die sich mit Erfolg auf diese Klausel beruft, ist ab dem Zeitpunkt, zu dem das
|
||||
Hindernis ihr die Leistungserbringung unmöglich macht, von ihrer Pflicht zur Erfüllung ihrer
|
||||
vertraglichen Verpflichtungen und von jeder Schadensersatzpflicht oder jedem anderen
|
||||
vertraglichen Rechtsbehelf wegen Vertragsverletzung suspendiert, sofern der Eintritt eines
|
||||
Ereignisses Höherer Gewalt der anderen Partei unverzüglich mitgeteilt wird. Ist die Auswirkung
|
||||
des geltend gemachten Hindernisses oder Ereignisses vorübergehend, so gelten die eben
|
||||
dargelegten Folgen nur so lange, wie das geltend gemachte Hindernis die Vertragserfüllung
|
||||
7
|
||||
durch die betroffene Partei verhindert sowie einer angemessenen Anlaufzeit. Dauert das
|
||||
Hindernis oder Ereignis höherer Gewalt bei der betroffenen Partei länger als vier (4) Monate
|
||||
an, ist die andere Partei zur ganzen oder teilweisen außerordentlichen Kündigung bzw. zum
|
||||
Rücktritt vom Vertrag berechtigt. Wechselseitige Schadensersatzansprüche wegen dieser
|
||||
Kündigung bzw. dieses Rücktritts sind ausgeschlossen.
|
||||
10.4 Der Kunde ist verpflichtet, auf Verlangen von KLZ innerhalb einer angemessenen Frist zu
|
||||
erklären, ob er wegen einem bei KLZ eingetretenen Ereignis Höherer Gewalt den Vertrag
|
||||
kündigt bzw. von diesem Zurücktritt oder weiterhin auf der Durchführung der Lieferung der
|
||||
Ware besteht.
|
||||
10.5 Sofern sich aufgrund eines Ereignisses Höherer Gewalt die wirtschaftliche Bedeutung oder
|
||||
der Inhalt der Lieferung der Ware erheblich verändert oder auf den Betrieb von KLZ erheblich
|
||||
einwirkt, werden die Parteien den zugrundeliegenden Vertrag unter Beachtung von Treu und
|
||||
Glauben angemessen anpassen. Soweit dies wirtschaftlich nicht vertretbar ist, steht KLZ das
|
||||
Recht zu, von dem Vertrag zurückzutreten. Will KLZ von diesem Rücktrittsrecht Gebrauch
|
||||
machen, so hat er dies nach Erkenntnis der Tragweite des Ereignisses Höherer Gewalt
|
||||
unverzüglich dem Kunden mitzuteilen, und zwar auch dann, wenn zunächst mit dem Kunden
|
||||
eine Verlängerung der Lieferzeit vereinbart war.
|
||||
11 Gewährleistung und Mängelrüge
|
||||
11.1 Mängelansprüche bestehen nicht bei nur unerheblicher Abweichung der gelieferten Ware von
|
||||
den vereinbarten Spezifikationen, bei unerheblicher Beeinträchtigung der von KLZ
|
||||
vorgegebenen Brauchbarkeit, bei natürlicher Abnutzung, übermäßiger Beanspruchung,
|
||||
Verwendung ungeeigneter Betriebsmittel, oder bei Mängeln, die aufgrund besonderer äußerer
|
||||
Einflüsse entstehen, sowie bei nicht reproduzierbaren Softwarefehlern. Werden vom Kunden
|
||||
oder von Dritten unsachgemäß Änderungen oder Instandsetzungsarbeiten vorgenommen, so
|
||||
bestehen für diese und die daraus entstehenden Folgen ebenfalls keine Mängelansprüche.
|
||||
11.2 Eine Gewährleistungsverpflichtung von KLZ besteht nur, wenn der Kunde den ihm
|
||||
obliegenden Untersuchungs- und Rügepflichten fristgerecht nachgekommen ist. Eingehende
|
||||
Waren hat der Kunde unverzüglich auf äußerlich erkennbare Transportschäden und Identität
|
||||
der gelieferten Ware mit der Bestellung zu überprüfen. Äußerlich erkennbare Mängel sind
|
||||
unverzüglich, spätestens jedoch innerhalb von drei (3) Werktagen ab Übergabe, sonstige
|
||||
Mängel innerhalb von fünf (5) Werktagen nach ihrer Entdeckung gegenüber KLZ im Rahmen
|
||||
einer Mängelrüge mindestens in Textform geltend zu machen.
|
||||
11.3 Mängelrügen müssen eine genaue Beschreibung der aufgetretenen Mängel, der betroffenen
|
||||
Lieferungen und wenn möglich, auch eine bildhafte Dokumentation des Mangels enthalten.
|
||||
KLZ ist berechtigt, mangelhafte Teile auf eigene Kosten zur Übersendung und Untersuchung
|
||||
anzufordern.
|
||||
11.4 Beim Verlegen von Kabeln in Gräben oder Rohren, bzw. in Bauwerken ist eine ständige
|
||||
Sichtkontrolle durch den Kabelverleger vorzunehmen. Der Kabelverleger hat laufend zu
|
||||
überprüfen, ob Auffälligkeiten zu vermerken sind.
|
||||
11.5 KLZ ist berechtigt, innerhalb einer angemessenen Frist, anerkannte Mängel nach eigener
|
||||
Wahl durch Beseitigung (Nachbesserung) oder durch Lieferung mangelfreier Ware
|
||||
8
|
||||
(Ersatzlieferung) zu beheben. Der Kunde wird KLZ bei der Beseitigung von Mängeln in
|
||||
zumutbarem Umfang unterstützen.
|
||||
11.6 Die Kosten der Nacherfüllung und Mangeluntersuchung werden bei berechtigten
|
||||
Beanstandungen von KLZ getragen. Dies gilt nicht für Aufwendungen, die dadurch entstehen,
|
||||
dass die gelieferten Waren durch den Kunden an einen anderen als den Ort der gewerblichen
|
||||
Niederlassung des Kunden verbracht worden sind, es sei denn, das Verbringen entspricht dem
|
||||
vertraglich vorgesehenen Verwendungszweck der gelieferten Ware. Satz 1 gilt ebenfalls nicht
|
||||
für Mehraufwendungen, die dadurch entstehen, dass der Kunde die gelieferte Ware ohne
|
||||
Zustimmung von KLZ verändert hat.
|
||||
11.7 Hat der Kunde die Ware verbaut, obwohl er positive Kenntnis oder grob fahrlässige Unkenntnis
|
||||
des Mangels hatte, ist KLZ nicht dazu verpflichtet, die Ein- und Ausbaukosten der Ware zu
|
||||
erstatten.
|
||||
11.8 Der Kunde kann erst nach zweimaligem Fehlschlagen der Mängelbeseitigung vom Vertrag
|
||||
zurücktreten oder den vereinbarten Kaufpreis angemessen mindern. Bei nur unerheblicher
|
||||
Minderung des Werts oder der Tauglichkeit der gelieferten Waren ist der Rücktritt
|
||||
ausgeschlossen, ebenso wenn der Kunde sich im Annahmeverzug befindet oder wenn der
|
||||
Kunde den Mangel zu vertreten hat. Solange der Kunde KLZ gegenüber nicht den Rücktritt
|
||||
vom Vertrag erklärt oder Schadensersatz wegen Nichterfüllung verlangt hat, ist KLZ auch nach
|
||||
Ablauf der vom Kunden gesetzten Frist zur Nacherfüllung berechtigt, es sei denn, der Kunde
|
||||
hat zuvor schriftlich die Ablehnung der Nacherfüllung angezeigt.
|
||||
11.9 Beruht ein Mangel auf dem Verschulden von KLZ, kann der Kunde unter den in Ziffer 12
|
||||
bestimmten Voraussetzungen Schadensersatz verlangen. Ziffer 12 gilt entsprechend für den
|
||||
Ersatz vergeblicher Aufwendungen, den der Kunde anstelle eines Schadensersatzes statt der
|
||||
Leistung verlangen kann.
|
||||
11.10 Besteht Uneinigkeit zwischen den Parteien in Bezug auf Mängel an den Waren, ist ein
|
||||
Gutachten bei dem VDE-Institut über die streitige Frage einzuholen. Das Ergebnis des
|
||||
Gutachtens des VDE-Instituts ist für die Feststellung der Mangelfreiheit oder eines Mangels
|
||||
maßgebend. KLZ ist nur dann verpflichtet, die Kosten des Gutachtens des VDE-Instituts zu
|
||||
tragen, wenn das Gutachten einen Mangel feststellt.
|
||||
11.11 Die gesetzlichen Mängelgewährleistungsansprüche des Kunden verjähren innerhalb von
|
||||
zwölf (12) Monaten ab Gefahrübergang. §§ 438 Abs. 1 Nr. 2, 479 Abs. 1 und
|
||||
634a Abs. 1 Nr. 2 BGB bleiben hiervon unberührt. Diese Verkürzung der Verjährungsfrist gilt
|
||||
nicht bei Vorsatz, grober Fahrlässigkeit, der Verletzung von Leben, Körper, Gesundheit, oder
|
||||
jedweder wesentlicher Vertragspflichtverletzung, zwingender gesetzlicher Haftungsnormen
|
||||
wie denen des ProdHaftG, sowie bei Nichteinhaltung einer Beschaffenheitsgarantie oder dem
|
||||
arglistigen Verschweigen eines Mangels. Die gesetzlichen Regelungen über Ablaufhemmung,
|
||||
Hemmung und Neubeginn der Fristen bleiben unberührt.
|
||||
12 Haftung
|
||||
12.1 KLZ haftet uneingeschränkt für Vorsatz und grobe Fahrlässigkeit. KLZ haftet ferner
|
||||
uneingeschränkt für Schäden aus der Verletzung des Lebens, des Körpers oder der
|
||||
Gesundheit, sowie für arglistiges Verschweigen eines Mangels und Verletzung von
|
||||
9
|
||||
garantierten Beschaffenheitsmerkmalen, sowie im Rahmen zwingender gesetzlicher
|
||||
Vorschriften, wie etwa denen des Produkthaftungsgesetzes.
|
||||
12.2 Bei der Verletzung von wesentlichen Vertragspflichten haftet KLZ im Übrigen der Höhe nach
|
||||
begrenzt auf den bei Vertragsschluss für KLZ typischerweise vorhersehbaren Schaden bzw.
|
||||
die typischerweise vorhersehbaren Aufwendungen. Vertragswesentlich sind die
|
||||
Verpflichtungen, deren Erfüllung die ordnungsgemäße Vertragsdurchführung erst ermöglichen
|
||||
und auf deren Einhaltung der Kunde vertraut und auch vertrauen darf. Im Übrigen haftet KLZ
|
||||
bei leichter Fahrlässigkeit nicht auf Schadensersatz.
|
||||
12.3 Eine Haftung für mittelbare Schäden und Folgeschäden, die Folge von Mängeln der gelieferten
|
||||
Waren sind, besteht nur für solche Schäden, die bei bestimmungsgemäßer Verwendung der
|
||||
gelieferten Ware typischerweise zu erwarten sind.
|
||||
12.4 Die Haftung entfällt für Schäden, die auf Reparaturen, Änderungen oder anderen Eingriffen
|
||||
oder auf die Verwendung von Nicht-Originalteilen von KLZ im Rahmen der Reparatur oder
|
||||
Änderung bzw. des Eingriffs zurückzuführen sind, die der Kunde ohne Zustimmung von KLZ
|
||||
vorgenommen hat.
|
||||
12.5 Soweit die Haftung von KLZ ausgeschlossen oder beschränkt ist, gilt dies auch für die Haftung
|
||||
der Angestellten, Arbeitnehmer, Mitarbeiter, Vertreter und Erfüllungsgehilfen von KLZ.
|
||||
13 Produkthaftung
|
||||
13.1 Der Kunde wird die gelieferten Waren ausschließlich vertragsgemäß und entsprechend den
|
||||
verfügbaren Anleitungen von KLZ verwenden, insbesondere wird er vorhandene
|
||||
Warnhinweise über Gefahren bei Gebrauch der gelieferten Waren nicht verändern oder
|
||||
entfernen. Bei Verletzung dieser Pflicht stellt der Kunde KLZ im Innenverhältnis von
|
||||
Produkthaftungsansprüchen Dritter frei, es sei denn der Kunde ist für den die Haftung
|
||||
auslösenden Fehler nicht verantwortlich.
|
||||
13.2 Wird KLZ aufgrund eines Produktfehlers zu einem Produktrückruf oder einer -warnung
|
||||
veranlasst, so wird der Kunde nach besten Kräften bei den Maßnahmen mitwirken, die KLZ
|
||||
für erforderlich und zweckmäßig hält. KLZ ist verpflichtet, die Kosten des Produktrückrufs oder
|
||||
der -warnung zu tragen, es sei denn KLZ ist für den Produktfehler und den eingetretenen
|
||||
Schaden nach produkthaftungsrechtlichen Grundsätzen nicht verantwortlich.
|
||||
13.3 Der Kunde wird KLZ unverzüglich über ihm bekanntwerdende Risiken bei der Verwendung der
|
||||
gelieferten Ware und mögliche Produktfehler in Textform informieren.
|
||||
14 Exportkontrolle
|
||||
14.1 Lieferungen von Waren von KLZ stehen unter dem Vorbehalt, dass der Vertragserfüllung keine
|
||||
Hindernisse aufgrund von nationalen oder internationalen exportkontrollrechtlichen
|
||||
Vorschriften entgegenstehen. Sofern sich die Durchführung des Vertrages aufgrund von
|
||||
Exportkontrollprüfungen oder Genehmigungsverfahren verzögert, werden Fristen und
|
||||
Lieferzeiten außer Kraft gesetzt. KLZ ist berechtigt, von der Einzelbestellung zurückzutreten
|
||||
und den Liefervertrag fristlos zu kündigen, wenn ein solcher Rücktritt oder eine Kündigung zur
|
||||
Einhaltung nationaler oder internationalen Exportkontrollbestimmung erforderlich ist. Die
|
||||
10
|
||||
Geltendmachung von Schadensersatzansprüchen oder anderen Rechten durch den Kunden
|
||||
wegen vorgenannter Kündigung oder Verzögerung ist ausgeschlossen.
|
||||
14.2 Der Kunde verpflichtet sich zur Einhaltung des nationalen und internationalen
|
||||
Exportkontrollrechts, insbesondere bei Weitergabe der gelieferten Ware an Dritte.
|
||||
15 Urheberrechte und Datenschutz
|
||||
15.1 An Kostenvoranschlägen, Zeichnungen, Modellen, Teilen, Schablonen, Berechnungen,
|
||||
Beschreibungen, Mustern und anderen Unterlagen (im Folgenden: „Unterlagen“ genannt)
|
||||
behält sich KLZ seine eigentums- und urheberrechtlichen Verwertungsrechte
|
||||
uneingeschränkt vor. Die Unterlagen dürfen nur nach vorheriger schriftlicher Zustimmung
|
||||
von KLZ Dritten zugänglich gemacht werden und sind KLZ auf Verlangen unverzüglich
|
||||
zurückzugeben.
|
||||
15.2 Ohne ausdrückliche Zustimmung von KLZ in Textform ist der Kunde nicht berechtigt, die
|
||||
gelieferte Ware zu Werbezwecken zu verwenden.
|
||||
15.3 KLZ erhebt und verwendet personenbezogenen Daten ausschließlich gemäß den
|
||||
Bestimmungen der Datenschutzgrundverordnung und des Bundesdatenschutzgesetzes.
|
||||
15.4 Personenbezogenen Daten, die für die Begründung, inhaltliche Ausgestaltung, Abwicklung
|
||||
oder Änderung des Vertragsverhältnisses erforderlich sind (Bestandsdaten), werden
|
||||
ausschließlich zur Durchführung des zwischen KLZ und dem Kunden abgeschlossenen
|
||||
Vertrages verwendet. Zur Abwicklung des Vertragsverhältnisses müssen diese Daten im
|
||||
erforderlichen Umfang an Dritte (z.B. Transporteure oder Spediteure) weitergegeben werden.
|
||||
Der Kunde erteilt für die vorbeschriebene Verwendung seiner personenbezogenen Daten
|
||||
hiermit eine entsprechende Einwilligung.
|
||||
15.5 Diese Einwilligungserklärung ist jederzeit -ohne Angabe von Gründen- widerruflich. Der
|
||||
Widerruf ist zu richten an info@klz-cables.com .
|
||||
15.6 Eine darüberhinausgehende Nutzung der Bestandsdaten des Kunden für Zwecke der
|
||||
Werbung, der Marktforschung oder zur bedarfsgerechten Gestaltung der Angebote von KLZ
|
||||
bedarf der gesonderten ausdrücklichen Einwilligung des Kunden.
|
||||
15.7 Auf Anordnung einer zuständigen Stelle darf KLZ im Einzelfall Auskunft über
|
||||
personenbezogene Daten des Kunden im Rahmen der Anordnung an die zuständige Stelle
|
||||
erteilen, soweit dies für Zwecke der Strafverfolgung, zur Gefahrenabwehr, zur Erfüllung der
|
||||
gesetzlichen Aufgaben der Verfassungsschutzbehörden oder zur Durchsetzung der Rechte
|
||||
am geistigen Eigentum erforderlich ist.
|
||||
16 Geheimhaltung
|
||||
16.1 „Vertrauliche Informationen“ sind
|
||||
(i) alle Informationen, Daten, Zeichnungen, Unterlagen, Materialien, Know-how oder
|
||||
sonstige Geschäftsgeheimnisse im Sinne von § 2 Nr. 1 des Gesetzes zum Schutz von
|
||||
Geschäftsgeheimnissen (GeschGehG), insbesondere alle Entwürfe, Skizzen,
|
||||
Zeichnungen, technischen Protokolle, Modelle oder elektronische Daten, gleich in
|
||||
welcher Verkörperung,
|
||||
11
|
||||
(ii) sowie alle nicht offenkundigen kaufmännischen, betrieblichen oder technischen
|
||||
Informationen oder Gegenstände, gleich in welcher Verkörperung, die dem Kunden im
|
||||
Rahmen der Geschäftsbeziehung mit KLZ offengelegt oder anderweitig bekannt
|
||||
werden,
|
||||
(iii) die entweder direkt als vertraulich gekennzeichnet wurden oder nachträglich schriftlich
|
||||
oder in Textform als vertraulich gekennzeichnet werden. Des Weiteren gelten auch
|
||||
solche Informationen als vertraulich, welche der Natur nach bzw. aus den Umständen
|
||||
heraus oder aus anderen Gründen erkennbar als vertraulich anzusehen sind, soweit
|
||||
die Parteien nichts anderes in Schriftform vereinbart haben.
|
||||
16.2 Vertrauliche Informationen im Sinne von Ziffer 16.1 sind jedoch nicht solche Informationen,
|
||||
(i) die der Kunde von öffentlich zugänglichen Quellen bezogen hat und die allgemein
|
||||
bekannt oder auf legalem Wege zugänglich sind und diese Quellen die Informationen
|
||||
ebenfalls auf legalem Wege erlangt haben,
|
||||
(ii) die der Kunde von einem berechtigten Dritten rechtmäßig erhalten hat,
|
||||
(iii) die der Kunde eigenständig entwickelt hat,
|
||||
(iv) die kraft Gesetzes oder aufgrund gerichtlicher oder behördlicher Anordnung
|
||||
offengelegt wurden oder offenzulegen sind. Der Kunde ist in diesem Fall verpflichtet,
|
||||
KLZ vor einer solchen Offenlegung der Informationen unverzüglich zu informieren und
|
||||
zu versuchen, die Offenlegung zu verhindern, soweit dies mit angemessenen und
|
||||
gesetzlich zulässigen Mitteln möglich ist.
|
||||
16.3 Die Darlegungs- und Beweislast für das Vorliegen einer in Ziffer 16.2 benannten
|
||||
Ausnahmetatbestände trägt diejenige Partei, die sich auf das Vorliegen einer oder mehrerer
|
||||
dieser Ausnahmetatbestände beruft.
|
||||
16.4 Der Kunde verpflichtet sich, die Vertraulichen Informationen strikt vertraulich zu behandeln und
|
||||
ausschließlich zur Erfüllung des Vertragszwecks zu verwenden. Die Vertraulichen
|
||||
Informationen dürfen durch den Kunden nur an solche Mitarbeiter oder andere Personen
|
||||
weitergegeben werden, die sie zur Erreichung des Vertragszwecks zwingend erhalten
|
||||
müssen. Sämtliche Mitarbeiter des Kunden, welche Kenntnis von Vertraulichen Informationen
|
||||
erhalten, müssen durch den Kunden ebenfalls zur Geheimhaltung hinsichtlich der
|
||||
Vertraulichen Informationen verpflichtet sein oder werden. Diese Geheimhaltungspflicht soll
|
||||
auch für die Zeit nach einem etwaigen Ausscheiden des Mitarbeiters aus dem Unternehmen
|
||||
des Kunden für weitere fünf (5) Jahre gelten, soweit dies rechtlich möglich ist.
|
||||
16.5 Der Kunde verpflichtet sich, auf erstes Anfordern von KLZ und/oder nach Beendigung der
|
||||
Geschäftsbeziehung oder Beendigung des zugrundeliegenden Vertrage, gleich aus welchem
|
||||
Rechtsgrund, alle unter diese Geheimhaltungsvereinbarung fallenden Vertraulichen
|
||||
Informationen und davon angefertigte Kopien an KLZ herauszugeben oder sachgemäß zu
|
||||
zerstören bzw. sicher und dauerhaft zu löschen und die vollständige Herausgabe, vollständige
|
||||
sachgemäße Zerstörung oder vollständige sichere und dauerhafte Löschung KLZ gegenüber
|
||||
schriftlich zu bestätigen.
|
||||
16.6 Ziffer 16.5 gilt nicht für Vertrauliche Informationen, die zwingenden gesetzlichen
|
||||
Aufbewahrungspflichten unterliegen oder in automatisierten Back-Up Dateien im Rahmen des
|
||||
ordnungsgemäßen Sicherungsprozesses sicher gespeichert wurden. Für diese Vertraulichen
|
||||
Informationen gelten die Bestimmungen dieser Geheimhaltungsvereinbarung bis zu ihrer
|
||||
12
|
||||
Herausgabe, sicheren und dauerhaften Löschung oder vollständigen sachgemäßen
|
||||
Zerstörung fort.
|
||||
16.7 Die Geheimhaltungsverpflichtungen gemäß dieser Ziffer 16 gelten für einen Zeitraum von
|
||||
fünf (5) Jahren nach Beendigung des zugrundeliegenden Vertrages, gleich aus welchem
|
||||
Rechtsgrund, fort.
|
||||
16.8 Für jeden Einzelfall eines Verstoßes des Kunden gegen die Pflichten aus dieser
|
||||
Geheimhaltungsvereinbarung ist der Kunde zur Zahlung einer Vertragsstrafe an KLZ
|
||||
verpflichtet, deren Höhe in das billige Ermessen von KLZ gestellt wird und im Streitfall durch
|
||||
das zuständige Gericht überprüft werden kann. Durch die Geltendmachung der Vertragsstrafe
|
||||
werden weitergehende Schadensersatzansprüche von KLZ nicht ausgeschlossen.
|
||||
17. Technische Beratungsdienstleistungen
|
||||
17.1 Die technische Unterstützung ersetzt weder die Fachplanung noch die Ausführungs- oder
|
||||
Prüfverantwortung des beauftragten Ingenieurbüros, Planers oder der ausführenden
|
||||
Fachfirma bzw. verantwortlichen Abteilung.
|
||||
Alle Hinweise, Einschätzungen und Empfehlungen der MB Grid Solutions and Services
|
||||
erfolgen ohne Gewähr und entbinden den jeweiligen Auftragnehmer nicht von seiner eigenen
|
||||
fachlichen Prüfung, Planung und Verantwortung.
|
||||
18 Schlussbestimmungen
|
||||
18.1 Änderungen oder Ergänzungen dieser AVB bedürfen der Textform. Dies gilt auch für die
|
||||
Abänderung des Textformerfordernisses selbst.
|
||||
18.2 Auf Verträge zwischen KLZ und dem Kunden, der Auslegung dieser AVB, sowie die gesamten
|
||||
Rechtsbeziehungen zwischen KLZ und dem Kunden gilt ausschließlich das Recht der
|
||||
Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts und des Kollisionsrechts.
|
||||
Erfüllungsort sowie ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im
|
||||
Zusammenhang mit der Rechtsbeziehung zwischen KLZ und dem Kunden ist Stuttgart.
|
||||
18.3 Sollten einzelne Bestimmungen dieser AVB unwirksam oder undurchführbar sein oder werden,
|
||||
so wird hierdurch die Wirksamkeit aller sonstigen Bestimmungen dieser AVB im Übrigen nicht
|
||||
berührt. Die unwirksame oder undurchführbare Bestimmung wird von den Vertragsparteien
|
||||
einvernehmlich durch eine rechtswirksame Regelung ersetzt, welche dem wirtschaftlichen
|
||||
Erfolg und Zweck der unwirksamen oder undurchführbaren Bestimmung am nächsten kommt.
|
||||
Dies gilt gleichermaßen für den Fall einer Regelungslücke.
|
||||
Stand: April 2026
|
||||
13"""
|
||||
|
||||
# Remove page numbers on their own lines
|
||||
text = re.sub(r'\n\d+\n', '\n', text)
|
||||
# Remove empty lines
|
||||
text = re.sub(r'\n\n+', '\n', text)
|
||||
|
||||
lines = text.split('\n')
|
||||
formatted = []
|
||||
|
||||
# Ensure it starts with markdown headers
|
||||
formatted.append("# Allgemeine Verkaufsbedingungen (AVB)\n")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line == "Allgemeine Verkaufsbedingungen":
|
||||
continue
|
||||
if line == "Stand: April 2026":
|
||||
formatted.insert(1, line + "\n")
|
||||
continue
|
||||
|
||||
# Regex to see if it's a main heading, e.g. "1 Geltungsbereich"
|
||||
m_main = re.match(r'^(\d+)\s+(.+)$', line)
|
||||
# Regex to see if and it's not a sub heading like "1.1."
|
||||
if m_main and not '.' in m_main.group(1):
|
||||
formatted.append(f"\n## {m_main.group(1)}. {m_main.group(2)}\n")
|
||||
continue
|
||||
|
||||
m_main2 = re.match(r'^(\d+\.)\s+(.+)$', line)
|
||||
if m_main2:
|
||||
formatted.append(f"\n## {m_main2.group(1)} {m_main2.group(2)}\n")
|
||||
continue
|
||||
|
||||
# Regex for sub items, e.g. "1.1 Ausschließlich..."
|
||||
m_sub = re.match(r'^(\d+\.\d+)\s+(.+)$', line)
|
||||
if m_sub:
|
||||
formatted.append(f"\n**{m_sub.group(1)}** {m_sub.group(2)}")
|
||||
continue
|
||||
|
||||
# Continuation lines
|
||||
formatted[-1] += f" {line}"
|
||||
|
||||
result = "\n".join(formatted).strip() + "\n"
|
||||
|
||||
# Replace KLZ with MB Grid Solutions
|
||||
result = result.replace("KLZ Vertriebs GmbH", "MB Grid Solutions & Services")
|
||||
result = result.replace("KLZ", "MB Grid Solutions & Services")
|
||||
|
||||
with open("/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/context/avbs.md", "w") as f:
|
||||
f.write(result)
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"legal": "Rechtliches",
|
||||
"impressum": "Impressum",
|
||||
"datenschutz": "Datenschutz",
|
||||
"agb": "AVB",
|
||||
"avb": "AVB",
|
||||
"rights": "Alle Rechte vorbehalten.",
|
||||
"madeWith": "Entwickelt mit",
|
||||
"precision": "Präzision",
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mb-grid-solutions.com",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.22",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"scripts": {
|
||||
@@ -37,9 +37,7 @@
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/storage-s3": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-email/components": "^1.0.8",
|
||||
"@sentry/nextjs": "^10.38.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.13.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -50,7 +48,6 @@
|
||||
"pino": "^10.3.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-email": "^5.2.8",
|
||||
"sharp": "^0.34.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
|
||||
1497
pnpm-lock.yaml
generated
1497
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,40 @@
|
||||
import puppeteer, { HTTPResponse } from 'puppeteer';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import puppeteer, { HTTPResponse } from "puppeteer";
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const targetUrl =
|
||||
process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) ||
|
||||
process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
'http://localhost:3000';
|
||||
const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.ASSET_CHECK_LIMIT
|
||||
? parseInt(process.env.ASSET_CHECK_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: ${limit} pages\n`);
|
||||
|
||||
// 1. Fetch Sitemap to discover all routes
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
let urls: string[] = [];
|
||||
|
||||
try {
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
urls = $('url loc')
|
||||
urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Normalize to target URL instance
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
@@ -42,13 +44,20 @@ async function main() {
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
// Simplify selection: home pages + a slice of the rest
|
||||
const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith('/en'));
|
||||
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
||||
urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
||||
const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith("/en"));
|
||||
const others = urls.filter(
|
||||
(u) => !homeEN.includes(u) && !homeDE.includes(u),
|
||||
);
|
||||
urls = [
|
||||
...homeDE,
|
||||
...homeEN,
|
||||
...others.slice(0, limit - (homeEN.length + homeDE.length)),
|
||||
];
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`❌ Failed to fetch sitemap: ${errorBody}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -56,50 +65,64 @@ async function main() {
|
||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
process.env.CHROME_PATH ||
|
||||
undefined,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Inject Gatekeeper session bypassing auth screens
|
||||
await page.setCookie({
|
||||
name: 'mb_gatekeeper_session',
|
||||
name: "mintel_gatekeeper_session",
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: targetUrl.startsWith('https://'),
|
||||
secure: targetUrl.startsWith("https://"),
|
||||
});
|
||||
|
||||
let hasBrokenAssets = false;
|
||||
let hasConsoleErrors = false;
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> = [];
|
||||
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> =
|
||||
[];
|
||||
const consoleErrorsList: Array<{
|
||||
type: string;
|
||||
error: string;
|
||||
page: string;
|
||||
}> = [];
|
||||
|
||||
// Listen for unhandled exceptions natively in the page
|
||||
page.on('pageerror', (err: any) => {
|
||||
page.on("pageerror", (err: unknown) => {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
error: err.message,
|
||||
type: "PAGE_ERROR",
|
||||
error: errorMessage,
|
||||
page: page.url(),
|
||||
});
|
||||
hasConsoleErrors = true;
|
||||
});
|
||||
|
||||
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
|
||||
page.on('console', (msg) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
page.on("console", (msg: any) => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warn') {
|
||||
if (type === "error" || type === "warn") {
|
||||
const text = msg.text();
|
||||
|
||||
// Exclude common browser extension noise or third party tracker warnings
|
||||
if (
|
||||
text.includes('google-analytics') ||
|
||||
text.includes('googletagmanager') ||
|
||||
text.includes('SES Removing unpermitted intrinsics') ||
|
||||
text.includes('Third-party cookie will be blocked') ||
|
||||
text.includes('Fast Refresh')
|
||||
text.includes("google-analytics") ||
|
||||
text.includes("googletagmanager") ||
|
||||
text.includes("SES Removing unpermitted intrinsics") ||
|
||||
text.includes("Third-party cookie will be blocked") ||
|
||||
text.includes("Fast Refresh")
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -113,19 +136,19 @@ async function main() {
|
||||
});
|
||||
|
||||
// Listen to ALL network responses
|
||||
page.on('response', (response: HTTPResponse) => {
|
||||
page.on("response", (response: HTTPResponse) => {
|
||||
const status = response.status();
|
||||
// Catch classic 404s and 500s on ANY fetch/image/script
|
||||
if (
|
||||
status >= 400 &&
|
||||
status !== 999 &&
|
||||
!response.url().includes('google-analytics') &&
|
||||
!response.url().includes('googletagmanager')
|
||||
!response.url().includes("google-analytics") &&
|
||||
!response.url().includes("googletagmanager")
|
||||
) {
|
||||
const type = response.request().resourceType();
|
||||
|
||||
// We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s.
|
||||
if (['image', 'script', 'stylesheet', 'fetch', 'xhr'].includes(type)) {
|
||||
if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) {
|
||||
brokenAssetsList.push({
|
||||
url: response.url(),
|
||||
status: status,
|
||||
@@ -142,7 +165,7 @@ async function main() {
|
||||
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
|
||||
try {
|
||||
// Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger
|
||||
await page.goto(u, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 });
|
||||
|
||||
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||
await page.evaluate(async () => {
|
||||
@@ -163,8 +186,9 @@ async function main() {
|
||||
|
||||
// Wait a tiny bit more for final lazy loads
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
} catch (err: any) {
|
||||
console.error(`⚠️ Timeout or navigation error on ${u}: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`⚠️ Timeout or navigation error on ${u}: ${errorBody}`);
|
||||
// Don't fail the whole script just because one page timed out, but flag it
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
@@ -174,7 +198,9 @@ async function main() {
|
||||
|
||||
// 4. Report Results
|
||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
|
||||
console.error(
|
||||
`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`,
|
||||
);
|
||||
console.table(brokenAssetsList);
|
||||
}
|
||||
|
||||
@@ -184,7 +210,9 @@ async function main() {
|
||||
}
|
||||
|
||||
if (hasBrokenAssets || hasConsoleErrors) {
|
||||
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
|
||||
console.error(
|
||||
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: None (Full Sitemap)\n`);
|
||||
|
||||
try {
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||
@@ -47,36 +51,46 @@ async function main() {
|
||||
const u = urls[i];
|
||||
try {
|
||||
const res = await axios.get(u, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: {
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
// Generate a safe filename that retains URL information
|
||||
const urlStr = new URL(u);
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const filename = `${safePath || 'index'}.html`;
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
"_",
|
||||
);
|
||||
const filename = `${safePath || "index"}.html`;
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||
} catch (err: any) {
|
||||
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
|
||||
throw new Error(`Failed to fetch page: ${u} - ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
console.error(`❌ HTTP Error fetching ${u}: ${errorBody}`);
|
||||
throw new Error(`Failed to fetch page: ${u} - ${errorBody}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n💻 Executing html-validate...`);
|
||||
try {
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`✅ HTML Validation passed perfectly!`);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error(`❌ HTML Validation found issues.`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`\n❌ Error during HTML Validation:`, error.message);
|
||||
} catch (error: unknown) {
|
||||
const errorBody = error instanceof Error ? error.message : String(error);
|
||||
console.error(`\n❌ Error during HTML Validation:`, errorBody);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
scripts/patch_qa.py
Normal file
43
scripts/patch_qa.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import re
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace Registry Auth
|
||||
old_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc"""
|
||||
|
||||
new_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||
|
||||
content = content.replace(old_auth, new_auth)
|
||||
|
||||
# Replace Install dependencies (for jobs that don't have the rm -rf)
|
||||
old_install_1 = """ - name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile"""
|
||||
|
||||
new_install_1 = """ - name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile"""
|
||||
|
||||
content = content.replace(old_install_1, new_install_1)
|
||||
|
||||
# Replace Install dependencies (for prepare job)
|
||||
old_install_2 = """ - name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --no-frozen-lockfile --reporter=append-only"""
|
||||
|
||||
new_install_2 = """ - name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile --reporter=append-only"""
|
||||
|
||||
content = content.replace(old_install_2, new_install_2)
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||
f.write(content)
|
||||
20
scripts/revert_auth.py
Normal file
20
scripts/revert_auth.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import re
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
bad_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||
|
||||
good_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc"""
|
||||
|
||||
content = content.replace(bad_auth, good_auth)
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||
f.write(content)
|
||||
@@ -1,8 +1,32 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
interface Pa11yConfig {
|
||||
defaults: {
|
||||
threshold?: number;
|
||||
runners?: string[];
|
||||
ignore?: string[];
|
||||
chromeLaunchConfig?: {
|
||||
executablePath?: string;
|
||||
args?: string[];
|
||||
};
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
};
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
interface Pa11yResult {
|
||||
type?: string;
|
||||
message?: string;
|
||||
code?: string;
|
||||
context?: string;
|
||||
selector?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WCAG Audit Script
|
||||
@@ -12,9 +36,14 @@ import * as path from 'path';
|
||||
* 3. Runs pa11y-ci on those URLs
|
||||
*/
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.PAGESPEED_LIMIT
|
||||
? parseInt(process.env.PAGESPEED_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
||||
@@ -22,32 +51,32 @@ async function main() {
|
||||
|
||||
try {
|
||||
// 1. Fetch Sitemap
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Cleanup, filter and normalize domains to targetUrl
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -55,7 +84,9 @@ async function main() {
|
||||
console.log(
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl);
|
||||
const home = urls.filter(
|
||||
(u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl,
|
||||
);
|
||||
const others = urls.filter((u) => !home.includes(u));
|
||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||
}
|
||||
@@ -64,15 +95,16 @@ async function main() {
|
||||
urls.forEach((u) => console.log(` - ${u}`));
|
||||
|
||||
// 2. Prepare pa11y-ci config
|
||||
const baseConfigPath = path.join(process.cwd(), '.pa11yci.json');
|
||||
let baseConfig: any = { defaults: {} };
|
||||
const baseConfigPath = path.join(process.cwd(), ".pa11yci.json");
|
||||
let baseConfig: Pa11yConfig = { defaults: {} };
|
||||
if (fs.existsSync(baseConfigPath)) {
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
|
||||
}
|
||||
|
||||
// Extract domain for cookie
|
||||
const urlObj = new URL(targetUrl);
|
||||
const domain = urlObj.hostname;
|
||||
// Extract domain for cookie (not currently used)
|
||||
// const urlObj = new URL(targetUrl);
|
||||
// domain is not used, so remove or keep if needed later? Linter says unused.
|
||||
// const domain = urlObj.hostname;
|
||||
|
||||
// Update config with discovered URLs and gatekeeper cookie
|
||||
const tempConfig = {
|
||||
@@ -80,19 +112,21 @@ async function main() {
|
||||
defaults: {
|
||||
...baseConfig.defaults,
|
||||
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
||||
runners: ['axe'],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
|
||||
runners: ["axe"],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"],
|
||||
chromeLaunchConfig: {
|
||||
...baseConfig.defaults?.chromeLaunchConfig,
|
||||
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
|
||||
...(process.env.CHROME_PATH
|
||||
? { executablePath: process.env.CHROME_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
timeout: 60000, // Increase timeout for slower pages
|
||||
},
|
||||
@@ -100,13 +134,13 @@ async function main() {
|
||||
};
|
||||
|
||||
// Create output directory
|
||||
const outputDir = path.join(process.cwd(), '.pa11yci');
|
||||
const outputDir = path.join(process.cwd(), ".pa11yci");
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempConfigPath = path.join(outputDir, 'config.temp.json');
|
||||
const reportPath = path.join(outputDir, 'report.json');
|
||||
const tempConfigPath = path.join(outputDir, "config.temp.json");
|
||||
const reportPath = path.join(outputDir, "report.json");
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
||||
|
||||
// 3. Execute pa11y-ci
|
||||
@@ -115,16 +149,16 @@ async function main() {
|
||||
|
||||
try {
|
||||
execSync(pa11yCommand, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// pa11y-ci exits with non-zero if issues are found, which is expected
|
||||
}
|
||||
|
||||
// 4. Summarize Results
|
||||
if (fs.existsSync(reportPath)) {
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||||
console.log(`\n📊 WCAG Audit Summary:\n`);
|
||||
|
||||
const summaryTable = Object.keys(reportData.results).map((url) => {
|
||||
@@ -136,31 +170,47 @@ async function main() {
|
||||
|
||||
if (Array.isArray(results)) {
|
||||
// pa11y action execution errors come as objects with a message but no type
|
||||
const actionErrors = results.filter((r: any) => !r.type && r.message).length;
|
||||
errors = results.filter((r: any) => r.type === 'error').length + actionErrors;
|
||||
warnings = results.filter((r: any) => r.type === 'warning').length;
|
||||
notices = results.filter((r: any) => r.type === 'notice').length;
|
||||
const actionErrors = results.filter(
|
||||
(r: Pa11yResult) => !r.type && r.message,
|
||||
).length;
|
||||
errors =
|
||||
results.filter((r: Pa11yResult) => r.type === "error").length +
|
||||
actionErrors;
|
||||
warnings = results.filter(
|
||||
(r: Pa11yResult) => r.type === "warning",
|
||||
).length;
|
||||
notices = results.filter(
|
||||
(r: Pa11yResult) => r.type === "notice",
|
||||
).length;
|
||||
}
|
||||
|
||||
// Clean URL for display
|
||||
const displayUrl = url.replace(targetUrl, '') || '/';
|
||||
const displayUrl = url.replace(targetUrl, "") || "/";
|
||||
|
||||
return {
|
||||
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
|
||||
URL:
|
||||
displayUrl.length > 50
|
||||
? displayUrl.substring(0, 47) + "..."
|
||||
: displayUrl,
|
||||
Errors: errors,
|
||||
Warnings: warnings,
|
||||
Notices: notices,
|
||||
Status: errors === 0 ? '✅' : '❌',
|
||||
Status: errors === 0 ? "✅" : "❌",
|
||||
};
|
||||
});
|
||||
|
||||
console.table(summaryTable);
|
||||
|
||||
const totalErrors = summaryTable.reduce((acc, curr) => acc + curr.Errors, 0);
|
||||
const totalErrors = summaryTable.reduce(
|
||||
(acc, curr) => acc + curr.Errors,
|
||||
0,
|
||||
);
|
||||
const totalPages = summaryTable.length;
|
||||
const cleanPages = summaryTable.filter((p) => p.Errors === 0).length;
|
||||
|
||||
console.log(`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`);
|
||||
console.log(
|
||||
`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`,
|
||||
);
|
||||
if (totalErrors > 0) {
|
||||
console.log(` Total Errors discovered: ${totalErrors}`);
|
||||
process.exitCode = 1;
|
||||
@@ -168,18 +218,22 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log(`\n✨ WCAG Audit completed!`);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`\n❌ Error during WCAG Audit:`);
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error instanceof AxiosError) {
|
||||
console.error(`Status: ${error.response?.status}`);
|
||||
console.error(`URL: ${error.config?.url}`);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
const errorBody = error instanceof Error ? error.message : String(error);
|
||||
console.error(errorBody);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up temp config file, keep report
|
||||
const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json');
|
||||
const tempConfigPath = path.join(
|
||||
process.cwd(),
|
||||
".pa11yci/config.temp.json",
|
||||
);
|
||||
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
76
test_output.txt
Normal file
76
test_output.txt
Normal file
@@ -0,0 +1,76 @@
|
||||
WARN Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||
WARN Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||
|
||||
> mb-grid-solutions.com@1.3.20 test /Volumes/Alpha SSD/Coding/mb-grid-solutions.com
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.0.18 /Volumes/Alpha SSD/Coding/mb-grid-solutions.com
|
||||
|
||||
❯ tests/api-contact.test.ts (0 test)
|
||||
✓ lib/env.test.ts (6 tests) 4ms
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
✓ tests/contact.test.tsx (3 tests) 148ms
|
||||
✓ tests/home.test.tsx (3 tests) 123ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL tests/api-contact.test.ts [ tests/api-contact.test.ts ]
|
||||
Error: Failed to resolve import "@payload-config" from "app/api/contact/route.ts". Does the file exist?
|
||||
Plugin: vite:import-analysis
|
||||
File: /Volumes/Alpha SSD/Coding/mb-grid-solutions.com/app/api/contact/route.ts:3:26
|
||||
1 | import { NextResponse } from "next/server";
|
||||
2 | import { getPayload } from "payload";
|
||||
3 | import configPromise from "@payload-config";
|
||||
| ^
|
||||
4 | import { getServerAppServices } from "@/lib/services/create-services.server";
|
||||
5 | import {
|
||||
❯ TransformPluginContext._formatLog ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28999:43
|
||||
❯ TransformPluginContext.error ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28996:14
|
||||
❯ normalizeUrl ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27119:18
|
||||
❯ ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27177:32
|
||||
❯ TransformPluginContext.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27145:4
|
||||
❯ EnvironmentPluginContainer.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28797:14
|
||||
❯ loadAndTransform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22670:26
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
|
||||
|
||||
|
||||
Test Files 1 failed | 3 passed (4)
|
||||
Tests 12 passed (12)
|
||||
Start at 14:27:09
|
||||
Duration 1.47s (transform 453ms, setup 660ms, import 1.35s, tests 275ms, environment 1.52s)
|
||||
|
||||
ELIFECYCLE Test failed. See above for more details.
|
||||
@@ -14,5 +14,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "tests", "tests_bak"]
|
||||
"exclude": ["node_modules", "tests", "tests_bak", "scripts"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@@ -16,11 +17,11 @@ export default defineConfig({
|
||||
},
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup.tsx'],
|
||||
alias: {
|
||||
'next/server': 'next/server.js',
|
||||
'@payload-config': new URL('./tests/__mocks__/payload-config.ts', import.meta.url).pathname,
|
||||
'@': new URL('./', import.meta.url).pathname,
|
||||
},
|
||||
alias: [
|
||||
{ find: 'next/server', replacement: 'next/server.js' },
|
||||
{ find: '@payload-config', replacement: fileURLToPath(new URL('./tests/__mocks__/payload-config.ts', import.meta.url)) },
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./', import.meta.url)) },
|
||||
],
|
||||
exclude: ['**/node_modules/**', '**/.next/**'],
|
||||
server: {
|
||||
deps: {
|
||||
|
||||
Reference in New Issue
Block a user