Compare commits

...

6 Commits

Author SHA1 Message Date
1f0de18755 fix(form): resolve silent failures and cleanup deployment pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Failing after 22m37s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-11 23:21:13 +02:00
eeb0920afd chore: stabilize deployment and resolve asset 404s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Failing after 12m38s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-11 21:55:24 +02:00
f8eec731c0 fix: final YAML syntax repair for docker-compose.yml (v1.15.14)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Failing after 20m27s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-04-11 19:33:25 +02:00
8b80af3d1a fix: repair YAML indentation in docker-compose.yml (v1.15.13)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Failing after 21m34s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-11 19:32:19 +02:00
de42890476 fix: override CMD to apps/web/server.js (v1.15.12)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Successful in 14m35s
Build & Deploy / 🚀 Deploy (push) Failing after 12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-11 19:12:51 +02:00
fbd33da925 fix: stabilize build-time DB connection (v1.15.11)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🏗️ Build (push) Successful in 13m25s
Build & Deploy / 🚀 Deploy (push) Failing after 13s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-11 18:06:45 +02:00
6 changed files with 113 additions and 13 deletions

View File

@@ -154,6 +154,11 @@ jobs:
run: |
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: 🧹 Clean runner
run: |
docker system prune -af --volumes
continue-on-error: true
- name: 🏗️ Build and Push
uses: docker/build-push-action@v5
with:
@@ -172,7 +177,7 @@ jobs:
S3_BUCKET=${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
S3_REGION=${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
S3_PREFIX=${{ secrets.S3_PREFIX || vars.S3_PREFIX || 'mintel.me' }}
DATABASE_URI=${{ secrets.DATABASE_URI || (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD && format('postgres://directus:{0}@postgres-db:5432/directus', secrets.TESTING_DIRECTUS_DB_PASSWORD)) || 'postgres://payload:payload@postgres-db:5432/payload' }}
DATABASE_URI=${{ secrets.DATABASE_URI || (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD && format('postgres://directus:{0}@postgres-db:5432/directus', secrets.TESTING_DIRECTUS_DB_PASSWORD)) || 'postgres://payload:payload@127.0.0.1:5432/payload' }}
PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET || 'secret' }}
BUILD_ID=${{ github.sha }}
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
@@ -418,14 +423,60 @@ jobs:
if: always()
run: docker builder prune -f --filter "until=1h"
# (JOB 5: Post-Deploy Verification was removed to reduce pipeline noise)
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Post-Deploy Verification
# ──────────────────────────────────────────────────────────────────────────────
post_deploy_checks:
name: 🩺 Smoke Test
needs: [prepare, deploy]
runs-on: docker
if: needs.deploy.result == 'success'
container:
image: alpine:latest
steps:
- name: 🌐 Check Production URL
shell: sh
run: |
# Wait longer (up to 2 minutes) for Next.js to fully prime
COUNT=0
MAX=24
URL="${{ needs.prepare.outputs.next_public_url }}"
echo "Verifying $URL is responsive..."
while [ $COUNT -lt $MAX ]; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/" || echo "000")
if [ "$STATUS" = "200" ]; then
echo "✅ Site is UP (200 OK)"
break
fi
echo "⏳ Wait for 200 OK (Status: $STATUS)..."
sleep 5
COUNT=$((COUNT + 1))
done
if [ "$STATUS" != "200" ]; then
echo "❌ Site failed smoke test after 2 minutes! (Status: $STATUS)"
exit 1
fi
- name: 🌐 Check Case Study Assets
shell: sh
run: |
URL="${{ needs.prepare.outputs.next_public_url }}/case-studies/klz-cables"
echo "Verifying case study $URL..."
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
if [ "$STATUS" != "200" ]; then
echo "❌ Case study page failed! (Status: $STATUS)"
exit 1
fi
echo "✅ Case study page is UP"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 6: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notify
needs: [prepare, deploy]
needs: [prepare, deploy, post_deploy_checks]
if: always()
runs-on: docker
container:

View File

