From 8f32c80801720e6ce089f092c69824c80ebe3b9c Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 16 Feb 2026 18:10:52 +0100 Subject: [PATCH] chore: optimize cms startup, refactor scripts and implement real-time dev mode --- docker-compose.yml | 4 + package.json | 15 +- packages/acquisition-manager/package.json | 6 +- packages/acquisition-manager/src/module.vue | 53 ++- packages/acquisition/build.mjs | 10 +- packages/acquisition/package.json | 2 +- packages/acquisition/src/index.ts | 356 ++++++++++------- packages/cli/src/index.ts | 2 +- packages/cloner-library/build.mjs | 4 +- packages/cloner-library/src/AssetManager.ts | 151 +++---- packages/cloner-library/src/PageCloner.ts | 400 +++++++++++-------- packages/cloner-library/src/WebsiteCloner.ts | 223 ++++++----- packages/cms-infra/database/data.db | Bin 352256 -> 360448 bytes packages/cms-infra/docker-compose.yml | 4 +- packages/cms-infra/package.json | 5 +- packages/company-manager/package.json | 6 +- packages/company-manager/src/module.vue | 9 +- packages/customer-manager/package.json | 6 +- packages/customer-manager/src/module.vue | 139 ++++++- packages/eslint-config/next.js | 55 ++- packages/feedback-commander/package.json | 2 +- packages/gatekeeper/postcss.config.cjs | 1 - packages/gatekeeper/tailwind.config.cjs | 1 - packages/husky-config/lint-staged.js | 1 - packages/infra/scripts/sync-directus.sh | 4 +- packages/people-manager/package.json | 6 +- packages/people-manager/src/module.vue | 9 +- packages/unified-dashboard/package.json | 2 +- packages/unified-dashboard/src/module.vue | 18 +- pnpm-lock.yaml | 148 ++++++- scripts/cms-up.sh | 27 ++ scripts/patch-cms.sh | 66 +++ scripts/sync-extensions.sh | 68 ++-- 33 files changed, 1185 insertions(+), 618 deletions(-) create mode 100755 scripts/cms-up.sh create mode 100755 scripts/patch-cms.sh diff --git a/docker-compose.yml b/docker-compose.yml index 48d555b..9b59505 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,8 @@ services: - "traefik.enable=true" - "traefik.http.routers.sample-website.rule=Host(`${TRAEFIK_HOST:-sample-website.localhost}`)" - "traefik.http.services.sample-website.loadbalancer.server.port=3000" + - "caddy=http://${TRAEFIK_HOST:-acquisition.localhost}" + - "caddy.reverse_proxy={{upstreams 3000}}" directus: image: registry.infra.mintel.me/mintel/directus:${IMAGE_TAG:-latest} @@ -58,6 +60,8 @@ services: - "traefik.enable=true" - "traefik.http.routers.sample-website-directus.rule=Host(`${DIRECTUS_HOST:-cms.sample-website.localhost}`)" - "traefik.http.services.sample-website-directus.loadbalancer.server.port=8055" + - "caddy=http://${DIRECTUS_HOST:-cms.at.localhost}" + - "caddy.reverse_proxy={{upstreams 8055}}" at-mintel-directus-db: image: postgres:15-alpine diff --git a/package.json b/package.json index 081cc85..02c93a0 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,16 @@ "changeset": "changeset", "version-packages": "changeset version", "sync-versions": "tsx scripts/sync-versions.ts --", - "cms:push:infra": "./scripts/sync-directus.sh push infra", - "cms:pull:infra": "./scripts/sync-directus.sh pull infra", + "cms:dev": "pnpm --filter @mintel/cms-infra dev", + "cms:up": "pnpm --filter @mintel/cms-infra up", + "cms:down": "pnpm --filter @mintel/cms-infra down", + "cms:logs": "pnpm --filter @mintel/cms-infra logs", "cms:schema:snapshot": "./scripts/cms-snapshot.sh", "cms:schema:apply": "./scripts/cms-apply.sh local", "cms:schema:apply:infra": "./scripts/cms-apply.sh infra", - "cms:up": "cd packages/cms-infra && npm run up -- --force-recreate", - "cms:down": "cd packages/cms-infra && npm run down", - "cms:logs": "cd packages/cms-infra && npm run logs", - "dev:infra": "docker-compose up -d directus directus-db", + "cms:sync:push": "./scripts/sync-directus.sh push infra", + "cms:sync:pull": "./scripts/sync-directus.sh pull infra", + "build:extensions": "./scripts/sync-extensions.sh", "release": "pnpm build && changeset publish", "release:tag": "pnpm build && pnpm -r publish --no-git-checks --access public", "prepare": "husky" @@ -63,4 +64,4 @@ "@sentry/nextjs": "10.38.0" } } -} +} \ No newline at end of file diff --git a/packages/acquisition-manager/package.json b/packages/acquisition-manager/package.json index 2a21f8d..92d5864 100644 --- a/packages/acquisition-manager/package.json +++ b/packages/acquisition-manager/package.json @@ -20,9 +20,11 @@ "build": "directus-extension build", "dev": "directus-extension build -w" }, + "dependencies": { + "@mintel/directus-extension-toolkit": "workspace:*" + }, "devDependencies": { "@directus/extensions-sdk": "11.0.2", - "@mintel/directus-extension-toolkit": "workspace:*", "vue": "^3.4.0" } -} +} \ No newline at end of file diff --git a/packages/acquisition-manager/src/module.vue b/packages/acquisition-manager/src/module.vue index 5b3c2b3..a046785 100644 --- a/packages/acquisition-manager/src/module.vue +++ b/packages/acquisition-manager/src/module.vue @@ -72,6 +72,15 @@ + + + + Kunde verlinken + import { ref, onMounted, computed } from 'vue'; import { useApi } from '@directus/extensions-sdk'; +import { useRoute, useRouter } from 'vue-router'; import { MintelManagerLayout, MintelSelect, MintelStatCard } from '@mintel/directus-extension-toolkit'; const api = useApi(); +const route = useRoute(); +const router = useRouter(); const leads = ref([]); const selectedLeadId = ref(null); const loadingAudit = ref(false); @@ -204,6 +216,7 @@ const newLead = ref({ const companies = ref([]); const people = ref([]); +const customers = ref([]); const companyOptions = computed(() => companies.value.map(c => ({ @@ -242,7 +255,7 @@ const selectedLead = computed(() => leads.value.find(l => l.id === selectedLeadI async function fetchData() { try { - const [leadsResp, peopleResp, companiesResp] = await Promise.all([ + const [leadsResp, peopleResp, companiesResp, customersResp] = await Promise.all([ api.get('/items/leads', { params: { sort: '-date_created', @@ -250,11 +263,13 @@ async function fetchData() { } }), api.get('/items/people', { params: { sort: 'last_name' } }), - api.get('/items/companies', { params: { sort: 'name' } }) + api.get('/items/companies', { params: { sort: 'name' } }), + api.get('/items/customers', { params: { fields: ['company'] } }) ]); leads.value = leadsResp.data.data; people.value = peopleResp.data.data; companies.value = companiesResp.data.data; + customers.value = customersResp.data.data; if (!selectedLeadId.value && leads.value.length > 0) { selectedLeadId.value = leads.value[0].id; @@ -264,6 +279,33 @@ async function fetchData() { } } +function isCustomer(companyId: string | any) { + if (!companyId) return false; + const id = typeof companyId === 'object' ? companyId.id : companyId; + return customers.value.some(c => (typeof c.company === 'object' ? c.company.id : c.company) === id); +} + +async function linkAsCustomer() { + if (!selectedLead.value) return; + + const companyId = selectedLead.value.company + ? (typeof selectedLead.value.company === 'object' ? selectedLead.value.company.id : selectedLead.value.company) + : null; + + const personId = selectedLead.value.contact_person + ? (typeof selectedLead.value.contact_person === 'object' ? selectedLead.value.contact_person.id : selectedLead.value.contact_person) + : null; + + router.push({ + name: 'module-customer-manager', + query: { + create: 'true', + company: companyId, + contact_person: personId + } + }); +} + async function fetchLeads() { await fetchData(); } @@ -391,7 +433,12 @@ function getStatusColor(status: string) { } } -onMounted(fetchData); +onMounted(async () => { + await fetchData(); + if (route.query.create === 'true') { + openCreateDrawer(); + } +}); `; - finalContent = finalContent.slice(0, headEnd) + stabilityCss + finalContent.slice(headEnd); - } - - const finalPath = path.join(domainDir, htmlFilename); - fs.writeFileSync(finalPath, finalContent); - return finalPath; - } finally { - await browser.close(); + page.on("response", (response) => { + if (response.status() === 200) { + const url = response.url(); + if ( + url.match( + /\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)/i, + ) + ) { + foundAssets.add(url); } + } + }); + + try { + await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 90000 }); + + // Scroll Wave + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const timer = setInterval(() => { + const scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= scrollHeight) { + clearInterval(timer); + window.scrollTo(0, 0); + resolve(true); + } + }, 100); + }); + }); + + const fullHeight = await page.evaluate(() => document.body.scrollHeight); + await page.setViewportSize({ width: 1920, height: fullHeight + 1000 }); + await page.waitForTimeout(3000); + + // Sanitization + await page.evaluate(() => { + const assetPattern = + /\.(jpg|jpeg|png|gif|svg|webp|mp4|webm|woff2?|ttf|otf)/i; + document.querySelectorAll("*").forEach((el) => { + if ( + ["META", "LINK", "HEAD", "SCRIPT", "STYLE", "SVG", "PATH"].includes( + el.tagName, + ) + ) + return; + const htmlEl = el as HTMLElement; + const style = window.getComputedStyle(htmlEl); + if (style.opacity === "0" || style.visibility === "hidden") { + htmlEl.style.setProperty("opacity", "1", "important"); + htmlEl.style.setProperty("visibility", "visible", "important"); + } + for (const attr of Array.from(el.attributes)) { + const name = attr.name.toLowerCase(); + const val = attr.value; + if ( + assetPattern.test(val) || + name.includes("src") || + name.includes("image") + ) { + if (el.tagName === "IMG") { + const img = el as HTMLImageElement; + if (name.includes("srcset")) img.srcset = val; + else if (!img.src || img.src.includes("data:")) img.src = val; + } + if (el.tagName === "SOURCE") + (el as HTMLSourceElement).srcset = val; + if (el.tagName === "VIDEO" || el.tagName === "AUDIO") + (el as HTMLMediaElement).src = val; + if ( + val.match(/^(https?:\/\/|\/\/|\/)/) && + !name.includes("href") + ) { + const bg = htmlEl.style.backgroundImage; + if (!bg || bg === "none") + htmlEl.style.backgroundImage = `url('${val}')`; + } + } + } + }); + if (document.body) { + document.body.style.setProperty("opacity", "1", "important"); + document.body.style.setProperty("visibility", "visible", "important"); + } + }); + + await page.waitForLoadState("networkidle"); + await page.waitForTimeout(1000); + + const content = await page.content(); + const regexPatterns = [ + /(?:src|href|url|data-[a-z-]+|srcset)=["']([^"'<>\s]+?\.(?:css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)(?:\?[^"']*)?)["']/gi, + /url\(["']?([^"')]*)["']?\)/gi, + ]; + + for (const pattern of regexPatterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + try { + foundAssets.add(new URL(match[1], targetUrl).href); + } catch { + // Ignore invalid URLs + } + } + } + + for (const url of foundAssets) { + const local = await this.assetManager.downloadFile(url, assetsDir); + if (local) { + urlMap[url] = local; + const clean = url.split("?")[0]; + urlMap[clean] = local; + if (clean.endsWith(".css")) { + try { + const { data } = await axios.get(url, { + headers: { "User-Agent": this.userAgent }, + }); + const processedCss = + await this.assetManager.processCssRecursively( + data, + url, + assetsDir, + urlMap, + ); + const relPath = this.assetManager.sanitizePath( + new URL(url).hostname + new URL(url).pathname, + ); + fs.writeFileSync(path.join(assetsDir, relPath), processedCss); + } catch { + // Ignore stylesheet download/process failures + } + } + } + } + + let finalContent = content; + const sortedUrls = Object.keys(urlMap).sort( + (a, b) => b.length - a.length, + ); + if (sortedUrls.length > 0) { + const escaped = sortedUrls.map((u) => + u.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + ); + const masterRegex = new RegExp(`(${escaped.join("|")})`, "g"); + finalContent = finalContent.replace( + masterRegex, + (match) => urlMap[match] || match, + ); + } + + const commonDirs = [ + "/wp-content/", + "/wp-includes/", + "/assets/", + "/static/", + "/images/", + ]; + for (const dir of commonDirs) { + const localDir = `./assets/${urlObj.hostname}${dir}`; + finalContent = finalContent + .split(`"${dir}`) + .join(`"${localDir}`) + .split(`'${dir}`) + .join(`'${localDir}`) + .split(`(${dir}`) + .join(`(${localDir}`); + } + + const domainPattern = new RegExp( + `https?://(www\\.)?${urlObj.hostname.replace(/\./g, "\\.")}[^"']*`, + "gi", + ); + finalContent = finalContent.replace(domainPattern, () => "./"); + + finalContent = finalContent.replace( + /]*>([\s\S]*?)<\/script>/gi, + (match, scriptContent) => { + const lower = scriptContent.toLowerCase(); + return lower.includes("google-analytics") || + lower.includes("gtag") || + lower.includes("fbq") || + lower.includes("lazy") || + lower.includes("tracker") + ? "" + : match; + }, + ); + + const headEnd = finalContent.indexOf(""); + if (headEnd > -1) { + const stabilityCss = `\n`; + finalContent = + finalContent.slice(0, headEnd) + + stabilityCss + + finalContent.slice(headEnd); + } + + const finalPath = path.join(domainDir, htmlFilename); + fs.writeFileSync(finalPath, finalContent); + return finalPath; + } finally { + await browser.close(); } + } } diff --git a/packages/cloner-library/src/WebsiteCloner.ts b/packages/cloner-library/src/WebsiteCloner.ts index d39a79b..c274242 100644 --- a/packages/cloner-library/src/WebsiteCloner.ts +++ b/packages/cloner-library/src/WebsiteCloner.ts @@ -1,123 +1,150 @@ -import { PlaywrightCrawler, RequestQueue } from 'crawlee'; -import * as path from 'node:path'; -import * as fs from 'node:fs'; -import { execSync } from 'node:child_process'; +import { PlaywrightCrawler, RequestQueue } from "crawlee"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import { execSync } from "node:child_process"; export interface WebsiteClonerOptions { - baseOutputDir: string; - maxRequestsPerCrawl?: number; - maxConcurrency?: number; + baseOutputDir: string; + maxRequestsPerCrawl?: number; + maxConcurrency?: number; } export class WebsiteCloner { - private options: WebsiteClonerOptions; + private options: WebsiteClonerOptions; - constructor(options: WebsiteClonerOptions) { - this.options = { - maxRequestsPerCrawl: 100, - maxConcurrency: 3, - ...options - }; + constructor(options: WebsiteClonerOptions) { + this.options = { + maxRequestsPerCrawl: 100, + maxConcurrency: 3, + ...options, + }; + } + + public async clone( + targetUrl: string, + outputDirName?: string, + ): Promise { + const urlObj = new URL(targetUrl); + const domain = urlObj.hostname; + const finalOutputDirName = outputDirName || domain.replace(/\./g, "-"); + const baseOutputDir = path.resolve( + this.options.baseOutputDir, + finalOutputDirName, + ); + + if (fs.existsSync(baseOutputDir)) { + fs.rmSync(baseOutputDir, { recursive: true, force: true }); } + fs.mkdirSync(baseOutputDir, { recursive: true }); - public async clone(targetUrl: string, outputDirName?: string): Promise { - const urlObj = new URL(targetUrl); - const domain = urlObj.hostname; - const finalOutputDirName = outputDirName || domain.replace(/\./g, '-'); - const baseOutputDir = path.resolve(this.options.baseOutputDir, finalOutputDirName); + console.log(`🚀 Starting perfect recursive clone of ${targetUrl}...`); + console.log(`📂 Output: ${baseOutputDir}`); - if (fs.existsSync(baseOutputDir)) { - fs.rmSync(baseOutputDir, { recursive: true, force: true }); - } - fs.mkdirSync(baseOutputDir, { recursive: true }); + const requestQueue = await RequestQueue.open(); + await requestQueue.addRequest({ url: targetUrl }); - console.log(`🚀 Starting perfect recursive clone of ${targetUrl}...`); - console.log(`📂 Output: ${baseOutputDir}`); + const crawler = new PlaywrightCrawler({ + requestQueue, + maxRequestsPerCrawl: this.options.maxRequestsPerCrawl, + maxConcurrency: this.options.maxConcurrency, - const requestQueue = await RequestQueue.open(); - await requestQueue.addRequest({ url: targetUrl }); + async requestHandler({ request, enqueueLinks, log }) { + const url = request.url; + log.info(`Capturing ${url}...`); - const crawler = new PlaywrightCrawler({ - requestQueue, - maxRequestsPerCrawl: this.options.maxRequestsPerCrawl, - maxConcurrency: this.options.maxConcurrency, + const u = new URL(url); + let relPath = u.pathname; + if (relPath === "/" || relPath === "") relPath = "/index.html"; + if (!relPath.endsWith(".html") && !path.extname(relPath)) + relPath += "/index.html"; + if (relPath.startsWith("/")) relPath = relPath.substring(1); - async requestHandler({ request, enqueueLinks, log }) { - const url = request.url; - log.info(`Capturing ${url}...`); + const fullPath = path.join(baseOutputDir, relPath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - const u = new URL(url); - let relPath = u.pathname; - if (relPath === '/' || relPath === '') relPath = '/index.html'; - if (!relPath.endsWith('.html') && !path.extname(relPath)) relPath += '/index.html'; - if (relPath.startsWith('/')) relPath = relPath.substring(1); - - const fullPath = path.join(baseOutputDir, relPath); - fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - - try { - // Note: This assumes single-file-cli is available in the environment - execSync(`npx single-file-cli "${url}" "${fullPath}" --browser-headless=true --browser-wait-until=networkidle0`, { - stdio: 'inherit' - }); - } catch (e) { - log.error(`Failed to capture ${url} with SingleFile`); - } - - await enqueueLinks({ - strategy: 'same-domain', - transformRequestFunction: (req) => { - if (/\.(download|pdf|zip|gz|exe|png|jpg|jpeg|gif|svg|css|js)$/i.test(req.url)) return false; - return req; - } - }); + try { + // Note: This assumes single-file-cli is available in the environment + execSync( + `npx single-file-cli "${url}" "${fullPath}" --browser-headless=true --browser-wait-until=networkidle0`, + { + stdio: "inherit", }, + ); + } catch (_e) { + log.error(`Failed to capture ${url} with SingleFile`); + } + + await enqueueLinks({ + strategy: "same-domain", + transformRequestFunction: (req) => { + if ( + /\.(download|pdf|zip|gz|exe|png|jpg|jpeg|gif|svg|css|js)$/i.test( + req.url, + ) + ) + return false; + return req; + }, }); + }, + }); - await crawler.run(); + await crawler.run(); - console.log('🔗 Rewriting internal links for offline navigation...'); - const allFiles = this.getFiles(baseOutputDir).filter(f => f.endsWith('.html')); + console.log("🔗 Rewriting internal links for offline navigation..."); + const allFiles = this.getFiles(baseOutputDir).filter((f) => + f.endsWith(".html"), + ); - for (const file of allFiles) { - let content = fs.readFileSync(file, 'utf8'); - const fileRelToRoot = path.relative(baseOutputDir, file); + for (const file of allFiles) { + let content = fs.readFileSync(file, "utf8"); + const fileRelToRoot = path.relative(baseOutputDir, file); - content = content.replace(/href="([^"]+)"/g, (match, href) => { - if (href.startsWith(targetUrl) || href.startsWith('/') || (!href.includes('://') && !href.startsWith('data:'))) { - try { - const linkUrl = new URL(href, targetUrl); - if (linkUrl.hostname === domain) { - let linkPath = linkUrl.pathname; - if (linkPath === '/' || linkPath === '') linkPath = '/index.html'; - if (!linkPath.endsWith('.html') && !path.extname(linkPath)) linkPath += '/index.html'; - if (linkPath.startsWith('/')) linkPath = linkPath.substring(1); + content = content.replace(/href="([^"]+)"/g, (match, href) => { + if ( + href.startsWith(targetUrl) || + href.startsWith("/") || + (!href.includes("://") && !href.startsWith("data:")) + ) { + try { + const linkUrl = new URL(href, targetUrl); + if (linkUrl.hostname === domain) { + let linkPath = linkUrl.pathname; + if (linkPath === "/" || linkPath === "") linkPath = "/index.html"; + if (!linkPath.endsWith(".html") && !path.extname(linkPath)) + linkPath += "/index.html"; + if (linkPath.startsWith("/")) linkPath = linkPath.substring(1); - const relativeLink = path.relative(path.dirname(fileRelToRoot), linkPath); - return `href="${relativeLink}"`; - } - } catch (e) { } - } - return match; - }); - - fs.writeFileSync(file, content); - } - - console.log(`\n✅ Done! Perfect clone complete in: ${baseOutputDir}`); - return baseOutputDir; - } - - private getFiles(dir: string, fileList: string[] = []) { - const files = fs.readdirSync(dir); - for (const file of files) { - const name = path.join(dir, file); - if (fs.statSync(name).isDirectory()) { - this.getFiles(name, fileList); - } else { - fileList.push(name); + const relativeLink = path.relative( + path.dirname(fileRelToRoot), + linkPath, + ); + return `href="${relativeLink}"`; } + } catch (_e) { + // Ignore link rewriting failures + } } - return fileList; + return match; + }); + + fs.writeFileSync(file, content); } + + console.log(`\n✅ Done! Perfect clone complete in: ${baseOutputDir}`); + return baseOutputDir; + } + + private getFiles(dir: string, fileList: string[] = []) { + const files = fs.readdirSync(dir); + for (const file of files) { + const name = path.join(dir, file); + if (fs.statSync(name).isDirectory()) { + this.getFiles(name, fileList); + } else { + fileList.push(name); + } + } + return fileList; + } } diff --git a/packages/cms-infra/database/data.db b/packages/cms-infra/database/data.db index f0b07e7bfc0da2062d83b32155c890fc2f1d409f..6e91a91bb6a7c0e69ce4e0b04246b09f1da43d80 100644 GIT binary patch delta 7132 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 delta 14490 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 c8J6 import { ref, onMounted } from 'vue'; import { useApi } from '@directus/extensions-sdk'; +import { useRoute } from 'vue-router'; import { MintelManagerLayout } from '@mintel/directus-extension-toolkit'; const api = useApi(); +const route = useRoute(); const companies = ref([]); const selectedCompany = ref(null); const feedback = ref(null); @@ -201,7 +203,12 @@ async function deleteCompany() { } } -onMounted(fetchData); +onMounted(async () => { + await fetchData(); + if (route.query.create === 'true') { + openCreateDrawer(); + } +});