Compare commits
22 Commits
v1.3.2
...
v1.3.13-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| e0803715a7 | |||
| 8427d348dd | |||
| 7109b3c32e | |||
| 18717e7faf | |||
| 3478f0f42f | |||
| 836cc66334 | |||
| 9bb2186f7a | |||
| 55123132f4 | |||
| bf1dfe4015 | |||
| f03e417eb7 | |||
| e0312aeba9 | |||
| d5c0b91c97 | |||
| 155918e265 | |||
| fb5b55f1dd | |||
| 9f00828b80 | |||
| bb0debe29d | |||
| f2e855fff9 | |||
| b603490683 | |||
| 68075edb77 | |||
| 217e58e5f4 | |||
| e7cf7f8946 | |||
| 1a1196b740 |
@@ -74,6 +74,7 @@ AUTH_COOKIE_NAME=mintel_gatekeeper_session
|
||||
|
||||
# Sentry / Glitchtip (Error Tracking)
|
||||
SENTRY_DSN=
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Gotify (In-App Notifications)
|
||||
# GOTIFY_URL=
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Build & Deploy
|
||||
# Infrastructure Maintenance: Production stabilization and email adapter fix triggered on 2026-04-12.
|
||||
name: 🚀 Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -36,13 +37,6 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🧹 Maintenance (High Density Cleanup)
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Purging old build layers and dangling images..."
|
||||
docker image prune -f
|
||||
docker builder prune -f --filter "until=6h"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -187,6 +181,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: 🐳 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: 🔐 Registry Login
|
||||
@@ -200,6 +196,8 @@ jobs:
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
tags: registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mb-grid-solutions:buildcache
|
||||
@@ -223,7 +221,7 @@ jobs:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
|
||||
# Secrets mapping (Database & CMS)
|
||||
PAYLOAD_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_SECRET) || secrets.PAYLOAD_SECRET || secrets.DIRECTUS_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||
PAYLOAD_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_SECRET) || secrets.PAYLOAD_SECRET || secrets.MINTEL_PRIVATE_TOKEN || secrets.DIRECTUS_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||
DATABASE_URI: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DATABASE_URI) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DATABASE_URI) || secrets.DATABASE_URI || vars.DATABASE_URI }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB || vars.POSTGRES_DB || 'payload' }}
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER || vars.POSTGRES_USER || 'directus' }}
|
||||
@@ -275,12 +273,14 @@ jobs:
|
||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
||||
|
||||
# Generate Environment File
|
||||
echo "🔍 Verifying Sentry DSN presence..."
|
||||
cat > .env.deploy << EOF
|
||||
# Generated by CI - $TARGET
|
||||
IMAGE_TAG=$IMAGE_TAG
|
||||
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN
|
||||
SENTRY_DSN=$SENTRY_DSN
|
||||
NEXT_PUBLIC_SENTRY_DSN=$SENTRY_DSN
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
@@ -341,11 +341,24 @@ jobs:
|
||||
|
||||
# Apply Payload Migrations using the target app container's programmatic endpoint
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '→ Waiting for DB and Running Payload Migrations...' && \
|
||||
for i in {1..5}; do \
|
||||
for i in {1..15}; do \
|
||||
echo \"Attempt \$i...\"; \
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T mb-grid-app sh -c 'curl -s -f -X POST -H \"Authorization: Bearer \$PAYLOAD_SECRET\" http://localhost:3000/api/payload/migrate \
|
||||
|| { echo \"HTTP error or DB not ready.\"; exit 1; }' && { echo '✅ Migrations successful!'; break; } \
|
||||
|| { if [ \$i -eq 5 ]; then echo '❌ Migration failed after 5 attempts!'; exit 1; else echo '⏳ Retrying in 5s...'; sleep 5; fi; }; \
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T mb-grid-app sh -c 'curl -s -i -X POST -H \"Authorization: Bearer \$PAYLOAD_SECRET\" http://localhost:3000/api/payload/migrate' > /tmp/migrate_res 2>&1; \
|
||||
if grep -q \"200 OK\" /tmp/migrate_res; then \
|
||||
echo \"✅ Migrations successful!\"; \
|
||||
cat /tmp/migrate_res; \
|
||||
break; \
|
||||
else \
|
||||
echo \"❌ Attempt \$i failed. Response:\"; \
|
||||
cat /tmp/migrate_res; \
|
||||
if [ \$i -eq 15 ]; then \
|
||||
echo \"❌ Migration failed after 15 attempts! Dumping app logs...\"; \
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' logs --tail 100 mb-grid-app; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo \"⏳ Retrying in 5s...\"; \
|
||||
sleep 5; \
|
||||
fi; \
|
||||
done"
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
|
||||
26
.htmlvalidate.json
Normal file
26
.htmlvalidate.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": ["html-validate:recommended", "html-validate:document"],
|
||||
"rules": {
|
||||
"require-sri": "off",
|
||||
"meta-refresh": "off",
|
||||
"heading-level": "warn",
|
||||
"no-trailing-whitespace": "off",
|
||||
"wcag/h37": "warn",
|
||||
"no-inline-style": "off",
|
||||
"svg-focusable": "off",
|
||||
"attribute-boolean-style": "off",
|
||||
"attr-case": "off",
|
||||
"void-style": "off",
|
||||
"no-implicit-button-type": "off",
|
||||
"unique-landmark": "off",
|
||||
"long-title": "off",
|
||||
"valid-id": "off",
|
||||
"element-required-attributes": "off",
|
||||
"attribute-empty-style": "off",
|
||||
"element-permitted-content": "off",
|
||||
"element-required-content": "off",
|
||||
"element-permitted-parent": "off",
|
||||
"no-implicit-close": "off",
|
||||
"close-order": "off"
|
||||
}
|
||||
}
|
||||
21
.pa11yci.json
Normal file
21
.pa11yci.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"defaults": {
|
||||
"standard": "WCAG2AA",
|
||||
"runners": ["axe", "htmlcs"],
|
||||
"ignore": [],
|
||||
"timeout": 50000,
|
||||
"wait": 1000,
|
||||
"chromeLaunchConfig": {
|
||||
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
|
||||
},
|
||||
"threshold": 25
|
||||
},
|
||||
"urls": [
|
||||
"http://localhost:3000/",
|
||||
"http://localhost:3000/ueber-uns",
|
||||
"http://localhost:3000/kontakt",
|
||||
"http://localhost:3000/impressum",
|
||||
"http://localhost:3000/datenschutz",
|
||||
"http://localhost:3000/agb"
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default function AGB() {
|
||||
const filePath = path.join(process.cwd(), "context/agbs.md");
|
||||
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)
|
||||
@@ -74,7 +74,7 @@ export default function AGB() {
|
||||
<p className="text-slate-500 font-medium">{stand}</p>
|
||||
</div>
|
||||
<a
|
||||
href="/assets/AGB MB Grid 1-2026.pdf"
|
||||
href="/assets/AVB MB Grid 4-2026.pdf"
|
||||
download
|
||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||
>
|
||||
@@ -91,7 +91,24 @@ export default function AGB() {
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{section.content.map((paragraph, pIndex) => (
|
||||
<p key={pIndex}>{paragraph}</p>
|
||||
<p key={pIndex}>
|
||||
{paragraph.split(/(\[.*?\]\(.*?\))/g).map((part, i) => {
|
||||
const match = part.match(/\[(.*?)\]\((.*?)\)/);
|
||||
if (match) {
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={match[2]}
|
||||
className="text-primary hover:underline font-medium"
|
||||
download={match[2].endsWith(".pdf")}
|
||||
>
|
||||
{match[1]}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
})}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ConfirmationMessage,
|
||||
} from "@mintel/mail";
|
||||
import React from "react";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const services = getServerAppServices();
|
||||
@@ -77,11 +78,33 @@ export async function POST(req: Request) {
|
||||
services.errors.captureException(payloadError, { phase: "payload_save" });
|
||||
}
|
||||
|
||||
// 2. Email sending via Payload (which uses configured nodemailer)
|
||||
// 2. Email sending via standalone Nodemailer (bypassing Payload adapter)
|
||||
try {
|
||||
const { config } = await import("@/lib/config");
|
||||
const clientName = "MB Grid Solutions";
|
||||
|
||||
// Robust recipient resolution
|
||||
const recipients = Array.isArray(config.mail.recipients)
|
||||
? config.mail.recipients.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const to =
|
||||
recipients.length > 0
|
||||
? recipients.join(",")
|
||||
: process.env.CONTACT_RECIPIENT || "info@mb-grid-solutions.com";
|
||||
|
||||
logger.info("Instantiating standalone nodemailer transport");
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
auth: {
|
||||
user: config.mail.user,
|
||||
pass: config.mail.pass,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("Preparing to send notification email", { to, host: config.mail.host });
|
||||
|
||||
// 2a. Notification to MB Grid
|
||||
const notificationHtml = await render(
|
||||
React.createElement(ContactFormNotification, {
|
||||
@@ -92,19 +115,23 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
);
|
||||
|
||||
await payload.sendEmail({
|
||||
from: config.mail.from,
|
||||
to:
|
||||
config.mail.recipients.join(",") ||
|
||||
process.env.CONTACT_RECIPIENT ||
|
||||
"info@mb-grid-solutions.com",
|
||||
replyTo: email,
|
||||
subject: `Kontaktanfrage von ${name}`,
|
||||
html: notificationHtml,
|
||||
});
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: config.mail.from,
|
||||
to,
|
||||
replyTo: email,
|
||||
subject: `Kontaktanfrage von ${name}`,
|
||||
html: notificationHtml,
|
||||
});
|
||||
logger.info("Notification email sent successfully", { messageId: info?.messageId });
|
||||
} catch (notifyError) {
|
||||
logger.error("Failed to send notification email", { error: notifyError });
|
||||
throw notifyError; // Re-throw to be caught by the outer SMTP catch
|
||||
}
|
||||
|
||||
// 2b. Confirmation to the User
|
||||
try {
|
||||
logger.info("Preparing to send confirmation email", { to: email });
|
||||
const confirmationHtml = await render(
|
||||
React.createElement(ConfirmationMessage, {
|
||||
name,
|
||||
@@ -112,16 +139,17 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
);
|
||||
|
||||
await payload.sendEmail({
|
||||
const info = await transporter.sendMail({
|
||||
from: config.mail.from,
|
||||
to: email,
|
||||
subject: `Ihre Kontaktanfrage bei ${clientName}`,
|
||||
html: confirmationHtml,
|
||||
});
|
||||
logger.info("Confirmation email sent successfully", { messageId: info?.messageId });
|
||||
} catch (confirmError) {
|
||||
logger.warn(
|
||||
"Failed to send confirmation email, but notification was sent",
|
||||
{ error: confirmError },
|
||||
{ error: confirmError instanceof Error ? confirmError.message : String(confirmError) },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
Liefer- und Zahlungsbedingungen
|
||||
Stand Januar 2026
|
||||
|
||||
|
||||
1. Allgemeines
|
||||
Diese Liefer- und Zahlungsbedingungen (L&Z) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere L&Z gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren L&Z abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere L&Z gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen.
|
||||
|
||||
Nebenabreden, Vorbehalte, Änderungen, Ergänzungen usw. bedürfen zu ihrer Wirksamkeit unserer schriftlichen Bestätigung.
|
||||
|
||||
Hinweise auf die Geltung gesetzlicher Vorschriften haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen L&Z nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin.
|
||||
|
||||
2. Angebote
|
||||
Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freibleibend; die Bestellung des Kunden ist als Angebot gemäß § 145 BGB zu qualifizieren.
|
||||
|
||||
3. Preise
|
||||
Die Preise gelten für den in unseren Angeboten und Auftragsbestätigungen aufgeführten Leistungs- und Lieferumfang. Mehrleistungen werden gesondert berechnet. Die Hohlpreise verstehen sich in Euro zuzüglich Metallzuschlag, gegebenenfalls Verpackung, auftragsspezifischer Schnittkosten und der gesetzlichen Mehrwertsteuer.
|
||||
|
||||
4. Metallnotierung
|
||||
Basis zur Kupferabrechnung ist die Notierung „LME Copper official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
|
||||
|
||||
Basis zur Aluminiumabrechnung ist die Notierung „LME Aluminium official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite www.westmetall.com entnehmen. Die Prämienzuschläge können stark variieren und MB Grid Solutions & Services behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
|
||||
|
||||
5. Metallzahl
|
||||
Die von uns ausgewiesene Metallzahl ist eine rein kaufmännische Berechnungsgröße für den Metallinhalt, die in die Berechnung des Gesamtpreises eines Kabels eingeht. Damit entsprechen wir Ihrem Wunsch eine Vergleichbarkeit in ihrem System auf Hohlpreisbasis zu ermöglichen. Die Metallzahl gibt damit nicht das Gewicht des tatsächlich im Kabel enthaltenen Leitermetalls an. Sie ist ein rein kalkulatorischer Berechnungsfaktor, der jedoch keine unmittelbaren Rückschlüsse auf die im Kabel verwendeten Kupfer- bzw. Aluminiummengen zulässt. Wir weisen ausdrücklich darauf hin, final nur den Vollpreis für Vergleichszwecke heranzuziehen. Soweit Sie es wünschen andere Metallzahlen zu Grunde zu legen, sind wir gerne dazu bereit, das Angebot in den Bestandteilen umzurechnen. Bei jeglicher Änderung bleibt aber der Vollpreis der gleiche Betrag.
|
||||
|
||||
6. Auftragsänderung / Auftragsstorno
|
||||
Nach Auftragsbestätigung werden Änderungen an bestätigten Aufträgen nur nach Prüfung und gesonderter ausdrücklicher Zustimmung durch uns akzeptiert. Wir behalten uns bei allen Auftragsänderungen das Recht vor, einen durch die Änderung entstandenen Mehraufwand, wie z.B. Bearbeitungskosten oder Entsorgungskosten in Rechnung zu stellen.
|
||||
|
||||
7. Eigentumsvorbehalt
|
||||
Wir behalten uns an den von uns gelieferten Waren – nachfolgend: Vorbehaltsware – bis zur vollständigen Begleichung aller unserer Forderungen aus den Geschäftsbeziehungen mit dem Besteller, das Eigentum vor. Der Eigentumsvorbehalt bleibt auch dann bestehen, wenn einzelne Forderungen in eine laufende Rechnung aufgenommen werden (Kontokorrentvorbehalt).
|
||||
|
||||
8. Zahlungsbedingungen | Aufrechnung | Zurückbehaltungsrechte
|
||||
Unsere Rechnungen sind 10 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Rechnungsstellung bzw. Datum ist grundsätzlich der Tag der Übergabe an den Spediteur soweit wir aus unseren deutschen Lägern liefern. Ansonsten gilt bei Direktimporten der Tag der Verzollung, der zeitnah zum Anliefertag liegt. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
|
||||
|
||||
9. Liefervorbehalt | Teillieferungen
|
||||
Sämtliche Lieferzusagen unsererseits stehen, sofern nichts anderes ausdrücklich schriftlich vereinbart ist, unter dem Vorbehalt der richtigen und rechtzeitigen Belieferung durch unsere Produzenten. Wir behalten uns jederzeit Teillieferungen vor. Darüber hinaus behalten wir uns branchenübliche Über- oder Unterlieferungen bis zu 10 % der bestellten Menge vor.
|
||||
|
||||
10. Lieferfristen und Liefertermine
|
||||
Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern wir verbindliche Lieferfristen aus Gründen, die wir nicht zu vertreten haben, nicht einhalten können (Nichtverfügbarkeit der Leistung), werden wir den Besteller hierüber unverzüglich informieren und gleichzeitig die voraussichtliche, neue Lieferfrist mitteilen. Ist die Leistung auch innerhalb der neuen Lieferfrist nicht verfügbar, sind wir berechtigt, ganz oder teilweise vom Vertrag zurückzutreten. Eine bereits erbrachte Gegenleistung des Bestellers werden wir unverzüglich erstatten. Nichtverfügbarkeit der Leistung liegt beispielsweise vor bei nicht rechtzeitiger Selbstbelieferung durch unseren Zulieferer, wenn wir ein kongruentes Deckungsgeschäft abgeschlossen haben, bei sonstigen Störungen in der Lieferkette etwa aufgrund höherer Gewalt oder wenn wir im Einzelfall zur Beschaffung nicht verpflichtet sind.
|
||||
|
||||
Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Käufer erforderlich.
|
||||
|
||||
Die gesetzlichen Rechte bleiben im Übrigen unberührt.
|
||||
|
||||
Fixgeschäfte setzen die ausdrückliche schriftliche Bezeichnung als solche voraus. Ansonsten ist der Besteller stets verpflichtet, uns schriftlich eine angemessene Nachfrist zu setzen, wenn von uns zugesagte Termine und/ oder Fristen nicht einhalten werden. Wird auch die Nachfrist nicht eingehalten, ist der Besteller berechtigt, vom Vertrag zurückzutreten. Im Fall höherer Gewalt und/oder sonstiger von uns nicht vorhersehbarer außergewöhnlicher und/oder unverschuldeter Umstände, auch wenn sie bei unserem Vorlieferanten eintreten, verlängert sich eine von uns zugesagte Lieferfrist bis zur Behebung des vorerwähnten Ereignisses. Ist dieser Zeitpunkt nicht überblickbar, sind sowohl der Besteller als auch wir berechtigt, von dem abgeschlossenen Vertrag zurückzutreten. In diesem Fall sind beiderseits Schadensersatzansprüche ausgeschlossen. Wir verpflichten uns, bei Bekanntwerden vorerwähnter Umstände den Besteller hiervon unverzüglich zu benachrichtigen.
|
||||
|
||||
Ist die Einhaltung eines Termins davon abhängig, dass uns seitens des Bestellers bestimmte Angaben und/oder Pläne, Freigabeerklärungen oder ähnliches erteilt werden, beginnt die Lieferfrist erst von dem Zeitpunkt an zu laufen, zu dem uns die vollständigen Angaben des Bestellers schriftlich vorliegen. Wird die Anlieferung auf Wunsch des Bestellers über den vertraglich vorgesehenen Zeitpunkt hinausgeschoben, kann von uns beginnend mit einer Frist von frühestens 10 Werktagen nach Anzeige der Versandbereitschaft dem Besteller ein Lagergeld in Höhe von 2 % des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt berechnet werden.
|
||||
|
||||
11. Abrufaufträge
|
||||
Wird uns ein Abrufauftrag erteilt und werden über die Abruftermine keine gesonderten schriftlichen Vereinbarungen getroffen, ist der Besteller verpflichtet, uns die einzelnen Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei uns und Auslieferung mindestens 14 Werktage und die letzte Auslieferung spätestens 90 Tage nach unserer Auftragsbestätigung liegt.
|
||||
|
||||
12. Maß- und Gewichtsangaben
|
||||
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lässt, begründen keine Mängelhaftungsansprüche des Bestellers.
|
||||
|
||||
13. Gefahrübergang und -tragung
|
||||
Die Lieferung erfolgt DAP frei Bestimmungsort Deutschland, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist.
|
||||
|
||||
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen für Einlagerungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H. von 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. – mangels einer Lieferfrist – mit der Mitteilung der Versandbereitschaft der Ware.
|
||||
|
||||
Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt; die Pauschale ist aber auf weitergehende Geldansprüche anzurechnen. Dem Besteller bleibt der Nachweis gestattet, dass uns überhaupt kein oder nur ein wesentlich geringerer Schaden als vorstehende Pauschale entstanden ist. Rücksendungen an uns, die nicht vorher von uns schriftlich bestätigt worden sind, erfolgen auf alleinige Gefahr des Bestellers.
|
||||
|
||||
14. Mängelhaftung
|
||||
Wir haften nur dann für die Einhaltung objektiver Anforderungen an der Ware, wenn und soweit zwischen dem Besteller und uns keine Beschaffenheitsvereinbarung getroffen wurde. Die einzuhaltenden subjektiven Anforderungen gehen den einzuhaltenden objektiven Anforderungen vor. Im Zweifel ergeben sich die vereinbarten Anforderungen an die Ware aus dem von uns bereitgestellten Datenblatt. Einzelne, nicht immer auszuschließende marginale Abweichungen, dürfen durch Reparaturen, wie zum Beispiel Mantelmanschetten nachgebessert werden.
|
||||
|
||||
Jedwede Mängelhaftungsansprüche des Bestellers setzen voraus, dass dieser die ihm übersandte Ware unverzüglich, d. h. in der Regel sofort bei Anlieferung (noch in Anwesenheit des Transporteurs) auf ihre ordnungsgemäße Beschaffenheit hin überprüft und uns zu verzeichnende sichtbare Mängel unmittelbar nach Erhalt der Ware und verdeckte Mängel unmittelbar nach deren Feststellung schriftlich mitteilt. Soweit ein rechtzeitig gerügter, nicht nur unerheblicher Mangel der Kaufsache vorliegt, sind wir nach unserer Wahl zur Mangelbeseitigung oder zur Ersatzlieferung (Nacherfüllung) berechtigt.
|
||||
|
||||
Wir übernehmen im Rahmen der Nacherfüllung in keinem Fall Ein- oder Ausbaukosten, wenn und soweit die Mangelhaftigkeit der Ware zum Zeitpunkt des Einbaus dem Besteller bekannt oder grob fahrlässig unbekannt geblieben ist. Sind wir zur Mangelbeseitigung/Ersatzlieferung nicht bereit oder nicht in der Lage oder verzögert sich diese über angemessene Fristen hinaus aus Gründen, die wir zu vertreten haben, oder schlägt sie in sonstiger Weise fehl, so ist der Besteller nach seiner Wahl berechtigt, vom Vertrag zurückzutreten oder eine entsprechende Minderung des Kaufpreises zu verlangen.
|
||||
|
||||
Weitergehende Ansprüche des Bestellers, gleich aus welchem Rechtsgrund, sind nach näherer Maßgabe der Regelungen in nachstehender Ziffer 15 ausgeschlossen bzw. beschränkt. Die Verjährungsfristen für Mängelhaftungsansprüche beträgt 24 Monate ab Übergabe der Ware.
|
||||
|
||||
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht. Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Graben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein. Dies gilt auch bei der Annahme der Ware, wo offensichtliche Beschädigungen direkt zu kommunizieren sind. Spätere Ansprüche nach Akzeptanz einer einwandfreien Belieferung sind detailliert zu beweisen.
|
||||
|
||||
15. Schadenersatz | Gesamthaftung
|
||||
Wir haften unbeschränkt nur für Vorsatz und grobe Fahrlässigkeit sowie für Schäden aus einer Verletzung von Leben, Körper oder Gesundheit, die auf mindestens fahrlässiger Pflichtverletzung unsererseits oder unserer gesetzlichen Vertreter oder Erfüllungsgehilfen beruhen; ebenso haften wir unbeschränkt im Fall von uns übernommenen bzw. abgegebenen Garantien und Zusicherungen, sofern ein davon umfasster Mangel unsere Haftung auslöst sowie im Fall einer Haftung nach dem Produkthaftungsgesetz oder sonstigen Gefährdungshaftungstatbeständen. Im Fall sonstiger schuldhafter Verletzung wesentlicher Vertragspflichten („Kardinalpflichten“) ist unsere verbleibende Haftung auf den vertragstypischen vorhersehbaren Schaden beschränkt. Mangelfolgeschäden sowie entgangener Gewinn schließen wir grundsätzlich aus.
|
||||
|
||||
16. Kabeltrommeln
|
||||
Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen.
|
||||
|
||||
17. Technische Beratungsdienstleistungen
|
||||
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. Sonstiges
|
||||
Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG). Gerichtsstand ist nach unserer Wahl Stuttgart, der Erfüllungsort der Lieferverpflichtung oder das für den Sitz des Bestellers zuständige Gericht, sofern der Besteller Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen ist oder keinen allgemeinen Gerichtsstand im Inland hat.
|
||||
|
||||
Mit der Veröffentlichung der vorliegenden L&Z im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
|
||||
|
||||
Remshalden, 22.1.2026
|
||||
88
context/avbs.md
Normal file
88
context/avbs.md
Normal file
@@ -0,0 +1,88 @@
|
||||
Allgemeine Verkaufsbedingungen (AVB)
|
||||
Stand April 2026
|
||||
|
||||
1. Allgemeines
|
||||
Diese Allgemeinen Verkaufsbedingungen (AVB) der MB Grid Solutions & Services gelten ausschließlich; entgegenstehende oder von unseren Bedingungen abweichende Bedingungen des Kunden erkennen wir nicht an, es sei denn, wir hätten ausdrücklich schriftlich ihrer Geltung zugestimmt. Unsere AVB gelten auch dann, wenn wir in Kenntnis entgegenstehender oder von unseren AVB abweichender Bedingungen des Bestellers die Lieferung an diesen vorbehaltlos ausführen. Unsere AVB gelten nur gegenüber Unternehmern im Sinn von § 310 Abs. 1 BGB sowie juristischen Personen des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen.
|
||||
|
||||
Nebenabreden, Vorbehalte, Änderungen, Ergänzungen usw. bedürfen zu ihrer Wirksamkeit unserer schriftlichen Bestätigung.
|
||||
|
||||
Hinweise auf die Geltung gesetzlicher Vorschriften 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. Bezüglich Beratungsleistungen weisen wir ausdrücklich auf Punkt 17 hin.
|
||||
|
||||
2. Angebote
|
||||
Sofern nicht ausdrücklich als bindend bezeichnet, sind unsere Angebote freibleibend; die Bestellung des Kunden ist als Angebot gemäß § 145 BGB zu qualifizieren.
|
||||
|
||||
3. Preise
|
||||
Die Preise gelten für den in unseren Angeboten und Auftragsbestätigungen aufgeführten Leistungs- und Lieferumfang. Mehrleistungen werden gesondert berechnet. Die Hohlpreise verstehen sich in Euro zuzüglich Metallzuschlag, gegebenenfalls Verpackung, auftragsspezifischer Schnittkosten und der gesetzlichen Mehrwertsteuer.
|
||||
|
||||
4. Metallnotierung
|
||||
Basis zur Kupferabrechnung ist die Notierung „LME Copper official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann aktuellen von uns benannten Kupfer-Prämie.
|
||||
|
||||
Basis zur Aluminiumabrechnung ist die Notierung „LME Aluminium official price cash offer“, Durchschnitt des Liefervormonats zuzüglich der dann von uns benannten Aluminium-Prämie. USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können Sie der Web-Seite www.westmetall.com entnehmen. Die Prämienzuschläge können stark variieren und MB Grid Solutions & Services behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
|
||||
|
||||
5. Metallzahl
|
||||
Die von uns ausgewiesene Metallzahl ist eine rein kaufmännische Berechnungsgröße für den Metallinhalt, die in die Berechnung des Gesamtpreises eines Kabels eingeht. Damit entsprechen wir Ihrem Wunsch eine Vergleichbarkeit in ihrem System auf Hohlpreisbasis zu ermöglichen. Die Metallzahl gibt damit nicht das Gewicht des tatsächlich im Kabel enthaltenen Leitermetalls an. Sie ist ein rein kalkulatorischer Berechnungsfaktor, der jedoch keine unmittelbaren Rückschlüsse auf die im Kabel verwendeten Kupfer- bzw. Aluminiummengen zulässt. Wir weisen ausdrücklich darauf hin, final nur den Vollpreis für Vergleichszwecke heranzuziehen. Soweit Sie es wünschen andere Metallzahlen zu Grunde zu legen, sind wir gerne dazu bereit, das Angebot in den Bestandteilen umzurechnen. Bei jeglicher Änderung bleibt aber der Vollpreis der gleiche Betrag.
|
||||
|
||||
6. Auftragsänderung / Auftragsstorno
|
||||
Nach Auftragsbestätigung werden Änderungen an bestätigten Aufträgen nur nach Prüfung und gesonderter ausdrücklicher Zustimmung durch uns akzeptiert. Wir behalten uns bei allen Auftragsänderungen das Recht vor, einen durch die Änderung entstandenen Mehraufwand, wie z.B. Bearbeitungskosten oder Entsorgungskosten in Rechnung zu stellen.
|
||||
|
||||
7. Eigentumsvorbehalt
|
||||
Wir behalten uns an den von uns gelieferten Waren – nachfolgend: Vorbehaltsware – bis zur vollständigen Begleichung aller unserer Forderungen aus den Geschäftsbeziehungen mit dem Besteller, das Eigentum vor. Der Eigentumsvorbehalt bleibt auch dann bestehen, wenn einzelne Forderungen in eine laufende Rechnung aufgenommen werden (Kontokorrentvorbehalt).
|
||||
|
||||
8. Zahlungsbedingungen | Aufrechnung | Zurückbehaltungsrechte
|
||||
Unsere Rechnungen sind 10 Tage nach Rechnungsdatum ohne jeden Abzug zahlbar. Rechnungsstellung bzw. Datum ist grundsätzlich der Tag der Übergabe an den Spediteur soweit wir aus unseren deutschen Lägern liefern. Ansonsten gilt bei Direktimporten der Tag der Verzollung, der zeitnah zum Anliefertag liegt. Bei Nichteinhaltung der vereinbarten Zahlungsbedingungen sind wir berechtigt, Zinsen in Höhe von 7 %-Punkten über dem Basiszinssatz zu berechnen; das Recht zur Geltendmachung weitergehender Schäden, insbesondere nachgewiesener höherer Zinsen, bleibt hiervon unberührt.
|
||||
|
||||
9. Liefervorbehalt | Teillieferungen
|
||||
Sämtliche Lieferzusagen unsererseits stehen, sofern nichts anderes ausdrücklich schriftlich vereinbart ist, unter dem Vorbehalt der richtigen und rechtzeitigen Belieferung durch unsere Produzenten. Wir behalten uns jederzeit Teillieferungen vor. Darüber hinaus behalten wir uns branchenübliche Über- oder Unterlieferungen bis zu 10 % der bestellten Menge vor.
|
||||
|
||||
10. Lieferfristen und Liefertermine
|
||||
Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern wir verbindliche Lieferfristen aus Gründen, die wir nicht zu vertreten haben, nicht einhalten können (Nichtverfügbarkeit der Leistung), werden wir den Besteller hierüber unverzüglich informieren und gleichzeitig die voraussichtliche, neue Lieferfrist mitteilen. Ist die Leistung auch innerhalb der neuen Lieferfrist nicht verfügbar, sind wir berechtigt, ganz oder teilweise vom Vertrag zurückzutreten. Eine bereits erbrachte Gegenleistung des Bestellers werden wir unverzüglich erstatten. Nichtverfügbarkeit der Leistung liegt beispielsweise vor bei nicht rechtzeitiger Selbstbelieferung durch unseren Zulieferer, wenn wir ein kongruentes Deckungsgeschäft abgeschlossen haben, bei sonstigen Störungen in der Lieferkette etwa aufgrund höherer Gewalt oder wenn wir im Einzelfall zur Beschaffung nicht verpflichtet sind.
|
||||
|
||||
Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Käufer erforderlich.
|
||||
|
||||
Die gesetzlichen Rechte bleiben im Übrigen unberührt.
|
||||
|
||||
Fixgeschäfte setzen die ausdrückliche schriftliche Bezeichnung als solche voraus. Ansonsten ist der Besteller stets verpflichtet, uns schriftlich eine angemessene Nachfrist zu setzen, wenn von uns zugesagte Termine und/ oder Fristen nicht einhalten werden. Wird auch die Nachfrist nicht eingehalten, ist der Besteller berechtigt, vom Vertrag zurückzutreten. Im Fall höherer Gewalt und/oder sonstiger von uns nicht vorhersehbarer außergewöhnlicher und/oder unverschuldeter Umstände, auch wenn sie bei unserem Vorlieferanten eintreten, verlängert sich eine von uns zugesagte Lieferfrist bis zur Behebung des vorerwähnten Ereignisses. Ist dieser Zeitpunkt nicht überblickbar, sind sowohl der Besteller als auch wir berechtigt, von dem abgeschlossenen Vertrag zurückzutreten. In diesem Fall sind beiderseits Schadensersatzansprüche ausgeschlossen. Wir verpflichten uns, bei Bekanntwerden vorerwähnter Umstände den Besteller hiervon unverzüglich zu benachrichtigen.
|
||||
|
||||
Ist die Einhaltung eines Termins davon abhängig, dass uns seitens des Bestellers bestimmte Angaben und/oder Pläne, Freigabeerklärungen oder ähnliches erteilt werden, beginnt die Lieferfrist erst von dem Zeitpunkt an zu laufen, zu dem uns die vollständigen Angaben des Bestellers schriftlich vorliegen. Wird die Anlieferung auf Wunsch des Bestellers über den vertraglich vorgesehenen Zeitpunkt hinausgeschoben, kann von uns beginnend mit einer Frist von frühestens 10 Werktagen nach Anzeige der Versandbereitschaft dem Besteller ein Lagergeld in Höhe von 2 % des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt berechnet werden.
|
||||
|
||||
11. Abrufaufträge
|
||||
Wird uns ein Abrufauftrag erteilt und werden über die Abruftermine keine gesonderten schriftlichen Vereinbarungen getroffen, ist der Besteller verpflichtet, uns die einzelnen Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei uns und Auslieferung mindestens 14 Werktage und die letzte Auslieferung spätestens 90 Tage nach unserer Auftragsbestätigung liegt.
|
||||
|
||||
12. Maß- und Gewichtsangaben
|
||||
Alle Angaben über Durchmesser, Gewicht, technische Gestaltung, Herstellung und Umfang der von uns zu liefernden Ware stehen unter dem Vorbehalt der Abweichung innerhalb der handelsüblichen zulässigen Toleranzen. Darüber hinaus behalten wir uns Änderungen, die einer technischen Verbesserung dienen, jederzeit vor. Farbabweichungen und/oder Abweichungen in der äußeren Beschaffenheit der von uns zu liefernden Ware, die jedoch deren Qualität und technische Wirksamkeit unbeeinflusst lässt, begründen keine Mängelhaftungsansprüche des Bestellers.
|
||||
|
||||
13. Gefahrübergang und -tragung
|
||||
Die Lieferung erfolgt DAP frei Bestimmungsort Deutschland, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist.
|
||||
|
||||
Wird die bestellte Ware von uns versandbereit gestellt und/oder verzögert sich die Versendung und/oder der Abruf aus Gründen, die vom Besteller zu vertreten sind, sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen für Einlagerungen zu verlangen. Hierfür berechnen wir eine pauschale Entschädigung i.H. von 2% des Rechnungsbetrages für jeden angefangenen Monat, maximal jedoch 10 % insgesamt beginnend mit der Lieferfrist bzw. – mangels einer Lieferfrist – mit der Mitteilung der Versandbereitschaft der Ware.
|
||||
|
||||
Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt; die Pauschale ist aber auf weitergehende Geldansprüche anzurechnen. Dem Besteller bleibt der Nachweis gestattet, dass uns überhaupt kein oder nur ein wesentlich geringerer Schaden als vorstehende Pauschale entstanden ist. Rücksendungen an uns, die nicht vorher von uns schriftlich bestätigt worden sind, erfolgen auf alleinige Gefahr des Bestellers.
|
||||
|
||||
14. Mängelhaftung
|
||||
Wir haften nur dann für die Einhaltung objektiver Anforderungen an der Ware, wenn und soweit zwischen dem Besteller und uns keine Beschaffenheitsvereinbarung getroffen wurde. Die einzuhaltenden subjektiven Anforderungen gehen den einzuhaltenden objektiven Anforderungen vor. Im Zweifel ergeben sich die vereinbarten Anforderungen an die Ware aus dem von uns bereitgestellten Datenblatt. Einzelne, nicht immer auszuschließende marginale Abweichungen, dürfen durch Reparaturen, wie zum Beispiel Mantelmanschetten nachgebessert werden.
|
||||
|
||||
Jedwede Mängelhaftungsansprüche des Bestellers setzen voraus, dass dieser die ihm übersandte Ware unverzüglich, d. h. in der Regel sofort bei Anlieferung (noch in Anwesenheit des Transporteurs) auf ihre ordnungsgemäße Beschaffenheit hin überprüft und uns zu verzeichnende sichtbare Mängel unmittelbar nach Erhalt der Ware und verdeckte Mängel unmittelbar nach deren Feststellung schriftlich mitteilt. Soweit ein rechtzeitig gerügter, nicht nur unerheblicher Mangel der Kaufsache vorliegt, sind wir nach unserer Wahl zur Mangelbeseitigung oder zur Ersatzlieferung (Nacherfüllung) berechtigt.
|
||||
|
||||
Wir übernehmen im Rahmen der Nacherfüllung in keinem Fall Ein- oder Ausbaukosten, wenn und soweit die Mangelhaftigkeit der Ware zum Zeitpunkt des Einbaus dem Besteller bekannt oder grob fahrlässig unbekannt geblieben ist. Sind wir zur Mangelbeseitigung/Ersatzlieferung nicht bereit oder nicht in der Lage oder verzögert sich diese über angemessene Fristen hinaus aus Gründen, die wir zu vertreten haben, oder schlägt sie in sonstiger Weise fehl, so ist der Besteller nach seiner Wahl berechtigt, vom Vertrag zurückzutreten oder eine entsprechende Minderung des Kaufpreises zu verlangen.
|
||||
|
||||
Weitergehende Ansprüche des Bestellers, gleich aus welchem Rechtsgrund, sind nach näherer Maßgabe der Regelungen in nachstehender Ziffer 15 ausgeschlossen bzw. beschränkt. Die Verjährungsfristen für Mängelhaftungsansprüche beträgt 24 Monate ab Übergabe der Ware.
|
||||
|
||||
Sollte es bei einer Mängelrüge zu unterschiedlichen Meinungen bezüglich des Kabelschaden kommen, gilt hier im Zweifelsfall nur die Expertise des VDE-Instituts selbst. Andere, auch akkreditierte Testlabore, akzeptieren wir nicht. Wir weisen ausdrücklich daraufhin, dass beim Verlegen des Kabels in den Graben oder in Rohren, bzw. in Bauwerke eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen ist, ob Auffälligkeiten zu vermerken sind. Eine spätere Reklamation, die fahrlässiges Verhalten vermuten lässt, schränkt sich damit ein. Dies gilt auch bei der Annahme der Ware, wo offensichtliche Beschädigungen direkt zu kommunizieren sind. Spätere Ansprüche nach Akzeptanz einer einwandfreien Belieferung sind detailliert zu beweisen.
|
||||
|
||||
15. Schadenersatz | Gesamthaftung
|
||||
Wir haften unbeschränkt nur für Vorsatz und grobe Fahrlässigkeit sowie für Schäden aus einer Verletzung von Leben, Körper oder Gesundheit, die auf mindestens fahrlässiger Pflichtverletzung unsererseits oder unserer gesetzlichen Vertreter oder Erfüllungsgehilfen beruhen; ebenso haften wir unbeschränkt im Fall von uns übernommenen bzw. abgegebenen Garantien und Zusicherungen, sofern ein davon umfasster Mangel unsere Haftung auslöst sowie im Fall einer Haftung nach dem Produkthaftungsgesetz oder sonstigen Gefährdungshaftungstatbeständen. Im Fall sonstiger schuldhafter Verletzung wesentlicher Vertragspflichten („Kardinalpflichten“) ist unsere verbleibende Haftung auf den vertragstypischen vorhersehbaren Schaden beschränkt. Mangelfolgeschäden sowie entgangener Gewinn schließen wir grundsätzlich aus.
|
||||
|
||||
16. Kabeltrommeln
|
||||
Unsere Kabel werden auf stabilen Vollholztrommeln geliefert. Bei Lagerzeiten über 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ßen wir aus. Zusätzlich die [PDF zum Download](/assets/AVB MB Grid 4-2026.pdf). Auf Wunsch vermitteln wir Ihnen Partner, die diese Trommeln gegen eine Gebühr abholen.
|
||||
|
||||
17. Technische Beratungsdienstleistungen
|
||||
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. Sonstiges
|
||||
Es gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts (CISG). Gerichtsstand ist nach unserer Wahl Stuttgart, der Erfüllungsort der Lieferverpflichtung oder das für den Sitz des Bestellers zuständige Gericht, sofern der Besteller Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen ist oder keinen allgemeinen Gerichtsstand im Inland hat.
|
||||
|
||||
Mit der Veröffentlichung der vorliegenden AVB im Internet werden alle von uns früher verwendeten Bedingungen gegenstandslos.
|
||||
|
||||
Remshalden, 22.1.2026
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
# Public Router – paths that bypass Gatekeeper auth
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*|stats(/.*)?|errors(/.*)?)`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls=true"
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: mb-grid-solutions-internal
|
||||
name: ${PROJECT_NAME:-mb-grid-solutions}-internal
|
||||
infra:
|
||||
external: true
|
||||
|
||||
|
||||
10
instrumentation.ts
Normal file
10
instrumentation.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,8 @@ function createConfig() {
|
||||
|
||||
errors: {
|
||||
glitchtip: {
|
||||
// Use SENTRY_DSN for both server and client (proxied)
|
||||
dsn: env.SENTRY_DSN,
|
||||
// Use NEXT_PUBLIC_SENTRY_DSN if available (required for browser-side SDK)
|
||||
dsn: env.NEXT_PUBLIC_SENTRY_DSN || env.SENTRY_DSN,
|
||||
// The proxied origin used in the frontend
|
||||
proxyPath: "/errors",
|
||||
enabled: Boolean(env.SENTRY_DSN),
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
const envExtension = {
|
||||
// Project specific overrides or additions
|
||||
AUTH_COOKIE_NAME: z.string().default("mb_gatekeeper_session"),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
SENTRY_DSN: z.string().optional(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"legal": "Rechtliches",
|
||||
"impressum": "Impressum",
|
||||
"datenschutz": "Datenschutz",
|
||||
"agb": "AGB",
|
||||
"agb": "AVB",
|
||||
"rights": "Alle Rechte vorbehalten.",
|
||||
"madeWith": "Entwickelt mit",
|
||||
"precision": "Präzision",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
|
||||
export default createMiddleware({
|
||||
const intlMiddleware = createMiddleware({
|
||||
// A list of all locales that are supported
|
||||
locales: ["de"],
|
||||
|
||||
@@ -11,6 +13,17 @@ export default createMiddleware({
|
||||
localePrefix: "as-needed",
|
||||
});
|
||||
|
||||
export default function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Explicit bypass for analytics and error tracking paths
|
||||
if (pathname.startsWith("/stats") || pathname.startsWith("/errors")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// Matcher for all pages and internationalized pathnames
|
||||
// excluding api, _next, static files, etc.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mb-grid-solutions.com",
|
||||
"version": "1.0.0",
|
||||
"version": "1.3.13",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"scripts": {
|
||||
@@ -17,6 +17,9 @@
|
||||
"check:http": "tsx ./scripts/check-http.ts",
|
||||
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||
"check:locale": "tsx ./scripts/check-locale.ts",
|
||||
"check:html": "tsx ./scripts/check-html.ts",
|
||||
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
||||
"check:wcag": "tsx ./scripts/wcag-sitemap.ts",
|
||||
"backup:db": "bash ./scripts/backup-db.sh"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -36,7 +39,6 @@
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-email/components": "^1.0.8",
|
||||
"@sentry/nextjs": "^10.38.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.13.0",
|
||||
@@ -73,12 +75,15 @@
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"happy-dom": "^20.6.1",
|
||||
"html-validate": "^10.8.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"pa11y-ci": "^4.0.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.5.0",
|
||||
"puppeteer": "^24.37.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@@ -44,9 +44,6 @@ importers:
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.38.0
|
||||
version: 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(react@19.2.4)(webpack@5.104.1(esbuild@0.25.12))
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
bcryptjs:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
@@ -3488,10 +3485,6 @@ packages:
|
||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||
hasBin: true
|
||||
|
||||
bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
bcryptjs@3.0.3:
|
||||
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
||||
hasBin: true
|
||||
@@ -5467,10 +5460,6 @@ packages:
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
node-addon-api@8.6.0:
|
||||
resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -5480,10 +5469,6 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@@ -10779,11 +10764,6 @@ snapshots:
|
||||
|
||||
baseline-browser-mapping@2.9.19: {}
|
||||
|
||||
bcrypt@6.0.0:
|
||||
dependencies:
|
||||
node-addon-api: 8.6.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
bcryptjs@3.0.3: {}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
@@ -11509,8 +11489,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -11526,8 +11506,8 @@ snapshots:
|
||||
'@next/eslint-plugin-next': 16.1.6
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@8.57.1)
|
||||
@@ -11549,7 +11529,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -11560,22 +11540,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -11586,7 +11566,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -13015,14 +12995,10 @@ snapshots:
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
node-addon-api@8.6.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nodemailer@7.0.12: {}
|
||||
|
||||
BIN
public/assets/AVB MB Grid 4-2026.pdf
Normal file
BIN
public/assets/AVB MB Grid 4-2026.pdf
Normal file
Binary file not shown.
197
scripts/check-broken-assets.ts
Normal file
197
scripts/check-broken-assets.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
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.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';
|
||||
|
||||
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`;
|
||||
let urls: string[] = [];
|
||||
|
||||
try {
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
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(/\/$/, '')))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
|
||||
if (urls.length > limit) {
|
||||
console.log(
|
||||
`⚠️ 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))];
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Launch Headless Browser
|
||||
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'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Inject Gatekeeper session bypassing auth screens
|
||||
await page.setCookie({
|
||||
name: 'mb_gatekeeper_session',
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
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 }> = [];
|
||||
|
||||
// Listen for unhandled exceptions natively in the page
|
||||
page.on('pageerror', (err: any) => {
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
error: err.message,
|
||||
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) => {
|
||||
const type = msg.type();
|
||||
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')
|
||||
)
|
||||
return;
|
||||
|
||||
consoleErrorsList.push({
|
||||
type: type.toUpperCase(),
|
||||
error: text,
|
||||
page: page.url(),
|
||||
});
|
||||
hasConsoleErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to ALL network responses
|
||||
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')
|
||||
) {
|
||||
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)) {
|
||||
brokenAssetsList.push({
|
||||
url: response.url(),
|
||||
status: status,
|
||||
page: page.url(),
|
||||
});
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Scan each page
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const u = urls[i];
|
||||
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 });
|
||||
|
||||
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||
await page.evaluate(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 100;
|
||||
const timer = setInterval(() => {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
if (totalHeight >= scrollHeight) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 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}`);
|
||||
// Don't fail the whole script just because one page timed out, but flag it
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// 4. Report Results
|
||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
|
||||
console.table(brokenAssetsList);
|
||||
}
|
||||
|
||||
if (hasConsoleErrors && consoleErrorsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Console Errors/Warnings detected on the site!`);
|
||||
console.table(consoleErrorsList);
|
||||
}
|
||||
|
||||
if (hasBrokenAssets || hasConsoleErrors) {
|
||||
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
`\n🎉 SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken images or console errors!`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
83
scripts/check-html.ts
Normal file
83
scripts/check-html.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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';
|
||||
|
||||
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`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
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(/\/$/, '')))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
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 });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const u = urls[i];
|
||||
try {
|
||||
const res = await axios.get(u, {
|
||||
headers: { Cookie: `mb_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`;
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n💻 Executing html-validate...`);
|
||||
try {
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
|
||||
console.log(`✅ HTML Validation passed perfectly!`);
|
||||
} catch (e) {
|
||||
console.error(`❌ HTML Validation found issues.`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`\n❌ Error during HTML Validation:`, error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,49 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from "./src/payload/payload.config";
|
||||
|
||||
const createAdmin = async () => {
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
const email = "marc@mintel.me";
|
||||
const password = "Tim300493.";
|
||||
|
||||
console.log(`Creating/Updating admin: ${email}`);
|
||||
|
||||
try {
|
||||
// Check if user exists
|
||||
const users = await payload.find({
|
||||
collection: "users",
|
||||
where: {
|
||||
email: {
|
||||
equals: email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (users.totalDocs > 0) {
|
||||
console.log("User already exists. Updating password.");
|
||||
await payload.update({
|
||||
collection: "users",
|
||||
id: users.docs[0].id,
|
||||
data: {
|
||||
password,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
console.log("Admin user created successfully.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating admin:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
createAdmin();
|
||||
187
scripts/wcag-sitemap.ts
Normal file
187
scripts/wcag-sitemap.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* WCAG Audit Script
|
||||
*
|
||||
* 1. Fetches sitemap.xml from the target URL
|
||||
* 2. Extracts all URLs
|
||||
* 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';
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: ${limit} pages\n`);
|
||||
|
||||
try {
|
||||
// 1. Fetch Sitemap
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
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(/\/$/, '')))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (urls.length > limit) {
|
||||
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 others = urls.filter((u) => !home.includes(u));
|
||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||
}
|
||||
|
||||
console.log(`🧪 Pages to be tested:`);
|
||||
urls.forEach((u) => console.log(` - ${u}`));
|
||||
|
||||
// 2. Prepare pa11y-ci config
|
||||
const baseConfigPath = path.join(process.cwd(), '.pa11yci.json');
|
||||
let baseConfig: any = { defaults: {} };
|
||||
if (fs.existsSync(baseConfigPath)) {
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
|
||||
}
|
||||
|
||||
// Extract domain for cookie
|
||||
const urlObj = new URL(targetUrl);
|
||||
const domain = urlObj.hostname;
|
||||
|
||||
// Update config with discovered URLs and gatekeeper cookie
|
||||
const tempConfig = {
|
||||
...baseConfig,
|
||||
defaults: {
|
||||
...baseConfig.defaults,
|
||||
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
||||
runners: ['axe'],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
|
||||
chromeLaunchConfig: {
|
||||
...baseConfig.defaults?.chromeLaunchConfig,
|
||||
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
|
||||
args: [
|
||||
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
timeout: 60000, // Increase timeout for slower pages
|
||||
},
|
||||
urls: urls,
|
||||
};
|
||||
|
||||
// Create output directory
|
||||
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');
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
||||
|
||||
// 3. Execute pa11y-ci
|
||||
console.log(`\n💻 Executing pa11y-ci...`);
|
||||
const pa11yCommand = `npx pa11y-ci --config .pa11yci/config.temp.json --reporter json > .pa11yci/report.json`;
|
||||
|
||||
try {
|
||||
execSync(pa11yCommand, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err: any) {
|
||||
// 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'));
|
||||
console.log(`\n📊 WCAG Audit Summary:\n`);
|
||||
|
||||
const summaryTable = Object.keys(reportData.results).map((url) => {
|
||||
const results = reportData.results[url];
|
||||
// Results might have errors or just a top level message if it crashed
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
let notices = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Clean URL for display
|
||||
const displayUrl = url.replace(targetUrl, '') || '/';
|
||||
|
||||
return {
|
||||
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
|
||||
Errors: errors,
|
||||
Warnings: warnings,
|
||||
Notices: notices,
|
||||
Status: errors === 0 ? '✅' : '❌',
|
||||
};
|
||||
});
|
||||
|
||||
console.table(summaryTable);
|
||||
|
||||
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.`);
|
||||
if (totalErrors > 0) {
|
||||
console.log(` Total Errors discovered: ${totalErrors}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✨ WCAG Audit completed!`);
|
||||
} catch (error: any) {
|
||||
console.error(`\n❌ Error during WCAG Audit:`);
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(`Status: ${error.response?.status}`);
|
||||
console.error(`URL: ${error.config?.url}`);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up temp config file, keep report
|
||||
const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json');
|
||||
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -50,24 +50,29 @@ export default buildConfig({
|
||||
},
|
||||
prodMigrations: migrations,
|
||||
}),
|
||||
...(process.env.SMTP_HOST
|
||||
? {
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress:
|
||||
process.env.SMTP_FROM || "info@mb-grid-solutions.com",
|
||||
defaultFromName: "MB Grid Solutions CMS",
|
||||
transportOptions: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT || "587"),
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
secure: process.env.SMTP_SECURE === "true",
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress:
|
||||
process.env.MAIL_FROM ||
|
||||
process.env.SMTP_FROM ||
|
||||
"info@mb-grid-solutions.com",
|
||||
defaultFromName: "MB Grid Solutions CMS",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || process.env.SMTP_HOST || "localhost",
|
||||
port: parseInt(
|
||||
process.env.MAIL_PORT || process.env.SMTP_PORT || "587",
|
||||
),
|
||||
auth: {
|
||||
user:
|
||||
process.env.MAIL_USERNAME ||
|
||||
process.env.MAIL_USER ||
|
||||
process.env.SMTP_USER,
|
||||
pass: process.env.MAIL_PASSWORD || process.env.SMTP_PASS,
|
||||
},
|
||||
secure:
|
||||
process.env.MAIL_SECURE === "true" ||
|
||||
process.env.SMTP_SECURE === "true",
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
|
||||
@@ -6,6 +6,19 @@ const { mockCreate, mockSendEmail } = vi.hoisted(() => ({
|
||||
mockSendEmail: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock Nodemailer
|
||||
const { mockSendMail } = vi.hoisted(() => ({
|
||||
mockSendMail: vi.fn().mockResolvedValue({ messageId: "mock-message-id" }),
|
||||
}));
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn().mockReturnValue({
|
||||
sendMail: mockSendMail,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("payload", () => ({
|
||||
getPayload: vi.fn().mockResolvedValue({
|
||||
create: mockCreate,
|
||||
@@ -101,7 +114,7 @@ describe("Contact API Integration", () => {
|
||||
|
||||
// But it actually does NOTHING internally
|
||||
expect(mockCreate).not.toHaveBeenCalled();
|
||||
expect(mockSendEmail).not.toHaveBeenCalled();
|
||||
expect(mockSendMail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should successfully save to Payload and send emails", async () => {
|
||||
@@ -140,10 +153,10 @@ describe("Contact API Integration", () => {
|
||||
});
|
||||
|
||||
// 2. Verify Email Sending
|
||||
// Note: sendEmail is called twice (Notification + User Confirmation)
|
||||
expect(mockSendEmail).toHaveBeenCalledTimes(2);
|
||||
// Note: sendMail is called twice (Notification + User Confirmation)
|
||||
expect(mockSendMail).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockSendEmail).toHaveBeenNthCalledWith(
|
||||
expect(mockSendMail).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
subject: "Kontaktanfrage von Jane Doe",
|
||||
@@ -151,7 +164,7 @@ describe("Contact API Integration", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockSendEmail).toHaveBeenNthCalledWith(
|
||||
expect(mockSendMail).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
to: "jane@example.com",
|
||||
|
||||
Reference in New Issue
Block a user