@@ -41,6 +41,18 @@ const nextConfig = {
return [
// Umami proxy rewrite handled in app/stats/api/send/route.ts
// Sentry relay handled in app/errors/api/relay/route.ts
{
source: '/assets/:path*',
destination: '/showcase/klz-cables.com/assets/:path*',
},
{
source: '/wp-content/:path*',
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*',
},
{
source: '/wp-includes/:path*',
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*',
},
];
},
async redirects() {

View File

@@ -246,6 +246,7 @@ export function ContactForm({
onBack={() => setFlow("discovery")} // Can keep, but won't be seen if we hide button
onSubmit={() => handleSubmit()}
isSubmitting={isSubmitting}
error={error}
/>
);
}

View File

@@ -35,6 +35,7 @@ interface DirectMessageFlowProps {
onBack: () => void;
onSubmit: () => void;
isSubmitting: boolean;
error?: string | null;
}
export const DirectMessageFlow = ({
@@ -57,10 +58,32 @@ export const DirectMessageFlow = ({
onBack,
onSubmit,
isSubmitting,
error,
}: DirectMessageFlowProps) => {
return (
<div className="w-full max-w-3xl mx-auto px-4 py-12">
<div className="space-y-12">
{error && (
<Reveal width="100%" delay={0.1}>
<div className="bg-red-50 border border-red-100 rounded-2xl p-6 flex items-start gap-4 mb-8">
<div className="p-2 bg-red-100 rounded-lg text-red-600">
<Mail size={20} className="animate-pulse" />
</div>
<div className="space-y-1">
<h3 className="text-red-900 font-bold text-sm uppercase tracking-wider">
Übertragungsfehler
</h3>
<p className="text-red-700/80 font-medium text-base">
{error}
</p>
<div className="mt-2 text-[10px] font-mono text-red-400 uppercase tracking-widest">
Status: FEHLER_BEI_SEQUENZ_INIT
</div>
</div>
</div>
</Reveal>
)}
<Reveal width="100%" delay={0.2}>
<div className="space-y-4">
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">

View File

@@ -45,11 +45,23 @@ export async function sendEmail({
subject,
html,
}: SendEmailOptions) {
const recipients = to || env.MAIL_RECIPIENTS;
let recipients = to || env.MAIL_RECIPIENTS;
let from = env.MAIL_FROM;
if (!from) {
from = "info@mintel.me";
console.warn("MAIL_FROM is empty. Using fallback: info@mintel.me");
}
if (!recipients) {
recipients = "marc@mintel.me";
console.warn("MAIL_RECIPIENTS is empty. Using fallback: marc@mintel.me");
}
const transporter = getTransporter();
const mailOptions = {
from: env.MAIL_FROM,
from,
to: recipients,
replyTo,
subject,

View File

@@ -2,6 +2,7 @@ services:
mintel-me-app:
image: registry.infra.mintel.me/mintel/mintel.me:${IMAGE_TAG:-latest}
restart: always
command: node apps/web/server.js
networks:
- default
- infra
@@ -9,16 +10,9 @@ services:
- ${ENV_FILE:-.env}
labels:
- "traefik.enable=true"
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
# HTTP ⇒ HTTPS redirect
- 'traefik.http.routers.${PROJECT_NAME}-web.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
- "traefik.http.routers.${PROJECT_NAME}-web.entrypoints=web"
# - "traefik.http.routers.${PROJECT_NAME}-web.middlewares=redirect-https"
# HTTPS router (Standard)
- 'traefik.http.routers.${PROJECT_NAME}.rule=${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}'
- "traefik.http.routers.${PROJECT_NAME}.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
@@ -32,7 +26,7 @@ services:
- "caddy.reverse_proxy={{upstreams 3000}}"
# Public Router (Whitelist for OG Images, Sitemaps, Health)
- 'traefik.http.routers.${PROJECT_NAME}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/api/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
- 'traefik.http.routers.${PROJECT_NAME}-public.rule=(${TRAEFIK_HOST_RULE:-Host("${TRAEFIK_HOST:-mintel.localhost}")}) && (PathPrefix("/health") || PathPrefix("/api/health") || PathPrefix("/sitemap.xml") || PathPrefix("/robots.txt") || PathPrefix("/manifest.webmanifest") || PathPrefix("/api/og") || PathPrefix("/assets") || PathPrefix("/wp-content") || PathPrefix("/wp-includes") || PathPrefix("/showcase") || PathRegexp(".*opengraph-image.*") || PathRegexp(".*sitemap.*"))'
- "traefik.http.routers.${PROJECT_NAME}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
- "traefik.http.routers.${PROJECT_NAME}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
- "traefik.http.routers.${PROJECT_NAME}-public.tls=${TRAEFIK_TLS:-false}"
@@ -42,6 +36,7 @@ services:
# Middlewares
- "traefik.http.middlewares.${PROJECT_NAME}-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.${PROJECT_NAME}-ratelimit.ratelimit.burst=200"
- "traefik.http.middlewares.${PROJECT_NAME}-ratelimit.ratelimit.burst=50"
# Gatekeeper Router (Path-based)
@@ -59,6 +54,12 @@ services:
# Forwarded Headers
- "traefik.http.middlewares.${PROJECT_NAME}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.${PROJECT_NAME}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
gatekeeper:
profiles: ["gatekeeper"]