Compare commits

...

13 Commits

Author SHA1 Message Date
efd1341762 fix: add canvas build deps for gatekeeper x86 build
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 8s
Monorepo Pipeline / 🧪 Test (push) Failing after 7s
Monorepo Pipeline / 🏗️ Build (push) Failing after 7s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 23:10:47 +01:00
36a952db56 chore: sync versions to v1.8.21
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 7s
Monorepo Pipeline / 🏗️ Build (push) Failing after 7s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 19:39:04 +01:00
8c637f0220 chore: trigger x86 ci build
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m31s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m27s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m30s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 19:05:19 +01:00
6dd97e7a6b chore: trigger x86 build for mb-grid and mintel.me
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m17s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m22s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m25s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 18:45:47 +01:00
9f426470bb fix(ci): update build platform from arm64 to amd64 for the new x86 server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 50s
Monorepo Pipeline / 🏗️ Build (push) Failing after 56s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m8s
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 17:42:57 +01:00
960914ebb8 feat: content engine usw 2026-02-25 12:43:57 +01:00
a55a5bb834 fix: prevent .env changes during tagging and improve pre-push hook feedback
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m8s
Monorepo Pipeline / 🧪 Test (push) Successful in 4m16s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m34s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-02-23 14:10:04 +01:00
0aaf858f5b chore: sync versions to v1.8.20
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m57s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m38s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m12s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m42s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m31s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m50s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 8m14s
Monorepo Pipeline / 🚀 Release (push) Successful in 9m13s
2026-02-23 14:03:27 +01:00
ec562c1b2c fix: imgproxy issues 2026-02-23 14:03:17 +01:00
02e15c3f4a chore: sync versions to v1.8.19
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 3m54s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m12s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m42s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m7s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m43s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m37s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m47s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 6m39s
Monorepo Pipeline / 🚀 Release (push) Successful in 7m18s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s
2026-02-23 00:52:35 +01:00
cd4c2193ce feat: implement legacy imgproxy compatibility and URL mapping
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-23 00:14:13 +01:00
df7a464e03 fix(ci): sync lockfile and remove deleted model scripts
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 5m26s
Monorepo Pipeline / 🏗️ Build (push) Successful in 7m18s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m5s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m8s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m43s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m27s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m38s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 5m49s
Monorepo Pipeline / 🚀 Release (push) Successful in 6m24s
2026-02-22 23:40:30 +01:00
e2e0653de6 chore(image-processor): use Gemini 3 Flash Preview
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🏗️ Build (push) Failing after 23s
Monorepo Pipeline / 🧹 Lint (push) Failing after 8s
Monorepo Pipeline / 🧪 Test (push) Failing after 21s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-22 23:31:44 +01:00
63 changed files with 893 additions and 628 deletions

View File

@@ -24,5 +24,3 @@ coverage
**/.pnpm-store
.gitea
**/.gitea
models
**/models

2
.env
View File

@@ -1,5 +1,5 @@
# Project
IMAGE_TAG=v1.8.12
IMAGE_TAG=v1.8.19
PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582

View File

@@ -1,5 +1,5 @@
# Project
IMAGE_TAG=v1.8.16
IMAGE_TAG=v1.8.21
PROJECT_NAME=sample-website
PROJECT_COLOR=#82ed20

View File

@@ -214,7 +214,7 @@ jobs:
with:
context: .
file: ${{ matrix.file }}
platforms: linux/arm64
platforms: linux/amd64
pull: true
provenance: false
push: true

View File

@@ -35,8 +35,9 @@ do
# Push the updated tag directly (using --no-verify to avoid recursion)
git push origin "$TAG" --force --no-verify
echo "✨ All done! Hook integrated the sync and pushed for you."
exit 1 # Still exit 1 to abort the original (now outdated) push attempt
echo "✨ Success! The hook synchronized the versions and pushed the updated tag for you."
echo " Note: The original push command was aborted in favor of the auto-push. This is normal."
exit 0 # Change to exit 0 to not show as an 'error' in vscode/terminal, though original push will still be technically 'failed' by git
else
echo "✨ Versions already in sync for $TAG."
fi

View File

@@ -81,3 +81,4 @@ Client websites scaffolded via the CLI use a **tag-based deployment** strategy:
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
Trigger rebuilding for x86 architecture.

View File

