Compare commits
137 Commits
4a5017124a
...
v1.9.4
| Author | SHA1 | Date | |
|---|---|---|---|
| ee9cde1ed0 | |||
| 33cf701034 | |||
| 1fae5edee3 | |||
| 0e143bf9c1 | |||
| d86e26bc33 | |||
| a1c0736274 | |||
| 7b642426fb | |||
| 6a228248e0 | |||
| bd1a822d32 | |||
| 81af49f880 | |||
| 1defb5758f | |||
| b4dd073711 | |||
| 59ea4bfd02 | |||
| 4a20e1f51f | |||
| 9aa3ee42e4 | |||
| 0ac022df57 | |||
| e71965267d | |||
| 8d12f92da8 | |||
| 4303124ec5 | |||
| badf81644e | |||
| cdd38b3654 | |||
| 1a195a388a | |||
| b4fbf3bf2a | |||
| 8569105529 | |||
| 316afe004f | |||
| b20a999da8 | |||
| 237d68bc5a | |||
| 0fdc20cabb | |||
| 2aa617ce3b | |||
| 54cd94831d | |||
| c8df20bbee | |||
| 07755c9674 | |||
| ff7ba14a4a | |||
| ebe42adb6f | |||
| a45d0110d3 | |||
| 9abd4f4fe7 | |||
| 3a4fd1d06d | |||
| c0b9c55ecf | |||
| 7e320c08d9 | |||
| c5746978aa | |||
| cd88c2f20f | |||
| 1c87d5341e | |||
| 6a14c9924f | |||
| ee50808596 | |||
| e9fbe45feb | |||
| b27566a336 | |||
| 71ef49e73d | |||
| a98572e183 | |||
| eacb14ff7d | |||
| 41a090db58 | |||
| 2bdb6bbb98 | |||
| 99ee47507b | |||
| 2d96000385 | |||
| 39ea0a35dd | |||
| 1c24822787 | |||
| d21c12c2b4 | |||
| cdf2bb5fdc | |||
| c4aaea30c1 | |||
| cbb3cf0be3 | |||
| bc3a75a915 | |||
| 1455845d44 | |||
| db31f06bc0 | |||
| 546b8ee72b | |||
| 6174b44570 | |||
| 89d258e63d | |||
| 13a484ce59 | |||
| d82c836fcb | |||
| b2f6627ec5 | |||
| 2ab5a8a41f | |||
| e43c980a5d | |||
| 88b4626d6e | |||
| 90856da773 | |||
| 964cd79ca8 | |||
| 9c5e2c6099 | |||
| 984a641b90 | |||
| c8ff76f299 | |||
| 1fffdf00ee | |||
| 70de139cb0 | |||
| b015c62650 | |||
| b7dac5d463 | |||
| 10bdfdfe97 | |||
| 9ad63a0a82 | |||
| eb117cc0b8 | |||
| 23ee915194 | |||
| 3dff891023 | |||
| f55c27c43d | |||
| 3e04427646 | |||
| 6b51d63c8b | |||
| 60ca4ad656 | |||
| aae5275990 | |||
| b639fffe7f | |||
| ab15f7f35b | |||
| 025906889c | |||
| 760a6d6db3 | |||
| 7f8cea4728 | |||
| fb09b1de9a | |||
| cb4afe2e91 | |||
| 1f68234a49 | |||
| e2d68c2828 | |||
| cb6f133e0c | |||
| 7990189505 | |||
| 2167044543 | |||
| 0665e3e224 | |||
| 2bdcbfb907 | |||
| ac1e0081f7 | |||
| 4f452cf2a9 | |||
| 1404aa0406 | |||
| 9e10ce06ed | |||
| a400e6f94d | |||
| 2f95c8d968 | |||
| 9aa6f5f4d0 | |||
| 071302fe6b | |||
| cf3a96cead | |||
| af5f91e6f8 | |||
| 5e453418d6 | |||
| 10980ba8b3 | |||
| 6444aea5f6 | |||
| ad50929bf3 | |||
| 07928a182f | |||
| b493ce0ba0 | |||
| db445d0b76 | |||
| 22a6a06a4e | |||
| 4f66dd914c | |||
| bb54750085 | |||
| 5cbbd81384 | |||
| c167e36626 | |||
| 0fb872161d | |||
| a360ea6a98 | |||
| a537294832 | |||
| 459bdc6eda | |||
| 905ce98bc4 | |||
| ce63a1ac69 | |||
| 6444cf1e81 | |||
| 4b5609a75e | |||
| 8907963d57 | |||
| 844a5f5412 | |||
| 96de68a063 |
@@ -1,33 +0,0 @@
|
||||
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: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
pnpm lint &
|
||||
pnpm build &
|
||||
wait
|
||||
@@ -1,9 +1,10 @@
|
||||
# Heartbeat to trigger fresh CI run after stall
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**"
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
@@ -13,6 +14,9 @@ on:
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }}
|
||||
cancel-in-progress: true
|
||||
@@ -76,7 +80,11 @@ jobs:
|
||||
TRAEFIK_HOST="staging.${DOMAIN}"
|
||||
fi
|
||||
else
|
||||
TARGET="skip"
|
||||
TARGET="branch"
|
||||
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
||||
ENV_FILE=".env.branch-${SLUG}"
|
||||
TRAEFIK_HOST="${SLUG}.branch.${DOMAIN}"
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" != "skip" ]]; then
|
||||
@@ -97,37 +105,25 @@ jobs:
|
||||
echo "traefik_rule=$TRAEFIK_RULE"
|
||||
echo "next_public_url=https://$PRIMARY_HOST"
|
||||
echo "directus_url=https://cms.$PRIMARY_HOST"
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
if [[ "$TARGET" == "branch" ]]; then
|
||||
echo "project_name=$PRJ-branch-$SLUG"
|
||||
else
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
fi
|
||||
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 | head -1 | cut -d'"' -f4 | sed 's/\^//; s/\~//')
|
||||
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||
|
||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
|
||||
# Fetch script from monorepo (main)
|
||||
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=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "target=skip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Build Test)
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
needs: prepare
|
||||
needs: [prepare, deploy]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -143,28 +139,137 @@ jobs:
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
# This handles root package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
|
||||
# Handle apps/web/package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- 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
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
|
||||
echo "Testing API with token..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $T" https://git.infra.mintel.me/api/v1/user || echo "failed")
|
||||
echo "API returned: $HTTP_CODE"
|
||||
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VALID_TOKEN" ]; then
|
||||
echo "❌ All token/user combinations failed to authenticate!"
|
||||
T=$(echo "$TOKENS" | awk '{print $1}')
|
||||
echo "Attempting open diagnostic login with first token and user mmintel..."
|
||||
echo "$T" | docker login git.infra.mintel.me -u "mmintel" --password-stdin || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "::add-mask::$TOKEN"
|
||||
echo "token=$TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "user=$VALID_USER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
|
||||
# Also export for pnpm to pick it up from env if needed
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: 🏗️ Compile Sibling Monorepo
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm --filter "@mintel/web" exec tsc --noEmit
|
||||
pnpm --filter "@mintel/web" test
|
||||
- name: 🏗️ Build Test
|
||||
mkdir -p ci-logs
|
||||
echo "=== Compile Sibling Monorepo ===" >> ci-logs/summary.txt
|
||||
cp .npmrc _at-mintel/
|
||||
cd _at-mintel
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
pnpm --filter "...@mintel/payload-ai" \
|
||||
--filter @mintel/pdf... \
|
||||
--filter @mintel/concept-engine... \
|
||||
--filter @mintel/estimation-engine... \
|
||||
--filter @mintel/meme-generator... \
|
||||
build --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
- name: Install dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Install dependencies (Root) ===" >> ci-logs/summary.txt
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: 🧪 Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
run: pnpm build
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Test (@mintel/web) ===" >> ci-logs/summary.txt
|
||||
pnpm --filter @mintel/web test --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: Inspect on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "==== runner state ===="
|
||||
ls -la
|
||||
echo "==== _at-mintel state ===="
|
||||
ls -la _at-mintel || true
|
||||
echo "==== .npmrc check ===="
|
||||
cat .npmrc | sed -E 's/authToken=[a-f0-9]{5}.*/authToken=REDACTED/'
|
||||
echo "==== pnpm debug logs ===="
|
||||
[ -f pnpm-debug.log ] && tail -n 100 pnpm-debug.log || echo "No root pnpm-debug.log"
|
||||
[ -f _at-mintel/pnpm-debug.log ] && tail -n 100 _at-mintel/pnpm-debug.log || echo "No sibling pnpm-debug.log"
|
||||
- name: Extract QA Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "QA Failure Report" > ci-logs/summary.txt
|
||||
ls -R >> ci-logs/summary.txt
|
||||
[ -f pnpm-debug.log ] && cp pnpm-debug.log ci-logs/ || true
|
||||
[ -f _at-mintel/pnpm-debug.log ] && cp _at-mintel/pnpm-debug.log ci-logs/at-mintel-pnpm-debug.log || true
|
||||
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" root@alpha.mintel.me "mkdir -p ~/logs"
|
||||
scp -r -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" ci-logs/* root@alpha.mintel.me:~/logs/ || true
|
||||
rm "$SSH_KEY_FILE"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: 🏗️ Build
|
||||
needs: prepare
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -172,33 +277,71 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Provide sibling monorepo (context)
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
- name: 🧹 Free Disk Space
|
||||
run: |
|
||||
docker builder prune -af || true
|
||||
docker image prune -af || true
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: 🏗️ Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/arm64
|
||||
provenance: false
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache,mode=max
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||
secrets: |
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
- name: 🚨 Extract Build Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
set +e
|
||||
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 "Re-running docker build with plain progress to capture exact logs..."
|
||||
echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.user }}" --password-stdin > login.log 2>&1
|
||||
echo "${{ steps.discover_token.outputs.token }}" > /tmp/npm_token.txt
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg NPM_TOKEN=${{ steps.discover_token.outputs.token }} \
|
||||
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
||||
--progress plain \
|
||||
-t temp-image . > docker_build_failed.log 2>&1
|
||||
cat login.log >> docker_build_failed.log
|
||||
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
||||
# JOB 4: Deploy
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: 🚀 Deploy
|
||||
needs: [prepare, build, qa]
|
||||
needs: [prepare, build]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -214,10 +357,10 @@ jobs:
|
||||
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||
DATABASE_URI: postgres://${{ env.postgres_DB_USER }}:${{ env.postgres_DB_PASSWORD }}@postgres-db:5432/${{ env.postgres_DB_NAME }}
|
||||
DATABASE_URI: postgres://${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}:${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}@postgres-db:5432/${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
# 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 }}
|
||||
@@ -235,6 +378,14 @@ jobs:
|
||||
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 || '#ff00ff' }}
|
||||
|
||||
# S3 Object Storage
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
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 || github.event.repository.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -246,7 +397,6 @@ jobs:
|
||||
GATEKEEPER_HOST: gatekeeper.${{ 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"
|
||||
|
||||
@@ -254,15 +404,16 @@ jobs:
|
||||
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
|
||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
||||
|
||||
# Generate Environment File
|
||||
if [[ "$UMAMI_API_ENDPOINT" != http* ]]; then
|
||||
UMAMI_API_ENDPOINT="https://$UMAMI_API_ENDPOINT"
|
||||
fi
|
||||
|
||||
cat > .env.deploy << EOF
|
||||
# Generated by CI - $TARGET
|
||||
IMAGE_TAG=$IMAGE_TAG
|
||||
@@ -271,32 +422,29 @@ jobs:
|
||||
SENTRY_DSN=$SENTRY_DSN
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
# Payload DB
|
||||
postgres_DB_NAME=$postgres_DB_NAME
|
||||
postgres_DB_USER=$postgres_DB_USER
|
||||
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
||||
DATABASE_URI=$DATABASE_URI
|
||||
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
|
||||
# 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
|
||||
|
||||
S3_ENDPOINT=$S3_ENDPOINT
|
||||
S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
S3_BUCKET=$S3_BUCKET
|
||||
S3_REGION=$S3_REGION
|
||||
S3_PREFIX=$S3_PREFIX
|
||||
TARGET=$TARGET
|
||||
SENTRY_ENVIRONMENT=$TARGET
|
||||
PROJECT_NAME=$PROJECT_NAME
|
||||
@@ -305,6 +453,9 @@ jobs:
|
||||
TRAEFIK_HOST='$TRAEFIK_HOST'
|
||||
COMPOSE_PROFILES=$COMPOSE_PROFILES
|
||||
TRAEFIK_MIDDLEWARES=$AUTH_MIDDLEWARE
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_TLS=true
|
||||
TRAEFIK_CERT_RESOLVER=le
|
||||
EOF
|
||||
|
||||
- name: 🚀 SSH Deploy
|
||||
@@ -317,57 +468,186 @@ jobs:
|
||||
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/mintel.me"
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
# SSH keepalive to prevent timeout during long docker pull
|
||||
cat > ~/.ssh/config <<SSHCFG
|
||||
Host alpha.mintel.me
|
||||
ServerAliveInterval 15
|
||||
ServerAliveCountMax 20
|
||||
ConnectTimeout 30
|
||||
SSHCFG
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
elif [[ "$TARGET" == "testing" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/testing.mintel.me"
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/staging.mintel.me"
|
||||
else
|
||||
SITE_DIR="/home/deploy/sites/branch.mintel.me/${SLUG:-unknown}"
|
||||
fi
|
||||
|
||||
# Upload files
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
||||
|
||||
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"
|
||||
# Deploy
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
# Deploy — alpha is pre-logged into registry.infra.mintel.me, no credential passing needed
|
||||
ssh root@alpha.mintel.me "
|
||||
docker network create '${{ needs.prepare.outputs.project_name }}-internal' || true
|
||||
docker volume create 'mintel-me_payload-db-data' || true
|
||||
cd $SITE_DIR
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans
|
||||
"
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 5: Health Check
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
healthcheck:
|
||||
name: 🩺 Health Check
|
||||
needs: [prepare, deploy]
|
||||
if: needs.deploy.result == 'success'
|
||||
post_deploy_checks:
|
||||
name: 🧪 Post-Deploy Verification
|
||||
needs: [prepare, deploy, qa]
|
||||
if: success() || failure() # Run even if QA fails (due to E2E noise)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔍 Smoke Test
|
||||
- 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: Provide sibling monorepo
|
||||
run: |
|
||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
echo "Checking health of $URL..."
|
||||
for i in {1..12}; do
|
||||
if curl -s -f "$URL" > /dev/null; then
|
||||
echo "✅ Health check passed!"
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
for TOKEN_RAW in $TOKENS; do
|
||||
if [ -n "$TOKEN_RAW" ]; then
|
||||
TOKEN=$(echo "$TOKEN_RAW" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$TOKEN"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: 🏥 App Health Check
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: |
|
||||
echo "Waiting for app to start at $DEPLOY_URL ..."
|
||||
for i in {1..30}; do
|
||||
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' "$DEPLOY_URL" 2>&1) || true
|
||||
echo "Attempt $i: HTTP $HTTP_CODE"
|
||||
if [[ "$HTTP_CODE" =~ ^2 ]]; then
|
||||
echo "✅ App is up (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for service to be ready... ($i/12)"
|
||||
echo "⏳ Waiting... (got $HTTP_CODE)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed after 2 minutes."
|
||||
echo "❌ App health check failed after 30 attempts"
|
||||
exit 1
|
||||
- name: 🚀 OG Image Check
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: pnpm --filter @mintel/web check:og
|
||||
- name: 📝 E2E Smoke Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: |
|
||||
# Install system Chromium + dependencies (KLZ pattern)
|
||||
# Ubuntu's default 'chromium' is a snap wrapper, so we use xtradeb PPA for native binary
|
||||
sudo apt-get update && sudo apt-get install -y gnupg wget ca-certificates
|
||||
|
||||
# Setup xtradeb PPA for native chromium
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | sudo gpg --dearmor -o /etc/apt/keyrings/xtradeb.gpg || true
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" | sudo tee /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" | sudo tee /etc/apt/preferences.d/xtradeb
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 || sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||
|
||||
[ -f /usr/bin/chromium ] && sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
pnpm --filter @mintel/web check:forms
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
needs: [prepare, deploy, healthcheck]
|
||||
needs: [prepare, deploy, post_deploy_checks]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -375,11 +655,20 @@ jobs:
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
run: |
|
||||
STATUS="${{ needs.deploy.result }}"
|
||||
TITLE="mintel.me: $STATUS"
|
||||
[[ "$STATUS" == "success" ]] && PRIORITY=5 || PRIORITY=8
|
||||
DEPLOY="${{ needs.deploy.result }}"
|
||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
|
||||
if [[ "$DEPLOY" == "success" ]] && [[ "$SMOKE" == "success" || "$SMOKE" == "skipped" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="✅"
|
||||
else
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
fi
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=Deploy to ${{ needs.prepare.outputs.target }} finished with status $STATUS.\nVersion: ${{ needs.prepare.outputs.image_tag }}" \
|
||||
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
||||
-F "message=Deploy: $DEPLOY | Smoke: $SMOKE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
232
.gitea/workflows/qa.yml
Normal file
@@ -0,0 +1,232 @@
|
||||
name: Nightly QA
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Deploy"]
|
||||
branches: [main]
|
||||
types:
|
||||
- completed
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_URL: "https://testing.mintel.me"
|
||||
PROJECT_NAME: "mintel.me"
|
||||
|
||||
jobs:
|
||||
# ────────────────────────────────────────────────────
|
||||
# 1. Static Checks (HTML, Assets, HTTP)
|
||||
# ────────────────────────────────────────────────────
|
||||
static:
|
||||
name: 🔍 Static Analysis
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --fix-missing \
|
||||
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 \
|
||||
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 || true
|
||||
apt-get install -y libasound2t64 || apt-get install -y libasound2 || true
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🖼️ OG Images
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:og
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 2. E2E (Forms)
|
||||
# ────────────────────────────────────────────────────
|
||||
e2e:
|
||||
name: 📝 E2E
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 📝 E2E Form Submission Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:forms
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 3. Performance (Lighthouse)
|
||||
# ────────────────────────────────────────────────────
|
||||
lighthouse:
|
||||
name: 🎭 Lighthouse
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🎭 Desktop
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
- name: 📱 Mobile
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 4. Link Check & Dependency Audit
|
||||
# ────────────────────────────────────────────────────
|
||||
links:
|
||||
name: 🔗 Links & Deps
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 📦 Depcheck
|
||||
continue-on-error: true
|
||||
run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" || true
|
||||
- name: 🔗 Lychee Link Check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --accept 200,204,429 --timeout 10 --insecure --exclude "file://*" --exclude "https://logs.infra.mintel.me/*" --exclude "https://git.infra.mintel.me/*" --exclude "https://mintel.me/*" '*.md' 'docs/*.md'
|
||||
fail: false
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 5. Notification
|
||||
# ────────────────────────────────────────────────────
|
||||
notify:
|
||||
name: 🔔 Notify
|
||||
needs: [static, e2e, lighthouse, links]
|
||||
if: failure()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
STATIC="${{ needs.static.result }}"
|
||||
E2E="${{ needs.e2e.result }}"
|
||||
LIGHTHOUSE="${{ needs.lighthouse.result }}"
|
||||
LINKS="${{ needs.links.result }}"
|
||||
|
||||
if [[ "$STATIC" != "success" || "$LIGHTHOUSE" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
STATUS="Failed"
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS="Passed"
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI ${{ env.PROJECT_NAME }} QA $STATUS"
|
||||
MESSAGE="Static: $STATIC | E2E: $E2E | Lighthouse: $LIGHTHOUSE | Links: $LINKS
|
||||
${{ env.TARGET_URL }}"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=$MESSAGE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
10
.gitignore
vendored
@@ -47,3 +47,13 @@ pnpm-debug.log*
|
||||
.cache/
|
||||
cloned-websites/
|
||||
storage/
|
||||
data/postgres/
|
||||
|
||||
# Estimation Engine Data
|
||||
data/crawls/
|
||||
apps/web/out/estimations/
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
.turbo
|
||||
5
.npmrc
@@ -1,3 +1,2 @@
|
||||
@mintel:registry=https://npm.infra.mintel.me/
|
||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||
|
||||
1
.turbo/cache/41a721a9104bd76c-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "41a721a9104bd76c", "duration": 2524 }
|
||||
BIN
.turbo/cache/41a721a9104bd76c.tar.zst
vendored
Normal file
1
.turbo/cache/441277b34176cf11-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "441277b34176cf11", "duration": 2934 }
|
||||
BIN
.turbo/cache/441277b34176cf11.tar.zst
vendored
Normal file
1
.turbo/cache/708dc951079154e6-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "708dc951079154e6", "duration": 194 }
|
||||
BIN
.turbo/cache/708dc951079154e6.tar.zst
vendored
Normal file
1
.turbo/cache/84b66091bfb55705-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "84b66091bfb55705", "duration": 2417 }
|
||||
BIN
.turbo/cache/84b66091bfb55705.tar.zst
vendored
Normal file
1
.turbo/cache/ba4a4a0aae882f7f-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "ba4a4a0aae882f7f", "duration": 5009 }
|
||||
BIN
.turbo/cache/ba4a4a0aae882f7f.tar.zst
vendored
Normal file
15
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Builder
|
||||
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.21 AS builder
|
||||
FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Arguments for build-time configuration
|
||||
@@ -18,24 +18,29 @@ ENV CI=true
|
||||
# Copy manifest files specifically for better layer caching
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||
COPY apps/web/package.json ./apps/web/package.json
|
||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||
COPY _at-mintel* ./_at-mintel/
|
||||
|
||||
# Install dependencies with cache mount and dynamic .npmrc (High Fidelity pattern)
|
||||
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) && \
|
||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
pnpm install --frozen-lockfile && \
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc && \
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
echo "always-auth=true" >> .npmrc && \
|
||||
cd _at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app && pnpm install --no-frozen-lockfile && \
|
||||
rm .npmrc
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build application (monorepo filter)
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
RUN pnpm --filter @mintel/web build
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
|
||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Copy standalone output and static files (Monorepo paths)
|
||||
|
||||
334
apps/web/.turbo/turbo-lint.log
Normal file
@@ -0,0 +1,334 @@
|
||||
|
||||
> @mintel/web@0.1.0 lint /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> eslint app src scripts video
|
||||
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/about/page.tsx
|
||||
3:8 warning 'Image' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:3 warning 'ResultIllustration' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
11:3 warning 'HeroLines' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
12:3 warning 'ParticleNetwork' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
13:3 warning 'GridLines' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
16:10 warning 'Check' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
31:3 warning 'CodeSnippet' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
32:3 warning 'AbstractCircuit' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
53:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/case-studies/klz-cables/page.tsx
|
||||
8:3 warning 'H1' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/not-found.tsx
|
||||
6:8 warning 'Link' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/page.tsx
|
||||
18:3 warning 'MonoLabel' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
21:16 warning 'Container' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
23:24 warning 'CodeSnippet' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:10 warning 'IconList' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:20 warning 'IconListItem' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/technologies/[slug]/data.tsx
|
||||
1:24 warning 'Database' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/ai-estimate.ts
|
||||
8:10 warning 'fileURLToPath' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/check-og-images.ts
|
||||
19:11 warning 'body' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/generate-thumbnail.ts
|
||||
28:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/migrate-posts.ts
|
||||
107:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/pagespeed-sitemap.ts
|
||||
109:14 warning 'err' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ArticleMeme.tsx
|
||||
110:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ArticleQuote.tsx
|
||||
20:5 warning 'role' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/BlogOGImageTemplate.tsx
|
||||
41:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/CombinedQuotePDF.tsx
|
||||
30:9 warning 'date' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ComponentShareButton.tsx
|
||||
126:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/Configurator/ConfiguratorLayout.tsx
|
||||
24:3 warning 'title' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/Configurator/ReferenceInput.tsx
|
||||
7:10 warning 'cn' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/DirectMessageFlow.tsx
|
||||
3:10 warning 'motion' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/EmailTemplates.tsx
|
||||
1:13 warning 'React' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/pdf/LocalEstimationPDF.tsx
|
||||
94:9 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/BaseStep.tsx
|
||||
13:3 warning 'HelpCircle' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
14:3 warning 'ArrowRight' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/ContentStep.tsx
|
||||
103:25 warning 'index' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/DesignStep.tsx
|
||||
7:19 warning 'Palette' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
104:38 warning 'index' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/FeaturesStep.tsx
|
||||
8:18 warning 'AnimatePresence' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:10 warning 'Minus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:17 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/FunctionsStep.tsx
|
||||
7:18 warning 'AnimatePresence' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
8:10 warning 'Minus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
8:17 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/LanguageStep.tsx
|
||||
5:23 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
125:31 warning 'i' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/PresenceStep.tsx
|
||||
5:10 warning 'Checkbox' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/DiagramShareButton.tsx
|
||||
28:9 warning 'generateDiagramImage' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/DiagramState.tsx
|
||||
25:3 warning 'states' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Effects/CMSVisualizer.tsx
|
||||
8:3 warning 'Edit3' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Effects/CircuitBoard.tsx
|
||||
120:9 warning 'drawTrace' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
130:13 warning 'midX' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
131:13 warning 'midY' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/FAQSection.tsx
|
||||
5:10 warning 'Paragraph' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
7:11 warning 'FAQItem' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/FileExample.tsx
|
||||
3:27 warning 'useRef' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/IframeSection.tsx
|
||||
207:18 warning Empty block statement no-empty
|
||||
252:18 warning Empty block statement no-empty
|
||||
545:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ImageText.tsx
|
||||
25:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/MediumCard.tsx
|
||||
3:10 warning 'Card' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
34:13 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Mermaid.tsx
|
||||
248:18 warning 'err' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/PayloadRichText.tsx
|
||||
177:31 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
180:26 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
181:34 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
186:27 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
191:29 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
196:32 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ShareModal.tsx
|
||||
7:8 warning 'IconBlack' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
181:23 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
231:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
258:13 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/blog/BlogClient.tsx
|
||||
27:11 warning 'trackEvent' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/blog/BlogPostHeader.tsx
|
||||
54:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/migrations/20260227_171023_crm_collections.ts
|
||||
3:32 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
3:41 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
360:3 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
361:3 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/migrations/20260301_151838.ts
|
||||
3:32 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
3:41 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
110:3 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
111:3 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/actions/generateField.ts
|
||||
3:10 warning 'config' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/actions/optimizePost.ts
|
||||
4:10 warning 'revalidatePath' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleMemeBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleQuoteBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/BoldNumberBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ButtonBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/CarouselBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ComparisonRowBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramFlowBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramGanttBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramPieBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramSequenceBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramStateBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramTimelineBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ExternalLinkBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/FAQSectionBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
39:22 warning 'ai' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
39:26 warning 'render' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/IconListBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ImageTextBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LeadMagnetBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LeadParagraphBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LinkedInEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MarkerBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MemeCardBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MermaidBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MetricBarBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ParagraphBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PerformanceChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/RevealBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/SectionBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/StatsDisplayBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/StatsGridBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TLDRBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TrackedLinkBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TwitterEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/WaterfallChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/WebVitalsScoreBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/YouTubeEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/allBlocks.ts
|
||||
100:47 warning 'ai' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
100:51 warning 'render' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/collections/ContextFiles.ts
|
||||
2:8 warning 'fs' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
27:10 warning 'doc' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
27:15 warning 'operation' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/AiFieldButton.tsx
|
||||
13:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
59:14 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/GenerateSlugButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
23:19 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/GenerateThumbnailButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/OptimizeButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
✖ 141 problems (0 errors, 141 warnings)
|
||||
|
||||
5
apps/web/.turbo/turbo-test.log
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
> @mintel/web@0.1.0 test /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> echo "No tests configured"
|
||||
|
||||
No tests configured
|
||||
4
apps/web/.turbo/turbo-typecheck.log
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
> @mintel/web@0.1.0 typecheck /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> tsc --noEmit
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
||||
import config from "@payload-config";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export const handleServerFunctions = async (args: any) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
|
||||
import configPromise from "@payload-config";
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "../importMap";
|
||||
|
||||
type Args = {
|
||||
|
||||
@@ -1,99 +1 @@
|
||||
import { OptimizeButton as OptimizeButton_a629b3460534b7aa208597fdc5e30aec } from "@/src/payload/components/OptimizeButton";
|
||||
import { GenerateSlugButton as GenerateSlugButton_63aadb132a046b3f001fac7a715e5717 } from "@/src/payload/components/FieldGenerators/GenerateSlugButton";
|
||||
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
|
||||
import { GenerateThumbnailButton as GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e } from "@/src/payload/components/FieldGenerators/GenerateThumbnailButton";
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { AiFieldButton as AiFieldButton_da42292f87769a8025025b774910be6d } from "@/src/payload/components/FieldGenerators/AiFieldButton";
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker";
|
||||
import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector";
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||
|
||||
export const importMap = {
|
||||
"@/src/payload/components/OptimizeButton#OptimizeButton":
|
||||
OptimizeButton_a629b3460534b7aa208597fdc5e30aec,
|
||||
"@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton":
|
||||
GenerateSlugButton_63aadb132a046b3f001fac7a715e5717,
|
||||
"@/src/payload/components/TagSelector#default":
|
||||
default_76cec558bd86098fa1dab70b12eb818f,
|
||||
"@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton":
|
||||
GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
|
||||
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
|
||||
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
|
||||
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient":
|
||||
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton":
|
||||
AiFieldButton_da42292f87769a8025025b774910be6d,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
|
||||
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
|
||||
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
|
||||
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
|
||||
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
|
||||
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
|
||||
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
|
||||
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
|
||||
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
|
||||
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
|
||||
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
|
||||
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
|
||||
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
|
||||
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
|
||||
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
|
||||
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
|
||||
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
|
||||
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
|
||||
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
|
||||
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
|
||||
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@/src/payload/components/ColorPicker#default":
|
||||
default_2ebf44fdf8ebc607cf0de30cff485248,
|
||||
"@/src/payload/components/IconSelector#default":
|
||||
default_a1c6da8fb7dd9846a8b07123ff256d09,
|
||||
"@payloadcms/next/rsc#CollectionCards":
|
||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
export const importMap = {};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import React from "react";
|
||||
|
||||
import { handleServerFunctions } from "./actions";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -48,30 +48,10 @@ export const technologies: Record<string, TechInfo> = {
|
||||
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: "bg-blue-600 text-white",
|
||||
related: [
|
||||
{ name: "Directus CMS", slug: "directus-cms" },
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
"directus-cms": {
|
||||
title: "Directus CMS",
|
||||
subtitle: "The Open Data Platform",
|
||||
description:
|
||||
"Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn't dictate how your website looks.",
|
||||
icon: Database,
|
||||
benefits: [
|
||||
"Intuitive interface for non-technical editors",
|
||||
"Complete freedom regarding front-end design",
|
||||
"Real-time updates and live previews",
|
||||
"Highly secure and role-based access control",
|
||||
],
|
||||
customerValue:
|
||||
"Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.",
|
||||
color: "bg-purple-600 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
||||
],
|
||||
},
|
||||
|
||||
"tailwind-css": {
|
||||
title: "Tailwind CSS",
|
||||
subtitle: "Utility-First CSS Framework",
|
||||
|
||||
42
apps/web/app/api/health/cms/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
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() {
|
||||
const checks: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
checks.init = "ok";
|
||||
|
||||
// Verify each collection can be queried (catches missing locale tables, broken migrations)
|
||||
// Adjusted for mintel.me collections
|
||||
const collections = ["posts", "projects", "media", "inquiries"] as const;
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
await payload.find({ collection, limit: 1 });
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
35
apps/web/build.log
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 50s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
Error: Cannot find module '/Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/globals/AiSettings' imported from /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/index.js
|
||||
at ignore-listed frames {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/globals/AiSettings'
|
||||
}
|
||||
|
||||
> Build error occurred
|
||||
Error: Failed to collect page data for /blog/[slug]/opengraph-image-fx5gi7
|
||||
at ignore-listed frames {
|
||||
type: 'Error'
|
||||
}
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
38
apps/web/build2.log
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 48s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
TypeError: Unknown file extension ".css" for /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/react-image-crop@10.1.8_react@19.2.4/node_modules/react-image-crop/dist/ReactCrop.css
|
||||
at Object.getFileProtocolModuleFormat [as (file:] (node:internal/modules/esm/get_format:176:9) {
|
||||
code: 'ERR_UNKNOWN_FILE_EXTENSION'
|
||||
}
|
||||
TypeError: Unknown file extension ".css" for /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/react-image-crop@10.1.8_react@19.2.4/node_modules/react-image-crop/dist/ReactCrop.css
|
||||
at Object.getFileProtocolModuleFormat [as (file:] (node:internal/modules/esm/get_format:176:9) {
|
||||
code: 'ERR_UNKNOWN_FILE_EXTENSION'
|
||||
}
|
||||
|
||||
> Build error occurred
|
||||
Error: Failed to collect page data for /sitemap.xml
|
||||
at ignore-listed frames {
|
||||
type: 'Error'
|
||||
}
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
96
apps/web/build3.log
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 47s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
Generating static pages using 15 workers (0/25) ...
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
Generating static pages using 15 workers (6/25)
|
||||
Generating static pages using 15 workers (12/25)
|
||||
Generating static pages using 15 workers (18/25)
|
||||
✓ Generating static pages using 15 workers (25/25) in 3.1s
|
||||
Lexical => JSX converter: Blocks converter: found mintelTldr block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelTldr block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
Finalizing page optimization ...
|
||||
Collecting build traces ...
|
||||
|
||||
Route (app)
|
||||
┌ ○ /
|
||||
├ ○ /_not-found
|
||||
├ ○ /about
|
||||
├ ○ /about/opengraph-image-1ycygp
|
||||
├ ƒ /admin/[[...segments]]
|
||||
├ ƒ /api/[...slug]
|
||||
├ ƒ /api/health/cms
|
||||
├ ƒ /api/tweet/[id]
|
||||
├ ○ /blog
|
||||
├ ● /blog/[slug]
|
||||
│ ├ /blog/why-websites-break-after-updates
|
||||
│ └ /blog/maintenance-for-headless-systems
|
||||
├ ƒ /blog/[slug]/opengraph-image-fx5gi7
|
||||
├ ○ /case-studies
|
||||
├ ○ /case-studies/klz-cables
|
||||
├ ○ /contact
|
||||
├ ○ /contact/opengraph-image-upzrkl
|
||||
├ ƒ /errors/api/relay
|
||||
├ ○ /opengraph-image-12o0cb
|
||||
├ ○ /sitemap.xml
|
||||
├ ƒ /stats/api/send
|
||||
├ ● /tags/[tag]
|
||||
│ ├ /tags/maintenance
|
||||
│ ├ /tags/reliability
|
||||
│ ├ /tags/software-engineering
|
||||
│ └ /tags/architecture
|
||||
├ ● /technologies/[slug]
|
||||
│ ├ /technologies/next-js-14
|
||||
│ ├ /technologies/typescript
|
||||
│ ├ /technologies/tailwind-css
|
||||
│ └ /technologies/react
|
||||
└ ○ /websites
|
||||
|
||||
|
||||
○ (Static) prerendered as static content
|
||||
● (SSG) prerendered as static HTML (uses generateStaticParams)
|
||||
ƒ (Dynamic) server-rendered on demand
|
||||
|
||||
2
apps/web/ignore-css.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const Module = require("module");
|
||||
Module._extensions[".css"] = function () {};
|
||||
12
apps/web/ignore-css.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export async function load(url, context, nextLoad) {
|
||||
if (url.endsWith('.css') || url.endsWith('.scss')) {
|
||||
return {
|
||||
format: 'module',
|
||||
shortCircuit: true,
|
||||
source: 'export default {};'
|
||||
};
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const envExtension = {
|
||||
* Extends the default Mintel environment schema.
|
||||
*/
|
||||
export const envSchema = withMintelRefinements(
|
||||
z.object(mintelEnvSchema).extend(envExtension),
|
||||
z.object(mintelEnvSchema).extend(envExtension) as any,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,30 @@ const dirname = path.dirname(filename);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
serverExternalPackages: ['@mintel/content-engine'],
|
||||
serverExternalPackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/payload-ai',
|
||||
'@mintel/pdf',
|
||||
'canvas',
|
||||
'sharp',
|
||||
'puppeteer',
|
||||
'require-in-the-middle',
|
||||
'import-in-the-middle' // Sentry 10+ instrumentation dependencies
|
||||
],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.your-objectstorage.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'fsn1.your-objectstorage.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
||||
@@ -25,13 +48,7 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@mintel/content-engine': path.resolve(dirname, 'node_modules/@mintel/content-engine'),
|
||||
};
|
||||
return config;
|
||||
},
|
||||
outputFileTracingRoot: path.join(dirname, '../../'),
|
||||
};
|
||||
|
||||
const withMDX = createMDX({
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||
"scripts": {
|
||||
"dev": "pnpm run seed:context && next dev --turbo",
|
||||
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://directus:directus@127.0.0.1:54321/directus PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "tsx ./seed-context.ts",
|
||||
"dev": "pnpm run seed:context && next dev --webpack --hostname 0.0.0.0",
|
||||
"dev:native": "DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "node --import tsx --experimental-loader ./ignore-css.mjs ./seed-context.ts",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src scripts video",
|
||||
"test": "npm run test:links",
|
||||
"test": "echo \"No tests configured\"",
|
||||
"test:links": "tsx ./scripts/test-links.ts",
|
||||
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts",
|
||||
"generate-estimate": "tsx ./scripts/generate-estimate.ts",
|
||||
@@ -21,16 +21,28 @@
|
||||
"video:render:button": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
||||
"video:render:all": "npm run video:render:contact && npm run video:render:button",
|
||||
"pagespeed:test": "npx tsx ./scripts/pagespeed-sitemap.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:og": "tsx scripts/check-og-images.ts",
|
||||
"check:forms": "tsx scripts/check-forms.ts",
|
||||
"cms:push:testing": "bash ./scripts/cms-sync.sh push testing",
|
||||
"cms:pull:testing": "bash ./scripts/cms-sync.sh pull testing",
|
||||
"cms:push:staging": "bash ./scripts/cms-sync.sh push staging",
|
||||
"cms:pull:staging": "bash ./scripts/cms-sync.sh pull staging",
|
||||
"cms:push:prod": "bash ./scripts/cms-sync.sh push prod",
|
||||
"cms:pull:prod": "bash ./scripts/cms-sync.sh pull prod",
|
||||
"db:restore": "bash ./scripts/restore-db.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@mintel/cloner": "^1.8.0",
|
||||
"@mintel/concept-engine": "link:../../../at-mintel/packages/concept-engine",
|
||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
|
||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||
"@mintel/pdf": "^1.8.0",
|
||||
"@mintel/payload-ai": "^1.9.15",
|
||||
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
|
||||
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@next/third-parties": "^16.1.6",
|
||||
@@ -42,6 +54,7 @@
|
||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||
"@payloadcms/next": "^3.77.0",
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/storage-s3": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@remotion/bundler": "^4.0.414",
|
||||
@@ -60,10 +73,12 @@
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"clsx": "^2.1.1",
|
||||
"crawlee": "^3.15.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.12.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.11.0",
|
||||
"ioredis": "^5.9.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"mermaid": "^11.12.2",
|
||||
@@ -81,6 +96,8 @@
|
||||
"react-tweet": "^3.3.0",
|
||||
"recharts": "^3.7.0",
|
||||
"remotion": "^4.0.414",
|
||||
"replicate": "^1.4.0",
|
||||
"require-in-the-middle": "^8.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"shiki": "^1.24.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
@@ -88,20 +105,22 @@
|
||||
"webpack": "^5.96.1",
|
||||
"website-scraper": "^6.0.0",
|
||||
"website-scraper-puppeteer": "^2.0.0",
|
||||
"zod": "3.22.3"
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^10.0.0",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@mintel/cli": "^1.7.3",
|
||||
"@mintel/eslint-config": "^1.7.3",
|
||||
"@mintel/husky-config": "^1.7.3",
|
||||
"@mintel/next-config": "^1.7.3",
|
||||
"@mintel/next-utils": "^1.7.15",
|
||||
"@mintel/tsconfig": "^1.7.3",
|
||||
"@mintel/cli": "^1.9.0",
|
||||
"@mintel/eslint-config": "^1.9.0",
|
||||
"@mintel/husky-config": "^1.9.0",
|
||||
"@mintel/next-config": "^1.9.0",
|
||||
"@mintel/next-utils": "^1.9.0",
|
||||
"@mintel/tsconfig": "^1.9.0",
|
||||
"@next/eslint-plugin-next": "^16.1.6",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^25.0.6",
|
||||
"@types/nodemailer": "^7.0.10",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
@@ -112,9 +131,15 @@
|
||||
"eslint": "10.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
"require-extensions": "^0.0.4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.54.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.infra.mintel.me:mmintel/mintel.me.git"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +73,30 @@ export interface Config {
|
||||
inquiries: Inquiry;
|
||||
redirects: Redirect;
|
||||
"context-files": ContextFile;
|
||||
"crm-accounts": CrmAccount;
|
||||
"crm-contacts": CrmContact;
|
||||
"crm-topics": CrmTopic;
|
||||
"crm-interactions": CrmInteraction;
|
||||
projects: Project;
|
||||
"payload-kv": PayloadKv;
|
||||
"payload-locked-documents": PayloadLockedDocument;
|
||||
"payload-preferences": PayloadPreference;
|
||||
"payload-migrations": PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsJoins: {
|
||||
"crm-accounts": {
|
||||
topics: "crm-topics";
|
||||
contacts: "crm-contacts";
|
||||
interactions: "crm-interactions";
|
||||
projects: "projects";
|
||||
};
|
||||
"crm-contacts": {
|
||||
interactions: "crm-interactions";
|
||||
};
|
||||
"crm-topics": {
|
||||
interactions: "crm-interactions";
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
@@ -86,6 +104,13 @@ export interface Config {
|
||||
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
|
||||
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||
"context-files": ContextFilesSelect<false> | ContextFilesSelect<true>;
|
||||
"crm-accounts": CrmAccountsSelect<false> | CrmAccountsSelect<true>;
|
||||
"crm-contacts": CrmContactsSelect<false> | CrmContactsSelect<true>;
|
||||
"crm-topics": CrmTopicsSelect<false> | CrmTopicsSelect<true>;
|
||||
"crm-interactions":
|
||||
| CrmInteractionsSelect<false>
|
||||
| CrmInteractionsSelect<true>;
|
||||
projects: ProjectsSelect<false> | ProjectsSelect<true>;
|
||||
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
"payload-locked-documents":
|
||||
| PayloadLockedDocumentsSelect<false>
|
||||
@@ -164,6 +189,7 @@ export interface User {
|
||||
export interface Media {
|
||||
id: number;
|
||||
alt: string;
|
||||
prefix?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -253,6 +279,10 @@ export interface Post {
|
||||
*/
|
||||
export interface Inquiry {
|
||||
id: number;
|
||||
/**
|
||||
* Has this inquiry been converted into a CRM Lead?
|
||||
*/
|
||||
processed?: boolean | null;
|
||||
name: string;
|
||||
email: string;
|
||||
companyName?: string | null;
|
||||
@@ -308,6 +338,261 @@ export interface ContextFile {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Accounts represent companies or organizations. They are the central hub linking Contacts and Interactions together. Use this to track the overall relationship status.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-accounts".
|
||||
*/
|
||||
export interface CrmAccount {
|
||||
id: number;
|
||||
/**
|
||||
* Enter the official name of the business or the research project name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The main website of the account. Required for triggering the AI Website Analysis.
|
||||
*/
|
||||
website?: string | null;
|
||||
/**
|
||||
* Current lifecycle stage of this business relation.
|
||||
*/
|
||||
status?: ("lead" | "client" | "partner" | "lost") | null;
|
||||
/**
|
||||
* Indicates how likely this lead is to convert soon.
|
||||
*/
|
||||
leadTemperature?: ("cold" | "warm" | "hot") | null;
|
||||
/**
|
||||
* The internal team member responsible for this account.
|
||||
*/
|
||||
assignedTo?: (number | null) | User;
|
||||
/**
|
||||
* All generated PDF estimates and strategy documents appear here.
|
||||
*/
|
||||
reports?: (number | Media)[] | null;
|
||||
/**
|
||||
* Projects, deals, or specific topics active for this client.
|
||||
*/
|
||||
topics?: {
|
||||
docs?: (number | CrmTopic)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
/**
|
||||
* All contacts associated with this account.
|
||||
*/
|
||||
contacts?: {
|
||||
docs?: (number | CrmContact)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
/**
|
||||
* Timeline of all communication logged against this account.
|
||||
*/
|
||||
interactions?: {
|
||||
docs?: (number | CrmInteraction)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
/**
|
||||
* All high-level projects associated with this account.
|
||||
*/
|
||||
projects?: {
|
||||
docs?: (number | Project)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Group your interactions (emails, calls, notes) into Topics. This helps you keep track of specific projects with a client.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-topics".
|
||||
*/
|
||||
export interface CrmTopic {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Which account does this topic belong to?
|
||||
*/
|
||||
account: number | CrmAccount;
|
||||
status: "active" | "paused" | "won" | "lost";
|
||||
/**
|
||||
* Optional: What stage is this deal/project currently in?
|
||||
*/
|
||||
stage?: ("discovery" | "proposal" | "negotiation" | "implementation") | null;
|
||||
/**
|
||||
* Timeline of all emails and notes specifically related to this topic.
|
||||
*/
|
||||
interactions?: {
|
||||
docs?: (number | CrmInteraction)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Your CRM journal. Log what happened, when, on which channel, and attach any relevant files. This is for summaries and facts — not for sending messages.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-interactions".
|
||||
*/
|
||||
export interface CrmInteraction {
|
||||
id: number;
|
||||
/**
|
||||
* Where did this communication take place?
|
||||
*/
|
||||
type:
|
||||
| "email"
|
||||
| "call"
|
||||
| "meeting"
|
||||
| "whatsapp"
|
||||
| "social"
|
||||
| "document"
|
||||
| "note";
|
||||
direction?: ("inbound" | "outbound") | null;
|
||||
/**
|
||||
* When did this happen?
|
||||
*/
|
||||
date: string;
|
||||
subject: string;
|
||||
/**
|
||||
* Who was involved?
|
||||
*/
|
||||
contact?: (number | null) | CrmContact;
|
||||
account?: (number | null) | CrmAccount;
|
||||
/**
|
||||
* Optional: Group this entry under a specific project or topic.
|
||||
*/
|
||||
topic?: (number | null) | CrmTopic;
|
||||
/**
|
||||
* Summarize what happened, what was decided, or what the next steps are.
|
||||
*/
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ("ltr" | "rtl") | null;
|
||||
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
/**
|
||||
* Attach received documents, screenshots, contracts, or any relevant files.
|
||||
*/
|
||||
attachments?: (number | Media)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Contacts are the individual people linked to an Account. A person should only be created once and can be assigned to a company here.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-contacts".
|
||||
*/
|
||||
export interface CrmContact {
|
||||
id: number;
|
||||
fullName?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
/**
|
||||
* Primary email address for communication tracking.
|
||||
*/
|
||||
email: string;
|
||||
phone?: string | null;
|
||||
linkedIn?: string | null;
|
||||
/**
|
||||
* e.g. CEO, Marketing Manager, Technical Lead
|
||||
*/
|
||||
role?: string | null;
|
||||
/**
|
||||
* Link this person to an organization from the Accounts collection.
|
||||
*/
|
||||
account?: (number | null) | CrmAccount;
|
||||
/**
|
||||
* Timeline of all communication logged directly with this person.
|
||||
*/
|
||||
interactions?: {
|
||||
docs?: (number | CrmInteraction)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Manage high-level projects for your clients.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "projects".
|
||||
*/
|
||||
export interface Project {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Which account is this project for?
|
||||
*/
|
||||
account: number | CrmAccount;
|
||||
/**
|
||||
* Key contacts from the client side involved in this project.
|
||||
*/
|
||||
contact?: (number | CrmContact)[] | null;
|
||||
status: "draft" | "in_progress" | "review" | "completed";
|
||||
startDate?: string | null;
|
||||
targetDate?: string | null;
|
||||
valueMin?: number | null;
|
||||
valueMax?: number | null;
|
||||
/**
|
||||
* Project briefing, requirements, or notes.
|
||||
*/
|
||||
briefing?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ("ltr" | "rtl") | null;
|
||||
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
/**
|
||||
* Upload files, documents, or assets related to this project.
|
||||
*/
|
||||
attachments?: (number | Media)[] | null;
|
||||
/**
|
||||
* Granular deliverables or milestones within this project.
|
||||
*/
|
||||
milestones?:
|
||||
| {
|
||||
name: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
priority?: ("low" | "medium" | "high") | null;
|
||||
startDate?: string | null;
|
||||
targetDate?: string | null;
|
||||
/**
|
||||
* Internal team member responsible for this milestone.
|
||||
*/
|
||||
assignee?: (number | null) | User;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
@@ -355,6 +640,26 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: "context-files";
|
||||
value: number | ContextFile;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "crm-accounts";
|
||||
value: number | CrmAccount;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "crm-contacts";
|
||||
value: number | CrmContact;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "crm-topics";
|
||||
value: number | CrmTopic;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "crm-interactions";
|
||||
value: number | CrmInteraction;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "projects";
|
||||
value: number | Project;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -426,6 +731,7 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
prefix?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
@@ -498,6 +804,7 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
* via the `definition` "inquiries_select".
|
||||
*/
|
||||
export interface InquiriesSelect<T extends boolean = true> {
|
||||
processed?: T;
|
||||
name?: T;
|
||||
email?: T;
|
||||
companyName?: T;
|
||||
@@ -528,6 +835,100 @@ export interface ContextFilesSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-accounts_select".
|
||||
*/
|
||||
export interface CrmAccountsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
website?: T;
|
||||
status?: T;
|
||||
leadTemperature?: T;
|
||||
assignedTo?: T;
|
||||
reports?: T;
|
||||
topics?: T;
|
||||
contacts?: T;
|
||||
interactions?: T;
|
||||
projects?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-contacts_select".
|
||||
*/
|
||||
export interface CrmContactsSelect<T extends boolean = true> {
|
||||
fullName?: T;
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
email?: T;
|
||||
phone?: T;
|
||||
linkedIn?: T;
|
||||
role?: T;
|
||||
account?: T;
|
||||
interactions?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-topics_select".
|
||||
*/
|
||||
export interface CrmTopicsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
account?: T;
|
||||
status?: T;
|
||||
stage?: T;
|
||||
interactions?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "crm-interactions_select".
|
||||
*/
|
||||
export interface CrmInteractionsSelect<T extends boolean = true> {
|
||||
type?: T;
|
||||
direction?: T;
|
||||
date?: T;
|
||||
subject?: T;
|
||||
contact?: T;
|
||||
account?: T;
|
||||
topic?: T;
|
||||
content?: T;
|
||||
attachments?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "projects_select".
|
||||
*/
|
||||
export interface ProjectsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
account?: T;
|
||||
contact?: T;
|
||||
status?: T;
|
||||
startDate?: T;
|
||||
targetDate?: T;
|
||||
valueMin?: T;
|
||||
valueMax?: T;
|
||||
briefing?: T;
|
||||
attachments?: T;
|
||||
milestones?:
|
||||
| T
|
||||
| {
|
||||
name?: T;
|
||||
status?: T;
|
||||
priority?: T;
|
||||
startDate?: T;
|
||||
targetDate?: T;
|
||||
assignee?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
|
||||
@@ -4,6 +4,7 @@ import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
|
||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||
import { s3Storage } from "@payloadcms/storage-s3";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import sharp from "sharp";
|
||||
@@ -11,11 +12,16 @@ import sharp from "sharp";
|
||||
import { Users } from "./src/payload/collections/Users";
|
||||
import { Media } from "./src/payload/collections/Media";
|
||||
import { Posts } from "./src/payload/collections/Posts";
|
||||
import { emailWebhookHandler } from "./src/payload/endpoints/emailWebhook";
|
||||
import { aiEndpointHandler } from "./src/payload/endpoints/aiEndpoint";
|
||||
import { Inquiries } from "./src/payload/collections/Inquiries";
|
||||
import { Redirects } from "./src/payload/collections/Redirects";
|
||||
import { ContextFiles } from "./src/payload/collections/ContextFiles";
|
||||
|
||||
import { AiSettings } from "./src/payload/globals/AiSettings";
|
||||
import { CrmAccounts } from "./src/payload/collections/CrmAccounts";
|
||||
import { CrmContacts } from "./src/payload/collections/CrmContacts";
|
||||
import { CrmInteractions } from "./src/payload/collections/CrmInteractions";
|
||||
import { CrmTopics } from "./src/payload/collections/CrmTopics";
|
||||
import { Projects } from "./src/payload/collections/Projects";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
@@ -27,24 +33,35 @@ export default buildConfig({
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [Users, Media, Posts, Inquiries, Redirects, ContextFiles],
|
||||
globals: [AiSettings],
|
||||
...(process.env.MAIL_HOST
|
||||
? {
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST,
|
||||
port: parseInt(process.env.MAIL_PORT || "587"),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
collections: [
|
||||
Users,
|
||||
Media,
|
||||
Posts,
|
||||
Inquiries,
|
||||
Redirects,
|
||||
ContextFiles,
|
||||
CrmAccounts,
|
||||
CrmContacts,
|
||||
CrmTopics,
|
||||
CrmInteractions,
|
||||
Projects,
|
||||
],
|
||||
globals: [
|
||||
/* AiSettings as any */
|
||||
],
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || "localhost",
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME || "user",
|
||||
pass: process.env.MAIL_PASSWORD || "pass",
|
||||
},
|
||||
...(process.env.MAIL_HOST ? {} : { ignoreTLS: true }),
|
||||
},
|
||||
}),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
@@ -64,5 +81,34 @@ export default buildConfig({
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [],
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
? [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || "",
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
path: "/crm/incoming-email",
|
||||
method: "post",
|
||||
handler: emailWebhookHandler,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 5.0 MiB |
BIN
apps/web/public/blog/build-first-digital-architecture.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
apps/web/public/blog/builder-systems-threaten-independence.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
apps/web/public/blog/clean-code-for-business-value.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
apps/web/public/blog/crm-synchronization-headless.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
BIN
apps/web/public/blog/digital-longevity-architecture.png
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
apps/web/public/blog/fixed-price-digital-projects.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
apps/web/public/blog/gdpr-conformity-system-approach.png
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
BIN
apps/web/public/blog/green-it-sustainable-web.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
apps/web/public/blog/hidden-costs-of-wordpress-plugins.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
apps/web/public/blog/no-us-cloud-platforms.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
apps/web/public/blog/professional-hosting-operations.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
apps/web/public/blog/responsive-design-high-fidelity.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
apps/web/public/blog/slow-loading-costs-customers.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
apps/web/public/blog/website-without-cookie-banners.png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
apps/web/public/blog/why-agencies-are-slow.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
apps/web/public/blog/why-no-templates-matter.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
apps/web/public/blog/why-websites-break-after-updates.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
21
apps/web/scripts/backup-db.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
DB_CONTAINER="mintel-me-postgres-db-1"
|
||||
DB_USER="payload"
|
||||
DB_NAME="payload"
|
||||
|
||||
# Resolve backup dir relative to this script's location
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_DIR="${SCRIPT_DIR}/../../../backups"
|
||||
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||
BACKUP_FILE="${BACKUP_DIR}/payload_backup_${TIMESTAMP}.dump"
|
||||
|
||||
echo "Creating backup directory at ${BACKUP_DIR}..."
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
echo "Dumping database '${DB_NAME}' from container '${DB_CONTAINER}'..."
|
||||
docker exec ${DB_CONTAINER} pg_dump -U ${DB_USER} -F c ${DB_NAME} > "${BACKUP_FILE}"
|
||||
|
||||
echo "✅ Backup successful: ${BACKUP_FILE}"
|
||||
ls -lh "${BACKUP_FILE}"
|
||||
228
apps/web/scripts/check-forms.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import puppeteer from "puppeteer";
|
||||
|
||||
const targetUrl = process.env.TEST_URL || "http://localhost:3000";
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "secret";
|
||||
|
||||
async function fetchSitemapUrls(baseUrl: string): Promise<string[]> {
|
||||
const sitemapUrl = `${baseUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
try {
|
||||
const response = await fetch(sitemapUrl);
|
||||
const text = await response.text();
|
||||
|
||||
// Simple regex to extract loc tags
|
||||
const matches = text.matchAll(/<loc>(.*?)<\/loc>/g);
|
||||
let urls = Array.from(matches, (m) => m[1]);
|
||||
|
||||
// Normalize to target URL instance
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, baseUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
return urls;
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
||||
|
||||
let urls = await fetchSitemapUrls(targetUrl);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.warn(`⚠️ Falling back to just the homepage.`);
|
||||
urls = [targetUrl];
|
||||
}
|
||||
|
||||
// Launch browser with KLZ pattern: use system chromium via env
|
||||
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",
|
||||
"--disable-gpu",
|
||||
"--ignore-certificate-errors",
|
||||
"--disable-web-security",
|
||||
"--disable-features=IsolateOrigins,site-per-process",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
let hasBrokenAssets = false;
|
||||
let currentScannedUrl = urls[0] || "";
|
||||
|
||||
// Listen for console logging from the page for debugging
|
||||
page.on("console", (msg) => {
|
||||
const type = msg.type();
|
||||
// Only capture errors and warnings, not info/logs
|
||||
if (type === "error" || type === "warn") {
|
||||
const text = msg.text();
|
||||
// Exclude common noise
|
||||
if (
|
||||
text.includes("google-analytics") ||
|
||||
text.includes("googletagmanager") ||
|
||||
text.includes("Fast Refresh")
|
||||
)
|
||||
return;
|
||||
|
||||
console.log(` [PAGE ${type.toUpperCase()}] ${text}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on("pageerror", (err: Error) => {
|
||||
if (currentScannedUrl.includes("showcase")) return;
|
||||
console.error(` [PAGE EXCEPTION] ${err.message}`);
|
||||
});
|
||||
|
||||
// Listen to ALL network responses to catch broken assets (404/500)
|
||||
page.on("response", (response) => {
|
||||
const status = response.status();
|
||||
// Catch classic 404s and 500s on ANY fetch/image/script
|
||||
if (
|
||||
status >= 400 &&
|
||||
status !== 429 &&
|
||||
status !== 999 &&
|
||||
!response.url().includes("google-analytics") &&
|
||||
!response.url().includes("googletagmanager")
|
||||
) {
|
||||
const type = response.request().resourceType();
|
||||
|
||||
// We explicitly care about images, scripts, stylesheets, and fetches getting 404/500s.
|
||||
if (
|
||||
["image", "script", "stylesheet", "fetch", "xhr", "document"].includes(
|
||||
type,
|
||||
)
|
||||
) {
|
||||
// Exclude showcase routes from strict sub-asset checking since they proxy external content
|
||||
if (
|
||||
(currentScannedUrl.includes("showcase") ||
|
||||
response.url().includes("showcase")) &&
|
||||
type !== "document"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
` [REQUEST FAILED] ${response.url()} - Status: ${status} (${type})`,
|
||||
);
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Authenticate through Gatekeeper
|
||||
console.log(`\n🛡️ Authenticating through Gatekeeper...`);
|
||||
console.log(` Navigating to: ${urls[0]}`);
|
||||
|
||||
const response = await page.goto(urls[0], {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
// Give Gatekeeper a second to redirect if needed
|
||||
console.log(` Waiting for potential Gatekeeper redirect...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
console.log(` Response status: ${response?.status()}`);
|
||||
console.log(` Response URL: ${response?.url()}`);
|
||||
|
||||
const isGatekeeperPage = await page.$('input[name="password"]');
|
||||
if (isGatekeeperPage) {
|
||||
await page.type('input[name="password"]', gatekeeperPassword);
|
||||
await Promise.all([
|
||||
page.waitForNavigation({
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 120000,
|
||||
}),
|
||||
page.click('button[type="submit"]'),
|
||||
]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
console.log(`✅ Gatekeeper authentication successful!`);
|
||||
} else {
|
||||
console.log(`✅ Already authenticated (no Gatekeeper gate detected).`);
|
||||
}
|
||||
|
||||
// Scan each page
|
||||
console.log(`\n🧪 Testing all ${urls.length} pages...`);
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const u = urls[i];
|
||||
currentScannedUrl = u;
|
||||
console.log(`\n[${i + 1}/${urls.length}] Scanning: ${u}`);
|
||||
try {
|
||||
await page.goto(u, { waitUntil: "domcontentloaded", timeout: 120000 });
|
||||
|
||||
// Simulate a scroll to bottom to trigger lazy-loads if necessary
|
||||
await page.evaluate(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 500;
|
||||
const timer = setInterval(() => {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
// Stop scrolling if we reached the bottom or scrolled for more than 5 seconds
|
||||
if (totalHeight >= scrollHeight || totalHeight > 10000) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Small delay for final hydration and asynchronous asset loading
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const title = await page.title();
|
||||
console.log(` ✅ Page Title: ${title}`);
|
||||
|
||||
if (!title) {
|
||||
throw new Error(`Page title is missing.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
` ❌ Timeout or navigation error on ${u}: ${err.message}`,
|
||||
);
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`\n❌ Fatal Test Error: ${err.message}`);
|
||||
// Take a screenshot for debugging on crash
|
||||
try {
|
||||
const screenshotPath = "/tmp/e2e-failure.png";
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
console.log(`📸 Screenshot saved to ${screenshotPath}`);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
hasBrokenAssets = true;
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (hasBrokenAssets) {
|
||||
console.error(
|
||||
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n🎉 SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken assets!`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
104
apps/web/scripts/check-og-images.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
const BASE_URL = process.env.TEST_URL || "http://localhost:3000";
|
||||
|
||||
console.log(`\n🚀 Starting Dynamic OG Image Verification for ${BASE_URL}\n`);
|
||||
|
||||
const pages = ["/", "/about", "/contact"];
|
||||
|
||||
async function getOgImageUrl(pagePath: string): Promise<string | null> {
|
||||
const url = `${BASE_URL}${pagePath}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch page: ${response.status}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
|
||||
// Extract og:image content
|
||||
const match = html.match(/property="og:image"\s+content="([^"]+)"/);
|
||||
if (!match || !match[1]) {
|
||||
// Try name="twitter:image" as fallback or check if it's there
|
||||
const twitterMatch = html.match(
|
||||
/name="twitter:image"\s+content="([^"]+)"/,
|
||||
);
|
||||
return twitterMatch ? twitterMatch[1] : null;
|
||||
}
|
||||
return match[1];
|
||||
} catch (error) {
|
||||
console.error(` ❌ Failed to discover OG image for ${pagePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyImage(
|
||||
imageUrl: string,
|
||||
pagePath: string,
|
||||
): Promise<boolean> {
|
||||
// If the image URL is absolute and contains mintel.me (base domain),
|
||||
// we replace it with our BASE_URL to test the current environment's generated image
|
||||
let testUrl = imageUrl;
|
||||
if (imageUrl.startsWith("https://mintel.me")) {
|
||||
testUrl = imageUrl.replace("https://mintel.me", BASE_URL);
|
||||
} else if (imageUrl.startsWith("/")) {
|
||||
testUrl = `${BASE_URL}${imageUrl}`;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
const response = await fetch(testUrl);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
console.log(`Checking OG Image for ${pagePath}: ${testUrl}...`);
|
||||
|
||||
const body = await response.clone().text();
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!contentType?.includes("image/")) {
|
||||
throw new Error(`Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
|
||||
if (bytes.length < 1000) {
|
||||
throw new Error(`Image too small (${bytes.length} bytes)`);
|
||||
}
|
||||
|
||||
console.log(` ✅ OK (${bytes.length} bytes, ${duration}ms)`);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
console.error(` ❌ FAILED:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let allOk = true;
|
||||
|
||||
for (const page of pages) {
|
||||
console.log(`Discovering OG image for ${page}...`);
|
||||
const ogUrl = await getOgImageUrl(page);
|
||||
|
||||
if (!ogUrl) {
|
||||
console.error(` ❌ No OG image meta tag found for ${page}`);
|
||||
allOk = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const ok = await verifyImage(ogUrl, page);
|
||||
if (!ok) allOk = false;
|
||||
}
|
||||
|
||||
if (allOk) {
|
||||
console.log("\n✨ All OG images verified successfully!\n");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error("\n❌ Some OG images failed verification.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
294
apps/web/scripts/cms-sync.sh
Executable file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# CMS Data Sync Tool (mintel.me)
|
||||
# Safely syncs the Payload CMS PostgreSQL database between environments.
|
||||
# Media is handled via S3 and does NOT need syncing.
|
||||
#
|
||||
# Usage:
|
||||
# npm run cms:push:testing – Push local → testing
|
||||
# npm run cms:push:prod – Push local → production
|
||||
# npm run cms:pull:testing – Pull testing → local
|
||||
# npm run cms:pull:prod – Pull production → local
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
SYNC_SUCCESS="false"
|
||||
LOCAL_BACKUP_FILE=""
|
||||
REMOTE_BACKUP_FILE=""
|
||||
|
||||
cleanup_on_exit() {
|
||||
local exit_code=$?
|
||||
if [ "$SYNC_SUCCESS" != "true" ] && [ $exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Sync aborted or failed! (Exit code: $exit_code)"
|
||||
if [ "${DIRECTION:-}" = "push" ] && [ -n "${REMOTE_BACKUP_FILE:-}" ]; then
|
||||
echo "🔄 Rolling back $TARGET database..."
|
||||
ssh "$SSH_HOST" "gunzip -c $REMOTE_BACKUP_FILE | docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --quiet" || echo "⚠️ Rollback failed"
|
||||
echo "✅ Rollback complete."
|
||||
elif [ "${DIRECTION:-}" = "pull" ] && [ -n "${LOCAL_BACKUP_FILE:-}" ]; then
|
||||
echo "🔄 Rolling back local database..."
|
||||
gunzip -c "$LOCAL_BACKUP_FILE" | docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --quiet || echo "⚠️ Rollback failed"
|
||||
echo "✅ Rollback complete."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap 'cleanup_on_exit' EXIT
|
||||
|
||||
# Load environment variables
|
||||
if [ -f ../../.env ]; then
|
||||
set -a; source ../../.env; set +a
|
||||
fi
|
||||
if [ -f .env ]; then
|
||||
set -a; source .env; set +a
|
||||
fi
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
DIRECTION="${1:-}" # push | pull
|
||||
TARGET="${2:-}" # testing | prod
|
||||
SSH_HOST="root@alpha.mintel.me"
|
||||
LOCAL_DB_USER="${postgres_DB_USER:-payload}"
|
||||
LOCAL_DB_NAME="${postgres_DB_NAME:-payload}"
|
||||
LOCAL_DB_CONTAINER="mintel-me-postgres-db-1"
|
||||
|
||||
# Resolve directories
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_DIR="${SCRIPT_DIR}/../../../../backups"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
|
||||
# Remote credentials (resolved per-target from server env files)
|
||||
REMOTE_DB_USER=""
|
||||
REMOTE_DB_NAME=""
|
||||
|
||||
# Auto-detect migrations from apps/web/src/migrations/*.ts
|
||||
MIGRATIONS=()
|
||||
BATCH=1
|
||||
for migration_file in $(ls "${SCRIPT_DIR}/../src/migrations"/*.ts 2>/dev/null | sort); do
|
||||
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() {
|
||||
case "$TARGET" in
|
||||
testing)
|
||||
REMOTE_PROJECT="mintel-me-testing"
|
||||
REMOTE_DB_CONTAINER="mintel-me-testing-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="mintel-me-testing-mintel-me-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/testing.mintel.me"
|
||||
;;
|
||||
staging)
|
||||
REMOTE_PROJECT="mintel-me-staging"
|
||||
REMOTE_DB_CONTAINER="mintel-me-staging-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="mintel-me-staging-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/staging.mintel.me"
|
||||
;;
|
||||
prod|production)
|
||||
REMOTE_PROJECT="mintel-me-production"
|
||||
REMOTE_DB_CONTAINER="mintel-me-production-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="mintel-me-production-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
;;
|
||||
branch-*)
|
||||
local SLUG=${TARGET#branch-}
|
||||
REMOTE_PROJECT="mintel-me-branch-$SLUG"
|
||||
REMOTE_DB_CONTAINER="${REMOTE_PROJECT}-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="${REMOTE_PROJECT}-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/branch.mintel.me/$SLUG"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown target: $TARGET"
|
||||
echo " Valid targets: testing, staging, prod, branch-<slug>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Auto-detect remote DB credentials from the env file on the server
|
||||
echo "🔍 Detecting $TARGET database credentials..."
|
||||
|
||||
# Try specific environment file first, then fallback to .env and .env.*
|
||||
REMOTE_DB_USER=$(ssh "$SSH_HOST" "grep -h '^\(POSTGRES_USER\|postgres_DB_USER\)=' $REMOTE_SITE_DIR/.env.$TARGET $REMOTE_SITE_DIR/.env 2>/dev/null | head -1 | cut -d= -f2" || echo "")
|
||||
REMOTE_DB_NAME=$(ssh "$SSH_HOST" "grep -h '^\(POSTGRES_DB\|postgres_DB_NAME\)=' $REMOTE_SITE_DIR/.env.$TARGET $REMOTE_SITE_DIR/.env 2>/dev/null | head -1 | cut -d= -f2" || echo "")
|
||||
|
||||
# Fallback if empty
|
||||
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
|
||||
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
|
||||
echo " User: $REMOTE_DB_USER | DB: $REMOTE_DB_NAME"
|
||||
}
|
||||
|
||||
# ── Ensure local DB is running ─────────────────────────────────────────────
|
||||
ensure_local_db() {
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "$LOCAL_DB_CONTAINER"; then
|
||||
echo "❌ Local DB container not running: $LOCAL_DB_CONTAINER"
|
||||
echo " Please start the local dev environment first via 'pnpm dev:docker'."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Sanitize migrations table ──────────────────────────────────────────────
|
||||
sanitize_migrations() {
|
||||
local container="$1"
|
||||
local db_user="$2"
|
||||
local db_name="$3"
|
||||
local is_remote="$4" # "true" or "false"
|
||||
|
||||
echo "🔧 Sanitizing payload_migrations table..."
|
||||
local SQL="DELETE FROM payload_migrations WHERE batch = -1;"
|
||||
for entry in "${MIGRATIONS[@]}"; do
|
||||
local name="${entry%%:*}"
|
||||
local batch="${entry##*:}"
|
||||
SQL="$SQL INSERT INTO payload_migrations (name, batch) SELECT '$name', $batch WHERE NOT EXISTS (SELECT 1 FROM payload_migrations WHERE name = '$name');"
|
||||
done
|
||||
|
||||
if [ "$is_remote" = "true" ]; then
|
||||
ssh "$SSH_HOST" "docker exec $container psql -U $db_user -d $db_name -c \"$SQL\""
|
||||
else
|
||||
docker exec "$container" psql -U "$db_user" -d "$db_name" -c "$SQL"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Safety: Create backup before overwriting ───────────────────────────────
|
||||
backup_local_db() {
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local file="$BACKUP_DIR/mintel_pre_sync_${TIMESTAMP}.sql.gz"
|
||||
echo "📦 Creating safety backup of local DB → $file"
|
||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --clean --if-exists | gzip > "$file"
|
||||
echo "✅ Backup: $file ($(du -h "$file" | cut -f1))"
|
||||
LOCAL_BACKUP_FILE="$file"
|
||||
}
|
||||
|
||||
backup_remote_db() {
|
||||
local file="/tmp/mintel_pre_sync_${TIMESTAMP}.sql.gz"
|
||||
echo "📦 Creating safety backup of $TARGET DB → $SSH_HOST:$file"
|
||||
ssh "$SSH_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --clean --if-exists | gzip > $file"
|
||||
echo "✅ Remote backup: $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: push to trigger pipeline, or manually up."
|
||||
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 branch or run the pipeline first."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All $TARGET containers running."
|
||||
}
|
||||
|
||||
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
||||
do_push() {
|
||||
echo ""
|
||||
echo "┌──────────────────────────────────────────────────┐"
|
||||
echo "│ 📤 PUSH: local → $TARGET "
|
||||
echo "│ This will OVERWRITE the $TARGET database! "
|
||||
echo "│ A safety backup will be created first. "
|
||||
echo "└──────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
read -p "Are you sure? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||
|
||||
ensure_local_db
|
||||
check_remote_containers
|
||||
backup_remote_db
|
||||
|
||||
echo "📤 Dumping local database..."
|
||||
local dump="/tmp/mintel_push_${TIMESTAMP}.sql.gz"
|
||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --clean --if-exists | gzip > "$dump"
|
||||
|
||||
echo "📤 Transferring to $SSH_HOST..."
|
||||
scp "$dump" "$SSH_HOST:/tmp/mintel_push.sql.gz"
|
||||
|
||||
echo "🔄 Restoring database on $TARGET..."
|
||||
ssh "$SSH_HOST" "gunzip -c /tmp/mintel_push.sql.gz | docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --quiet"
|
||||
|
||||
sanitize_migrations "$REMOTE_DB_CONTAINER" "$REMOTE_DB_USER" "$REMOTE_DB_NAME" "true"
|
||||
|
||||
echo "🔄 Restarting $TARGET app container..."
|
||||
ssh "$SSH_HOST" "docker restart $REMOTE_APP_CONTAINER"
|
||||
|
||||
rm -f "$dump"
|
||||
ssh "$SSH_HOST" "rm -f /tmp/mintel_push.sql.gz"
|
||||
|
||||
SYNC_SUCCESS="true"
|
||||
echo ""
|
||||
echo "✅ DB Push to $TARGET complete!"
|
||||
}
|
||||
|
||||
# ── PULL: remote → local ──────────────────────────────────────────────────
|
||||
do_pull() {
|
||||
echo ""
|
||||
echo "┌──────────────────────────────────────────────────┐"
|
||||
echo "│ 📥 PULL: $TARGET → local "
|
||||
echo "│ This will OVERWRITE your local database! "
|
||||
echo "│ A safety backup will be created first. "
|
||||
echo "└──────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
read -p "Are you sure? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||
|
||||
ensure_local_db
|
||||
check_remote_containers
|
||||
backup_local_db
|
||||
|
||||
echo "📥 Dumping $TARGET database..."
|
||||
ssh "$SSH_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --clean --if-exists | gzip > /tmp/mintel_pull.sql.gz"
|
||||
|
||||
echo "📥 Downloading from $SSH_HOST..."
|
||||
scp "$SSH_HOST:/tmp/mintel_pull.sql.gz" "/tmp/mintel_pull.sql.gz"
|
||||
|
||||
echo "🔄 Restoring database locally..."
|
||||
gunzip -c "/tmp/mintel_pull.sql.gz" | docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --quiet
|
||||
|
||||
sanitize_migrations "$LOCAL_DB_CONTAINER" "$LOCAL_DB_USER" "$LOCAL_DB_NAME" "false"
|
||||
|
||||
rm -f "/tmp/mintel_pull.sql.gz"
|
||||
ssh "$SSH_HOST" "rm -f /tmp/mintel_pull.sql.gz"
|
||||
|
||||
SYNC_SUCCESS="true"
|
||||
echo ""
|
||||
echo "✅ DB Pull from $TARGET complete! Restart dev server to see changes."
|
||||
}
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────
|
||||
if [ -z "$DIRECTION" ] || [ -z "$TARGET" ]; then
|
||||
echo "📦 CMS Data Sync Tool (mintel.me)"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " npm run cms:push:testing Push local DB → testing"
|
||||
echo " npm run cms:push:staging Push local DB → staging"
|
||||
echo " npm run cms:push:prod Push local DB → production"
|
||||
echo " npm run cms:pull:testing Pull testing DB → local"
|
||||
echo " npm run cms:pull:staging Pull staging DB → local"
|
||||
echo " npm run cms:pull:prod Pull production DB → local"
|
||||
echo ""
|
||||
echo "Safety: A backup is always created before overwriting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_target
|
||||
|
||||
case "$DIRECTION" in
|
||||
push) do_push ;;
|
||||
pull) do_pull ;;
|
||||
*)
|
||||
echo "❌ Unknown direction: $DIRECTION (use 'push' or 'pull')"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
41
apps/web/scripts/create-user.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "../payload.config";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: "users",
|
||||
where: { email: { equals: "marc@mintel.me" } },
|
||||
});
|
||||
|
||||
if (existing.totalDocs > 0) {
|
||||
console.log("User already exists, updating password...");
|
||||
await payload.update({
|
||||
collection: "users",
|
||||
where: { email: { equals: "marc@mintel.me" } },
|
||||
data: {
|
||||
password: "Tim300493.",
|
||||
},
|
||||
});
|
||||
console.log("Password updated.");
|
||||
} else {
|
||||
console.log("Creating user...");
|
||||
await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
email: "marc@mintel.me",
|
||||
password: "Tim300493.",
|
||||
},
|
||||
});
|
||||
console.log("User marc@mintel.me created.");
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Failed to create user:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
99
apps/web/scripts/download-thumbnails.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import dotenv from "dotenv";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const client = new S3Client({
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
async function downloadFile(key: string, localPath: string) {
|
||||
try {
|
||||
const bucket = process.env.S3_BUCKET || "mintel";
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
|
||||
if (response.Body) {
|
||||
const dir = path.dirname(localPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const stream = fs.createWriteStream(localPath);
|
||||
const reader = response.Body as any;
|
||||
|
||||
// Node.js stream handling
|
||||
if (typeof reader.pipe === "function") {
|
||||
reader.pipe(stream);
|
||||
} else {
|
||||
// Alternative for web streams if necessary, but in Node it should have pipe
|
||||
const arr = await response.Body.transformToByteArray();
|
||||
fs.writeFileSync(localPath, arr);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("finish", resolve);
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to download ${key}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function parseMatter(content: string) {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (!match) return { data: {}, content };
|
||||
const data: Record<string, any> = {};
|
||||
match[1].split("\n").forEach((line) => {
|
||||
const [key, ...rest] = line.split(":");
|
||||
if (key && rest.length) {
|
||||
const field = key.trim();
|
||||
let val = rest.join(":").trim();
|
||||
data[field] = val.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
});
|
||||
return { data, content: match[2].trim() };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const webDir = path.resolve(__dirname, "..");
|
||||
const contentDir = path.join(webDir, "content", "blog");
|
||||
const publicDir = path.join(webDir, "public");
|
||||
const prefix = `${process.env.S3_PREFIX || "mintel-me"}/media/`;
|
||||
|
||||
const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
|
||||
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(path.join(contentDir, file), "utf-8");
|
||||
const { data } = parseMatter(content);
|
||||
|
||||
if (data.thumbnail) {
|
||||
const fileName = path.basename(data.thumbnail);
|
||||
const s3Key = `${prefix}${fileName}`;
|
||||
const localPath = path.join(publicDir, data.thumbnail.replace(/^\//, ""));
|
||||
|
||||
console.log(`Downloading ${s3Key} to ${localPath}...`);
|
||||
await downloadFile(s3Key, localPath);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Downloads complete.");
|
||||
}
|
||||
|
||||
run();
|
||||
168
apps/web/scripts/import-leads.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import fs from "node:fs";
|
||||
import * as xlsxImport from "xlsx";
|
||||
const xlsx = (xlsxImport as any).default || xlsxImport;
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "../payload.config";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log("Initializing Payload...");
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
const filePath = "/Users/marcmintel/Downloads/Akquise_Branchen.xlsx";
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error("File not found:", filePath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Reading Excel file: ${filePath}`);
|
||||
const wb = xlsx.readFile(filePath);
|
||||
|
||||
let accountsCreated = 0;
|
||||
let contactsCreated = 0;
|
||||
|
||||
for (const sheetName of wb.SheetNames) {
|
||||
if (
|
||||
sheetName === "Weitere Kundenideen" ||
|
||||
sheetName.includes("BKF Firmen")
|
||||
)
|
||||
continue;
|
||||
|
||||
let industry = sheetName
|
||||
.replace(/^\d+_/, "")
|
||||
.replace(/^\d+\.\s*/, "")
|
||||
.replace(/_/g, " ");
|
||||
console.log(
|
||||
`\n--- Importing Sheet: ${sheetName} -> Industry: ${industry} ---`,
|
||||
);
|
||||
const rows = xlsx.utils.sheet_to_json(wb.Sheets[sheetName]);
|
||||
|
||||
for (const row of rows) {
|
||||
const companyName = row["Unternehmen"]?.trim();
|
||||
const website = row["Webseitenlink"]?.trim();
|
||||
let email = row["Emailadresse"]?.trim();
|
||||
const contactName = row["Ansprechpartner"]?.trim();
|
||||
const position = row["Position"]?.trim();
|
||||
const statusRaw = row["Webseiten-Status (alt/gut/schlecht)"]
|
||||
?.trim()
|
||||
?.toLowerCase();
|
||||
const notes = row["Notizen"]?.trim();
|
||||
|
||||
if (!companyName) continue;
|
||||
|
||||
let websiteStatus = "unknown";
|
||||
if (statusRaw === "gut") websiteStatus = "gut";
|
||||
else if (statusRaw === "ok" || statusRaw === "okay")
|
||||
websiteStatus = "ok";
|
||||
else if (
|
||||
statusRaw === "schlecht" ||
|
||||
statusRaw === "alt" ||
|
||||
statusRaw === "veraltet"
|
||||
)
|
||||
websiteStatus = "schlecht";
|
||||
|
||||
// Find or create account
|
||||
let accountId;
|
||||
const whereClause = website
|
||||
? { website: { equals: website } }
|
||||
: { name: { equals: companyName } };
|
||||
|
||||
const existingAccounts = await payload.find({
|
||||
collection: "crm-accounts",
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (existingAccounts.docs.length > 0) {
|
||||
accountId = existingAccounts.docs[0].id;
|
||||
console.log(`[SKIP] Account exists: ${companyName}`);
|
||||
} else {
|
||||
try {
|
||||
const newAccount = await payload.create({
|
||||
collection: "crm-accounts",
|
||||
data: {
|
||||
name: companyName,
|
||||
website: website || "",
|
||||
status: "lead",
|
||||
leadTemperature: "cold",
|
||||
industry,
|
||||
websiteStatus,
|
||||
notes,
|
||||
} as any,
|
||||
});
|
||||
accountId = newAccount.id;
|
||||
accountsCreated++;
|
||||
console.log(`[OK] Created account: ${companyName}`);
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
`[ERROR] Failed to create account ${companyName}:`,
|
||||
err.message,
|
||||
);
|
||||
continue; // Skip contact creation if account failed
|
||||
}
|
||||
}
|
||||
|
||||
// Handle contact
|
||||
if (email) {
|
||||
// Some rows have multiple emails or contacts. Let's just pick the first email if there are commas.
|
||||
if (email.includes(",")) email = email.split(",")[0].trim();
|
||||
|
||||
const existingContacts = await payload.find({
|
||||
collection: "crm-contacts",
|
||||
where: { email: { equals: email } },
|
||||
});
|
||||
|
||||
if (existingContacts.docs.length === 0) {
|
||||
let firstName = "Team";
|
||||
let lastName = companyName; // fallback
|
||||
|
||||
if (contactName) {
|
||||
// If multiple contacts are listed, just take the first one
|
||||
const firstContact = contactName.split(",")[0].trim();
|
||||
const parts = firstContact.split(" ");
|
||||
if (parts.length > 1) {
|
||||
lastName = parts.pop();
|
||||
firstName = parts.join(" ");
|
||||
} else {
|
||||
firstName = firstContact;
|
||||
lastName = "Contact";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await payload.create({
|
||||
collection: "crm-contacts",
|
||||
data: {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
role: position,
|
||||
account: accountId as any,
|
||||
},
|
||||
});
|
||||
contactsCreated++;
|
||||
console.log(` -> [OK] Created contact: ${email}`);
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
` -> [ERROR] Failed to create contact ${email}:`,
|
||||
err.message,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(` -> [SKIP] Contact exists: ${email}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nMigration completed successfully!`);
|
||||
console.log(
|
||||
`Created ${accountsCreated} Accounts and ${contactsCreated} Contacts.`,
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
console.error("Migration failed:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
44
apps/web/scripts/list-s3-media.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const client = new S3Client({
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const bucket = process.env.S3_BUCKET || "mintel";
|
||||
const prefix = `${process.env.S3_PREFIX || "mintel-me"}/media/`;
|
||||
|
||||
console.log(`Listing objects in bucket: ${bucket}, prefix: ${prefix}`);
|
||||
|
||||
const command = new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
});
|
||||
|
||||
const response = await client.send(command);
|
||||
|
||||
if (!response.Contents) {
|
||||
console.log("No objects found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${response.Contents.length} objects:`);
|
||||
response.Contents.forEach((obj) => {
|
||||
console.log(` - ${obj.Key} (${obj.Size} bytes)`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error listing S3 objects:", err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,156 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "../payload.config";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
|
||||
|
||||
function parseMatter(content: string) {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
if (!match) return { data: {}, content };
|
||||
const data: Record<string, any> = {};
|
||||
match[1].split("\n").forEach((line) => {
|
||||
const [key, ...rest] = line.split(":");
|
||||
if (key && rest.length) {
|
||||
const field = key.trim();
|
||||
let val = rest.join(":").trim();
|
||||
if (val.startsWith("[")) {
|
||||
// basic array parsing
|
||||
data[field] = val
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
||||
} else {
|
||||
data[field] = val.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
}
|
||||
});
|
||||
return { data, content: match[2].trim() };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const contentDir = path.join(process.cwd(), "content", "blog");
|
||||
const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(contentDir, file);
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const { data, content: body } = parseMatter(content);
|
||||
|
||||
const slug = file.replace(/\.mdx$/, "");
|
||||
console.log(`Migrating ${slug}...`);
|
||||
|
||||
try {
|
||||
const existing = await payload.find({
|
||||
collection: "posts",
|
||||
where: { slug: { equals: slug } },
|
||||
});
|
||||
|
||||
const lexicalBlocks = parseMarkdownToLexical(body);
|
||||
const lexicalAST = {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: lexicalBlocks,
|
||||
direction: "ltr",
|
||||
},
|
||||
};
|
||||
|
||||
// Handle thumbnail mapping
|
||||
let featuredImageId = null;
|
||||
if (data.thumbnail) {
|
||||
try {
|
||||
// Remove leading slash and find local file
|
||||
const localPath = path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
data.thumbnail.replace(/^\//, ""),
|
||||
);
|
||||
const fileName = path.basename(localPath);
|
||||
|
||||
if (fs.existsSync(localPath)) {
|
||||
// Check if media already exists in Payload
|
||||
const existingMedia = await payload.find({
|
||||
collection: "media",
|
||||
where: { filename: { equals: fileName } },
|
||||
});
|
||||
|
||||
if (existingMedia.docs.length > 0) {
|
||||
featuredImageId = existingMedia.docs[0].id;
|
||||
} else {
|
||||
// Upload new media item
|
||||
const fileData = fs.readFileSync(localPath);
|
||||
const { size } = fs.statSync(localPath);
|
||||
|
||||
const newMedia = await payload.create({
|
||||
collection: "media",
|
||||
data: {
|
||||
alt: data.title || fileName,
|
||||
},
|
||||
file: {
|
||||
data: fileData,
|
||||
name: fileName,
|
||||
mimetype: fileName.endsWith(".png")
|
||||
? "image/png"
|
||||
: fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")
|
||||
? "image/jpeg"
|
||||
: "image/webp",
|
||||
size,
|
||||
},
|
||||
});
|
||||
featuredImageId = newMedia.id;
|
||||
console.log(` ↑ Uploaded thumbnail: ${fileName}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
` ⚠ Warning: Could not process thumbnail ${data.thumbnail}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (existing.docs.length === 0) {
|
||||
await payload.create({
|
||||
collection: "posts",
|
||||
data: {
|
||||
title: data.title || slug,
|
||||
slug,
|
||||
description: data.description || "",
|
||||
date: data.date
|
||||
? new Date(data.date).toISOString()
|
||||
: new Date().toISOString(),
|
||||
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
||||
content: lexicalAST as any,
|
||||
featuredImage: featuredImageId,
|
||||
},
|
||||
});
|
||||
console.log(`✔ Inserted ${slug}`);
|
||||
} else {
|
||||
await payload.update({
|
||||
collection: "posts",
|
||||
id: existing.docs[0].id,
|
||||
data: {
|
||||
content: lexicalAST as any,
|
||||
featuredImage: featuredImageId,
|
||||
},
|
||||
});
|
||||
console.log(`✔ Updated AST and thumbnail for ${slug}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`✘ FAILED ${slug}: ${err.message}`);
|
||||
if (err.data?.errors) {
|
||||
console.error(
|
||||
` Validation errors:`,
|
||||
JSON.stringify(err.data.errors, null, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Migration complete.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
61
apps/web/scripts/restore-db.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Payload CMS Database Restore
|
||||
# Restores a backup created by backup-db.sh
|
||||
# Usage: pnpm run db:restore <backup-file>
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
# Load environment variables
|
||||
if [ -f ../../.env ]; then
|
||||
set -a; source ../../.env; set +a
|
||||
fi
|
||||
if [ -f .env ]; then
|
||||
set -a; source .env; set +a
|
||||
fi
|
||||
|
||||
DB_NAME="${postgres_DB_NAME:-payload}"
|
||||
DB_USER="${postgres_DB_USER:-payload}"
|
||||
DB_CONTAINER="mintel-me-postgres-db-1"
|
||||
|
||||
BACKUP_FILE="${1:-}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_DIR="${SCRIPT_DIR}/../../../../backups"
|
||||
|
||||
if [ -z "$BACKUP_FILE" ]; then
|
||||
echo "❌ Usage: pnpm run db:restore <backup-file>"
|
||||
echo ""
|
||||
echo "📋 Available backups in $BACKUP_DIR:"
|
||||
ls -lh "$BACKUP_DIR"/*.dump 2>/dev/null | awk '{print " " $NF " (" $5 ")"}' || echo " No backups found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "❌ Backup file not found: $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if container is running
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then
|
||||
echo "❌ Database container '$DB_CONTAINER' is not running."
|
||||
echo " Start it with: pnpm dev:docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "⚠️ WARNING: This will REPLACE ALL DATA in the '$DB_NAME' database!"
|
||||
echo " Backup file: $BACKUP_FILE"
|
||||
echo ""
|
||||
read -p "Are you sure? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔄 Restoring database from $BACKUP_FILE..."
|
||||
# Uses pg_restore for custom format dumps (-F c) produced by backup-db.sh
|
||||
cat "$BACKUP_FILE" | docker exec -i "$DB_CONTAINER" pg_restore -U "$DB_USER" -d "$DB_NAME" --clean --if-exists
|
||||
|
||||
echo "✅ Database restored successfully!"
|
||||
@@ -9,7 +9,40 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
let payload;
|
||||
let retries = 5;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
console.log(
|
||||
`Connecting to database (URI: ${process.env.DATABASE_URI || "default"})...`,
|
||||
);
|
||||
payload = await getPayload({ config: configPromise });
|
||||
break;
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.code === "ECONNREFUSED" ||
|
||||
e.code === "ENOTFOUND" ||
|
||||
e.message?.includes("ECONNREFUSED") ||
|
||||
e.message?.includes("ENOTFOUND") ||
|
||||
e.message?.includes("cannot connect to Postgres")
|
||||
) {
|
||||
console.log(
|
||||
`Database not ready (${e.code || "UNKNOWN"}), retrying in 3 seconds... (${retries} retries left)`,
|
||||
);
|
||||
retries--;
|
||||
await new Promise((res) => setTimeout(res, 3000));
|
||||
} else {
|
||||
console.error("Fatal connection error:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new Error(
|
||||
"Failed to connect to the database after multiple retries.",
|
||||
);
|
||||
}
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: "context-files",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
@@ -70,16 +71,11 @@ const AGBSection = ({
|
||||
);
|
||||
|
||||
interface AgbsPDFProps {
|
||||
headerIcon?: string;
|
||||
footerLogo?: string;
|
||||
mode?: "estimation" | "full";
|
||||
}
|
||||
|
||||
export const AgbsPDF = ({
|
||||
headerIcon,
|
||||
footerLogo,
|
||||
mode = "full",
|
||||
}: AgbsPDFProps) => {
|
||||
export const AgbsPDF = ({ footerLogo, mode = "full" }: AgbsPDFProps) => {
|
||||
const date = new Date().toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
@@ -215,8 +211,6 @@ export const AgbsPDF = ({
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
footerLogo={footerLogo}
|
||||
icon={headerIcon}
|
||||
pageNumber="10"
|
||||
showPageNumber={false}
|
||||
>
|
||||
{content}
|
||||
@@ -227,7 +221,12 @@ export const AgbsPDF = ({
|
||||
return (
|
||||
<PDFPage size="A4" style={pdfStyles.page}>
|
||||
<FoldingMarks />
|
||||
<Header icon={headerIcon} showAddress={false} />
|
||||
<Header
|
||||
icon={""}
|
||||
showAddress={false}
|
||||
sender={companyData as any}
|
||||
recipient={{} as any}
|
||||
/>
|
||||
{content}
|
||||
<Footer
|
||||
logo={footerLogo}
|
||||
|
||||
@@ -47,8 +47,7 @@ export const CombinedQuotePDF = ({
|
||||
};
|
||||
|
||||
const layoutProps = {
|
||||
date,
|
||||
icon: estimationProps.headerIcon,
|
||||
headerIcon: estimationProps.headerIcon,
|
||||
footerLogo: estimationProps.footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
@@ -73,7 +72,7 @@ export const CombinedQuotePDF = ({
|
||||
footerLogo={estimationProps.footerLogo}
|
||||
/>
|
||||
)}
|
||||
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
|
||||
<SimpleLayout {...layoutProps} showPageNumber={false}>
|
||||
<ClosingModule />
|
||||
</SimpleLayout>
|
||||
</PDFDocument>
|
||||
|
||||
@@ -77,12 +77,17 @@ export const LocalEstimationPDF = ({
|
||||
ustId: "DE367588065",
|
||||
};
|
||||
|
||||
const bankData = {
|
||||
name: "N26",
|
||||
bic: "NTSBDEB1XXX",
|
||||
iban: "DE50 1001 1001 2620 4328 65",
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
state,
|
||||
date,
|
||||
icon: headerIcon,
|
||||
headerIcon: headerIcon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
};
|
||||
|
||||
let pageCounter = 1;
|
||||
@@ -103,12 +108,12 @@ export const LocalEstimationPDF = ({
|
||||
{/* BriefingModule Page REMOVED as per user request ("die zweite seite ist leer, weg damit") */}
|
||||
|
||||
{state.sitemap && state.sitemap.length > 0 && (
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps} showPageNumber={false}>
|
||||
<SitemapModule state={state} />
|
||||
</SimpleLayout>
|
||||
)}
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps} showPageNumber={false}>
|
||||
<EstimationModule
|
||||
state={state}
|
||||
positions={positions}
|
||||
@@ -117,11 +122,11 @@ export const LocalEstimationPDF = ({
|
||||
/>
|
||||
</SimpleLayout>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps} showPageNumber={false}>
|
||||
<TransparenzModule pricing={pricing} />
|
||||
</SimpleLayout>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps} showPageNumber={false}>
|
||||
<ClosingModule />
|
||||
</SimpleLayout>
|
||||
</PDFDocument>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { calculatePositions as logicCalculatePositions } from "@mintel/pdf";
|
||||
import { FormState } from "./types";
|
||||
|
||||
// @ts-ignore
|
||||
export type { Position } from "@mintel/pdf";
|
||||
|
||||
export const calculatePositions = (state: FormState, pricing: any) =>
|
||||
|
||||
@@ -130,11 +130,7 @@ const jsxConverters: JSXConverters = {
|
||||
<mdxComponents.IconList>
|
||||
{node.fields.items?.map((item: any, i: number) => (
|
||||
// @ts-ignore
|
||||
<mdxComponents.IconListItem
|
||||
key={i}
|
||||
icon={item.icon || "check"}
|
||||
title={item.title}
|
||||
>
|
||||
<mdxComponents.IconListItem key={i} icon={item.icon || "check"}>
|
||||
{item.description}
|
||||
</mdxComponents.IconListItem>
|
||||
))}
|
||||
|
||||
2868
apps/web/src/migrations/20260227_171023_crm_collections.json
Normal file
392
apps/web/src/migrations/20260227_171023_crm_collections.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_posts_status" AS ENUM('draft', 'published');
|
||||
CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published');
|
||||
CREATE TYPE "public"."enum_crm_accounts_status" AS ENUM('lead', 'client', 'lost');
|
||||
CREATE TYPE "public"."enum_crm_accounts_lead_temperature" AS ENUM('cold', 'warm', 'hot');
|
||||
CREATE TYPE "public"."enum_crm_interactions_type" AS ENUM('email', 'call', 'meeting', 'note');
|
||||
CREATE TYPE "public"."enum_crm_interactions_direction" AS ENUM('inbound', 'outbound');
|
||||
CREATE TABLE "users_sessions" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"created_at" timestamp(3) with time zone,
|
||||
"expires_at" timestamp(3) with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "users" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"reset_password_token" varchar,
|
||||
"reset_password_expiration" timestamp(3) with time zone,
|
||||
"salt" varchar,
|
||||
"hash" varchar,
|
||||
"login_attempts" numeric DEFAULT 0,
|
||||
"lock_until" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
CREATE TABLE "media" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"alt" varchar NOT NULL,
|
||||
"prefix" varchar DEFAULT 'mintel-me/media',
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"url" varchar,
|
||||
"thumbnail_u_r_l" varchar,
|
||||
"filename" varchar,
|
||||
"mime_type" varchar,
|
||||
"filesize" numeric,
|
||||
"width" numeric,
|
||||
"height" numeric,
|
||||
"focal_x" numeric,
|
||||
"focal_y" numeric,
|
||||
"sizes_thumbnail_url" varchar,
|
||||
"sizes_thumbnail_width" numeric,
|
||||
"sizes_thumbnail_height" numeric,
|
||||
"sizes_thumbnail_mime_type" varchar,
|
||||
"sizes_thumbnail_filesize" numeric,
|
||||
"sizes_thumbnail_filename" varchar,
|
||||
"sizes_card_url" varchar,
|
||||
"sizes_card_width" numeric,
|
||||
"sizes_card_height" numeric,
|
||||
"sizes_card_mime_type" varchar,
|
||||
"sizes_card_filesize" numeric,
|
||||
"sizes_card_filename" varchar,
|
||||
"sizes_tablet_url" varchar,
|
||||
"sizes_tablet_width" numeric,
|
||||
"sizes_tablet_height" numeric,
|
||||
"sizes_tablet_mime_type" varchar,
|
||||
"sizes_tablet_filesize" numeric,
|
||||
"sizes_tablet_filename" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "posts_tags" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"tag" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "posts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar,
|
||||
"slug" varchar,
|
||||
"description" varchar,
|
||||
"date" timestamp(3) with time zone,
|
||||
"featured_image_id" integer,
|
||||
"content" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"_status" "enum_posts_status" DEFAULT 'draft'
|
||||
);
|
||||
|
||||
CREATE TABLE "_posts_v_version_tags" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"tag" varchar,
|
||||
"_uuid" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "_posts_v" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"parent_id" integer,
|
||||
"version_title" varchar,
|
||||
"version_slug" varchar,
|
||||
"version_description" varchar,
|
||||
"version_date" timestamp(3) with time zone,
|
||||
"version_featured_image_id" integer,
|
||||
"version_content" jsonb,
|
||||
"version_updated_at" timestamp(3) with time zone,
|
||||
"version_created_at" timestamp(3) with time zone,
|
||||
"version__status" "enum__posts_v_version_status" DEFAULT 'draft',
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"latest" boolean
|
||||
);
|
||||
|
||||
CREATE TABLE "inquiries" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"company_name" varchar,
|
||||
"project_type" varchar,
|
||||
"message" varchar,
|
||||
"is_free_text" boolean DEFAULT false,
|
||||
"config" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "redirects" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"from" varchar NOT NULL,
|
||||
"to" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "context_files" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"filename" varchar NOT NULL,
|
||||
"content" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_accounts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"website" varchar,
|
||||
"status" "enum_crm_accounts_status" DEFAULT 'lead',
|
||||
"lead_temperature" "enum_crm_accounts_lead_temperature",
|
||||
"assigned_to_id" integer,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_accounts_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_contacts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"first_name" varchar NOT NULL,
|
||||
"last_name" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"phone" varchar,
|
||||
"linked_in" varchar,
|
||||
"role" varchar,
|
||||
"account_id" integer,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_interactions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"type" "enum_crm_interactions_type" DEFAULT 'email' NOT NULL,
|
||||
"direction" "enum_crm_interactions_direction",
|
||||
"date" timestamp(3) with time zone NOT NULL,
|
||||
"contact_id" integer,
|
||||
"account_id" integer,
|
||||
"subject" varchar NOT NULL,
|
||||
"content" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_kv" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar NOT NULL,
|
||||
"data" jsonb NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_locked_documents" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"global_slug" varchar,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_locked_documents_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer,
|
||||
"media_id" integer,
|
||||
"posts_id" integer,
|
||||
"inquiries_id" integer,
|
||||
"redirects_id" integer,
|
||||
"context_files_id" integer,
|
||||
"crm_accounts_id" integer,
|
||||
"crm_contacts_id" integer,
|
||||
"crm_interactions_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar,
|
||||
"value" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_migrations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"batch" numeric,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ai_settings_custom_sources" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"source_name" varchar NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ai_settings" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone,
|
||||
"created_at" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "posts_tags" ADD CONSTRAINT "posts_tags_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "posts" ADD CONSTRAINT "posts_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v_version_tags" ADD CONSTRAINT "_posts_v_version_tags_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_posts_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts" ADD CONSTRAINT "crm_accounts_assigned_to_id_users_id_fk" FOREIGN KEY ("assigned_to_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts_rels" ADD CONSTRAINT "crm_accounts_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."crm_accounts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts_rels" ADD CONSTRAINT "crm_accounts_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_contacts" ADD CONSTRAINT "crm_contacts_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_contact_id_crm_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."crm_contacts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_inquiries_fk" FOREIGN KEY ("inquiries_id") REFERENCES "public"."inquiries"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_redirects_fk" FOREIGN KEY ("redirects_id") REFERENCES "public"."redirects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_context_files_fk" FOREIGN KEY ("context_files_id") REFERENCES "public"."context_files"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_accounts_fk" FOREIGN KEY ("crm_accounts_id") REFERENCES "public"."crm_accounts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_contacts_fk" FOREIGN KEY ("crm_contacts_id") REFERENCES "public"."crm_contacts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_interactions_fk" FOREIGN KEY ("crm_interactions_id") REFERENCES "public"."crm_interactions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "ai_settings_custom_sources" ADD CONSTRAINT "ai_settings_custom_sources_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."ai_settings"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
|
||||
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
|
||||
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||
CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
|
||||
CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
|
||||
CREATE INDEX "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "media" USING btree ("sizes_thumbnail_filename");
|
||||
CREATE INDEX "media_sizes_card_sizes_card_filename_idx" ON "media" USING btree ("sizes_card_filename");
|
||||
CREATE INDEX "media_sizes_tablet_sizes_tablet_filename_idx" ON "media" USING btree ("sizes_tablet_filename");
|
||||
CREATE INDEX "posts_tags_order_idx" ON "posts_tags" USING btree ("_order");
|
||||
CREATE INDEX "posts_tags_parent_id_idx" ON "posts_tags" USING btree ("_parent_id");
|
||||
CREATE UNIQUE INDEX "posts_slug_idx" ON "posts" USING btree ("slug");
|
||||
CREATE INDEX "posts_featured_image_idx" ON "posts" USING btree ("featured_image_id");
|
||||
CREATE INDEX "posts_updated_at_idx" ON "posts" USING btree ("updated_at");
|
||||
CREATE INDEX "posts_created_at_idx" ON "posts" USING btree ("created_at");
|
||||
CREATE INDEX "posts__status_idx" ON "posts" USING btree ("_status");
|
||||
CREATE INDEX "_posts_v_version_tags_order_idx" ON "_posts_v_version_tags" USING btree ("_order");
|
||||
CREATE INDEX "_posts_v_version_tags_parent_id_idx" ON "_posts_v_version_tags" USING btree ("_parent_id");
|
||||
CREATE INDEX "_posts_v_parent_idx" ON "_posts_v" USING btree ("parent_id");
|
||||
CREATE INDEX "_posts_v_version_version_slug_idx" ON "_posts_v" USING btree ("version_slug");
|
||||
CREATE INDEX "_posts_v_version_version_featured_image_idx" ON "_posts_v" USING btree ("version_featured_image_id");
|
||||
CREATE INDEX "_posts_v_version_version_updated_at_idx" ON "_posts_v" USING btree ("version_updated_at");
|
||||
CREATE INDEX "_posts_v_version_version_created_at_idx" ON "_posts_v" USING btree ("version_created_at");
|
||||
CREATE INDEX "_posts_v_version_version__status_idx" ON "_posts_v" USING btree ("version__status");
|
||||
CREATE INDEX "_posts_v_created_at_idx" ON "_posts_v" USING btree ("created_at");
|
||||
CREATE INDEX "_posts_v_updated_at_idx" ON "_posts_v" USING btree ("updated_at");
|
||||
CREATE INDEX "_posts_v_latest_idx" ON "_posts_v" USING btree ("latest");
|
||||
CREATE INDEX "inquiries_updated_at_idx" ON "inquiries" USING btree ("updated_at");
|
||||
CREATE INDEX "inquiries_created_at_idx" ON "inquiries" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "redirects_from_idx" ON "redirects" USING btree ("from");
|
||||
CREATE INDEX "redirects_updated_at_idx" ON "redirects" USING btree ("updated_at");
|
||||
CREATE INDEX "redirects_created_at_idx" ON "redirects" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "context_files_filename_idx" ON "context_files" USING btree ("filename");
|
||||
CREATE INDEX "context_files_updated_at_idx" ON "context_files" USING btree ("updated_at");
|
||||
CREATE INDEX "context_files_created_at_idx" ON "context_files" USING btree ("created_at");
|
||||
CREATE INDEX "crm_accounts_assigned_to_idx" ON "crm_accounts" USING btree ("assigned_to_id");
|
||||
CREATE INDEX "crm_accounts_updated_at_idx" ON "crm_accounts" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_accounts_created_at_idx" ON "crm_accounts" USING btree ("created_at");
|
||||
CREATE INDEX "crm_accounts_rels_order_idx" ON "crm_accounts_rels" USING btree ("order");
|
||||
CREATE INDEX "crm_accounts_rels_parent_idx" ON "crm_accounts_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "crm_accounts_rels_path_idx" ON "crm_accounts_rels" USING btree ("path");
|
||||
CREATE INDEX "crm_accounts_rels_media_id_idx" ON "crm_accounts_rels" USING btree ("media_id");
|
||||
CREATE UNIQUE INDEX "crm_contacts_email_idx" ON "crm_contacts" USING btree ("email");
|
||||
CREATE INDEX "crm_contacts_account_idx" ON "crm_contacts" USING btree ("account_id");
|
||||
CREATE INDEX "crm_contacts_updated_at_idx" ON "crm_contacts" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_contacts_created_at_idx" ON "crm_contacts" USING btree ("created_at");
|
||||
CREATE INDEX "crm_interactions_contact_idx" ON "crm_interactions" USING btree ("contact_id");
|
||||
CREATE INDEX "crm_interactions_account_idx" ON "crm_interactions" USING btree ("account_id");
|
||||
CREATE INDEX "crm_interactions_updated_at_idx" ON "crm_interactions" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_interactions_created_at_idx" ON "crm_interactions" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
|
||||
CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
|
||||
CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
|
||||
CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
|
||||
CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
|
||||
CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_posts_id_idx" ON "payload_locked_documents_rels" USING btree ("posts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_inquiries_id_idx" ON "payload_locked_documents_rels" USING btree ("inquiries_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_redirects_id_idx" ON "payload_locked_documents_rels" USING btree ("redirects_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_context_files_id_idx" ON "payload_locked_documents_rels" USING btree ("context_files_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_accounts_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_accounts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_contacts_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_contacts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_interactions_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_interactions_id");
|
||||
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
|
||||
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
|
||||
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
|
||||
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
|
||||
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
|
||||
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");
|
||||
CREATE INDEX "ai_settings_custom_sources_order_idx" ON "ai_settings_custom_sources" USING btree ("_order");
|
||||
CREATE INDEX "ai_settings_custom_sources_parent_id_idx" ON "ai_settings_custom_sources" USING btree ("_parent_id");`);
|
||||
}
|
||||
|
||||
export async function down({
|
||||
db,
|
||||
payload,
|
||||
req,
|
||||
}: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "users_sessions" CASCADE;
|
||||
DROP TABLE "users" CASCADE;
|
||||
DROP TABLE "media" CASCADE;
|
||||
DROP TABLE "posts_tags" CASCADE;
|
||||
DROP TABLE "posts" CASCADE;
|
||||
DROP TABLE "_posts_v_version_tags" CASCADE;
|
||||
DROP TABLE "_posts_v" CASCADE;
|
||||
DROP TABLE "inquiries" CASCADE;
|
||||
DROP TABLE "redirects" CASCADE;
|
||||
DROP TABLE "context_files" CASCADE;
|
||||
DROP TABLE "crm_accounts" CASCADE;
|
||||
DROP TABLE "crm_accounts_rels" CASCADE;
|
||||
DROP TABLE "crm_contacts" CASCADE;
|
||||
DROP TABLE "crm_interactions" CASCADE;
|
||||
DROP TABLE "payload_kv" CASCADE;
|
||||
DROP TABLE "payload_locked_documents" CASCADE;
|
||||
DROP TABLE "payload_locked_documents_rels" CASCADE;
|
||||
DROP TABLE "payload_preferences" CASCADE;
|
||||
DROP TABLE "payload_preferences_rels" CASCADE;
|
||||
DROP TABLE "payload_migrations" CASCADE;
|
||||
DROP TABLE "ai_settings_custom_sources" CASCADE;
|
||||
DROP TABLE "ai_settings" CASCADE;
|
||||
DROP TYPE "public"."enum_posts_status";
|
||||
DROP TYPE "public"."enum__posts_v_version_status";
|
||||
DROP TYPE "public"."enum_crm_accounts_status";
|
||||
DROP TYPE "public"."enum_crm_accounts_lead_temperature";
|
||||
DROP TYPE "public"."enum_crm_interactions_type";
|
||||
DROP TYPE "public"."enum_crm_interactions_direction";`);
|
||||
}
|
||||
3671
apps/web/src/migrations/20260301_151838.json
Normal file
155
apps/web/src/migrations/20260301_151838.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_crm_topics_status" AS ENUM('active', 'paused', 'won', 'lost');
|
||||
CREATE TYPE "public"."enum_crm_topics_stage" AS ENUM('discovery', 'proposal', 'negotiation', 'implementation');
|
||||
CREATE TYPE "public"."enum_projects_milestones_status" AS ENUM('todo', 'in_progress', 'done');
|
||||
CREATE TYPE "public"."enum_projects_milestones_priority" AS ENUM('low', 'medium', 'high');
|
||||
CREATE TYPE "public"."enum_projects_status" AS ENUM('draft', 'in_progress', 'review', 'completed');
|
||||
ALTER TYPE "public"."enum_crm_accounts_status" ADD VALUE 'partner' BEFORE 'lost';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'whatsapp' BEFORE 'note';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'social' BEFORE 'note';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'document' BEFORE 'note';
|
||||
CREATE TABLE "crm_topics" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"account_id" integer NOT NULL,
|
||||
"status" "enum_crm_topics_status" DEFAULT 'active' NOT NULL,
|
||||
"stage" "enum_crm_topics_stage",
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_interactions_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "projects_milestones" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"status" "enum_projects_milestones_status" DEFAULT 'todo' NOT NULL,
|
||||
"priority" "enum_projects_milestones_priority" DEFAULT 'medium',
|
||||
"start_date" timestamp(3) with time zone,
|
||||
"target_date" timestamp(3) with time zone,
|
||||
"assignee_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"account_id" integer NOT NULL,
|
||||
"status" "enum_projects_status" DEFAULT 'draft' NOT NULL,
|
||||
"start_date" timestamp(3) with time zone,
|
||||
"target_date" timestamp(3) with time zone,
|
||||
"value_min" numeric,
|
||||
"value_max" numeric,
|
||||
"briefing" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "projects_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"crm_contacts_id" integer,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'note';
|
||||
ALTER TABLE "inquiries" ADD COLUMN "processed" boolean DEFAULT false;
|
||||
ALTER TABLE "crm_contacts" ADD COLUMN "full_name" varchar;
|
||||
ALTER TABLE "crm_interactions" ADD COLUMN "topic_id" integer;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "crm_topics_id" integer;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "projects_id" integer;
|
||||
ALTER TABLE "crm_topics" ADD CONSTRAINT "crm_topics_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions_rels" ADD CONSTRAINT "crm_interactions_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."crm_interactions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions_rels" ADD CONSTRAINT "crm_interactions_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_milestones" ADD CONSTRAINT "projects_milestones_assignee_id_users_id_fk" FOREIGN KEY ("assignee_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "projects_milestones" ADD CONSTRAINT "projects_milestones_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_crm_contacts_fk" FOREIGN KEY ("crm_contacts_id") REFERENCES "public"."crm_contacts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "crm_topics_account_idx" ON "crm_topics" USING btree ("account_id");
|
||||
CREATE INDEX "crm_topics_updated_at_idx" ON "crm_topics" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_topics_created_at_idx" ON "crm_topics" USING btree ("created_at");
|
||||
CREATE INDEX "crm_interactions_rels_order_idx" ON "crm_interactions_rels" USING btree ("order");
|
||||
CREATE INDEX "crm_interactions_rels_parent_idx" ON "crm_interactions_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "crm_interactions_rels_path_idx" ON "crm_interactions_rels" USING btree ("path");
|
||||
CREATE INDEX "crm_interactions_rels_media_id_idx" ON "crm_interactions_rels" USING btree ("media_id");
|
||||
CREATE INDEX "projects_milestones_order_idx" ON "projects_milestones" USING btree ("_order");
|
||||
CREATE INDEX "projects_milestones_parent_id_idx" ON "projects_milestones" USING btree ("_parent_id");
|
||||
CREATE INDEX "projects_milestones_assignee_idx" ON "projects_milestones" USING btree ("assignee_id");
|
||||
CREATE INDEX "projects_account_idx" ON "projects" USING btree ("account_id");
|
||||
CREATE INDEX "projects_updated_at_idx" ON "projects" USING btree ("updated_at");
|
||||
CREATE INDEX "projects_created_at_idx" ON "projects" USING btree ("created_at");
|
||||
CREATE INDEX "projects_rels_order_idx" ON "projects_rels" USING btree ("order");
|
||||
CREATE INDEX "projects_rels_parent_idx" ON "projects_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "projects_rels_path_idx" ON "projects_rels" USING btree ("path");
|
||||
CREATE INDEX "projects_rels_crm_contacts_id_idx" ON "projects_rels" USING btree ("crm_contacts_id");
|
||||
CREATE INDEX "projects_rels_media_id_idx" ON "projects_rels" USING btree ("media_id");
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_topic_id_crm_topics_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."crm_topics"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_topics_fk" FOREIGN KEY ("crm_topics_id") REFERENCES "public"."crm_topics"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_projects_fk" FOREIGN KEY ("projects_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "crm_interactions_topic_idx" ON "crm_interactions" USING btree ("topic_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_topics_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_topics_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_projects_id_idx" ON "payload_locked_documents_rels" USING btree ("projects_id");`);
|
||||
}
|
||||
|
||||
export async function down({
|
||||
db,
|
||||
payload,
|
||||
req,
|
||||
}: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "crm_topics" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "crm_interactions_rels" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects_milestones" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects_rels" DISABLE ROW LEVEL SECURITY;
|
||||
DROP TABLE "crm_topics" CASCADE;
|
||||
DROP TABLE "crm_interactions_rels" CASCADE;
|
||||
DROP TABLE "projects_milestones" CASCADE;
|
||||
DROP TABLE "projects" CASCADE;
|
||||
DROP TABLE "projects_rels" CASCADE;
|
||||
ALTER TABLE "crm_interactions" DROP CONSTRAINT "crm_interactions_topic_id_crm_topics_id_fk";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_crm_topics_fk";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_projects_fk";
|
||||
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DATA TYPE text;
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DEFAULT 'lead'::text;
|
||||
DROP TYPE "public"."enum_crm_accounts_status";
|
||||
CREATE TYPE "public"."enum_crm_accounts_status" AS ENUM('lead', 'client', 'lost');
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DEFAULT 'lead'::"public"."enum_crm_accounts_status";
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DATA TYPE "public"."enum_crm_accounts_status" USING "status"::"public"."enum_crm_accounts_status";
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DATA TYPE text;
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'email'::text;
|
||||
DROP TYPE "public"."enum_crm_interactions_type";
|
||||
CREATE TYPE "public"."enum_crm_interactions_type" AS ENUM('email', 'call', 'meeting', 'note');
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'email'::"public"."enum_crm_interactions_type";
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DATA TYPE "public"."enum_crm_interactions_type" USING "type"::"public"."enum_crm_interactions_type";
|
||||
DROP INDEX "crm_interactions_topic_idx";
|
||||
DROP INDEX "payload_locked_documents_rels_crm_topics_id_idx";
|
||||
DROP INDEX "payload_locked_documents_rels_projects_id_idx";
|
||||
ALTER TABLE "inquiries" DROP COLUMN "processed";
|
||||
ALTER TABLE "crm_contacts" DROP COLUMN "full_name";
|
||||
ALTER TABLE "crm_interactions" DROP COLUMN "topic_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "crm_topics_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "projects_id";
|
||||
DROP TYPE "public"."enum_crm_topics_status";
|
||||
DROP TYPE "public"."enum_crm_topics_stage";
|
||||
DROP TYPE "public"."enum_projects_milestones_status";
|
||||
DROP TYPE "public"."enum_projects_milestones_priority";
|
||||
DROP TYPE "public"."enum_projects_status";`);
|
||||
}
|
||||
15
apps/web/src/migrations/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as migration_20260227_171023_crm_collections from "./20260227_171023_crm_collections";
|
||||
import * as migration_20260301_151838 from "./20260301_151838";
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20260227_171023_crm_collections.up,
|
||||
down: migration_20260227_171023_crm_collections.down,
|
||||
name: "20260227_171023_crm_collections",
|
||||
},
|
||||
{
|
||||
up: migration_20260301_151838.up,
|
||||
down: migration_20260301_151838.down,
|
||||
name: "20260301_151838",
|
||||
},
|
||||
];
|
||||
@@ -1,191 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { config } from "../../../content-engine.config";
|
||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||
import configPromise from "@payload-config";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
|
||||
async function getOrchestrator() {
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
throw new Error(
|
||||
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
|
||||
);
|
||||
}
|
||||
|
||||
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
|
||||
return new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateSlugAction(
|
||||
title: string,
|
||||
draftContent: string,
|
||||
oldSlug?: string,
|
||||
instructions?: string,
|
||||
) {
|
||||
try {
|
||||
const orchestrator = await getOrchestrator();
|
||||
const newSlug = await orchestrator.generateSlug(
|
||||
draftContent,
|
||||
title,
|
||||
instructions,
|
||||
);
|
||||
|
||||
if (oldSlug && oldSlug !== newSlug) {
|
||||
const payload = await getPayloadHMR({ config: configPromise });
|
||||
await payload.create({
|
||||
collection: "redirects",
|
||||
data: {
|
||||
from: oldSlug,
|
||||
to: newSlug,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, slug: newSlug };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateThumbnailAction(
|
||||
draftContent: string,
|
||||
title?: string,
|
||||
instructions?: string,
|
||||
) {
|
||||
try {
|
||||
const payload = await getPayloadHMR({ config: configPromise });
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||
}
|
||||
if (!REPLICATE_KEY) {
|
||||
throw new Error(
|
||||
"Missing REPLICATE_API_KEY in .env (Required for Thumbnails)",
|
||||
);
|
||||
}
|
||||
|
||||
const importDynamic = new Function(
|
||||
"modulePath",
|
||||
"return import(modulePath)",
|
||||
);
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
const { ThumbnailGenerator } = await importDynamic(
|
||||
"@mintel/thumbnail-generator",
|
||||
);
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
|
||||
|
||||
const prompt = await orchestrator.generateVisualPrompt(
|
||||
draftContent || title || "Technology",
|
||||
instructions,
|
||||
);
|
||||
|
||||
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
|
||||
await tg.generateImage(prompt, tmpPath);
|
||||
|
||||
const fileData = await fs.readFile(tmpPath);
|
||||
const stat = await fs.stat(tmpPath);
|
||||
const fileName = path.basename(tmpPath);
|
||||
|
||||
const newMedia = await payload.create({
|
||||
collection: "media",
|
||||
data: {
|
||||
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||
},
|
||||
file: {
|
||||
data: fileData,
|
||||
name: fileName,
|
||||
mimetype: "image/png",
|
||||
size: stat.size,
|
||||
},
|
||||
});
|
||||
|
||||
// Cleanup temp file
|
||||
await fs.unlink(tmpPath).catch(() => {});
|
||||
|
||||
return { success: true, mediaId: newMedia.id };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
export async function generateSingleFieldAction(
|
||||
documentTitle: string,
|
||||
documentContent: string,
|
||||
fieldName: string,
|
||||
fieldDescription: string,
|
||||
instructions?: string,
|
||||
) {
|
||||
try {
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||
|
||||
const payload = await getPayloadHMR({ config: configPromise });
|
||||
|
||||
// Fetch context documents from DB
|
||||
const contextDocsData = await payload.find({
|
||||
collection: "context-files",
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs
|
||||
.map((doc) => `--- ${doc.filename} ---\n${doc.content}`)
|
||||
.join("\n\n");
|
||||
|
||||
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
|
||||
PROJECT STRATEGY & CONTEXT:
|
||||
${projectContext}
|
||||
|
||||
DOCUMENT TITLE: ${documentTitle}
|
||||
DOCUMENT DRAFT:\n${documentContent}\n
|
||||
YOUR TASK: Generate the exact value for a specific field named "${fieldName}".
|
||||
${fieldDescription ? `FIELD DESCRIPTION / CONSTRAINTS: ${fieldDescription}\n` : ""}
|
||||
${instructions ? `EDITOR INSTRUCTIONS for this field: ${instructions}\n` : ""}
|
||||
CRITICAL RULES:
|
||||
1. Respond ONLY with the requested content value.
|
||||
2. NO markdown wrapping blocks (like \`\`\`mermaid or \`\`\`html) around the output! Just the raw code or text.
|
||||
3. If the field implies a diagram or flow, output RAW Mermaid.js code.
|
||||
4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`;
|
||||
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "google/gemini-3-flash-preview",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
return { success: true, text };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { config } from "../../../content-engine.config";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { parseMarkdownToLexical } from "../utils/lexicalParser";
|
||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
export async function optimizePostText(
|
||||
draftContent: string,
|
||||
instructions?: string,
|
||||
) {
|
||||
try {
|
||||
const payload = await getPayloadHMR({ config: configPromise });
|
||||
const globalAiSettings = await payload.findGlobal({ slug: "ai-settings" });
|
||||
const customSources =
|
||||
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
throw new Error(
|
||||
"OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.",
|
||||
);
|
||||
}
|
||||
|
||||
const importDynamic = new Function(
|
||||
"modulePath",
|
||||
"return import(modulePath)",
|
||||
);
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
// Fetch context documents purely from DB
|
||||
const contextDocsData = await payload.find({
|
||||
collection: "context-files",
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs.map((doc) => doc.content);
|
||||
|
||||
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||
content: draftContent,
|
||||
projectContext,
|
||||
availableComponents: config.components,
|
||||
instructions,
|
||||
internalLinks: [],
|
||||
customSources,
|
||||
});
|
||||
|
||||
// The orchestrator currently returns Markdown + JSX tags.
|
||||
// We convert this mixed string into a basic Lexical AST map.
|
||||
|
||||
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||
throw new Error("AI returned invalid markup.");
|
||||
}
|
||||
|
||||
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
lexicalAST: {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: "ltr",
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("Failed to optimize post:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || "An unknown error occurred during optimization.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
@@ -37,7 +37,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
@@ -49,7 +49,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ArticleMemeBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für alt ein.",
|
||||
@@ -44,7 +44,7 @@ export const ArticleMemeBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
@@ -39,7 +39,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
@@ -51,7 +51,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
@@ -63,7 +63,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
@@ -75,7 +75,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für sourceUrl ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
description: "e.g. 53% or 2.5M€",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -38,7 +38,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -50,7 +50,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
@@ -62,7 +62,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für sourceUrl ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ButtonBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -35,7 +35,7 @@ export const CarouselBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
description: "Optional overarching description for the comparison.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeLabel ein.",
|
||||
@@ -57,7 +57,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeText ein.",
|
||||
@@ -71,7 +71,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für positiveLabel ein.",
|
||||
@@ -84,7 +84,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für positiveText ein.",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DiagramFlowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DiagramGanttBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DiagramPieBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const DiagramSequenceBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DiagramStateBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const DiagramTimelineBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ExternalLinkBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const H2Block: MintelBlock = {
|
||||
description: "Geben Sie den Text für die H2-Überschrift ein.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const H3Block: MintelBlock = {
|
||||
description: "Geben Sie den Text für die H3-Überschrift ein.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const HeadingBlock: MintelBlock = {
|
||||
description: "Der Text der Überschrift.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export const IconListBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
@@ -56,7 +56,7 @@ export const IconListBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für description ein.",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ImageTextBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
description: "The strong headline for the Call-to-Action",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
description: "The value proposition text.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -53,7 +53,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für buttonText ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const LeadParagraphBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
|
||||