Compare commits
33 Commits
v2.1.0-rc.
...
3b45a967f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b45a967f7 | |||
| cadb104917 | |||
| 0be885428d | |||
| 009f12a3bf | |||
| 8e2a06d6f2 | |||
| 4f2bf3fa51 | |||
| 064ebf45e3 | |||
| e6dfeaffef | |||
| 7cdfe5d7f8 | |||
| 83f4b8eea8 | |||
| 97e76c7cac | |||
| 6caa850045 | |||
| 04ce0ecedd | |||
| 083859d52d | |||
| a13074902b | |||
| 4280f11772 | |||
| 3049c1b6e7 | |||
| 647f9a5f19 | |||
| a2872be02e | |||
| 9c3c7bd34b | |||
| 45602db7ff | |||
| 89405e6e18 | |||
| 57d54231eb | |||
| 5c4225d0a9 | |||
| e1101f2e60 | |||
| 0be6076512 | |||
| 62400943c2 | |||
| 4c60029e21 | |||
| b3c5b911d9 | |||
| 89f00c79a1 | |||
| 98ac3dbd10 | |||
| 0db4c819ff | |||
| 08a3b0be7b |
10
.env
10
.env
@@ -25,3 +25,13 @@ MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
|
|||||||
PAYLOAD_DB_NAME=payload
|
PAYLOAD_DB_NAME=payload
|
||||||
PAYLOAD_DB_USER=payload
|
PAYLOAD_DB_USER=payload
|
||||||
PAYLOAD_DB_PASSWORD=120in09oenaoinsd9iaidon
|
PAYLOAD_DB_PASSWORD=120in09oenaoinsd9iaidon
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Hetzner S3 Object Storage
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
S3_ENDPOINT=https://fsn1.your-objectstorage.com
|
||||||
|
S3_ACCESS_KEY=ROB3MSWMEIGRL7N94ZKS
|
||||||
|
S3_SECRET_KEY=9QJV3NE8xeLxhyufhNU7lsUB0RffJxPhGuEuFSH3
|
||||||
|
S3_BUCKET=mintel
|
||||||
|
S3_REGION=fsn1
|
||||||
|
S3_PREFIX=klz-cables
|
||||||
@@ -27,14 +27,13 @@ jobs:
|
|||||||
|
|
||||||
- name: 🔐 Configure Private Registry
|
- name: 🔐 Configure Private Registry
|
||||||
run: |
|
run: |
|
||||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
echo "//$REGISTRY/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install --no-frozen-lockfile
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: 🧪 QA Checks
|
- name: 🧪 QA Checks
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ jobs:
|
|||||||
TRAEFIK_HOST="${SLUG}.branch.mintel.me"
|
TRAEFIK_HOST="${SLUG}.branch.mintel.me"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Standardize Traefik Rule
|
# Standardize Traefik Rule (escaped backticks for Traefik v3)
|
||||||
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
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?"":" || ")}')
|
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\x60%s\x60)%s", $i, (i==NF?"":" || ")}')
|
||||||
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
||||||
else
|
else
|
||||||
TRAEFIK_RULE="Host(\"$TRAEFIK_HOST\")"
|
TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)'
|
||||||
PRIMARY_HOST="$TRAEFIK_HOST"
|
PRIMARY_HOST="$TRAEFIK_HOST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -172,18 +172,20 @@ jobs:
|
|||||||
|
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: |
|
||||||
|
pnpm store prune
|
||||||
|
pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
- name: 🔒 Security Audit
|
- name: 🔒 Security Audit
|
||||||
run: pnpm audit --audit-level high
|
run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)"
|
||||||
- name: 🧪 QA Checks
|
- name: 🧪 QA Checks
|
||||||
if: github.event.inputs.skip_checks != 'true'
|
if: github.event.inputs.skip_checks != 'true'
|
||||||
env:
|
env:
|
||||||
TURBO_TELEMETRY_DISABLED: "1"
|
TURBO_TELEMETRY_DISABLED: "1"
|
||||||
run: npx turbo run lint check:spell typecheck test --cache-dir=".turbo"
|
run: npx turbo run lint typecheck test --cache-dir=".turbo"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 3: Build & Push
|
# JOB 3: Build & Push
|
||||||
@@ -208,16 +210,16 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
provenance: false
|
provenance: false
|
||||||
platforms: linux/arm64
|
platforms: linux/amd64
|
||||||
build-args: |
|
build-args: |
|
||||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||||
tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }}
|
tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }}
|
||||||
secrets: |
|
secrets: |
|
||||||
"NPM_TOKEN=${{ secrets.REGISTRY_PASS }}"
|
"NPM_TOKEN=${{ secrets.NPM_TOKEN }}"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 4: Deploy
|
# JOB 4: Deploy
|
||||||
@@ -286,7 +288,7 @@ jobs:
|
|||||||
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
|
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
|
||||||
|
|
||||||
# Gatekeeper Origin
|
# Gatekeeper Origin
|
||||||
GATEKEEPER_ORIGIN="https://$GATEKEEPER_HOST"
|
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "# Generated by CI - $TARGET"
|
echo "# Generated by CI - $TARGET"
|
||||||
@@ -383,20 +385,29 @@ jobs:
|
|||||||
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
||||||
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
|
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
|
||||||
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
|
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
|
||||||
ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \"
|
|
||||||
DO \\\$\\\$ BEGIN
|
# Auto-detect migrations from src/migrations/*.ts
|
||||||
DELETE FROM payload_migrations WHERE batch = -1;
|
BATCH=1
|
||||||
INSERT INTO payload_migrations (name, batch)
|
VALUES=""
|
||||||
SELECT name, batch FROM (VALUES
|
for f in $(ls src/migrations/*.ts 2>/dev/null | sort); do
|
||||||
('20260223_195005_products_collection', 1),
|
NAME=$(basename "$f" .ts)
|
||||||
('20260223_195151_remove_sku_unique', 2),
|
[ -n "$VALUES" ] && VALUES="$VALUES,"
|
||||||
('20260225_003500_add_pages_collection', 3)
|
VALUES="$VALUES ('$NAME', $BATCH)"
|
||||||
) AS v(name, batch)
|
((BATCH++))
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name);
|
done
|
||||||
EXCEPTION WHEN undefined_table THEN
|
|
||||||
RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization';
|
if [ -n "$VALUES" ]; then
|
||||||
END \\\$\\\$;
|
ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \"
|
||||||
\"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)"
|
DO \\\$\\\$ BEGIN
|
||||||
|
DELETE FROM payload_migrations WHERE batch = -1;
|
||||||
|
INSERT INTO payload_migrations (name, batch)
|
||||||
|
SELECT name, batch FROM (VALUES $VALUES) AS v(name, batch)
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name);
|
||||||
|
EXCEPTION WHEN undefined_table THEN
|
||||||
|
RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization';
|
||||||
|
END \\\$\\\$;
|
||||||
|
\"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Restart app to pick up clean migration state
|
# Restart app to pick up clean migration state
|
||||||
APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1"
|
APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1"
|
||||||
@@ -431,11 +442,11 @@ jobs:
|
|||||||
node-version: 20
|
node-version: 20
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: deps
|
id: deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --no-frozen-lockfile
|
||||||
- name: 📦 Cache APT Packages
|
- name: 📦 Cache APT Packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -472,6 +483,26 @@ jobs:
|
|||||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
||||||
|
|
||||||
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
||||||
|
- name: 🏥 CMS Deep Health Check
|
||||||
|
env:
|
||||||
|
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
|
GK_PASS: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
run: |
|
||||||
|
echo "Waiting 10s for app to fully start..."
|
||||||
|
sleep 10
|
||||||
|
echo "Checking basic health..."
|
||||||
|
curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; }
|
||||||
|
echo "✅ Basic health OK"
|
||||||
|
echo "Checking CMS DB connectivity..."
|
||||||
|
RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || {
|
||||||
|
echo "❌ CMS health check failed!"
|
||||||
|
echo "$RESPONSE"
|
||||||
|
echo ""
|
||||||
|
echo "This usually means Payload CMS migrations failed or DB tables are missing."
|
||||||
|
echo "Check: docker logs \$APP_CONTAINER | grep -i error"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "✅ CMS health: $RESPONSE"
|
||||||
- name: 🚀 OG Image Check
|
- name: 🚀 OG Image Check
|
||||||
if: always() && steps.deps.outcome == 'success'
|
if: always() && steps.deps.outcome == 'success'
|
||||||
env:
|
env:
|
||||||
|
|||||||
17
Dockerfile
17
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM registry.infra.mintel.me/mintel/nextjs:v1.7.10 AS base
|
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Arguments for build-time configuration
|
# Arguments for build-time configuration
|
||||||
@@ -25,9 +25,9 @@ COPY pnpm-lock.yaml package.json .npmrc* ./
|
|||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||||
--mount=type=secret,id=NPM_TOKEN \
|
--mount=type=secret,id=NPM_TOKEN \
|
||||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
|
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
|
||||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc && \
|
||||||
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||||
pnpm install --frozen-lockfile && \
|
pnpm install --no-frozen-lockfile && \
|
||||||
rm .npmrc
|
rm .npmrc
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
@@ -43,10 +43,15 @@ CMD ["pnpm", "dev:local"]
|
|||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
# Limit memory to 1GB to prevent ResourceExhausted in combination with worker limits
|
# Limit memory to 1GB to prevent ResourceExhausted in combination with worker limits
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||||
|
|
||||||
|
# Force Turbopack (Rust/Rayon) and Node.js to use strictly 3 threads to avoid starving the Gitea Runner VPS CPU
|
||||||
|
ENV RAYON_NUM_THREADS=3
|
||||||
|
ENV UV_THREADPOOL_SIZE=3
|
||||||
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Stage 3: Runner
|
# Stage 2: Runner
|
||||||
FROM registry.infra.mintel.me/mintel/runtime:v1.7.10 AS runner
|
FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
||||||
|
|||||||
@@ -462,3 +462,4 @@ Proprietary - KLZ Cables
|
|||||||
**Status**: ✅ **READY FOR DEPLOYMENT**
|
**Status**: ✅ **READY FOR DEPLOYMENT**
|
||||||
**Version**: 1.0.0
|
**Version**: 1.0.0
|
||||||
**Last Updated**: December 27, 2025
|
**Last Updated**: December 27, 2025
|
||||||
|
Trigger rebuilding for x86 architecture.
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '@payload-config';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep CMS Health Check
|
||||||
|
* Validates that Payload CMS can actually query the database.
|
||||||
|
* Used by post-deploy smoke tests to catch migration/schema issues.
|
||||||
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
// Payload is embedded within the Next.js app, so if this route responds, the CMS is up.
|
const checks: Record<string, string> = {};
|
||||||
// Further DB health checks can be implemented via Payload Local API later.
|
|
||||||
return NextResponse.json({ status: 'ok', message: 'Payload CMS is embedded.' }, { status: 200 });
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
checks.init = 'ok';
|
||||||
|
|
||||||
|
// Verify each collection can be queried (catches missing locale tables, broken migrations)
|
||||||
|
const collections = ['posts', 'products', 'pages', 'media'] as const;
|
||||||
|
for (const collection of collections) {
|
||||||
|
try {
|
||||||
|
await payload.find({ collection, limit: 1, locale: 'en' });
|
||||||
|
checks[collection] = 'ok';
|
||||||
|
} catch (e: any) {
|
||||||
|
checks[collection] = `error: ${e.message?.substring(0, 100)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasErrors = Object.values(checks).some(v => v.startsWith('error'));
|
||||||
|
return NextResponse.json(
|
||||||
|
{ status: hasErrors ? 'degraded' : 'ok', checks },
|
||||||
|
{ status: hasErrors ? 503 : 200 },
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ status: 'error', message: e.message?.substring(0, 200), checks },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,26 @@ export default function Hero({ data }: { data?: any }) {
|
|||||||
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
||||||
>
|
>
|
||||||
{data?.title ? (
|
{data?.title ? (
|
||||||
<span dangerouslySetInnerHTML={{ __html: data.title.replace(/<green>/g, '<span class="relative inline-block"><span class="relative z-10 text-accent italic inline-block">').replace(/<\/green>/g, '</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>') }} />
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data.title
|
||||||
|
.replace(
|
||||||
|
/<green>/g,
|
||||||
|
'<span class="relative inline-block"><span class="relative z-10 text-accent italic inline-block">',
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<\/green>/g,
|
||||||
|
'</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>',
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
t.rich('title', {
|
t.rich('title', {
|
||||||
green: (chunks) => (
|
green: (chunks) => (
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
<span className="relative z-10 text-accent italic inline-block">
|
||||||
|
{chunks}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
||||||
style={{ animationDelay: '500ms' }}
|
style={{ animationDelay: '500ms' }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
||||||
|
|
||||||
# Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
|
# Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/(health|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
||||||
@@ -46,9 +46,21 @@ services:
|
|||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
|
|
||||||
|
# Login redirect – the app's middleware sends users to /login but login lives at /gatekeeper/login
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.regex=^https?://[^/]+/([a-z]{2}/)?login(.*)"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.replacement=https://${TRAEFIK_HOST:-klz-cables.com}/gatekeeper/login$${2}"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.permanent=false"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?login`)"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls=${TRAEFIK_TLS:-false}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.middlewares=${PROJECT_NAME:-klz}-loginredirect"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.service=${PROJECT_NAME:-klz}-app-svc"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.priority=2002"
|
||||||
|
|
||||||
klz-gatekeeper:
|
klz-gatekeeper:
|
||||||
profiles: [ "gatekeeper" ]
|
profiles: [ "gatekeeper" ]
|
||||||
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
image: registry.infra.mintel.me/mintel/gatekeeper:testing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
infra:
|
infra:
|
||||||
@@ -61,13 +73,13 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-gatekeeper-svc.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-klz}-gatekeeper-svc.loadbalancer.server.port=3000"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.address=http://${PROJECT_NAME:-klz}-gatekeeper:3000/api/verify"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.address=http://${PROJECT_NAME:-klz}-gatekeeper:3000/gatekeeper/api/verify"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.trustForwardHeader=true"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.trustForwardHeader=true"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
# Gatekeeper Public Router (Login/Auth UI)
|
# Gatekeeper Public Router (Login/Auth UI) — basePath mode on main domain
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=Host(`${GATEKEEPER_HOST:-gatekeeper.klz-cables.com}`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathPrefix(`/gatekeeper`)"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
||||||
|
|||||||
10
lib/blog.ts
10
lib/blog.ts
@@ -59,15 +59,14 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
slug: { equals: slug },
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
draft: isDev,
|
draft: config.showDrafts,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,15 +106,14 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
sort: '-date',
|
sort: '-date',
|
||||||
draft: isDev,
|
draft: config.showDrafts,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function createConfig() {
|
|||||||
isStaging: target === 'staging',
|
isStaging: target === 'staging',
|
||||||
isTesting: target === 'testing',
|
isTesting: target === 'testing',
|
||||||
isDevelopment: target === 'development',
|
isDevelopment: target === 'development',
|
||||||
|
showDrafts: target === 'development' || target === 'testing' || target === 'staging',
|
||||||
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
||||||
gatekeeperUrl: env.GATEKEEPER_URL,
|
gatekeeperUrl: env.GATEKEEPER_URL,
|
||||||
|
|
||||||
@@ -116,6 +117,9 @@ export const config = {
|
|||||||
get isDevelopment() {
|
get isDevelopment() {
|
||||||
return getConfig().isDevelopment;
|
return getConfig().isDevelopment;
|
||||||
},
|
},
|
||||||
|
get showDrafts() {
|
||||||
|
return getConfig().showDrafts;
|
||||||
|
},
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return getConfig().baseUrl;
|
return getConfig().baseUrl;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getPayload } from 'payload';
|
import { getPayload } from 'payload';
|
||||||
import configPromise from '@payload-config';
|
import configPromise from '@payload-config';
|
||||||
import { mapSlugToFileSlug } from './slugs';
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export interface ProductFrontmatter {
|
export interface ProductFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -26,13 +27,12 @@ export async function getProductMetadata(
|
|||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{ slug: { equals: fileSlug } },
|
{ slug: { equals: fileSlug } },
|
||||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
@@ -70,13 +70,12 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{ slug: { equals: fileSlug } },
|
{ slug: { equals: fileSlug } },
|
||||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
@@ -127,11 +126,10 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
@@ -157,11 +155,10 @@ export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
|||||||
images: true,
|
images: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' || process.env.TARGET === 'staging';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ export default async function middleware(request: NextRequest) {
|
|||||||
if (internalHosts.includes(urlObj.hostname)) {
|
if (internalHosts.includes(urlObj.hostname)) {
|
||||||
const proto = headers.get('x-forwarded-proto') || 'https';
|
const proto = headers.get('x-forwarded-proto') || 'https';
|
||||||
// Prioritize x-forwarded-host (passed by Traefik) over the local Host header
|
// Prioritize x-forwarded-host (passed by Traefik) over the local Host header
|
||||||
const hostHeader =
|
const fallbackHost = process.env.NEXT_PUBLIC_BASE_URL
|
||||||
headers.get('x-forwarded-host') || headers.get('host') || 'testing.klz-cables.com';
|
? new URL(process.env.NEXT_PUBLIC_BASE_URL).host
|
||||||
|
: 'klz-cables.com';
|
||||||
|
const hostHeader = headers.get('x-forwarded-host') || headers.get('host') || fallbackHost;
|
||||||
|
|
||||||
urlObj.protocol = proto;
|
urlObj.protocol = proto;
|
||||||
|
|
||||||
@@ -13,9 +13,8 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
||||||
cpus: 1,
|
cpus: 3,
|
||||||
workerThreads: false,
|
workerThreads: false,
|
||||||
memoryBasedWorkersCount: true,
|
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -4,10 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mintel/mail": "1.8.3",
|
"@mintel/mail": "^1.8.21",
|
||||||
"@mintel/next-config": "1.8.3",
|
"@mintel/next-config": "^1.8.21",
|
||||||
"@mintel/next-feedback": "1.8.10",
|
"@mintel/next-feedback": "^1.8.21",
|
||||||
"@mintel/next-utils": "^1.7.15",
|
"@mintel/next-utils": "^1.8.21",
|
||||||
"@payloadcms/db-postgres": "^3.77.0",
|
"@payloadcms/db-postgres": "^3.77.0",
|
||||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||||
"@payloadcms/next": "^3.77.0",
|
"@payloadcms/next": "^3.77.0",
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"@payloadcms/ui": "^3.77.0",
|
"@payloadcms/ui": "^3.77.0",
|
||||||
"@react-email/components": "^1.0.7",
|
"@react-email/components": "^1.0.7",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.5.0",
|
||||||
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@sentry/nextjs": "^10.39.0",
|
"@sentry/nextjs": "^10.39.0",
|
||||||
"@types/recharts": "^2.0.1",
|
"@types/recharts": "^2.0.1",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
@@ -45,6 +48,7 @@
|
|||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"svg-to-pdfkit": "^0.1.8",
|
"svg-to-pdfkit": "^0.1.8",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"three": "^0.183.1",
|
||||||
"xlsx": "npm:@e965/xlsx@^0.20.3",
|
"xlsx": "npm:@e965/xlsx@^0.20.3",
|
||||||
"zod": "3.25.76"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
@@ -53,8 +57,8 @@
|
|||||||
"@commitlint/config-conventional": "^20.4.0",
|
"@commitlint/config-conventional": "^20.4.0",
|
||||||
"@cspell/dict-de-de": "^4.1.2",
|
"@cspell/dict-de-de": "^4.1.2",
|
||||||
"@lhci/cli": "^0.15.1",
|
"@lhci/cli": "^0.15.1",
|
||||||
"@mintel/eslint-config": "1.8.3",
|
"@mintel/eslint-config": "^1.8.21",
|
||||||
"@mintel/tsconfig": "1.8.3",
|
"@mintel/tsconfig": "^1.8.21",
|
||||||
"@next/bundle-analyzer": "^16.1.6",
|
"@next/bundle-analyzer": "^16.1.6",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"@vitest/ui": "^4.0.16",
|
"@vitest/ui": "^4.0.16",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
titleSuffix: ' – KLZ Cables',
|
titleSuffix: ' – KLZ Cables',
|
||||||
icons: [
|
icons: [{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' }],
|
||||||
{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
localization: {
|
localization: {
|
||||||
@@ -80,18 +78,21 @@ export default buildConfig({
|
|||||||
`postgresql://${process.env.PAYLOAD_DB_USER || 'payload'}:${process.env.PAYLOAD_DB_PASSWORD || '120in09oenaoinsd9iaidon'}@127.0.0.1:54322/${process.env.PAYLOAD_DB_NAME || 'payload'}`,
|
`postgresql://${process.env.PAYLOAD_DB_USER || 'payload'}:${process.env.PAYLOAD_DB_PASSWORD || '120in09oenaoinsd9iaidon'}@127.0.0.1:54322/${process.env.PAYLOAD_DB_NAME || 'payload'}`,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
email: nodemailerAdapter({
|
email: process.env.MAIL_HOST
|
||||||
defaultFromAddress: process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
|
? nodemailerAdapter({
|
||||||
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'KLZ Cables',
|
defaultFromAddress:
|
||||||
transportOptions: {
|
process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
|
||||||
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
|
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'KLZ Cables',
|
||||||
port: Number(process.env.MAIL_PORT) || 587,
|
transportOptions: {
|
||||||
auth: {
|
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
|
||||||
user: process.env.MAIL_USERNAME,
|
port: Number(process.env.MAIL_PORT) || 587,
|
||||||
pass: process.env.MAIL_PASSWORD,
|
auth: {
|
||||||
},
|
user: process.env.MAIL_USERNAME,
|
||||||
},
|
pass: process.env.MAIL_PASSWORD,
|
||||||
}),
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
sharp,
|
sharp,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
});
|
});
|
||||||
|
|||||||
589
pnpm-lock.yaml
generated
589
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -53,12 +53,17 @@ TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
|||||||
REMOTE_DB_USER=""
|
REMOTE_DB_USER=""
|
||||||
REMOTE_DB_NAME=""
|
REMOTE_DB_NAME=""
|
||||||
|
|
||||||
# Migration names to insert after restore (keeps Payload from prompting)
|
# Auto-detect migrations from src/migrations/*.ts (no manual maintenance needed)
|
||||||
MIGRATIONS=(
|
MIGRATIONS=()
|
||||||
"20260223_195005_products_collection:1"
|
BATCH=1
|
||||||
"20260223_195151_remove_sku_unique:2"
|
for migration_file in $(ls src/migrations/*.ts 2>/dev/null | sort); do
|
||||||
"20260225_003500_add_pages_collection:3"
|
name=$(basename "$migration_file" .ts)
|
||||||
)
|
MIGRATIONS+=("$name:$BATCH")
|
||||||
|
((BATCH++))
|
||||||
|
done
|
||||||
|
if [ ${#MIGRATIONS[@]} -eq 0 ]; then
|
||||||
|
echo "⚠️ No migration files found in src/migrations/"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Resolve target environment ─────────────────────────────────────────────
|
# ── Resolve target environment ─────────────────────────────────────────────
|
||||||
resolve_target() {
|
resolve_target() {
|
||||||
@@ -158,6 +163,29 @@ backup_remote_db() {
|
|||||||
REMOTE_BACKUP_FILE="$file"
|
REMOTE_BACKUP_FILE="$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Pre-flight: Verify remote containers exist ─────────────────────────────
|
||||||
|
check_remote_containers() {
|
||||||
|
echo "🔍 Checking $TARGET containers..."
|
||||||
|
local missing=0
|
||||||
|
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_DB_CONTAINER" | grep -q .; then
|
||||||
|
echo "❌ Database container '$REMOTE_DB_CONTAINER' not found on $SSH_HOST"
|
||||||
|
echo " → Deploy $TARGET first: git push to trigger pipeline, or run:"
|
||||||
|
echo " ssh $SSH_HOST \"cd $REMOTE_SITE_DIR && docker compose -p $REMOTE_PROJECT --env-file .env.\$TARGET up -d\""
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_APP_CONTAINER" | grep -q .; then
|
||||||
|
echo "❌ App container '$REMOTE_APP_CONTAINER' not found on $SSH_HOST"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
if [ $missing -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "💡 The $TARGET environment hasn't been deployed yet."
|
||||||
|
echo " Push to the '$TARGET' branch or run the pipeline first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All $TARGET containers running."
|
||||||
|
}
|
||||||
|
|
||||||
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
||||||
do_push() {
|
do_push() {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -171,8 +199,9 @@ do_push() {
|
|||||||
echo ""
|
echo ""
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||||
|
|
||||||
# 0. Ensure local DB is running
|
# 0. Ensure local DB is running & remote containers exist
|
||||||
ensure_local_db
|
ensure_local_db
|
||||||
|
check_remote_containers
|
||||||
|
|
||||||
# 1. Safety backup of remote
|
# 1. Safety backup of remote
|
||||||
backup_remote_db
|
backup_remote_db
|
||||||
@@ -226,8 +255,9 @@ do_pull() {
|
|||||||
echo ""
|
echo ""
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||||
|
|
||||||
# 0. Ensure local DB is running
|
# 0. Ensure local DB is running & remote containers exist
|
||||||
ensure_local_db
|
ensure_local_db
|
||||||
|
check_remote_containers
|
||||||
|
|
||||||
# 1. Safety backup of local
|
# 1. Safety backup of local
|
||||||
backup_local_db
|
backup_local_db
|
||||||
|
|||||||
Reference in New Issue
Block a user