@@ -1,6 +1,16 @@
FROM node:20.18-bookworm-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN apt-get update && apt-get install -y \
build-essential \
python3 \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
libexpat1 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm@10.30.1
FROM base AS build
@@ -13,14 +23,17 @@ RUN pnpm --filter image-service build
FROM base
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/image-service/node_modules ./apps/image-service/node_modules
COPY --from=build /app/packages/image-processor/node_modules ./packages/image-processor/node_modules
RUN mkdir -p /app/apps/image-service/dist
# Instead of copying node_modules which contains native C++ bindings for canvas and tfjs-node,
# we copy the package.json files and install natively in the final stage so the bindings are correct.
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/image-service/package.json ./apps/image-service/package.json
COPY packages/image-processor/package.json ./packages/image-processor/package.json
RUN pnpm install --frozen-lockfile --filter image-service...
COPY --from=build /app/apps/image-service/dist ./apps/image-service/dist
COPY --from=build /app/apps/image-service/package.json ./apps/image-service/package.json
COPY --from=build /app/packages/image-processor/dist ./packages/image-processor/dist
COPY --from=build /app/packages/image-processor/package.json ./packages/image-processor/package.json
COPY --from=build /app/packages/image-processor/models ./packages/image-processor/models
EXPOSE 8080
WORKDIR /app/apps/image-service

View File

