Compare commits

...

180 Commits

Author SHA1 Message Date
77d87e7be9 chore: bump version to 1.4.0 for MDX migration release
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 41s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Failing after 12m19s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-04 10:27:19 +02:00
5fb73d57dd feat: complete migration to static MDX, stabilize local infrastructure and fix i18n issues
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-05-04 10:26:45 +02:00
41ef9092d6 chore: remove postgres db from docker-compose and package.json scripts 2026-05-03 17:33:03 +02:00
afeb53e7cf refactor: completely remove payload cms and convert to static/mdx approaches 2026-05-03 17:32:28 +02:00
8dd5c077c5 test(qa): trigger qa workflow on push
Some checks failed
Nightly QA (Inlined) / 🧪 Quality Assurance (push) Failing after 1m22s
CI - Quality Assurance / 🧪 QA (pull_request) Successful in 3m28s
2026-05-03 11:50:43 +02:00
dbae239031 fix(qa): consolidate workflow and fix chromium pathing issues
All checks were successful
CI - Quality Assurance / 🧪 QA (pull_request) Successful in 3m31s
2026-05-03 11:50:26 +02:00
bbcf859d12 test(ci): remove path restriction to trigger qa pipeline
Some checks failed
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 57s
CI - Quality Assurance / 🧪 QA (pull_request) Successful in 3m44s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 3m36s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Successful in 1m15s
Nightly QA (Inlined) / ♿ Accessibility (push) Failing after 2m27s
Nightly QA (Inlined) / 🔔 Notify (push) Has been cancelled
Nightly QA (Inlined) / 🎭 Lighthouse (push) Has been cancelled
2026-05-03 11:44:32 +02:00
81c6e02faf fix(ci): update lockfile and package.json with pnpm onlyBuiltDependencies
All checks were successful
CI - Quality Assurance / 🧪 QA (pull_request) Successful in 3m40s
2026-05-03 11:44:05 +02:00
ed98c40079 test(ci): trigger nightly qa on stabilization branch
Some checks failed
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Failing after 14s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Has been skipped
Nightly QA (Inlined) / ♿ Accessibility (push) Has been skipped
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Has been skipped
Nightly QA (Inlined) / 🎭 Lighthouse (push) Has been skipped
CI - Quality Assurance / 🧪 QA (pull_request) Failing after 13s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
2026-05-03 11:42:32 +02:00
0633326c1e fix(ci): stabilize nightly qa pipeline browser dependencies and pnpm build scripts
Some checks failed
CI - Quality Assurance / 🧪 QA (pull_request) Failing after 14s
2026-05-03 11:42:11 +02:00
cad27d6a02 fix(ci): stabilize nightly qa pipeline browser dependencies and depcheck ignores
All checks were successful
CI - Quality Assurance / 🧪 QA (pull_request) Successful in 1m41s
2026-05-03 11:40:17 +02:00
f4aad36ea6 chore(ci): stabilize nightly qa pipeline with browser libs and cli fixes
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m41s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m12s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 15s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 3m11s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 23s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m0s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m43s
Nightly QA (Inlined) / ♿ Accessibility (push) Failing after 2m25s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 2m28s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
2026-05-03 11:35:37 +02:00
88edf08993 chore(qa): harden nightly pipeline jobs (browser install, depcheck ignores, link-check robustness)
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m11s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-05-03 11:31:09 +02:00
e0cdac971f chore(qa): harden nightly pipeline jobs (browser install, depcheck ignores, link-check robustness)
Some checks failed
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🔍 Prepare (push) Has been cancelled
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 31s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m6s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Successful in 45s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m15s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m56s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
2026-05-03 11:31:05 +02:00
cc728410c2 fix: revert bad registry auth in qa.yml
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 3s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 32s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 38s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m57s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 31s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m5s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m50s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m28s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 2m47s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-05-03 10:12:17 +02:00
59e1ae7926 ci: fix registry auth secrets to match ci.yml and enforce frozen-lockfile
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 32s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 51s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 40s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m12s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m16s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m57s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 1s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-05-03 10:03:57 +02:00
a36a7fe19d ci: fix broken symlinks and lychee args in qa.yml
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 3s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m38s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m38s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m33s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 23s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m3s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m5s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m22s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 2m24s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
2026-04-27 09:31:25 +02:00
13cbc0291f fix: resolve HTML hierarchy, accessibility and PDF link issues
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m19s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m30s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m45s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 6m54s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m29s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m38s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m0s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 2m2s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 1s
- Fix heading level skip in ContactContent success state (H3 -> H2)
- Clean up deprecated iframe attributes in ContactContent
- Add aria-label to footer logo link for WCAG compliance
- Rename PDF assets to hyphenated filenames to fix link checks
- Update AGB page with new PDF link format
2026-04-25 23:04:52 +02:00
1315985ba2 fix(qa): resolve html validation errors and clean up unused dependencies
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m41s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 6m38s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m22s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m28s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m6s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m23s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m15s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-04-25 22:50:16 +02:00
04309e3c02 chore(qa): update gatekeeper password fallback to lassmichrein
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m43s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 6m42s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m20s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m24s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m0s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m20s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 1s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m46s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-04-25 22:37:25 +02:00
8e2a6a0378 chore(qa): permanently inline QA logic to guarantee expansion
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m41s
Nightly QA (Inlined) / 🏗️ Prepare & Install (push) Successful in 6m51s
Nightly QA (Inlined) / 🔍 Static Analysis (push) Failing after 1m40s
Nightly QA (Inlined) / 🧪 Maintenance & Links (push) Failing after 1m54s
Nightly QA (Inlined) / ♿ Accessibility (push) Successful in 2m32s
Nightly QA (Inlined) / 🎭 Lighthouse (push) Failing after 1m43s
Nightly QA (Inlined) / 🔔 Notify (push) Successful in 2s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m49s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m44s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-25 19:19:05 +02:00
5a48dcd75f chore(qa): use full URL for reusable workflow to fix expansion
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
Nightly QA / call-qa-workflow (push) Failing after 1m1s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m35s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 19:16:28 +02:00
73cb55ee8d chore(qa): restore reusable workflow with Gitea compatibility fixes
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m34s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-04-25 19:13:49 +02:00
5ffd78d60f ci: use upload-artifact@v3 for gitea compatibility
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m38s
Nightly QA (Inline) / 🏗️ Prepare & Install (push) Successful in 6m58s
Nightly QA (Inline) / 🔍 Static Analysis (push) Failing after 17s
Nightly QA (Inline) / 🧪 Maintenance & Links (push) Failing after 1m26s
Nightly QA (Inline) / ♿ Accessibility (push) Successful in 2m26s
Nightly QA (Inline) / 🎭 Lighthouse (push) Failing after 2m15s
Nightly QA (Inline) / 🔔 Notify (push) Successful in 2s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m39s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m59s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-25 18:57:06 +02:00
b0b8902f30 ci: add inlined QA workflow for definitive testing
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 3s
Nightly QA (Inline) / 🏗️ Prepare & Install (push) Failing after 38s
Nightly QA (Inline) / 🔍 Static Analysis (push) Has been skipped
Nightly QA (Inline) / ♿ Accessibility (push) Has been skipped
Nightly QA (Inline) / 🧪 Maintenance & Links (push) Has been skipped
Nightly QA (Inline) / 🎭 Lighthouse (push) Has been skipped
Nightly QA (Inline) / 🔔 Notify (push) Successful in 2s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m27s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 18:51:30 +02:00
0dcb64ddca ci: restore secrets and use new template SHA
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 9s
Nightly QA / call-qa-workflow (push) Failing after 1m46s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m52s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 18:39:11 +02:00
6bd4914586 ci: use @debug-qa for QA template
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
Nightly QA / call-qa-workflow (push) Failing after 1m37s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:35:55 +02:00
4de69aa7ed ci: test secrets: inherit with new SHA
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
Nightly QA / call-qa-workflow (push) Failing after 8s
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:34:16 +02:00
e217d9c7f7 ci: use exact SHA for QA template
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
Nightly QA / call-qa-workflow (push) Failing after 1m41s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m55s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 18:30:59 +02:00
f97b37d3c8 ci: trigger QA run
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m9s
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:28:06 +02:00
d4705d616b ci: use @main for QA template
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 10s
Nightly QA / call-qa-workflow (push) Failing after 1m39s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:25:10 +02:00
d336980f27 ci: update QA template SHA to 151c98c
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
Nightly QA / call-qa-workflow (push) Failing after 5s
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:24:42 +02:00
f663497277 ci: update QA template SHA to ba17d79
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 4s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 18:23:52 +02:00
3dee7c73f1 ci: update QA template SHA to 46120db
Some checks failed
Nightly QA / call-qa-workflow (push) Failing after 3s
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 10s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:22:23 +02:00
ac4677ca50 ci: trigger QA with diagnostics
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
Nightly QA / call-qa-workflow (push) Failing after 1m6s
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-25 18:19:26 +02:00
ce492c221c ci: update QA template for diagnostics
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 4s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-25 18:18:39 +02:00
fbd405c270 chore: force template refresh with sha
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 10s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m30s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m33s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m48s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 38s
2026-04-24 13:22:25 +02:00
acfee13df7 chore: trigger nightly qa check
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 48s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m33s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m31s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m46s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-04-24 13:06:39 +02:00
a5d43df53a chore: finalize Nightly QA stabilization
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 10s
Nightly QA / call-qa-workflow (push) Failing after 1m38s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m51s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m44s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m57s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-24 12:38:07 +02:00
adda98eb00 chore: use npm registry host in debug
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
QA Debug / 🏗️ Prepare & Install (push) Successful in 35s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m31s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m39s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m55s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-24 12:20:04 +02:00
3523547eca chore: trigger standardized registry auth
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
QA Debug / 🏗️ Prepare & Install (push) Failing after 51s
Nightly QA / call-qa-workflow (push) Failing after 51s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 12:16:18 +02:00
2ddef98924 chore: enable push for qa-debug
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 31s
QA Debug / 🏗️ Prepare & Install (push) Failing after 51s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m47s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 12:12:27 +02:00
272d3eccbf chore: add qa-debug workflow
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-04-24 12:12:18 +02:00
24475adef3 chore: trigger hardened step order
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m9s
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-24 12:08:48 +02:00
473079181f chore: use REGISTRY_PASS fallback for tokens
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 33s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m31s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 12:02:54 +02:00
67e86b0fa3 chore: trigger hardened registry auth
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
Nightly QA / call-qa-workflow (push) Failing after 51s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-24 11:59:11 +02:00
1c441b2d99 chore: trigger hardened docker prune
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
Nightly QA / call-qa-workflow (push) Failing after 56s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m31s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:55:07 +02:00
e7446a5444 chore: use hardened-cleanup branch for QA
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
Nightly QA / call-qa-workflow (push) Failing after 46s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m32s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:48:14 +02:00
d50daa8db9 fix: pass NPM_TOKEN to QA workflow
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 32s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m29s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:46:12 +02:00
b69986572e chore: force template refresh via SHA
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
Nightly QA / call-qa-workflow (push) Failing after 43s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m34s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:42:36 +02:00
18e02e3a33 chore: trigger QA
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 47s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
2026-04-24 11:41:08 +02:00
f726b3e978 ci: trigger nightly qa
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 3s
Nightly QA / call-qa-workflow (push) Failing after 50s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m34s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m35s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m52s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-04-24 11:18:51 +02:00
832be8c6bf chore: trigger manual nightly qa check
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m23s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:16:19 +02:00
2b934dd70e chore: revert temporary trigger
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 9s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m33s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-24 11:07:13 +02:00
33e84fb538 test: trigger nightly qa with hardened template
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 11s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
Nightly QA / call-qa-workflow (push) Failing after 1m44s
2026-04-24 11:06:14 +02:00
52f4f30336 ci: add aggressive runner cleanup steps to workflows
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m36s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m32s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m55s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / call-qa-workflow (push) Failing after 29s
2026-04-24 01:14:34 +02:00
e8b8a4073e ci: fix ENOSPC by clearing docker builder cache before build
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 10m18s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m34s
2026-04-23 22:58:23 +02:00
5821a5dee4 fix: resolve vitest alias override bug breaking the QA test suite
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m42s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 10m25s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-23 14:28:29 +02:00
dfc7536268 fix: commit missing eslint-disable and lockfile to unblock pipeline
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m28s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 14m26s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 12s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m34s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / call-qa-workflow (push) Failing after 41s
2026-04-22 11:04:42 +02:00
9f26a179fa chore: bump version to 1.3.19 to deploy linter fix
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Failing after 45s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 12m49s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-22 10:43:40 +02:00
5043058660 fix: exclude scripts from tsconfig compilation and fix msg type to resolve pipeline build error
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Failing after 52s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 13m25s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-22 10:27:26 +02:00
a37091ad71 chore: bump version to 1.3.17
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m33s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 12m20s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-04-22 10:01:01 +02:00
9539aa35eb fix: move second 'Stand' mention to the bottom of the page
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m15s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-22 10:00:51 +02:00
3ad24ef615 chore: bump version to 1.3.16
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m33s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 15m20s
🚀 Build & Deploy / 🚀 Deploy (push) Failing after 1m22s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-04-22 09:26:44 +02:00
3018ae5412 fix: ensure 'Stand' is correctly detected and displayed in the main text body
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m15s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 12m23s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-04-22 09:26:30 +02:00
fd3f4c82c5 chore: bump version to 1.3.15
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m38s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 14m3s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 10s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m43s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-22 08:59:04 +02:00
3906838dc1 fix: repair AVB parser to correctly detect ## headers and strip markdown artifacts
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m8s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 11m43s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 7s
2026-04-22 08:58:36 +02:00
785e36af08 fix: resolve TS2345 in pageerror handler by using unknown and type guard
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m53s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 11m58s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m27s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 26s
2026-04-21 19:32:07 +02:00
d4c32476d6 fix: remove unused urlObj to satisfy linter
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Failing after 50s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-21 19:26:25 +02:00
9b9d4634dd fix: rename AGB to AVB in navigation and components for consistency
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 1m5s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-21 19:21:47 +02:00
c9e48cd5a4 fix: resolve lint errors in CI scripts
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Failing after 45s
🚀 Build & Deploy / 🏗️ Build (push) Failing after 11m9s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-21 19:05:24 +02:00
bb5828cbfc chore: remove temp format script
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 40s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-04-21 18:58:31 +02:00
f29338f78f docs: textuell aktualisierte avb
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 49s
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-04-21 18:58:24 +02:00
e0803715a7 chore: avb transition and automated testing setup
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 1m0s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m28s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-21 18:47:43 +02:00
8427d348dd fix: ensure smtp metadata logger doesn't crash in tests
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m13s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 14m2s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m51s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 40s
2026-04-15 00:05:45 +02:00
7109b3c32e fix: add detailed smtp logging to debug confirmation email
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Failing after 49s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m44s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-14 22:28:45 +02:00
18717e7faf fix: resolve vitest hoisting issue in contact api test
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m9s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m8s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m26s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 34s
2026-04-13 22:32:16 +02:00
3478f0f42f fix: remove problematic docker maintenance step from prepare job
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Failing after 39s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m34s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 21:45:42 +02:00
836cc66334 chore: re-trigger pipeline after network failure in #2226
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Failing after 13s
🚀 Build & Deploy / 🧪 QA (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Has been skipped
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 19:11:33 +02:00
9bb2186f7a test: update contact api test to mock nodemailer
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Failing after 6s
🚀 Build & Deploy / 🧪 QA (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Has been skipped
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 19:11:04 +02:00
55123132f4 fix(mail): bypass payload mail adapter and use standalone nodemailer
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 40s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m4s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 17:28:19 +02:00
bf1dfe4015 fix(mail): robust recipient logic and sentry dsn verification
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m18s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m3s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 14s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m58s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 11:49:09 +02:00
f03e417eb7 fix(prod): stabilize production environment and fix email adapter initialization
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m41s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m22s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m26s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 34s
2026-04-12 15:09:39 +02:00
e0312aeba9 chore: trigger deployment after infra disk cleanup
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m17s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 17m23s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m17s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-04-12 11:29:44 +02:00
d5c0b91c97 fix(analytics): restore Umami tracking by fixing build-args and bypassing gatekeeper
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🏗️ Build (push) Failing after 5m6s
Build & Deploy / 🧪 QA (push) Failing after 5m22s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 33s
2026-04-12 01:55:48 +02:00
155918e265 chore: trigger deployment after infrastructure and db fixes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 8m56s
Build & Deploy / 🏗️ Build (push) Successful in 15m30s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 33s
2026-04-10 14:13:24 +02:00
fb5b55f1dd fix(ci): add QEMU support for multi-arch builds (linux/amd64)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Successful in 15m12s
Build & Deploy / 🚀 Deploy (push) Failing after 1m36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-10 12:44:00 +02:00
9f00828b80 fix: restore glitchtip nesting and fix type errors
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m32s
Build & Deploy / 🏗️ Build (push) Successful in 5m10s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m17s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-09 12:24:33 +02:00
bb0debe29d fix: correct syntax error in lib/config.ts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 43s
2026-04-09 12:17:28 +02:00
f2e855fff9 chore: fix sentry logging instrumentation and infrastructure alignment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Failing after 1m9s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-09 11:32:47 +02:00
b603490683 fix(ci): add diagnostic logging and increase migration retries
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m13s
Build & Deploy / 🏗️ Build (push) Successful in 4m37s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m15s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / call-qa-workflow (push) Failing after 43s
2026-03-11 21:37:56 +01:00
68075edb77 fix(ci): increase migration retry attempts to prevent flaky deployments
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Successful in 2m17s
Build & Deploy / 🏗️ Build (push) Successful in 4m39s
Build & Deploy / 🚀 Deploy (push) Failing after 58s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 19:06:53 +01:00
217e58e5f4 feat(legal): update AGB to version 3-2026 and add support for markdown links in AGB text
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 5m30s
Build & Deploy / 🏗️ Build (push) Successful in 7m33s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m37s
Build & Deploy / 🚀 Deploy (push) Successful in 10s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 18:54:55 +01:00
e7cf7f8946 chore: remove test scripts breaking build
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 4m31s
Build & Deploy / 🏗️ Build (push) Successful in 6m22s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m12s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 14:49:36 +01:00
1a1196b740 fix: isolate internal docker networks
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🏗️ Build (push) Failing after 5m5s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-03-11 14:47:17 +01:00
7915afea2c fix: database fallback to directus user
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m18s
Build & Deploy / 🏗️ Build (push) Failing after 3m35s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-11 14:19:09 +01:00
16916654c0 feat: add database backup script and npm command
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m17s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 12:36:34 +01:00
6daf5c66a8 refactor: remove all legacy directus environment variables and standardize on postgres
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 5m14s
Build & Deploy / 🏗️ Build (push) Successful in 7m26s
Build & Deploy / 🚀 Deploy (push) Failing after 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 12:22:57 +01:00
95b594d8cd feat: link migrations to payload config for production bundling and simplify dockerfile
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 5m22s
Build & Deploy / 🏗️ Build (push) Successful in 7m24s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m39s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 11:48:55 +01:00
cd6651cc43 fix(migrations): use robust ESM import extensions in Dockerfile for v1.1.9
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 5m34s
Build & Deploy / 🏗️ Build (push) Successful in 8m28s
Build & Deploy / 🚀 Deploy (push) Failing after 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
Nightly QA / call-qa-workflow (push) Failing after 37s
2026-03-11 00:55:05 +01:00
855390a27b fix(deploy): use dedicated dist-migrations dir and fix sed path in Dockerfile
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 4m28s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Failing after 31s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-11 00:46:10 +01:00
ea8bd46973 fix(deploy): add .js extensions to transpiled migration imports for ESM compatibility
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 5m30s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-11 00:38:23 +01:00
ff269b1f84 fix(deploy): transpile migrations to JS for production compatibility
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 5m3s
Build & Deploy / 🏗️ Build (push) Successful in 7m14s
Build & Deploy / 🚀 Deploy (push) Failing after 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-11 00:26:46 +01:00
afa586c833 fix(deploy): bundle migrations into Next.js standalone output
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 1m12s
Build & Deploy / 🏗️ Build (push) Failing after 3m58s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-03-11 00:24:08 +01:00
9b55c42f35 fix(deploy): move migrations and include them in Docker image
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 5m13s
Build & Deploy / 🏗️ Build (push) Successful in 7m46s
Build & Deploy / 🚀 Deploy (push) Failing after 51s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-03-11 00:11:52 +01:00
554f958ba2 chore(deploy): fix registry URLs in docker-compose.yaml
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 5m27s
Build & Deploy / 🏗️ Build (push) Successful in 8m22s
Build & Deploy / 🚀 Deploy (push) Successful in 22s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m42s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-10 23:28:50 +01:00
c9f174e828 fix(middleware): exclude /admin from next-intl rewrites
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m22s
Build & Deploy / 🏗️ Build (push) Successful in 4m37s
Build & Deploy / 🚀 Deploy (push) Failing after 7s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-10 18:04:36 +01:00
8dce4890c4 chore(ci): migrate docker registry publishers to git.infra.mintel.me 2026-03-03 12:25:30 +01:00
963e572291 fix(lint): remove explicit any from check-http error handler
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m58s
Build & Deploy / 🏗️ Build (push) Successful in 4m27s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m16s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / call-qa-workflow (push) Failing after 34s
2026-02-28 23:07:27 +01:00
9887324469 fix(ci): point CMS deep health check to correct api/health endpoint and improve check-http error formatting
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 31s
Build & Deploy / 🏗️ Build (push) Successful in 2m6s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-28 22:56:41 +01:00
78da0fdea9 chore(deps): update pnpm-lock.yaml after local install
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 4m20s
Build & Deploy / 🏗️ Build (push) Successful in 6m48s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 19m17s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / call-qa-workflow (push) Failing after 26s
2026-02-28 00:29:49 +01:00
91db336c0e fix(deploy): add missing compress middleware declaration to fix 404 traefik router drops
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m54s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-02-28 00:23:22 +01:00
cfbff88e45 fix(deploy): rename traefik routers dynamically by PROJECT_NAME to prevent testing environment from overwriting production proxy rules
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m52s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 13s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-02-28 00:19:04 +01:00
90b41d2a15 fix(deploy): remove literal quotes from TRAEFIK_HOST in env generation to fix invalid traefik router syntax
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 2m26s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-28 00:07:53 +01:00
3f45293c2e fix(deploy): rewrite traefik routers and add public route for sitemap to bypass gatekeeper
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 2m48s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 23:55:52 +01:00
7e957d6fb4 fix(deploy): add retry loop to payload migration curl to handle slow postgres startups
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m19s
Build & Deploy / 🏗️ Build (push) Successful in 4m40s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 2m29s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 23:37:47 +01:00
4334d31445 test: disable happy-dom iframe and resource loading to silence AbortError logs
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 2m22s
Build & Deploy / 🏗️ Build (push) Successful in 4m48s
Build & Deploy / 🚀 Deploy (push) Failing after 11s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 22:55:50 +01:00
1559037029 fix(ci): resolve eslint type errors in qa smoke test scripts
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 3s
Build & Deploy / 🧪 QA (push) Successful in 2m1s
Build & Deploy / 🏗️ Build (push) Successful in 2m38s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m23s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 21:09:07 +01:00
b7438f2718 ci: add missing core smoke test scripts and dependencies
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🧪 QA (push) Failing after 1m5s
Build & Deploy / 🏗️ Build (push) Successful in 4m47s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 19:24:53 +01:00
a090373825 fix(pipeline): add default network to app container so it can reach postgres db
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 2m52s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-27 19:13:18 +01:00
3c3d019924 fix(pipeline): correct bash string escaping and env fallbacks for migration script
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 3m18s
Build & Deploy / 🏗️ Build (push) Successful in 4m14s
Build & Deploy / 🚀 Deploy (push) Failing after 16s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 6s
2026-02-27 19:05:10 +01:00
c6d20119c7 fix(pipeline): provide fallback PAYLOAD_SECRET so migration curl can authenticate
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m25s
Build & Deploy / 🏗️ Build (push) Successful in 3m30s
Build & Deploy / 🚀 Deploy (push) Failing after 16s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-27 18:52:43 +01:00
04a19742da fix(pipeline): resolve typescript error inside payload migration endpoint
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 3m36s
Build & Deploy / 🏗️ Build (push) Successful in 4m46s
Build & Deploy / 🚀 Deploy (push) Failing after 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 12s
2026-02-27 18:40:45 +01:00
39b96a51db fix(pipeline): resolve eslint type error in payload migration endpoint
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Failing after 43s
Build & Deploy / 🏗️ Build (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-27 18:27:02 +01:00
d27e1f91ad ci: add nightly QA pipeline utilizing reusable templates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 57s
Build & Deploy / 🏗️ Build (push) Failing after 2m52s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-27 18:15:47 +01:00
18cd576ee9 ci: modernize deploy pipeline to use reusable at-mintel core-smoke-tests template
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-27 18:14:35 +01:00
3d2f240cf6 feat(cms): robust api based migration endpoint for prod
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 31s
Build & Deploy / 🏗️ Build (push) Failing after 2m1s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 15:18:10 +01:00
6260b40b91 fix: remove tsx from payload migrate command
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m57s
Build & Deploy / 🏗️ Build (push) Successful in 2m47s
Build & Deploy / 🚀 Deploy (push) Failing after 18s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 14:23:34 +01:00
109c8389f3 test: add api integration tests for contact form
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m28s
Build & Deploy / 🏗️ Build (push) Successful in 4m45s
Build & Deploy / 🚀 Deploy (push) Failing after 13s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 13:34:23 +01:00
55cb073a6d feat(cms): migrate from directus to payloadcms
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m55s
Build & Deploy / 🏗️ Build (push) Successful in 11m40s
Build & Deploy / 🚀 Deploy (push) Failing after 8s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-27 12:56:35 +01:00
fb87fd52f7 fix(traefik): parameterize cms auth middleware
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 3m13s
Build & Deploy / 🏗️ Build (push) Successful in 10m7s
Build & Deploy / 🚀 Deploy (push) Failing after 7s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-27 11:36:09 +01:00
da9b2fb9cf fix(gatekeeper): use testing tag instead of v1.8.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-27 11:34:31 +01:00
5032700c2c fix(gatekeeper): upgrade gatekeeper tag to v1.8.21
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-02-27 11:33:46 +01:00
d44838254c fix(ci): add pnpm store prune to fix integrity errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 2m50s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-27 11:28:50 +01:00
1742604a7a chore(workspace): add gitea repository url to all packages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-27 11:27:24 +01:00
ca59f32b99 fix(ci): update to v1.8.21 for x86 base images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 9s
Build & Deploy / 🏗️ Build (push) Failing after 42s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-26 19:45:34 +01:00
0e98659506 fix(infra): prevent redirect loop by excluding gatekeeper path from app router
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 4m27s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🩺 Health Check (push) Successful in 19s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 17:58:25 +01:00
744e1da716 fix(infra): resolve identity shadowing by standardizing internal hostnames and isolated networks
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m42s
Build & Deploy / 🧪 QA (push) Successful in 2m14s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🩺 Health Check (push) Failing after 13s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 17:53:37 +01:00
f2e38f9c29 fix(infra): harden health checks and fix directus security warnings
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 3m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 17:19:12 +01:00
b85312c433 fix(infra): full KLZ parity for gatekeeper (dynamic COOKIE_DOMAIN + X-Forwarded-Host)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m44s
Build & Deploy / 🧪 QA (push) Successful in 2m32s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-12 17:10:44 +01:00
081ebec567 fix(infra): forward Cookie header to gatekeeper for session verification
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Successful in 1m43s
Build & Deploy / 🧪 QA (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 12s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-02-12 17:05:03 +01:00
d9dece37e5 fix(infra): remove redundant gatekeeper environment defaults to allow clean inheritance
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🏗️ Build (push) Successful in 1m44s
Build & Deploy / 🧪 QA (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🩺 Health Check (push) Successful in 2s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 16:59:01 +01:00
9495772d1a fix(ci): upgrade upstream verification logic to klz-2026 standard
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🧪 QA (push) Successful in 2m5s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🩺 Health Check (push) Successful in 2s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 15:44:22 +01:00
248a0dc1f0 fix(ci): fix typo in directus router rule Host
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 24s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 15:11:27 +01:00
eecc1b6108 fix(ci): fix YAML syntax in docker-compose.yaml
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🧪 QA (push) Successful in 2m29s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 15:06:27 +01:00
e0b38e617d fix(ci): consolidate middleware definitions to avoid missing middleware error
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 2m28s
Build & Deploy / 🚀 Deploy (push) Failing after 6s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:57:36 +01:00
8a9339f00f fix(ci): set high priority for Traefik routers to avoid 404
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 2m29s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:52:10 +01:00
f23fa4e2c8 fix(ci): fix Traefik rule syntax and add Host header to ForwardAuth
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🏗️ Build (push) Successful in 1m43s
Build & Deploy / 🧪 QA (push) Successful in 2m29s
Build & Deploy / 🚀 Deploy (push) Successful in 10s
Build & Deploy / 🩺 Health Check (push) Failing after 29s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:47:50 +01:00
e177693aae fix(ci): standardize project name to mb-grid-solutions to avoid conflicts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 2m54s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 13:54:37 +01:00
39920bf432 chore(ci): trigger deployment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m41s
Build & Deploy / 🧪 QA (push) Successful in 2m8s
Build & Deploy / 🚀 Deploy (push) Successful in 11s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-12 00:52:29 +01:00
04d3dac627 fix(ci): pass missing TRAEFIK_HOST to deployment env generation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m41s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-02-12 00:50:39 +01:00
bc0a6627c0 fix(gatekeeper): upgrade to v1.7.12
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 3m24s
Build & Deploy / 🏗️ Build (push) Successful in 4m44s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🩺 Health Check (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 22:49:37 +01:00
8030e45920 feat(pipeline): add smart dependency waiting for upstream releases
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 22:42:26 +01:00
fbc7b9bba0 fix(gatekeeper): upgrade to v1.7.11
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🩺 Health Check (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 22:35:43 +01:00
05a90df512 fix(gatekeeper): standardize auth headers and path-based routing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m37s
Build & Deploy / 🏗️ Build (push) Successful in 1m40s
Build & Deploy / 🚀 Deploy (push) Successful in 9s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 22:04:28 +01:00
817ee05710 test: restore and fix tests broken by lazy-loading
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🏗️ Build (push) Successful in 1m41s
Build & Deploy / 🧪 QA (push) Successful in 2m1s
Build & Deploy / 🚀 Deploy (push) Failing after 7s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 19:49:52 +01:00
5d01c2e963 fix: remove useless tests
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 43s
Build & Deploy / 🏗️ Build (push) Successful in 2m11s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 19:45:06 +01:00
1e32b8fbea fix(lint): remove unused imports in HomeContent.tsx
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 49s
Build & Deploy / 🏗️ Build (push) Successful in 2m22s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 19:32:35 +01:00
1919d8bc2a perf: implement multi-phase performance optimizations for PageSpeed 90+
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Successful in 1m41s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Resized and compressed oversized logo (204KB -> 21KB)
- Optimized large media images (hs-kabel.png, contact-hero.jpg)
- Implemented dynamic lazy-loading for home page sections
- Tuned Sentry traces sample rate (1.0 -> 0.1)
- Refined font loading and fixed redundant analytics tracking
2026-02-11 19:16:39 +01:00
67d47e3ec7 fix(deploy): pin gatekeeper version and add protocol normalization
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 1m41s
Build & Deploy / 🧪 QA (push) Successful in 2m7s
Build & Deploy / 🚀 Deploy (push) Failing after 13s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 15:29:30 +01:00
2f8d015823 fix(deploy): use correct docker-compose extension (.yaml)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Successful in 2m8s
Build & Deploy / 🚀 Deploy (push) Failing after 7s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 15:21:18 +01:00
e18bd0b6f3 chore: fix docker build failure, resolve zod conflict and stabilize test suite
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m36s
Build & Deploy / 🏗️ Build (push) Successful in 3m58s
Build & Deploy / 🚀 Deploy (push) Failing after 9s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 15:08:02 +01:00
2ca79ee23a fix: resolve redundant success messages and next/server resolution errors in tests
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Failing after 25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 14:42:05 +01:00
e28c3c0f96 chore: standardize project environment and CI/CD maintenance
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 58s
Build & Deploy / 🏗️ Build (push) Failing after 2m13s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 12:23:06 +01:00
8f3f56a12c fix: harmonize Zod versions to v3.24.1, restore build, and update tests
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 31s
Build & Deploy / 🏗️ Build (push) Successful in 6m51s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 11:58:29 +01:00
8d547c559e chore: standardize
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 42s
Build & Deploy / 🏗️ Build (push) Failing after 2m14s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 11:05:37 +01:00
8ff4503270 refactor: standardize env and directus logic using enhanced @mintel/next-utils
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
Build & Deploy / 🏗️ Build (push) Failing after 1m51s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-02-10 23:47:18 +01:00
ad08c6c1f3 ci: simplify QA checks to avoid potential hangs
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Failing after 6s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-10 22:33:24 +01:00
1f188c84b4 ci: fix workflow syntax (EOF indentation)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 3m29s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-10 22:25:21 +01:00
e50cdade6c ci: complete pipeline standardization 2026-02-10 22:20:06 +01:00
17bbb2f0e0 ci: fix SSH variable expansion in deployment 2026-02-10 22:17:49 +01:00
ffb73e4b06 ci: restore missing Directus and Mail secrets in deployment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Failing after 5s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-10 22:10:51 +01:00
71b30ba8c5 fix: sentry issues
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m55s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
2026-02-10 13:49:22 +01:00
e9ea253021 fix: build and lint
All checks were successful
Build & Deploy / 🔍 Prepare Environment (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🔔 Notifications (push) Successful in 1s
2026-02-10 00:31:51 +01:00
237bd46593 feat: more transparency
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 39s
Build & Deploy / 🏗️ Build (push) Failing after 30s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s
2026-02-10 00:22:53 +01:00
40ebdb31d9 fix: analytics 2026-02-10 00:15:36 +01:00
8f39ec3d35 fix: resolve lint errors in layout and route by updating analytics interface
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 1m59s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s
2026-02-10 00:01:31 +01:00
7734440b90 refactor: remove arrogant marketing terms and localize to German
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 48s
Build & Deploy / 🏗️ Build (push) Failing after 3m1s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s
2026-02-09 23:50:44 +01:00
42295c3c41 feat: improved analytics
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 36s
Build & Deploy / 🏗️ Build (push) Failing after 1m56s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s
2026-02-09 23:36:05 +01:00
1e00690dd8 fix: umami tracking internationalization
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Failing after 1m56s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s
2026-02-09 23:10:27 +01:00
104 changed files with 8525 additions and 3334 deletions

View File

@@ -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=

36
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,36 @@
name: CI - Quality Assurance
on:
pull_request:
jobs:
qa:
name: 🧪 QA
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 || secrets.REGISTRY_PASS }}" >> .npmrc
- name: Install dependencies
run: |
rm -rf .next .turbo node_modules || true
pnpm install --frozen-lockfile
- name: 🧪 Parallel Checks
run: |
pnpm lint &
pnpm build &
wait

View File

@@ -1,4 +1,5 @@
name: Build & Deploy
# Infrastructure Maintenance: Production stabilization and runner cleanup triggered on 2026-04-23.
name: 🚀 Build & Deploy
on:
push:
@@ -7,138 +8,130 @@ on:
tags:
- 'v*'
workflow_dispatch:
inputs:
skip_checks:
description: 'Skip tests? (true/false)'
required: false
default: 'false'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }}
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────────────────────────────────────
# JOB 1: Prepare Environment
# ──────────────────────────────────────────────────────────────────────────────
prepare:
name: 🔍 Prepare Environment
name: 🔍 Prepare
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
outputs:
target: ${{ steps.determine.outputs.target }}
image_tag: ${{ steps.determine.outputs.image_tag }}
env_file: ${{ steps.determine.outputs.env_file }}
traefik_host: ${{ steps.determine.outputs.traefik_host }}
next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }}
directus_url: ${{ steps.determine.outputs.directus_url }}
directus_host: ${{ steps.determine.outputs.directus_host }}
gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }}
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
gatekeeper_rule: ${{ steps.determine.outputs.gatekeeper_rule }}
traefik_middlewares: ${{ steps.determine.outputs.traefik_middlewares }}
next_public_url: ${{ steps.determine.outputs.next_public_url }}
project_name: ${{ steps.determine.outputs.project_name }}
short_sha: ${{ steps.determine.outputs.short_sha }}
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 🔍 Debug Info
shell: bash
run: |
echo "ref_name: ${{ github.ref_name }}"
echo "ref_type: ${{ github.ref_type }}"
echo "tag: ${{ github.ref_name }}"
- name: 🧹 Maintenance (Runner Cleanup)
continue-on-error: true
shell: bash
run: |
docker image prune -f || true
docker builder prune -f --filter "until=24h" || true
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: 🔍 Determine Environment
- name: 🔍 Environment ermitteln
id: determine
shell: bash
run: |
REF="${{ github.ref }}"
REF_NAME="${{ github.ref_name }}"
REF_TYPE="${{ github.ref_type }}"
REF="${{ github.ref_name }}"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
DOMAIN_BASE="mb-grid-solutions.com"
PRJ_ID="mb-grid-solutions"
DOMAIN="mb-grid-solutions.com"
PRJ="mb-grid-solutions"
echo "Detecting environment for ref: $REF ($REF_NAME, type: $REF_TYPE)"
# Fallback for REF_TYPE if missing
if [[ -z "$REF_TYPE" ]]; then
if [[ "$REF" == refs/tags/* ]]; then
REF_TYPE="tag"
elif [[ "$REF" == refs/heads/* ]]; then
REF_TYPE="branch"
fi
fi
if [[ "$REF_TYPE" == "branch" && "$REF_NAME" == "main" ]]; then
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
TARGET="testing"
IMAGE_TAG="testing-${SHORT_SHA}"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="testing.${DOMAIN_BASE}"
GATEKEEPER_HOST="gatekeeper.testing.${DOMAIN_BASE}"
NEXT_PUBLIC_BASE_URL="https://testing.${DOMAIN_BASE}"
DIRECTUS_URL="https://cms.testing.${DOMAIN_BASE}"
DIRECTUS_HOST="cms.testing.${DOMAIN_BASE}"
elif [[ "$REF_TYPE" == "tag" ]]; then
if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
TRAEFIK_HOST="testing.${DOMAIN}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
TARGET="production"
IMAGE_TAG="$REF_NAME"
IMAGE_TAG="$REF"
ENV_FILE=".env.prod"
TRAEFIK_HOST="${DOMAIN_BASE}" # Primary domain
GATEKEEPER_HOST="gatekeeper.${DOMAIN_BASE}"
NEXT_PUBLIC_BASE_URL="https://${DOMAIN_BASE}"
DIRECTUS_URL="https://cms.${DOMAIN_BASE}"
DIRECTUS_HOST="cms.${DOMAIN_BASE}"
elif [[ "$REF_NAME" =~ -rc || "$REF_NAME" =~ -beta || "$REF_NAME" =~ -alpha ]]; then
TARGET="staging"
IMAGE_TAG="$REF_NAME"
ENV_FILE=".env.staging"
TRAEFIK_HOST="staging.${DOMAIN_BASE}"
GATEKEEPER_HOST="gatekeeper.staging.${DOMAIN_BASE}"
NEXT_PUBLIC_BASE_URL="https://staging.${DOMAIN_BASE}"
DIRECTUS_URL="https://cms.staging.${DOMAIN_BASE}"
DIRECTUS_HOST="cms.staging.${DOMAIN_BASE}"
TRAEFIK_HOST="${DOMAIN}, www.${DOMAIN}"
else
TARGET="skip"
echo "Tag $REF_NAME did not match any environment pattern."
TARGET="staging"
IMAGE_TAG="$REF"
ENV_FILE=".env.staging"
TRAEFIK_HOST="staging.${DOMAIN}"
fi
else
TARGET="skip"
echo "Ref type $REF_TYPE is not handled for deployment."
fi
# Determine Rules based on target (if not skipped)
if [[ "$TARGET" != "skip" ]]; then
if [[ "$TARGET" == "production" ]]; then
TRAEFIK_RULE="Host(\`${DOMAIN_BASE}\`) || Host(\`www.${DOMAIN_BASE}\`)"
GATEKEEPER_RULE="(Host(\`${DOMAIN_BASE}\`) || Host(\`www.${DOMAIN_BASE}\`)) && PathPrefix(\`/gatekeeper\`) || Host(\`gatekeeper.${DOMAIN_BASE}\`)"
TRAEFIK_MIDDLEWARES="compress"
# Standardize Traefik Rule
if [[ "$TRAEFIK_HOST" == *","* ]]; then
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\`%s\`)%s", $i, (i==NF?"":" || ")}')
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
else
TRAEFIK_RULE="Host(\`${TRAEFIK_HOST}\`)"
GATEKEEPER_RULE="(Host(\`${TRAEFIK_HOST}\`) && PathPrefix(\`/gatekeeper\`)) || Host(\`gatekeeper.${TRAEFIK_HOST}\`)"
TRAEFIK_MIDDLEWARES="${PRJ_ID}-${TARGET}-auth"
TRAEFIK_RULE="Host(\`$TRAEFIK_HOST\`)"
PRIMARY_HOST="$TRAEFIK_HOST"
fi
{
echo "target=$TARGET"
echo "image_tag=$IMAGE_TAG"
echo "env_file=$ENV_FILE"
echo "traefik_host=$PRIMARY_HOST"
echo "traefik_rule=$TRAEFIK_RULE"
echo "next_public_url=https://$PRIMARY_HOST"
echo "project_name=$PRJ-$TARGET"
echo "short_sha=$SHORT_SHA"
} >> "$GITHUB_OUTPUT"
# ⏳ Wait for Upstream Packages/Images if Tagged
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "🔎 Checking for @mintel dependencies in package.json..."
# Extract any @mintel/ version (they should be synced in monorepo)
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | grep -v "next-utils" | cut -d'"' -f4 | sed 's/\^//; s/\~//' | sort -V | tail -1)
TAG_TO_WAIT="v$UPSTREAM_VERSION"
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
# 1. Discovery (Works without token for public repositories)
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" | grep "$TAG_TO_WAIT" | tail -n1 | awk '{print $1}')
if [[ -z "$UPSTREAM_SHA" ]]; then
echo "❌ Error: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
exit 1
fi
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
# 2. Status Check (Requires PAT for cross-repo API access)
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
if [[ -n "$POLL_TOKEN" ]]; then
echo "⏳ POLL_TOKEN found. Checking upstream build status..."
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
chmod +x wait-for-upstream.sh
GITEA_TOKEN="$POLL_TOKEN" ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
else
echo " No PAT secret found. Skipping build status wait (Actions API is restricted)."
echo " If this build fails, ensure that mmintel/at-mintel $TAG_TO_WAIT has finished its Docker build."
fi
fi
fi
else
echo "target=skip" >> "$GITHUB_OUTPUT"
fi
echo "Target determined: $TARGET"
echo "Image tag: $IMAGE_TAG"
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
echo "env_file=$ENV_FILE" >> "$GITHUB_OUTPUT"
echo "traefik_host=$TRAEFIK_HOST" >> "$GITHUB_OUTPUT"
echo "traefik_rule=$TRAEFIK_RULE" >> "$GITHUB_OUTPUT"
echo "gatekeeper_rule=$GATEKEEPER_RULE" >> "$GITHUB_OUTPUT"
echo "traefik_middlewares=$TRAEFIK_MIDDLEWARES" >> "$GITHUB_OUTPUT"
echo "gatekeeper_host=$GATEKEEPER_HOST" >> "$GITHUB_OUTPUT"
echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" >> "$GITHUB_OUTPUT"
echo "directus_url=$DIRECTUS_URL" >> "$GITHUB_OUTPUT"
echo "directus_host=$DIRECTUS_HOST" >> "$GITHUB_OUTPUT"
echo "project_name=$PRJ_ID-$TARGET" >> "$GITHUB_OUTPUT"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: QA (Lint, Build Test)
# ──────────────────────────────────────────────────────────────────────────────
qa:
name: 🧪 QA
needs: prepare
@@ -147,31 +140,44 @@ 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
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
shell: bash
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: 🔐 Registry Auth
run: |
corepack enable
pnpm install --frozen-lockfile
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: 🧪 Lint
shell: bash
run: pnpm lint
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
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: |
rm -rf .next .turbo node_modules || true
pnpm store prune
pnpm install --no-frozen-lockfile
- name: 🧪 QA Checks
if: github.event.inputs.skip_checks != 'true'
run: |
pnpm lint
pnpm exec tsc --noEmit
pnpm test run
- name: 🏗️ Build Test
shell: bash
if: github.event.inputs.skip_checks != 'true'
run: pnpm build
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NEXT_PUBLIC_BASE_URL: https://dummy.test
# ──────────────────────────────────────────────────────────────────────────────
# JOB 3: Build & Push
# ──────────────────────────────────────────────────────────────────────────────
build:
name: 🏗️ Build
needs: prepare
@@ -182,134 +188,341 @@ 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: 🧹 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
shell: bash
run: |
docker buildx build \
--pull \
--platform linux/arm64 \
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} \
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
--build-arg UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} \
--build-arg 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 }} \
-t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} \
--push .
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64
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
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mb-grid-solutions:buildcache,mode=max
secrets: |
"NPM_TOKEN=${{ secrets.REGISTRY_PASS }}"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy
# ──────────────────────────────────────────────────────────────────────────────
deploy:
name: 🚀 Deploy
needs: [prepare, build, qa]
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
TARGET: ${{ needs.prepare.outputs.target }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
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.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' }}
POSTGRES_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_POSTGRES_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_POSTGRES_PASSWORD) || secrets.POSTGRES_PASSWORD || vars.POSTGRES_PASSWORD || 'directus' }}
# Secrets mapping (Mail)
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
# Authentication
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
AUTH_COOKIE_NAME: ${{ secrets.AUTH_COOKIE_NAME || vars.AUTH_COOKIE_NAME || 'mintel_gatekeeper_session' }}
COOKIE_DOMAIN: ${{ secrets.COOKIE_DOMAIN || vars.COOKIE_DOMAIN || '.mb-grid-solutions.com' }}
# Monitoring & Services
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
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' }}
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#82ed20' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 📝 Generate Environment
shell: bash
env:
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
# Middleware & Auth Logic
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
STD_MW="${PROJECT_NAME}-forward,compress"
if [[ "$TARGET" == "production" ]]; then
AUTH_MIDDLEWARE="$STD_MW"
COMPOSE_PROFILES=""
else
# Order: Forward (Proto) -> Auth -> Compression
AUTH_MIDDLEWARE="${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,compress"
COMPOSE_PROFILES="gatekeeper"
fi
# Gatekeeper Origin
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
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
# Database & Payload
DATABASE_URI=\${DATABASE_URI:-postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@mb-grid-db:5432/$POSTGRES_DB}
PAYLOAD_SECRET=${PAYLOAD_SECRET:-you-need-to-set-a-payload-secret}
POSTGRES_DB=$POSTGRES_DB
POSTGRES_USER=$POSTGRES_USER
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
# Mail
MAIL_HOST=$MAIL_HOST
MAIL_PORT=$MAIL_PORT
MAIL_USERNAME=$MAIL_USERNAME
MAIL_PASSWORD=$MAIL_PASSWORD
MAIL_FROM=$MAIL_FROM
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
# Authentication
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
AUTH_COOKIE_NAME=$AUTH_COOKIE_NAME
COOKIE_DOMAIN=$COOKIE_DOMAIN
# Analytics
UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
TARGET=$TARGET
SENTRY_ENVIRONMENT=$TARGET
PROJECT_NAME=$PROJECT_NAME
ENV_FILE=$ENV_FILE
TRAEFIK_HOST=$TRAEFIK_HOST
COMPOSE_PROFILES=$COMPOSE_PROFILES
TRAEFIK_MIDDLEWARES=$AUTH_MIDDLEWARE
EOF
- name: 🚀 SSH Deploy
shell: bash
env:
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
# Transfer and Restart
SITE_DIR="/home/deploy/sites/mb-grid-solutions.com"
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR"
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
scp docker-compose.yaml root@alpha.mintel.me:$SITE_DIR/docker-compose.yaml
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
# 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..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 -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'"
- name: 🧹 Post-Deploy Cleanup (Runner)
if: always()
run: docker builder prune -f --filter "until=1h"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Post-Deploy Verification (Smoke Tests + Quality Gates)
# ──────────────────────────────────────────────────────────────────────────────
post_deploy_checks:
name: 🧪 Post-Deploy Verification
needs: [prepare, deploy]
if: needs.deploy.result == 'success' && needs.prepare.outputs.target != 'branch'
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:
fetch-depth: 1
- name: 🚀 Deploy via SSH
shell: bash
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "Deploying to alpha.mintel.me"
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
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
id: deps
run: |
pnpm store prune
pnpm install --no-frozen-lockfile
- name: 📦 Cache APT Packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-cache-${{ runner.os }}-${{ runner.arch }}-chromium
# Generate Environment File
cat > .env.deploy << 'EOF'
ENV_FILE=${{ needs.prepare.outputs.env_file }}
IMAGE_TAG=${{ needs.prepare.outputs.image_tag }}
TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }}
TRAEFIK_RULE=${{ needs.prepare.outputs.traefik_rule }}
TRAEFIK_MIDDLEWARES=${{ needs.prepare.outputs.traefik_middlewares }}
GATEKEEPER_RULE=${{ needs.prepare.outputs.gatekeeper_rule }}
GATEKEEPER_HOST=${{ needs.prepare.outputs.gatekeeper_host }}
PROJECT_NAME=${{ needs.prepare.outputs.project_name }}
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }}
- name: 💾 Cache Chromium
id: cache-chromium
uses: actions/cache@v4
with:
path: /usr/bin/chromium
key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }}
# Directus
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }}
INTERNAL_DIRECTUS_URL=http://directus:8055
DIRECTUS_API_TOKEN=${{ secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }}
DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }}
DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }}
DIRECTUS_DB_NAME=${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
DIRECTUS_DB_USER=${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD }}
DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }}
DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }}
# Mail
MAIL_HOST=${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
MAIL_PORT=${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
MAIL_USERNAME=${{ secrets.SMTP_USER || vars.SMTP_USER }}
MAIL_PASSWORD=${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
MAIL_FROM=${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
MAIL_RECIPIENTS=${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
- name: 🔍 Install Chromium (Native & ARM64)
if: steps.cache-chromium.outputs.cache-hit != 'true'
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
apt-get update
apt-get install -y gnupg wget ca-certificates
OS_ID=$(. /etc/os-release && echo $ID)
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
if [ "$OS_ID" = "debian" ]; then
apt-get install -y chromium
else
mkdir -p /etc/apt/keyrings
KEY_ID="82BB6851C64F6880"
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update
apt-get install -y --allow-downgrades chromium
fi
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
# Authentication
GATEKEEPER_PASSWORD=${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
AUTH_COOKIE_NAME=${{ secrets.AUTH_COOKIE_NAME || vars.AUTH_COOKIE_NAME || 'mintel_gatekeeper_session' }}
COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN || vars.COOKIE_DOMAIN || '.mb-grid-solutions.com' }}
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
- name: 🏥 CMS Deep Health Check
env:
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
GK_PASS: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
run: |
echo "Waiting 10s for app to fully start..."
sleep 10
echo "Checking basic health..."
curl -sf "$DEPLOY_URL/api/health" || { echo "❌ Basic health check failed"; exit 1; }
echo "✅ Basic health OK"
# External Services
SENTRY_DSN=${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
GOTIFY_URL=${{ secrets.GOTIFY_URL || vars.GOTIFY_URL }}
GOTIFY_TOKEN=${{ secrets.GOTIFY_TOKEN || vars.GOTIFY_TOKEN }}
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
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' }}
# Project
PROJECT_COLOR=${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#82ed20' }}
EOF
- name: 🌐 Core Smoke Tests (HTTP, API, Locale)
if: always() && steps.deps.outcome == 'success'
uses: https://git.infra.mintel.me/mmintel/at-mintel/.gitea/actions/core-smoke-tests@main
with:
TARGET_URL: ${{ needs.prepare.outputs.next_public_url }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
APP_DIR="/home/deploy/sites/mb-grid-solutions.com"
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me "mkdir -p $APP_DIR"
scp -o StrictHostKeyChecking=accept-new .env.deploy root@alpha.mintel.me:$APP_DIR/${{ needs.prepare.outputs.env_file }}
scp -o StrictHostKeyChecking=accept-new docker-compose.yaml root@alpha.mintel.me:$APP_DIR/docker-compose.yaml
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << 'EOF'
set -e
APP_DIR="/home/deploy/sites/mb-grid-solutions.com"
cd $APP_DIR
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} pull
docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} up -d --wait --remove-orphans
docker system prune -f --filter "until=24h"
EOF
- name: 📝 E2E Form Submission Test
if: always() && steps.deps.outcome == 'success'
env:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
run: pnpm test run
# ──────────────────────────────────────────────────────────────────────────────
# JOB 6: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notifications
needs: [prepare, deploy]
name: 🔔 Notify
needs: [prepare, deploy, post_deploy_checks]
if: always()
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Notify Gotify
- name: 🔔 Gotify
shell: bash
run: |
STATUS="${{ needs.deploy.result }}"
COLOR="info"
[[ "$STATUS" == "success" ]] && PRIORITY=5 || PRIORITY=8
curl -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=mb-grid-solutions Deployment" \
-F "message=Status: $STATUS for ${{ needs.prepare.outputs.target }} (${{ needs.prepare.outputs.image_tag }})" \
-F "priority=$PRIORITY"
DEPLOY="${{ needs.deploy.result }}"
SMOKE="${{ needs.post_deploy_checks.result }}"
PERF="${{ needs.post_deploy_checks.result }}"
TARGET="${{ needs.prepare.outputs.target }}"
VERSION="${{ needs.prepare.outputs.image_tag }}"
URL="${{ needs.prepare.outputs.next_public_url }}"
# Gotify priority scale:
# 1-3 = low (silent/info)
# 4-5 = normal
# 6-7 = high (warning)
# 8-10 = critical (alarm)
if [[ "$DEPLOY" != "success" ]]; then
PRIORITY=10
EMOJI="🚨"
STATUS_LINE="DEPLOY FAILED"
elif [[ "$SMOKE" != "success" ]]; then
PRIORITY=8
EMOJI="⚠️"
STATUS_LINE="Smoke tests failed"
elif [[ "$PERF" != "success" ]]; then
PRIORITY=5
EMOJI="📉"
STATUS_LINE="Performance degraded"
else
PRIORITY=2
EMOJI="✅"
STATUS_LINE="All checks passed"
fi
TITLE="$EMOJI mb-grid-solutions.com $VERSION → $TARGET"
MESSAGE="$STATUS_LINE
Deploy: $DEPLOY | Smoke: $SMOKE | 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

106
.gitea/workflows/qa.yml Normal file
View File

@@ -0,0 +1,106 @@
name: Nightly QA (Inlined)
on:
push:
branches: [fix/nightly-qa-stabilization]
schedule:
- cron: '0 2 * * *' # Every night at 02:00
workflow_dispatch:
jobs:
qa:
name: 🧪 Quality Assurance
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NEXT_PUBLIC_BASE_URL: https://testing.mb-grid-solutions.com
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
steps:
- name: 🛑 Cleanup Workspace
run: |
rm -rf .next .turbo node_modules || true
- 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 || secrets.REGISTRY_PASS }}" >> .npmrc
- name: 🏗️ Install Dependencies
run: pnpm install --frozen-lockfile
- name: 🛠️ Patch @mintel/cli
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: 🔍 Static Analysis
id: static
run: |
echo "Running Lint..."
pnpm run lint
echo "Running Depcheck..."
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,@payloadcms/ui,@commitlint/*"
- name: ♿ Accessibility Audit
id: a11y
continue-on-error: true
run: pnpm run check:wcag
- name: 🔗 Link Checker
id: links
continue-on-error: true
run: |
curl -sLO https://github.com/lycheeverse/lychee/releases/download/v0.14.3/lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
tar -xzf lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
mv lychee /usr/local/bin/
lychee --accept 200,429,999 --exclude-mail --exclude "https://www.linkedin.com/.*" --exclude "https://fonts.gstatic.com/.*" https://testing.mb-grid-solutions.com
- name: 🎭 Performance & Lighthouse
id: perf
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=desktop
- name: 🔔 Notify Status
if: always()
run: |
STATUS="success"
if [[ "${{ steps.static.outcome }}" != "success" || "${{ steps.perf.outcome }}" != "success" ]]; then
STATUS="failure"
fi
PAYLOAD=$(cat <<EOF
{
"status": "$STATUS",
"project": "mb-grid-solutions.com",
"workflow": "Nightly QA",
"details": {
"Static": "${{ steps.static.outcome }}",
"A11y": "${{ steps.a11y.outcome }}",
"Links": "${{ steps.links.outcome }}",
"Perf": "${{ steps.perf.outcome }}"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
EOF
)
curl -X POST -H "Content-Type: application/json" \
-d "$PAYLOAD" \
${{ secrets.WEBHOOK_URL }} || echo "Notification failed"

6
.htmlvalidate-tmp/_.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

26
.htmlvalidate.json Normal file
View 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
View 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"
]
}

View File

@@ -1,57 +1,68 @@
# Start from the pre-built Nextjs Base image
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
# Ensure we are in a clean, standalone environment
RUN rm -rf packages apps pnpm-workspace.yaml 2>/dev/null || true
# Clean the workspace
RUN rm -rf ./*
# Build-time environment variables for Next.js
# Arguments for build-time configuration
ARG NEXT_PUBLIC_BASE_URL
ARG UMAMI_API_ENDPOINT
ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ARG UMAMI_API_ENDPOINT
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NPM_TOKEN
# Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NPM_TOKEN=$NPM_TOKEN
ENV SENTRY_SUPPRESS_TURBOPACK_WARNING=1
ENV SKIP_RUNTIME_ENV_VALIDATION=true
ENV CI=true
# Enable corepack
RUN corepack enable
# Enable pnpm
RUN corepack enable && corepack prepare pnpm@10.3.0 --activate
# Copy package files
COPY package.json pnpm-lock.yaml* .npmrc ./
# Copy lockfile and manifest for dependency installation caching
COPY pnpm-lock.yaml package.json .npmrc* ./
# Install dependencies
RUN pnpm install --no-frozen-lockfile
# Install dependencies with cache mount
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
pnpm store prune && \
pnpm install --no-frozen-lockfile
# Copy local files
# Copy source code
COPY . .
# Build the specific application
# Build application
RUN pnpm build
# Production runner image
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
# Production environment configuration
# Stage 2: Runner
FROM node:20-alpine AS runner
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
# Create nextjs user and group for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs && \
chown -R nextjs:nodejs /app
ENV HOSTNAME="0.0.0.0"
ENV PORT=3000
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Copy standalone output and static files
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Ensure the cache directory specifically is writeable (Mintel Standard #16)
RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next/cache
COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
USER nextjs

View File

@@ -1,108 +1,37 @@
import React from "react";
import { Download } from "lucide-react";
import fs from "fs";
import path from "path";
import { notFound } from "next/navigation";
import { LegalLayout } from "@/components/LegalLayout";
export default function AGB() {
const filePath = path.join(process.cwd(), "context/agbs.md");
const fileContent = fs.readFileSync(filePath, "utf8");
interface PageProps {
params: Promise<{ locale: string }>;
}
// Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs)
const blocks = fileContent
.split(/\n\s*\n/)
.map((b) => b.trim())
.filter((b) => b !== "");
export default async function Page({ params }: PageProps) {
const { locale } = await params;
const title = blocks[0] || "Liefer- und Zahlungsbedingungen";
const stand = blocks[1] || "Stand Januar 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;
const firstLine = lines[0];
if (/^\d+\./.test(firstLine)) {
// New section
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);
}
} else if (currentSection) {
// Continuation of current section
const blockText = lines.join(" ");
if (blockText) currentSection.content.push(blockText);
}
});
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();
}
}
let Content: React.ComponentType;
try {
const mdx = await import(`@/content/${locale}/avb.mdx`);
Content = mdx.default;
} catch (e) {
notFound();
}
const downloadButton = (
<a
href="/assets/avb-mb-grid-4-2026.pdf"
download
className="btn-primary !py-3 !px-6 flex items-center gap-2"
>
<Download size={18} />
Als PDF herunterladen
</a>
);
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
<div className="container-custom">
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
<div>
<h1 className="text-4xl font-extrabold text-primary mb-2">
{title}
</h1>
<p className="text-slate-500 font-medium">{stand}</p>
</div>
<a
href="/assets/AGB MB Grid 1-2026.pdf"
download
className="btn-primary !py-3 !px-6 flex items-center gap-2"
>
<Download size={18} />
Als PDF herunterladen
</a>
</div>
<div className="space-y-8 text-slate-600 leading-relaxed">
{sections.map((section, index) => (
<div key={index}>
<h2 className="text-2xl font-bold text-primary mb-4">
{section.title}
</h2>
<div className="space-y-4">
{section.content.map((paragraph, pIndex) => (
<p key={pIndex}>{paragraph}</p>
))}
</div>
</div>
))}
<div className="pt-8 border-t border-slate-100">
<p className="font-bold text-primary">{footer}</p>
</div>
</div>
</div>
</div>
</div>
<LegalLayout title="AVB" heroAction={downloadButton}>
<Content />
</LegalLayout>
);
}

View File

@@ -1,66 +1,25 @@
export default function Privacy() {
import React from "react";
import { notFound } from "next/navigation";
import { LegalLayout } from "@/components/LegalLayout";
interface PageProps {
params: Promise<{ locale: string }>;
}
export default async function Page({ params }: PageProps) {
const { locale } = await params;
let Content: React.ComponentType;
try {
const mdx = await import(`@/content/${locale}/datenschutz.mdx`);
Content = mdx.default;
} catch (e) {
notFound();
}
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
<div className="container-custom">
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
<h1 className="text-4xl font-extrabold text-primary mb-8">
Datenschutzerklärung
</h1>
<div className="space-y-8 text-slate-600 leading-relaxed">
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
1. Datenschutz auf einen Blick
</h2>
<p>
Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir
behandeln Ihre personenbezogenen Daten vertraulich und
entsprechend der gesetzlichen Datenschutzvorschriften sowie
dieser Datenschutzerklärung.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
2. Hosting
</h2>
<p>
Unsere Website wird bei Hetzner Online GmbH gehostet. Der
Serverstandort ist Deutschland. Wir haben einen Vertrag über
Auftragsverarbeitung (AVV) mit Hetzner geschlossen.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
3. Kontaktformular
</h2>
<p>
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen,
werden Ihre Angaben aus dem Anfrageformular inklusive der von
Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der
Anfrage und für den Fall von Anschlussfragen bei uns
gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung
weiter.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
4. Server-Log-Dateien
</h2>
<p>
Der Provider der Seiten erhebt und speichert automatisch
Informationen in sogenannten Server-Log-Dateien, die Ihr Browser
automatisch an uns übermittelt. Dies sind: Browsertyp und
Browserversion, verwendetes Betriebssystem, Referrer URL,
Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage,
IP-Adresse.
</p>
</div>
</div>
</div>
</div>
</div>
<LegalLayout title="Datenschutz">
<Content />
</LegalLayout>
);
}

View File

@@ -1,101 +1,25 @@
"use client";
import React from "react";
import { notFound } from "next/navigation";
import { LegalLayout } from "@/components/LegalLayout";
import { motion } from "framer-motion";
import { TechBackground } from "@/components/TechBackground";
interface PageProps {
params: Promise<{ locale: string }>;
}
export default async function Page({ params }: PageProps) {
const { locale } = await params;
let Content: React.ComponentType;
try {
const mdx = await import(`@/content/${locale}/impressum.mdx`);
Content = mdx.default;
} catch (e) {
notFound();
}
export default function Legal() {
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20 relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 50, damping: 20 }}
className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100 relative overflow-hidden group"
>
<div className="tech-corner top-8 left-8 border-t-2 border-l-2 opacity-20" />
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2 opacity-20" />
<h1 className="text-4xl font-extrabold text-primary mb-8 relative z-10">
Impressum
</h1>
<div className="space-y-8 text-slate-600 leading-relaxed relative z-10">
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Angaben gemäß § 5 TMG
</h2>
<p>
MB Grid Solutions & Services GmbH
<br />
Raiffeisenstraße 22
<br />
73630 Remshalden
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Vertreten durch
</h2>
<p>
Michael Bodemer
<br />
Klaus Mintel
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">Kontakt</h2>
<p>
E-Mail:{" "}
<a
href="mailto:info@mb-grid-solutions.com"
className="text-accent hover:underline"
>
info@mb-grid-solutions.com
</a>
<br />
Web:{" "}
<a
href="https://www.mb-grid-solutions.com"
className="text-accent hover:underline"
>
www.mb-grid-solutions.com
</a>
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Registereintrag
</h2>
<p>
Eintragung im Handelsregister.
<br />
Registergericht: Amtsgericht Stuttgart
<br />
Registernummer: HRB 803379
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Urheberrecht
</h2>
<p>
Alle auf der Website veröffentlichten Texte, Bilder und
sonstigen Informationen unterliegen sofern nicht anders
gekennzeichnet dem Urheberrecht. Jede Vervielfältigung,
Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw.
Weitergabe der Inhalte ohne schriftliche Genehmigung ist
ausdrücklich untersagt.
</p>
</div>
</div>
</motion.div>
</div>
</div>
<LegalLayout title="Impressum">
<Content />
</LegalLayout>
);
}

View File

@@ -5,7 +5,6 @@ import "../globals.css";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { LazyMotion, domAnimation } from "framer-motion";
import AnalyticsProvider from "@/components/analytics/AnalyticsProvider";
import { config } from "@/lib/config";
@@ -13,6 +12,7 @@ const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
weight: ["400", "700", "800"], // Explicit weights to optimize download
});
export const metadata: Metadata = {
@@ -114,8 +114,22 @@ export default async function RootLayout({
await import("@/lib/services/create-services.server")
).getServerAppServices();
// Populate analytics context with headers for high-fidelity server-side tracking
const { headers } = await import("next/headers");
const requestHeaders = await headers();
if (serverServices.analytics.setServerContext) {
serverServices.analytics.setServerContext({
userAgent: requestHeaders.get("user-agent") || undefined,
language:
requestHeaders.get("accept-language")?.split(",")[0] || undefined,
referrer: requestHeaders.get("referer") || undefined,
ip: requestHeaders.get("x-forwarded-for")?.split(",")[0] || undefined,
});
}
// Track server-side (initial load)
serverServices.analytics.trackPageview("/");
// serverServices.analytics.trackPageview("/"); // Removed to avoid double-tracking and incorrect path reporting
return (
<html lang={locale} className={`${inter.variable}`}>
@@ -128,9 +142,7 @@ export default async function RootLayout({
<body className="antialiased">
<NextIntlClientProvider messages={messages}>
<AnalyticsProvider websiteId={config.analytics.umami.websiteId} />
<LazyMotion features={domAnimation}>
<Layout>{children}</Layout>
</LazyMotion>
<Layout>{children}</Layout>
</NextIntlClientProvider>
</body>
</html>

View File

@@ -86,7 +86,7 @@ export default async function Image() {
letterSpacing: "0.1em",
}}
>
Engineering Excellence
Technische Beratung
</div>
</div>
@@ -118,6 +118,8 @@ export default async function Image() {
{/* Title */}
<div
style={{
display: "flex",
flexDirection: "row",
fontSize: "72px",
fontWeight: "900",
color: "#0f172a",
@@ -126,12 +128,19 @@ export default async function Image() {
letterSpacing: "-0.02em",
}}
>
MB Grid <span style={{ color: "#10b981" }}>Solutions</span>
MB Grid{" "}
<span
style={{ color: "#10b981", display: "flex", marginLeft: "16px" }}
>
Solutions
</span>
</div>
{/* Subtitle */}
<div
style={{
display: "flex",
flexDirection: "column",
fontSize: "32px",
fontWeight: "500",
color: "#64748b",
@@ -140,9 +149,8 @@ export default async function Image() {
lineHeight: 1.4,
}}
>
Energiekabelprojekte & Technische Beratung
<br />
bis 110 kV
<span>Energiekabelprojekte & Technische Beratung</span>
<span>bis 110 kV</span>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import React from "react";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import HomeContent from "@/components/HomeContent";
export const metadata: Metadata = {
@@ -7,6 +9,24 @@ export const metadata: Metadata = {
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.",
};
export default function Page() {
return <HomeContent />;
interface PageProps {
params: Promise<{ locale: string }>;
}
export default async function Page({ params }: PageProps) {
const { locale } = await params;
// Dynamically import the MDX content based on locale
let Content: React.ComponentType;
try {
const mdx = await import(`@/content/${locale}/index.mdx`);
Content = mdx.default;
} catch (e) {
// Fallback if MDX is not yet created for this locale
return <HomeContent />;
}
// Render the MDX content.
// We can use mdx-components.tsx to map MDX tags to our high-fidelity sections.
return <Content />;
}

View File

@@ -86,7 +86,7 @@ export default async function Image() {
letterSpacing: "0.1em",
}}
>
Engineering Excellence
Technische Beratung
</div>
</div>
@@ -118,6 +118,8 @@ export default async function Image() {
{/* Title */}
<div
style={{
display: "flex",
flexDirection: "row",
fontSize: "72px",
fontWeight: "900",
color: "#0f172a",
@@ -126,12 +128,19 @@ export default async function Image() {
letterSpacing: "-0.02em",
}}
>
MB Grid <span style={{ color: "#10b981" }}>Solutions</span>
MB Grid{" "}
<span
style={{ color: "#10b981", display: "flex", marginLeft: "16px" }}
>
Solutions
</span>
</div>
{/* Subtitle */}
<div
style={{
display: "flex",
flexDirection: "column",
fontSize: "32px",
fontWeight: "500",
color: "#64748b",
@@ -140,9 +149,8 @@ export default async function Image() {
lineHeight: 1.4,
}}
>
Energiekabelprojekte & Technische Beratung
<br />
bis 110 kV
<span>Energiekabelprojekte & Technische Beratung</span>
<span>bis 110 kV</span>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import React from "react";
import { Metadata } from "next";
import AboutContent from "@/components/AboutContent";
import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "Über uns",
@@ -7,6 +8,21 @@ export const metadata: Metadata = {
"Erfahren Sie mehr über MB Grid Solutions, unsere Expertise und unser Manifest für technische Exzellenz.",
};
export default function Page() {
return <AboutContent />;
interface PageProps {
params: Promise<{ locale: string }>;
}
export default async function Page({ params }: PageProps) {
const { locale } = await params;
// Dynamically import the MDX content based on locale
let Content: React.ComponentType;
try {
const mdx = await import(`@/content/${locale}/about.mdx`);
Content = mdx.default;
} catch (e) {
notFound();
}
return <Content />;
}

View File

@@ -1,16 +1,33 @@
import { NextResponse } from "next/server";
import * as nodemailer from "nodemailer";
import directus, { ensureAuthenticated } from "@/lib/directus";
import { createItem } from "@directus/sdk";
import { getServerAppServices } from "@/lib/services/create-services.server";
import {
render,
ContactFormNotification,
ConfirmationMessage,
} from "@mintel/mail";
import React from "react";
import nodemailer from "nodemailer";
export async function POST(req: Request) {
const services = getServerAppServices();
const logger = services.logger.child({ action: "contact_submission" });
// Set analytics context from request headers for high-fidelity server-side tracking
if (services.analytics.setServerContext) {
services.analytics.setServerContext({
userAgent: req.headers.get("user-agent") || undefined,
language: req.headers.get("accept-language")?.split(",")[0] || undefined,
referrer: req.headers.get("referer") || undefined,
ip: req.headers.get("x-forwarded-for")?.split(",")[0] || undefined,
});
}
try {
const { name, email, company, message, website } = await req.json();
// Track attempt
services.analytics.track("contact-form-attempt");
// Honeypot check
if (website) {
logger.info("Spam detected (honeypot)");
@@ -27,70 +44,100 @@ export async function POST(req: Request) {
if (!message || message.length < 20) {
return NextResponse.json({ error: "message_too_short" }, { status: 400 });
}
if (message.length > 4000) {
return NextResponse.json({ error: "message_too_long" }, { status: 400 });
}
// 1. Directus save
let directusSaved = false;
try {
await ensureAuthenticated();
await directus.request(
createItem("contact_submissions", {
name,
email,
company: company || "Nicht angegeben",
message,
}),
);
logger.info("Contact submission saved to Directus");
directusSaved = true;
} catch (directusError) {
const errorMessage =
directusError instanceof Error
? directusError.message
: String(directusError);
logger.error("Failed to save to Directus", {
error: errorMessage,
details: directusError,
});
services.errors.captureException(directusError, {
phase: "directus_save",
});
// We still try to send the email even if Directus fails
}
// 2. Email sending
// 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,
secure: config.mail.port === 465,
auth: {
user: config.mail.user,
pass: config.mail.pass,
},
});
await transporter.sendMail({
from: config.mail.from,
to: config.mail.recipients.join(",") || "info@mb-grid-solutions.com",
replyTo: email,
subject: `Kontaktanfrage von ${name}`,
text: `
Name: ${name}
Firma: ${company || "Nicht angegeben"}
E-Mail: ${email}
Zeitpunkt: ${new Date().toISOString()}
Nachricht:
${message}
`,
logger.info("Preparing to send notification email", {
to,
host: config.mail.host,
});
logger.info("Email sent successfully");
// 2a. Notification to MB Grid
const notificationHtml = await render(
React.createElement(ContactFormNotification, {
name,
email,
message,
company,
}),
);
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,
clientName,
}),
);
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 instanceof Error
? confirmError.message
: String(confirmError),
},
);
}
logger.info("Emails sent successfully");
// Notify success for important leads
await services.notifications.notify({
@@ -102,22 +149,17 @@ ${message}
logger.error("SMTP Error", { error: smtpError });
services.errors.captureException(smtpError, { phase: "smtp_send" });
// If Directus failed AND SMTP failed, then we really have a problem
if (!directusSaved) {
return NextResponse.json(
{ error: "Systemfehler (Speicherung und Versand fehlgeschlagen)" },
{ status: 500 },
);
}
// If Directus was successful, we tell the user "Ok" but we know internally it was a partial failure
await services.notifications.notify({
title: "🚨 SMTP Fehler (Kontaktformular)",
message: `Anfrage von ${name} (${email}) in Directus gespeichert, aber E-Mail-Versand fehlgeschlagen: ${smtpError instanceof Error ? smtpError.message : String(smtpError)}`,
priority: 8,
});
return NextResponse.json(
{ error: "Systemfehler (E-Mail-Versand fehlgeschlagen)" },
{ status: 500 },
);
}
// Track success
services.analytics.track("contact-form-success", {
has_company: Boolean(company),
});
return NextResponse.json({ message: "Ok" });
} catch (error) {
logger.error("Global API Error", { error });

View File

@@ -11,10 +11,10 @@ import {
ShieldCheck,
Truck,
} from "lucide-react";
import { Reveal } from "./Reveal";
import { TechBackground } from "./TechBackground";
import { Counter } from "./Counter";
import { Button } from "./Button";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
import { Counter } from "@/components/Counter";
import { Button } from "@/components/Button";
import { useTranslations } from "next-intl";
export default function About() {
@@ -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>

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState } from "react";
import { m } from "framer-motion";
import { motion } from "framer-motion";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
@@ -62,7 +62,7 @@ export const Button = ({
);
const spotlight = (
<m.div
<motion.div
className="absolute inset-0 z-0 pointer-events-none transition-opacity duration-500"
style={{
opacity: isHovered ? 1 : 0,

View File

@@ -4,11 +4,11 @@ import React, { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { Mail, MapPin, CheckCircle } from "lucide-react";
import { Button } from "./Button";
import { Counter } from "./Counter";
import { Reveal } from "./Reveal";
import { TechBackground } from "./TechBackground";
import { StatusModal } from "./StatusModal";
import { Button } from "@/components/Button";
import { Counter } from "@/components/Counter";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
import { StatusModal } from "@/components/StatusModal";
import { useTranslations } from "next-intl";
export default function Contact() {
@@ -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>
@@ -197,6 +193,7 @@ export default function Contact() {
) : (
<form
onSubmit={handleSubmit}
aria-label={t("form.submit")}
className="space-y-6 relative z-10"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">

View File

@@ -1,21 +1,29 @@
"use client";
import { m } from "framer-motion";
import {
BarChart3,
CheckCircle2,
ChevronRight,
Shield,
Zap,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "./Button";
import { Counter } from "./Counter";
import { Reveal } from "./Reveal";
import { TechBackground } from "./TechBackground";
import { Button } from "@/components/Button";
import { Counter } from "@/components/Counter";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
const PortfolioSection = dynamic(() =>
import("./sections/PortfolioSection").then((mod) => mod.PortfolioSection),
);
const ExpertiseSection = dynamic(() =>
import("./sections/ExpertiseSection").then((mod) => mod.ExpertiseSection),
);
const TechnicalSpecsSection = dynamic(() =>
import("./sections/TechnicalSpecsSection").then(
(mod) => mod.TechnicalSpecsSection,
),
);
const CTASection = dynamic(() =>
import("./sections/CTASection").then((mod) => mod.CTASection),
);
export default function Home() {
const t = useTranslations("Index");
@@ -74,7 +82,7 @@ export default function Home() {
fill
className="object-cover"
priority
quality={90}
quality={75}
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-100/80 via-white/90 to-white/40 md:to-transparent" />
<TechBackground />
@@ -127,272 +135,11 @@ export default function Home() {
</div>
</section>
{/* Portfolio Section */}
<section className="bg-slate-950 text-accent relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={2} className="section-number !text-white/5" />
<Reveal className="flex flex-col md:flex-row md:items-end justify-between gap-8 mb-16">
<div>
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{t("portfolio.tag")}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
{t("portfolio.title")}
</h2>
<p className="text-slate-400 text-base md:text-xl">
{t("portfolio.description")}
</p>
</div>
<Link
href="/ueber-uns"
className="text-accent font-bold flex items-center gap-2 hover:text-white transition-colors group"
>
{t("portfolio.link")}{" "}
<ChevronRight
className="transition-transform group-hover:translate-x-1"
size={20}
/>
</Link>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
icon: <Zap size={32} />,
title: t("portfolio.items.beratung.title"),
desc: t("portfolio.items.beratung.desc"),
},
{
icon: <Shield size={32} />,
title: t("portfolio.items.begleitung.title"),
desc: t("portfolio.items.begleitung.desc"),
},
{
icon: <BarChart3 size={32} />,
title: t("portfolio.items.beschaffung.title"),
desc: t("portfolio.items.beschaffung.desc"),
},
].map((item, i) => (
<Reveal key={i} delay={i * 0.1}>
<div className="bg-white/5 p-8 rounded-2xl border border-white/10 group hover:-translate-y-2 transition-[box-shadow,transform] duration-300 h-full relative overflow-hidden">
<div className="absolute top-0 right-0 w-16 h-16 bg-accent/5 -mr-8 -mt-8 rounded-full group-hover:bg-accent/10 transition-colors" />
<div className="w-16 h-16 rounded-2xl bg-accent/10 text-accent flex items-center justify-center mb-8 group-hover:bg-accent group-hover:text-white transition-colors relative z-10">
{item.icon}
</div>
<h3 className="text-2xl font-bold text-white mb-4 relative z-10">
{item.title}
</h3>
<p className="text-slate-400 leading-relaxed relative z-10">
{item.desc}
</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
{/* Expertise Section */}
<section className="bg-white relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={3} className="section-number" />
<div className="grid grid-cols-1 lg:grid-cols-2 items-center gap-16 md:gap-24">
<Reveal direction="right">
<div className="relative overflow-hidden rounded-2xl shadow-lg group">
<div className="absolute inset-0 bg-accent/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-10 pointer-events-none" />
<Image
src="/media/cables/hs-kabel.png"
alt="Technical Engineering"
width={800}
height={600}
className="w-full h-[400px] md:h-[500px] object-cover hover:scale-105 transition-transform duration-700"
/>
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 z-20" />
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 z-20" />
</div>
</Reveal>
<div>
<Reveal>
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{t("expertise.tag")}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-primary mb-6 md:mb-8">
{t("expertise.title")}
</h2>
<p className="text-slate-600 text-base md:text-xl mb-8 md:mb-12">
{t("expertise.description")}
</p>
</Reveal>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{t.raw("expertise.groups").map((item: string, i: number) => (
<Reveal key={i} delay={i * 0.05}>
<div className="flex items-center gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 hover:border-accent/30 transition-colors group relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-accent/0 group-hover:bg-accent/100 transition-all duration-300" />
<div className="w-8 h-8 rounded-full bg-white flex items-center justify-center shadow-sm group-hover:bg-accent group-hover:text-white transition-colors">
<CheckCircle2 size={16} />
</div>
<span className="text-primary font-semibold">{item}</span>
</div>
</Reveal>
))}
</div>
</div>
</div>
</div>
</section>
{/* Technical Specs Section */}
<section className="relative py-24 md:py-32 text-white overflow-hidden bg-slate-900">
<div className="absolute inset-0 opacity-20">
<Image
src="/media/drums/about-hero.jpg"
alt="Background"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-b from-slate-900 via-slate-900/80 to-slate-900" />
</div>
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={4} className="section-number !text-white/5" />
{/* Data Stream Effect */}
<div className="absolute -top-10 right-0 w-px h-64 bg-gradient-to-b from-transparent via-accent/50 to-transparent animate-pulse" />
<div className="absolute -bottom-10 left-10 w-px h-64 bg-gradient-to-b from-transparent via-accent/30 to-transparent animate-pulse delay-700" />
<Reveal className="mb-20">
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{t("specs.tag")}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
{t("specs.title")}
</h2>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{[
{
label: t("specs.items.kabel.label"),
value: t("specs.items.kabel.value"),
desc: t("specs.items.kabel.desc"),
},
{
label: t("specs.items.spannung.label"),
value: t("specs.items.spannung.value"),
desc: t("specs.items.spannung.desc"),
},
{
label: t("specs.items.technologie.label"),
value: t("specs.items.technologie.value"),
desc: t("specs.items.technologie.desc"),
},
].map((item, i) => (
<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">
{item.label}
</h4>
<p className="text-2xl font-bold text-white mb-4 leading-tight">
{item.value}
</p>
<p className="text-slate-400 leading-relaxed">{item.desc}</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-white relative overflow-hidden">
<TechBackground />
{/* Decorative Background Elements */}
<div className="absolute top-0 left-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl translate-x-1/2 translate-y-1/2" />
<div className="container-custom relative z-10">
<Counter value={5} className="section-number" />
<Reveal>
<div className="relative rounded-3xl md:rounded-[2.5rem] bg-primary p-8 md:p-24 overflow-hidden group">
{/* Corner Accents */}
<div className="tech-corner top-8 left-8 border-t-2 border-l-2" />
<div className="tech-corner top-8 right-8 border-t-2 border-r-2" />
<div className="tech-corner bottom-8 left-8 border-b-2 border-l-2" />
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2" />
<div className="absolute top-0 right-0 w-1/2 h-full opacity-10 pointer-events-none">
<svg
viewBox="0 0 400 400"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<m.circle
animate={{ r: [400, 410, 400], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut",
}}
cx="400"
cy="0"
r="400"
stroke="white"
strokeWidth="2"
/>
<m.circle
animate={{ r: [300, 310, 300], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5,
}}
cx="400"
cy="0"
r="300"
stroke="white"
strokeWidth="2"
/>
<m.circle
animate={{ r: [200, 210, 200], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: 1,
}}
cx="400"
cy="0"
r="200"
stroke="white"
strokeWidth="2"
/>
</svg>
</div>
<div className="relative z-10">
<h2 className="text-3xl md:text-6xl font-bold text-white mb-6 md:mb-8 leading-tight">
{t("cta.title")}
</h2>
<p className="text-slate-300 text-lg md:text-xl mb-8 md:mb-12 leading-relaxed">
{t("cta.subtitle")}
</p>
<Button
href="/kontakt"
variant="accent"
showArrow
className="w-full sm:w-auto !px-10 !py-5 text-lg"
>
{t("cta.button")}
</Button>
</div>
</div>
</Reveal>
</div>
</section>
{/* Dynamic Sections */}
<PortfolioSection />
<ExpertiseSection />
<TechnicalSpecsSection />
<CTASection />
</div>
);
}

View File

@@ -1,13 +1,13 @@
"use client";
import { AnimatePresence, m } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowUp, Home, Info, Menu, X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Button } from "./Button";
import { Reveal } from "./Reveal";
import { Button } from "@/components/Button";
import { Reveal } from "@/components/Reveal";
import { useTranslations } from "next-intl";
const Layout = ({ children }: { children: React.ReactNode }) => {
@@ -118,7 +118,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isMobileMenuOpen && (
<m.div
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
@@ -143,7 +143,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
{t("nav.cta")}
</Button>
</nav>
</m.div>
</motion.div>
)}
</AnimatePresence>
@@ -166,12 +166,12 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
<div className="absolute inset-0 grid-pattern opacity-[0.08] pointer-events-none" />
{/* Animated Tech Lines */}
<m.div
<motion.div
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
/>
<m.div
<motion.div
animate={{ x: ["100%", "-100%"] }}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
@@ -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>
@@ -275,7 +279,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
<div className="bg-slate-950 py-6 border-t border-white/5">
<div className="container-custom">
<p className="text-[10px] uppercase tracking-[0.2em] text-slate-600 text-center md:text-left">
Website developed by{" "}
Website entwickelt von{" "}
<a
href="https://mintel.me"
target="_blank"

View File

@@ -0,0 +1,39 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { TechBackground } from "@/components/TechBackground";
interface LegalLayoutProps {
title: string;
heroAction?: React.ReactNode;
children: React.ReactNode;
}
export function LegalLayout({ title, heroAction, children }: LegalLayoutProps) {
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20 relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 50, damping: 20 }}
className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100 relative overflow-hidden group"
>
<div className="tech-corner top-8 left-8 border-t-2 border-l-2 opacity-20" />
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2 opacity-20" />
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8 relative z-10">
<h1 className="text-4xl font-extrabold text-primary">{title}</h1>
{heroAction}
</div>
<div className="space-y-8 text-slate-600 leading-relaxed relative z-10 prose prose-slate max-w-none prose-h1:hidden prose-h2:text-xl prose-h2:font-bold prose-h2:text-primary prose-h2:mb-4">
{children}
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { m } from "framer-motion";
import { motion } from "framer-motion";
interface RevealProps {
children: React.ReactNode;
@@ -30,7 +30,7 @@ export const Reveal = ({
};
return (
<m.div
<motion.div
initial={{
opacity: 0,
...directions[direction],
@@ -68,7 +68,7 @@ export const Reveal = ({
className={`${fullWidth ? "w-full" : ""} ${className} motion-fix will-change-[transform,opacity]`}
>
{children}
</m.div>
</motion.div>
);
};
@@ -84,7 +84,7 @@ export const Stagger = ({
staggerDelay = 0.1,
}: StaggerProps) => {
return (
<m.div
<motion.div
initial="initial"
whileInView="animate"
viewport={{ once: true, margin: "-50px" }}
@@ -98,6 +98,6 @@ export const Stagger = ({
className={className}
>
{children}
</m.div>
</motion.div>
);
};

View File

@@ -1,9 +1,9 @@
"use client";
import React from "react";
import { m, AnimatePresence, LazyMotion, domAnimation } from "framer-motion";
import { motion, AnimatePresence } from "framer-motion";
import { CheckCircle, AlertCircle, X } from "lucide-react";
import { Button } from "./Button";
import { Button } from "@/components/Button";
interface StatusModalProps {
isOpen: boolean;
@@ -23,82 +23,80 @@ export const StatusModal = ({
buttonText,
}: StatusModalProps) => {
return (
<LazyMotion features={domAnimation}>
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6">
{/* Backdrop */}
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl overflow-hidden group"
>
{/* Tech Decoration */}
<div className="absolute top-0 left-0 w-full h-2 bg-slate-100 overflow-hidden">
<motion.div
initial={{ x: "-100%" }}
animate={{ x: "100%" }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
className={`absolute inset-0 w-1/2 ${type === "success" ? "bg-accent" : "bg-red-500"} opacity-30`}
/>
</div>
<button
onClick={onClose}
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
/>
{/* Modal Content */}
<m.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl overflow-hidden group"
className="absolute top-6 right-6 p-2 text-slate-400 hover:text-primary transition-colors hover:bg-slate-50 rounded-xl"
>
{/* Tech Decoration */}
<div className="absolute top-0 left-0 w-full h-2 bg-slate-100 overflow-hidden">
<m.div
initial={{ x: "-100%" }}
animate={{ x: "100%" }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
className={`absolute inset-0 w-1/2 ${type === "success" ? "bg-accent" : "bg-red-500"} opacity-30`}
/>
</div>
<X size={20} />
</button>
<button
onClick={onClose}
className="absolute top-6 right-6 p-2 text-slate-400 hover:text-primary transition-colors hover:bg-slate-50 rounded-xl"
<div className="p-8 md:p-12 text-center">
<div
className={`w-20 h-20 rounded-full ${type === "success" ? "bg-accent/10 text-accent" : "bg-red-50 text-red-500"} flex items-center justify-center mx-auto mb-8 relative`}
>
<X size={20} />
</button>
<div className="p-8 md:p-12 text-center">
<div
className={`w-20 h-20 rounded-full ${type === "success" ? "bg-accent/10 text-accent" : "bg-red-50 text-red-500"} flex items-center justify-center mx-auto mb-8 relative`}
>
<div
className={`absolute inset-0 ${type === "success" ? "bg-accent/20" : "bg-red-500/20"} rounded-full animate-ping opacity-20`}
/>
{type === "success" ? (
<CheckCircle size={40} />
) : (
<AlertCircle size={40} />
)}
</div>
<h3 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
{title}
</h3>
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
{message}
</p>
<Button
onClick={onClose}
variant={type === "success" ? "accent" : "primary"}
className="w-full py-5 text-lg"
showArrow
>
{buttonText}
</Button>
className={`absolute inset-0 ${type === "success" ? "bg-accent/20" : "bg-red-500/20"} rounded-full animate-ping opacity-20`}
/>
{type === "success" ? (
<CheckCircle size={40} />
) : (
<AlertCircle size={40} />
)}
</div>
{/* Decorative Corners */}
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 opacity-20" />
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 opacity-20" />
</m.div>
</div>
)}
</AnimatePresence>
</LazyMotion>
<h2 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
{title}
</h2>
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
{message}
</p>
<Button
onClick={onClose}
variant={type === "success" ? "accent" : "primary"}
className="w-full py-5 text-lg"
showArrow
>
{buttonText}
</Button>
</div>
{/* Decorative Corners */}
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 opacity-20" />
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 opacity-20" />
</motion.div>
</div>
)}
</AnimatePresence>
);
};

View File

@@ -0,0 +1,52 @@
"use client";
import React from "react";
import Image from "next/image";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
import { Counter } from "@/components/Counter";
interface AboutHeroProps {
tagline: string;
title: React.ReactNode;
subtitle: string;
}
export function AboutHero({ tagline, title, subtitle }: AboutHeroProps) {
return (
<section className="relative min-h-[60vh] flex items-center pt-44 pb-20 overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/media/drums/about-hero.jpg"
alt="About MB Grid Solutions"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-gradient-to-r from-white via-white/95 to-white/40" />
<TechBackground />
</div>
<div className="container-custom relative z-10">
<div className="text-left relative">
<Counter value={1} className="section-number" />
<Reveal delay={0.1}>
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{tagline}
</span>
</Reveal>
<Reveal delay={0.2}>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold text-primary mb-6 md:mb-8 leading-tight">
{title}
</h1>
</Reveal>
<Reveal delay={0.3}>
<p className="text-slate-600 text-lg md:text-2xl leading-relaxed mb-8">
{subtitle}
</p>
</Reveal>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import React from "react";
import { Linkedin } from "lucide-react";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
interface Person {
name: string;
role: string;
linkedin: string;
}
interface AboutIntroProps {
paragraphs: string[];
team: Person[];
}
export function AboutIntro({ paragraphs, team }: AboutIntroProps) {
return (
<section className="bg-white relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<div className="section-number">02</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<Reveal direction="right">
<div className="space-y-6 text-lg text-slate-600 leading-relaxed relative">
<div className="absolute -left-4 top-0 w-1 h-full bg-accent/10" />
{paragraphs.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
</Reveal>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{team.map((person, i) => (
<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">
<h2 className="text-xl font-bold text-primary">
{person.name}
</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>
</div>
<p className="text-accent text-sm font-bold uppercase tracking-wider relative z-10">
{person.role}
</p>
</div>
</Reveal>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,116 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { Reveal } from "@/components/Reveal";
import { Counter } from "@/components/Counter";
import { TechBackground } from "@/components/TechBackground";
import { Button } from "@/components/Button";
import { useTranslations } from "next-intl";
interface CTASectionProps {
title?: string;
subtitle?: string;
buttonText?: string;
}
export const CTASection = ({
title,
subtitle,
buttonText,
}: CTASectionProps) => {
const t = useTranslations("Index");
const displayTitle = title || t("cta.title");
const displaySubtitle = subtitle || t("cta.subtitle");
const displayButtonText = buttonText || t("cta.button");
return (
<section className="bg-white relative overflow-hidden">
<TechBackground />
{/* Decorative Background Elements */}
<div className="absolute top-0 left-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl translate-x-1/2 translate-y-1/2" />
<div className="container-custom relative z-10">
<Counter value={5} className="section-number" />
<Reveal>
<div className="relative rounded-3xl md:rounded-[2.5rem] bg-primary p-8 md:p-24 overflow-hidden group">
{/* Corner Accents */}
<div className="tech-corner top-8 left-8 border-t-2 border-l-2" />
<div className="tech-corner top-8 right-8 border-t-2 border-r-2" />
<div className="tech-corner bottom-8 left-8 border-b-2 border-l-2" />
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2" />
<div className="absolute top-0 right-0 w-1/2 h-full opacity-10 pointer-events-none">
<svg
viewBox="0 0 400 400"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<motion.circle
animate={{ r: [400, 410, 400], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut",
}}
cx="400"
cy="0"
r="400"
stroke="white"
strokeWidth="2"
/>
<motion.circle
animate={{ r: [300, 310, 300], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5,
}}
cx="400"
cy="0"
r="300"
stroke="white"
strokeWidth="2"
/>
<motion.circle
animate={{ r: [200, 210, 200], opacity: [0.1, 0.2, 0.1] }}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: 1,
}}
cx="400"
cy="0"
r="200"
stroke="white"
strokeWidth="2"
/>
</svg>
</div>
<div className="relative z-10">
<h2 className="text-3xl md:text-6xl font-bold text-white mb-6 md:mb-8 leading-tight">
{displayTitle}
</h2>
<p className="text-slate-300 text-lg md:text-xl mb-8 md:mb-12 leading-relaxed">
{displaySubtitle}
</p>
<Button
href="/kontakt"
variant="accent"
showArrow
className="w-full sm:w-auto !px-10 !py-5 text-lg"
>
{displayButtonText}
</Button>
</div>
</div>
</Reveal>
</div>
</section>
);
};

View File

@@ -0,0 +1,81 @@
"use client";
import React from "react";
import Image from "next/image";
import { CheckCircle2 } from "lucide-react";
import { Reveal } from "@/components/Reveal";
import { Counter } from "@/components/Counter";
import { TechBackground } from "@/components/TechBackground";
import { useTranslations } from "next-intl";
interface ExpertiseSectionProps {
tag?: string;
title?: string;
description?: string;
groups?: string[];
}
export const ExpertiseSection = ({
tag,
title,
description,
groups,
}: ExpertiseSectionProps) => {
const t = useTranslations("Index");
const displayTag = tag || t("expertise.tag");
const displayTitle = title || t("expertise.title");
const displayDescription = description || t("expertise.description");
const displayGroups = groups || t.raw("expertise.groups");
return (
<section className="bg-white relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={3} className="section-number" />
<div className="grid grid-cols-1 lg:grid-cols-2 items-center gap-16 md:gap-24">
<Reveal direction="right">
<div className="relative overflow-hidden rounded-2xl shadow-lg group">
<div className="absolute inset-0 bg-accent/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-10 pointer-events-none" />
<Image
src="/media/cables/hs-kabel.png"
alt="Technische Beratung"
width={800}
height={600}
className="w-full h-[400px] md:h-[500px] object-cover hover:scale-105 transition-transform duration-700"
/>
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 z-20" />
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 z-20" />
</div>
</Reveal>
<div>
<Reveal>
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{displayTag}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-primary mb-6 md:mb-8">
{displayTitle}
</h2>
<p className="text-slate-600 text-base md:text-xl mb-8 md:mb-12">
{displayDescription}
</p>
</Reveal>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{displayGroups.map((item: string, i: number) => (
<Reveal key={i} delay={i * 0.05}>
<div className="flex items-center gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 hover:border-accent/30 transition-colors group relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-accent/0 group-hover:bg-accent/100 transition-all duration-300" />
<div className="w-8 h-8 rounded-full bg-white flex items-center justify-center shadow-sm group-hover:bg-accent group-hover:text-white transition-colors">
<CheckCircle2 size={16} />
</div>
<span className="text-primary font-semibold">{item}</span>
</div>
</Reveal>
))}
</div>
</div>
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,79 @@
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@/components/Button";
import { Counter } from "@/components/Counter";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
interface HeroProps {
tag: string;
title: React.ReactNode;
subtitle: string;
ctaPrimary: string;
ctaSecondary: string;
}
export function Hero({
tag,
title,
subtitle,
ctaPrimary,
ctaSecondary,
}: HeroProps) {
return (
<section className="relative min-h-[90vh] flex items-center pt-44 pb-20 overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/media/business/hero-bg.jpg"
alt="MB Grid Solutions Hero"
fill
className="object-cover"
priority
quality={75}
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-100/80 via-white/90 to-white/40 md:to-transparent" />
<TechBackground />
</div>
<div className="container-custom relative z-10">
<div className="text-left relative">
<Counter value={1} className="section-number" />
<Reveal delay={0.1}>
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-accent/10 text-accent text-xs font-bold uppercase tracking-wider mb-6">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-accent"></span>
</span>
{tag}
</span>
</Reveal>
<Reveal delay={0.2}>
<h1 className="text-4xl sm:text-5xl md:text-7xl font-extrabold text-primary mb-6 md:mb-8 leading-[1.1]">
{title}
</h1>
</Reveal>
<Reveal delay={0.3}>
<p className="text-slate-600 text-lg md:text-2xl leading-relaxed mb-8 md:mb-12 max-w-2xl">
{subtitle}
</p>
</Reveal>
<Reveal delay={0.4}>
<div className="flex flex-wrap gap-4">
<Button href="/kontakt" variant="accent" showArrow>
{ctaPrimary}
</Button>
<Button href="/ueber-uns" variant="ghost">
{ctaSecondary}
</Button>
</div>
</Reveal>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,80 @@
"use client";
import React from "react";
import {
Award,
Clock,
Lightbulb,
Truck,
MessageSquare,
ShieldCheck,
} from "lucide-react";
import { Reveal } from "@/components/Reveal";
import { TechBackground } from "@/components/TechBackground";
import { Counter } from "@/components/Counter";
const manifestIcons = [
Award,
Clock,
Lightbulb,
Truck,
MessageSquare,
ShieldCheck,
];
interface ManifestItem {
title: string;
desc: string;
}
interface ManifestSectionProps {
tagline: string;
title: string;
subtitle: string;
items: ManifestItem[];
}
export function ManifestSection({
tagline,
title,
subtitle,
items,
}: ManifestSectionProps) {
return (
<section className="bg-slate-950 text-accent relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={3} className="section-number !text-white/5" />
<Reveal className="mb-20">
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{tagline}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
{title}
</h2>
<p className="text-slate-400 text-base md:text-lg">{subtitle}</p>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{items.map((item, i) => {
const Icon = manifestIcons[i % manifestIcons.length];
return (
<Reveal key={i} delay={i * 0.1}>
<div className="bg-white/5 p-10 rounded-3xl border border-white/10 group hover:-translate-y-1 transition-[box-shadow,transform] duration-300 h-full motion-fix relative 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" />
<div className="text-accent mb-6">
<Icon size={32} />
</div>
<h3 className="text-xl font-bold text-white mb-4">
{i + 1}. {item.title}
</h3>
<p className="text-slate-400 leading-relaxed">{item.desc}</p>
</div>
</Reveal>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,109 @@
"use client";
import React from "react";
import Link from "next/link";
import { ChevronRight, Zap, Shield, BarChart3 } from "lucide-react";
import { Reveal } from "@/components/Reveal";
import { Counter } from "@/components/Counter";
import { TechBackground } from "@/components/TechBackground";
import { useTranslations } from "next-intl";
interface PortfolioItem {
icon: React.ReactNode;
title: string;
desc: string;
}
interface PortfolioSectionProps {
tag?: string;
title?: string;
description?: string;
linkText?: string;
items?: PortfolioItem[];
}
export const PortfolioSection = ({
tag,
title,
description,
linkText,
items,
}: PortfolioSectionProps) => {
const t = useTranslations("Index");
const displayTag = tag || t("portfolio.tag");
const displayTitle = title || t("portfolio.title");
const displayDescription = description || t("portfolio.description");
const displayLinkText = linkText || t("portfolio.link");
const defaultItems = [
{
icon: <Zap size={32} />,
title: t("portfolio.items.beratung.title"),
desc: t("portfolio.items.beratung.desc"),
},
{
icon: <Shield size={32} />,
title: t("portfolio.items.begleitung.title"),
desc: t("portfolio.items.begleitung.desc"),
},
{
icon: <BarChart3 size={32} />,
title: t("portfolio.items.beschaffung.title"),
desc: t("portfolio.items.beschaffung.desc"),
},
];
const displayItems = items || defaultItems;
return (
<section className="bg-slate-950 text-accent relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={2} className="section-number !text-white/5" />
<Reveal className="flex flex-col md:flex-row md:items-end justify-between gap-8 mb-16">
<div>
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{displayTag}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
{displayTitle}
</h2>
<p className="text-slate-400 text-base md:text-xl">
{displayDescription}
</p>
</div>
<Link
href="/ueber-uns"
className="text-accent font-bold flex items-center gap-2 hover:text-white transition-colors group"
>
{displayLinkText}{" "}
<ChevronRight
className="transition-transform group-hover:translate-x-1"
size={20}
/>
</Link>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{displayItems.map((item, i) => (
<Reveal key={i} delay={i * 0.1}>
<div className="bg-white/5 p-8 rounded-2xl border border-white/10 group hover:-translate-y-2 transition-[box-shadow,transform] duration-300 h-full relative overflow-hidden">
<div className="absolute top-0 right-0 w-16 h-16 bg-accent/5 -mr-8 -mt-8 rounded-full group-hover:bg-accent/10 transition-colors" />
<div className="w-16 h-16 rounded-2xl bg-accent/10 text-accent flex items-center justify-center mb-8 group-hover:bg-accent group-hover:text-white transition-colors relative z-10">
{item.icon}
</div>
<h3 className="text-2xl font-bold text-white mb-4 relative z-10">
{item.title}
</h3>
<p className="text-slate-400 leading-relaxed relative z-10">
{item.desc}
</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,99 @@
"use client";
import React from "react";
import Image from "next/image";
import { Reveal } from "@/components/Reveal";
import { Counter } from "@/components/Counter";
import { TechBackground } from "@/components/TechBackground";
import { useTranslations } from "next-intl";
interface SpecItem {
label: string;
value: string;
desc: string;
}
interface TechnicalSpecsSectionProps {
tag?: string;
title?: string;
items?: SpecItem[];
}
export const TechnicalSpecsSection = ({
tag,
title,
items,
}: TechnicalSpecsSectionProps) => {
const t = useTranslations("Index");
const displayTag = tag || t("specs.tag");
const displayTitle = title || t("specs.title");
const defaultItems = [
{
label: t("specs.items.kabel.label"),
value: t("specs.items.kabel.value"),
desc: t("specs.items.kabel.desc"),
},
{
label: t("specs.items.spannung.label"),
value: t("specs.items.spannung.value"),
desc: t("specs.items.spannung.desc"),
},
{
label: t("specs.items.technologie.label"),
value: t("specs.items.technologie.value"),
desc: t("specs.items.technologie.desc"),
},
];
const displayItems = items || defaultItems;
return (
<section className="relative py-24 md:py-32 text-white overflow-hidden bg-slate-900">
<div className="absolute inset-0 opacity-20">
<Image
src="/media/drums/about-hero.jpg"
alt="Background"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-b from-slate-900 via-slate-900/80 to-slate-900" />
</div>
<TechBackground />
<div className="container-custom relative z-10">
<Counter value={4} className="section-number !text-white/5" />
{/* Data Stream Effect */}
<div className="absolute -top-10 right-0 w-px h-64 bg-gradient-to-b from-transparent via-accent/50 to-transparent animate-pulse" />
<div className="absolute -bottom-10 left-10 w-px h-64 bg-gradient-to-b from-transparent via-accent/30 to-transparent animate-pulse delay-700" />
<Reveal className="mb-20">
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
{displayTag}
</span>
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
{displayTitle}
</h2>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{displayItems.map((item, i) => (
<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" />
<h3 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
{item.label}
</h3>
<p className="text-2xl font-bold text-white mb-4 leading-tight">
{item.value}
</p>
<p className="text-slate-400 leading-relaxed">{item.desc}</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
};

67
content/de/about.mdx Normal file
View File

@@ -0,0 +1,67 @@
import { AboutHero } from "@/components/sections/AboutHero";
import { AboutIntro } from "@/components/sections/AboutIntro";
import { ManifestSection } from "@/components/sections/ManifestSection";
import { CTASection } from "@/components/sections/CTASection";
<AboutHero
tagline="Über uns"
title={<>Zuverlässige Begleitung für Ihre <span className="text-accent">Netzinfrastruktur</span></>}
subtitle="Herstellerneutrale Beratung in der Energiekabeltechnologie. Wir verstehen uns als Ihr technischer Lotse."
/>
<AboutIntro
paragraphs={[
"Unsere Wurzeln liegen in der tiefen praktischen Erfahrung unserer technischen Berater und unserer Netzwerke im globalem Kabelmarkt. Wir vereinen Tradition mit modernster Innovation, um zuverlässige Energielösungen für Projekte bis 110 kV zu realisieren.",
"Wir verstehen die Herausforderungen der Energiewende und bieten herstellerneutrale Beratung, die auf Fakten, Normen und jahrzehntelanger Erfahrung basiert."
]}
team={[
{
name: "Michael Bodemer",
role: "Geschäftsführung & Inhaber",
linkedin: "https://www.linkedin.com/in/michael-bodemer-33b493122/"
},
{
name: "Klaus Mintel",
role: "Geschäftsführung",
linkedin: "https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
}
]}
/>
<ManifestSection
tagline="Werte"
title="Unser Manifest"
subtitle="Werte, die unsere tägliche Arbeit leiten und den Erfolg Ihrer Projekte sichern."
items={[
{
title: "Kompetenz",
desc: "Jahrzehntelange Erfahrung kombiniert mit europaweitem Know-how in modernsten Anlagen."
},
{
title: "Verfügbarkeit",
desc: "Schnelle und verlässliche Unterstützung ohne unnötige Verzögerungen."
},
{
title: "Lösungen",
desc: "Wir stellen die richtigen Fragen, um die technisch und wirtschaftlich beste Lösung zu finden."
},
{
title: "Logistik",
desc: "Von der Fertigungsüberwachung bis zum termingerechten Fracht-Tracking."
},
{
title: "Offenheit",
desc: "Wir hören zu und passen unsere Prozesse kontinuierlich an Ihren Erfolg an."
},
{
title: "Zuverlässigkeit",
desc: "Wir halten, was wir versprechen ohne Ausnahme. Verbindlichkeit ist unser Fundament."
}
]}
/>
<CTASection
title="Bereit für Ihr nächstes Projekt?"
subtitle="Lassen Sie uns gemeinsam die optimale Lösung für Ihre Energieinfrastruktur finden. Wir beraten Sie herstellerneutral und kompetent."
buttonText="Jetzt Kontakt aufnehmen"
/>

254
content/de/avb.mdx Normal file
View File

@@ -0,0 +1,254 @@
# Allgemeine Verkaufsbedingungen (AVB)
Stand: April 2026
## 1. Geltungsbereich
**1.1** Ausschließlich die nachfolgenden Allgemeinen Verkaufsbedingungen (nachfolgend „AVB“) gelten für den gesamten Geschäftsverkehr (nachfolgend „Lieferung von Waren“) der MB Grid Solutions & Services, Raiffeisenstraße 22, 73630 Remshalden (nachfolgend „MB Grid Solutions & Services“) 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services in Textform z.B. durch eine Auftragsbestätigung per E-Mail oder Fax zustande. Der Annahme des Angebots durch MB Grid Solutions & Services steht es gleich, wenn MB Grid Solutions & Services den Auftrag oder die Bestellung binnen vierzehn (14) Kalendertagen ab Zugang des Angebots bei MB Grid Solutions & Services vorbehaltlos ausführt.
**2.2** Alle Angebote von MB Grid Solutions & Services 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 MB Grid Solutions & Services.
**2.3** Der Umfang der von MB Grid Solutions & Services zu liefernden Waren ergibt sich aus dem jeweiligen Angebot oder der jeweiligen Auftragsbestätigung von MB Grid Solutions & Services.
**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 ausdrücklich von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services. 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 MB Grid Solutions & Services offensichtliche Irrtümer, Schreib- oder Rechenfehler enthält, ist das Dokument für MB Grid Solutions & Services hinsichtlich der offensichtlichen Fehler nicht verbindlich. Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services berechtigt, ganz oder teilweise vom Vertrag zurückzutreten.
## 3. Abrufaufträge
**3.1** Erteilt der Kunde MB Grid Solutions & Services einen Abrufauftrag und werden über die Abruftermine keine gesonderten Vereinbarungen in Textform getroffen, ist der Kunde verpflichtet, MB Grid Solutions & Services die einzelnen Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei MB Grid Solutions & Services 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 MB Grid Solutions & Services handelt.
## 4. Preise und Zahlungsbedingungen
**4.1** Die in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services angegebenen Preise gelten für den in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services 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 MB Grid Solutions & Services ausgewiesenen Hohlpreise verstehen sich darüber hinaus zuzüglich Metallzuschlag und auftragsspezifischer Schnittkosten.
**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. MB Grid Solutions & Services ist zum Rücktritt von dem betroffenen Vertrag berechtigt, wenn innerhalb von vierzehn (14) Kalendertagen ab Anzeige der Preiserhöhung durch MB Grid Solutions & Services 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 MB Grid Solutions & Services und ohne Abzug zu bezahlen. Die Zahlungsfrist gilt mit vollständiger Gutschrift des ausstehenden Kaufpreises auf dem Konto von MB Grid Solutions & Services 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 MB Grid Solutions & Services bedarf.
**4.5** Befindet sich der Kunde mit einer Zahlung in Verzug, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services an den Kunden. Darüber hinaus stehen MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
## 6. Metallzahl
**6.1** Die von MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services durch dessen Vorlieferanten. MB Grid Solutions & Services ist im Fall der nicht ordnungsgemäßen Selbstbelieferung zum Rücktritt vom Vertrag berechtigt. MB Grid Solutions & Services informiert den Kunden unverzüglich, wenn und soweit MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services mit der Lieferung der Ware nur dann in Verzug, wenn der Kunde MB Grid Solutions & Services nach Ablauf der Lieferfrist eine angemessene 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 MB Grid Solutions & Services 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** MB Grid Solutions & Services 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 MB Grid Solutions & Services gegenüber dem Kunden aufgrund dessen verzögerter Übernahme der Ware anzurechnen. Dem Kunden bleibt der Nachweis gestattet, dass MB Grid Solutions & Services überhaupt kein oder nur ein geringerer Schaden entstanden ist.
**7.10** Kabel werden von MB Grid Solutions & Services auf stabilen Vollholztrommeln geliefert. Auf Wunsch vermittelt MB Grid Solutions & Services 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 MB Grid Solutions & Services aus.
**7.11** Rücksendungen von Waren an MB Grid Solutions & Services, die nicht vorher von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services in dem Eigentum von MB Grid Solutions & Services (nachfolgend „Vorbehaltsware“). Dabei ist unerheblich, aus welchem Rechtsgrund die Forderungen von MB Grid Solutions & Services 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 MB Grid Solutions & Services und MB Grid Solutions & Services erwirbt Eigentum an der neuen Sache. Wird die Vorbehaltsware mit anderen Sachen zu einer einheitlichen Sache verbunden, verarbeitet oder untrennbar vermischt oder vermengt oder in sonstiger Weise verbunden, erwirbt MB Grid Solutions & Services 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, MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services ab; MB Grid Solutions & Services 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 MB Grid Solutions & Services beeinträchtigt werden, hat der Kunde MB Grid Solutions & Services unverzüglich in Textform anzuzeigen. Der Kunde hat den Dritten darüber hinaus unverzüglich in Textform darauf hinzuweisen, dass MB Grid Solutions & Services Eigentümer der Vorbehaltsware ist. Soweit der Dritte nicht in der Lage ist, MB Grid Solutions & Services 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 MB Grid Solutions & Services ab; MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services vereinbarten Kaufpreises für die Vorbehaltsware; MB Grid Solutions & Services nimmt die Abtretung an.
**9.8** Der Kunde wird ermächtigt, die abgetretenen Forderungen für MB Grid Solutions & Services einzuziehen. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services berechtigt. MB Grid Solutions & Services wird die Zustimmung nicht verweigern, sofern eine gleichwertige Sicherheit gestellt wird.
**9.9** MB Grid Solutions & Services kann die Ermächtigung zur Weiterveräußerung und Einziehung widerrufen, sobald sich der Kunde gegenüber MB Grid Solutions & Services im Zahlungsverzug befindet, oder das Insolvenzverfahren oder ein vergleichbares Verfahren über das Vermögen des Kunden eröffnet oder mangels Masse abgewiesen wurde.
**9.10** Der Kunde hat die eingezogenen Forderungen binnen zehn (10) Werktagen auf ein von MB Grid Solutions & Services benanntes Konto zu überweisen.
**9.11** MB Grid Solutions & Services 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, MB Grid Solutions & Services die Namen seiner Abnehmer bekanntzugeben und alle erforderlichen Unterlagen herauszugeben, die für die Einziehung der Forderung durch MB Grid Solutions & Services selbst erforderlich sind. Auch ist der Kunde ab Verzugseintritt verpflichtet, die Abtretung der Forderung an MB Grid Solutions & Services seinem Abnehmer gegenüber schriftlich mitzuteilen.
**9.12** MB Grid Solutions & Services 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 MB Grid Solutions & Services.
**9.13** Tritt MB Grid Solutions & Services bei vertragswidrigem Verhalten des Kunden, insbesondere Zahlungsverzug, vom Vertrag zurück (Verwertungsfall), ist MB Grid Solutions & Services berechtigt, die Vorbehaltsware herauszuverlangen. Der Kunde hat in diesem Fall MB Grid Solutions & Services alle Informationen und Unterlagen zu übergeben, die erforderlich sind, um die Vorbehaltsware herauszuverlangen und die an MB Grid Solutions & Services 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 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 MB Grid Solutions & Services innerhalb einer angemessenen Frist zu erklären, ob er wegen einem bei MB Grid Solutions & Services 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 MB Grid Solutions & Services erheblich einwirkt, werden die Parteien den zugrundeliegenden Vertrag unter Beachtung von Treu und Glauben angemessen anpassen. Soweit dies wirtschaftlich nicht vertretbar ist, steht MB Grid Solutions & Services das Recht zu, von dem Vertrag zurückzutreten. Will MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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** MB Grid Solutions & Services ist berechtigt, innerhalb einer angemessenen Frist, anerkannte Mängel nach eigener Wahl durch Beseitigung (Nachbesserung) oder durch Lieferung mangelfreier Ware (Ersatzlieferung) zu beheben. Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services verändert hat.
**11.7** Hat der Kunde die Ware verbaut, obwohl er positive Kenntnis oder grob fahrlässige Unkenntnis des Mangels hatte, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services gegenüber nicht den Rücktritt vom Vertrag erklärt oder Schadensersatz wegen Nichterfüllung verlangt hat, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services, 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. MB Grid Solutions & Services 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** MB Grid Solutions & Services haftet uneingeschränkt für Vorsatz und grobe Fahrlässigkeit. MB Grid Solutions & Services 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 garantierten Beschaffenheitsmerkmalen, sowie im Rahmen zwingender gesetzlicher Vorschriften, wie etwa denen des Produkthaftungsgesetzes.
**12.2** Bei der Verletzung von wesentlichen Vertragspflichten haftet MB Grid Solutions & Services im Übrigen der Höhe nach begrenzt auf den bei Vertragsschluss für MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services im Rahmen der Reparatur oder Änderung bzw. des Eingriffs zurückzuführen sind, die der Kunde ohne Zustimmung von MB Grid Solutions & Services vorgenommen hat.
**12.5** Soweit die Haftung von MB Grid Solutions & Services ausgeschlossen oder beschränkt ist, gilt dies auch für die Haftung der Angestellten, Arbeitnehmer, Mitarbeiter, Vertreter und Erfüllungsgehilfen von MB Grid Solutions & Services.
## 13. Produkthaftung
**13.1** Der Kunde wird die gelieferten Waren ausschließlich vertragsgemäß und entsprechend den verfügbaren Anleitungen von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services für erforderlich und zweckmäßig hält. MB Grid Solutions & Services ist verpflichtet, die Kosten des Produktrückrufs oder der -warnung zu tragen, es sei denn MB Grid Solutions & Services ist für den Produktfehler und den eingetretenen Schaden nach produkthaftungsrechtlichen Grundsätzen nicht verantwortlich.
**13.3** Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 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 MB Grid Solutions & Services seine eigentums- und urheberrechtlichen Verwertungsrechte uneingeschränkt vor. Die Unterlagen dürfen nur nach vorheriger schriftlicher Zustimmung von MB Grid Solutions & Services Dritten zugänglich gemacht werden und sind MB Grid Solutions & Services auf Verlangen unverzüglich zurückzugeben.
**15.2** Ohne ausdrückliche Zustimmung von MB Grid Solutions & Services in Textform ist der Kunde nicht berechtigt, die gelieferte Ware zu Werbezwecken zu verwenden.
**15.3** MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services bedarf der gesonderten ausdrücklichen Einwilligung des Kunden.
**15.7** Auf Anordnung einer zuständigen Stelle darf MB Grid Solutions & Services 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, (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 MB Grid Solutions & Services 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, MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 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 MB Grid Solutions & Services verpflichtet, deren Höhe in das billige Ermessen von MB Grid Solutions & Services gestellt wird und im Streitfall durch das zuständige Gericht überprüft werden kann. Durch die Geltendmachung der Vertragsstrafe werden weitergehende Schadensersatzansprüche von MB Grid Solutions & Services 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 MB Grid Solutions & Services und dem Kunden, der Auslegung dieser AVB, sowie die gesamten Rechtsbeziehungen zwischen MB Grid Solutions & Services 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 MB Grid Solutions & Services 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.

View File

@@ -0,0 +1,15 @@
---
title: Datenschutzerklärung
---
# 1. Datenschutz auf einen Blick
Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
# 2. Hosting
Unsere Website wird bei Hetzner Online GmbH gehostet. Der Serverstandort ist Deutschland. Wir haben einen Vertrag über Auftragsverarbeitung (AVV) mit Hetzner geschlossen.
# 3. Kontaktformular
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
# 4. Server-Log-Dateien
Der Provider der Seiten erhebt und speichert automatisch Informationen in sogenannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind: Browsertyp und Browserversion, verwendetes Betriebssystem, Referrer URL, Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage, IP-Adresse.

24
content/de/impressum.mdx Normal file
View File

@@ -0,0 +1,24 @@
---
title: Impressum
---
# Angaben gemäß § 5 TMG
MB Grid Solutions & Services GmbH
Raiffeisenstraße 22
73630 Remshalden
# Vertreten durch
Michael Bodemer
Klaus Mintel
# Kontakt
E-Mail: [info@mb-grid-solutions.com](mailto:info@mb-grid-solutions.com)
Web: [www.mb-grid-solutions.com](https://www.mb-grid-solutions.com)
# Registereintrag
Eintragung im Handelsregister.
Registergericht: Amtsgericht Stuttgart
Registernummer: HRB 803379
# Urheberrecht
Alle auf der Website veröffentlichten Texte, Bilder und sonstigen Informationen unterliegen sofern nicht anders gekennzeichnet dem Urheberrecht. Jede Vervielfältigung, Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw. Weitergabe der Inhalte ohne schriftliche Genehmigung ist ausdrücklich untersagt.

62
content/de/index.mdx Normal file
View File

@@ -0,0 +1,62 @@
import { Hero } from "@/components/sections/Hero";
import { PortfolioSection } from "@/components/sections/PortfolioSection";
import { ExpertiseSection } from "@/components/sections/ExpertiseSection";
import { TechnicalSpecsSection } from "@/components/sections/TechnicalSpecsSection";
import { CTASection } from "@/components/sections/CTASection";
<Hero
tag="Technische Beratung"
title={<>Spezialisierter Partner für <span className="text-accent">Energiekabelprojekte</span></>}
subtitle="Herstellerneutrale technische Beratung für Ihre Projekte in Mittel- und Hochspannungsnetzen bis zu 110 kV."
ctaPrimary="Projekt anfragen"
ctaSecondary="Mehr erfahren"
/>
<PortfolioSection
tag="Portfolio"
title="Unsere Leistungen"
description="Beratung durch unabhängige Experten mit jahrzehntelanger Erfahrung aus Engineering, Normengremien, Planung und Produktion."
linkText="Alle Details ansehen"
/>
<ExpertiseSection
tag="Expertise"
title="Anwendungen & Zielgruppen"
description="Wir unterstützen Sie bei der Realisierung Ihrer Kabelprojekte."
groups={[
"Energieversorger",
"Ingenieurbüros",
"Tiefbauunternehmen",
"Industrie",
"Projektierer EE",
"Planungsbüros"
]}
/>
<TechnicalSpecsSection
tag="Spezifikationen"
title="Technische Expertise"
items={[
{
label: "Kabeltypen",
value: "N2XS(FL)2Y, N2X(F)KLD2Y...",
desc: "Umfassende Expertise im Design gängiger Hochspannungskabel."
},
{
label: "Spannungsebenen",
value: "64/110 kV & Mittelspannung",
desc: "Spezialisierte Beratung für komplexe Infrastrukturprojekte."
},
{
label: "Leitertechnologie",
value: "Massiv-, Mehrdraht- & Milliken",
desc: "Optimierung des Leiterdesigns hinsichtlich Stromtragfähigkeit."
}
]}
/>
<CTASection
title="Bereit für Ihr nächstes Projekt?"
subtitle="Lassen Sie uns gemeinsam die optimale Lösung für Ihre Energieinfrastruktur finden. Wir beraten Sie herstellerneutral und kompetent."
buttonText="Jetzt Kontakt aufnehmen"
/>

67
content/en/about.mdx Normal file
View File

@@ -0,0 +1,67 @@
import { AboutHero } from "@/components/sections/AboutHero";
import { AboutIntro } from "@/components/sections/AboutIntro";
import { ManifestSection } from "@/components/sections/ManifestSection";
import { CTASection } from "@/components/sections/CTASection";
<AboutHero
tagline="About us"
title={<>Reliable support for your <span className="text-accent">Grid Infrastructure</span></>}
subtitle="Manufacturer-neutral consulting in power cable technology. We see ourselves as your technical pilot."
/>
<AboutIntro
paragraphs={[
"Our roots lie in the deep practical experience of our technical consultants and our networks in the global cable market. We combine tradition with state-of-the-art innovation to realize reliable energy solutions for projects up to 110 kV.",
"We understand the challenges of the energy transition and offer manufacturer-neutral consulting based on facts, standards, and decades of experience."
]}
team={[
{
name: "Michael Bodemer",
role: "Managing Director & Owner",
linkedin: "https://www.linkedin.com/in/michael-bodemer-33b493122/"
},
{
name: "Klaus Mintel",
role: "Managing Director",
linkedin: "https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
}
]}
/>
<ManifestSection
tagline="Values"
title="Our Manifest"
subtitle="Values that guide our daily work and ensure the success of your projects."
items={[
{
title: "Competence",
desc: "Decades of experience combined with Europe-wide know-how in state-of-the-art facilities."
},
{
title: "Availability",
desc: "Fast and reliable support without unnecessary delays."
},
{
title: "Solutions",
desc: "We ask the right questions to find the technically and economically best solution."
},
{
title: "Logistics",
desc: "From manufacturing monitoring to timely freight tracking."
},
{
title: "Openness",
desc: "We listen and continuously adapt our processes to your success."
},
{
title: "Reliability",
desc: "We keep what we promise without exception. Commitment is our foundation."
}
]}
/>
<CTASection
title="Ready for your next project?"
subtitle="Let's find the optimal solution for your energy infrastructure together. We advise you manufacturer-neutrally and competently."
buttonText="Contact us now"
/>

62
content/en/index.mdx Normal file
View File

@@ -0,0 +1,62 @@
import { Hero } from "@/components/sections/Hero";
import { PortfolioSection } from "@/components/sections/PortfolioSection";
import { ExpertiseSection } from "@/components/sections/ExpertiseSection";
import { TechnicalSpecsSection } from "@/components/sections/TechnicalSpecsSection";
import { CTASection } from "@/components/sections/CTASection";
<Hero
tag="Technical Consulting"
title={<>Specialized Partner for <span className="text-accent">Power Cable Projects</span></>}
subtitle="Manufacturer-neutral technical consulting for your projects in medium and high voltage networks up to 110 kV."
ctaPrimary="Inquire Project"
ctaSecondary="Learn More"
/>
<PortfolioSection
tag="Portfolio"
title="Our Services"
description="Consulting by independent experts with decades of experience in engineering, standards committees, planning, and production."
linkText="View all details"
/>
<ExpertiseSection
tag="Expertise"
title="Applications & Target Groups"
description="We support you in the realization of your cable projects."
groups={[
"Energy Suppliers",
"Engineering Offices",
"Civil Engineering Companies",
"Industry",
"RE Developers",
"Planning Offices"
]}
/>
<TechnicalSpecsSection
tag="Specifications"
title="Technical Expertise"
items={[
{
label: "Cable Types",
value: "N2XS(FL)2Y, N2X(F)KLD2Y...",
desc: "Comprehensive expertise in the design of common high voltage cables."
},
{
label: "Voltage Levels",
value: "64/110 kV & Medium Voltage",
desc: "Specialized consulting for complex infrastructure projects."
},
{
label: "Conductor Technology",
value: "Solid, Multi-wire & Milliken",
desc: "Optimization of conductor design regarding current carrying capacity."
}
]}
/>
<CTASection
title="Ready for your next project?"
subtitle="Let's find the optimal solution for your energy infrastructure together. We advise you manufacturer-neutrally and competently."
buttonText="Contact us now"
/>

View File

@@ -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

254
context/avbs.md Normal file
View File

@@ -0,0 +1,254 @@
# Allgemeine Verkaufsbedingungen (AVB)
Stand: April 2026
## 1. Geltungsbereich
**1.1** Ausschließlich die nachfolgenden Allgemeinen Verkaufsbedingungen (nachfolgend „AVB“) gelten für den gesamten Geschäftsverkehr (nachfolgend „Lieferung von Waren“) der MB Grid Solutions & Services, Raiffeisenstraße 22, 73630 Remshalden (nachfolgend „MB Grid Solutions & Services“) 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services in Textform z.B. durch eine Auftragsbestätigung per E-Mail oder Fax zustande. Der Annahme des Angebots durch MB Grid Solutions & Services steht es gleich, wenn MB Grid Solutions & Services den Auftrag oder die Bestellung binnen vierzehn (14) Kalendertagen ab Zugang des Angebots bei MB Grid Solutions & Services vorbehaltlos ausführt.
**2.2** Alle Angebote von MB Grid Solutions & Services 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 MB Grid Solutions & Services.
**2.3** Der Umfang der von MB Grid Solutions & Services zu liefernden Waren ergibt sich aus dem jeweiligen Angebot oder der jeweiligen Auftragsbestätigung von MB Grid Solutions & Services.
**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 ausdrücklich von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services. 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 MB Grid Solutions & Services offensichtliche Irrtümer, Schreib- oder Rechenfehler enthält, ist das Dokument für MB Grid Solutions & Services hinsichtlich der offensichtlichen Fehler nicht verbindlich. Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services berechtigt, ganz oder teilweise vom Vertrag zurückzutreten.
## 3. Abrufaufträge
**3.1** Erteilt der Kunde MB Grid Solutions & Services einen Abrufauftrag und werden über die Abruftermine keine gesonderten Vereinbarungen in Textform getroffen, ist der Kunde verpflichtet, MB Grid Solutions & Services die einzelnen Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei MB Grid Solutions & Services 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 MB Grid Solutions & Services handelt.
## 4. Preise und Zahlungsbedingungen
**4.1** Die in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services angegebenen Preise gelten für den in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services 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 MB Grid Solutions & Services ausgewiesenen Hohlpreise verstehen sich darüber hinaus zuzüglich Metallzuschlag und auftragsspezifischer Schnittkosten.
**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. MB Grid Solutions & Services ist zum Rücktritt von dem betroffenen Vertrag berechtigt, wenn innerhalb von vierzehn (14) Kalendertagen ab Anzeige der Preiserhöhung durch MB Grid Solutions & Services 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 MB Grid Solutions & Services und ohne Abzug zu bezahlen. Die Zahlungsfrist gilt mit vollständiger Gutschrift des ausstehenden Kaufpreises auf dem Konto von MB Grid Solutions & Services 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 MB Grid Solutions & Services bedarf.
**4.5** Befindet sich der Kunde mit einer Zahlung in Verzug, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services an den Kunden. Darüber hinaus stehen MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
## 6. Metallzahl
**6.1** Die von MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services durch dessen Vorlieferanten. MB Grid Solutions & Services ist im Fall der nicht ordnungsgemäßen Selbstbelieferung zum Rücktritt vom Vertrag berechtigt. MB Grid Solutions & Services informiert den Kunden unverzüglich, wenn und soweit MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services mit der Lieferung der Ware nur dann in Verzug, wenn der Kunde MB Grid Solutions & Services nach Ablauf der Lieferfrist eine angemessene 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 MB Grid Solutions & Services 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** MB Grid Solutions & Services 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 MB Grid Solutions & Services gegenüber dem Kunden aufgrund dessen verzögerter Übernahme der Ware anzurechnen. Dem Kunden bleibt der Nachweis gestattet, dass MB Grid Solutions & Services überhaupt kein oder nur ein geringerer Schaden entstanden ist.
**7.10** Kabel werden von MB Grid Solutions & Services auf stabilen Vollholztrommeln geliefert. Auf Wunsch vermittelt MB Grid Solutions & Services 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 MB Grid Solutions & Services aus.
**7.11** Rücksendungen von Waren an MB Grid Solutions & Services, die nicht vorher von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services in dem Eigentum von MB Grid Solutions & Services (nachfolgend „Vorbehaltsware“). Dabei ist unerheblich, aus welchem Rechtsgrund die Forderungen von MB Grid Solutions & Services 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 MB Grid Solutions & Services und MB Grid Solutions & Services erwirbt Eigentum an der neuen Sache. Wird die Vorbehaltsware mit anderen Sachen zu einer einheitlichen Sache verbunden, verarbeitet oder untrennbar vermischt oder vermengt oder in sonstiger Weise verbunden, erwirbt MB Grid Solutions & Services 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, MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services ab; MB Grid Solutions & Services 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 MB Grid Solutions & Services beeinträchtigt werden, hat der Kunde MB Grid Solutions & Services unverzüglich in Textform anzuzeigen. Der Kunde hat den Dritten darüber hinaus unverzüglich in Textform darauf hinzuweisen, dass MB Grid Solutions & Services Eigentümer der Vorbehaltsware ist. Soweit der Dritte nicht in der Lage ist, MB Grid Solutions & Services 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 MB Grid Solutions & Services ab; MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services vereinbarten Kaufpreises für die Vorbehaltsware; MB Grid Solutions & Services nimmt die Abtretung an.
**9.8** Der Kunde wird ermächtigt, die abgetretenen Forderungen für MB Grid Solutions & Services einzuziehen. MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services berechtigt. MB Grid Solutions & Services wird die Zustimmung nicht verweigern, sofern eine gleichwertige Sicherheit gestellt wird.
**9.9** MB Grid Solutions & Services kann die Ermächtigung zur Weiterveräußerung und Einziehung widerrufen, sobald sich der Kunde gegenüber MB Grid Solutions & Services im Zahlungsverzug befindet, oder das Insolvenzverfahren oder ein vergleichbares Verfahren über das Vermögen des Kunden eröffnet oder mangels Masse abgewiesen wurde.
**9.10** Der Kunde hat die eingezogenen Forderungen binnen zehn (10) Werktagen auf ein von MB Grid Solutions & Services benanntes Konto zu überweisen.
**9.11** MB Grid Solutions & Services 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, MB Grid Solutions & Services die Namen seiner Abnehmer bekanntzugeben und alle erforderlichen Unterlagen herauszugeben, die für die Einziehung der Forderung durch MB Grid Solutions & Services selbst erforderlich sind. Auch ist der Kunde ab Verzugseintritt verpflichtet, die Abtretung der Forderung an MB Grid Solutions & Services seinem Abnehmer gegenüber schriftlich mitzuteilen.
**9.12** MB Grid Solutions & Services 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 MB Grid Solutions & Services.
**9.13** Tritt MB Grid Solutions & Services bei vertragswidrigem Verhalten des Kunden, insbesondere Zahlungsverzug, vom Vertrag zurück (Verwertungsfall), ist MB Grid Solutions & Services berechtigt, die Vorbehaltsware herauszuverlangen. Der Kunde hat in diesem Fall MB Grid Solutions & Services alle Informationen und Unterlagen zu übergeben, die erforderlich sind, um die Vorbehaltsware herauszuverlangen und die an MB Grid Solutions & Services 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 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 MB Grid Solutions & Services innerhalb einer angemessenen Frist zu erklären, ob er wegen einem bei MB Grid Solutions & Services 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 MB Grid Solutions & Services erheblich einwirkt, werden die Parteien den zugrundeliegenden Vertrag unter Beachtung von Treu und Glauben angemessen anpassen. Soweit dies wirtschaftlich nicht vertretbar ist, steht MB Grid Solutions & Services das Recht zu, von dem Vertrag zurückzutreten. Will MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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** MB Grid Solutions & Services ist berechtigt, innerhalb einer angemessenen Frist, anerkannte Mängel nach eigener Wahl durch Beseitigung (Nachbesserung) oder durch Lieferung mangelfreier Ware (Ersatzlieferung) zu beheben. Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services verändert hat.
**11.7** Hat der Kunde die Ware verbaut, obwohl er positive Kenntnis oder grob fahrlässige Unkenntnis des Mangels hatte, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services gegenüber nicht den Rücktritt vom Vertrag erklärt oder Schadensersatz wegen Nichterfüllung verlangt hat, ist MB Grid Solutions & Services 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 MB Grid Solutions & Services, 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. MB Grid Solutions & Services 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** MB Grid Solutions & Services haftet uneingeschränkt für Vorsatz und grobe Fahrlässigkeit. MB Grid Solutions & Services 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 garantierten Beschaffenheitsmerkmalen, sowie im Rahmen zwingender gesetzlicher Vorschriften, wie etwa denen des Produkthaftungsgesetzes.
**12.2** Bei der Verletzung von wesentlichen Vertragspflichten haftet MB Grid Solutions & Services im Übrigen der Höhe nach begrenzt auf den bei Vertragsschluss für MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services im Rahmen der Reparatur oder Änderung bzw. des Eingriffs zurückzuführen sind, die der Kunde ohne Zustimmung von MB Grid Solutions & Services vorgenommen hat.
**12.5** Soweit die Haftung von MB Grid Solutions & Services ausgeschlossen oder beschränkt ist, gilt dies auch für die Haftung der Angestellten, Arbeitnehmer, Mitarbeiter, Vertreter und Erfüllungsgehilfen von MB Grid Solutions & Services.
## 13. Produkthaftung
**13.1** Der Kunde wird die gelieferten Waren ausschließlich vertragsgemäß und entsprechend den verfügbaren Anleitungen von MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services für erforderlich und zweckmäßig hält. MB Grid Solutions & Services ist verpflichtet, die Kosten des Produktrückrufs oder der -warnung zu tragen, es sei denn MB Grid Solutions & Services ist für den Produktfehler und den eingetretenen Schaden nach produkthaftungsrechtlichen Grundsätzen nicht verantwortlich.
**13.3** Der Kunde wird MB Grid Solutions & Services 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 MB Grid Solutions & Services 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. MB Grid Solutions & Services 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 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 MB Grid Solutions & Services seine eigentums- und urheberrechtlichen Verwertungsrechte uneingeschränkt vor. Die Unterlagen dürfen nur nach vorheriger schriftlicher Zustimmung von MB Grid Solutions & Services Dritten zugänglich gemacht werden und sind MB Grid Solutions & Services auf Verlangen unverzüglich zurückzugeben.
**15.2** Ohne ausdrückliche Zustimmung von MB Grid Solutions & Services in Textform ist der Kunde nicht berechtigt, die gelieferte Ware zu Werbezwecken zu verwenden.
**15.3** MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services bedarf der gesonderten ausdrücklichen Einwilligung des Kunden.
**15.7** Auf Anordnung einer zuständigen Stelle darf MB Grid Solutions & Services 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, (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 MB Grid Solutions & Services 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, MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 MB Grid Solutions & Services 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 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 MB Grid Solutions & Services verpflichtet, deren Höhe in das billige Ermessen von MB Grid Solutions & Services gestellt wird und im Streitfall durch das zuständige Gericht überprüft werden kann. Durch die Geltendmachung der Vertragsstrafe werden weitergehende Schadensersatzansprüche von MB Grid Solutions & Services 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 MB Grid Solutions & Services und dem Kunden, der Auslegung dieser AVB, sowie die gesamten Rechtsbeziehungen zwischen MB Grid Solutions & Services 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 MB Grid Solutions & Services 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.

View File

@@ -1,40 +1,31 @@
services:
app:
image: node:20-alpine
mb-grid-app:
image: node:20
working_dir: /app
# Use pnpm since the project uses it, and run the next dev script directly
command: sh -c "corepack enable pnpm && pnpm i && pnpm dev:next"
volumes:
- .:/app
environment:
NODE_ENV: development
# Docker Internal Communication
DIRECTUS_URL: http://directus:8055
# Build / dependency installation
NPM_TOKEN: ${NPM_TOKEN}
CI: 'true'
- mb-grid-node-modules:/app/node_modules
ports:
- "3000:3000"
labels:
- "traefik.enable=true"
# Clear all production-related TLS/Middleware settings for the main routers
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.entrypoints=web"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.tls=false"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.middlewares="
# Remove Gatekeeper for local dev simply by not defining it or overwriting?
# Actually, gatekeeper is a separate service. We can keep it or ignore it.
# But the app router normally points to gatekeeper middleware.
# By clearing middlewares above, we bypass gatekeeper for local dev.
directus:
labels:
- "traefik.enable=true"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.entrypoints=web"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.rule=Host(`${DIRECTUS_HOST:-cms.mb-grid-solutions.localhost}`)"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.tls=false"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.middlewares="
ports:
- "8055:8055"
command: sh -c "corepack enable pnpm && pnpm install && pnpm dev:next"
environment:
PUBLIC_URL: http://${DIRECTUS_HOST:-cms.mb-grid-solutions.localhost}
NODE_ENV: development
GATEKEEPER_ORIGIN: ${GATEKEEPER_ORIGIN}
COOKIE_DOMAIN: ${COOKIE_DOMAIN}
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
NPM_TOKEN: ${NPM_TOKEN}
labels:
- "caddy=http://mb-grid-solutions.localhost"
- "caddy.reverse_proxy=mb-grid-app:3000"
networks:
- default
- infra
networks:
default:
name: mb-grid-solutions-internal
infra:
external: true
volumes:
mb-grid-node-modules:

View File

@@ -1,32 +1,36 @@
services:
app:
mb-grid-app:
image: registry.infra.mintel.me/mintel/mb-grid-solutions:${IMAGE_TAG:-latest}
restart: always
networks:
- default
- infra
env_file:
- ${ENV_FILE:-.env}
labels:
- "traefik.enable=true"
- "traefik.http.routers.${PROJECT_NAME}.rule=${TRAEFIK_RULE:-Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)}"
- "traefik.http.routers.${PROJECT_NAME}.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME}.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME}.tls=true"
- "traefik.http.services.${PROJECT_NAME}.loadbalancer.server.port=3000"
- "traefik.http.routers.${PROJECT_NAME}.middlewares=${TRAEFIK_MIDDLEWARES:-${PROJECT_NAME}-auth}"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.tls=true"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.priority=1000"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.service=${PROJECT_NAME:-mb-grid}-app-svc"
- "traefik.http.services.${PROJECT_NAME:-mb-grid}-app-svc.loadbalancer.server.port=3000"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}.middlewares=${TRAEFIK_MIDDLEWARES:-mb-grid-auth,mb-grid-forward,compress}"
- "traefik.docker.network=infra"
# Gatekeeper Router (Shared Host + dedicated Subdomain)
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.rule=${GATEKEEPER_RULE:-(Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathPrefix(`/gatekeeper`)) || Host(`gatekeeper.${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)}"
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.tls=true"
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.service=${PROJECT_NAME}-gatekeeper"
# 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.*|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"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.service=${PROJECT_NAME:-mb-grid}-app-svc"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.priority=2000"
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.address=http://${PROJECT_NAME}-gatekeeper:3000/api/verify"
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.authResponseHeaders=X-Auth-User"
- "traefik.docker.network=infra"
# Forwarded Headers (Protocol Normalization)
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
- "traefik.http.middlewares.compress.compress=true"
healthcheck:
test: [ "CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" ]
interval: 10s
@@ -34,88 +38,55 @@ services:
retries: 5
start_period: 30s
gatekeeper:
image: registry.infra.mintel.me/mintel/gatekeeper:latest
mb-grid-gatekeeper:
image: registry.infra.mintel.me/mintel/gatekeeper:testing
container_name: ${PROJECT_NAME:-mb-grid-solutions}-gatekeeper
restart: always
profiles: [ "gatekeeper" ]
restart: unless-stopped
networks:
infra:
aliases:
- ${PROJECT_NAME:-mb-grid-solutions}-gatekeeper
- mb-grid-gatekeeper
env_file:
- ${ENV_FILE:-.env}
environment:
PORT: ${PORT:-3000}
PROJECT_NAME: ${PROJECT_NAME:-MB Grid Solutions}
PROJECT_COLOR: ${PROJECT_COLOR:-#82ed20}
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.mb-grid-solutions.com}
AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-mintel_gatekeeper_session}
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-mintel}
# Dedicated Base URL for Gatekeeper subdomain to prevent redirect loops
NEXT_PUBLIC_BASE_URL: https://${GATEKEEPER_HOST:-gatekeeper.mb-grid-solutions.localhost}
COOKIE_DOMAIN: ${COOKIE_DOMAIN}
AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME}
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD}
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
healthcheck:
test: [ "CMD", "node", "-e", "fetch('http://127.0.0.1:3000/gatekeeper/login').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" ]
interval: 10s
timeout: 5s
retries: 5
labels:
- "traefik.enable=true"
- "traefik.http.services.${PROJECT_NAME}-gatekeeper.loadbalancer.server.port=3000"
- "traefik.http.services.mb-grid-gatekeeper-svc.loadbalancer.server.port=3000"
# Gatekeeper Verification Middleware
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-auth.forwardauth.address=http://${PROJECT_NAME:-mb-grid}-gatekeeper:3000/gatekeeper/api/verify"
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid}-auth.forwardauth.authResponseHeaders=X-Auth-User"
# Gatekeeper Public Router (Login/Auth UI)
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.rule=(Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathPrefix(`/gatekeeper`))"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.tls=true"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.priority=2000"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.service=${PROJECT_NAME:-mb-grid}-gatekeeper-svc"
- "traefik.docker.network=infra"
directus:
image: directus/directus:11
restart: always
networks:
- infra
- backend
env_file:
- ${ENV_FILE:-.env}
environment:
DB_CLIENT: 'pg'
DB_HOST: 'directus-db'
DB_PORT: '5432'
WEBSOCKETS_ENABLED: 'true'
PUBLIC_URL: ${DIRECTUS_URL}
KEY: ${DIRECTUS_KEY}
SECRET: ${DIRECTUS_SECRET}
ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL}
ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD}
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
DB_USER: ${DIRECTUS_DB_USER:-directus}
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
# Telemetry & Performance
LOGGER_LEVEL: ${LOG_LEVEL:-info}
SENTRY_DSN: ${SENTRY_DSN}
SENTRY_ENVIRONMENT: ${TARGET:-development}
volumes:
- ./directus/uploads:/directus/uploads
- ./directus/extensions:/directus/extensions
labels:
- "traefik.enable=true"
- "traefik.http.routers.${PROJECT_NAME}-directus.rule=Host(`${DIRECTUS_HOST:-cms.mb-grid-solutions.localhost}`)"
- "traefik.http.routers.${PROJECT_NAME}-directus.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME}-directus.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME}-directus.tls=true"
- "traefik.http.routers.${PROJECT_NAME}-directus.middlewares=${PROJECT_NAME}-forward,compress"
- "traefik.http.services.${PROJECT_NAME}-directus.loadbalancer.server.port=8055"
- "traefik.http.middlewares.${PROJECT_NAME}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.docker.network=infra"
directus-db:
image: postgres:15-alpine
restart: always
networks:
- backend
env_file:
- ${ENV_FILE:-.env}
environment:
POSTGRES_DB: ${DIRECTUS_DB_NAME:-directus}
POSTGRES_USER: ${DIRECTUS_DB_USER:-directus}
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
volumes:
- directus-db-data:/var/lib/postgresql/data
networks:
default:
name: ${PROJECT_NAME:-mb-grid-solutions}-internal
infra:
external: true
backend:
internal: true
volumes:
directus-db-data:

View File

@@ -20,7 +20,7 @@ CI (Woodpecker)
https://ci.infra.mintel.me
Container Registry
https://registry.infra.mintel.me
https://git.infra.mintel.me
Errors (GlitchTip)
https://errors.infra.mintel.me
@@ -76,13 +76,13 @@ This directory contains:
All production images must be built by CI and pushed to the Mintel Registry.
Registry:
registry.infra.mintel.me
git.infra.mintel.me
Image naming:
registry.infra.mintel.me/ORG/APP_NAME:TAG
git.infra.mintel.me/mmintel/APP_NAME:TAG
Example:
registry.infra.mintel.me/mintel/mb-grid-solutions:latest
git.infra.mintel.me/mmintel/mb-grid-solutions:latest
---
@@ -204,8 +204,8 @@ steps:
build:
image: woodpeckerci/plugin-docker
settings:
registry: registry.infra.mintel.me
repo: registry.infra.mintel.me/mintel/mb-grid-solutions
registry: git.infra.mintel.me
repo: git.infra.mintel.me/mmintel/mb-grid-solutions
username:
from_secret: REGISTRY_USER
password:

10
instrumentation.ts Normal file
View 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');
}
}

View File

@@ -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),
@@ -61,14 +61,6 @@ function createConfig() {
from: env.MAIL_FROM,
recipients: env.MAIL_RECIPIENTS,
},
directus: {
url: env.DIRECTUS_URL,
adminEmail: env.DIRECTUS_ADMIN_EMAIL,
password: env.DIRECTUS_ADMIN_PASSWORD,
token: env.DIRECTUS_API_TOKEN,
internalUrl: env.INTERNAL_DIRECTUS_URL,
proxyPath: "/cms",
},
notifications: {
gotify: {
url: env.GOTIFY_URL,
@@ -131,9 +123,6 @@ export const config = {
get mail() {
return getConfig().mail;
},
get directus() {
return getConfig().directus;
},
get notifications() {
return getConfig().notifications;
},
@@ -176,12 +165,6 @@ export function getMaskedConfig() {
from: c.mail.from,
recipients: c.mail.recipients,
},
directus: {
url: c.directus.url,
adminEmail: mask(c.directus.adminEmail),
password: mask(c.directus.password),
token: mask(c.directus.token),
},
notifications: {
gotify: {
url: c.notifications.gotify.url,

View File

@@ -1,43 +0,0 @@
import { createDirectus, rest, authentication } from "@directus/sdk";
import { config } from "./config";
import { getServerAppServices } from "./services/create-services.server";
const { url, adminEmail, password, token, internalUrl } = config.directus;
// Use internal URL if on server to bypass Gatekeeper/Auth/Proxy issues
const effectiveUrl =
typeof window === "undefined" && internalUrl ? internalUrl : url;
const client = createDirectus(effectiveUrl).with(rest()).with(authentication());
/**
* Ensures the client is authenticated.
* Falls back to login with admin credentials if no static token is provided.
*/
export async function ensureAuthenticated() {
if (token) {
client.setToken(token);
return;
}
if (adminEmail && password) {
try {
await client.login({ email: adminEmail, password: password });
return;
} catch (e) {
if (typeof window === "undefined") {
getServerAppServices().errors.captureException(e, {
phase: "directus_auth_fallback",
});
}
console.error("Failed to authenticate with Directus login fallback:", e);
throw e;
}
}
throw new Error(
"Missing Directus authentication credentials (token or admin email/password)",
);
}
export default client;

70
lib/env.test.ts Normal file
View File

@@ -0,0 +1,70 @@
import { describe, it, expect } from "vitest";
import { envSchema } from "./env";
describe("envSchema", () => {
it("should allow missing MAIL_HOST in development", () => {
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "http://localhost:3000",
TARGET: "development",
});
expect(result.success).toBe(true);
});
it("should require MAIL_HOST in production", () => {
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "https://example.com",
TARGET: "production",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toBe(
"MAIL_HOST is required in non-development environments",
);
}
});
it("should require MAIL_HOST in testing", () => {
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "https://testing.example.com",
TARGET: "testing",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toBe(
"MAIL_HOST is required in non-development environments",
);
}
});
it("should require MAIL_HOST in staging", () => {
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "https://staging.example.com",
TARGET: "staging",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toBe(
"MAIL_HOST is required in non-development environments",
);
}
});
it("should pass if MAIL_HOST is provided in production", () => {
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "https://example.com",
TARGET: "production",
MAIL_HOST: "smtp.example.com",
});
expect(result.success).toBe(true);
});
it("should skip MAIL_HOST requirement if SKIP_RUNTIME_ENV_VALIDATION is true", () => {
process.env.SKIP_RUNTIME_ENV_VALIDATION = "true";
const result = envSchema.safeParse({
NEXT_PUBLIC_BASE_URL: "https://example.com",
TARGET: "production",
});
expect(result.success).toBe(true);
delete process.env.SKIP_RUNTIME_ENV_VALIDATION;
});
});

View File

@@ -1,144 +1,41 @@
import { z } from "zod";
/**
* Helper to treat empty strings as undefined.
*/
const preprocessEmptyString = (val: unknown) => (val === "" ? undefined : val);
import {
validateMintelEnv,
mintelEnvSchema,
withMintelRefinements,
} from "@mintel/next-utils";
/**
* Environment variable schema.
* Extends the default Mintel environment schema which already includes:
* - Directus (URL, TOKEN, INTERNAL_URL, etc.)
* - Mail (HOST, PORT, etc.)
* - Gotify
* - Logging
* - Analytics
*/
export const envSchema = z
.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
NEXT_PUBLIC_BASE_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
NEXT_PUBLIC_TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
// Analytics
UMAMI_WEBSITE_ID: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
UMAMI_API_ENDPOINT: z.preprocess(
preprocessEmptyString,
z.string().url().default("https://analytics.infra.mintel.me"),
),
// Error Tracking
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
// Logging
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
// Mail
MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PORT: z.preprocess(
preprocessEmptyString,
z.coerce.number().default(587),
),
MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_RECIPIENTS: z.preprocess(
(val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
z.array(z.string()).default([]),
),
// Directus
DIRECTUS_URL: z.preprocess(
preprocessEmptyString,
z.string().url().default("http://localhost:8055"),
),
DIRECTUS_ADMIN_EMAIL: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
DIRECTUS_ADMIN_PASSWORD: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
DIRECTUS_API_TOKEN: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
INTERNAL_DIRECTUS_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
// Deploy Target
TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
// Gotify
GOTIFY_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
})
.superRefine((data, ctx) => {
const target = data.NEXT_PUBLIC_TARGET || data.TARGET;
const isDev = target === "development" || !target;
const isBuildTimeValidation =
process.env.SKIP_RUNTIME_ENV_VALIDATION === "true";
const isServer = typeof window === "undefined";
// Only enforce server-only variables when running on the server.
// In the browser, non-NEXT_PUBLIC_ variables are undefined and should not trigger validation errors.
if (isServer && !isDev && !isBuildTimeValidation && !data.MAIL_HOST) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "MAIL_HOST is required in non-development environments",
path: ["MAIL_HOST"],
});
}
});
export type Env = z.infer<typeof envSchema>;
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(),
};
/**
* Collects all environment variables from the process.
* Explicitly references NEXT_PUBLIC_ variables for Next.js inlining.
* Full schema including Mintel base and refinements
*/
export const envSchema = withMintelRefinements(
z.object(mintelEnvSchema).extend(envExtension),
);
/**
* Validated environment object.
*/
export const env = validateMintelEnv(envExtension);
/**
* For legacy compatibility with existing code.
*/
export function getRawEnv() {
return {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
UMAMI_WEBSITE_ID:
process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
UMAMI_API_ENDPOINT:
process.env.UMAMI_API_ENDPOINT ||
process.env.UMAMI_SCRIPT_URL ||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
SENTRY_DSN: process.env.SENTRY_DSN,
LOG_LEVEL: process.env.LOG_LEVEL,
MAIL_HOST: process.env.MAIL_HOST,
MAIL_PORT: process.env.MAIL_PORT,
MAIL_USERNAME: process.env.MAIL_USERNAME,
MAIL_PASSWORD: process.env.MAIL_PASSWORD,
MAIL_FROM: process.env.MAIL_FROM,
MAIL_RECIPIENTS: process.env.MAIL_RECIPIENTS,
DIRECTUS_URL: process.env.DIRECTUS_URL,
DIRECTUS_ADMIN_EMAIL: process.env.DIRECTUS_ADMIN_EMAIL,
DIRECTUS_ADMIN_PASSWORD: process.env.DIRECTUS_ADMIN_PASSWORD,
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
TARGET: process.env.TARGET,
GOTIFY_URL: process.env.GOTIFY_URL,
GOTIFY_TOKEN: process.env.GOTIFY_TOKEN,
};
return env;
}

View File

@@ -73,4 +73,15 @@ export interface AnalyticsService {
* ```
*/
trackPageview(url?: string): void;
/**
* Set the server-side context for the current request.
* This is used for server-side tracking (e.g. from Next.js proxy).
*/
setServerContext?(context: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
}): void;
}

View File

@@ -68,4 +68,16 @@ export class NoopAnalyticsService implements AnalyticsService {
trackPageview(_url?: string) {
// intentionally noop - analytics are disabled
}
/**
* No-op implementation of setServerContext.
*/
setServerContext(_context: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
}) {
// intentionally noop - analytics are disabled
}
}

View File

@@ -25,6 +25,12 @@ export type UmamiAnalyticsServiceOptions = {
export class UmamiAnalyticsService implements AnalyticsService {
private websiteId?: string;
private endpoint: string;
private serverContext?: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
};
constructor(private readonly options: UmamiAnalyticsServiceOptions) {
this.websiteId = config.analytics.umami.websiteId;
@@ -36,6 +42,19 @@ export class UmamiAnalyticsService implements AnalyticsService {
: "/stats";
}
/**
* Set the server-side context for the current request.
* This allows the service to use real request headers for tracking.
*/
setServerContext(context: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
}) {
this.serverContext = context;
}
/**
* Internal method to send the payload to Umami API.
*/
@@ -47,24 +66,47 @@ export class UmamiAnalyticsService implements AnalyticsService {
const payload = {
website: this.websiteId,
hostname:
typeof window !== "undefined" ? window.location.hostname : "server",
typeof window !== "undefined"
? window.location.hostname
: this.serverContext?.referrer
? new URL(this.serverContext.referrer).hostname
: "server",
screen:
typeof window !== "undefined"
? `${window.screen.width}x${window.screen.height}`
: undefined,
language:
typeof window !== "undefined" ? navigator.language : undefined,
referrer: typeof window !== "undefined" ? document.referrer : undefined,
typeof window !== "undefined"
? navigator.language
: this.serverContext?.language,
referrer:
typeof window !== "undefined"
? document.referrer
: this.serverContext?.referrer,
...data,
};
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
// Set User-Agent
if (typeof window !== "undefined") {
headers["User-Agent"] = navigator.userAgent;
} else if (this.serverContext?.userAgent) {
headers["User-Agent"] = this.serverContext.userAgent;
} else {
headers["User-Agent"] = "Mintel-Server-Proxy";
}
// Forward client IP if available (Umami must be configured to trust this)
if (this.serverContext?.ip) {
headers["X-Forwarded-For"] = this.serverContext.ip;
}
const response = await fetch(`${this.endpoint}/api/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent":
typeof window === "undefined" ? "KLZ-Server" : navigator.userAgent,
},
headers,
body: JSON.stringify({ type, payload }),
keepalive: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -93,7 +135,9 @@ export class UmamiAnalyticsService implements AnalyticsService {
url:
typeof window !== "undefined"
? window.location.pathname + window.location.search
: undefined,
: this.serverContext?.referrer
? new URL(this.serverContext.referrer).pathname
: "/",
});
}
@@ -106,7 +150,9 @@ export class UmamiAnalyticsService implements AnalyticsService {
url ||
(typeof window !== "undefined"
? window.location.pathname + window.location.search
: undefined),
: this.serverContext?.referrer
? new URL(this.serverContext.referrer).pathname
: "/"),
});
}
}

View File

@@ -31,33 +31,63 @@ export class PinoLoggerService implements LoggerService {
}
trace(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.trace(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).trace(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).trace(msg, ...args);
}
}
debug(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.debug(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).debug(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).debug(msg, ...args);
}
}
info(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.info(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).info(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).info(msg, ...args);
}
}
warn(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.warn(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).warn(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).warn(msg, ...args);
}
}
error(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.error(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).error(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).error(msg, ...args);
}
}
fatal(msg: string, ...args: unknown[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.logger.fatal(msg, ...(args as any));
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).fatal(args[0] as object, msg, ...args.slice(1));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.logger as any).fatal(msg, ...args);
}
}
child(bindings: Record<string, unknown>): LoggerService {

7
mdx-components.tsx Normal file
View File

@@ -0,0 +1,7 @@
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
};
}

View File

@@ -1,7 +1,7 @@
{
"Index": {
"hero": {
"tag": "Engineering Excellence",
"tag": "Technische Beratung",
"title": "Spezialisierter Partner für Energiekabelprojekte",
"titleHighlight": "Energiekabelprojekte",
"subtitle": "Herstellerneutrale technische Beratung für Ihre Projekte in Mittel- und Hochspannungsnetzen bis zu 110 kV.",
@@ -31,7 +31,7 @@
"expertise": {
"tag": "Expertise",
"title": "Anwendungen & Zielgruppen",
"description": "Wir unterstützen Akteure der Energiewende bei der Realisierung komplexer Kabelprojekte mit höchster Präzision.",
"description": "Wir unterstützen Sie bei der Realisierung Ihrer Kabelprojekte.",
"groups": [
"Energieversorger",
"Ingenieurbüros",
@@ -81,18 +81,18 @@
"legal": "Rechtliches",
"impressum": "Impressum",
"datenschutz": "Datenschutz",
"agb": "AGB",
"avb": "AVB",
"rights": "Alle Rechte vorbehalten.",
"madeWith": "Made with",
"precision": "precision",
"inGermany": "in Germany"
"madeWith": "Entwickelt mit",
"precision": "Präzision",
"inGermany": "in Deutschland"
}
},
"About": {
"hero": {
"tagline": "Über uns",
"title": "Wir gestalten die Infrastructure der Zukunft",
"subtitle": "MB Grid Solution steht für technische Exzellenz in der Energiekabeltechnologie. Wir verstehen uns als Ihr technischer Lotse."
"title": "Zuverlässige Begleitung für Ihre Netzinfrastruktur",
"subtitle": "Herstellerneutrale Beratung in der Energiekabeltechnologie. Wir verstehen uns als Ihr technischer Lotse."
},
"intro": {
"p1": "Unsere Wurzeln liegen in der tiefen praktischen Erfahrung unserer technischen Berater und unserer Netzwerke im globalem Kabelmarkt. Wir vereinen Tradition mit modernster Innovation, um zuverlässige Energielösungen für Projekte bis 110 kV zu realisieren.",
@@ -148,7 +148,8 @@
"info": {
"email": "E-Mail",
"address": "Anschrift",
"company": "MB Grid Solutions & Services GmbH"
"company": "MB Grid Solutions & Services GmbH",
"mapTitle": "Standort Karte"
},
"form": {
"name": "Name *",

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,7 +1,9 @@
import withMintelConfig from "@mintel/next-config";
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
async rewrites() {
const umamiUrl =
process.env.UMAMI_API_ENDPOINT ||
@@ -17,12 +19,22 @@ const nextConfig = {
source: "/stats/:path*",
destination: `${umamiUrl}/:path*`,
},
{
source: "/:locale(de)/stats/:path*",
destination: `${umamiUrl}/:path*`,
},
{
source: "/errors/:path*",
destination: `${glitchtipUrl}/:path*`,
},
{
source: "/:locale(de)/errors/:path*",
destination: `${glitchtipUrl}/:path*`,
},
];
},
};
export default withMintelConfig(nextConfig);
const withMDX = createMDX({});
export default withMintelConfig(withMDX(nextConfig));

View File

@@ -1,33 +1,39 @@
{
"name": "mb-grid-solutions.com",
"version": "1.0.0",
"version": "1.4.0",
"type": "module",
"packageManager": "pnpm@10.18.3",
"scripts": {
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mb-grid-solutions.localhost\\n🗄️ CMS: http://cms.mb-grid-solutions.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker compose down --remove-orphans && docker compose up app directus directus-db",
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mb-grid-solutions.localhost\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker compose down --remove-orphans && docker compose up mb-grid-app",
"dev:next": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint app components lib scripts",
"test": "vitest",
"prepare": "husky",
"cms:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus.ts",
"cms:push:staging": "./scripts/sync-directus.sh push staging",
"cms:pull:staging": "./scripts/sync-directus.sh pull staging",
"cms:push:testing": "./scripts/sync-directus.sh push testing",
"cms:pull:testing": "./scripts/sync-directus.sh pull testing",
"cms:push:prod": "./scripts/sync-directus.sh push production",
"cms:pull:prod": "./scripts/sync-directus.sh pull production",
"pagespeed:test": "mintel pagespeed test"
"pagespeed:test": "mintel pagespeed test",
"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": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@mintel/next-config": "^1.1.13",
"@mintel/next-utils": "^1.1.13",
"@aws-sdk/client-s3": "^3.999.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mintel/mail": "^1.8.21",
"@mintel/next-config": "^1.8.21",
"@mintel/next-utils": "^1.8.21",
"@next/mdx": "^16.2.4",
"@sentry/nextjs": "^10.38.0",
"@types/mdx": "^2.0.13",
"framer-motion": "^12.29.2",
"lucide-react": "^0.562.0",
"next": "^16.1.6",
@@ -36,16 +42,16 @@
"pino": "^10.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"zod": "^4.3.6"
"sharp": "^0.34.5",
"zod": "^3.24.1"
},
"devDependencies": {
"@commitlint/cli": "^20.4.0",
"@commitlint/config-conventional": "^20.4.0",
"@directus/sdk": "^21.0.0",
"@mintel/cli": "^1.1.13",
"@mintel/eslint-config": "^1.1.13",
"@mintel/husky-config": "^1.1.13",
"@mintel/tsconfig": "^1.1.13",
"@mintel/cli": "^1.8.21",
"@mintel/eslint-config": "^1.8.21",
"@mintel/husky-config": "^1.8.21",
"@mintel/tsconfig": "^1.8.21",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -54,17 +60,39 @@
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.23",
"autoprefixer": "^10.4.24",
"axios": "^1.13.6",
"cheerio": "^1.2.0",
"eslint": "^8.57.1",
"eslint-config-next": "15.1.6",
"happy-dom": "^20.6.1",
"html-validate": "^10.13.1",
"husky": "^9.1.7",
"jsdom": "^27.4.0",
"lint-staged": "^16.2.7",
"pa11y-ci": "^4.1.0",
"pino-pretty": "^13.1.3",
"postcss": "^8.5.6",
"prettier": "^3.5.0",
"prettier": "^3.8.1",
"puppeteer": "^24.42.0",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"pnpm": {
"onlyBuiltDependencies": [
"puppeteer",
"sharp",
"esbuild",
"@parcel/watcher",
"@swc/core",
"@sentry/cli",
"unrs-resolver"
]
},
"repository": {
"type": "git",
"url": "ssh://git@git.infra.mintel.me:2222/mmintel/mb-grid-solutions.com.git"
}
}

5243
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,5 @@ export default createMiddleware({
export const config = {
// Matcher for all pages and internationalized pathnames
// excluding api, _next, static files, etc.
matcher: [
"/((?!api|stats|errors|_next|_vercel|.*\\..*).*)",
"/",
"/(de)/:path*",
],
matcher: ["/((?!api|_next|.*\\..*).*)"],
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 736 KiB

After

Width:  |  Height:  |  Size: 428 KiB

53
scripts/backup-db.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────────────
# Payload CMS Database Backup
# Creates a timestamped pg_dump of the Payload Postgres database.
# Usage: npm run backup:db
# ────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# Load environment variables
if [ -f .env ]; then
set -a; source .env; set +a
fi
# Fallback for local development if not in .env
DB_NAME="${POSTGRES_DB:-payload}"
DB_USER="${POSTGRES_USER:-postgres}"
# For production, we need the container name.
# We'll use the PROJECT_NAME to find it if possible, otherwise use a default.
PROJECT_NAME="${PROJECT_NAME:-mb-grid-solutions-production}"
DB_CONTAINER="${DB_CONTAINER:-${PROJECT_NAME}-mb-grid-db-1}"
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Check if container is running
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then
echo "❌ Database container '$DB_CONTAINER' is not running."
echo " Check your docker-compose status."
exit 1
fi
echo "📦 Backing up Payload database..."
echo " Container: $DB_CONTAINER"
echo " Database: $DB_NAME"
echo " Output: $BACKUP_FILE"
# Run pg_dump inside the container and compress
# We use directus as user for now if we haven't fully switched to postgres user in all environments
# But the script should be consistent with the environment.
docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE"
# Show result
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo ""
echo "✅ Backup complete: $BACKUP_FILE ($SIZE)"
echo ""
# Show existing backups
echo "📋 Available backups:"
ls -lh "$BACKUP_DIR"/*.sql.gz 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'

136
scripts/check-apis.ts Normal file
View File

@@ -0,0 +1,136 @@
import axios from "axios";
import dns from "dns";
import { promisify } from "util";
import url from "url";
const resolve4 = promisify(dns.resolve4);
// This script verifies that external logging and analytics APIs are reachable
// from the deployment environment (which could be behind corporate firewalls or VPNs).
const umamiEndpoint =
process.env.UMAMI_API_ENDPOINT || "https://analytics.infra.mintel.me";
const sentryDsn = process.env.SENTRY_DSN || "";
async function checkUmami() {
console.log(`\n🔍 Checking Umami Analytics API Availability...`);
console.log(` Endpoint: ${umamiEndpoint}`);
try {
// Umami usually exposes a /api/heartbeat or /api/health if we know the route.
// Trying root or /api/auth/verify (which will give 401 but proves routing works).
// A simple GET to the configured endpoint should return a 200 or 401, not a 5xx/timeout.
const response = await axios.get(
`${umamiEndpoint.replace(/\/$/, "")}/api/health`,
{
timeout: 5000,
validateStatus: () => true, // Accept any status, we just want to know it's reachable and not 5xx
},
);
// As long as it's not a 502/503/504 Bad Gateway/Timeout, the service is "up" from our perspective
if (response.status >= 500) {
throw new Error(
`Umami API responded with server error HTTP ${response.status}`,
);
}
console.log(` ✅ Umami Analytics is reachable (HTTP ${response.status})`);
return true;
} catch (error) {
const err = error as Error;
// If /api/health fails completely, maybe try a DNS check as a fallback
try {
console.warn(
` ⚠️ HTTP check failed, falling back to DNS resolution...`,
);
const umamiHost = new url.URL(umamiEndpoint).hostname;
await resolve4(umamiHost);
console.log(
` ✅ Umami Analytics DNS resolved successfully (${umamiHost})`,
);
return true;
} catch (error) {
const dnsErr = error as Error;
console.error(
` ❌ CRITICAL: Umami Analytics is completely unreachable! ${err.message} | DNS: ${dnsErr.message}`,
);
return false;
}
}
}
async function checkSentry() {
console.log(`\n🔍 Checking Glitchtip/Sentry Error Tracking Availability...`);
if (!sentryDsn) {
console.log(` No SENTRY_DSN provided in environment. Skipping.`);
return true;
}
try {
const parsedDsn = new url.URL(sentryDsn);
const host = parsedDsn.hostname;
console.log(` Host: ${host}`);
// We do a DNS lookup to ensure the runner can actually resolve the tracking server
const addresses = await resolve4(host);
if (addresses && addresses.length > 0) {
console.log(` ✅ Glitchtip/Sentry domain resolved: ${addresses[0]}`);
// Optional: Quick TCP/HTTP check to the host root (Glitchtip usually runs on 80/443 root)
try {
const proto = parsedDsn.protocol || "https:";
await axios.get(`${proto}//${host}/api/0/`, {
timeout: 5000,
validateStatus: () => true,
});
console.log(` ✅ Glitchtip/Sentry API root responds to HTTP.`);
} catch {
console.log(
` ⚠️ Glitchtip/Sentry HTTP ping failed or timed out, but DNS is valid. Proceeding.`,
);
}
return true;
}
throw new Error("No IP addresses found for DSN host");
} catch (error) {
const err = error as Error;
console.error(
` ❌ CRITICAL: Glitchtip/Sentry DSN is invalid or hostname is unresolvable! ${err.message}`,
);
return false;
}
}
async function main() {
console.log("🚀 Starting External API Connectivity Smoke Test...");
let hasErrors = false;
const umamiOk = await checkUmami();
if (!umamiOk) hasErrors = true;
const sentryOk = await checkSentry();
if (!sentryOk) hasErrors = true;
if (hasErrors) {
console.error(
`\n🚨 POST-DEPLOY CHECK FAILED: One or more critical external APIs are unreachable.`,
);
console.error(
` This might mean the deployment environment lacks outbound internet access, `,
);
console.error(
` DNS is misconfigured, or the upstream services are down.`,
);
process.exit(1);
}
console.log(`\n🎉 SUCCESS: All required external APIs are reachable!`);
process.exit(0);
}
main();

View File

@@ -0,0 +1,225 @@
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: `mintel_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: unknown) {
const errorBody = err instanceof Error ? err.message : String(err);
console.error(`❌ Failed to fetch sitemap: ${errorBody}`);
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: "mintel_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: unknown) => {
const errorMessage = err instanceof Error ? err.message : String(err);
consoleErrorsList.push({
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)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page.on("console", (msg: any) => {
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: 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;
}
}
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();

97
scripts/check-html.ts Normal file
View File

@@ -0,0 +1,97 @@
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: `mintel_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: `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`;
fs.writeFileSync(path.join(outputDir, filename), res.data);
} 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",
});
console.log(`✅ HTML Validation passed perfectly!`);
} catch {
console.error(`❌ HTML Validation found issues.`);
process.exit(1);
}
} 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 });
}
}
main();

91
scripts/check-http.ts Normal file
View File

@@ -0,0 +1,91 @@
import axios from "axios";
import * as cheerio from "cheerio";
const targetUrl =
process.argv[2] ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:3000";
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "lassmichrein";
async function main() {
console.log(`\n🚀 Starting HTTP Sitemap Validation for: ${targetUrl}\n`);
try {
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const response = await axios.get(sitemapUrl, {
headers: { Cookie: `mintel_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} target URLs in sitemap.`);
if (urls.length === 0) {
console.error("❌ No URLs found in sitemap. Is the site up?");
process.exit(1);
}
console.log(`\n🔍 Verifying HTTP Status Codes (Limit: None)...`);
let hasErrors = false;
// Run fetches sequentially to avoid overwhelming the server during CI
for (let i = 0; i < urls.length; i++) {
const u = urls[i];
try {
const res = await axios.get(u, {
headers: {
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
},
validateStatus: null, // Don't throw on error status
});
if (res.status >= 400) {
console.error(`❌ ERROR ${res.status}: ${res.statusText} -> ${u}`);
hasErrors = true;
} else {
console.log(`✅ OK ${res.status} -> ${u}`);
}
} catch (error) {
const err = error as Error;
console.error(`❌ NETWORK ERROR: ${err.message} -> ${u}`);
hasErrors = true;
}
}
if (hasErrors) {
console.error(
`\n❌ HTTP Sitemap Validation Failed. One or more pages returned an error.`,
);
process.exit(1);
} else {
console.log(
`\n✨ Success: All ${urls.length} pages are healthy! (HTTP 200)`,
);
process.exit(0);
}
} catch (e: unknown) {
if (axios.isAxiosError(e) && e.response) {
console.error(
`\n❌ Critical Error during Sitemap Fetch: HTTP ${e.response.status} ${e.response.statusText}`,
);
} else {
const errorMsg = e instanceof Error ? e.message : String(e);
console.error(`\n❌ Critical Error during Sitemap Fetch: ${errorMsg}`);
}
process.exit(1);
}
}
main();

199
scripts/check-locale.ts Normal file
View File

@@ -0,0 +1,199 @@
import axios from "axios";
import * as cheerio from "cheerio";
/**
* Locale & Language Switcher Smoke Test
*
* For every URL in the sitemap:
* 1. Fetches the page HTML
* 2. Extracts <link rel="alternate" hreflang="..." href="..."> tags
* 3. Verifies each alternate URL uses correctly translated slugs
* 4. Verifies each alternate URL returns HTTP 200
*/
const targetUrl =
process.argv[2] ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:3000";
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "lassmichrein";
// Expected slug translations: German key → English value
const SLUG_MAP: Record<string, string> = {
// Add translations if mb-grid translates URLs: e.g. produkte: 'products'
};
// Reverse map: English → German
const REVERSE_SLUG_MAP: Record<string, string> = Object.fromEntries(
Object.entries(SLUG_MAP).map(([de, en]) => [en, de]),
);
const headers = { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` };
function getExpectedTranslation(
sourcePath: string,
sourceLocale: string,
targetLocale: string,
): string {
const segments = sourcePath.split("/").filter(Boolean);
// First segment is locale
segments[0] = targetLocale;
const map = sourceLocale === "de" ? SLUG_MAP : REVERSE_SLUG_MAP;
return (
"/" +
segments
.map((seg, i) => {
if (i === 0) return seg; // locale
return map[seg] || seg; // translate or keep (product names like n2x2y stay the same)
})
.join("/")
);
}
async function main() {
console.log(`\n🌐 Starting Locale Smoke Test for: ${targetUrl}\n`);
// 1. Fetch sitemap
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const sitemapRes = await axios.get(sitemapUrl, {
headers,
validateStatus: (s) => s < 400,
});
const $sitemap = cheerio.load(sitemapRes.data, { xmlMode: true });
let urls = $sitemap("url loc")
.map((_i, el) => $sitemap(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.\n`);
let totalChecked = 0;
let totalPassed = 0;
let totalFailed = 0;
const failures: string[] = [];
for (const url of urls) {
const path = new URL(url).pathname;
const locale = path.split("/")[1];
if (!locale || !["de", "en"].includes(locale)) continue;
try {
const res = await axios.get(url, { headers, validateStatus: null });
if (res.status >= 400) continue; // Skip pages that are already broken (check-http catches those)
const $ = cheerio.load(res.data);
// Extract hreflang alternate links
const alternates: { hreflang: string; href: string }[] = [];
$('link[rel="alternate"][hreflang]').each((_i, el) => {
const hreflang = $(el).attr("hreflang") || "";
let href = $(el).attr("href") || "";
if (href && hreflang && hreflang !== "x-default") {
href = href.replace(urlPattern, targetUrl.replace(/\/$/, ""));
alternates.push({ hreflang, href });
}
});
if (alternates.length === 0) {
// Some pages may not have alternates, that's OK
continue;
}
totalChecked++;
// Validate each alternate
let pageOk = true;
for (const alt of alternates) {
if (alt.hreflang === locale) continue; // Same locale, skip
// 1. Check slug translation is correct
const expectedPath = getExpectedTranslation(path, locale, alt.hreflang);
const actualPath = new URL(alt.href).pathname;
if (actualPath !== expectedPath) {
console.error(
`❌ SLUG MISMATCH: ${path} → hreflang="${alt.hreflang}" expected ${expectedPath} but got ${actualPath}`,
);
failures.push(
`Slug mismatch: ${path}${alt.hreflang}: expected ${expectedPath}, got ${actualPath}`,
);
pageOk = false;
continue;
}
// 2. Check alternate URL returns 200
try {
const altRes = await axios.get(alt.href, {
headers,
validateStatus: null,
maxRedirects: 5,
});
if (altRes.status >= 400) {
console.error(
`❌ BROKEN ALTERNATE: ${path}${alt.href} returned ${altRes.status}`,
);
failures.push(
`Broken alternate: ${path}${alt.href} (${altRes.status})`,
);
pageOk = false;
}
} catch (error) {
const err = error as Error;
console.error(
`❌ NETWORK ERROR: ${path}${alt.href}: ${err.message}`,
);
failures.push(`Network error: ${path}${alt.href}: ${err.message}`);
pageOk = false;
}
}
if (pageOk) {
console.log(
`${path} — alternates OK (${alternates
.map((a) => a.hreflang)
.filter((h) => h !== locale)
.join(", ")})`,
);
totalPassed++;
} else {
totalFailed++;
}
} catch (error) {
const err = error as Error;
console.error(`❌ NETWORK ERROR fetching ${url}: ${err.message}`);
totalFailed++;
}
}
console.log(`\n${"─".repeat(60)}`);
console.log(`📊 Locale Smoke Test Results:`);
console.log(` Pages checked: ${totalChecked}`);
console.log(` Passed: ${totalPassed}`);
console.log(` Failed: ${totalFailed}`);
if (failures.length > 0) {
console.log(`\n❌ Failures:`);
failures.forEach((f) => console.log(`${f}`));
console.log(`\n❌ Locale Smoke Test FAILED.`);
process.exit(1);
} else {
console.log(
`\n✨ All locale alternates are correctly translated and reachable!`,
);
process.exit(0);
}
}
main().catch((err) => {
console.error(`\n❌ Critical error:`, err.message);
process.exit(1);
});

43
scripts/patch_qa.py Normal file
View 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
View 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)

View File

@@ -1,150 +0,0 @@
import {
createMintelDirectusClient,
ensureDirectusAuthenticated,
} from "@mintel/next-utils";
import { createCollection, createField, updateSettings } from "@directus/sdk";
const client = createMintelDirectusClient();
async function setupBranding() {
const prjName = process.env.PROJECT_NAME || "MB Grid Solutions";
const prjColor = process.env.PROJECT_COLOR || "#82ed20";
console.log(`🎨 Refining Directus Branding for ${prjName}...`);
await ensureDirectusAuthenticated(client);
const cssInjection = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
body, .v-app { font-family: 'Outfit', sans-serif !important; }
.public-view .v-card {
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.9) !important;
border-radius: 32px !important;
box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.4) !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
}
.v-navigation-drawer { background: #000c24 !important; }
.v-list-item--active {
color: ${prjColor} !important;
background: rgba(130, 237, 32, 0.1) !important;
}
</style>
<div style="font-family: 'Outfit', sans-serif; text-align: center; margin-top: 24px;">
<p style="color: rgba(255,255,255,0.6); font-size: 11px; letter-spacing: 2px; margin-bottom: 4px; font-weight: 600; text-transform: uppercase;">Mintel Infrastructure Engine</p>
<h1 style="color: #ffffff; font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -0.5px;">${prjName.toUpperCase()} <span style="color: ${prjColor};">SYNC.</span></h1>
</div>
`;
try {
await client.request(
updateSettings({
project_name: prjName,
project_color: prjColor,
public_note: cssInjection,
module_bar_background: "#00081a",
theme_light_overrides: {
primary: prjColor,
borderRadius: "12px",
navigationBackground: "#000c24",
navigationForeground: "#ffffff",
moduleBarBackground: "#00081a",
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any),
);
console.log("✨ Branding applied!");
await createCollectionAndFields();
console.log("🏗️ Schema alignment complete!");
} catch (error) {
console.error("❌ Error during bootstrap:", error);
}
}
async function createCollectionAndFields() {
const collectionName = "contact_submissions";
try {
await client.request(
createCollection({
collection: collectionName,
schema: {},
meta: {
icon: "contact_mail",
display_template: "{{name}} <{{email}}>",
group: null,
sort: null,
collapse: "open",
},
}),
);
// Add ID field
await client.request(
createField(collectionName, {
field: "id",
type: "integer",
meta: { hidden: true },
schema: { is_primary_key: true, has_auto_increment: true },
}),
);
console.log(`✅ Collection ${collectionName} created.`);
} catch {
console.log(` Collection ${collectionName} exists.`);
}
const safeAddField = async (
field: string,
type: string,
meta: Record<string, unknown> = {},
) => {
try {
await client.request(createField(collectionName, { field, type, meta }));
console.log(`✅ Field ${field} added.`);
} catch {
// Ignore if exists
}
};
await safeAddField("name", "string", {
interface: "input",
display: "raw",
width: "half",
});
await safeAddField("email", "string", {
interface: "input",
display: "raw",
width: "half",
});
await safeAddField("company", "string", {
interface: "input",
display: "raw",
width: "half",
});
await safeAddField("message", "text", {
interface: "textarea",
display: "raw",
width: "full",
});
await safeAddField("date_created", "timestamp", {
interface: "datetime",
special: ["date-created"],
display: "datetime",
display_options: { relative: true },
width: "half",
});
}
setupBranding()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error("🚨 Fatal bootstrap error:", err);
process.exit(1);
});

View File

@@ -1,131 +0,0 @@
#!/bin/bash
# Configuration
REMOTE_HOST="${SSH_HOST:-root@alpha.mintel.me}"
ACTION=$1
ENV=$2
# Help
if [ -z "$ACTION" ] || [ -z "$ENV" ]; then
echo "Usage: ./scripts/sync-directus.sh [push|pull] [testing|staging|production]"
echo ""
echo "Commands:"
echo " push Sync LOCAL data -> REMOTE"
echo " pull Sync REMOTE data -> LOCAL"
echo ""
echo "Environments:"
echo " testing, staging, production"
exit 1
fi
# Project Configuration (extracted from package.json and aligned with deploy.yml)
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//')
REMOTE_DIR="/home/deploy/sites/${PRJ_ID}.com"
case $ENV in
testing) PROJECT_NAME="${PRJ_ID}-testing"; ENV_FILE=".env.testing" ;;
staging) PROJECT_NAME="${PRJ_ID}-staging"; ENV_FILE=".env.staging" ;;
production) PROJECT_NAME="${PRJ_ID}-production"; ENV_FILE=".env.prod" ;;
*) echo "❌ Invalid environment: $ENV"; exit 1 ;;
esac
# DB Details (matching docker-compose defaults)
DB_USER="directus"
DB_NAME="directus"
echo "🔍 Detecting local database..."
LOCAL_DB_CONTAINER=$(docker compose ps -q directus-db)
if [ -z "$LOCAL_DB_CONTAINER" ]; then
# Check if it exists but is stopped
LOCAL_DB_EXISTS=$(docker compose ps -a -q directus-db)
if [ -n "$LOCAL_DB_EXISTS" ]; then
echo "⏳ Local directus-db is stopped. Starting it..."
docker compose up -d directus-db
# Wait a few seconds for PG to be ready
sleep 2
LOCAL_DB_CONTAINER=$(docker compose ps -q directus-db)
fi
fi
if [ -z "$LOCAL_DB_CONTAINER" ]; then
echo "❌ Local directus-db container not found. Is it defined in docker-compose.yaml?"
exit 1
fi
if [ "$ACTION" == "push" ]; then
echo "🚀 Pushing LOCAL -> $ENV ($PROJECT_NAME)..."
# 1. DB Dump
echo "📦 Dumping local database..."
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
# 2. Upload Dump
echo "📤 Uploading dump to remote server..."
scp dump.sql "$REMOTE_HOST:$REMOTE_DIR/dump.sql"
# 3. Restore on Remote
echo "🔄 Restoring dump on $ENV..."
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
if [ -z "$REMOTE_DB_CONTAINER" ]; then
echo "❌ Remote $ENV-db container not found!"
exit 1
fi
echo "🧹 Wiping remote database schema..."
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
echo "⚡ Restoring database..."
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
# 4. Sync Uploads
echo "📁 Syncing uploads (Local -> $ENV)..."
rsync -avz --progress ./directus/uploads/ "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/"
# Clean up
rm dump.sql
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
# 5. Restart Directus to trigger migrations and refresh schema cache
echo "🔄 Restarting remote Directus to apply migrations..."
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
echo "✨ Push to $ENV complete!"
elif [ "$ACTION" == "pull" ]; then
echo "📥 Pulling $ENV Data -> LOCAL..."
# 1. DB Dump on Remote
echo "📦 Dumping remote database ($ENV)..."
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
if [ -z "$REMOTE_DB_CONTAINER" ]; then
echo "❌ Remote $ENV-db container not found!"
exit 1
fi
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql"
# 2. Download Dump
echo "📥 Downloading dump..."
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
# 3. Restore Locally
echo "🧹 Wiping local database schema..."
docker exec "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
echo "⚡ Restoring database locally..."
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
# 4. Sync Uploads
echo "📁 Syncing uploads ($ENV -> Local)..."
rsync -avz --progress "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" ./directus/uploads/
# Clean up
rm dump.sql
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
echo "✨ Pull to Local complete!"
fi

86
scripts/upload-s3.ts Normal file
View File

@@ -0,0 +1,86 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import * as fs from "fs";
import * as path from "path";
const S3_ENDPOINT = process.env.S3_ENDPOINT;
const S3_REGION = process.env.S3_REGION || "fsn1";
const S3_BUCKET = process.env.S3_BUCKET;
const S3_PREFIX = process.env.S3_PREFIX;
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY;
const S3_SECRET_KEY = process.env.S3_SECRET_KEY;
if (!S3_ENDPOINT || !S3_BUCKET || !S3_ACCESS_KEY || !S3_SECRET_KEY) {
console.error("Missing S3 credentials in environment");
process.exit(1);
}
const s3Client = new S3Client({
region: S3_REGION,
endpoint: S3_ENDPOINT,
credentials: {
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET_KEY,
},
forcePathStyle: true,
});
async function uploadDirectory(dirPath: string, prefix: string) {
const files = fs.readdirSync(dirPath, { withFileTypes: true });
for (const file of files) {
if (file.name === ".DS_Store" || file.name === ".gitkeep") continue;
const fullPath = path.join(dirPath, file.name);
// Combine prefix with filename, ensuring no double slashes, e.g., mb-grid-solutions/media/filename.ext
const s3Key = `${prefix}/${file.name}`.replace(/\/+/g, "/");
if (file.isDirectory()) {
await uploadDirectory(fullPath, s3Key);
} else {
const fileContent = fs.readFileSync(fullPath);
let contentType = "application/octet-stream";
if (file.name.endsWith(".png")) contentType = "image/png";
else if (file.name.endsWith(".jpg") || file.name.endsWith(".jpeg"))
contentType = "image/jpeg";
else if (file.name.endsWith(".svg")) contentType = "image/svg+xml";
else if (file.name.endsWith(".webp")) contentType = "image/webp";
else if (file.name.endsWith(".pdf")) contentType = "application/pdf";
try {
await s3Client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
Body: fileContent,
ContentType: contentType,
ACL: "public-read", // Hetzner requires public-read for public access usually
}),
);
console.log(`✅ Uploaded ${file.name} to ${S3_BUCKET}/${s3Key}`);
} catch (err) {
console.error(`❌ Failed to upload ${file.name}:`, err);
}
}
}
}
async function main() {
const mediaDir = path.resolve(process.cwd(), "public/media");
if (fs.existsSync(mediaDir)) {
console.log("Uploading public/media...");
// Media inside Payload CMS uses prefix/media usually, like mb-grid-solutions/media
await uploadDirectory(mediaDir, `${S3_PREFIX}/media`);
} else {
console.log("No public/media directory found.");
}
const assetsDir = path.resolve(process.cwd(), "public/assets");
if (fs.existsSync(assetsDir)) {
console.log("Uploading public/assets...");
await uploadDirectory(assetsDir, `${S3_PREFIX}/assets`);
} else {
console.log("No public/assets directory found.");
}
}
main().catch(console.error);

241
scripts/wcag-sitemap.ts Normal file
View File

@@ -0,0 +1,241 @@
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
*
* 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: `mintel_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: Pa11yConfig = { defaults: {} };
if (fs.existsSync(baseConfigPath)) {
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
}
// 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 = {
...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: `mintel_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 {
// 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: 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, "") || "/";
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: unknown) {
console.error(`\n❌ Error during WCAG Audit:`);
if (error instanceof AxiosError) {
console.error(`Status: ${error.response?.status}`);
console.error(`URL: ${error.config?.url}`);
} else {
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",
);
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
}
}
main();

13
sentry.client.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import * as Sentry from "@sentry/nextjs";
import { config } from "./lib/config";
if (config.errors.glitchtip.enabled) {
Sentry.init({
dsn: config.errors.glitchtip.dsn,
tracesSampleRate: 0.1,
debug: config.isDevelopment,
environment: config.target || "production",
// Use the proxy path defined in config
tunnel: config.errors.glitchtip.proxyPath,
});
}

16
sentry.edge.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as Sentry from "@sentry/nextjs";
import { config } from "./lib/config";
if (config.errors.glitchtip.enabled) {
console.log("Initializing Sentry in Edge runtime...", {
environment: config.target || "production",
});
Sentry.init({
dsn: config.errors.glitchtip.dsn,
tracesSampleRate: 1.0,
debug: true, // Force debug for now to see why it's failing
environment: config.target || "production",
});
} else {
console.warn("Sentry is DISABLED in Edge runtime (missing DSN)");
}

11
sentry.server.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import * as Sentry from "@sentry/nextjs";
import { config } from "./lib/config";
if (config.errors.glitchtip.enabled) {
Sentry.init({
dsn: config.errors.glitchtip.dsn,
tracesSampleRate: 1.0,
debug: config.isDevelopment,
environment: config.target || "production",
});
}

76
test_output.txt Normal file
View 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.

View File

View File

@@ -0,0 +1 @@
export default {};

152
tests/api-contact.test.ts Normal file
View File

@@ -0,0 +1,152 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock Nodemailer
const { mockSendMail } = vi.hoisted(() => ({
mockSendMail: vi.fn().mockResolvedValue({ messageId: "mock-message-id" }),
}));
vi.mock("nodemailer", () => ({
default: {
createTransport: vi.fn().mockReturnValue({
sendMail: mockSendMail,
}),
},
}));
// Mock Email Template renders
vi.mock("@mintel/mail", () => ({
render: vi.fn().mockResolvedValue("<html>Mocked Email HTML</html>"),
ContactFormNotification: () => "ContactFormNotification",
ConfirmationMessage: () => "ConfirmationMessage",
}));
// Mock Notifications and Analytics
const { mockNotify, mockTrack, mockCaptureException } = vi.hoisted(() => ({
mockNotify: vi.fn(),
mockTrack: vi.fn(),
mockCaptureException: vi.fn(),
}));
vi.mock("@/lib/services/create-services.server", () => ({
getServerAppServices: () => ({
logger: {
child: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
},
analytics: {
setServerContext: vi.fn(),
track: mockTrack,
},
notifications: {
notify: mockNotify,
},
errors: {
captureException: mockCaptureException,
},
}),
}));
// Import the route handler we want to test
import { POST } from "../app/api/contact/route";
import { NextResponse } from "next/server";
import type { Mock } from "vitest";
describe("Contact API Integration", () => {
beforeEach(() => {
vi.clearAllMocks();
(NextResponse.json as Mock).mockImplementation((body: any, init?: any) => ({
status: init?.status || 200,
json: async () => body,
}));
});
it("should validate and decline empty or short messages", async () => {
const req = new Request("http://localhost/api/contact", {
method: "POST",
body: JSON.stringify({
name: "Test User",
email: "test@example.com",
message: "too short",
}),
});
const response = await POST(req);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("message_too_short");
// Ensure email was NOT called
expect(mockSendMail).not.toHaveBeenCalled();
});
it("should catch honeypot submissions", async () => {
const req = new Request("http://localhost/api/contact", {
method: "POST",
body: JSON.stringify({
name: "Spam Bot",
email: "spam@example.com",
message: "This is a very long spam message that passes length checks.",
website: "http://spam.com", // Honeypot filled
}),
});
const response = await POST(req);
// Honeypot returns 200 OK so the bot thinks it succeeded
expect(response.status).toBe(200);
// But it actually does NOTHING internally
expect(mockSendMail).not.toHaveBeenCalled();
});
it("should successfully send emails", async () => {
const req = new Request("http://localhost/api/contact", {
method: "POST",
headers: {
"user-agent": "vitest",
"x-forwarded-for": "127.0.0.1",
},
body: JSON.stringify({
name: "Jane Doe",
email: "jane@example.com",
company: "Jane Tech",
message:
"Hello, I am interested in exploring your high-voltage grid solutions.",
}),
});
const response = await POST(req);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.message).toBe("Ok");
// 1. Verify Email Sending
// Note: sendMail is called twice (Notification + User Confirmation)
expect(mockSendMail).toHaveBeenCalledTimes(2);
expect(mockSendMail).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
subject: "Kontaktanfrage von Jane Doe",
replyTo: "jane@example.com",
}),
);
expect(mockSendMail).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
to: "jane@example.com",
subject: "Ihre Kontaktanfrage bei MB Grid Solutions",
}),
);
// 2. Verify notifications and analytics
expect(mockTrack).toHaveBeenCalledWith("contact-form-success", {
has_company: true,
});
});
});

129
tests/contact.test.tsx Normal file
View File

@@ -0,0 +1,129 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextIntlClientProvider } from "next-intl";
import messages from "../messages/de.json";
// Mocks MUST be defined before component import to ensure they are picked up
vi.mock("../components/Reveal", () => ({
Reveal: ({ children }: any) => <>{children}</>,
Stagger: ({ children }: any) => <>{children}</>,
}));
// Better FormData mock for happy-dom
global.FormData = class MockFormData {
private data = new Map();
constructor(form?: HTMLFormElement) {
if (form) {
const elements = form.elements as any;
for (let i = 0; i < elements.length; i++) {
const item = elements.item(i);
if (item.name && item.value) {
this.data.set(item.name, item.value);
}
}
}
}
append(key: string, value: any) {
this.data.set(key, value);
}
get(key: string) {
return this.data.get(key);
}
entries() {
return Array.from(this.data.entries())[Symbol.iterator]();
}
} as any;
// Mock alert
const alertMock = vi.fn();
global.alert = alertMock;
// Import component AFTER mocks
import Contact from "../components/ContactContent";
// Mock fetch
const fetchMock = vi.fn();
global.fetch = fetchMock;
const renderContact = () => {
return render(
<NextIntlClientProvider locale="de" messages={messages}>
<Contact />
</NextIntlClientProvider>,
);
};
describe("Contact Page", () => {
beforeEach(() => {
vi.clearAllMocks();
fetchMock.mockReset();
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
});
it("renders the contact form correctly", () => {
renderContact();
expect(screen.getByLabelText(/Name \*/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Firma/i)).toBeInTheDocument();
expect(screen.getByLabelText(/E-Mail \*/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Nachricht \*/i)).toBeInTheDocument();
});
it("submits the form successfully", async () => {
renderContact();
fireEvent.change(screen.getByLabelText(/Name \*/i), {
target: { value: "John Doe" },
});
fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
target: { value: "This is a test message that is long enough." },
});
const form = screen.getByRole("form");
fireEvent.submit(form);
await waitFor(
() => {
expect(fetchMock).toHaveBeenCalled();
},
{ timeout: 2000 },
);
expect(
(await screen.findAllByText(/Anfrage erfolgreich übermittelt/i)).length,
).toBeGreaterThanOrEqual(1);
expect(
(await screen.findAllByText(/Ihr Anliegen wurde erfasst/i)).length,
).toBeGreaterThanOrEqual(1);
});
it("handles submission errors", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
json: async () => ({ error: "Server error" }),
});
renderContact();
fireEvent.change(screen.getByLabelText(/Name \*/i), {
target: { value: "John Doe" },
});
fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
target: { value: "This is a test message." },
});
const form = screen.getByRole("form");
fireEvent.submit(form);
expect(await screen.findByText(/Server error/i)).toBeInTheDocument();
});
});

37
tests/home.test.tsx Normal file
View File

@@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Home from "../components/HomeContent";
import { NextIntlClientProvider } from "next-intl";
import messages from "../messages/de.json";
const renderHome = () => {
return render(
<NextIntlClientProvider locale="de" messages={messages}>
<Home />
</NextIntlClientProvider>,
);
};
describe("Home Page", () => {
it("renders the hero section with correct title", () => {
renderHome();
expect(
screen.getByRole("heading", { name: /Spezialisierter Partner/i }),
).toBeInTheDocument();
});
it("contains the CTA button", () => {
renderHome();
const ctaButton = screen.getByRole("link", { name: /Projekt anfragen/i });
expect(ctaButton).toBeInTheDocument();
expect(ctaButton).toHaveAttribute("href", "/kontakt");
});
it("renders the portfolio section", async () => {
renderHome();
expect(await screen.findByText(/Unsere Leistungen/i)).toBeInTheDocument();
// Use getAllByText because it appears in both hero description and card title
const elements = await screen.findAllByText(/Technische Beratung/i);
expect(elements.length).toBeGreaterThan(0);
});
});

56
tests/setup.tsx Normal file
View File

@@ -0,0 +1,56 @@
import "@testing-library/jest-dom/vitest";
import React from "react";
import { vi } from "vitest";
// Mock next/navigation
vi.mock("next/navigation", () => ({
usePathname: () => "/",
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
}));
// Mock next-intl to avoid transitive next/server issues
vi.mock("next-intl/middleware", () => ({
default: vi.fn(() => (req: any) => req),
}));
vi.mock("next-intl/server", () => ({
getRequestConfig: vi.fn(),
}));
// Mock next/server
vi.mock("next/server", () => ({
NextResponse: {
json: vi.fn(),
next: vi.fn(),
redirect: vi.fn(),
},
}));
// Mock next/dynamic to be synchronous in tests
vi.mock("next/dynamic", () => ({
default: vi.fn((loader) => {
return (props: any) => {
const [Component, setComponent] = React.useState<any>(null);
React.useEffect(() => {
loader().then((mod: any) => {
setComponent(
() =>
mod.default ||
mod.PortfolioSection ||
mod.ExpertiseSection ||
mod.TechnicalSpecsSection ||
mod.CTASection ||
mod,
);
});
}, []);
return Component ? <Component {...props} /> : null;
};
}),
}));

View File

@@ -1,126 +0,0 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import Contact from "../app/kontakt/page";
// Mock fetch
const fetchMock = vi.fn();
global.fetch = fetchMock;
// Mock alert
const alertMock = vi.fn();
global.alert = alertMock;
describe("Contact Page", () => {
beforeEach(() => {
fetchMock.mockClear();
alertMock.mockClear();
});
it("renders the contact form correctly", () => {
render(<Contact />);
expect(screen.getByLabelText(/Name \*/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Firma/i)).toBeInTheDocument();
expect(screen.getByLabelText(/E-Mail \*/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Nachricht \*/i)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Nachricht senden/i }),
).toBeInTheDocument();
});
it("submits the form successfully", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
render(<Contact />);
fireEvent.change(screen.getByLabelText(/Name \*/i), {
target: { value: "John Doe" },
});
fireEvent.change(screen.getByLabelText(/Firma/i), {
target: { value: "Acme Corp" },
});
fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
target: { value: "This is a test message that is long enough." },
});
fireEvent.click(screen.getByRole("button", { name: /Nachricht senden/i }));
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
"/api/contact",
expect.objectContaining({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "John Doe",
company: "Acme Corp",
email: "john@example.com",
message: "This is a test message that is long enough.",
website: "",
}),
}),
);
});
expect(screen.getByText(/Nachricht gesendet/i)).toBeInTheDocument();
expect(
screen.getByText(/Vielen Dank für Ihre Anfrage/i),
).toBeInTheDocument();
});
it("handles submission errors", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
json: async () => ({ error: "Server error" }),
});
render(<Contact />);
fireEvent.change(screen.getByLabelText(/Name \*/i), {
target: { value: "John Doe" },
});
fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
target: { value: "This is a test message that is long enough." },
});
fireEvent.click(screen.getByRole("button", { name: /Nachricht senden/i }));
await waitFor(() => {
expect(alertMock).toHaveBeenCalledWith("Fehler: Server error");
});
});
it("handles network errors", async () => {
fetchMock.mockRejectedValueOnce(new Error("Network error"));
render(<Contact />);
fireEvent.change(screen.getByLabelText(/Name \*/i), {
target: { value: "John Doe" },
});
fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
target: { value: "john@example.com" },
});
fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
target: { value: "This is a test message that is long enough." },
});
fireEvent.click(screen.getByRole("button", { name: /Nachricht senden/i }));
await waitFor(() => {
expect(alertMock).toHaveBeenCalledWith(
"Es gab einen Fehler beim Senden Ihrer Nachricht.",
);
});
});
});

View File

@@ -1,27 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Home from "../app/page";
describe("Home Page", () => {
it("renders the hero section with correct title", () => {
render(<Home />);
expect(
screen.getByText(/Spezialisierter Partner für Energiekabelprojekte/i),
).toBeInTheDocument();
});
it("contains the CTA button", () => {
render(<Home />);
const ctaButton = screen.getByRole("link", { name: /Projekt anfragen/i });
expect(ctaButton).toBeInTheDocument();
expect(ctaButton).toHaveAttribute("href", "/kontakt");
});
it("renders the portfolio section", () => {
render(<Home />);
expect(screen.getByText(/Unsere Leistungen/i)).toBeInTheDocument();
// Use getAllByText because it appears in both hero description and card title
const elements = screen.getAllByText(/Technische Beratung/i);
expect(elements.length).toBeGreaterThan(0);
});
});

View File

@@ -1,12 +0,0 @@
import "@testing-library/jest-dom";
import { vi } from "vitest";
// Mock next/navigation
vi.mock("next/navigation", () => ({
usePathname: () => "/",
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
}),
}));

View File

@@ -13,5 +13,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules", "tests", "tests_bak"]
"exclude": ["node_modules", "tests", "tests_bak", "scripts"]
}

Some files were not shown because too many files have changed in this diff Show More