@@ -1,6 +1,6 @@
{
"name": "image-service",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,37 +1,45 @@
import Fastify from "fastify";
import { processImageWithSmartCrop } from "@mintel/image-processor";
import {
processImageWithSmartCrop,
parseImgproxyOptions,
mapUrl,
} from "@mintel/image-processor";
const fastify = Fastify({
logger: true,
});
fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => {
// Compatibility endpoint for old imgproxy calls (optional, but requested by some systems sometimes)
// For now, replacing logic in clients is preferred. So we just redirect or error.
return reply
.status(400)
.send({ error: "Legacy imgproxy API not supported. Use /process" });
});
fastify.get("/process", async (request, reply) => {
const query = request.query as {
url?: string;
w?: string;
h?: string;
q?: string;
format?: string;
const { options, urlSafeB64 } = request.params as {
options: string;
urlSafeB64: string;
};
const { url } = query;
const width = parseInt(query.w || "800", 10);
const height = parseInt(query.h || "600", 10);
const quality = parseInt(query.q || "80", 10);
const format = (query.format || "webp") as "webp" | "jpeg" | "png" | "avif";
if (!url) {
return reply.status(400).send({ error: 'Parameter "url" is required' });
// urlSafeB64 might be "plain/http://..." or a Base64 string
let url = "";
if (urlSafeB64.startsWith("plain/")) {
url = urlSafeB64.substring(6);
} else {
try {
url = Buffer.from(urlSafeB64, "base64").toString("utf-8");
} catch (e) {
return reply.status(400).send({ error: "Invalid Base64 URL" });
}
}
const parsedOptions = parseImgproxyOptions(options);
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
return handleProcessing(mappedUrl, parsedOptions, reply);
});
// Helper to avoid duplication
async function handleProcessing(url: string, options: any, reply: any) {
const width = options.width || 800;
const height = options.height || 600;
const quality = options.quality || 80;
const format = options.format || "webp";
try {
const response = await fetch(url);
if (!response.ok) {
@@ -48,7 +56,6 @@ fastify.get("/process", async (request, reply) => {
height,
format,
quality,
openRouterApiKey: process.env.OPENROUTER_API_KEY,
});
reply.header("Content-Type", `image/${format}`);
@@ -60,6 +67,29 @@ fastify.get("/process", async (request, reply) => {
.status(500)
.send({ error: "Internal Server Error processing image" });
}
}
fastify.get("/process", async (request, reply) => {
const query = request.query as {
url?: string;
w?: string;
h?: string;
q?: string;
format?: string;
};
const { url } = query;
const width = parseInt(query.w || "800", 10);
const height = parseInt(query.h || "600", 10);
const quality = parseInt(query.q || "80", 10);
const format = (query.format || "webp") as any;
if (!url) {
return reply.status(400).send({ error: 'Parameter "url" is required' });
}
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
return handleProcessing(mappedUrl, { width, height, quality, format }, reply);
});
fastify.get("/health", async () => {

View File

@@ -1,6 +1,6 @@
{
"name": "sample-website",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

14
optimize-images.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Ghost Image Optimizer
# Target directory for Ghost content
TARGET_DIR="/home/deploy/sites/marisas.world/content/images"
echo "Starting image optimization for $TARGET_DIR..."
# Find all original images, excluding the 'size/' directory where Ghost stores thumbnails
# Resize images larger than 2500px down to 2500px width
# Compress JPEG/PNG to 80% quality
find "$TARGET_DIR" -type d -name "size" -prune -o \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) -type f -exec mogrify -resize '2500x>' -quality 80 {} +
echo "Optimization complete."

View File

@@ -57,7 +57,7 @@
"pino-pretty": "^13.1.3",
"require-in-the-middle": "^8.0.1"
},
"version": "1.8.16",
"version": "1.8.21",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

View File

@@ -2,7 +2,7 @@
"name": "acquisition-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "acquisition",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"directus:extension": {
"type": "endpoint",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cli",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cloner",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cms-infra",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -2,7 +2,7 @@
"name": "company-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -0,0 +1,38 @@
{
"name": "@mintel/concept-engine",
"version": "1.8.21",
"private": true,
"description": "AI-powered web project concept generation and analysis",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"concept": "./dist/cli.js"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"clean": "rm -rf dist",
"lint": "eslint src --ext .ts",
"concept": "tsx src/cli.ts run"
},
"dependencies": {
"@crawlee/cheerio": "^3.11.2",
"@mintel/journaling": "workspace:*",
"@mintel/page-audit": "workspace:*",
"axios": "^1.7.9",
"cheerio": "1.0.0-rc.12",
"commander": "^13.1.0",
"dotenv": "^16.4.7",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^20.17.17",
"tsup": "^8.3.6",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/content-engine",
"version": "1.8.16",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -17,6 +17,7 @@ export interface OptimizationTask {
availableComponents?: ComponentDefinition[];
instructions?: string;
internalLinks?: { title: string; slug: string }[];
customSources?: string[];
}
export interface OptimizeFileOptions {
@@ -211,7 +212,32 @@ export class AiBlogPostOrchestrator {
console.log(`✅ Saved optimized file to: ${finalPath}`);
}
private async generateVisualPrompt(content: string): Promise<string> {
async generateSlug(content: string, title?: string, instructions?: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content: `You generate SEO-optimized URL slugs for B2B blog posts based on the provided content.
Return ONLY a JSON object with a single string field "slug".
Example: {"slug": "how-to-optimize-react-performance"}
Rules: Use lowercase letters, numbers, and hyphens only. No special characters. Keep it concise (2-5 words).`,
},
{ role: "user", content: `Title: ${title || "Unknown"}\n\nContent:\n${content.slice(0, 3000)}...${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the slug:\n${instructions}` : ""}` },
],
response_format: { type: "json_object" },
});
try {
const parsed = JSON.parse(response.choices[0].message.content || '{"slug": ""}');
let slug = parsed.slug || "new-post";
return slug.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
} catch {
return "new-post";
}
}
public async generateVisualPrompt(content: string, instructions?: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: this.model,
messages: [
@@ -227,7 +253,7 @@ FOCUS: The core metaphor or technical concept of the article.
Example output: "A complex network of glowing fiber optic nodes forming a recursive pyramid structure, technical blue lineart style."`,
},
{ role: "user", content: content.slice(0, 5000) },
{ role: "user", content: `${content.slice(0, 5000)}${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the visual prompt:\n${instructions}` : ""}` },
],
max_tokens: 100,
});
@@ -303,6 +329,7 @@ Example output: "A complex network of glowing fiber optic nodes forming a recurs
);
const realPosts = await this.researchAgent.fetchRealSocialPosts(
task.content.slice(0, 500),
task.customSources
);
socialPosts.push(...realPosts);
}
@@ -470,7 +497,6 @@ BLOG POST BEST PRACTICES (MANDATORY):
- MEME DIVERSITY: Du MUSST ZWINGEND für jedes Meme (sofern passend) abwechslungsreiche Templates nutzen. Um dies zu garantieren, wurde für diesen Artikel das folgende Template ausgewählt: '${forcedMeme}'. Du MUSST EXAKT DIESES TEMPLATE NUTZEN. Versuche nicht, es durch ein Standard-Template wie 'drake' zu ersetzen!
- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze ArticleQuote (mit isCompany=true für Firmen). Für Personen lass isCompany weg.
- Füge zwingend ein prägnantes 'TL;DR' ganz am Anfang ein.
- Füge ein sauberes TableOfContents ein.
- Verwende unsere Komponenten stilvoll für Visualisierungen.
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
- Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab.

View File

@@ -2,7 +2,7 @@
"name": "customer-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/directus-extension-toolkit",
"version": "1.8.16",
"version": "1.8.21",
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/eslint-config",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,46 @@
{
"name": "@mintel/estimation-engine",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"estimate": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
"lint": "eslint src",
"estimate": "tsx src/cli.ts"
},
"dependencies": {
"@mintel/concept-engine": "workspace:*",
"axios": "^1.6.0",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"dotenv": "^17.3.1",
"ink": "^5.1.0",
"ink-spinner": "^5.0.0",
"ink-select-input": "^6.0.0",
"ink-text-input": "^6.0.0",
"react": "^18.2.0",
"openai": "^4.82.0"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"tsup": "^8.3.5",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
}

View File

@@ -2,7 +2,7 @@
"name": "feedback-commander",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/gatekeeper",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/husky-config",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1 @@
404: Not Found

View File

@@ -0,0 +1,30 @@
[
{
"weights":
[
{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},
{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},
{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},
{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},
{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},
{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},
{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},
{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},
{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},
{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},
{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},
{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},
{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},
{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},
{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},
{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},
{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},
{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},
{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}
],
"paths":
[
"tiny_face_detector_model.bin"
]
}
]

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/image-processor",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",
@@ -13,11 +13,14 @@
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"dev": "tsup src/index.ts --format esm --watch --dts",
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src"
},
"dependencies": {
"@tensorflow/tfjs": "^4.22.0",
"@vladmandic/face-api": "^1.7.15",
"canvas": "^3.2.1",
"sharp": "^0.33.2"
},
"devDependencies": {

View File

@@ -1,55 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as https from "node:https";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MODELS_DIR = path.join(__dirname, "..", "models");
const BASE_URL =
"https://raw.githubusercontent.com/vladmandic/face-api/master/model/";
const models = [
"tiny_face_detector_model-weights_manifest.json",
"tiny_face_detector_model-shard1",
];
async function downloadModel(filename: string) {
const destPath = path.join(MODELS_DIR, filename);
if (fs.existsSync(destPath)) {
console.log(`Model ${filename} already exists.`);
return;
}
return new Promise((resolve, reject) => {
console.log(`Downloading ${filename}...`);
const file = fs.createWriteStream(destPath);
https
.get(BASE_URL + filename, (response) => {
response.pipe(file);
file.on("finish", () => {
file.close();
resolve(true);
});
})
.on("error", (err) => {
fs.unlinkSync(destPath);
reject(err);
});
});
}
async function main() {
if (!fs.existsSync(MODELS_DIR)) {
fs.mkdirSync(MODELS_DIR, { recursive: true });
}
for (const model of models) {
await downloadModel(model);
}
console.log("All models downloaded successfully!");
}
main().catch(console.error);

View File

@@ -1,96 +1,112 @@
import sharp from "sharp";
import { Canvas, Image, ImageData } from "canvas";
// Use the ESM no-bundle build to avoid the default Node entrypoint
// which hardcodes require('@tensorflow/tfjs-node') and crashes in Docker.
// This build uses pure @tensorflow/tfjs (JS-only, no native C++ bindings).
// @ts-ignore - direct path import has no type declarations
import * as faceapi from "@vladmandic/face-api/dist/face-api.esm-nobundle.js";
import * as tf from "@tensorflow/tfjs";
import path from "path";
import { fileURLToPath } from "url";
// Polyfill required by face-api for Node.js
faceapi.env.monkeyPatch({ Canvas, Image, ImageData } as any);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MODEL_URL = path.join(__dirname, "../models");
// State flag to ensure we only load weights once
let modelsLoaded = false;
async function loadModelsOnce() {
if (modelsLoaded) return;
// Initialize pure JS CPU backend (no native bindings needed)
await tf.setBackend("cpu");
await tf.ready();
// Load the microscopic TinyFaceDetector (~190KB)
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODEL_URL);
modelsLoaded = true;
}
export interface ProcessImageOptions {
width: number;
height: number;
format?: "webp" | "jpeg" | "png" | "avif";
quality?: number;
openRouterApiKey?: string;
}
interface FaceDetection {
x: number;
y: number;
width: number;
height: number;
}
/**
* Detects faces using OpenRouter Vision API.
* Uses a small preview to save bandwidth and tokens.
* Maps a URL based on the IMGPROXY_URL_MAPPING environment variable.
* Format: "match1:replace1,match2:replace2"
*/
async function detectFacesWithCloud(
inputBuffer: Buffer,
apiKey: string,
): Promise<FaceDetection[]> {
try {
// Generate a small preview for vision API (max 512px)
const preview = await sharp(inputBuffer)
.resize(512, 512, { fit: "inside" })
.jpeg({ quality: 60 })
.toBuffer();
export function mapUrl(url: string, mappingString?: string): string {
if (!mappingString) return url;
const base64Image = preview.toString("base64");
const response = await fetch(
"https://openrouter.ai/api/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://mintel.me",
"X-Title": "Mintel Image Service",
},
body: JSON.stringify({
model: "google/gemini-2.0-flash-001", // Fast, cheap, and supports vision
messages: [
{
role: "user",
content: [
{
type: "text",
text: 'Detect all human faces in this image. Return ONLY a JSON array of bounding boxes like: [{"x": 0.1, "y": 0.2, "width": 0.05, "height": 0.05}]. Coordinates must be normalized (0 to 1). If no faces, return [].',
},
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`,
},
},
],
},
],
response_format: { type: "json_object" },
}),
},
);
if (!response.ok) {
throw new Error(`OpenRouter API error: ${response.statusText}`);
const mappings = mappingString.split(",").map((m) => {
if (m.includes("|")) {
return m.split("|");
}
const data = (await response.json()) as any;
const content = data.choices[0]?.message?.content;
// Legacy support for simple "host:target" or cases where one side might have a protocol
// We try to find the split point that isn't part of a protocol "://"
const colonIndices = [];
for (let i = 0; i < m.length; i++) {
if (m[i] === ":") {
// Check if this colon is part of "://"
if (!(m[i + 1] === "/" && m[i + 2] === "/")) {
colonIndices.push(i);
}
}
}
if (!content) return [];
if (colonIndices.length === 0) return [m];
// The model might return directly or wrapped in a json field
const parsed = typeof content === "string" ? JSON.parse(content) : content;
const detections = (parsed.faces || parsed.detections || parsed) as any[];
// In legacy mode with colons, we take the LAST non-protocol colon as the separator
// This handles "http://host:port" or "host:http://target" better
const lastColon = colonIndices[colonIndices.length - 1];
return [m.substring(0, lastColon), m.substring(lastColon + 1)];
});
if (!Array.isArray(detections)) return [];
let mappedUrl = url;
return detections.map((d) => ({
x: d.x,
y: d.y,
width: d.width,
height: d.height,
}));
} catch (error) {
console.error("Cloud face detection failed:", error);
return [];
for (const [match, replace] of mappings) {
if (match && replace && url.includes(match)) {
mappedUrl = url.replace(match, replace);
}
}
return mappedUrl;
}
/**
* Parses legacy imgproxy options string.
* Example: rs:fill:300:400/q:80
*/
export function parseImgproxyOptions(
optionsStr: string,
): Partial<ProcessImageOptions> {
const parts = optionsStr.split("/");
const options: Partial<ProcessImageOptions> = {};
for (const part of parts) {
if (part.startsWith("rs:")) {
const [, , w, h] = part.split(":");
if (w) options.width = parseInt(w, 10);
if (h) options.height = parseInt(h, 10);
} else if (part.startsWith("q:")) {
const q = part.split(":")[1];
if (q) options.quality = parseInt(q, 10);
} else if (part.startsWith("ext:")) {
const ext = part.split(":")[1] as any;
if (["webp", "jpeg", "png", "avif"].includes(ext)) {
options.format = ext;
}
}
}
return options;
}
export async function processImageWithSmartCrop(
@@ -104,32 +120,41 @@ export async function processImageWithSmartCrop(
throw new Error("Could not read image metadata");
}
const detections = options.openRouterApiKey
? await detectFacesWithCloud(inputBuffer, options.openRouterApiKey)
: [];
// Load ML models (noop if already loaded)
await loadModelsOnce();
// Convert sharp image to a Node-compatible canvas Image for face-api
const jpegBuffer = await sharpImage.jpeg().toBuffer();
const img = new Image();
img.src = jpegBuffer;
const canvas = new Canvas(img.width, img.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
// Detect faces locally using the tiny model
// Requires explicit any cast since the types expect HTML elements in browser contexts
const detections = await faceapi.detectAllFaces(
canvas as any,
new faceapi.TinyFaceDetectorOptions(),
);
let cropPosition: "center" | "attention" | number = "attention"; // Fallback to sharp's attention if no faces
// If faces are found, calculate the bounding box containing all faces
if (detections.length > 0) {
// Map normalized coordinates back to pixels
const pixelDetections = detections.map((d) => ({
x: d.x * (metadata.width || 0),
y: d.y * (metadata.height || 0),
width: d.width * (metadata.width || 0),
height: d.height * (metadata.height || 0),
}));
// We have faces! Calculate the bounding box that contains all of them
let minX = metadata.width;
let minY = metadata.height;
let maxX = 0;
let maxY = 0;
for (const det of pixelDetections) {
if (det.x < minX) minX = Math.max(0, det.x);
if (det.y < minY) minY = Math.max(0, det.y);
if (det.x + det.width > maxX)
maxX = Math.min(metadata.width, det.x + det.width);
if (det.y + det.height > maxY)
maxY = Math.min(metadata.height, det.y + det.height);
for (const det of detections) {
const box = det.box;
if (box.x < minX) minX = Math.max(0, box.x);
if (box.y < minY) minY = Math.max(0, box.y);
if (box.x + box.width > maxX)
maxX = Math.min(metadata.width, box.x + box.width);
if (box.y + box.height > maxY)
maxY = Math.min(metadata.height, box.y + box.height);
}
const centerX = Math.floor(minX + (maxX - minX) / 2);
@@ -141,32 +166,39 @@ export async function processImageWithSmartCrop(
let cropWidth = metadata.width;
let cropHeight = metadata.height;
// Determine the maximal crop window that maintains aspect ratio
if (currentRatio > targetRatio) {
cropWidth = Math.floor(metadata.height * targetRatio);
} else {
cropHeight = Math.floor(metadata.width / targetRatio);
}
// Center the crop window over the center of the faces
let cropX = Math.floor(centerX - cropWidth / 2);
let cropY = Math.floor(centerY - cropHeight / 2);
// Keep crop window inside image bounds
if (cropX < 0) cropX = 0;
if (cropY < 0) cropY = 0;
if (cropX + cropWidth > metadata.width) cropX = metadata.width - cropWidth;
if (cropY + cropHeight > metadata.height)
cropY = metadata.height - cropHeight;
// Pre-crop the image to isolate the faces before resizing
sharpImage.extract({
left: cropX,
top: cropY,
width: cropWidth,
height: cropHeight,
});
// As we manually calculated the exact focal box, we can now just center it
cropPosition = "center";
}
let finalImage = sharpImage.resize(options.width, options.height, {
fit: "cover",
position: detections.length > 0 ? "center" : "attention",
position: cropPosition,
});
const format = options.format || "webp";

View File

@@ -0,0 +1,19 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
// Bundle face-api and tensorflow inline (they're pure JS).
// Keep sharp and canvas external (they have native C++ bindings).
noExternal: [
"@vladmandic/face-api",
"@tensorflow/tfjs",
"@tensorflow/tfjs-backend-wasm"
],
external: [
"sharp",
"canvas"
],
});

View File

@@ -1,6 +1,6 @@
# Step 1: Builder stage
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat curl
RUN apk add --no-cache libc6-compat curl python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev
WORKDIR /app
RUN corepack enable pnpm
ENV CI=true
@@ -25,7 +25,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
pnpm config set store-dir /pnpm/store && \
pnpm i --frozen-lockfile
pnpm i --no-frozen-lockfile
# Copy the rest of the source
COPY . .

View File

@@ -189,7 +189,7 @@ jobs:
with:
context: .
file: packages/infra/docker/Dockerfile.nextjs
platforms: linux/arm64
platforms: linux/amd64
pull: true
provenance: false
build-args: |

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/infra",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/journaling",
"version": "1.8.16",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",

View File

@@ -176,6 +176,7 @@ Return JSON: { "facts": [ { "statement": "...", "source": "Organization Name Onl
*/
async fetchRealSocialPosts(
topic: string,
customSources?: string[],
retries = 1,
): Promise<SocialPost[]> {
console.log(
@@ -220,7 +221,7 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
if (!videos || videos.length === 0) {
console.warn(`⚠️ [Serper] No videos found for query: "${queryStr}"`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
@@ -237,11 +238,16 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
if (ytVideos.length === 0) {
console.warn(`⚠️ [Serper] No YouTube videos in search results.`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
// Step 3: Ask the LLM to evaluate the relevance of the found videos
const sourceExamples = customSources && customSources.length > 0
? `Specifically prioritize content from: ${customSources.join(", ")}.`
: `(e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte).`;
const evalPrompt = `You are a strict technical evaluator. You must select the MOST RELEVANT educational tech video from the list below based on this core article context: "${topic.slice(0, 800)}..."
Videos:
@@ -249,7 +255,7 @@ ${ytVideos.map((v, i) => `[ID: ${i}] Title: "${v.title}" | Channel: "${v.channel
RULES:
1. The video MUST be highly relevant to the EXACT technical topic of the context.
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel (e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte). AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel ${sourceExamples} AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
3. If none of the videos are strictly relevant to the core technical or business subject (e.g. they are just casually mentioning the word), YOU MUST RETURN -1. Be extremely critical. Do not just pick the "best of the worst".
4. If one is highly relevant, return its ID number.
@@ -273,7 +279,7 @@ Return ONLY a JSON object: {"bestVideoId": number}`;
if (bestIdx < 0 || bestIdx >= ytVideos.length) {
console.warn(`⚠️ [Serper] LLM rejected all videos as irrelevant.`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
@@ -342,7 +348,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.`
try {
let parsed = JSON.parse(
response.choices[0].message.content ||
'{"trendsKeywords": [], "dcVariables": []}',
'{"trendsKeywords": [], "dcVariables": []}',
);
if (Array.isArray(parsed)) {
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/mail",
"version": "1.8.16",
"version": "1.8.21",
"private": false,
"publishConfig": {
"access": "public",

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/meme-generator",
"version": "1.8.16",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-config",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-feedback",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-observability",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-utils",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/observability",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,39 @@
{
"name": "@mintel/page-audit",
"version": "1.8.21",
"private": true,
"description": "AI-powered website IST-analysis using DataForSEO and Gemini",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"page-audit": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
"audit": "tsx src/cli.ts"
},
"dependencies": {
"chalk": "^5.3.0",
"cheerio": "^1.0.0",
"commander": "^12.0.0",
"dotenv": "^17.3.1",
"openai": "^4.82.0"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^20.0.0",
"tsup": "^8.3.5",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/pdf",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -13,6 +13,8 @@ import {
Footer,
FoldingMarks,
DocumentTitle,
COLORS,
FONT_SIZES,
} from "./pdf/SharedUI.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
@@ -29,23 +31,23 @@ const localStyles = PDFStyleSheet.create({
marginBottom: 6,
},
monoNumber: {
fontSize: 7,
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: "#94a3b8",
color: COLORS.TEXT_LIGHT,
letterSpacing: 2,
width: 25,
},
sectionTitle: {
fontSize: 9,
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: "#000000",
color: COLORS.CHARCOAL,
textTransform: "uppercase",
letterSpacing: 0.5,
},
officialText: {
fontSize: 8,
fontSize: FONT_SIZES.BODY,
lineHeight: 1.5,
color: "#334155",
color: COLORS.TEXT_MAIN,
textAlign: "justify",
paddingLeft: 25,
},
@@ -100,7 +102,7 @@ export const AgbsPDF = ({
};
const content = (
<>
<PDFView>
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[`Stand: ${date}`]}
@@ -142,7 +144,7 @@ export const AgbsPDF = ({
<AGBSection index="05" title="Abnahme">
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv
nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine
nutzt oder innerhalb von 30 Tagen nach Bereitstellung keine
wesentlichen Mängel angezeigt werden. Optische Abweichungen,
Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel
dar.
@@ -206,7 +208,7 @@ export const AgbsPDF = ({
bleibt die Wirksamkeit der übrigen Regelungen unberührt.
</AGBSection>
</PDFView>
</>
</PDFView>
);
if (mode === "full") {
@@ -214,9 +216,8 @@ export const AgbsPDF = ({
<SimpleLayout
companyData={companyData}
bankData={bankData}
headerIcon={headerIcon}
footerLogo={footerLogo}
icon={headerIcon}
pageNumber="10"
showPageNumber={false}
>
{content}
@@ -232,7 +233,7 @@ export const AgbsPDF = ({
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
_bankData={bankData}
showDetails={false}
showPageNumber={false}
/>

View File

@@ -46,7 +46,7 @@ export const CombinedQuotePDF = ({
const layoutProps = {
date,
icon: estimationProps.headerIcon,
headerIcon: estimationProps.headerIcon,
footerLogo: estimationProps.footerLogo,
companyData,
bankData,
@@ -71,7 +71,7 @@ export const CombinedQuotePDF = ({
footerLogo={estimationProps.footerLogo}
/>
)}
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
<SimpleLayout {...layoutProps} showPageNumber={false}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>

View File

@@ -50,7 +50,7 @@ export const EstimationPDF = ({
const commonProps = {
state,
date,
icon: headerIcon,
headerIcon,
footerLogo,
companyData,
};
@@ -64,17 +64,17 @@ export const EstimationPDF = ({
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
</PDFPage>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<BriefingModule state={state} />
</SimpleLayout>
{state.sitemap && state.sitemap.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<SitemapModule state={state} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<EstimationModule
state={state}
positions={positions}
@@ -83,11 +83,11 @@ export const EstimationPDF = ({
/>
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>

View File

@@ -8,58 +8,48 @@ const simpleStyles = StyleSheet.create({
industrialPage: {
padding: 30,
paddingTop: 20,
backgroundColor: '#ffffff',
flexDirection: 'column',
backgroundColor: '#FFFFFF',
fontFamily: 'Outfit',
},
industrialNumber: {
fontSize: 60,
fontWeight: 'bold',
color: '#f1f5f9',
position: 'absolute',
top: -10,
right: 0,
zIndex: -1,
},
industrialSection: {
marginTop: 16,
paddingTop: 12,
flexDirection: 'row',
position: 'relative',
contentView: {
flex: 1,
marginTop: 20,
},
});
interface SimpleLayoutProps {
children: React.ReactNode;
pageNumber?: string;
icon?: string;
headerIcon?: string;
footerLogo?: string;
companyData: any;
bankData?: any;
showDetails?: boolean;
showPageNumber?: boolean;
children: React.ReactNode;
}
export const SimpleLayout = ({
children,
pageNumber,
icon,
export const SimpleLayout: React.FC<SimpleLayoutProps> = ({
headerIcon,
footerLogo,
companyData,
bankData,
showPageNumber = true
}: SimpleLayoutProps) => {
showDetails = false,
showPageNumber = true,
children,
}) => {
return (
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
<Header icon={icon} showAddress={false} />
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
<PDFView style={simpleStyles.industrialSection}>
<PDFView style={{ width: '100%' }}>
{children}
</PDFView>
<PDFPage size="A4" style={simpleStyles.industrialPage}>
<Header icon={headerIcon} sender={companyData.name} showAddress={false} />
<PDFView style={simpleStyles.contentView}>
{children}
</PDFView>
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
_bankData={bankData}
showDetails={showDetails}
showPageNumber={showPageNumber}
/>
</PDFPage>

View File

@@ -0,0 +1,53 @@
import { renderToFile, Document as PDFDocument, Font } from "@react-pdf/renderer";
import { createElement } from "react";
import { AgbsPDF } from "./components/AgbsPDF.js";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Standard Font Registration
Font.register({
family: 'Outfit',
fonts: [
{ src: 'Helvetica' },
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
],
});
Font.register({
family: 'Helvetica',
fonts: [
{ src: 'Helvetica' },
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
],
});
async function generate() {
const outDir = path.join(__dirname, "../../../out");
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
const outputPath = path.resolve(outDir, "AGB_Mintel.pdf");
console.log("Generating High-Fidelity AGB PDF...");
const headerIcon = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Icon-White-Transparent.png";
const footerLogo = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Logo-Black-Transparent.png";
// WRAP IN DOCUMENT - MANDATORY FOR standalone rendering
const document = createElement(PDFDocument, {
title: "Allgemeine Geschäftsbedingungen - Marc Mintel",
author: "Marc Mintel",
},
createElement(AgbsPDF, { mode: "full", headerIcon, footerLogo })
);
await renderToFile(document, outputPath);
console.log(`Generated: ${outputPath}`);
}
generate().catch(console.error);

View File

@@ -2,7 +2,7 @@
"name": "people-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/thumbnail-generator",
"version": "1.8.16",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -56,8 +56,8 @@ export class ThumbnailGenerator {
}
}
// Default to the requested nano-banana-pro model unless explicitly provided
const model = options?.model || "google/nano-banana-pro";
// Default to the requested flux-1.1-pro model unless explicitly provided
const model = options?.model || "black-forest-labs/flux-1.1-pro";
const output = await this.replicate.run(model as `${string}/${string}`, {
input: inputPayload,

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/tsconfig",
"version": "1.8.16",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -2,7 +2,7 @@
"name": "unified-dashboard",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.16",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

14
plan_free.hbs Normal file
View File

@@ -0,0 +1,14 @@
<div class="membership-card free">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Free"}}</h2>
<h3 class="membership-card-price"><sup>$</sup>0</h3>
<div class="membership-card-options">
<ul>
<li>Full access to posts for subscribers</li>
<li>Weekly email newsletter</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="{{@site.url}}/signup/" class="global-button">{{t "Subscribe now"}}</a>
</div>

16
plan_monthly.hbs Normal file
View File

@@ -0,0 +1,16 @@
<div class="membership-card monthly">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Monthly"}}</h2>
<h3 class="membership-card-price">{{price monthly_price currency=currency}}</h3>
<div class="membership-card-options">
<ul>
<li>Full access to all premium posts</li>
<li>Weekly email newsletter</li>
<li>Support independent publishing</li>
<li>Simple, secure card payment</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="#" class="global-button" data-members-plan="Monthly">{{t "Subscribe now"}}</a>
</div>

17
plan_yearly.hbs Normal file
View File

@@ -0,0 +1,17 @@
<div class="membership-card yearly">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Yearly"}}</h2>
<h3 class="membership-card-price">{{price yearly_price currency=currency}}</h3>
<div class="membership-card-options">
<ul>
<li>Full access to all premium posts</li>
<li>Weekly email newsletter</li>
<li>Support independent publishing</li>
<li>Simple, secure card payment</li>
<li>One easy payment instead of 12!</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="#" class="global-button" data-members-plan="Yearly">{{t "Subscribe now"}}</a>
</div>

611
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,6 @@ if (fs.existsSync(appsDir)) {
}
// Update .env files
updateEnv(".env");
updateEnv(".env.example");
console.log("✨ All versions synced!");